mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
feat(agent-manager): preview edits in side panel
This commit is contained in:
@@ -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.
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
@@ -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 = () => (
|
||||
<Show when={data.openDiff && files().some((file) => view(file))}>
|
||||
<span data-slot="tool-trigger-actions">
|
||||
<Tooltip value={i18n.t("ui.messagePart.openInDiffViewer")} placement="top" gutter={4}>
|
||||
<IconButton
|
||||
icon="square-arrow-top-right"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
openAllDiff()
|
||||
}}
|
||||
aria-label={i18n.t("ui.messagePart.openInDiffViewer")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Show>
|
||||
)
|
||||
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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
@@ -2900,6 +2904,7 @@ ToolRegistry.register({
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
{allDiffAction()}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -2941,12 +2946,6 @@ ToolRegistry.register({
|
||||
|
||||
<span
|
||||
data-slot="apply-patch-filename"
|
||||
classList={{ clickable: !!data.openFile }}
|
||||
onClick={(e: MouseEvent) => {
|
||||
if (!data.openFile) return
|
||||
e.stopPropagation()
|
||||
data.openFile(file.filePath)
|
||||
}}
|
||||
>
|
||||
{getFilename(file.relativePath)}
|
||||
</span>
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface DiffVirtualFile {
|
||||
patch?: string
|
||||
additions: number
|
||||
deletions: number
|
||||
files?: Omit<DiffVirtualFile, "files" | "initialDiffStyle">[]
|
||||
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
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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<Record<string, string>>(defaultBindings)
|
||||
|
||||
const [setup, setSetup] = createSignal<SetupState>({ active: false, message: "" })
|
||||
const worktrees = () => registry.active().worktrees()
|
||||
const setWorktrees = (v: Parameters<Setter<WorktreeState[]>>[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<Setter<SectionState[]>>[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<SidePanelState>(null)
|
||||
const diffOpen = () => sidePanel() === SidePanel.Diff
|
||||
@@ -317,12 +312,16 @@ const AgentManagerContent: Component = () => {
|
||||
setReviewActive(false)
|
||||
setSidePanel(SidePanel.Terminal)
|
||||
}
|
||||
|
||||
const [reviewOpenByContext, setReviewOpenByContext] = createSignal<Record<string, boolean>>({})
|
||||
const [reviewCommentsByContext, setReviewCommentsByContext] = createSignal<Record<string, ReviewComment[]>>({})
|
||||
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<Record<string, RunStatus>> = (v) => registry.active().setRunStatuses(v)
|
||||
const runScriptConfigured = () => registry.active().runScriptConfigured()
|
||||
const setRunScriptConfigured = (v: Parameters<Setter<boolean>>[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<string>()
|
||||
const [activePendingId, setActivePendingId] = createSignal<string | undefined>()
|
||||
@@ -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)}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={editPreview.preview()}>
|
||||
<EditPreviewPanel state={editPreview} visible={() => sidePanel() === SidePanel.EditPreview} />
|
||||
</Show>
|
||||
<SideTerminalPanel
|
||||
state={terms}
|
||||
contextKey={terms.sideKey}
|
||||
@@ -2760,7 +2759,7 @@ const AgentManagerContent: Component = () => {
|
||||
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 = () => {
|
||||
<ProviderShell.Session>
|
||||
<ProviderShell.Chat>
|
||||
<WorktreeModeProvider>
|
||||
<DataBridge>
|
||||
<AgentManagerContent />
|
||||
</DataBridge>
|
||||
<DiffStyleProvider>
|
||||
<DataBridge>
|
||||
<AgentManagerContent />
|
||||
</DataBridge>
|
||||
</DiffStyleProvider>
|
||||
</WorktreeModeProvider>
|
||||
</ProviderShell.Chat>
|
||||
</ProviderShell.Session>
|
||||
|
||||
@@ -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<EditPreview | undefined>
|
||||
updateStyle: (style: "unified" | "split") => void
|
||||
updateMarkdown: (render: boolean) => void
|
||||
close: () => void
|
||||
}
|
||||
visible: Accessor<boolean>
|
||||
}
|
||||
|
||||
export const EditPreviewPanel: Component<Props> = (props) => {
|
||||
const { t } = useLanguage()
|
||||
|
||||
return (
|
||||
<section
|
||||
class="am-edit-preview-panel"
|
||||
classList={{ "am-edit-preview-panel-visible": props.visible() }}
|
||||
aria-label={t("agentManager.editPreview.title")}
|
||||
aria-hidden={!props.visible()}
|
||||
inert={!props.visible()}
|
||||
>
|
||||
<header class="am-edit-preview-header">
|
||||
<div class="am-edit-preview-heading">
|
||||
<Icon name="edit" size="small" />
|
||||
<span>{t("agentManager.editPreview.title")}</span>
|
||||
</div>
|
||||
<Show when={props.state.preview()}>
|
||||
{(preview) => (
|
||||
<RadioGroup
|
||||
options={["unified", "split"] as const}
|
||||
current={preview().style}
|
||||
value={(style) => 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)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<IconButton
|
||||
icon="close"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="am-edit-preview-close"
|
||||
type="button"
|
||||
label={t("agentManager.editPreview.close")}
|
||||
onClick={props.state.close}
|
||||
/>
|
||||
</header>
|
||||
<Show when={props.state.preview()}>
|
||||
{(preview) => (
|
||||
<div class="am-edit-preview-files">
|
||||
<For each={preview().diff.files ?? [preview().diff]}>
|
||||
{(diff) => (
|
||||
<VirtualDiffView
|
||||
diff={diff}
|
||||
diffStyle={preview().style}
|
||||
onDiffStyleChange={props.state.updateStyle}
|
||||
markdownRender={preview().markdown}
|
||||
onMarkdownRenderChange={props.state.updateMarkdown}
|
||||
styleSelect={false}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<EditPreview | undefined>,
|
||||
current: Accessor<string | null | undefined>,
|
||||
selection: Accessor<string | null | undefined>,
|
||||
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<DiffStyle>
|
||||
onStyleChange?: (style: DiffStyle) => void
|
||||
}
|
||||
|
||||
export function createEditPreview(opts: Options) {
|
||||
const [preview, setPreview] = createSignal<EditPreview>()
|
||||
|
||||
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<EditPreview | undefined>, open, updateStyle, updateMarkdown, close }
|
||||
}
|
||||
|
||||
export function isEditPreviewDiff(value: unknown): value is PermissionFileDiff {
|
||||
if (!value || typeof value !== "object") return false
|
||||
const diff = value as Partial<PermissionFileDiff>
|
||||
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<DiffStyle>,
|
||||
onStyleChange?: (style: DiffStyle) => void,
|
||||
) {
|
||||
const state = createEditPreview({
|
||||
show: () => {
|
||||
history(false)
|
||||
review(false)
|
||||
show()
|
||||
},
|
||||
hide,
|
||||
style,
|
||||
onStyleChange,
|
||||
})
|
||||
onCleanup(attachEditPreviewEvent(state.open))
|
||||
return state
|
||||
}
|
||||
@@ -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": "فتح",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "باز کردن",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "開く",
|
||||
|
||||
@@ -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": "열기",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Открыть",
|
||||
|
||||
@@ -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": "เปิด",
|
||||
|
||||
@@ -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ç",
|
||||
|
||||
@@ -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": "Відкрити",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,6 +10,7 @@ export enum SidePanel {
|
||||
PR = "pr",
|
||||
Terminal = "terminal",
|
||||
Subagents = "subagents",
|
||||
EditPreview = "edit-preview",
|
||||
}
|
||||
|
||||
function viewportWidth(viewport: number): number {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<VirtualDiffViewProps> = (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 (
|
||||
<div class="am-review-layout" data-virtualized={heavy() ? "true" : undefined}>
|
||||
<div class="am-review-toolbar">
|
||||
<div class="am-review-toolbar-left">
|
||||
<Show when={props.styleSelect !== false}>
|
||||
<RadioGroup
|
||||
options={["unified", "split"] as const}
|
||||
current={props.diffStyle}
|
||||
value={(style) => style}
|
||||
label={(style) =>
|
||||
style === "unified" ? t("ui.sessionReview.diffStyle.unified") : t("ui.sessionReview.diffStyle.split")
|
||||
}
|
||||
size="small"
|
||||
onSelect={(style) => {
|
||||
if (style) props.onDiffStyleChange(style)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<span class="am-review-toolbar-stats">
|
||||
<FileIcon node={{ path: props.diff.file, type: "file" }} />
|
||||
<Show when={directory()}>
|
||||
<span class="am-review-toolbar-dir">{`\u2066${directory()}/\u2069`}</span>
|
||||
</Show>
|
||||
<span class="am-review-toolbar-fname">{filename()}</span>
|
||||
<span class="am-review-toolbar-adds">+{counts().additions}</span>
|
||||
<span class="am-review-toolbar-dels">-{counts().deletions}</span>
|
||||
</span>
|
||||
</div>
|
||||
<Show when={isMarkdownFile(props.diff.file)}>
|
||||
<Tooltip value={props.markdownRender ? "Show raw Markdown" : "Render Markdown"} placement="bottom">
|
||||
<IconButton
|
||||
icon={props.markdownRender ? "code" : "eye"}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.markdownRender ? "Show raw Markdown" : "Render Markdown"}
|
||||
onClick={() => props.onMarkdownRenderChange(!props.markdownRender)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="am-review-diff" style={{ width: "100%" }} ref={(el) => (scroller = el)}>
|
||||
<Show
|
||||
when={view()}
|
||||
fallback={<div class="am-edit-preview-unavailable">Diff preview unavailable for this file.</div>}
|
||||
>
|
||||
{(current) => (
|
||||
<Show
|
||||
when={props.markdownRender && isMarkdownFile(props.diff.file)}
|
||||
fallback={
|
||||
<Diff
|
||||
fileDiff={current().fileDiff}
|
||||
diffStyle={props.diffStyle}
|
||||
hunkSeparators="simple"
|
||||
virtualized={heavy()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MarkdownDiffView diff={{ file: props.diff.file, before: current().before, after: current().after }} />
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<DiffVirtualFile | null>(null)
|
||||
const [diff, setDiff] = createSignal<VirtualDiffFile | null>(null)
|
||||
const [style, setStyle] = createSignal<DiffStyle>("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 (
|
||||
<div class="am-review-layout">
|
||||
<Show when={diff()}>
|
||||
{(d) => (
|
||||
<>
|
||||
<div class="am-review-toolbar">
|
||||
<div class="am-review-toolbar-left">
|
||||
<RadioGroup
|
||||
options={["unified", "split"] as const}
|
||||
current={style()}
|
||||
size="small"
|
||||
value={(s) => s}
|
||||
label={(s) =>
|
||||
s === "unified" ? t("ui.sessionReview.diffStyle.unified") : t("ui.sessionReview.diffStyle.split")
|
||||
}
|
||||
onSelect={(s) => {
|
||||
if (s) setStyle(s)
|
||||
}}
|
||||
/>
|
||||
<span class="am-review-toolbar-stats">
|
||||
<FileIcon node={{ path: d().file, type: "file" }} />
|
||||
<Show when={directory()}>
|
||||
<span class="am-review-toolbar-dir">{`\u2066${directory()}/\u2069`}</span>
|
||||
</Show>
|
||||
<span class="am-review-toolbar-fname">{filename()}</span>
|
||||
<span class="am-review-toolbar-adds">+{d().additions}</span>
|
||||
<span class="am-review-toolbar-dels">-{d().deletions}</span>
|
||||
</span>
|
||||
</div>
|
||||
<Show when={isMarkdownFile(d().file)}>
|
||||
<Tooltip value={markdown() ? "Show raw Markdown" : "Render Markdown"} placement="bottom">
|
||||
<IconButton
|
||||
icon={markdown() ? "code" : "eye"}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={markdown() ? "Show raw Markdown" : "Render Markdown"}
|
||||
onClick={() => {
|
||||
const next = !markdown()
|
||||
setMarkdown(next)
|
||||
getVSCodeAPI().postMessage({ type: "diffVirtual.setMarkdownRender", render: next })
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="am-review-diff" style={{ width: "100%" }} ref={(el) => (scrollerRef = el)}>
|
||||
<Show when={view()}>
|
||||
{(v) => (
|
||||
<Show
|
||||
when={markdown() && isMarkdownFile(d().file)}
|
||||
fallback={<Diff fileDiff={v().fileDiff} diffStyle={style()} hunkSeparators="simple" />}
|
||||
>
|
||||
<MarkdownDiffView diff={{ file: d().file, before: v().before, after: v().after }} />
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={files().length > 0}>
|
||||
<div class="am-review-layout">
|
||||
<div class="am-review-toolbar">
|
||||
<div class="am-review-toolbar-left">
|
||||
<RadioGroup
|
||||
options={["unified", "split"] as const}
|
||||
current={style()}
|
||||
value={(value) => value}
|
||||
label={(value) =>
|
||||
value === "unified" ? t("ui.sessionReview.diffStyle.unified") : t("ui.sessionReview.diffStyle.split")
|
||||
}
|
||||
size="small"
|
||||
onSelect={(value) => {
|
||||
if (value) {
|
||||
setStyle(value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="am-edit-preview-files">
|
||||
<For each={files()}>
|
||||
{(current) => (
|
||||
<VirtualDiffView
|
||||
diff={current}
|
||||
diffStyle={style()}
|
||||
onDiffStyleChange={setStyle}
|
||||
markdownRender={markdown()}
|
||||
onMarkdownRenderChange={(render) => {
|
||||
setMarkdown(render)
|
||||
getVSCodeAPI().postMessage({ type: "diffVirtual.setMarkdownRender", render })
|
||||
}}
|
||||
styleSelect={false}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<PermissionDiffProps> = (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<PermissionDiffProps> = (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<PermissionDiffProps> = (props) => {
|
||||
</div>
|
||||
<div data-slot="permission-diff-actions">
|
||||
<DiffChanges changes={props.filediff} />
|
||||
<Tooltip value="View in new tab">
|
||||
<IconButton size="small" icon="expand" onClick={openInTab} aria-label="View diff in new tab" />
|
||||
<Tooltip value={worktree ? t("agentManager.editPreview.openInPanel") : "View in new tab"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
icon="expand"
|
||||
onClick={openInTab}
|
||||
aria-label={worktree ? t("agentManager.editPreview.openInPanel") : "View diff in new tab"}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createContext, createSignal, useContext, type Accessor, type ParentComponent } from "solid-js"
|
||||
|
||||
export type DiffStyle = "unified" | "split"
|
||||
|
||||
interface DiffStyleContextValue {
|
||||
style: Accessor<DiffStyle>
|
||||
setStyle: (style: DiffStyle) => void
|
||||
}
|
||||
|
||||
const DiffStyleContext = createContext<DiffStyleContextValue>()
|
||||
|
||||
export const DiffStyleProvider: ParentComponent = (props) => {
|
||||
const [style, setStyle] = createSignal<DiffStyle>("unified")
|
||||
return <DiffStyleContext.Provider value={{ style, setStyle }}>{props.children}</DiffStyleContext.Provider>
|
||||
}
|
||||
|
||||
export function useDiffStyle(): DiffStyleContextValue | undefined {
|
||||
return useContext(DiffStyleContext)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export interface PermissionFileDiff {
|
||||
patch?: string
|
||||
additions: number
|
||||
deletions: number
|
||||
files?: PermissionFileDiff[]
|
||||
}
|
||||
|
||||
export interface PermissionPatchFile {
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user