diff --git a/.changeset/edit-preview-agent-manager.md b/.changeset/edit-preview-agent-manager.md new file mode 100644 index 0000000000..65de1eab4a --- /dev/null +++ b/.changeset/edit-preview-agent-manager.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Open edit, write, and patch changes in the Agent Manager side panel instead of a new editor tab. Clicking a tool or file name expands the change inline, while the open-diff button shows the full change in the Agent Manager panel, or in a diff tab outside Agent Manager. Multi-file patches now open every changed file, each sized to its own diff, and the panel keeps the unified or split view you last selected. diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 8300f6943f..7b41eee9e0 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -2552,8 +2552,6 @@ ToolRegistry.register({ }) }) const canOpenDiff = () => !!data.openDiff && !!path() && !!view() - const canOpenFile = () => !!data.openFile && !!path() - const openDiff = () => { const v = view() if (!canOpenDiff() || !v) return @@ -2565,19 +2563,6 @@ ToolRegistry.register({ }) } - const handleFileClick = (e: MouseEvent) => { - e.stopPropagation() - - if (canOpenDiff()) { - openDiff() - return - } - - if (canOpenFile()) { - data.openFile!(path()) - } - } - const handleOpenDiffClick = (e: MouseEvent) => { e.stopPropagation() openDiff() @@ -2604,7 +2589,6 @@ ToolRegistry.register({ path={props.input.filePath?.includes("/") ? getDirectory(props.input.filePath!) : undefined} changes={props.metadata.filediff} animate={reveal()} - onClick={canOpenDiff() || canOpenFile() ? handleFileClick : undefined} /> )} @@ -2669,8 +2653,6 @@ ToolRegistry.register({ return normalize(diff) }) const canOpenDiff = () => !!data.openDiff && !!props.input.filePath && !!view() - const canOpenFile = () => !!data.openFile && !!props.input.filePath - const openDiff = () => { const v = view() if (!data.openDiff || !props.input.filePath || !v) return @@ -2682,17 +2664,6 @@ ToolRegistry.register({ }) } - const handleFileClick = (e: MouseEvent) => { - e.stopPropagation() - if (canOpenDiff()) { - openDiff() - return - } - if (canOpenFile()) { - data.openFile!(props.input.filePath!) - } - } - const handleOpenDiffClick = (e: MouseEvent) => { e.stopPropagation() openDiff() @@ -2719,7 +2690,6 @@ ToolRegistry.register({ path={props.input.filePath?.includes("/") ? getDirectory(props.input.filePath!) : undefined} changes={props.metadata.filediff} animate={reveal()} - onClick={canOpenDiff() || canOpenFile() ? handleFileClick : undefined} /> )} @@ -2802,13 +2772,55 @@ ToolRegistry.register({ const view = (file: ApplyPatchFile) => { const patch = file.patch ?? file.diff if (!patch) return - return normalize({ + const value = normalize({ file: file.relativePath, patch, additions: file.additions, deletions: file.deletions, }) + // apply_patch can report a file whose payload is not a parsable unified + // diff. Rendering it yields an empty "+0 -0" pane, so treat such a file + // as having no preview instead of showing a blank diff. + if (!value.fileDiff.hunks.length) return + return value } + const openAllDiff = () => { + const diffs = files().flatMap((file) => { + const diff = view(file) + return diff + ? [{ + file: file.relativePath, + patch: diff.patch, + // Patch metadata reports 0 for some added files; fall back to the + // parsed hunks so the header matches the rendered diff. + additions: diff.additions || diff.fileDiff.additionLines.length, + deletions: diff.deletions || diff.fileDiff.deletionLines.length, + }] + : [] + }) + const first = diffs[0] + if (!data.openDiff || !first) return + data.openDiff(diffs.length === 1 ? first : { ...first, files: diffs }) + } + const allDiffAction = () => ( + view(file))}> + + + e.preventDefault()} + onClick={(e) => { + e.stopPropagation() + openAllDiff() + }} + aria-label={i18n.t("ui.messagePart.openInDiffViewer")} + /> + + + + ) const pending = createMemo(() => busy(props.status)) const reveal = useToolReveal(pending, () => props.reveal !== false) const single = createMemo(() => { @@ -2877,14 +2889,6 @@ ToolRegistry.register({ path={file().relativePath.includes("/") ? getDirectory(file().relativePath) : undefined} changes={{ additions: file().additions, deletions: file().deletions }} animate={reveal()} - onClick={ - data.openFile && file().filePath - ? (e: MouseEvent) => { - e.stopPropagation() - data.openFile!(file().filePath) - } - : undefined - } /> )} @@ -2900,6 +2904,7 @@ ToolRegistry.register({ + {allDiffAction()} } > @@ -2941,12 +2946,6 @@ ToolRegistry.register({ { - if (!data.openFile) return - e.stopPropagation() - data.openFile(file.filePath) - }} > {getFilename(file.relativePath)} diff --git a/packages/kilo-vscode/src/DiffVirtualProvider.ts b/packages/kilo-vscode/src/DiffVirtualProvider.ts index 721cb71c3f..6f0661771f 100644 --- a/packages/kilo-vscode/src/DiffVirtualProvider.ts +++ b/packages/kilo-vscode/src/DiffVirtualProvider.ts @@ -9,6 +9,7 @@ export interface DiffVirtualFile { patch?: string additions: number deletions: number + files?: Omit[] initialDiffStyle: "unified" | "split" } @@ -33,8 +34,9 @@ export class DiffVirtualProvider implements vscode.Disposable { public open(diff: DiffVirtualFile): void { this.pending = diff + const count = diff.files?.length ?? 1 const filename = diff.file.split("/").pop() ?? diff.file - const title = `Changes: ${filename}` + const title = count > 1 ? `Changes: ${count} files` : `Changes: ${filename}` if (this.panel) { this.panel.title = title diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index 62769624dc..76556dc0bf 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -15,6 +15,7 @@ import { WorktreeImporter } from "../../src/agent-manager/worktree-importer" const ROOT = path.resolve(import.meta.dir, "../..") const KILO_PROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts") +const EDIT_PREVIEW_PANEL_FILE = path.join(ROOT, "webview-ui/agent-manager/EditPreviewPanel.tsx") const CSS_FILES = [ path.join(ROOT, "webview-ui/agent-manager/agent-manager.css"), path.join(ROOT, "webview-ui/agent-manager/agent-manager-review.css"), @@ -22,6 +23,7 @@ const CSS_FILES = [ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx"), path.join(ROOT, "webview-ui/agent-manager/SubagentPanel.tsx"), + path.join(ROOT, "webview-ui/agent-manager/EditPreviewPanel.tsx"), path.join(ROOT, "webview-ui/agent-manager/UnassignedSessionsSection.tsx"), path.join(ROOT, "webview-ui/agent-manager/NewWorktreeDialog.tsx"), path.join(ROOT, "webview-ui/agent-manager/ProjectSelect.tsx"), @@ -30,6 +32,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/diff-viewer/FullScreenDiffView.tsx"), path.join(ROOT, "webview-ui/diff-viewer/ImageDiffView.tsx"), path.join(ROOT, "webview-ui/diff-viewer/MarkdownDiffView.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/VirtualDiffView.tsx"), path.join(ROOT, "webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx"), path.join(ROOT, "webview-ui/diff-viewer/markdown-comment-ranges.ts"), path.join(ROOT, "webview-ui/diff-viewer/DiffEndMarker.tsx"), @@ -165,6 +168,30 @@ describe("Agent Manager CSS/TSX Consistency", () => { }) }) +describe("Agent Manager edit preview", () => { + it("provides a visible close action", () => { + const source = fs.readFileSync(EDIT_PREVIEW_PANEL_FILE, "utf-8") + expect(source).toContain('icon="close"') + expect(source).toContain('class="am-edit-preview-close"') + expect(source).toContain("onClick={props.state.close}") + }) + + it("drives every stacked file from one shared style control", () => { + const source = fs.readFileSync(EDIT_PREVIEW_PANEL_FILE, "utf-8") + expect(source).toContain("RadioGroup") + expect(source).toContain("styleSelect={false}") + }) + + it("sizes stacked files to their own diff instead of a fixed height", () => { + const css = readAllCss() + expect(css).toContain(".am-edit-preview-files > .am-review-layout") + expect(css).not.toContain("flex: 0 0 min(420px, 50%)") + const view = fs.readFileSync(path.join(ROOT, "webview-ui/diff-viewer/VirtualDiffView.tsx"), "utf-8") + expect(view).toContain("value.fileDiff.hunks.length") + expect(view).toContain("virtualized={heavy()}") + }) +}) + describe("Agent Manager Provider Messages", () => { function getMethodBody(name: string): string { const project = new Project({ compilerOptions: { allowJs: true } }) diff --git a/packages/kilo-vscode/tests/unit/databridge-shape.test.ts b/packages/kilo-vscode/tests/unit/databridge-shape.test.ts index be286d3408..c432b6b9a7 100644 --- a/packages/kilo-vscode/tests/unit/databridge-shape.test.ts +++ b/packages/kilo-vscode/tests/unit/databridge-shape.test.ts @@ -62,15 +62,23 @@ describe("DataBridge shape (perf regression guard)", () => { describe("DataBridge openDiff wiring (regression guard)", () => { const openDiffBlock = () => { - const match = src.match(/const\s+openDiff\s*=\s*\(diff:\s*\{[\s\S]*?\n\s*\}\n\n\s*const\s+openUrl/) + const match = src.match( + /const\s+openDiff\s*=\s*\(diff:\s*PermissionFileDiff\)[\s\S]*?\n\s*\}\n\n\s*const\s+openUrl/, + ) expect(match).toBeTruthy() return match![0] } it("wires openDiff to the openDiffVirtual webview message", () => { expect(openDiffBlock()).toMatch( - /postMessage\(\{\s*type:\s*["']openDiffVirtual["']\s*,\s*diff\s*,\s*initialDiffStyle:\s*["']split["']\s*\}\)/, + /postMessage\(\{\s*type:\s*["']openDiffVirtual["']\s*,\s*diff\s*,\s*initialDiffStyle:\s*diffStyle\?\.style\(\)\s*\?\?\s*["']unified["']\s*\}\)/, ) expect(src).toContain("onOpenDiff={openDiff}") }) + + it("routes Agent Manager diffs to the inspector event", () => { + expect(src).toContain("dispatchAgentManagerEditPreview") + expect(src).toContain('initialDiffStyle: diffStyle?.style() ?? "unified"') + expect(src).toContain("useWorktreeMode") + }) }) diff --git a/packages/kilo-vscode/tests/unit/edit-preview.test.ts b/packages/kilo-vscode/tests/unit/edit-preview.test.ts new file mode 100644 index 0000000000..631b9569bd --- /dev/null +++ b/packages/kilo-vscode/tests/unit/edit-preview.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "bun:test" +import { createRoot } from "solid-js" +import { + createEditPreview, + isEditPreviewDiff, + previewMatchesContext, +} from "../../webview-ui/agent-manager/edit-preview" + +const diff = { + file: "src/example.ts", + patch: "@@ -1 +1 @@\n-old\n+new\n", + additions: 1, + deletions: 1, +} + +describe("Agent Manager edit preview", () => { + it("replaces the current patch and opens the inspector", () => { + createRoot((dispose) => { + const calls = { shown: 0, hidden: 0 } + const preview = createEditPreview({ + show: () => calls.shown++, + hide: () => calls.hidden++, + }) + + preview.open(diff, "session-1", "unified") + expect(preview.preview()).toEqual({ diff, sessionID: "session-1", style: "unified", markdown: false }) + + preview.open({ ...diff, file: "src/next.ts" }, "session-2") + expect(preview.preview()?.diff.file).toBe("src/next.ts") + expect(preview.preview()?.style).toBe("unified") + expect(calls.shown).toBe(2) + + preview.close() + expect(preview.preview()).toBeUndefined() + expect(calls.hidden).toBe(1) + dispose() + }) + }) + + it("updates the display preferences without changing the patch", () => { + createRoot((dispose) => { + const preview = createEditPreview({ show: () => undefined, hide: () => undefined }) + preview.open(diff) + preview.updateStyle("unified") + preview.updateMarkdown(true) + + expect(preview.preview()?.diff).toEqual(diff) + expect(preview.preview()?.style).toBe("unified") + expect(preview.preview()?.markdown).toBe(true) + dispose() + }) + }) + + it("uses the shared style when opening a preview", () => { + createRoot((dispose) => { + const style = () => "split" as const + const changes: string[] = [] + const preview = createEditPreview({ + show: () => undefined, + hide: () => undefined, + style, + onStyleChange: (value) => changes.push(value), + }) + preview.open(diff) + expect(preview.preview()?.style).toBe("split") + preview.updateStyle("unified") + expect(changes).toEqual(["unified"]) + dispose() + }) + }) + + it("validates edit preview payloads", () => { + expect(isEditPreviewDiff(diff)).toBe(true) + expect( + isEditPreviewDiff({ + ...diff, + files: [diff, { ...diff, file: "src/other.ts" }], + }), + ).toBe(true) + expect(isEditPreviewDiff({ ...diff, additions: "1" })).toBe(false) + expect(isEditPreviewDiff({ file: "src/example.ts" })).toBe(false) + expect(isEditPreviewDiff({ ...diff, files: [] })).toBe(false) + }) + + it("keeps a preview scoped to its current session and worktree", () => { + expect(previewMatchesContext("session-1", "session-1", "wt-1", "wt-1")).toBe(true) + expect(previewMatchesContext("session-1", "session-2", "wt-1", "wt-1")).toBe(false) + expect(previewMatchesContext("session-1", "session-1", "wt-2", "wt-1")).toBe(false) + expect(previewMatchesContext("session-1", "session-1", "local", undefined)).toBe(true) + expect(previewMatchesContext("session-1", "session-1", null, undefined)).toBe(true) + expect(previewMatchesContext("session-1", "session-1", "wt-1", undefined)).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts index 8eed5fc7f6..05e8f0c849 100644 --- a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts @@ -182,6 +182,11 @@ describe("Edit tool diff-first click contract (source)", () => { expect(editBlock).toMatch(/props\.input\.oldString\s*\?\?\s*""/) expect(editBlock).toMatch(/props\.input\.newString\s*\?\?\s*""/) }) + + it("edit file names leave the parent trigger responsible for inline expansion", () => { + expect(editBlock).not.toContain("handleFileClick") + expect(editBlock).toContain("handleOpenDiffClick") + }) }) describe("Write and apply_patch patch rendering contracts (source)", () => { @@ -197,12 +202,36 @@ describe("Write and apply_patch patch rendering contracts (source)", () => { expect(writeBlock).toContain('mode="diff"') }) + it("write file names leave the parent trigger responsible for inline expansion", () => { + expect(writeBlock).not.toContain("handleFileClick") + expect(writeBlock).toContain("handleOpenDiffClick") + }) + it("apply_patch tool can render from patch metadata without before/after", () => { expect(patchBlock).toContain("file.patch") expect(patchBlock).toContain("normalize({") expect(patchBlock).toContain("file: file.relativePath") expect(patchBlock).toContain('mode="diff"') }) + + it("apply_patch tool exposes the diff action for each file", () => { + expect(patchBlock).toContain("data.openDiff") + expect(patchBlock).toContain("const allDiffAction = ()") + expect(patchBlock).toContain("{allDiffAction()}") + expect(patchBlock).not.toContain("data.openFile(file.filePath)") + }) + + it("apply_patch skips files whose patch has no parsable hunks", () => { + expect(patchBlock).toContain("value.fileDiff.hunks.length") + expect(patchBlock).toContain("diff.additions || diff.fileDiff.additionLines.length") + expect(patchBlock).toContain("diff.deletions || diff.fileDiff.deletionLines.length") + }) + + it("apply_patch open action preserves every file in a multi-file payload", () => { + expect(patchBlock).toContain("const diffs = files().flatMap") + expect(patchBlock).toContain("files: diffs") + expect(patchBlock).toContain("diffs.length === 1 ? first") + }) }) describe("Bash tool static terminal preview (source)", () => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 955df36d06..a76614f874 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -73,6 +73,7 @@ import { FeedbackProvider } from "../src/context/feedback" import { MemoryProvider } from "../src/context/memory" import { SessionProvider, useSession } from "../src/context/session" import { WorktreeModeProvider } from "../src/context/worktree-mode" +import { DiffStyleProvider, useDiffStyle } from "../src/context/diff-style" import { ProviderShell } from "../src/context/provider-shell" import { ChatView } from "../src/components/chat" import HistoryView from "../src/components/history/HistoryView" @@ -179,7 +180,9 @@ import { SidebarToggleButton } from "./SidebarToggleButton" import { setTabWidths } from "./tab-widths" import { clampPanelWidth, createPanelResize, maxPanelWidth, minPanelWidth, SidePanel } from "./side-panel-layout" import { SubagentPanel } from "./SubagentPanel" -import { createSubagentTabs } from "./subagent-tabs" +import { attachSubagentEvent, createSubagentTabs } from "./subagent-tabs" +import { EditPreviewPanel } from "./EditPreviewPanel" +import { createAgentManagerEditPreview, createEditPreviewContextGuard } from "./edit-preview" import { buildShortcutCategories } from "./shortcuts" import { tracker } from "./telemetry" import { createChatFocus, createFocusBridge, createPromptFocus, forgetTerminalFocus, hasQuestionOption } from "./focus" @@ -189,7 +192,6 @@ import "./agent-manager.css" import "./agent-manager-review.css" import { cycleAgent as cycle } from "../src/context/session-agent" const REVIEW_TAB_ID = "review" - interface SetupState { active: boolean message: string @@ -198,7 +200,6 @@ interface SetupState { worktreeId?: string errorCode?: string } - /** Sidebar selection: LOCAL for local repo, worktree ID for a worktree, or null for an unassigned session. */ type SidebarSelection = typeof LOCAL | string | null export type SidePanelState = SidePanel | null @@ -214,9 +215,7 @@ const AgentManagerContent: Component = () => { const dialog = useDialog() const mode = createModeRouter() let sidebarSearchMenu: SidebarSearchMenuRef | undefined - const [kb, setKb] = createSignal>(defaultBindings) - const [setup, setSetup] = createSignal({ active: false, message: "" }) const worktrees = () => registry.active().worktrees() const setWorktrees = (v: Parameters>[0]) => registry.active().setWorktrees(v) @@ -252,9 +251,7 @@ const AgentManagerContent: Component = () => { ) const isActivePayload = (pid: string | undefined) => projectList().length === 0 || pid === undefined || pid === activeProjectId() - const repoDefaultBranch = () => defaultBaseBranch() ?? repoDetectedBranch() ?? "main" - const DEFAULT_SIDEBAR_WIDTH = 260 const MIN_SIDEBAR_WIDTH = 200 const MAX_SIDEBAR_WIDTH_RATIO = 0.4 @@ -287,11 +284,9 @@ const AgentManagerContent: Component = () => { const toggleSidebar = sidebar.toggle const sections = () => registry.active().sections() const setSections = (v: Parameters>[0]) => registry.active().setSections(v) - // rAF coalescing for resize handlers — at most one signal write per frame let sidebarRaf: number | undefined let pendingSidebarWidth: number | undefined - const [history, setHistory] = createSignal(false) const [sidePanel, setSidePanel] = createSignal(null) const diffOpen = () => sidePanel() === SidePanel.Diff @@ -317,12 +312,16 @@ const AgentManagerContent: Component = () => { setReviewActive(false) setSidePanel(SidePanel.Terminal) } - const [reviewOpenByContext, setReviewOpenByContext] = createSignal>({}) const [reviewCommentsByContext, setReviewCommentsByContext] = createSignal>({}) const reviewComposer = createReviewComposer() const [reviewActive, setReviewActive] = createSignal(false) - const [reviewDiffStyle, setReviewDiffStyle] = createSignal<"unified" | "split">("unified") + const diffStyle = useDiffStyle()! + const setSharedDiffStyle = (style: "unified" | "split") => { + if (diffStyle.style() === style) return + diffStyle.setStyle(style) + vscode.postMessage({ type: "agentManager.setReviewDiffStyle", style }) + } const subagents = createSubagentTabs({ current: session.currentSessionID, sync: (id, parentID) => session.syncSession(id, parentID, "inspector"), @@ -334,16 +333,29 @@ const AgentManagerContent: Component = () => { }, hide: () => setSidePanel(null), }) + const editPreview = createAgentManagerEditPreview( + setHistory, + setReviewActive, + () => setSidePanel(SidePanel.EditPreview), + () => setSidePanel(null), + diffStyle.style, + setSharedDiffStyle, + ) + createEditPreviewContextGuard( + editPreview.preview, + () => session.currentSessionID() ?? undefined, + () => selection() ?? null, + (id: string) => managedSessions().find((item) => item.id === id)?.worktreeId ?? undefined, + editPreview.close, + ) const markdown = createMarkdownRender(vscode) // Per-worktree git stats (diff additions/deletions, commits missing from origin) const worktreeStats = () => registry.active().worktreeStats() - const prStatuses = () => registry.active().prStatuses() const runStatuses = () => registry.active().runStatuses() const setRunStatuses: Setter> = (v) => registry.active().setRunStatuses(v) const runScriptConfigured = () => registry.active().runScriptConfigured() const setRunScriptConfigured = (v: Parameters>[0]) => registry.active().setRunScriptConfigured(v) - // Local repo git stats (branch name, diff additions/deletions, commits) const localStats = () => registry.active().localStats() const projectLive = createProjectLive({ @@ -351,7 +363,6 @@ const AgentManagerContent: Component = () => { active: isActivePayload, branch: (branch) => setRepoBranch(branch), }) - const PENDING_PREFIX = "pending:" const closedDrafts = new Set() const [activePendingId, setActivePendingId] = createSignal() @@ -1113,7 +1124,7 @@ const AgentManagerContent: Component = () => { // server won't connect to send the sessionsLoaded message. if (state.isGitRepo === false && !sessionsLoaded()) setSessionsLoaded(true) if (state.reviewDiffStyle === "split" || state.reviewDiffStyle === "unified") { - setReviewDiffStyle(state.reviewDiffStyle) + diffStyle.setStyle(state.reviewDiffStyle) } markdown.setRender(state.reviewMarkdownRender === true) const current = session.currentSessionID() @@ -1222,17 +1233,8 @@ const AgentManagerContent: Component = () => { if (match) projectNav.jump(parseInt(match[1]!) - 1) } } - const subagent = (event: Event) => { - const detail = (event as CustomEvent<{ sessionID?: unknown; title?: unknown; parentSessionID?: unknown }>).detail - if (typeof detail?.sessionID !== "string") return - subagents.open( - detail.sessionID, - typeof detail.title === "string" ? detail.title : undefined, - typeof detail.parentSessionID === "string" ? detail.parentSessionID : undefined, - ) - } + const detachSubagent = attachSubagentEvent(subagents.open) window.addEventListener("message", handler) - window.addEventListener("agentManager.openSubagent", subagent) // Prevent Cmd/Ctrl shortcuts from triggering native browser actions const preventDefaults = (e: KeyboardEvent) => { if (!(e.metaKey || e.ctrlKey)) return @@ -1281,7 +1283,7 @@ const AgentManagerContent: Component = () => { confirmDeleteWorktree(sel) } window.addEventListener("keydown", deleteKeyHandler) - onCleanup(() => window.removeEventListener("agentManager.openSubagent", subagent)) + onCleanup(detachSubagent) // Reveal the ⌘/Ctrl+1-9 jump badges on all sidebar items while the modifier is held. // Capture phase so the terminal's key handlers can't swallow them; blur resets state @@ -1755,12 +1757,6 @@ const AgentManagerContent: Component = () => { return diffNotices()[key] }) - const setSharedDiffStyle = (style: "unified" | "split") => { - if (reviewDiffStyle() === style) return - setReviewDiffStyle(style) - vscode.postMessage({ type: "agentManager.setReviewDiffStyle", style }) - } - const requestDiffFile = (file: string) => { const id = diffScopeId() if (!id) return @@ -2658,7 +2654,7 @@ const AgentManagerContent: Component = () => { notice={diffNotice()} lead={diffScopeControls(true)} canRevert={scopeCapabilities(review.scope()).revert} - diffStyle={reviewDiffStyle()} + diffStyle={diffStyle.style()} onDiffStyleChange={setSharedDiffStyle} markdownRender={markdown.render()} onMarkdownRenderChange={markdown.update} @@ -2712,6 +2708,9 @@ const AgentManagerContent: Component = () => { onClosePanel={() => setSidePanel(null)} /> + + sidePanel() === SidePanel.EditPreview} /> + { composer={reviewComposer} onSendAll={closeReviewTab} onSendClick={() => metrics.track("send_review_comments", "fullscreen_review")} - diffStyle={reviewDiffStyle()} + diffStyle={diffStyle.style()} onDiffStyleChange={setSharedDiffStyle} markdownRender={markdown.render()} onMarkdownRenderChange={markdown.update} @@ -2788,9 +2787,11 @@ export const AgentManagerApp: Component = () => { - - - + + + + + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/EditPreviewPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/EditPreviewPanel.tsx new file mode 100644 index 0000000000..794cf46721 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/EditPreviewPanel.tsx @@ -0,0 +1,81 @@ +import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { RadioGroup } from "@kilocode/kilo-ui/radio-group" +import { For, Show, type Accessor, type Component } from "solid-js" +import { VirtualDiffView } from "../diff-viewer/VirtualDiffView" +import { useLanguage } from "../src/context/language" +import type { EditPreview } from "./edit-preview" + +interface Props { + state: { + preview: Accessor + updateStyle: (style: "unified" | "split") => void + updateMarkdown: (render: boolean) => void + close: () => void + } + visible: Accessor +} + +export const EditPreviewPanel: Component = (props) => { + const { t } = useLanguage() + + return ( +
+
+
+ + {t("agentManager.editPreview.title")} +
+ + {(preview) => ( + style} + label={(style) => + style === "unified" ? t("ui.sessionReview.diffStyle.unified") : t("ui.sessionReview.diffStyle.split") + } + size="small" + onSelect={(style) => { + if (style) props.state.updateStyle(style) + }} + /> + )} + + +
+ + {(preview) => ( +
+ + {(diff) => ( + + )} + +
+ )} +
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 3e78c77166..8bfc317c81 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -5066,6 +5066,89 @@ body.vscode-high-contrast-light { pointer-events: auto; } +.am-edit-preview-panel { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + opacity: 0; + pointer-events: none; + z-index: 1; + background: var(--surface-base); + will-change: opacity; +} + +.am-edit-preview-panel-visible { + opacity: 1; + pointer-events: auto; +} + +.am-edit-preview-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + height: 32px; + padding: 0 4px 0 8px; + flex-shrink: 0; + border-bottom: 1px solid var(--border-weak-base); + background: var(--surface-base); +} + +.am-edit-preview-heading { + display: flex; + align-items: center; + min-width: 0; + gap: 6px; + color: var(--text-base); + font-size: var(--font-size-small); + font-weight: 600; +} + +.am-edit-preview-close { + flex-shrink: 0; +} + +.am-edit-preview-files { + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; + overflow-y: auto; +} + +/* Each file is as tall as its own diff; the list is the single scroller, so a + one-line change no longer reserves a tall empty pane. */ +.am-edit-preview-files > .am-review-layout { + flex: 0 0 auto; + min-height: 0; + border-bottom: 1px solid var(--border-weak-base); +} + +.am-edit-preview-files > .am-review-layout > .am-review-diff { + flex: 0 0 auto; + overflow: visible; +} + +/* Unbounded or extreme diffs keep Pierre's virtualizer, which needs a capped + viewport of its own. */ +.am-edit-preview-files > .am-review-layout[data-virtualized="true"] { + height: min(70vh, 640px); +} + +.am-edit-preview-files > .am-review-layout[data-virtualized="true"] > .am-review-diff { + flex: 1; + overflow-y: auto; +} + +.am-edit-preview-unavailable { + padding: 16px; + color: var(--text-weak); + font-size: var(--font-size-small); +} + .am-subagent-header { display: flex; align-items: center; diff --git a/packages/kilo-vscode/webview-ui/agent-manager/edit-preview.ts b/packages/kilo-vscode/webview-ui/agent-manager/edit-preview.ts new file mode 100644 index 0000000000..88eca77dd6 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/edit-preview.ts @@ -0,0 +1,133 @@ +import { createEffect, createSignal, on, onCleanup, type Accessor } from "solid-js" +import type { PermissionFileDiff } from "../src/types/messages" +import type { DiffStyle } from "../src/context/diff-style" +import { LOCAL } from "./navigate" + +export interface EditPreview { + diff: PermissionFileDiff + sessionID?: string + style: "unified" | "split" + markdown: boolean +} + +export function previewMatchesContext( + previewSessionID: string | undefined, + currentSessionID: string | null | undefined, + selection: string | null | undefined, + worktreeID: string | undefined, +): boolean { + if (!previewSessionID || previewSessionID !== currentSessionID) return false + if (worktreeID) return worktreeID === selection + return selection === LOCAL || selection === null +} + +export function createEditPreviewContextGuard( + preview: Accessor, + current: Accessor, + selection: Accessor, + owner: (sessionID: string) => string | undefined, + close: () => void, +) { + createEffect( + on( + () => { + const item = preview() + const worktree = item?.sessionID ? owner(item.sessionID) : undefined + return `${item?.sessionID ?? ""}:${current() ?? ""}:${selection() ?? "unassigned"}:${worktree ?? "local"}` + }, + () => { + const item = preview() + if (item && !previewMatchesContext(item.sessionID, current(), selection(), owner(item.sessionID!))) close() + }, + { defer: true }, + ), + ) +} + +interface Options { + show: () => void + hide: () => void + style?: Accessor + onStyleChange?: (style: DiffStyle) => void +} + +export function createEditPreview(opts: Options) { + const [preview, setPreview] = createSignal() + + const open = (diff: PermissionFileDiff, sessionID?: string, style?: DiffStyle) => { + setPreview({ diff, sessionID, style: style ?? opts.style?.() ?? "unified", markdown: false }) + opts.show() + } + + const updateStyle = (style: "unified" | "split") => { + setPreview((current) => (current ? { ...current, style } : current)) + opts.onStyleChange?.(style) + } + + const updateMarkdown = (markdown: boolean) => { + setPreview((current) => (current ? { ...current, markdown } : current)) + } + + const close = () => { + setPreview(undefined) + opts.hide() + } + + return { preview: preview as Accessor, open, updateStyle, updateMarkdown, close } +} + +export function isEditPreviewDiff(value: unknown): value is PermissionFileDiff { + if (!value || typeof value !== "object") return false + const diff = value as Partial + return ( + typeof diff.file === "string" && + typeof diff.additions === "number" && + typeof diff.deletions === "number" && + (diff.patch === undefined || typeof diff.patch === "string") && + (diff.files === undefined || + (Array.isArray(diff.files) && diff.files.length > 0 && diff.files.every((file) => isEditPreviewDiff(file)))) + ) +} + +export function handleEditPreviewEvent( + event: Event, + open: (diff: PermissionFileDiff, sessionID?: string, style?: "unified" | "split") => void, +): void { + const detail = (event as CustomEvent<{ diff?: unknown; sessionID?: unknown; initialDiffStyle?: unknown }>).detail + if (!isEditPreviewDiff(detail?.diff)) return + open( + detail.diff, + typeof detail.sessionID === "string" ? detail.sessionID : undefined, + detail.initialDiffStyle === "split" ? "split" : "unified", + ) +} + +export function attachEditPreviewEvent( + open: (diff: PermissionFileDiff, sessionID?: string, style?: "unified" | "split") => void, +): () => void { + const handler = (event: Event) => handleEditPreviewEvent(event, open) + window.addEventListener("agentManager.openEditPreview", handler) + return () => window.removeEventListener("agentManager.openEditPreview", handler) +} + +export function createAgentManagerEditPreview( + history: (value: boolean) => void, + review: (value: boolean) => void, + show: () => void, + hide: () => void, + style?: Accessor, + onStyleChange?: (style: DiffStyle) => void, +) { + const state = createEditPreview({ + show: () => { + history(false) + review(false) + show() + }, + hide, + style, + onStyleChange, + }) + onCleanup(attachEditPreviewEvent(state.open)) + return state +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 46e5a8f093..b41cd49c9e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -148,6 +148,9 @@ export const dict = { "agentManager.diff.revertFile": "استعادة الملف", "agentManager.diff.revertSuccess": "تم استعادة الملف", "agentManager.diff.revertError": "فشل الاستعادة", + "agentManager.editPreview.title": "معاينة التعديل", + "agentManager.editPreview.close": "إغلاق معاينة التعديل", + "agentManager.editPreview.openInPanel": "عرض التغييرات في اللوحة", "agentManager.diff.applyBranchOnly": "لا يعمل تطبيق التغييرات إلا على فرق الفرع الكامل. انتقل إلى نطاق Branch لتطبيقها.", "agentManager.open.button": "فتح", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index c9266c9ba5..f3fc299700 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -151,6 +151,9 @@ export const dict = { "agentManager.diff.revertFile": "Reverter arquivo", "agentManager.diff.revertSuccess": "Arquivo revertido", "agentManager.diff.revertError": "Falha ao reverter", + "agentManager.editPreview.title": "Pré-visualização da edição", + "agentManager.editPreview.close": "Fechar pré-visualização da edição", + "agentManager.editPreview.openInPanel": "Ver alterações no painel", "agentManager.diff.applyBranchOnly": "Aplicar funciona apenas no diff completo da branch. Mude para o escopo Branch para aplicar.", "agentManager.open.button": "Abrir", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index a8bc759c30..8be268bcd2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -151,6 +151,9 @@ export const dict = { "agentManager.diff.revertFile": "Vrati datoteku", "agentManager.diff.revertSuccess": "Datoteka vraćena", "agentManager.diff.revertError": "Vraćanje neuspješno", + "agentManager.editPreview.title": "Pregled uređivanja", + "agentManager.editPreview.close": "Zatvori pregled uređivanja", + "agentManager.editPreview.openInPanel": "Prikaži izmjene u panelu", "agentManager.diff.applyBranchOnly": "Primijeni radi samo s kompletnim diffom grane. Prebacite se na opseg Branch da biste primijenili.", "agentManager.open.button": "Otvori", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index b71e435d58..15f7bbb6b8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -152,6 +152,9 @@ export const dict = { "agentManager.diff.revertFile": "Gendan fil", "agentManager.diff.revertSuccess": "Fil gendannet", "agentManager.diff.revertError": "Gendannelse fejlede", + "agentManager.editPreview.title": "Forhåndsvisning af redigering", + "agentManager.editPreview.close": "Luk forhåndsvisning af redigering", + "agentManager.editPreview.openInPanel": "Vis ændringer i panelet", "agentManager.diff.applyBranchOnly": "Anvend virker kun på hele Branch-diffen. Skift til Branch-området for at anvende.", "agentManager.open.button": "Åbn", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 293f2460ca..a5223fdba2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -153,6 +153,9 @@ export const dict = { "agentManager.diff.revertFile": "Datei zurücksetzen", "agentManager.diff.revertSuccess": "Datei zurückgesetzt", "agentManager.diff.revertError": "Zurücksetzen fehlgeschlagen", + "agentManager.editPreview.title": "Bearbeitungsvorschau", + "agentManager.editPreview.close": "Bearbeitungsvorschau schließen", + "agentManager.editPreview.openInPanel": "Änderungen im Panel anzeigen", "agentManager.diff.applyBranchOnly": "Anwenden funktioniert nur für den vollständigen Branch-Diff. Wechsle zum Bereich Branch, um anzuwenden.", "agentManager.open.button": "Öffnen", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 87ed6c4dc4..fde7c6acbf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -155,6 +155,9 @@ export const dict = { "agentManager.diff.revertFile": "Revert file", "agentManager.diff.revertSuccess": "File reverted", "agentManager.diff.revertError": "Revert failed", + "agentManager.editPreview.title": "Edit preview", + "agentManager.editPreview.close": "Close edit preview", + "agentManager.editPreview.openInPanel": "View changes in panel", "agentManager.diff.applyBranchOnly": "Apply works on the full branch diff. Switch to the Branch scope to apply.", "agentManager.open.button": "Open", "agentManager.open.tooltip": "Open this worktree in VS Code", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index 974b421691..eb4df11d81 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -152,6 +152,9 @@ export const dict = { "agentManager.diff.revertFile": "Revertir archivo", "agentManager.diff.revertSuccess": "Archivo revertido", "agentManager.diff.revertError": "Error al revertir", + "agentManager.editPreview.title": "Vista previa de la edición", + "agentManager.editPreview.close": "Cerrar vista previa de la edición", + "agentManager.editPreview.openInPanel": "Ver cambios en el panel", "agentManager.diff.applyBranchOnly": "Aplicar solo funciona con el diff completo de la rama. Cambia al ámbito Branch para aplicar.", "agentManager.open.button": "Abrir", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts index 3941b76e1c..5564634077 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -155,6 +155,9 @@ export const dict = { "agentManager.diff.revertFile": "بازگردانی فایل", "agentManager.diff.revertSuccess": "فایل بازگردانی شد", "agentManager.diff.revertError": "بازگردانی ناموفق بود", + "agentManager.editPreview.title": "پیش‌نمایش ویرایش", + "agentManager.editPreview.close": "بستن پیش‌نمایش ویرایش", + "agentManager.editPreview.openInPanel": "نمایش تغییرات در پنل", "agentManager.diff.applyBranchOnly": "اعمال تغییرات روی اختلاف کامل شاخه انجام می‌شود. برای اعمال، به محدوده Branch بروید.", "agentManager.open.button": "باز کردن", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 39270201ee..1f6f3ae21f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -152,6 +152,9 @@ export const dict = { "agentManager.diff.revertFile": "Rétablir le fichier", "agentManager.diff.revertSuccess": "Fichier rétabli", "agentManager.diff.revertError": "Échec du rétablissement", + "agentManager.editPreview.title": "Aperçu de la modification", + "agentManager.editPreview.close": "Fermer l'aperçu de la modification", + "agentManager.editPreview.openInPanel": "Afficher les modifications dans le panneau", "agentManager.diff.applyBranchOnly": "Appliquer ne fonctionne que sur le diff complet de la branche. Passez à la portée Branch pour appliquer.", "agentManager.open.button": "Ouvrir", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 5ea268f2ac..c5317417da 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -158,6 +158,9 @@ export const dict = { "agentManager.diff.revertFile": "Ripristina file", "agentManager.diff.revertSuccess": "File ripristinato", "agentManager.diff.revertError": "Ripristino non riuscito", + "agentManager.editPreview.title": "Anteprima modifica", + "agentManager.editPreview.close": "Chiudi anteprima modifica", + "agentManager.editPreview.openInPanel": "Visualizza modifiche nel pannello", "agentManager.diff.applyBranchOnly": "Applica funziona solo sul diff completo del branch. Passa all'ambito Branch per applicare.", "agentManager.open.button": "Apri", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index 8b9ed40d07..4bc129feaa 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -151,6 +151,9 @@ export const dict = { "agentManager.diff.revertFile": "ファイルを元に戻す", "agentManager.diff.revertSuccess": "ファイルを元に戻しました", "agentManager.diff.revertError": "元に戻せませんでした", + "agentManager.editPreview.title": "編集プレビュー", + "agentManager.editPreview.close": "編集プレビューを閉じる", + "agentManager.editPreview.openInPanel": "変更をパネルで表示", "agentManager.diff.applyBranchOnly": "適用はブランチ全体の差分に対してのみ利用できます。適用するにはスコープを Branch に切り替えてください。", "agentManager.open.button": "開く", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index 748f2b74ae..333e549912 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -149,6 +149,9 @@ export const dict = { "agentManager.diff.revertFile": "파일 되돌리기", "agentManager.diff.revertSuccess": "파일이 되돌려졌습니다", "agentManager.diff.revertError": "되돌리기 실패", + "agentManager.editPreview.title": "편집 미리보기", + "agentManager.editPreview.close": "편집 미리보기 닫기", + "agentManager.editPreview.openInPanel": "패널에서 변경 사항 보기", "agentManager.diff.applyBranchOnly": "적용은 전체 브랜치 diff에서만 작동합니다. 적용하려면 범위를 Branch로 전환하세요.", "agentManager.open.button": "열기", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 39fb0b2a0e..ec334a4665 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -157,6 +157,9 @@ export const dict = { "agentManager.diff.revertFile": "Bestand terugzetten", "agentManager.diff.revertSuccess": "Bestand teruggezet", "agentManager.diff.revertError": "Terugzetten mislukt", + "agentManager.editPreview.title": "Voorbeeld van bewerking", + "agentManager.editPreview.close": "Voorbeeld van bewerking sluiten", + "agentManager.editPreview.openInPanel": "Wijzigingen in paneel bekijken", "agentManager.diff.applyBranchOnly": "Toepassen werkt alleen op de volledige branch-diff. Schakel naar het bereik Branch om toe te passen.", "agentManager.open.button": "Openen", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 72296af152..2491452046 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -150,6 +150,9 @@ export const dict = { "agentManager.diff.revertFile": "Tilbakestill fil", "agentManager.diff.revertSuccess": "Fil tilbakestilt", "agentManager.diff.revertError": "Tilbakestilling feilet", + "agentManager.editPreview.title": "Forhåndsvisning av redigering", + "agentManager.editPreview.close": "Lukk forhåndsvisning av redigering", + "agentManager.editPreview.openInPanel": "Vis endringer i panelet", "agentManager.diff.applyBranchOnly": "Bruk fungerer kun på hele Branch-diffen. Bytt til Branch-omfanget for å bruke.", "agentManager.open.button": "Åpne", "agentManager.open.tooltip": "Åpne dette Worktree-et i VS Code", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index f01f867fa5..463035a53f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -151,6 +151,9 @@ export const dict = { "agentManager.diff.revertFile": "Cofnij plik", "agentManager.diff.revertSuccess": "Plik cofnięty", "agentManager.diff.revertError": "Cofanie nie powiodło się", + "agentManager.editPreview.title": "Podgląd edycji", + "agentManager.editPreview.close": "Zamknij podgląd edycji", + "agentManager.editPreview.openInPanel": "Wyświetl zmiany w panelu", "agentManager.diff.applyBranchOnly": "Funkcja Zastosuj działa tylko z pełnym diffem brancha. Przełącz się na zakres Branch, aby zastosować.", "agentManager.open.button": "Otwórz", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index d4fb0b5189..bd56890caf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -152,6 +152,9 @@ export const dict = { "agentManager.diff.revertFile": "Откатить файл", "agentManager.diff.revertSuccess": "Файл откатан", "agentManager.diff.revertError": "Ошибка отката", + "agentManager.editPreview.title": "Предпросмотр редактирования", + "agentManager.editPreview.close": "Закрыть предпросмотр редактирования", + "agentManager.editPreview.openInPanel": "Показать изменения на панели", "agentManager.diff.applyBranchOnly": "Применение работает только с полным diff ветки. Чтобы применить изменения, переключитесь на область Branch.", "agentManager.open.button": "Открыть", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index bde66f8ed9..ff41b2fad3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -146,6 +146,9 @@ export const dict = { "agentManager.diff.revertFile": "ย้อนกลับไฟล์", "agentManager.diff.revertSuccess": "ย้อนกลับไฟล์แล้ว", "agentManager.diff.revertError": "ย้อนกลับล้มเหลว", + "agentManager.editPreview.title": "ตัวอย่างการแก้ไข", + "agentManager.editPreview.close": "ปิดตัวอย่างการแก้ไข", + "agentManager.editPreview.openInPanel": "ดูการเปลี่ยนแปลงในแผง", "agentManager.diff.applyBranchOnly": "นำไปใช้ได้เฉพาะกับ diff ของ Branch ทั้งหมดเท่านั้น สลับไปที่ขอบเขต Branch เพื่อใช้งาน", "agentManager.open.button": "เปิด", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index 93a9e7eba9..e89abe9651 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -158,6 +158,9 @@ export const dict = { "agentManager.diff.revertFile": "Dosyayı geri al", "agentManager.diff.revertSuccess": "Dosya geri alındı", "agentManager.diff.revertError": "Geri alma başarısız", + "agentManager.editPreview.title": "Düzenleme önizlemesi", + "agentManager.editPreview.close": "Düzenleme önizlemesini kapat", + "agentManager.editPreview.openInPanel": "Değişiklikleri panelde görüntüle", "agentManager.diff.applyBranchOnly": "Uygula yalnızca tam Branch diff'inde çalışır. Uygulamak için Branch kapsamına geçin.", "agentManager.open.button": "Aç", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 510756af1b..8657fb87f3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -159,6 +159,9 @@ export const dict = { "agentManager.diff.revertFile": "Скасувати зміни файлу", "agentManager.diff.revertSuccess": "Файл відновлено", "agentManager.diff.revertError": "Не вдалося відновити", + "agentManager.editPreview.title": "Попередній перегляд редагування", + "agentManager.editPreview.close": "Закрити попередній перегляд редагування", + "agentManager.editPreview.openInPanel": "Переглянути зміни на панелі", "agentManager.diff.applyBranchOnly": "Застосування працює лише з повним diff гілки. Щоб застосувати зміни, перемкніться на область Branch.", "agentManager.open.button": "Відкрити", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 9a57b4022f..2f11ea967c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -145,6 +145,9 @@ export const dict = { "agentManager.diff.revertFile": "还原文件", "agentManager.diff.revertSuccess": "文件已还原", "agentManager.diff.revertError": "还原失败", + "agentManager.editPreview.title": "编辑预览", + "agentManager.editPreview.close": "关闭编辑预览", + "agentManager.editPreview.openInPanel": "在面板中查看更改", "agentManager.diff.applyBranchOnly": "应用仅适用于完整的分支差异。请切换到 Branch 范围后再应用。", "agentManager.open.button": "打开", "agentManager.open.tooltip": "在 VS Code 中打开此 Worktree", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 173b6c9e93..f511fd1580 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -145,6 +145,9 @@ export const dict = { "agentManager.diff.revertFile": "還原檔案", "agentManager.diff.revertSuccess": "檔案已還原", "agentManager.diff.revertError": "還原失敗", + "agentManager.editPreview.title": "編輯預覽", + "agentManager.editPreview.close": "關閉編輯預覽", + "agentManager.editPreview.openInPanel": "在面板中檢視變更", "agentManager.diff.applyBranchOnly": "套用僅適用於完整的分支差異。請切換至 Branch 範圍後再套用。", "agentManager.open.button": "開啟", "agentManager.open.tooltip": "在 VS Code 中開啟此 Worktree", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts b/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts index d89341e31f..8f7bde5010 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts @@ -10,6 +10,7 @@ export enum SidePanel { PR = "pr", Terminal = "terminal", Subagents = "subagents", + EditPreview = "edit-preview", } function viewportWidth(viewport: number): number { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/subagent-tabs.ts b/packages/kilo-vscode/webview-ui/agent-manager/subagent-tabs.ts index 4ab84f8f20..6b6aaa786e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/subagent-tabs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/subagent-tabs.ts @@ -88,3 +88,17 @@ export function createSubagentTabs(opts: Options) { return { tabs, active, open, select, close, closeOthers, reorder } } + +export function attachSubagentEvent(open: (id: string, title?: string, parentID?: string) => void): () => void { + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ sessionID?: unknown; title?: unknown; parentSessionID?: unknown }>).detail + if (typeof detail?.sessionID !== "string") return + open( + detail.sessionID, + typeof detail.title === "string" ? detail.title : undefined, + typeof detail.parentSessionID === "string" ? detail.parentSessionID : undefined, + ) + } + window.addEventListener("agentManager.openSubagent", handler) + return () => window.removeEventListener("agentManager.openSubagent", handler) +} diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/VirtualDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/VirtualDiffView.tsx new file mode 100644 index 0000000000..e5722e9e52 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/VirtualDiffView.tsx @@ -0,0 +1,150 @@ +import { createEffect, createMemo, on, onCleanup, Show, type Component } from "solid-js" +import { Diff } from "@kilocode/kilo-ui/diff" +import { FileIcon } from "@kilocode/kilo-ui/file-icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { RadioGroup } from "@kilocode/kilo-ui/radio-group" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { normalize } from "@kilocode/kilo-ui/session-diff" +import { useLanguage } from "../src/context/language" +import { EXTREME_DIFF_CHANGED_LINES } from "./diff-open-policy" +import { isMarkdownFile, MarkdownDiffView } from "./MarkdownDiffView" + +export interface VirtualDiffFile { + file: string + patch?: string + additions: number + deletions: number + files?: VirtualDiffFile[] +} + +export interface VirtualDiffViewProps { + diff: VirtualDiffFile + diffStyle: "unified" | "split" + onDiffStyleChange: (style: "unified" | "split") => void + markdownRender: boolean + onMarkdownRenderChange: (render: boolean) => void + /** Hidden when a shared control already drives every stacked file. */ + styleSelect?: boolean +} + +export const VirtualDiffView: Component = (props) => { + const { t } = useLanguage() + let scroller: HTMLDivElement | undefined + + createEffect( + on( + () => props.diff, + () => { + if (scroller) scroller.scrollTop = 0 + }, + { defer: true }, + ), + ) + + onCleanup(() => { + scroller = undefined + }) + + const filename = () => { + const file = props.diff.file + return file.includes("/") ? (file.split("/").pop() ?? file) : file + } + + const directory = () => { + const file = props.diff.file + if (!file.includes("/")) return null + return file.split("/").slice(0, -1).join("/") + } + + // A patch that Pierre cannot turn into hunks has nothing to show; the caller + // renders the unavailable state instead of an empty diff container. + const view = createMemo(() => { + if (!props.diff.patch) return + const value = normalize(props.diff) + if (!value.fileDiff.hunks.length) return + return value + }) + + // Provided counts win, but added files often report 0 while the patch has + // real lines, so the parsed hunks are the fallback. + const counts = createMemo(() => { + const value = view() + if (!value) return { additions: props.diff.additions, deletions: props.diff.deletions } + return { + additions: props.diff.additions || value.fileDiff.additionLines.length, + deletions: props.diff.deletions || value.fileDiff.deletionLines.length, + } + }) + + // Hunk-bounded patches render fully and let the surrounding list scroll, so a + // small change no longer reserves a tall pane. Only unbounded or extreme + // diffs keep Pierre's own virtualizer, which needs a capped scroll box. + const heavy = createMemo( + () => !props.diff.patch || counts().additions + counts().deletions > EXTREME_DIFF_CHANGED_LINES, + ) + + return ( +
+
+
+ + style} + label={(style) => + style === "unified" ? t("ui.sessionReview.diffStyle.unified") : t("ui.sessionReview.diffStyle.split") + } + size="small" + onSelect={(style) => { + if (style) props.onDiffStyleChange(style) + }} + /> + + + + + {`\u2066${directory()}/\u2069`} + + {filename()} + +{counts().additions} + -{counts().deletions} + +
+ + + props.onMarkdownRenderChange(!props.markdownRender)} + /> + + +
+
(scroller = el)}> + Diff preview unavailable for this file.
} + > + {(current) => ( + + } + > + + + )} + +
+ + ) +} diff --git a/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx b/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx index 33661a4f60..bacfb38bee 100644 --- a/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx @@ -1,4 +1,4 @@ -import { createMemo, createSignal, onCleanup, Show, createEffect, on } from "solid-js" +import { createMemo, createSignal, For, onCleanup, Show } from "solid-js" import type { Component } from "solid-js" import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code" import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff" @@ -7,49 +7,29 @@ import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" import { Code } from "@kilocode/kilo-ui/code" import { Diff } from "@kilocode/kilo-ui/diff" import { File } from "@kilocode/kilo-ui/file" -import { FileIcon } from "@kilocode/kilo-ui/file-icon" -import { IconButton } from "@kilocode/kilo-ui/icon-button" -import { RadioGroup } from "@kilocode/kilo-ui/radio-group" import { ThemeProvider } from "@kilocode/kilo-ui/theme" -import { Tooltip } from "@kilocode/kilo-ui/tooltip" -import { normalize } from "@kilocode/kilo-ui/session-diff" +import { RadioGroup } from "@kilocode/kilo-ui/radio-group" import { LanguageProvider, useLanguage } from "../src/context/language" import { ServerProvider, useServer } from "../src/context/server" import { getVSCodeAPI, VSCodeProvider } from "../src/context/vscode" -import { isMarkdownFile, MarkdownDiffView } from "../diff-viewer/MarkdownDiffView" +import { VirtualDiffView, type VirtualDiffFile } from "../diff-viewer/VirtualDiffView" type DiffStyle = "unified" | "split" -interface DiffVirtualFile { - file: string - patch?: string - additions: number - deletions: number -} - const DiffVirtualContent: Component = () => { const { t } = useLanguage() - const [diff, setDiff] = createSignal(null) + const [diff, setDiff] = createSignal(null) const [style, setStyle] = createSignal("unified") const [markdown, setMarkdown] = createSignal(false) - let scrollerRef: HTMLDivElement | undefined - - createEffect( - on( - diff, - () => { - if (scrollerRef) { - scrollerRef.scrollTop = 0 - } - }, - { defer: true }, - ), - ) + const files = createMemo(() => { + const current = diff() + return current?.files?.length ? current.files : current ? [current] : [] + }) const handler = (event: MessageEvent) => { const msg = event.data as { type: string - diff?: DiffVirtualFile + diff?: VirtualDiffFile initialDiffStyle?: DiffStyle markdownRender?: boolean } @@ -63,84 +43,46 @@ const DiffVirtualContent: Component = () => { window.addEventListener("message", handler) onCleanup(() => window.removeEventListener("message", handler)) - const filename = () => { - const f = diff()?.file ?? "" - return f.includes("/") ? (f.split("/").pop() ?? f) : f - } - - const directory = () => { - const f = diff()?.file ?? "" - if (!f.includes("/")) return null - return f.split("/").slice(0, -1).join("/") - } - - const view = createMemo(() => { - const d = diff() - if (!d?.patch) return - return normalize(d) - }) - return ( -
- - {(d) => ( - <> -
-
- s} - label={(s) => - s === "unified" ? t("ui.sessionReview.diffStyle.unified") : t("ui.sessionReview.diffStyle.split") - } - onSelect={(s) => { - if (s) setStyle(s) - }} - /> - - - - {`\u2066${directory()}/\u2069`} - - {filename()} - +{d().additions} - -{d().deletions} - -
- - - { - const next = !markdown() - setMarkdown(next) - getVSCodeAPI().postMessage({ type: "diffVirtual.setMarkdownRender", render: next }) - }} - /> - - -
-
(scrollerRef = el)}> - - {(v) => ( - } - > - - - )} - -
- - )} -
-
+ 0}> +
+
+
+ value} + label={(value) => + value === "unified" ? t("ui.sessionReview.diffStyle.unified") : t("ui.sessionReview.diffStyle.split") + } + size="small" + onSelect={(value) => { + if (value) { + setStyle(value) + } + }} + /> +
+
+
+ + {(current) => ( + { + setMarkdown(render) + getVSCodeAPI().postMessage({ type: "diffVirtual.setMarkdownRender", render }) + }} + styleSelect={false} + /> + )} + +
+
+
) } diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index a0162ce04e..b8676417f7 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -14,6 +14,10 @@ import { SidebarEmptyState } from "./components/chat/SidebarEmptyState" import { SidebarTopBar } from "./components/chat/SidebarTopBar" import { registerExpandedTaskTool } from "./components/chat/TaskToolExpanded" import { registerVscodeToolOverrides } from "./components/chat/VscodeToolOverrides" +import { useWorktreeMode } from "./context/worktree-mode" +import { useDiffStyle } from "./context/diff-style" +import { dispatchAgentManagerEditPreview } from "./utils/agent-manager-events" +import type { PermissionFileDiff } from "./types/messages" // Override the upstream "task" tool renderer with the fully-expanded version // that shows child session parts inline in the VS Code sidebar. @@ -51,6 +55,8 @@ export const DataBridge: Component<{ children: any }> = (props) => { const vscode = useVSCode() const prov = useProvider() const server = useServer() + const worktree = useWorktreeMode() + const diffStyle = useDiffStyle() // Memos for fields that change infrequently (not per-token) — cheap and // avoids allocating a fresh array/object on every consumer read. @@ -124,8 +130,16 @@ export const DataBridge: Component<{ children: any }> = (props) => { vscode.postMessage({ type: "openFile", filePath, line, column, sessionID }) } - const openDiff = (diff: { file: string; patch?: string; additions: number; deletions: number }) => { - vscode.postMessage({ type: "openDiffVirtual", diff, initialDiffStyle: "split" }) + const openDiff = (diff: PermissionFileDiff) => { + if (worktree) { + dispatchAgentManagerEditPreview({ + diff, + sessionID: session.currentSessionID(), + initialDiffStyle: diffStyle?.style() ?? "unified", + }) + return + } + vscode.postMessage({ type: "openDiffVirtual", diff, initialDiffStyle: diffStyle?.style() ?? "unified" }) } const openUrl = (url: string) => { diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx index 3136bdf086..b6393c0925 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx @@ -6,6 +6,11 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { normalize } from "@kilocode/kilo-ui/session-diff" import type { PermissionFileDiff } from "../../types/messages" import { useVSCode } from "../../context/vscode" +import { useSession } from "../../context/session" +import { useWorktreeMode } from "../../context/worktree-mode" +import { useDiffStyle } from "../../context/diff-style" +import { dispatchAgentManagerEditPreview } from "../../utils/agent-manager-events" +import { useLanguage } from "../../context/language" interface PermissionDiffProps { filediff: PermissionFileDiff @@ -13,6 +18,10 @@ interface PermissionDiffProps { export const PermissionDiff: Component = (props) => { const vscode = useVSCode() + const session = useSession() + const worktree = useWorktreeMode() + const diffStyle = useDiffStyle() + const { t } = useLanguage() const filename = createMemo(() => { const parts = props.filediff.file.split("/") return parts[parts.length - 1] ?? props.filediff.file @@ -31,10 +40,18 @@ export const PermissionDiff: Component = (props) => { }) const openInTab = () => { + if (worktree) { + dispatchAgentManagerEditPreview({ + diff: props.filediff, + sessionID: session.currentSessionID(), + initialDiffStyle: diffStyle?.style() ?? "unified", + }) + return + } vscode.postMessage({ type: "openDiffVirtual", diff: props.filediff, - initialDiffStyle: "unified", + initialDiffStyle: diffStyle?.style() ?? "unified", }) } @@ -66,8 +83,13 @@ export const PermissionDiff: Component = (props) => {
- - + +
diff --git a/packages/kilo-vscode/webview-ui/src/context/diff-style.tsx b/packages/kilo-vscode/webview-ui/src/context/diff-style.tsx new file mode 100644 index 0000000000..64b6427248 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/diff-style.tsx @@ -0,0 +1,19 @@ +import { createContext, createSignal, useContext, type Accessor, type ParentComponent } from "solid-js" + +export type DiffStyle = "unified" | "split" + +interface DiffStyleContextValue { + style: Accessor + setStyle: (style: DiffStyle) => void +} + +const DiffStyleContext = createContext() + +export const DiffStyleProvider: ParentComponent = (props) => { + const [style, setStyle] = createSignal("unified") + return {props.children} +} + +export function useDiffStyle(): DiffStyleContextValue | undefined { + return useContext(DiffStyleContext) +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/permissions.ts b/packages/kilo-vscode/webview-ui/src/types/messages/permissions.ts index c56b9bd795..d00118e1f6 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/permissions.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/permissions.ts @@ -18,6 +18,7 @@ export interface PermissionFileDiff { patch?: string additions: number deletions: number + files?: PermissionFileDiff[] } export interface PermissionPatchFile { diff --git a/packages/kilo-vscode/webview-ui/src/utils/agent-manager-events.ts b/packages/kilo-vscode/webview-ui/src/utils/agent-manager-events.ts new file mode 100644 index 0000000000..ad3058923b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/agent-manager-events.ts @@ -0,0 +1,11 @@ +import type { PermissionFileDiff } from "../types/messages" + +export interface AgentManagerEditPreviewDetail { + diff: PermissionFileDiff + sessionID?: string + initialDiffStyle: "unified" | "split" +} + +export function dispatchAgentManagerEditPreview(detail: AgentManagerEditPreviewDetail): void { + window.dispatchEvent(new CustomEvent("agentManager.openEditPreview", { detail })) +} diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index b25786815d..1013034556 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -55,6 +55,16 @@ export type OpenDiffFn = (diff: { patch?: string // kilocode_change additions: number deletions: number + // kilocode_change start - multi-file patch preview payload + files?: Array<{ + file: string + before?: string + after?: string + patch?: string + additions: number + deletions: number + }> + // kilocode_change end }) => void export type OpenUrlFn = (url: string) => void