diff --git a/.changeset/rendered-document-inspector.md b/.changeset/rendered-document-inspector.md new file mode 100644 index 0000000000..a625b494b3 --- /dev/null +++ b/.changeset/rendered-document-inspector.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add an Agent Manager document inspector that previews Markdown and text files, with inline Markdown review comments that can be sent to the agent. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png index 0178a46d92..2a0fedc6d1 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a9f984fc6d004e151c6e82bd095e9038423d8fa6443a91bf1834c44f0af62ce -size 748576 +oid sha256:96a537574ebee768dab6914ffb449665d6930d2d577bf1dc42eb38d975e9be10 +size 749704 diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index e00294fc72..408721fbd3 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -119,6 +119,7 @@ color: var(--text-base); } } + } [data-slot="message-part-title-filename"] { @@ -154,6 +155,7 @@ direction: rtl; text-align: left; } + } /* Task tool child-session tool list (v1.0.25 style) */ diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 8300f6943f..0c561d3710 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -1427,10 +1427,13 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { if (!exists) { el.classList.remove("file-link-candidate") el.classList.remove("file-link") + el.classList.remove("plan-document-link") el.removeAttribute("data-file-candidate") el.removeAttribute("data-file-path") + el.removeAttribute("data-file-kind") el.removeAttribute("data-file-line") el.removeAttribute("data-file-col") + el.removeAttribute("title") return } // Strip ./ prefix for the click handler — VS Code resolves relative @@ -1440,6 +1443,16 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { el.classList.add("file-link") el.setAttribute("data-file-path", clean) el.removeAttribute("data-file-candidate") + // A plan document gets a document glyph and a localized kind hint instead of + // an inline badge, so the reference stays readable inside a sentence. + el.classList.remove("plan-document-link") + el.removeAttribute("data-file-kind") + el.removeAttribute("title") + if (/(?:^|\/)(?:plans|\.plans?)\/.*\.md$/i.test(clean)) { + el.classList.add("plan-document-link") + el.setAttribute("data-file-kind", "plan") + el.setAttribute("title", i18n.t("ui.patch.action.plan")) + } } const dispatch = (el: HTMLElement, p: string) => { @@ -2053,9 +2066,9 @@ ToolRegistry.register({ animate={props.reveal} onClick={data.openFile ? () => data.openFile!(filepath) : undefined} /> - )} + )} - 0}> + 0}>
{(file) => ( @@ -2941,7 +2954,9 @@ ToolRegistry.register({ { if (!data.openFile) return e.stopPropagation() diff --git a/packages/kilo-vscode/esbuild.js b/packages/kilo-vscode/esbuild.js index 2fa1876afd..ec5b59f98b 100644 --- a/packages/kilo-vscode/esbuild.js +++ b/packages/kilo-vscode/esbuild.js @@ -275,6 +275,7 @@ function getWebviewsConfig() { kiloclaw: "webview-ui/kiloclaw/index.tsx", marketplace: "webview-ui/marketplace/index.tsx", "diff-viewer": "webview-ui/diff-viewer/index.tsx", + documents: "webview-ui/documents/index.tsx", "diff-virtual": "webview-ui/diff-virtual/index.tsx", webview: "webview-ui/src/index.tsx", }, diff --git a/packages/kilo-vscode/knip.json b/packages/kilo-vscode/knip.json index 18a96ea110..497095ed5c 100644 --- a/packages/kilo-vscode/knip.json +++ b/packages/kilo-vscode/knip.json @@ -3,6 +3,7 @@ "entry": [ "src/extension.ts", "webview-ui/agent-manager/index.tsx", + "webview-ui/documents/index.tsx", "webview-ui/diff-viewer/index.tsx", "webview-ui/diff-virtual/index.tsx", "webview-ui/kiloclaw/index.tsx", diff --git a/packages/kilo-vscode/src/DocumentViewerProvider.ts b/packages/kilo-vscode/src/DocumentViewerProvider.ts new file mode 100644 index 0000000000..41f1409900 --- /dev/null +++ b/packages/kilo-vscode/src/DocumentViewerProvider.ts @@ -0,0 +1,151 @@ +import * as vscode from "vscode" +import type { ReviewCommentEntry } from "./shared/review-comments" +import { readDocument } from "./documents/document-reader" +import { openRelativeFile } from "./review-utils" +import { buildWebviewHtml } from "./utils" +import type { KiloConnectionService } from "./services/cli-backend" + +interface Context { + sessionId?: string + directory?: string +} + +type Comment = ReviewCommentEntry + +export interface DocumentViewerOptions { + onComments: (comments: Comment[], autoSend: boolean) => void +} + +export class DocumentViewerProvider implements vscode.Disposable { + public static readonly viewType = "kilo-code.new.DocumentsPanel" + private panel: vscode.WebviewPanel | undefined + private pending: { file: string; sessionId?: string; directory?: string; line?: number; column?: number } | undefined + private readonly contexts = new Map() + private currentKey = "" + private readonly disposables: vscode.Disposable[] = [] + + constructor( + private readonly extensionUri: vscode.Uri, + private readonly connection: KiloConnectionService, + private readonly options: DocumentViewerOptions, + ) {} + + openFromCommand(input: { + file: string + sessionId?: string + directory?: string + line?: number + column?: number + }): void { + const contextKey = this.contextKey(input.sessionId, input.directory) + this.contexts.set(contextKey, { sessionId: input.sessionId, directory: input.directory }) + this.currentKey = contextKey + const next = { ...input, contextKey } + if (this.panel) { + this.panel.reveal(this.panel.viewColumn ?? vscode.ViewColumn.One) + void this.panel.webview.postMessage({ type: "document.open", ...next }) + return + } + this.pending = input + this.createPanel() + } + + dispose(): void { + this.panel?.dispose() + for (const item of this.disposables) item.dispose() + this.disposables.length = 0 + this.contexts.clear() + } + + private contextKey(sessionId?: string, directory?: string): string { + return `${sessionId ?? "local"}:${directory ?? ""}` + } + + private createPanel(): void { + const panel = vscode.window.createWebviewPanel( + DocumentViewerProvider.viewType, + "Documents", + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [this.extensionUri], + }, + ) + panel.iconPath = { + light: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-light.svg"), + dark: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-dark.svg"), + } + panel.webview.html = this.html(panel.webview) + this.panel = panel + this.disposables.push( + panel.webview.onDidReceiveMessage((message) => this.message(message as Record)), + panel.onDidDispose(() => { + this.panel = undefined + this.pending = undefined + this.currentKey = "" + }), + ) + } + + private message(message: Record): void { + if (message.type === "webviewReady") return this.ready() + if (message.type === "document.request") { + return this.request(message) + } + if (message.type === "document.sendComments" && Array.isArray(message.comments)) { + this.options.onComments(message.comments as Comment[], message.autoSend === true) + return + } + if (message.type === "document.openFile" && typeof message.file === "string") { + const context = this.contexts.get(this.currentKey) + openRelativeFile( + context?.directory, + message.file, + typeof message.line === "number" ? message.line : undefined, + typeof message.column === "number" ? message.column : undefined, + ) + return + } + if (message.type === "document.close") this.panel?.dispose() + } + + private ready(): void { + if (!this.pending || !this.panel) return + const input = this.pending + this.pending = undefined + void this.panel.webview.postMessage({ + type: "document.open", + ...input, + contextKey: this.contextKey(input.sessionId, input.directory), + }) + } + + private request(message: Record): void { + const file = typeof message.file === "string" ? message.file : undefined + const contextKey = typeof message.contextKey === "string" ? message.contextKey : undefined + if (!file || !contextKey) return + const context = this.contexts.get(contextKey) + const result = context?.directory + ? readDocument(context.directory, file) + : { error: "The document context is no longer available." } + this.panel?.webview.postMessage({ + type: "document.result", + sessionId: context?.sessionId ?? "", + contextKey, + requestedFile: file, + ...result, + }) + } + + private html(webview: vscode.Webview): string { + return buildWebviewHtml(webview, { + scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "documents.js")), + styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "documents.css")), + iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")), + workerUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")), + title: "Documents", + port: this.connection.getServerInfo()?.port, + }) + } +} diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 2268e4b71f..58db489f1d 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -464,6 +464,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private createWorktreeHandler: ((baseBranch?: string, branchName?: string) => Promise) | null = null private diffVirtualProvider: import("./DiffVirtualProvider").DiffVirtualProvider | undefined + private diffViewerProvider: import("./diff/DiffViewerProvider").DiffViewerProvider | undefined + private documentViewerProvider: import("./DocumentViewerProvider").DocumentViewerProvider | undefined private remoteService: RemoteStatusService | null = null private unsubscribeRemote: (() => void) | null = null @@ -561,6 +563,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.diffVirtualProvider = provider } + public setDiffViewerProvider(provider: import("./diff/DiffViewerProvider").DiffViewerProvider): void { + this.diffViewerProvider = provider + } + + public setDocumentViewerProvider(provider: import("./DocumentViewerProvider").DocumentViewerProvider): void { + this.documentViewerProvider = provider + } + getTelemetryProperties(): Record { return { appName: "kilo-code", @@ -1592,6 +1602,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // the live currentSession — see editor-actions.ts's validateFiles case. dir: (sessionID) => this.getWorkspaceDirectory(sessionID ?? this.currentSession?.id), diff: this.diffVirtualProvider, + openMarkdown: (file, sessionID) => { + if (!this.documentViewerProvider) return false + this.documentViewerProvider.openFromCommand({ + sessionId: sessionID, + directory: this.getWorkspaceDirectory(sessionID ?? this.currentSession?.id), + file, + }) + return true + }, storage: this.extensionContext?.globalStorageUri, post: (msg) => this.postMessage(msg), }) diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 1a5cbd986e..3c4172f44f 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -854,39 +854,21 @@ export class AgentManagerProvider implements Disposable { return null } if (m.type === "agentManager.requestDiffBranches") { - void this.sendDiffBranches(m.sessionId, m.scope) + void this.diffs.postBranches(composeDiffId(m.sessionId, normalizeScope(m.scope))) return null } if (m.type === "agentManager.setDiffBaseBranch") { void this.diffs .setBase(composeDiffId(m.sessionId, normalizeScope(m.scope)), m.branch) .catch((err) => this.log("Failed to set diff base:", err instanceof Error ? err.message : String(err))) - .then(() => void this.sendDiffBranches(m.sessionId, m.scope)) + .then(() => void this.diffs.postBranches(composeDiffId(m.sessionId, normalizeScope(m.scope)))) return null } if (m.type === "agentManager.openFile") { this.openWorktreeFile(m.sessionId, m.filePath, m.line, m.column) return null } - } - - private async sendDiffBranches(sessionId: string, scope?: string): Promise { - const id = composeDiffId(sessionId, normalizeScope(scope)) - const result = await this.diffs.branches(id).catch((err) => { - this.log("Failed to list diff branches:", err instanceof Error ? err.message : String(err)) - return undefined - }) - if (!result) return - this.postToWebview({ - type: "agentManager.diffBranches", - sessionId: id, - branches: result.branches, - defaultBranch: result.defaultBranch, - autoBase: result.autoBase, - currentBase: result.currentBase, - isAuto: result.isAuto, - currentBranch: result.currentBranch, - }) + if (m.type === "agentManager.requestDocument") return this.diffs.document(m.sessionId, m.file, m.contextKey) } private onBridgeMessage(m: AgentManagerInMessage): Record | null | undefined { diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 9029cb63ae..310469668e 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -369,6 +369,19 @@ interface WorktreeDiffFileMessage { diff: WorktreeDiffEntry | null } +interface DocumentMessage { + type: "agentManager.document" + sessionId: string + contextKey?: string + file: string + requestedFile?: string + content?: string + kind?: "text" | "image" + mime?: string + data?: string + error?: string +} + interface RevertWorktreeFileResultMessage { type: "agentManager.revertWorktreeFileResult" sessionId: string @@ -447,6 +460,7 @@ export type AgentManagerOutMessage = | WorktreeDiffNoticeMessage | WorktreeDiffMessage | WorktreeDiffFileMessage + | DocumentMessage | RevertWorktreeFileResultMessage | DiffBranchesMessage | PRStatusOutMessage @@ -794,6 +808,13 @@ interface OpenFileIn { column?: number } +interface RequestDocumentIn { + type: "agentManager.requestDocument" + sessionId: string + file: string + contextKey?: string +} + // Pass-through messages intercepted for side effects interface GenericOpenFileIn { type: "openFile" @@ -1073,6 +1094,7 @@ export type AgentManagerInMessage = | OpenSessionsIn | VisibleSessionIn | OpenFileIn + | RequestDocumentIn | GenericOpenFileIn | PreviewImageIn | SaveImageIn diff --git a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts index b8a4b269e7..d4ca705992 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts @@ -8,6 +8,7 @@ import type { ApplyConflict, GitOps } from "./GitOps" import { shouldStopDiffPolling } from "./delete-worktree" import { remoteRef, type ManagedSession, type WorktreeStateManager } from "./WorktreeStateManager" import { parseDiffId, scopeToSourceId } from "./diff-scope" +import { readDocument } from "../documents/document-reader" import type { AgentManagerOutMessage, WorktreeDiffEntry } from "./types" const LOCAL_DIFF_ID = "local" as const @@ -182,6 +183,50 @@ export class WorktreeDiffController { await this.controller.requestFile(file) } + /** Resolve the base-branch choices for a context and push them to the webview. */ + public async postBranches(id: string): Promise { + const result = await this.branches(id).catch((err) => { + this.ctx.log("Failed to list diff branches:", err instanceof Error ? err.message : String(err)) + return undefined + }) + if (!result) return + this.ctx.post({ + type: "agentManager.diffBranches", + sessionId: id, + branches: result.branches, + defaultBranch: result.defaultBranch, + autoBase: result.autoBase, + currentBase: result.currentBase, + isAuto: result.isAuto, + currentBranch: result.currentBranch, + }) + } + + /** + * Read one file from a worktree for the document inspector. Reuses this + * controller's state/root context because a document read is a worktree file + * read, resolved against the same directory the diff for that context uses. + */ + public document(sessionId: string, file: string, contextKey?: string): null { + void this.ready("stateReady rejected, continuing document resolve:").then(() => { + const state = this.ctx.getState() + const worktree = sessionId === LOCAL_DIFF_ID ? undefined : state?.getWorktree(sessionId) + const session = worktree || sessionId === LOCAL_DIFF_ID ? undefined : state?.getSession(sessionId) + const root = + sessionId === LOCAL_DIFF_ID + ? this.ctx.getRoot() + : (worktree?.path ?? + (session?.worktreeId + ? state?.getWorktree(session.worktreeId)?.path + : session + ? this.ctx.getRoot() + : undefined)) + const result = root ? readDocument(root, file) : { error: "The document context is no longer available." } + this.ctx.post({ type: "agentManager.document", sessionId, file, requestedFile: file, contextKey, ...result }) + }) + return null + } + public start(id: string): void { if (this.controller.isPolling && this.controller.currentId === id) return this.ctx.log(`Starting diff polling for ${id}`) diff --git a/packages/kilo-vscode/src/diff/DiffViewerProvider.ts b/packages/kilo-vscode/src/diff/DiffViewerProvider.ts index c17b5a34f4..854b405dd9 100644 --- a/packages/kilo-vscode/src/diff/DiffViewerProvider.ts +++ b/packages/kilo-vscode/src/diff/DiffViewerProvider.ts @@ -55,6 +55,9 @@ export class DiffViewerProvider implements vscode.Disposable { if (this.panel && this.controller) { this.panel.reveal(this.panel.viewColumn ?? vscode.ViewColumn.One) + void this.panel.webview.postMessage({ type: "diffViewer.initialFile", file: this.ctx.initialFile }) + if (this.ctx.initialMarkdown !== undefined) + void this.panel.webview.postMessage({ type: "diffViewer.initialMarkdown", render: this.ctx.initialMarkdown }) this.controller.setContext(this.ctx) const nextId = this.catalog.defaultSourceId(this.ctx) if (nextId && nextId !== this.controller.currentId) { @@ -77,7 +80,13 @@ export class DiffViewerProvider implements vscode.Disposable { * the source picker hidden — the view becomes a static "diff of this turn" * rather than the switchable workspace/session viewer. */ - openFromCommand(arg?: { sessionId?: string; turnId?: string; initialSourceId?: string; directory?: string }): void { + openFromCommand(arg?: { + sessionId?: string + turnId?: string + initialSourceId?: string + directory?: string + file?: string + }): void { const sessionId = arg?.sessionId ?? this.sessionIdProvider() const explicit = !!arg && "directory" in arg const dir = explicit ? arg.directory : sessionId ? this.sessionDirectoryProvider(sessionId) : undefined @@ -87,6 +96,8 @@ export class DiffViewerProvider implements vscode.Disposable { sessionId, dir, initialSourceId: turnInitialSourceId ?? arg?.initialSourceId, + initialFile: arg?.file, + ...(typeof arg?.file === "string" && /\.(md|mdx|markdown)$/i.test(arg.file) ? { initialMarkdown: true } : {}), hidePicker: !!turnInitialSourceId, }) } @@ -229,6 +240,9 @@ export class DiffViewerProvider implements vscode.Disposable { workspaceDirectory: this.ctx?.dir ?? getWorkspaceRoot(), }) void this.panel.webview.postMessage({ type: "diffViewer.markdownRender", render: getDiffMarkdownRender() }) + void this.panel.webview.postMessage({ type: "diffViewer.initialFile", file: this.ctx?.initialFile }) + if (this.ctx?.initialMarkdown !== undefined) + void this.panel.webview.postMessage({ type: "diffViewer.initialMarkdown", render: this.ctx.initialMarkdown }) const initial = this.ctx ? this.catalog.defaultSourceId(this.ctx) : undefined if (initial) this.swap(initial) } diff --git a/packages/kilo-vscode/src/diff/types.ts b/packages/kilo-vscode/src/diff/types.ts index 3ff72904e1..b8b5c01574 100644 --- a/packages/kilo-vscode/src/diff/types.ts +++ b/packages/kilo-vscode/src/diff/types.ts @@ -3,6 +3,10 @@ export interface PanelContext { sessionId?: string /** Overrides the computed default source on open. */ initialSourceId?: string + /** Select a file when the source first loads. */ + initialFile?: string + /** Render Markdown when the viewer was opened from a Markdown file link. */ + initialMarkdown?: boolean /** * Hides the source picker header in the diff viewer. Used for panels that * open in a fixed view (e.g. a specific turn's diff) diff --git a/packages/kilo-vscode/src/documents/document-reader.ts b/packages/kilo-vscode/src/documents/document-reader.ts new file mode 100644 index 0000000000..7765394bfe --- /dev/null +++ b/packages/kilo-vscode/src/documents/document-reader.ts @@ -0,0 +1,49 @@ +import * as fs from "fs" +import * as path from "path" + +const MAX_TEXT_BYTES = 2_000_000 +const MAX_IMAGE_BYTES = 5_000_000 + +export type DocumentResult = + | { file: string; kind: "text"; content: string } + | { file: string; kind: "image"; mime: string; data: string } + | { error: string } + +function mime(file: string): string | undefined { + const ext = path.extname(file).toLowerCase() + if (ext === ".png") return "image/png" + if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg" + if (ext === ".gif") return "image/gif" + if (ext === ".webp") return "image/webp" + if (ext === ".svg") return "image/svg+xml" + return undefined +} + +export function readDocument(root: string, file: string): DocumentResult { + if (!file) return { error: "Invalid document path." } + + try { + const base = fs.realpathSync(root) + const target = path.isAbsolute(file) ? file : path.resolve(root, file) + const resolved = fs.realpathSync(target) + if (resolved !== base && !resolved.startsWith(base + path.sep)) + return { error: "Document is outside the worktree." } + + const stat = fs.statSync(resolved) + if (!stat.isFile()) return { error: "Document is not a file." } + + const type = mime(resolved) + const limit = type ? MAX_IMAGE_BYTES : MAX_TEXT_BYTES + if (stat.size > limit) return { error: "Document is too large to preview." } + + const relative = path.relative(base, resolved).split(path.sep).join("/") + if (type) return { file: relative, kind: "image", mime: type, data: fs.readFileSync(resolved).toString("base64") } + + const content = fs.readFileSync(resolved) + if (content.includes(0)) return { error: "Binary files cannot be previewed." } + return { file: relative, kind: "text", content: content.toString("utf8") } + } catch (error) { + console.error("[Kilo New] AgentManagerProvider: Cannot read document:", error) + return { error: "Document could not be read." } + } +} diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index c31bfedd68..1de4f7f6d6 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -4,6 +4,7 @@ import { AgentManagerProvider } from "./agent-manager/AgentManagerProvider" import { VscodeHost } from "./agent-manager/vscode-host" import { KiloClawProvider } from "./kiloclaw/KiloClawProvider" import { DiffViewerProvider } from "./diff/DiffViewerProvider" +import { DocumentViewerProvider } from "./DocumentViewerProvider" import { DiffSourceCatalog } from "./diff/sources/catalog" import { DiffVirtualProvider } from "./DiffVirtualProvider" import { SettingsEditorProvider } from "./SettingsEditorProvider" @@ -278,8 +279,15 @@ export function activate(context: vscode.ExtensionContext) { diffViewerProvider.setCommentHandler((comments, autoSend) => { void provider.appendReviewComments(comments, autoSend) }) + provider.setDiffViewerProvider(diffViewerProvider) context.subscriptions.push(diffViewerProvider) + const documentViewerProvider = new DocumentViewerProvider(context.extensionUri, connectionService, { + onComments: (comments, autoSend) => void provider.appendReviewComments(comments, autoSend), + }) + provider.setDocumentViewerProvider(documentViewerProvider) + context.subscriptions.push(documentViewerProvider) + // Create diff virtual provider (lightweight single-file diff for permission approval) const diffVirtualProvider = new DiffVirtualProvider(context.extensionUri) provider.setDiffVirtualProvider(diffVirtualProvider) @@ -325,6 +333,15 @@ export function activate(context: vscode.ExtensionContext) { }), ) + context.subscriptions.push( + vscode.window.registerWebviewPanelSerializer(DocumentViewerProvider.viewType, { + deserializeWebviewPanel(panel: vscode.WebviewPanel) { + panel.dispose() + return Promise.resolve() + }, + }), + ) + context.subscriptions.push( vscode.window.registerWebviewPanelSerializer(DiffViewerProvider.viewType, { deserializeWebviewPanel(panel: vscode.WebviewPanel) { diff --git a/packages/kilo-vscode/src/kilo-provider/editor-actions.ts b/packages/kilo-vscode/src/kilo-provider/editor-actions.ts index 982c587045..f639b46b7f 100644 --- a/packages/kilo-vscode/src/kilo-provider/editor-actions.ts +++ b/packages/kilo-vscode/src/kilo-provider/editor-actions.ts @@ -14,6 +14,10 @@ type EditorOpenMessage = { sessionID?: string } +function isMarkdownFile(file: string): boolean { + return /\.(md|mdx|markdown)$/i.test(file) +} + function openExternal(url: unknown): void { if (typeof url !== "string") return void vscode.env.openExternal(vscode.Uri.parse(url)) @@ -76,6 +80,7 @@ export function handleEditorAction( opts: { dir: (sessionID?: string) => string diff?: DiffVirtualProvider + openMarkdown?: (file: string, sessionID?: string) => boolean storage?: vscode.Uri post?: (msg: unknown) => void }, @@ -84,7 +89,10 @@ export function handleEditorAction( // Resolve the directory from the session the file reference was rendered // for (when the webview provides it), not whatever session happens to be // current — mirrors the validateFiles case below. - if (message.filePath) openFile(opts.dir(message.sessionID), message.filePath, message.line, message.column) + if (message.filePath) { + if (isMarkdownFile(message.filePath) && opts.openMarkdown?.(message.filePath, message.sessionID)) return true + openFile(opts.dir(message.sessionID), message.filePath, message.line, message.column) + } return true } if (message.type === "openContent") { 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..5f0f1df1d5 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -27,6 +27,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/ProjectSelect.tsx"), path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"), path.join(ROOT, "webview-ui/agent-manager/DiffPanel.tsx"), + path.join(ROOT, "webview-ui/documents/DocumentPanel.tsx"), 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"), diff --git a/packages/kilo-vscode/tests/unit/agent-manager-documents.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-documents.test.ts new file mode 100644 index 0000000000..4d73a993e9 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-manager-documents.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "bun:test" +import { createRoot, createSignal } from "solid-js" +import { createDocumentComments, createDocuments } from "../../webview-ui/documents/state" +import type { AgentManagerDocumentMessage } from "../../webview-ui/src/types/messages" + +describe("Agent Manager document state", () => { + it("keeps tabs, content, and comments scoped when switching worktrees and projects", () => { + createRoot((dispose) => { + const sent: unknown[] = [] + const vscode = { postMessage: (message: unknown) => sent.push(message) } as Parameters[0] + const [scope, setScope] = createSignal("project-a:wt-a") + const [session, setSession] = createSignal("ses-a") + const docs = createDocuments(vscode, scope, session) + const comments = createDocumentComments(scope) + + docs.open("plans/plan.md") + expect(sent[0]).toMatchObject({ sessionId: "ses-a", contextKey: "project-a:wt-a" }) + docs.onMessage({ + type: "agentManager.document", + sessionId: "ses-a", + contextKey: "project-a:wt-a", + requestedFile: "plans/plan.md", + file: "plans/plan.md", + kind: "text", + content: "# Worktree A", + } satisfies AgentManagerDocumentMessage) + comments.setComments([{ id: "a", file: "plans/plan.md", side: "additions", line: 1, comment: "A" }]) + + setScope("project-a:wt-b") + setSession("ses-b") + expect(docs.tabs()).toEqual([]) + expect(docs.document("plans/plan.md")).toBeUndefined() + expect(comments.comments()).toEqual([]) + + docs.onMessage({ + type: "agentManager.document", + sessionId: "ses-a", + contextKey: "project-a:wt-a", + requestedFile: "plans/late.md", + file: "plans/late.md", + kind: "text", + content: "# Late A", + } satisfies AgentManagerDocumentMessage) + expect(docs.tabs()).toEqual([]) + expect(docs.document("plans/late.md")).toBeUndefined() + + docs.open("plans/plan.md") + expect(sent[1]).toMatchObject({ sessionId: "ses-b", contextKey: "project-a:wt-b" }) + docs.onMessage({ + type: "agentManager.document", + sessionId: "ses-b", + contextKey: "project-a:wt-b", + requestedFile: "plans/plan.md", + file: "plans/plan.md", + kind: "text", + content: "# Worktree B", + } satisfies AgentManagerDocumentMessage) + comments.setComments([{ id: "b", file: "plans/plan.md", side: "additions", line: 1, comment: "B" }]) + + setScope("project-a:wt-a") + setSession("ses-a") + expect(docs.tabs()).toHaveLength(1) + expect(docs.document("plans/plan.md")?.content).toBe("# Worktree A") + expect(comments.comments().map((item) => item.comment)).toEqual(["A"]) + + setScope("project-b:wt-a") + expect(docs.tabs()).toEqual([]) + expect(comments.comments()).toEqual([]) + dispose() + }) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts index 8142d2c20c..50f7958210 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-i18n.test.ts @@ -28,6 +28,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"), path.join(ROOT, "webview-ui/agent-manager/terminal/SideTerminalPanel.tsx"), path.join(ROOT, "webview-ui/agent-manager/terminal/TerminalDestinationButton.tsx"), + path.join(ROOT, "webview-ui/documents/DocumentPanel.tsx"), ] /** diff --git a/packages/kilo-vscode/tests/unit/document-reader.test.ts b/packages/kilo-vscode/tests/unit/document-reader.test.ts new file mode 100644 index 0000000000..114674c00c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/document-reader.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "bun:test" +import * as fs from "node:fs" +import * as os from "node:os" +import * as path from "node:path" +import { readDocument } from "../../src/documents/document-reader" + +function workspace() { + return fs.mkdtempSync(path.join(os.tmpdir(), "kilo-document-")) +} + +describe("readDocument", () => { + it("reads text files inside the worktree", () => { + const root = workspace() + fs.writeFileSync(path.join(root, "plan.md"), "# Plan\n") + + expect(readDocument(root, "plan.md")).toEqual({ file: "plan.md", kind: "text", content: "# Plan\n" }) + }) + + it("rejects paths outside the worktree", () => { + const root = workspace() + const outside = path.join(root, "..", "outside.md") + fs.writeFileSync(outside, "secret") + + expect(readDocument(root, "../outside.md")).toEqual({ error: "Document is outside the worktree." }) + }) + + it("rejects binary files", () => { + const root = workspace() + fs.writeFileSync(path.join(root, "data.bin"), Buffer.from([1, 0, 2])) + + expect(readDocument(root, "data.bin")).toEqual({ error: "Binary files cannot be previewed." }) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 982225b381..f520353b59 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -179,6 +179,8 @@ import { SidebarToggleButton } from "./SidebarToggleButton" import { setTabWidths } from "./tab-widths" import { clampPanelWidth, createPanelResize, maxPanelWidth, minPanelWidth, SidePanel } from "./side-panel-layout" import { SubagentPanel } from "./SubagentPanel" +import { DocumentPanelHost } from "./documents/DocumentPanelHost" +import { createDocumentInspector } from "../documents/state" import { createSubagentController } from "./subagent-tabs" import { buildShortcutCategories } from "./shortcuts" import { tracker } from "./telemetry" @@ -288,7 +290,6 @@ const AgentManagerContent: Component = () => { 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 @@ -321,6 +322,18 @@ const AgentManagerContent: Component = () => { const reviewComposer = createReviewComposer() const [reviewActive, setReviewActive] = createSignal(false) const [reviewDiffStyle, setReviewDiffStyle] = createSignal<"unified" | "split">("unified") + const documentInspector = createDocumentInspector( + vscode, + selection, + currentProjectId, + () => sidePanel() === SidePanel.Documents, + () => { + setHistory(false) + setReviewActive(false) + setSidePanel(SidePanel.Documents) + }, + () => setSidePanel(null), + ) const subagentCtl = createSubagentController({ project: currentProjectId, current: session.currentSessionID, @@ -490,15 +503,9 @@ const AgentManagerContent: Component = () => { ) onCleanup(() => clearTimeout(pendingDeleteTimer)) - // Per-context tab memory lives in the active project's store: maps sidebar - // selection ("local" or a worktree id) -> last active session/pending ID const tabMemory = () => registry.active().tabMemory.all() - const reviewOpen = createMemo(() => { - const sel = selection() - if (sel === null) return false - return reviewOpenByContext()[sel] === true - }) + const reviewOpen = createMemo(() => selection() !== null && reviewOpenByContext()[selection()!] === true) const setReviewOpenForContext = (context: string, open: boolean) => { setReviewOpenByContext((prev) => { @@ -584,7 +591,6 @@ const AgentManagerContent: Component = () => { const isPending = (id: string) => id.startsWith(PENDING_PREFIX) reportRemoteSessions(vscode, localSessionIDs, managedSessions, isPending) - // Drag-and-drop state for tab reordering const [draggingTab, setDraggingTab] = createSignal() const freezeTabs = () => { @@ -593,7 +599,6 @@ const AgentManagerContent: Component = () => { } const releaseTabs = () => setTabWidths(false) - // Tab ordering: context key → ordered session ID array (recovered from extension state) const worktreeTabOrder = () => registry.active().tabOrder() const setWorktreeTabOrder: Setter> = (v) => registry.active().setTabOrder(v) // Sidebar worktree order (persisted to extension state) @@ -941,9 +946,6 @@ const AgentManagerContent: Component = () => { if (el instanceof HTMLElement) scrollIntoView(el) } - // Sidebar previous/next + numeric-shortcut nav. Multi-project mode traverses - // every expanded project and atomically activates via activateSelection; - // single-project mode keeps the legacy in-process path. const projectNav = createProjectNav( { multiProject, @@ -1261,10 +1263,6 @@ const AgentManagerContent: Component = () => { } window.addEventListener("keydown", preventDefaults, true) - // Cmd/Ctrl+/ toggles the terminal even when VS Code's webview keybinding - // forwarding drops the key before it reaches the workbench (reported with - // the prompt input focused). When forwarding does work, the extension - // echoes the shortcut back as an action message and sideCtl dedupes it. const shortcut = (e: KeyboardEvent) => sideCtl.press(e) window.addEventListener("keydown", shortcut, true) @@ -1634,8 +1632,6 @@ const AgentManagerContent: Component = () => { } }) - // Diff context = sidebar selection (worktree id or LOCAL), stable across - // session tab switches inside the context so the git scopes don't refetch. const diffCtx = createMemo(() => selection() ?? undefined) // Active session within the diff context. The Session scope follows it, so @@ -1685,8 +1681,6 @@ const AgentManagerContent: Component = () => { /> ) - // Start/stop diff watch when the panel opens/closes, the review tab opens, - // or the composite id (context, scope, active session) changes. createEffect(() => { const panel = diffOpen() const active = reviewActive() @@ -2102,7 +2096,6 @@ const AgentManagerContent: Component = () => { closeReviewTab() } - // Drag-and-drop handlers for tab reordering const tabLookup = createMemo(() => new Map(activeTabs().map((s) => [s.id, s]))) const tabIds = createMemo(() => { const ids = activeTabs().map((s) => s.id) @@ -2454,6 +2447,9 @@ const AgentManagerContent: Component = () => { prStatus={() => activePR()?.pr} prOpen={prOpen} onTogglePR={togglePRPanel} + documentsOpen={documentInspector.isOpen} + documentsAvailable={documentInspector.available} + onToggleDocuments={documentInspector.toggle} subagentsAvailable={() => subagentCtl.tabs.tabs().length > 0 || subagentCtl.toolbar.available().length > 0} subagentsOpen={() => sidePanel() === SidePanel.Subagents} onToggleSubagents={subagentCtl.toolbar.toggle} @@ -2680,6 +2676,7 @@ const AgentManagerContent: Component = () => { if (id) vscode.postMessage({ type: "agentManager.openFile", sessionId: id, filePath: file, line }) }} + onOpenDocument={documentInspector.open} onRevertFile={metrics.use("revert_file", "side_review", revertCtl.revert)} revertingFiles={revertCtl.reverting()} activeTerminalId={terms.activeId()} @@ -2709,6 +2706,13 @@ const AgentManagerContent: Component = () => { onClosePanel={() => setSidePanel(null)} /> + setSidePanel(null)} + onSendAll={focusCtl.focus} + activeTerminalId={terms.activeId()} + visible={documentInspector.isOpen} + /> tooltip: Value icon: Value + /** Renders instead of `icon`, for tabs that need a per-filetype glyph. */ + iconNode?: Value iconStatus?: Value<"success" | "failure" | undefined> class?: string focused?: boolean @@ -71,8 +73,15 @@ export const ClosableTabChrome: Component = (props) => { > - }> - + }> + + + } + > + {value(props.iconNode!)} {label()} @@ -133,6 +142,7 @@ export const SortableClosableTab: Component< label={props.label} tooltip={props.tooltip} icon={props.icon} + iconNode={props.iconNode} iconStatus={props.iconStatus} class={props.class} focused={props.focused} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index dba5a6a3e1..9513aacae6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -92,6 +92,7 @@ interface DiffPanelProps { onExpand?: () => void onRequestDiff?: (file: string) => void onOpenFile?: (relativePath: string, line?: number) => void + onOpenDocument?: (relativePath: string) => void onRevertFile?: (file: string) => void revertingFiles?: Set activeTerminalId?: string @@ -650,6 +651,20 @@ export const DiffPanel: Component = (props) => { /> + + + { + e.stopPropagation() + props.onOpenDocument?.(diff.file) + }} + /> + + PRStatus | undefined prOpen: () => boolean onTogglePR: () => void + documentsOpen: () => boolean + documentsAvailable: () => boolean + onToggleDocuments: () => void subagentsAvailable: () => boolean subagentsOpen: () => boolean onToggleSubagents: () => void @@ -235,6 +238,18 @@ export const TabBar: Component = (props) => ( )} + + + + + + onClosePanel: () => void + onSendAll?: () => void + activeTerminalId?: string + visible: Accessor +} + +export const DocumentPanelHost: Component = (props) => ( + +) 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 c0d3f6dfd9..f698a65e9b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -215,6 +215,14 @@ export const dict = { "agentManager.review.imageUnavailable": "معاينة الصورة غير متاحة في لقطة الجلسة هذه.", "agentManager.review.endOfLongDiff": "لقد وصلت إلى النهاية!", + "agentManager.documents.title": "المستندات", + "agentManager.documents.toggle": "إظهار/إخفاء لوحة المستندات", + "agentManager.documents.tabs": "المستندات المفتوحة", + "agentManager.documents.empty": "افتح ملفًا من الدردشة أو diff لمعاينته هنا.", + "agentManager.documents.loading": "جارٍ تحميل المستند...", + "agentManager.documents.preview": "معاينة المستند", + "agentManager.documents.source": "عرض التعليمات البرمجية المصدر", + "agentManager.documents.comments": "{{count}} تعليقًا", "agentManager.import.pullRequest": "طلب سحب", "agentManager.import.pastePrUrl": "الصق رابط PR...", "agentManager.import.open": "فتح", 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 ebe8e68720..9cf9c7d09d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -219,6 +219,14 @@ export const dict = { "A visualização da imagem não está disponível para este instantâneo da sessão.", "agentManager.review.endOfLongDiff": "Você chegou ao fim!", + "agentManager.documents.title": "Documentos", + "agentManager.documents.toggle": "Mostrar/ocultar painel de documentos", + "agentManager.documents.tabs": "Documentos abertos", + "agentManager.documents.empty": "Abra um arquivo pelo chat ou pelo diff para pré-visualizá-lo aqui.", + "agentManager.documents.loading": "Carregando documento...", + "agentManager.documents.preview": "Visualizar documento", + "agentManager.documents.source": "Mostrar código-fonte", + "agentManager.documents.comments": "{{count}} comentários", "agentManager.import.pullRequest": "Solicitação de extração", "agentManager.import.pastePrUrl": "Cole a URL do PR...", "agentManager.import.open": "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 6dd20f40f5..73b278d2fd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -218,6 +218,14 @@ export const dict = { "agentManager.review.imageUnavailable": "Pregled slike nije dostupan za ovaj snimak sesije.", "agentManager.review.endOfLongDiff": "Došli ste do kraja!", + "agentManager.documents.title": "Dokumenti", + "agentManager.documents.toggle": "Prikaži/sakrij panel dokumenata", + "agentManager.documents.tabs": "Otvoreni dokumenti", + "agentManager.documents.empty": "Otvorite datoteku iz chata ili diff da biste je ovdje pregledali.", + "agentManager.documents.loading": "Učitavanje dokumenta...", + "agentManager.documents.preview": "Pregled dokumenta", + "agentManager.documents.source": "Prikaži izvorni kod", + "agentManager.documents.comments": "{{count}} komentara", "agentManager.import.pullRequest": "Zahtjev za povlačenje", "agentManager.import.pastePrUrl": "Zalijepite PR URL...", "agentManager.import.open": "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 95958ff3a9..b332004b29 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -220,6 +220,14 @@ export const dict = { "Forhåndsvisning af billedet er ikke tilgængelig for dette snapshot af sessionen.", "agentManager.review.endOfLongDiff": "Du nåede slutningen!", + "agentManager.documents.title": "Dokumenter", + "agentManager.documents.toggle": "Slå dokumentpanelet til/fra", + "agentManager.documents.tabs": "Åbnede dokumenter", + "agentManager.documents.empty": "Åbn en fil fra chatten eller diff for at få vist en forhåndsvisning her.", + "agentManager.documents.loading": "Indlæser dokument...", + "agentManager.documents.preview": "Forhåndsvis dokument", + "agentManager.documents.source": "Vis kildekode", + "agentManager.documents.comments": "{{count}} kommentarer", "agentManager.import.pullRequest": "Pull request", "agentManager.import.pastePrUrl": "Indsæt PR URL...", "agentManager.import.open": "Å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 aa28ebaee5..070b4316de 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -220,6 +220,14 @@ export const dict = { "agentManager.review.imageUnavailable": "Für diesen Sitzungssnapshot ist keine Bildvorschau verfügbar.", "agentManager.review.endOfLongDiff": "Du hast das Ende erreicht!", + "agentManager.documents.title": "Dokumente", + "agentManager.documents.toggle": "Dokumentenbereich ein-/ausblenden", + "agentManager.documents.tabs": "Geöffnete Dokumente", + "agentManager.documents.empty": "Öffnen Sie eine Datei im Chat oder im Diff, um sie hier in der Vorschau anzuzeigen.", + "agentManager.documents.loading": "Dokument wird geladen...", + "agentManager.documents.preview": "Dokument in der Vorschau anzeigen", + "agentManager.documents.source": "Quelltext anzeigen", + "agentManager.documents.comments": "{{count}} Kommentare", "agentManager.import.pullRequest": "Pull Request", "agentManager.import.pastePrUrl": "PR-URL einfügen...", "agentManager.import.open": "Ö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 7f33c3de8b..0db5faff8b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -220,6 +220,14 @@ export const dict = { "agentManager.review.imageUnreadable": "This image could not be rendered.", "agentManager.review.imageUnavailable": "Image preview is unavailable for this session snapshot.", "agentManager.review.endOfLongDiff": "You made it to the end!", + "agentManager.documents.title": "Documents", + "agentManager.documents.toggle": "Toggle document panel", + "agentManager.documents.tabs": "Open documents", + "agentManager.documents.empty": "Open a file from chat or the diff to preview it here.", + "agentManager.documents.loading": "Loading document...", + "agentManager.documents.preview": "Preview document", + "agentManager.documents.source": "Show source", + "agentManager.documents.comments": "{{count}} comments", "agentManager.import.pullRequest": "Pull Request", "agentManager.import.pastePrUrl": "Paste PR URL...", 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 5da8de8a60..2dd60ac5f0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -220,6 +220,14 @@ export const dict = { "La vista previa de la imagen no está disponible para esta instantánea de la sesión.", "agentManager.review.endOfLongDiff": "¡Llegaste al final!", + "agentManager.documents.title": "Documentos", + "agentManager.documents.toggle": "Mostrar u ocultar el panel de documentos", + "agentManager.documents.tabs": "Documentos abiertos", + "agentManager.documents.empty": "Abre un archivo desde el chat o el diff para previsualizarlo aquí.", + "agentManager.documents.loading": "Cargando documento...", + "agentManager.documents.preview": "Previsualizar documento", + "agentManager.documents.source": "Mostrar código fuente", + "agentManager.documents.comments": "{{count}} comentarios", "agentManager.import.pullRequest": "Pull Request", "agentManager.import.pastePrUrl": "Pegar URL del PR...", "agentManager.import.open": "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 32d59ed527..36baef0e4c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -222,6 +222,14 @@ export const dict = { "agentManager.review.imageUnavailable": "پیش‌نمایش تصویر برای این نمونه جلسه در دسترس نیست.", "agentManager.review.endOfLongDiff": "به انتها رسیدید!", + "agentManager.documents.title": "اسناد", + "agentManager.documents.toggle": "نمایش/پنهان کردن پنل اسناد", + "agentManager.documents.tabs": "اسناد باز", + "agentManager.documents.empty": "برای پیش‌نمایش فایل در اینجا، آن را از چت یا diff باز کنید.", + "agentManager.documents.loading": "در حال بارگذاری سند...", + "agentManager.documents.preview": "پیش‌نمایش سند", + "agentManager.documents.source": "نمایش کد منبع", + "agentManager.documents.comments": "{{count}} نظر", "agentManager.import.pullRequest": "درخواست ادغام", "agentManager.import.pastePrUrl": "URL درخواست PR را وارد کنید...", "agentManager.import.open": "باز کردن", 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 6fad71c484..2e2cbec4ae 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -220,6 +220,14 @@ export const dict = { "agentManager.review.imageUnavailable": "L’aperçu de l’image n’est pas disponible pour cet instantané de session.", "agentManager.review.endOfLongDiff": "Vous êtes arrivé à la fin !", + "agentManager.documents.title": "Documents", + "agentManager.documents.toggle": "Afficher/masquer le panneau des documents", + "agentManager.documents.tabs": "Documents ouverts", + "agentManager.documents.empty": "Ouvrez un fichier depuis le chat ou le diff pour en afficher un aperçu ici.", + "agentManager.documents.loading": "Chargement du document...", + "agentManager.documents.preview": "Prévisualiser le document", + "agentManager.documents.source": "Afficher le code source", + "agentManager.documents.comments": "{{count}} commentaires", "agentManager.import.pullRequest": "Pull Request", "agentManager.import.pastePrUrl": "Coller l'URL du PR...", "agentManager.import.open": "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 004e12c5ea..01f709d840 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -226,6 +226,14 @@ export const dict = { "L'anteprima dell'immagine non è disponibile per questa istantanea della sessione.", "agentManager.review.endOfLongDiff": "Sei arrivato alla fine!", + "agentManager.documents.title": "Documenti", + "agentManager.documents.toggle": "Mostra/nascondi il pannello dei documenti", + "agentManager.documents.tabs": "Documenti aperti", + "agentManager.documents.empty": "Apri un file dalla chat o dal diff per visualizzarne qui l'anteprima.", + "agentManager.documents.loading": "Caricamento del documento...", + "agentManager.documents.preview": "Visualizza anteprima del documento", + "agentManager.documents.source": "Mostra il codice sorgente", + "agentManager.documents.comments": "{{count}} commenti", "agentManager.import.pullRequest": "Pull Request", "agentManager.import.pastePrUrl": "Incolla URL PR...", "agentManager.import.open": "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 1fdaed5978..a7e151c6dc 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -219,6 +219,14 @@ export const dict = { "agentManager.review.imageUnavailable": "このセッションスナップショットでは画像プレビューを利用できません。", "agentManager.review.endOfLongDiff": "最後まで到達しました!", + "agentManager.documents.title": "ドキュメント", + "agentManager.documents.toggle": "ドキュメントパネルを切り替え", + "agentManager.documents.tabs": "開いているドキュメント", + "agentManager.documents.empty": "チャットまたは diff からファイルを開くと、ここでプレビューできます。", + "agentManager.documents.loading": "ドキュメントを読み込み中...", + "agentManager.documents.preview": "ドキュメントをプレビュー", + "agentManager.documents.source": "ソースを表示", + "agentManager.documents.comments": "{{count}}件のコメント", "agentManager.import.pullRequest": "プルリクエスト", "agentManager.import.pastePrUrl": "PR URLを貼り付け...", "agentManager.import.open": "開く", 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 492d8cc4df..8a6be27270 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -217,6 +217,14 @@ export const dict = { "agentManager.review.imageUnavailable": "이 세션 스냅샷에서는 이미지 미리보기를 사용할 수 없습니다.", "agentManager.review.endOfLongDiff": "끝까지 도달했습니다!", + "agentManager.documents.title": "문서", + "agentManager.documents.toggle": "문서 패널 전환", + "agentManager.documents.tabs": "열린 문서", + "agentManager.documents.empty": "채팅이나 diff에서 파일을 열어 여기에서 미리 보세요.", + "agentManager.documents.loading": "문서 로드 중...", + "agentManager.documents.preview": "문서 미리 보기", + "agentManager.documents.source": "소스 보기", + "agentManager.documents.comments": "댓글 {{count}}개", "agentManager.import.pullRequest": "풀 리퀘스트", "agentManager.import.pastePrUrl": "PR URL 붙여넣기...", "agentManager.import.open": "열기", 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 228f04eafc..3fd5257163 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -224,6 +224,14 @@ export const dict = { "agentManager.review.imageUnavailable": "Afbeeldingsvoorbeeld is niet beschikbaar voor deze sessiesnapshot.", "agentManager.review.endOfLongDiff": "Je hebt het einde bereikt!", + "agentManager.documents.title": "Documenten", + "agentManager.documents.toggle": "Documentenpaneel tonen/verbergen", + "agentManager.documents.tabs": "Geopende documenten", + "agentManager.documents.empty": "Open een bestand vanuit de chat of diff om het hier te bekijken.", + "agentManager.documents.loading": "Document wordt geladen...", + "agentManager.documents.preview": "Voorbeeld van document bekijken", + "agentManager.documents.source": "Broncode weergeven", + "agentManager.documents.comments": "{{count}} opmerkingen", "agentManager.import.pullRequest": "Pull request", "agentManager.import.pastePrUrl": "Plak PR URL...", "agentManager.import.open": "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 2a2ee0b58a..357e783011 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -217,6 +217,14 @@ export const dict = { "Forhåndsvisning av bildet er ikke tilgjengelig for dette øyeblikksbildet av økten.", "agentManager.review.endOfLongDiff": "Du nådde slutten!", + "agentManager.documents.title": "Dokumenter", + "agentManager.documents.toggle": "Vis/skjul dokumentpanelet", + "agentManager.documents.tabs": "Åpne dokumenter", + "agentManager.documents.empty": "Åpne en fil fra chatten eller diff for å forhåndsvise den her.", + "agentManager.documents.loading": "Laster inn dokument...", + "agentManager.documents.preview": "Forhåndsvis dokument", + "agentManager.documents.source": "Vis kildekode", + "agentManager.documents.comments": "{{count}} kommentarer", "agentManager.import.pullRequest": "Pull request", "agentManager.import.pastePrUrl": "Lim inn PR URL...", "agentManager.import.open": "Åpne", 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 6beb998364..c0301ffad9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -218,6 +218,14 @@ export const dict = { "agentManager.review.imageUnavailable": "Podgląd obrazu jest niedostępny dla tej migawki sesji.", "agentManager.review.endOfLongDiff": "Dotarłeś do końca!", + "agentManager.documents.title": "Dokumenty", + "agentManager.documents.toggle": "Pokaż/ukryj panel dokumentów", + "agentManager.documents.tabs": "Otwarte dokumenty", + "agentManager.documents.empty": "Otwórz plik z czatu lub diff, aby wyświetlić jego podgląd tutaj.", + "agentManager.documents.loading": "Ładowanie dokumentu...", + "agentManager.documents.preview": "Podgląd dokumentu", + "agentManager.documents.source": "Pokaż kod źródłowy", + "agentManager.documents.comments": "{{count}} komentarzy", "agentManager.import.pullRequest": "Prośba o scalenie", "agentManager.import.pastePrUrl": "Wklej URL PR...", "agentManager.import.open": "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 24ee5384ff..35cbe21c26 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -219,6 +219,14 @@ export const dict = { "agentManager.review.imageUnavailable": "Предварительный просмотр изображения недоступен для этого снимка сеанса.", "agentManager.review.endOfLongDiff": "Вы дошли до конца!", + "agentManager.documents.title": "Документы", + "agentManager.documents.toggle": "Показать или скрыть панель документов", + "agentManager.documents.tabs": "Открытые документы", + "agentManager.documents.empty": "Откройте файл из чата или diff, чтобы просмотреть его здесь.", + "agentManager.documents.loading": "Загрузка документа...", + "agentManager.documents.preview": "Предпросмотр документа", + "agentManager.documents.source": "Показать исходный код", + "agentManager.documents.comments": "{{count}} комментариев", "agentManager.import.pullRequest": "Запрос на слияние", "agentManager.import.pastePrUrl": "Вставьте URL PR...", "agentManager.import.open": "Открыть", 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 a2c52d371f..939bc3bdbf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -213,6 +213,14 @@ export const dict = { "agentManager.review.imageUnavailable": "ไม่สามารถแสดงตัวอย่างรูปภาพสำหรับสแนปช็อตของเซสชันนี้ได้", "agentManager.review.endOfLongDiff": "คุณมาถึงท้ายสุดแล้ว!", + "agentManager.documents.title": "เอกสาร", + "agentManager.documents.toggle": "สลับแผงเอกสาร", + "agentManager.documents.tabs": "เอกสารที่เปิดอยู่", + "agentManager.documents.empty": "เปิดไฟล์จากแชทหรือ diff เพื่อดูตัวอย่างที่นี่", + "agentManager.documents.loading": "กำลังโหลดเอกสาร...", + "agentManager.documents.preview": "ดูตัวอย่างเอกสาร", + "agentManager.documents.source": "แสดงซอร์สโค้ด", + "agentManager.documents.comments": "{{count}} ความคิดเห็น", "agentManager.import.pullRequest": "คำขอรวมโค้ด", "agentManager.import.pastePrUrl": "วาง URL ของ PR...", "agentManager.import.open": "เปิด", 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 92941842ea..d35af80614 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -226,6 +226,14 @@ export const dict = { "agentManager.review.imageUnavailable": "Bu oturum anlık görüntüsü için görsel önizlemesi kullanılamıyor.", "agentManager.review.endOfLongDiff": "Sonuna geldiniz!", + "agentManager.documents.title": "Belgeler", + "agentManager.documents.toggle": "Belge panelini aç/kapat", + "agentManager.documents.tabs": "Açık belgeler", + "agentManager.documents.empty": "Burada önizlemek için sohbetten veya diff üzerinden bir dosya açın.", + "agentManager.documents.loading": "Belge yükleniyor...", + "agentManager.documents.preview": "Belgeyi önizle", + "agentManager.documents.source": "Kaynak kodu göster", + "agentManager.documents.comments": "{{count}} yorum", "agentManager.import.pullRequest": "Çekme İsteği", "agentManager.import.pastePrUrl": "PR URL'sini yapıştırın...", "agentManager.import.open": "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 df1a71373d..8bac6ffc16 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -227,6 +227,14 @@ export const dict = { "agentManager.review.imageUnavailable": "Попередній перегляд зображення недоступний для цього знімка сеансу.", "agentManager.review.endOfLongDiff": "Ви дійшли до кінця!", + "agentManager.documents.title": "Документи", + "agentManager.documents.toggle": "Показати або приховати панель документів", + "agentManager.documents.tabs": "Відкриті документи", + "agentManager.documents.empty": "Відкрийте файл із чату або diff, щоб переглянути його тут.", + "agentManager.documents.loading": "Завантаження документа...", + "agentManager.documents.preview": "Попередній перегляд документа", + "agentManager.documents.source": "Показати вихідний код", + "agentManager.documents.comments": "{{count}} коментарів", "agentManager.import.pullRequest": "Пул-реквест", "agentManager.import.pastePrUrl": "Вставте URL PR...", "agentManager.import.open": "Відкрити", 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 21b5a33dd0..3922b7eb19 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -211,6 +211,14 @@ export const dict = { "agentManager.review.imageUnavailable": "此会话快照无法提供图片预览。", "agentManager.review.endOfLongDiff": "你已经到末尾了!", + "agentManager.documents.title": "文档", + "agentManager.documents.toggle": "切换文档面板", + "agentManager.documents.tabs": "打开的文档", + "agentManager.documents.empty": "从聊天或 diff 中打开文件,即可在此处预览。", + "agentManager.documents.loading": "正在加载文档...", + "agentManager.documents.preview": "预览文档", + "agentManager.documents.source": "显示源代码", + "agentManager.documents.comments": "{{count}} 条评论", "agentManager.import.pullRequest": "拉取请求", "agentManager.import.pastePrUrl": "粘贴 PR URL...", "agentManager.import.open": "打开", 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 d96ed189fe..4f9fd01972 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -211,6 +211,14 @@ export const dict = { "agentManager.review.imageUnavailable": "此工作階段快照無法預覽圖片。", "agentManager.review.endOfLongDiff": "你已經到最後了!", + "agentManager.documents.title": "文件", + "agentManager.documents.toggle": "切換文件面板", + "agentManager.documents.tabs": "已開啟的文件", + "agentManager.documents.empty": "從聊天或 diff 中開啟檔案,即可在此處預覽。", + "agentManager.documents.loading": "正在載入文件...", + "agentManager.documents.preview": "預覽文件", + "agentManager.documents.source": "顯示原始碼", + "agentManager.documents.comments": "{{count}} 則留言", "agentManager.import.pullRequest": "提取請求", "agentManager.import.pastePrUrl": "貼上 PR URL...", "agentManager.import.open": "開啟", 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..f1d87f7e33 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", + Documents = "documents", } function viewportWidth(viewport: number): number { diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx index 0811537142..fdc7573008 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx @@ -46,6 +46,7 @@ const DiffViewerContent: Component = () => { const [loadingFiles, setLoadingFiles] = createSignal>(new Set()) const [availableSources, setAvailableSources] = createSignal([]) const [currentSourceId, setCurrentSourceId] = createSignal(undefined) + const [initialFile, setInitialFile] = createSignal(undefined) const [capabilities, setCapabilities] = createSignal(undefined) const [notice, setNotice] = createSignal(undefined) const [branches, setBranches] = createSignal([]) @@ -136,6 +137,14 @@ const DiffViewerContent: Component = () => { setMarkdown(msg.render) return } + if ((msg as { type: string; file?: string }).type === "diffViewer.initialFile") { + setInitialFile((msg as { file?: string }).file) + return + } + if ((msg as { type: string; render?: boolean }).type === "diffViewer.initialMarkdown") { + setMarkdown((msg as { render?: boolean }).render === true) + return + } if (msg.type === "setAvailableSources") { setAvailableSources(msg.descriptors) setCurrentSourceId(msg.currentId) @@ -266,6 +275,7 @@ const DiffViewerContent: Component = () => { onOpenFile={(relativePath) => { post({ type: "openFile", filePath: relativePath }) }} + initialFile={initialFile()} onRevertFile={(file) => { markReverting(file, true) post({ type: "diffViewer.revertFile", file }) diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index 04eff9c0d3..f15f6e617d 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -91,6 +91,7 @@ interface FullScreenDiffViewProps { onMarkdownRenderChange?: (render: boolean) => void onRequestDiff?: (file: string) => void onOpenFile?: (relativePath: string, line?: number) => void + initialFile?: string onRevertFile?: (file: string) => void revertingFiles?: Set activeTerminalId?: string @@ -202,6 +203,7 @@ export const FullScreenDiffView: Component = (props) => let nextId = 0 let draftMeta: AnnotationMeta | null = composer().draft let editMeta: AnnotationMeta | null = composer().edit + let initialFileKey: string | undefined let rootRef: HTMLDivElement | undefined const [scroller, setScroller] = createSignal() const [virtualizer, setVirtualizer] = createSignal() @@ -255,6 +257,18 @@ export const FullScreenDiffView: Component = (props) => focusRoot() } + createEffect( + on( + () => [props.sessionKey, props.diffs, props.initialFile] as const, + ([key, diffs, initial]) => { + if (!initial || !diffs.some((diff) => diff.file === initial)) return + const next = `${key ?? ""}:${initial}` + if (initialFileKey === next) return + initialFileKey = next + setActiveFile(initial) + }, + ), + ) createEffect( on( () => props.sessionKey, diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx index f0a5c0288e..70324d1ca0 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx @@ -145,12 +145,16 @@ export const MarkdownAnnotationLayer: Component = } const paneBox = pane.getBoundingClientRect() + let bottom = 0 for (const anchor of list) { const box = anchor.element.getBoundingClientRect() const row = document.createElement("div") row.className = "am-markdown-target" - row.style.top = `${box.top - paneBox.top}px` - row.style.height = `${Math.max(20, box.height)}px` + const height = Math.max(20, box.height) + const top = Math.max(box.top - paneBox.top, bottom) + row.style.top = `${top}px` + row.style.height = `${height}px` + bottom = top + height if (props.enableGutterUtility) { const button = document.createElement("button") diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownDiffView.tsx index 3627809ccd..78533670ec 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownDiffView.tsx @@ -24,7 +24,7 @@ export function isMarkdownFile(file: string): boolean { return /\.(md|mdx|markdown)$/i.test(file) } -interface PaneProps { +export interface MarkdownPaneProps { title?: string text: string side: AnnotationSide @@ -36,7 +36,7 @@ interface PaneProps { onLineNumberClick: ((event: { annotationSide: AnnotationSide; lineNumber: number }) => void) | undefined } -const MarkdownPane: Component = (props) => { +export const MarkdownPane: Component = (props) => { let pane: HTMLElement | undefined let body: HTMLDivElement | undefined const interactive = () => diff --git a/packages/kilo-vscode/webview-ui/documents/DocumentPanel.tsx b/packages/kilo-vscode/webview-ui/documents/DocumentPanel.tsx new file mode 100644 index 0000000000..c3dfcf0a17 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/documents/DocumentPanel.tsx @@ -0,0 +1,364 @@ +import { Dynamic } from "solid-js/web" +import { Component, Show, Accessor, createMemo, createSignal, createEffect, on } from "solid-js" +import { MarkdownPane } from "../diff-viewer/MarkdownDiffView" +import { isMarkdownPath, type DocumentData, type DocumentTab } from "./state" +import { InspectorTabStrip } from "../agent-manager/InspectorTabStrip" +import { SortableClosableTab } from "../agent-manager/ClosableTab" +import { useCodeComponent } from "@kilocode/kilo-ui/context/code" +import { FileIcon } from "@kilocode/kilo-ui/file-icon" +import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Button } from "@kilocode/kilo-ui/button" +import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" +import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pierre/diffs" +import type { WorktreeFileDiff } from "../src/types/messages" +import type { ReviewComment } from "../diff-viewer/review-comments" +import { + buildFileAnnotations, + buildReviewAnnotation, + createReviewComposer, + sendReviewComments, + type AnnotationLabels, + type AnnotationMeta, + type ReviewComposer, + type ReviewDraft, +} from "../diff-viewer/review-annotations" +import { lineCount } from "../diff-viewer/review-comments" +import { useLanguage } from "../src/context/language" + +export interface DocumentPanelProps { + tabs: Accessor + active: Accessor + getData: (file: string) => DocumentData | undefined + comments: ReviewComment[] + onCommentsChange: (comments: ReviewComment[]) => void + onSelect: (id: string) => void + onClose: (id: string) => void + onCloseOthers: (id: string) => void + onReorder: (from: string, to: string) => void + onOpenFile: (file: string, line?: number, column?: number) => void + onClosePanel: () => void + onSendAll?: () => void + activeTerminalId?: string + visible: Accessor +} + +function pathName(file: string): string { + return file.slice(file.lastIndexOf("/") + 1) +} + +function virtualDiff(file: string, content: string): WorktreeFileDiff { + return { + file, + before: "", + after: content, + additions: lineCount(content), + deletions: 0, + status: "added", + } +} + +function sendAllKeybind(t: (key: string) => string): string { + return typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) + ? t("agentManager.review.sendAllShortcut.mac") + : t("agentManager.review.sendAllShortcut.other") +} + +function handleSendAllKeyDown(event: KeyboardEvent, comments: ReviewComment[], send: () => void): void { + if (event.key !== "Enter" || (!event.metaKey && !event.ctrlKey)) return + const target = event.target + if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) return + if (target instanceof HTMLElement && target.isContentEditable) return + if (comments.length === 0) return + event.preventDefault() + event.stopPropagation() + send() +} + +export const DocumentPanel: Component = (props) => { + const { t } = useLanguage() + const code = useCodeComponent() + const [source, setSource] = createSignal(false) + const [draft, setDraft] = createSignal(null) + const [editing, setEditing] = createSignal(null) + const composer: ReviewComposer = createReviewComposer() + let draftMeta: AnnotationMeta | null = null + let editMeta: AnnotationMeta | null = null + let nextId = 0 + let rootRef: HTMLElement | undefined + + const selected = createMemo(() => { + const id = props.active() + return props.tabs().find((tab) => tab.id === id) + }) + const data = () => { + const tab = selected() + return tab ? props.getData(tab.file) : undefined + } + const file = () => selected()?.file ?? "" + const content = () => data()?.content ?? "" + const diff = () => virtualDiff(file(), content()) + const labels = (): AnnotationLabels => ({ + commentOnLine: (line) => t("agentManager.review.commentOnLine", { line }), + editCommentOnLine: (line) => t("agentManager.review.editCommentOnLine", { line }), + placeholder: t("agentManager.review.commentPlaceholder"), + cancel: t("common.cancel"), + comment: t("agentManager.review.commentAction"), + send: t("prompt.action.send"), + save: t("common.save"), + sendToChat: t("agentManager.review.sendToChat"), + edit: t("common.edit"), + delete: t("common.delete"), + }) + + const updateComments = (next: ReviewComment[]) => props.onCommentsChange(next) + const comments = () => props.comments.filter((item) => item.file === file()) + const focusRoot = () => { + requestAnimationFrame(() => { + requestAnimationFrame(() => rootRef?.focus()) + }) + } + const addComment = (path: string, side: AnnotationSide, line: number, text: string, selectedText: string) => { + updateComments([ + ...props.comments, + { id: `doc-${++nextId}-${Date.now()}`, file: path, side, line, comment: text, selectedText }, + ]) + setDraft(null) + draftMeta = null + composer.draft = null + focusRoot() + } + const sendComment = (path: string, side: AnnotationSide, line: number, text: string, selectedText: string) => { + sendReviewComments( + [{ id: `doc-${++nextId}-${Date.now()}`, file: path, side, line, comment: text, selectedText }], + props.activeTerminalId, + ) + setDraft(null) + draftMeta = null + composer.draft = null + focusRoot() + } + const updateComment = (id: string, text: string) => { + updateComments(props.comments.map((item) => (item.id === id ? { ...item, comment: text } : item))) + setEditing(null) + editMeta = null + composer.edit = null + focusRoot() + } + const deleteComment = (id: string) => { + updateComments(props.comments.filter((item) => item.id !== id)) + setEditing(null) + editMeta = null + composer.edit = null + focusRoot() + } + const cancelDraft = () => { + setDraft(null) + draftMeta = null + composer.draft = null + focusRoot() + } + const annotations = (): DiffLineAnnotation[] => { + const result = buildFileAnnotations(file(), comments(), editing(), draft(), draftMeta, editMeta) + draftMeta = result.draftMeta + editMeta = result.editMeta + composer.draft = draft() ? draftMeta : null + composer.edit = editing() ? editMeta : null + return result.annotations + } + const renderAnnotation = (annotation: DiffLineAnnotation) => + buildReviewAnnotation(annotation, { + diffs: [diff()], + editing: editing(), + setEditing: (id) => setEditing(id), + addComment, + sendComment, + updateComment, + deleteComment, + cancelDraft, + labels: labels(), + activeTerminalId: props.activeTerminalId, + }) + const gutter = (range: SelectedLineRange) => { + if (draft()) return + const side: AnnotationSide = "additions" + const next = { file: file(), side, line: range.start, endLine: range.end } + draftMeta = { type: "draft", comment: null, ...next } + composer.draft = draftMeta + setDraft(next) + } + const sendAll = () => { + if (props.comments.length === 0) return + sendReviewComments(props.comments, props.activeTerminalId) + updateComments([]) + if (props.onSendAll) props.onSendAll() + else focusRoot() + } + + createEffect( + on( + () => [file(), content(), props.comments, data()?.loading, data()?.error] as const, + ([path, text, current]) => { + if (!path) return + if (data()?.loading || data()?.error || data()?.content === undefined) return + const max = lineCount(text) + const valid = current.filter((item) => item.file !== path || (item.line >= 1 && item.line <= max)) + if (valid.length !== current.length) updateComments(valid) + const currentDraft = draft() + if (currentDraft && currentDraft.file === path && currentDraft.line > max) cancelDraft() + }, + { defer: true }, + ), + ) + createEffect( + on( + () => [file(), props.tabs()] as const, + () => { + setDraft(null) + setEditing(null) + draftMeta = null + editMeta = null + composer.draft = null + composer.edit = null + }, + { defer: true }, + ), + ) + + const close = (id: string, focus: { restore: () => void }) => { + props.onClose(id) + if (props.tabs().length > 0) focus.restore() + } + + return ( +
handleSendAllKeyDown(event, props.comments, sendAll)} + tabIndex={-1} + ref={rootRef} + > +
+
+ + {t("agentManager.documents.title")} + {props.tabs().length} +
+
+ + + setSource((value) => !value)} + /> + + + props.onOpenFile(file(), selected()?.line, selected()?.column)} + /> + + + +
+
+ props.tabs().map((tab) => tab.id)} + active={props.active} + label={t("agentManager.documents.tabs")} + overlay={(id) => props.tabs().find((tab) => tab.id === id)?.file ?? ""} + onSelect={props.onSelect} + onReorder={props.onReorder} + renderTab={(id, api) => { + const tab = props.tabs().find((item) => item.id === id)! + return ( + } + showKeybind={false} + active={props.active() === id} + role="tab" + selected={props.active() === id} + tabIndex={props.active() === id ? 0 : -1} + onKeyDown={(event) => api.focus.key(id, event)} + onSelect={() => props.onSelect(id)} + onMiddleClick={(event) => { + if (event.button !== 1) return + event.preventDefault() + close(id, api.focus) + }} + onClose={() => close(id, api.focus)} + onCloseOthers={() => props.onCloseOthers(id)} + /> + ) + }} + /> + {t("agentManager.documents.empty")}
}> + +
{t("agentManager.documents.loading")}
+
+ {(error) =>
{error()}
}
+ +
+ {file()} +
+
+ +
+ + } + > + props.onOpenFile(file(), event.lineNumber)} + /> + +
+
+
+ 0}> + + + + ) +} diff --git a/packages/kilo-vscode/webview-ui/documents/index.tsx b/packages/kilo-vscode/webview-ui/documents/index.tsx new file mode 100644 index 0000000000..bde3e404a4 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/documents/index.tsx @@ -0,0 +1,82 @@ +import { onCleanup, onMount, type Component, createSignal } from "solid-js" +import { render } from "solid-js/web" +import "@kilocode/kilo-ui/styles" +import "../src/styles/chat.css" +import "../agent-manager/agent-manager.css" +import "../agent-manager/agent-manager-review.css" +import { ProviderShell } from "../src/context/provider-shell" +import { useVSCode } from "../src/context/vscode" +import { DocumentPanel } from "./DocumentPanel" +import { createDocumentComments, createDocuments, type DocumentMessage } from "./state" + +const App: Component = () => { + const vscode = useVSCode() + const [scope, setScope] = createSignal(null) + const [session, setSession] = createSignal(null) + const docs = createDocuments(vscode, scope, session, (id, file, contextKey) => + vscode.postMessage({ type: "document.request", sessionId: id, file, contextKey }), + ) + const comments = createDocumentComments(scope) + const [visible, setVisible] = createSignal(true) + + const open = (message: { sessionId?: string; contextKey: string; file: string; line?: number; column?: number }) => { + setScope(message.contextKey) + setSession(message.sessionId ?? null) + docs.open(message.file, message.sessionId ?? "", message.line, message.column) + setVisible(true) + } + + onMount(() => { + const message = vscode.onMessage((item) => { + if (item.type === "document.open") { + open(item) + return + } + if (item.type === "document.result") docs.onMessage(item as DocumentMessage) + }) + const review = (event: MessageEvent) => { + if (event.data?.type !== "appendReviewComments" || !Array.isArray(event.data.comments)) return + vscode.postMessage({ + type: "document.sendComments", + comments: event.data.comments, + autoSend: !!event.data.autoSend, + }) + } + window.addEventListener("message", review) + onCleanup(() => { + message() + window.removeEventListener("message", review) + }) + }) + + return ( + vscode.postMessage({ type: "document.openFile", file, line, column })} + onClosePanel={() => { + setVisible(false) + vscode.postMessage({ type: "document.close" }) + }} + visible={visible} + /> + ) +} + +const root = document.getElementById("root") +if (!root) throw new Error("Root element not found") +render( + () => ( + + + + ), + root, +) diff --git a/packages/kilo-vscode/webview-ui/documents/state.ts b/packages/kilo-vscode/webview-ui/documents/state.ts new file mode 100644 index 0000000000..c78307898c --- /dev/null +++ b/packages/kilo-vscode/webview-ui/documents/state.ts @@ -0,0 +1,214 @@ +import { createSignal, onCleanup, onMount, type Accessor } from "solid-js" +import type { useVSCode } from "../src/context/vscode" +import type { ReviewComment } from "../diff-viewer/review-comments" +export interface DocumentMessage { + type?: "document.result" | "agentManager.document" + sessionId: string + contextKey?: string + file: string + requestedFile?: string + content?: string + kind?: "text" | "image" + mime?: string + data?: string + error?: string +} + +export interface DocumentTab { + id: string + file: string + sessionId?: string + line?: number + column?: number +} + +export interface DocumentData { + file: string + content?: string + kind?: "text" | "image" + mime?: string + data?: string + error?: string + loading: boolean +} + +function key(context: string, file: string): string { + return `${context}:${file}` +} + +export function isMarkdownPath(file: string): boolean { + return /\.(md|mdx|markdown)$/i.test(file) +} + +export function createDocuments( + vscode: ReturnType, + context: Accessor, + session: Accessor = context, + send: (sessionId: string, file: string, contextKey: string) => void = (sessionId, file, contextKey) => + vscode.postMessage({ type: "agentManager.requestDocument", sessionId, file, contextKey }), +) { + const [tabs, setTabs] = createSignal>({}) + const [active, setActive] = createSignal>({}) + const [data, setData] = createSignal>({}) + + const current = () => context() ?? "" + const list = () => tabs()[current()] ?? [] + const selected = () => active()[current()] + const document = (file: string) => data()[key(current(), file)] + + const request = (file: string, sessionId = session() ?? "", contextKey = current()) => { + const ctx = current() + if (!ctx) return + const id = key(ctx, file) + setData((prev) => ({ ...prev, [id]: { ...(prev[id] ?? { file }), file, loading: true, error: undefined } })) + send(sessionId, file, contextKey) + } + + const open = (file: string, sessionId = session() ?? "", line?: number, column?: number) => { + const ctx = current() + if (!ctx || !file) return + setTabs((prev) => { + const list = prev[ctx] ?? [] + const id = key(ctx, file) + if (list.some((tab) => tab.id === id)) { + return { ...prev, [ctx]: list.map((tab) => (tab.id === id ? { ...tab, sessionId, line, column } : tab)) } + } + return { ...prev, [ctx]: [...list, { id, file, sessionId, line, column }] } + }) + setActive((prev) => ({ ...prev, [ctx]: key(ctx, file) })) + request(file, sessionId) + return true + } + + const select = (id: string) => { + const ctx = current() + if (!ctx) return + const tab = (tabs()[ctx] ?? []).find((item) => item.id === id) + if (!tab) return + setActive((prev) => ({ ...prev, [ctx]: id })) + if (!document(tab.file)) request(tab.file, tab.sessionId) + } + + const close = (id: string) => { + const ctx = current() + const list = tabs()[ctx] ?? [] + const index = list.findIndex((tab) => tab.id === id) + if (index < 0) return + const next = list.filter((tab) => tab.id !== id) + setTabs((prev) => ({ ...prev, [ctx]: next })) + if (active()[ctx] !== id) return + const target = next[Math.min(index, next.length - 1)] + setActive((prev) => ({ ...prev, [ctx]: target?.id })) + } + + const closeOthers = (id: string) => { + const ctx = current() + const tab = (tabs()[ctx] ?? []).find((item) => item.id === id) + if (!tab) return + setTabs((prev) => ({ ...prev, [ctx]: [tab] })) + setActive((prev) => ({ ...prev, [ctx]: id })) + } + + const reorder = (from: string, to: string) => { + const ctx = current() + const list = tabs()[ctx] ?? [] + const fromIndex = list.findIndex((tab) => tab.id === from) + const toIndex = list.findIndex((tab) => tab.id === to) + if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return + const next = [...list] + const [item] = next.splice(fromIndex, 1) + next.splice(toIndex, 0, item!) + setTabs((prev) => ({ ...prev, [ctx]: next })) + } + + const onMessage = (message: DocumentMessage) => { + const id = key(message.contextKey ?? message.sessionId, message.requestedFile ?? message.file) + setData((prev) => ({ + ...prev, + [id]: { + file: message.file, + content: message.content, + kind: message.kind, + mime: message.mime, + data: message.data, + error: message.error, + loading: false, + }, + })) + } + + return { tabs: list, active: selected, document, open, select, close, closeOthers, reorder, onMessage, request } +} + +export function createDocumentComments(context: Accessor) { + const [byContext, setByContext] = createSignal>({}) + const comments = () => { + const ctx = context() + return ctx ? (byContext()[ctx] ?? []) : [] + } + const setComments = (value: ReviewComment[]) => { + const ctx = context() + if (!ctx) return + setByContext((prev) => ({ ...prev, [ctx]: value })) + } + return { comments, setComments } +} + +export function createDocumentInspector( + vscode: ReturnType, + context: Accessor, + project: Accessor, + isOpen: Accessor, + openPanel: () => void, + closePanel: () => void, +) { + const scope = () => `${project() ?? "single"}:${context() ?? ""}` + const documents = createDocuments(vscode, scope, context) + const comments = createDocumentComments(scope) + const open = (file?: string, sessionId?: string, line?: number, column?: number) => { + if (!file) { + openPanel() + return true + } + const sid = sessionId ?? context() + if (!sid || !documents.open(file, sid, line, column)) return false + openPanel() + return true + } + onMount(() => { + const handler = (event: Event) => handleDocumentOpen(event, open) + const message = vscode.onMessage((item) => { + if (item.type === "document.result" || item.type === "agentManager.document") documents.onMessage(item) + }) + window.addEventListener("kilo:open-file", handler) + onCleanup(() => { + window.removeEventListener("kilo:open-file", handler) + message() + }) + }) + // The toolbar button is a way back to already-open documents, not a way to + // open an empty panel: documents arrive from a file reference or a diff row. + // Tabs are keyed per worktree, so this hides itself on a worktree with none, + // and stays visible while the panel is open so it can still be toggled shut. + const available = () => documents.tabs().length > 0 || isOpen() + const openFile = (file: string, line?: number, column?: number) => { + const sessionId = context() + if (sessionId) vscode.postMessage({ type: "agentManager.openFile", sessionId, filePath: file, line, column }) + } + const toggle = () => (isOpen() ? closePanel() : open()) + return { documents, comments, open, openFile, toggle, available, isOpen, scope } +} + +export function handleDocumentOpen( + event: Event, + open: (file: string, sessionId?: string, line?: number, column?: number) => boolean, +): void { + const detail = (event as CustomEvent<{ filePath?: unknown; sessionID?: unknown; line?: unknown; column?: unknown }>) + .detail + const file = detail?.filePath + if (typeof file !== "string" || !file) return + const sessionId = typeof detail.sessionID === "string" ? detail.sessionID : undefined + const line = typeof detail.line === "number" ? detail.line : undefined + const column = typeof detail.column === "number" ? detail.column : undefined + if (open(file, sessionId, line, column)) event.preventDefault() +} diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index a0162ce04e..53cac0212b 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -121,6 +121,11 @@ export const DataBridge: Component<{ children: any }> = (props) => { } const open = (filePath: string, line?: number, column?: number, sessionID?: string) => { + const event = new CustomEvent("kilo:open-file", { + cancelable: true, + detail: { filePath, line, column, sessionID }, + }) + if (!window.dispatchEvent(event)) return vscode.postMessage({ type: "openFile", filePath, line, column, sessionID }) } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 3b42083c99..ce9b9f25e7 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -76,6 +76,7 @@ function PlanExitCard(props: { part: ToolPart }) {
{label()}{" "} + {language.t("ui.patch.action.plan")} {display()} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx index cfb85b31df..56412887c4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx @@ -6,10 +6,9 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Markdown } from "@kilocode/kilo-ui/markdown" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useDialog } from "@kilocode/kilo-ui/context/dialog" +import { isPRReviewComment } from "../../../../src/shared/review-comments" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" -import { useWorktreeMode } from "../../context/worktree-mode" -import { isPRReviewComment } from "../../../../src/shared/review-comments" import type { ReviewCommentEntry } from "../../types/messages" import { fileName } from "./prompt-input-utils" @@ -24,13 +23,9 @@ interface ReviewCommentsProps { export const ReviewComments: Component = (props) => { const language = useLanguage() const vscode = useVSCode() - const worktree = useWorktreeMode() const dialog = useDialog() const author = (item: ReviewCommentEntry) => (isPRReviewComment(item) ? item.author : "") - const side = (item: ReviewCommentEntry) => { - if (isPRReviewComment(item)) return "" - return item.side === "deletions" ? "-" : "+" - } + const side = (item: ReviewCommentEntry) => (isPRReviewComment(item) ? "" : item.side === "deletions" ? "-" : "+") const line = (item: ReviewCommentEntry) => (item.line ? `${side(item)}${item.line}` : "") const body = (item: ReviewCommentEntry) => (isPRReviewComment(item) ? item.body : item.comment) const snippet = (item: ReviewCommentEntry) => (isPRReviewComment(item) ? item.diffHunk : item.selectedText) @@ -42,17 +37,18 @@ export const ReviewComments: Component = (props) => { const open = (item: ReviewCommentEntry) => { if (!item.file) return - if (worktree && props.sessionID) { + const event = new CustomEvent("kilo:open-file", { + cancelable: true, + detail: { filePath: item.file, line: item.line, column: 1, sessionID: props.sessionID }, + }) + if (window.dispatchEvent(event)) vscode.postMessage({ - type: "agentManager.openFile", - sessionId: props.sessionID, + type: "openFile", filePath: item.file, line: item.line, + column: 1, + sessionID: props.sessionID, }) - dialog.close() - return - } - vscode.postMessage({ type: "openFile", filePath: item.file, line: item.line, column: 1 }) dialog.close() } @@ -134,7 +130,11 @@ export const ReviewComments: Component = (props) => { {(item) => (
-