Merge pull request #13245 from Kilo-Org/implement-markdown-file-viewer-sidebar

feat(vscode): add Agent Manager document inspector
This commit is contained in:
Marius
2026-08-20 14:18:00 +02:00
committed by GitHub
92 changed files with 1739 additions and 95 deletions
@@ -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.
@@ -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()
+1
View File
@@ -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",
},
+1
View File
@@ -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,
})
}
}
+19
View File
@@ -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)
}
+4
View File
@@ -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." }
}
}
+17
View File
@@ -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": "Laperçu de limage nest 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.',
-24
View File
@@ -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
}
+19
View File
@@ -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 */
}
+1
View File
@@ -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": "(لا توجد إجابة)",
+1
View File
@@ -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)",
+1
View File
@@ -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
+1
View File
@@ -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)",
+1
View File
@@ -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
+1
View File
@@ -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)",
+1
View File
@@ -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
+1
View File
@@ -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)",
+1
View File
@@ -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)",
+1
View File
@@ -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)",
+1
View File
@@ -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": "(उत्तर नहीं दिया गया)",
+1
View File
@@ -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)",
+1
View File
@@ -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)",
+1
View File
@@ -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": "(回答なし)",
+1
View File
@@ -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": "(답변 없음)",
+1
View File
@@ -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)",
+1
View File
@@ -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)",
+1
View File
@@ -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": "(جواب نئیں دتا گیا)",
+1
View File
@@ -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)",
+1
View File
@@ -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": "(нет ответа)",
+1
View File
@@ -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)",
+1
View File
@@ -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": "(ไม่มีคำตอบ)",
+1
View File
@@ -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)",
+1
View File
@@ -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": "(немає відповіді)",
+1
View File
@@ -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": "(جواب نہیں دیا گیا)",
+1
View File
@@ -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)",
+1
View File
@@ -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": "(无答案)",
+1
View File
@@ -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": "(無答案)",