mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge pull request #13245 from Kilo-Org/implement-markdown-file-viewer-sidebar
feat(vscode): add Agent Manager document inspector
This commit is contained in:
@@ -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.
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5a9f984fc6d004e151c6e82bd095e9038423d8fa6443a91bf1834c44f0af62ce
|
||||
size 748576
|
||||
oid sha256:96a537574ebee768dab6914ffb449665d6930d2d577bf1dc42eb38d975e9be10
|
||||
size 749704
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</For>
|
||||
<Show when={images().length > 0}>
|
||||
<Show when={images().length > 0}>
|
||||
<div data-slot="tool-read-images">
|
||||
<For each={images()}>
|
||||
{(file) => (
|
||||
@@ -2941,7 +2954,9 @@ ToolRegistry.register({
|
||||
|
||||
<span
|
||||
data-slot="apply-patch-filename"
|
||||
classList={{ clickable: !!data.openFile }}
|
||||
classList={{
|
||||
clickable: !!data.openFile,
|
||||
}}
|
||||
onClick={(e: MouseEvent) => {
|
||||
if (!data.openFile) return
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, Context>()
|
||||
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<string, unknown>)),
|
||||
panel.onDidDispose(() => {
|
||||
this.panel = undefined
|
||||
this.pending = undefined
|
||||
this.currentKey = ""
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private message(message: Record<string, unknown>): 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<string, unknown>): 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -464,6 +464,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private createWorktreeHandler: ((baseBranch?: string, branchName?: string) => Promise<void>) | 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<string, unknown> {
|
||||
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),
|
||||
})
|
||||
|
||||
@@ -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<void> {
|
||||
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<string, unknown> | null | undefined {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void> {
|
||||
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}`)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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." }
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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<typeof createDocuments>[0]
|
||||
const [scope, setScope] = createSignal("project-a:wt-a")
|
||||
const [session, setSession] = createSignal<string | null>("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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -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." })
|
||||
})
|
||||
})
|
||||
@@ -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<Setter<SectionState[]>>[0]) => registry.active().setSections(v)
|
||||
|
||||
// rAF coalescing for resize handlers — at most one signal write per frame
|
||||
let sidebarRaf: number | undefined
|
||||
let pendingSidebarWidth: number | undefined
|
||||
|
||||
@@ -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<string | undefined>()
|
||||
|
||||
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<Record<string, string[]>> = (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)}
|
||||
/>
|
||||
</Show>
|
||||
<DocumentPanelHost
|
||||
inspector={documentInspector}
|
||||
onClosePanel={() => setSidePanel(null)}
|
||||
onSendAll={focusCtl.focus}
|
||||
activeTerminalId={terms.activeId()}
|
||||
visible={documentInspector.isOpen}
|
||||
/>
|
||||
<SideTerminalPanel
|
||||
state={terms}
|
||||
contextKey={terms.sideKey}
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface ClosableTabProps {
|
||||
label: Value<string>
|
||||
tooltip: Value<string>
|
||||
icon: Value<TabIcon>
|
||||
/** Renders instead of `icon`, for tabs that need a per-filetype glyph. */
|
||||
iconNode?: Value<JSX.Element>
|
||||
iconStatus?: Value<"success" | "failure" | undefined>
|
||||
class?: string
|
||||
focused?: boolean
|
||||
@@ -71,8 +73,15 @@ export const ClosableTabChrome: Component<ClosableTabProps> = (props) => {
|
||||
>
|
||||
<span class="am-tab-title">
|
||||
<span class="am-tab-icon" data-run-status={status()}>
|
||||
<Show when={icon() === "spinner"} fallback={<Icon name={icon() as IconProps["name"]} size="small" />}>
|
||||
<Spinner class="am-tab-spinner" />
|
||||
<Show
|
||||
when={props.iconNode}
|
||||
fallback={
|
||||
<Show when={icon() === "spinner"} fallback={<Icon name={icon() as IconProps["name"]} size="small" />}>
|
||||
<Spinner class="am-tab-spinner" />
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
{value(props.iconNode!)}
|
||||
</Show>
|
||||
</span>
|
||||
<span class="am-tab-label">{label()}</span>
|
||||
@@ -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}
|
||||
|
||||
@@ -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<string>
|
||||
activeTerminalId?: string
|
||||
@@ -650,6 +651,20 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={isMarkdownFile(diff.file) && props.onOpenDocument && !isDeleted()}>
|
||||
<Tooltip value={t("agentManager.documents.preview")} placement="top">
|
||||
<IconButton
|
||||
icon="book-open-check"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={t("agentManager.documents.preview")}
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
props.onOpenDocument?.(diff.file)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={props.onRevertFile && props.canRevert !== false}>
|
||||
<Tooltip value={t("agentManager.diff.revertFile")} placement="top">
|
||||
<IconButton
|
||||
|
||||
@@ -58,6 +58,9 @@ export interface TabBarProps {
|
||||
prStatus: () => 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<TabBarProps> = (props) => (
|
||||
</Tooltip>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.documentsAvailable()}>
|
||||
<Tooltip value={props.t("agentManager.documents.toggle")} placement="bottom">
|
||||
<IconButton
|
||||
icon="book-open-check"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.t("agentManager.documents.toggle")}
|
||||
class={props.documentsOpen() ? "am-tab-diff-btn-active" : ""}
|
||||
onClick={props.onToggleDocuments}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={props.subagentsAvailable()}>
|
||||
<Tooltip value="Subagents" placement="bottom">
|
||||
<IconButton
|
||||
|
||||
@@ -1471,6 +1471,26 @@ html[data-theme="kilo-vscode"]
|
||||
background: color-mix(in srgb, var(--surface-interactive-base) 10%, transparent);
|
||||
}
|
||||
|
||||
.am-document-panel .am-document-tab {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.am-document-panel .am-document-tab:hover,
|
||||
.am-document-panel .am-document-tab:focus-within,
|
||||
.am-document-panel .am-document-tab.am-tab-active {
|
||||
border: 0;
|
||||
background: var(--button-ghost-hover, var(--surface-base-hover, rgba(128, 128, 128, 0.2)));
|
||||
}
|
||||
|
||||
/* The tab shows the real per-filetype glyph, so it keeps its own colors and
|
||||
only needs a consistent box size next to the label. */
|
||||
.am-document-panel .am-document-tab .am-document-tab-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-tab-target {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1932,6 +1952,7 @@ body.am-wt-dragging-active * {
|
||||
}
|
||||
|
||||
.am-diff-panel:focus,
|
||||
.am-document-panel:focus,
|
||||
.am-review-layout:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -5066,6 +5087,113 @@ body.vscode-high-contrast-light {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Document inspector. It remains mounted while another inspector mode is active
|
||||
so loaded document tabs and staged Markdown comments survive mode switches. */
|
||||
.am-document-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
background: var(--surface-base);
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.am-document-panel-visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.am-document-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 4px 0 8px;
|
||||
flex-shrink: 0;
|
||||
background: var(--surface-base);
|
||||
}
|
||||
|
||||
.am-document-heading,
|
||||
.am-document-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.am-document-heading {
|
||||
color: var(--text-strong);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.am-document-count {
|
||||
color: var(--text-weak);
|
||||
font-size: var(--kilo-font-size-11);
|
||||
}
|
||||
|
||||
.am-document-empty,
|
||||
.am-document-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
min-height: 96px;
|
||||
padding: 16px;
|
||||
color: var(--text-weak);
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.am-document-error {
|
||||
color: var(--text-error-base);
|
||||
}
|
||||
|
||||
.am-document-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.am-document-code {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.am-document-image-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.am-document-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.am-document-panel .am-inspector-tabs {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* The document body must not sit inside a padded wrapper: the Markdown pane
|
||||
paints the editor background, so any inset would expose the lighter panel
|
||||
surface around it and read as a border. The pane inherits the panel surface
|
||||
and fills the panel edge to edge instead. */
|
||||
.am-document-panel .am-markdown-pane,
|
||||
.am-document-panel .am-markdown-diff {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.am-subagent-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Accessor, Component } from "solid-js"
|
||||
import { DocumentPanel } from "../../documents/DocumentPanel"
|
||||
import { createDocumentInspector } from "../../documents/state"
|
||||
|
||||
interface Props {
|
||||
inspector: ReturnType<typeof createDocumentInspector>
|
||||
onClosePanel: () => void
|
||||
onSendAll?: () => void
|
||||
activeTerminalId?: string
|
||||
visible: Accessor<boolean>
|
||||
}
|
||||
|
||||
export const DocumentPanelHost: Component<Props> = (props) => (
|
||||
<DocumentPanel
|
||||
tabs={props.inspector.documents.tabs}
|
||||
active={props.inspector.documents.active}
|
||||
getData={props.inspector.documents.document}
|
||||
comments={props.inspector.comments.comments()}
|
||||
onCommentsChange={props.inspector.comments.setComments}
|
||||
onSelect={props.inspector.documents.select}
|
||||
onClose={props.inspector.documents.close}
|
||||
onCloseOthers={props.inspector.documents.closeOthers}
|
||||
onReorder={props.inspector.documents.reorder}
|
||||
onOpenFile={props.inspector.openFile}
|
||||
onClosePanel={props.onClosePanel}
|
||||
onSendAll={props.onSendAll}
|
||||
activeTerminalId={props.activeTerminalId}
|
||||
visible={props.visible}
|
||||
/>
|
||||
)
|
||||
@@ -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": "فتح",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "باز کردن",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "開く",
|
||||
|
||||
@@ -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": "열기",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Открыть",
|
||||
|
||||
@@ -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": "เปิด",
|
||||
|
||||
@@ -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ç",
|
||||
|
||||
@@ -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": "Відкрити",
|
||||
|
||||
@@ -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": "打开",
|
||||
|
||||
@@ -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": "開啟",
|
||||
|
||||
@@ -10,6 +10,7 @@ export enum SidePanel {
|
||||
PR = "pr",
|
||||
Terminal = "terminal",
|
||||
Subagents = "subagents",
|
||||
Documents = "documents",
|
||||
}
|
||||
|
||||
function viewportWidth(viewport: number): number {
|
||||
|
||||
@@ -46,6 +46,7 @@ const DiffViewerContent: Component = () => {
|
||||
const [loadingFiles, setLoadingFiles] = createSignal<Set<string>>(new Set())
|
||||
const [availableSources, setAvailableSources] = createSignal<DiffSourceDescriptor[]>([])
|
||||
const [currentSourceId, setCurrentSourceId] = createSignal<string | undefined>(undefined)
|
||||
const [initialFile, setInitialFile] = createSignal<string | undefined>(undefined)
|
||||
const [capabilities, setCapabilities] = createSignal<DiffSourceCapabilities | undefined>(undefined)
|
||||
const [notice, setNotice] = createSignal<DiffViewerNotice | undefined>(undefined)
|
||||
const [branches, setBranches] = createSignal<BranchInfo[]>([])
|
||||
@@ -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 })
|
||||
|
||||
@@ -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<string>
|
||||
activeTerminalId?: string
|
||||
@@ -202,6 +203,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (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<HTMLDivElement>()
|
||||
const [virtualizer, setVirtualizer] = createSignal<VirtualizerHandle>()
|
||||
@@ -255,6 +257,18 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (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,
|
||||
|
||||
@@ -145,12 +145,16 @@ export const MarkdownAnnotationLayer: Component<MarkdownAnnotationLayerProps> =
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@@ -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<PaneProps> = (props) => {
|
||||
export const MarkdownPane: Component<MarkdownPaneProps> = (props) => {
|
||||
let pane: HTMLElement | undefined
|
||||
let body: HTMLDivElement | undefined
|
||||
const interactive = () =>
|
||||
|
||||
@@ -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<DocumentTab[]>
|
||||
active: Accessor<string | undefined>
|
||||
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<boolean>
|
||||
}
|
||||
|
||||
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<DocumentPanelProps> = (props) => {
|
||||
const { t } = useLanguage()
|
||||
const code = useCodeComponent()
|
||||
const [source, setSource] = createSignal(false)
|
||||
const [draft, setDraft] = createSignal<ReviewDraft | null>(null)
|
||||
const [editing, setEditing] = createSignal<string | null>(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<AnnotationMeta>[] => {
|
||||
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<AnnotationMeta>) =>
|
||||
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 (
|
||||
<section
|
||||
class="am-document-panel"
|
||||
classList={{ "am-document-panel-visible": props.visible() }}
|
||||
aria-label={t("agentManager.documents.title")}
|
||||
aria-hidden={!props.visible()}
|
||||
inert={!props.visible()}
|
||||
onKeyDown={(event) => handleSendAllKeyDown(event, props.comments, sendAll)}
|
||||
tabIndex={-1}
|
||||
ref={rootRef}
|
||||
>
|
||||
<header class="am-document-header">
|
||||
<div class="am-document-heading">
|
||||
<Icon name="book-open-check" size="small" />
|
||||
<span>{t("agentManager.documents.title")}</span>
|
||||
<span class="am-document-count">{props.tabs().length}</span>
|
||||
</div>
|
||||
<div class="am-document-actions">
|
||||
<Show when={selected()}>
|
||||
<Tooltip
|
||||
value={source() ? t("agentManager.documents.preview") : t("agentManager.documents.source")}
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
icon={source() ? "eye" : "code"}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={source() ? t("agentManager.documents.preview") : t("agentManager.documents.source")}
|
||||
onClick={() => setSource((value) => !value)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip value={t("agentManager.diff.openFile")} placement="top">
|
||||
<IconButton
|
||||
icon="go-to-file"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={t("agentManager.diff.openFile")}
|
||||
onClick={() => props.onOpenFile(file(), selected()?.line, selected()?.column)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<IconButton
|
||||
icon="close"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={t("common.close")}
|
||||
onClick={props.onClosePanel}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<InspectorTabStrip
|
||||
ids={() => 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 (
|
||||
<SortableClosableTab
|
||||
id={id}
|
||||
class="am-document-tab"
|
||||
label={pathName(tab.file)}
|
||||
tooltip={tab.file}
|
||||
icon="open-file"
|
||||
iconNode={<FileIcon node={{ path: tab.file, type: "file" }} class="am-document-tab-icon" />}
|
||||
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)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Show when={selected()} fallback={<div class="am-document-empty">{t("agentManager.documents.empty")}</div>}>
|
||||
<Show when={data()?.loading}>
|
||||
<div class="am-document-state">{t("agentManager.documents.loading")}</div>
|
||||
</Show>
|
||||
<Show when={data()?.error}>{(error) => <div class="am-document-state am-document-error">{error()}</div>}</Show>
|
||||
<Show when={!data()?.loading && !data()?.error && data()?.kind === "image"}>
|
||||
<div class="am-document-image-wrap">
|
||||
<img src={`data:${data()?.mime};base64,${data()?.data}`} alt={file()} class="am-document-image" />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!data()?.loading && !data()?.error && data()?.kind !== "image"}>
|
||||
<div class="am-document-content">
|
||||
<Show
|
||||
when={!source() && isMarkdownPath(file())}
|
||||
fallback={
|
||||
<Dynamic component={code} file={{ name: file(), contents: content() }} class="am-document-code" />
|
||||
}
|
||||
>
|
||||
<MarkdownPane
|
||||
text={content()}
|
||||
side="additions"
|
||||
cache={`${file()}:document`}
|
||||
annotations={annotations()}
|
||||
renderAnnotation={renderAnnotation}
|
||||
enableGutterUtility={true}
|
||||
onGutterUtilityClick={gutter}
|
||||
onLineNumberClick={(event) => props.onOpenFile(file(), event.lineNumber)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={props.comments.length > 0}>
|
||||
<div class="am-diff-comments-footer">
|
||||
<span class="am-diff-comments-count">
|
||||
{t("agentManager.documents.comments", { count: props.comments.length })}
|
||||
</span>
|
||||
<TooltipKeybind title={t("agentManager.review.sendAllToChat")} keybind={sendAllKeybind(t)} placement="top">
|
||||
<Button variant="primary" size="small" onClick={sendAll}>
|
||||
{t("agentManager.review.sendAllToChat")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -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<string | null>(null)
|
||||
const [session, setSession] = createSignal<string | null>(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 (
|
||||
<DocumentPanel
|
||||
tabs={docs.tabs}
|
||||
active={docs.active}
|
||||
getData={docs.document}
|
||||
comments={comments.comments()}
|
||||
onCommentsChange={comments.setComments}
|
||||
onSelect={docs.select}
|
||||
onClose={docs.close}
|
||||
onCloseOthers={docs.closeOthers}
|
||||
onReorder={docs.reorder}
|
||||
onOpenFile={(file, line, column) => 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(
|
||||
() => (
|
||||
<ProviderShell.Root>
|
||||
<App />
|
||||
</ProviderShell.Root>
|
||||
),
|
||||
root,
|
||||
)
|
||||
@@ -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<typeof useVSCode>,
|
||||
context: Accessor<string | null>,
|
||||
session: Accessor<string | null> = context,
|
||||
send: (sessionId: string, file: string, contextKey: string) => void = (sessionId, file, contextKey) =>
|
||||
vscode.postMessage({ type: "agentManager.requestDocument", sessionId, file, contextKey }),
|
||||
) {
|
||||
const [tabs, setTabs] = createSignal<Record<string, DocumentTab[]>>({})
|
||||
const [active, setActive] = createSignal<Record<string, string | undefined>>({})
|
||||
const [data, setData] = createSignal<Record<string, DocumentData>>({})
|
||||
|
||||
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<string | null>) {
|
||||
const [byContext, setByContext] = createSignal<Record<string, ReviewComment[]>>({})
|
||||
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<typeof useVSCode>,
|
||||
context: Accessor<string | null>,
|
||||
project: Accessor<string | undefined>,
|
||||
isOpen: Accessor<boolean>,
|
||||
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()
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ function PlanExitCard(props: { part: ToolPart }) {
|
||||
<Show when={info()}>
|
||||
<div data-component="plan-exit-card">
|
||||
<span data-slot="plan-exit-label">{label()}</span>{" "}
|
||||
<span data-slot="plan-exit-badge">{language.t("ui.patch.action.plan")}</span>
|
||||
<a data-slot="plan-exit-link" href="#" onClick={open}>
|
||||
{display()}
|
||||
</a>
|
||||
|
||||
@@ -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<ReviewCommentsProps> = (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<ReviewCommentsProps> = (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<ReviewCommentsProps> = (props) => {
|
||||
<For each={props.comments}>
|
||||
{(item) => (
|
||||
<div class="prompt-review-chip">
|
||||
<button type="button" class="prompt-review-chip-body" onClick={() => show(item)}>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt-review-chip-body"
|
||||
onClick={() => (isPRReviewComment(item) ? show(item) : open(item))}
|
||||
>
|
||||
<span class="prompt-review-chip-icon">
|
||||
<Icon name={isPRReviewComment(item) ? "github" : "comment"} size="small" />
|
||||
</span>
|
||||
|
||||
@@ -23,7 +23,27 @@
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
[data-slot="plan-exit-badge"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 5px;
|
||||
border: 1px solid var(--border-interactive-base);
|
||||
border-radius: 999px;
|
||||
color: var(--text-interactive-base);
|
||||
font-size: var(--kilo-font-size-11);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-small);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
[data-slot="plan-exit-link"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-interactive-base);
|
||||
text-decoration: none;
|
||||
@@ -33,7 +53,14 @@
|
||||
word-break: break-all;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-interactive-base);
|
||||
background: var(--surface-interactive-weak);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--focus-base);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,28 @@ export interface AppendReviewCommentsMessage {
|
||||
autoSend?: boolean
|
||||
}
|
||||
|
||||
export interface DocumentResultMessage {
|
||||
type: "document.result"
|
||||
sessionId: string
|
||||
contextKey?: string
|
||||
file: string
|
||||
requestedFile?: string
|
||||
content?: string
|
||||
kind?: "text" | "image"
|
||||
mime?: string
|
||||
data?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface DocumentOpenMessage {
|
||||
type: "document.open"
|
||||
sessionId?: string
|
||||
contextKey: string
|
||||
file: string
|
||||
line?: number
|
||||
column?: number
|
||||
}
|
||||
|
||||
export interface AppendReviewCommentsToTerminalMessage {
|
||||
type: "appendReviewCommentsToTerminal"
|
||||
comments: ReviewCommentEntry[]
|
||||
@@ -968,6 +990,19 @@ export interface AgentManagerWorktreeDiffFileMessage {
|
||||
diff: WorktreeFileDiff | null
|
||||
}
|
||||
|
||||
export interface AgentManagerDocumentMessage {
|
||||
type: "agentManager.document"
|
||||
sessionId: string
|
||||
contextKey?: string
|
||||
file: string
|
||||
requestedFile?: string
|
||||
content?: string
|
||||
kind?: "text" | "image"
|
||||
mime?: string
|
||||
data?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
// Agent Manager: Diff loading state (extension → webview)
|
||||
export interface AgentManagerWorktreeDiffLoadingMessage {
|
||||
type: "agentManager.worktreeDiffLoading"
|
||||
@@ -1123,6 +1158,16 @@ export interface DiffViewerMarkdownRenderMessage {
|
||||
render: boolean
|
||||
}
|
||||
|
||||
export interface DiffViewerInitialFileMessage {
|
||||
type: "diffViewer.initialFile"
|
||||
file?: string
|
||||
}
|
||||
|
||||
export interface DiffViewerInitialMarkdownMessage {
|
||||
type: "diffViewer.initialMarkdown"
|
||||
render: boolean
|
||||
}
|
||||
|
||||
export interface SetAvailableSourcesMessage {
|
||||
type: "setAvailableSources"
|
||||
descriptors: DiffSourceDescriptor[]
|
||||
@@ -1284,6 +1329,8 @@ export interface AgentManagerFocusContextRequestedMessage {
|
||||
}
|
||||
|
||||
export type ExtensionMessage =
|
||||
| DocumentResultMessage
|
||||
| DocumentOpenMessage
|
||||
| AgentManagerFocusContextRequestedMessage
|
||||
| ReadyMessage
|
||||
| FontSizeChangedMessage
|
||||
@@ -1402,6 +1449,7 @@ export type ExtensionMessage =
|
||||
| WorkspaceDirectoryChangedMessage
|
||||
| AgentManagerWorktreeDiffMessage
|
||||
| AgentManagerWorktreeDiffFileMessage
|
||||
| AgentManagerDocumentMessage
|
||||
| AgentManagerWorktreeDiffLoadingMessage
|
||||
| AgentManagerWorktreeDiffNoticeMessage
|
||||
| AgentManagerApplyWorktreeDiffResultMessage
|
||||
@@ -1433,6 +1481,8 @@ export type ExtensionMessage =
|
||||
| DiffViewerRevertFileResultMessage
|
||||
| DiffViewerDiffFileMessage
|
||||
| DiffViewerMarkdownRenderMessage
|
||||
| DiffViewerInitialFileMessage
|
||||
| DiffViewerInitialMarkdownMessage
|
||||
| SetAvailableSourcesMessage
|
||||
| DiffViewerCapabilitiesMessage
|
||||
| DiffViewerNoticeMessage
|
||||
|
||||
@@ -867,6 +867,37 @@ export interface AgentManagerOpenFileRequest {
|
||||
column?: number
|
||||
}
|
||||
|
||||
export interface AgentManagerRequestDocumentMessage {
|
||||
type: "agentManager.requestDocument"
|
||||
sessionId: string
|
||||
file: string
|
||||
contextKey?: string
|
||||
}
|
||||
|
||||
export interface DocumentRequestMessage {
|
||||
type: "document.request"
|
||||
sessionId?: string
|
||||
file: string
|
||||
contextKey?: string
|
||||
}
|
||||
|
||||
export interface DocumentOpenFileMessage {
|
||||
type: "document.openFile"
|
||||
file: string
|
||||
line?: number
|
||||
column?: number
|
||||
}
|
||||
|
||||
export interface DocumentCloseMessage {
|
||||
type: "document.close"
|
||||
}
|
||||
|
||||
export interface DocumentSendCommentsMessage {
|
||||
type: "document.sendComments"
|
||||
comments: ReviewCommentEntry[]
|
||||
autoSend?: boolean
|
||||
}
|
||||
|
||||
// Create multiple worktree sessions for the same prompt (multi-version mode)
|
||||
export interface CreateMultiVersionRequest {
|
||||
type: "agentManager.createMultiVersion"
|
||||
@@ -1396,6 +1427,10 @@ export interface DismissAgentMigrationBannerMessage {
|
||||
}
|
||||
|
||||
export type WebviewMessage =
|
||||
| DocumentRequestMessage
|
||||
| DocumentOpenFileMessage
|
||||
| DocumentCloseMessage
|
||||
| DocumentSendCommentsMessage
|
||||
| SendMessageRequest
|
||||
| AbortRequest
|
||||
| RevertSessionRequest
|
||||
@@ -1525,6 +1560,7 @@ export type WebviewMessage =
|
||||
| CopyToClipboardRequest
|
||||
| ShowExistingLocalTerminalRequest
|
||||
| AgentManagerOpenFileRequest
|
||||
| AgentManagerRequestDocumentMessage
|
||||
| CreateMultiVersionRequest
|
||||
| SetTabOrderRequest
|
||||
| SetWorktreeOrderRequest
|
||||
|
||||
@@ -451,6 +451,7 @@ export namespace KiloSessionPrompt {
|
||||
info,
|
||||
"Use the chosen plan path as the main plan file. Do not write or edit other files unless the user explicitly asks and your permissions allow it.",
|
||||
"Project/user instructions about plan location (for example plans/ or .plans/) are authorized when permissions allow them; they do not conflict with this reminder. When finalizing, call plan_exit with the path of the plan file you wrote.",
|
||||
"In the visible final response, cite the saved plan path as an inline code span so the client can open it as a document. Cite other user-facing files you create the same way instead of pasting the full file into chat.",
|
||||
supportsPlanFollowup()
|
||||
? "When the plan is implementation-ready, write the main plan file and call plan_exit. Do not ask the user to choose between finalizing and refining in chat; the client follow-up after plan_exit asks whether to implement the saved plan or keep refining."
|
||||
: 'Before creating or updating the plan file, or calling plan_exit, ask the user to choose exactly one of: "Finalize and save the plan" or "Continue refining". If the user chooses to finalize, write the main plan file, then call plan_exit.',
|
||||
|
||||
@@ -2322,14 +2322,6 @@ export type AgentConfig = {
|
||||
steps?: number
|
||||
maxSteps?: number
|
||||
permission?: PermissionConfig
|
||||
requirements?: {
|
||||
skills?: Array<string>
|
||||
mcps?: Array<string>
|
||||
vscode_extensions?: Array<{
|
||||
name: string
|
||||
id: string
|
||||
}>
|
||||
}
|
||||
[key: string]:
|
||||
| unknown
|
||||
| string
|
||||
@@ -2354,14 +2346,6 @@ export type AgentConfig = {
|
||||
| "info"
|
||||
| number
|
||||
| PermissionConfig
|
||||
| {
|
||||
skills?: Array<string>
|
||||
mcps?: Array<string>
|
||||
vscode_extensions?: Array<{
|
||||
name: string
|
||||
id: string
|
||||
}>
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
|
||||
@@ -3109,14 +3093,6 @@ export type Agent = {
|
||||
options: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
requirements?: {
|
||||
skills?: Array<string>
|
||||
mcps?: Array<string>
|
||||
vscode_extensions?: Array<{
|
||||
name: string
|
||||
id: string
|
||||
}>
|
||||
}
|
||||
steps?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,25 @@
|
||||
text-decoration-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
/* A plan reference reads as a document, not a boxed chip: a small glyph in
|
||||
front of the path, keeping the same dotted-underline affordance every
|
||||
other validated file link already uses. No border, no badge, no fill. */
|
||||
&.plan-document-link::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0.95em;
|
||||
height: 0.95em;
|
||||
margin-inline-end: 0.3em;
|
||||
vertical-align: -0.15em;
|
||||
background-color: currentColor;
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z'/%3E%3Cpath d='M14 2v5h6'/%3E%3Cpath d='M9 13h6'/%3E%3Cpath d='M9 17h4'/%3E%3C/svg%3E");
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z'/%3E%3Cpath d='M14 2v5h6'/%3E%3Cpath d='M9 13h6'/%3E%3Cpath d='M9 17h4'/%3E%3C/svg%3E");
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-size: contain;
|
||||
}
|
||||
/* kilocode_change end */
|
||||
}
|
||||
|
||||
|
||||
Generated
+1
@@ -211,6 +211,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "تم الإنشاء",
|
||||
"ui.patch.action.moved": "منقول",
|
||||
"ui.patch.action.patched": "تم تطبيق رقعة",
|
||||
"ui.patch.action.plan": "خطة", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "تمت الإجابة عن {{count}}",
|
||||
"ui.question.answer.none": "(لا توجد إجابة)",
|
||||
|
||||
Generated
+1
@@ -188,6 +188,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Yaradıldı",
|
||||
"ui.patch.action.moved": "Köçürüldü",
|
||||
"ui.patch.action.patched": "Yamaq tətbiq edildi",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
"ui.question.subtitle.answered": "{{count}} cavablandı",
|
||||
"ui.question.answer.none": "(cavab yoxdur)",
|
||||
"ui.question.review.notAnswered": "(cavablanmayıb)",
|
||||
|
||||
Generated
+1
@@ -215,6 +215,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Criado",
|
||||
"ui.patch.action.moved": "Movido",
|
||||
"ui.patch.action.patched": "Patch aplicado",
|
||||
"ui.patch.action.plan": "Plano", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} respondidas",
|
||||
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
|
||||
|
||||
Generated
+1
@@ -203,6 +203,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Kreirano",
|
||||
"ui.patch.action.moved": "Premješteno",
|
||||
"ui.patch.action.patched": "Primijenjeno",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "Odgovoreno: {{count}}",
|
||||
"ui.question.answer.none": "(nema odgovora)",
|
||||
|
||||
Generated
+1
@@ -191,6 +191,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Oprettet",
|
||||
"ui.patch.action.moved": "Flyttet",
|
||||
"ui.patch.action.patched": "Patchet",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} besvaret",
|
||||
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
|
||||
|
||||
@@ -181,6 +181,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Erstellt",
|
||||
"ui.patch.action.moved": "Verschoben",
|
||||
"ui.patch.action.patched": "Gepatched",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} beantwortet",
|
||||
"ui.question.answer.none": "(keine Antwort)",
|
||||
|
||||
@@ -227,6 +227,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Created",
|
||||
"ui.patch.action.moved": "Moved",
|
||||
"ui.patch.action.patched": "Patched",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} answered",
|
||||
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
|
||||
|
||||
Generated
+1
@@ -199,6 +199,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Creado",
|
||||
"ui.patch.action.moved": "Movido",
|
||||
"ui.patch.action.patched": "Parcheado",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} respondidas",
|
||||
"ui.question.answer.none": "(sin respuesta)",
|
||||
|
||||
Generated
+1
@@ -170,6 +170,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Luotu",
|
||||
"ui.patch.action.moved": "Siirretty",
|
||||
"ui.patch.action.patched": "Paikattu",
|
||||
"ui.patch.action.plan": "Suunnitelma", // kilocode_change
|
||||
"ui.question.subtitle.answered": "Vastatut kysymykset: {{count}}",
|
||||
"ui.question.answer.none": "(ei vastausta)",
|
||||
"ui.question.review.notAnswered": "(ei vastattu)",
|
||||
|
||||
Generated
+1
@@ -201,6 +201,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Créé",
|
||||
"ui.patch.action.moved": "Déplacé",
|
||||
"ui.patch.action.patched": "Correctif appliqué",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "Réponses : {{count}}",
|
||||
"ui.question.answer.none": "(pas de réponse)",
|
||||
|
||||
Generated
+1
@@ -190,6 +190,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "बनाया गया",
|
||||
"ui.patch.action.moved": "ले जाया गया",
|
||||
"ui.patch.action.patched": "पैच किया गया",
|
||||
"ui.patch.action.plan": "योजना", // kilocode_change
|
||||
"ui.question.subtitle.answered": "{{count}} के उत्तर दिए गए",
|
||||
"ui.question.answer.none": "(कोई जवाब नहीं)",
|
||||
"ui.question.review.notAnswered": "(उत्तर नहीं दिया गया)",
|
||||
|
||||
Generated
+1
@@ -209,6 +209,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Dibuat",
|
||||
"ui.patch.action.moved": "Dipindahkan",
|
||||
"ui.patch.action.patched": "Ditambal",
|
||||
"ui.patch.action.plan": "Rencana", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} dijawab",
|
||||
"ui.question.answer.none": "(tidak ada jawaban)",
|
||||
|
||||
Generated
+1
@@ -193,6 +193,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Creato",
|
||||
"ui.patch.action.moved": "Spostato",
|
||||
"ui.patch.action.patched": "Modificato",
|
||||
"ui.patch.action.plan": "Piano", // kilocode_change
|
||||
"ui.question.subtitle.answered": "{{count}} risposte",
|
||||
"ui.question.answer.none": "(nessuna risposta)",
|
||||
"ui.question.review.notAnswered": "(senza risposta)",
|
||||
|
||||
Generated
+1
@@ -194,6 +194,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "作成済み",
|
||||
"ui.patch.action.moved": "移動済み",
|
||||
"ui.patch.action.patched": "パッチ適用済み",
|
||||
"ui.patch.action.plan": "計画", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}}件回答済み",
|
||||
"ui.question.answer.none": "(回答なし)",
|
||||
|
||||
Generated
+1
@@ -171,6 +171,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "생성됨",
|
||||
"ui.patch.action.moved": "이동됨",
|
||||
"ui.patch.action.patched": "패치됨",
|
||||
"ui.patch.action.plan": "계획", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}}개 답변됨",
|
||||
"ui.question.answer.none": "(답변 없음)",
|
||||
|
||||
Generated
+1
@@ -188,6 +188,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Gemaakt",
|
||||
"ui.patch.action.moved": "Verplaatst",
|
||||
"ui.patch.action.patched": "Bijgewerkt",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
"ui.question.subtitle.answered": "{{count}} beantwoord",
|
||||
"ui.question.answer.none": "(geen antwoord)",
|
||||
"ui.question.review.notAnswered": "(niet beantwoord)",
|
||||
|
||||
Generated
+1
@@ -174,6 +174,7 @@ export const dict: Record<Keys, string> = {
|
||||
"ui.patch.action.created": "Opprettet",
|
||||
"ui.patch.action.moved": "Flyttet",
|
||||
"ui.patch.action.patched": "Oppdatert",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} besvart",
|
||||
"ui.question.answer.none": "(ingen svar)",
|
||||
|
||||
Generated
+1
@@ -189,6 +189,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "بنایا گیا",
|
||||
"ui.patch.action.moved": "منتقل ہو گیا",
|
||||
"ui.patch.action.patched": "پیچ کیتا گیا",
|
||||
"ui.patch.action.plan": "ਯੋਜਨਾ", // kilocode_change
|
||||
"ui.question.subtitle.answered": "{{count}} جواب دتا گیا",
|
||||
"ui.question.answer.none": "(کوئی جواب نئیں)",
|
||||
"ui.question.review.notAnswered": "(جواب نئیں دتا گیا)",
|
||||
|
||||
Generated
+1
@@ -203,6 +203,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Utworzono",
|
||||
"ui.patch.action.moved": "Przeniesiono",
|
||||
"ui.patch.action.patched": "Załatano",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "Liczba odpowiedzi: {{count}}",
|
||||
"ui.question.answer.none": "(brak odpowiedzi)",
|
||||
|
||||
Generated
+1
@@ -202,6 +202,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Создано",
|
||||
"ui.patch.action.moved": "Перемещено",
|
||||
"ui.patch.action.patched": "Изменено",
|
||||
"ui.patch.action.plan": "План", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "Получено ответов: {{count}}",
|
||||
"ui.question.answer.none": "(нет ответа)",
|
||||
|
||||
Generated
+1
@@ -188,6 +188,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Skapad",
|
||||
"ui.patch.action.moved": "Flyttad",
|
||||
"ui.patch.action.patched": "Patchad",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
"ui.question.subtitle.answered": "{{count}} besvarade",
|
||||
"ui.question.answer.none": "(inget svar)",
|
||||
"ui.question.review.notAnswered": "(ej besvarad)",
|
||||
|
||||
Generated
+1
@@ -195,6 +195,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "สร้าง",
|
||||
"ui.patch.action.moved": "ย้าย",
|
||||
"ui.patch.action.patched": "แพตช์",
|
||||
"ui.patch.action.plan": "แผน", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "ตอบแล้ว {{count}} ข้อ",
|
||||
"ui.question.answer.none": "(ไม่มีคำตอบ)",
|
||||
|
||||
Generated
+1
@@ -201,6 +201,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "Oluşturuldu",
|
||||
"ui.patch.action.moved": "Taşındı",
|
||||
"ui.patch.action.patched": "Yamalandı",
|
||||
"ui.patch.action.plan": "Plan", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} yanıtlandı",
|
||||
"ui.question.answer.none": "(yanıt yok)",
|
||||
|
||||
Generated
+1
@@ -218,6 +218,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Створено",
|
||||
"ui.patch.action.moved": "Переміщено",
|
||||
"ui.patch.action.patched": "Застосовано патч",
|
||||
"ui.patch.action.plan": "План", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "{{count}} відповідей",
|
||||
"ui.question.answer.none": "(немає відповіді)",
|
||||
|
||||
Generated
+1
@@ -189,6 +189,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "بنا دیا گیا",
|
||||
"ui.patch.action.moved": "منتقل کر دیا گیا",
|
||||
"ui.patch.action.patched": "پیچ کیا گیا",
|
||||
"ui.patch.action.plan": "منصوبہ", // kilocode_change
|
||||
"ui.question.subtitle.answered": "{{count}} کے جواب دیے گئے",
|
||||
"ui.question.answer.none": "(کوئی جواب نہیں)",
|
||||
"ui.question.review.notAnswered": "(جواب نہیں دیا گیا)",
|
||||
|
||||
Generated
+1
@@ -188,6 +188,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.patch.action.created": "Đã tạo",
|
||||
"ui.patch.action.moved": "Đã di chuyển",
|
||||
"ui.patch.action.patched": "Đã vá",
|
||||
"ui.patch.action.plan": "Kế hoạch", // kilocode_change
|
||||
"ui.question.subtitle.answered": "{{count}} đã trả lời",
|
||||
"ui.question.answer.none": "(không có câu trả lời)",
|
||||
"ui.question.review.notAnswered": "(chưa trả lời)",
|
||||
|
||||
Generated
+1
@@ -197,6 +197,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "已创建",
|
||||
"ui.patch.action.moved": "已移动",
|
||||
"ui.patch.action.patched": "已应用补丁",
|
||||
"ui.patch.action.plan": "计划", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "已回答 {{count}} 个",
|
||||
"ui.question.answer.none": "(无答案)",
|
||||
|
||||
Generated
+1
@@ -197,6 +197,7 @@ export const dict = {
|
||||
"ui.patch.action.created": "已建立",
|
||||
"ui.patch.action.moved": "已移動",
|
||||
"ui.patch.action.patched": "已套用修補",
|
||||
"ui.patch.action.plan": "計畫", // kilocode_change
|
||||
|
||||
"ui.question.subtitle.answered": "已回答 {{count}} 題",
|
||||
"ui.question.answer.none": "(無答案)",
|
||||
|
||||
Reference in New Issue
Block a user