diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 906ff6bb19..f096222322 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -336,6 +336,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper vscode.env.openExternal(vscode.Uri.parse(message.url)) } break + case "openFile": + if (message.filePath) { + this.handleOpenFile(message.filePath, message.line, message.column) + } + break case "requestProviders": await this.fetchAndSendProviders() break @@ -1283,6 +1288,28 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } + /** + * Handle openFile request from the webview — open a file in the VS Code editor. + */ + private handleOpenFile(filePath: string, line?: number, column?: number): void { + const absolute = /^(?:\/|[a-zA-Z]:[\\/])/.test(filePath) + const uri = absolute + ? vscode.Uri.file(filePath) + : vscode.Uri.joinPath(vscode.Uri.file(this.getWorkspaceDirectory()), filePath) + vscode.workspace.openTextDocument(uri).then( + (doc) => { + const options: vscode.TextDocumentShowOptions = { preview: true } + if (line !== undefined && line > 0) { + const col = column !== undefined && column > 0 ? column - 1 : 0 + const pos = new vscode.Position(line - 1, col) + options.selection = new vscode.Range(pos, pos) + } + vscode.window.showTextDocument(doc, options) + }, + (err) => console.error("[Kilo New] KiloProvider: Failed to open file:", uri.fsPath, err), + ) + } + /** * Handle logout request from the webview. */ diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 7a91a7c32d..48059f142d 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -10,7 +10,7 @@ import { DataProvider } from "@kilocode/kilo-ui/context/data" import { Toast } from "@kilocode/kilo-ui/toast" import Settings from "./components/Settings" import ProfileView from "./components/ProfileView" -import { VSCodeProvider } from "./context/vscode" +import { VSCodeProvider, useVSCode } from "./context/vscode" import { ServerProvider, useServer } from "./context/server" import { ProviderProvider } from "./context/provider" import { ConfigProvider } from "./context/config" @@ -48,6 +48,7 @@ const DummyView: Component<{ title: string }> = (props) => { */ export const DataBridge: Component<{ children: any }> = (props) => { const session = useSession() + const vscode = useVSCode() const data = createMemo(() => { const id = session.currentSessionID() @@ -77,8 +78,12 @@ export const DataBridge: Component<{ children: any }> = (props) => { session.syncSession(sessionID) } + const open = (filePath: string, line?: number, column?: number) => { + vscode.postMessage({ type: "openFile", filePath, line, column }) + } + return ( - + {props.children} ) diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index a3cf047918..334efcd9c4 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -728,6 +728,13 @@ export interface OpenExternalRequest { url: string } +export interface OpenFileRequest { + type: "openFile" + filePath: string + line?: number + column?: number +} + export interface CancelLoginRequest { type: "cancelLogin" } @@ -951,6 +958,7 @@ export type WebviewMessage = | LogoutRequest | RefreshProfileRequest | OpenExternalRequest + | OpenFileRequest | CancelLoginRequest | SetOrganizationRequest | WebviewReadyRequest diff --git a/packages/ui/src/components/markdown.css b/packages/ui/src/components/markdown.css index 68ae93bda4..53ae7d2b5a 100644 --- a/packages/ui/src/components/markdown.css +++ b/packages/ui/src/components/markdown.css @@ -173,6 +173,21 @@ /* border-radius: 2px; */ /* background: var(--surface-base); */ /* box-shadow: 0 0 0 0.5px var(--border-weak-base); */ + + /* kilocode_change start */ + &.file-link { + cursor: pointer; + text-decoration-line: underline; + text-decoration-style: dotted; + text-underline-offset: 2px; + transition: color 0.15s ease; + + &:hover { + color: var(--text-interactive-base); + text-decoration-style: solid; + } + } + /* kilocode_change end */ } /* Tables */ diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 44db4f9aa5..c18f7a7263 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -34,6 +34,7 @@ overflow: hidden; background: var(--surface-weak); border: 1px solid var(--border-weak-base); + cursor: pointer; /* kilocode_change */ transition: border-color 0.15s ease; &:hover { @@ -274,6 +275,18 @@ [data-slot="message-part-title-filename"] { /* No text-transform - preserve original filename casing */ + + /* kilocode_change start */ + &.clickable { + cursor: pointer; + transition: color 0.15s ease; + + &:hover { + text-decoration: underline; + color: var(--text-base); + } + } + /* kilocode_change end */ } [data-slot="message-part-path"] { @@ -289,6 +302,18 @@ white-space: nowrap; direction: rtl; text-align: left; + + /* kilocode_change start */ + &.clickable { + cursor: pointer; + transition: color 0.15s ease; + + &:hover { + text-decoration: underline; + color: var(--text-base); + } + } + /* kilocode_change end */ } [data-slot="message-part-filename"] { @@ -797,6 +822,18 @@ text-overflow: ellipsis; white-space: nowrap; flex-grow: 1; + + /* kilocode_change start */ + &.clickable { + cursor: pointer; + transition: color 0.15s ease; + + &:hover { + text-decoration: underline; + color: var(--text-base); + } + } + /* kilocode_change end */ } [data-slot="apply-patch-deletion-count"] { @@ -833,4 +870,15 @@ flex-shrink: 0; color: var(--icon-weak); } + + /* kilocode_change start */ + &.clickable { + cursor: pointer; + transition: color 0.15s ease; + + &:hover { + color: var(--text-base); + } + } + /* kilocode_change end */ } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 42dc37abc5..3cd29c611f 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -682,11 +682,28 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { setTimeout(() => setCopied(false), 2000) } + // kilocode_change start + const handleMarkdownClick = (e: MouseEvent) => { + if (!data.openFile) return + const target = e.target + if (!(target instanceof HTMLElement)) return + const link = target.closest(".file-link[data-file-path]") + if (!link) return + const path = link.getAttribute("data-file-path") + if (!path) return + const lineAttr = link.getAttribute("data-file-line") + const colAttr = link.getAttribute("data-file-col") + const line = lineAttr ? parseInt(lineAttr, 10) : undefined + const column = colAttr ? parseInt(colAttr, 10) : undefined + data.openFile(path, line, column) + } + // kilocode_change end + return (
- + {/* kilocode_change */}
data.openFile!(props.input.filePath) : undefined + } + // kilocode_change end /> {(filepath) => ( -
+
data.openFile?.(filepath)} // kilocode_change + > {i18n.t("ui.tool.loaded")} {relativizeProjectPaths(filepath, data.directory)} @@ -1106,10 +1132,18 @@ ToolRegistry.register({ ToolRegistry.register({ name: "edit", render(props) { + const data = useData() // kilocode_change const i18n = useI18n() const diffComponent = useDiffComponent() const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath)) const filename = () => getFilename(props.input.filePath ?? "") + // kilocode_change start + const handleFileClick = (e: MouseEvent) => { + if (!data.openFile || !props.input.filePath) return + e.stopPropagation() + data.openFile(props.input.filePath) + } + // kilocode_change end return (
{i18n.t("ui.messagePart.title.edit")} - {filename()} + {/* kilocode_change start */} + + {filename()} + + {/* kilocode_change end */}
- {getDirectory(props.input.filePath!)} + {/* kilocode_change start */} + + {getDirectory(props.input.filePath!)} + + {/* kilocode_change end */}
@@ -1159,10 +1209,18 @@ ToolRegistry.register({ ToolRegistry.register({ name: "write", render(props) { + const data = useData() // kilocode_change const i18n = useI18n() const codeComponent = useCodeComponent() const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath)) const filename = () => getFilename(props.input.filePath ?? "") + // kilocode_change start + const handleFileClick = (e: MouseEvent) => { + if (!data.openFile || !props.input.filePath) return + e.stopPropagation() + data.openFile(props.input.filePath) + } + // kilocode_change end return (
{i18n.t("ui.messagePart.title.write")} - {filename()} + {/* kilocode_change start */} + + {filename()} + + {/* kilocode_change end */}
- {getDirectory(props.input.filePath!)} + {/* kilocode_change start */} + + {getDirectory(props.input.filePath!)} + + {/* kilocode_change end */}
@@ -1218,6 +1292,7 @@ interface ApplyPatchFile { ToolRegistry.register({ name: "apply_patch", render(props) { + const data = useData() // kilocode_change const i18n = useI18n() const diffComponent = useDiffComponent() const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[]) @@ -1265,7 +1340,19 @@ ToolRegistry.register({ - {file.relativePath} + {/* kilocode_change start */} + { + if (!data.openFile) return + e.stopPropagation() + data.openFile(file.filePath) + }} + > + {file.relativePath} + + {/* kilocode_change end */} diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index 137ea05656..26dcc2a759 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -52,6 +52,8 @@ export type SessionHrefFn = (sessionID: string) => string export type SyncSessionFn = (sessionID: string) => void | Promise +export type OpenFileFn = (filePath: string, line?: number, column?: number) => void // kilocode_change + export const { use: useData, provider: DataProvider } = createSimpleContext({ name: "Data", init: (props: { @@ -63,6 +65,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ onNavigateToSession?: NavigateToSessionFn onSessionHref?: SessionHrefFn onSyncSession?: SyncSessionFn + onOpenFile?: OpenFileFn // kilocode_change }) => { return { get store() { @@ -77,6 +80,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ navigateToSession: props.onNavigateToSession, sessionHref: props.onSessionHref, syncSession: props.onSyncSession, + openFile: props.onOpenFile, // kilocode_change } }, }) diff --git a/packages/ui/src/context/marked.tsx b/packages/ui/src/context/marked.tsx index 1ea1999f8d..315cf55aab 100644 --- a/packages/ui/src/context/marked.tsx +++ b/packages/ui/src/context/marked.tsx @@ -461,6 +461,26 @@ async function highlightCodeBlocks(html: string): Promise { export type NativeMarkdownParser = (markdown: string) => Promise +// kilocode_change start +// Matches text that looks like a file path: contains "/" and ends with a file extension, +// or starts with "./" or "../" or "/". Supports optional :line or :line:col suffix. +const FILE_PATH_RE = + /^((?:\/|\.\.?\/)?(?:[a-zA-Z0-9_@-][a-zA-Z0-9_@./-]*\/)*[a-zA-Z0-9_@.-]+\.[a-zA-Z0-9]+)(?::(\d+)(?::(\d+))?)?$/ + +function parseFilePath(text: string): { path: string; line?: number; column?: number } | undefined { + if (text.includes("://")) return undefined + if (text.includes(" ")) return undefined + const match = FILE_PATH_RE.exec(text) + if (!match) return undefined + if (!match[1].includes("/")) return undefined + return { + path: match[1], + line: match[2] ? parseInt(match[2], 10) : undefined, + column: match[3] ? parseInt(match[3], 10) : undefined, + } +} +// kilocode_change end + export const { use: useMarked, provider: MarkedProvider } = createSimpleContext({ name: "Marked", init: (props: { nativeParser?: NativeMarkdownParser }) => { @@ -471,6 +491,17 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext( const titleAttr = title ? ` title="${title}"` : "" return `${text}` }, + // kilocode_change start + codespan({ text }) { + const file = parseFilePath(text) + if (file) { + const lineAttr = file.line ? ` data-file-line="${file.line}"` : "" + const colAttr = file.column ? ` data-file-col="${file.column}"` : "" + return `${text}` + } + return `${text}` + }, + // kilocode_change end }, }, markedKatex({