mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
feat(vscode): add filesystem validation protocol for file links
Add a validateFiles request/response round-trip between the webview and the extension so the webview can confirm which inline code-span candidates are real files before promoting them to clickable links. The extension stat-checks candidate paths (new file-links.ts) and replies with the subset that exist. Routing lives in editor-actions alongside the other editor open actions, and openFile now falls back to a workspace filename search (single match opens, multiple prompts) with a "File not found" warning when a clicked path cannot be resolved.
This commit is contained in:
@@ -1233,6 +1233,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
dir: () => this.getWorkspaceDirectory(this.currentSession?.id),
|
||||
diff: this.diffVirtualProvider,
|
||||
storage: this.extensionContext?.globalStorageUri,
|
||||
post: (msg) => this.postMessage(msg),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as vscode from "vscode"
|
||||
import { buildPreviewPath, getPreviewCommand, getPreviewDir, parseImage, trimEntries } from "../image-preview"
|
||||
import { isAbsolutePath } from "../path-utils"
|
||||
import { validateFiles } from "./file-links"
|
||||
import type { DiffVirtualFile, DiffVirtualProvider } from "../DiffVirtualProvider"
|
||||
|
||||
type EditorOpenMessage = {
|
||||
@@ -73,6 +74,7 @@ export function handleEditorAction(
|
||||
dir: () => string
|
||||
diff?: DiffVirtualProvider
|
||||
storage?: vscode.Uri
|
||||
post?: (msg: unknown) => void
|
||||
},
|
||||
): boolean {
|
||||
if (message.type === "openFile") {
|
||||
@@ -83,6 +85,17 @@ export function handleEditorAction(
|
||||
if (message.content) openContent(message.content, message.language)
|
||||
return true
|
||||
}
|
||||
if (message.type === "validateFiles") {
|
||||
const id = (message as { id?: string }).id
|
||||
const paths = (message as { paths?: string[] }).paths
|
||||
if (id && paths && opts.post) {
|
||||
validateFiles(opts.dir(), paths).then(
|
||||
(existing) => opts.post!({ type: "validateFilesResult", id, existing }),
|
||||
(err) => console.error("[Kilo New] KiloProvider: validateFiles failed:", err),
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (message.type === "openExternal") {
|
||||
openExternal(message.url)
|
||||
return true
|
||||
@@ -105,6 +118,46 @@ function openContent(content: string, language?: string): void {
|
||||
)
|
||||
}
|
||||
|
||||
function show(uri: vscode.Uri, line?: number, column?: number): void {
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback when the exact path does not exist: search the workspace by filename.
|
||||
* Opens the file directly on a single match, prompts on multiple, warns on none.
|
||||
*/
|
||||
function findFallback(filePath: string, line?: number, column?: number): void {
|
||||
const name = filePath.split(/[\\/]/).pop() || filePath
|
||||
Promise.resolve(vscode.workspace.findFiles(`**/${name}`, "**/node_modules/**", 5)).then(
|
||||
(matches) => {
|
||||
if (matches.length === 1) {
|
||||
show(matches[0], line, column)
|
||||
return
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
const items = matches.map((m) => ({ label: vscode.workspace.asRelativePath(m), uri: m }))
|
||||
vscode.window.showQuickPick(items, { placeHolder: `Multiple matches for "${name}"` }).then((pick) => {
|
||||
if (pick) show(pick.uri, line, column)
|
||||
})
|
||||
return
|
||||
}
|
||||
vscode.window.showWarningMessage(`File not found: ${filePath}`)
|
||||
},
|
||||
(err: unknown) => console.error("[Kilo New] KiloProvider: findFiles failed:", err),
|
||||
)
|
||||
}
|
||||
|
||||
function openFile(dir: string, filePath: string, line?: number, column?: number): void {
|
||||
const uri = isAbsolutePath(filePath) ? vscode.Uri.file(filePath) : vscode.Uri.joinPath(vscode.Uri.file(dir), filePath)
|
||||
vscode.workspace.fs.stat(uri).then(
|
||||
@@ -113,19 +166,8 @@ function openFile(dir: string, filePath: string, line?: number, column?: number)
|
||||
vscode.commands.executeCommand("revealInExplorer", uri)
|
||||
return
|
||||
}
|
||||
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),
|
||||
)
|
||||
show(uri, line, column)
|
||||
},
|
||||
(err) => console.error("[Kilo New] KiloProvider: Path does not exist:", uri.fsPath, err),
|
||||
() => findFallback(filePath, line, column),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as vscode from "vscode"
|
||||
import { isAbsolutePath } from "../path-utils"
|
||||
|
||||
/**
|
||||
* Stat-check candidate paths and return which ones are actual files (not directories).
|
||||
*
|
||||
* The webview marks every inline code span as a file-link candidate; this confirms
|
||||
* which of those candidates resolve to a real file so the webview can promote them
|
||||
* to clickable links and leave the rest as plain code.
|
||||
*/
|
||||
export function validateFiles(root: string, paths: string[]): Promise<string[]> {
|
||||
const resolve = (p: string) =>
|
||||
isAbsolutePath(p) ? vscode.Uri.file(p) : vscode.Uri.joinPath(vscode.Uri.file(root), p)
|
||||
return Promise.all(
|
||||
paths.map((p) =>
|
||||
Promise.resolve(vscode.workspace.fs.stat(resolve(p))).then(
|
||||
(s) => (s.type & vscode.FileType.File ? p : null),
|
||||
() => null,
|
||||
),
|
||||
),
|
||||
).then((r) => r.filter((x): x is string => x !== null))
|
||||
}
|
||||
@@ -149,6 +149,35 @@ export const DataBridge: Component<{ children: any }> = (props) => {
|
||||
vscode.postMessage({ type: "openContent", content, language })
|
||||
}
|
||||
|
||||
// File existence validation for code span candidates
|
||||
const pending = new Map<string, (existing: string[]) => void>()
|
||||
const counter = { n: 0 }
|
||||
const validateFiles = (paths: string[]): Promise<string[]> => {
|
||||
const id = `vf-${++counter.n}`
|
||||
return new Promise((resolve) => {
|
||||
pending.set(id, resolve)
|
||||
vscode.postMessage({ type: "validateFiles", id, paths })
|
||||
setTimeout(() => {
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id)
|
||||
resolve([])
|
||||
}
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
const handler = (event: MessageEvent) => {
|
||||
const msg = event.data
|
||||
if (msg?.type === "validateFilesResult" && msg.id) {
|
||||
const cb = pending.get(msg.id)
|
||||
if (cb) {
|
||||
pending.delete(msg.id)
|
||||
cb(msg.existing ?? [])
|
||||
}
|
||||
}
|
||||
}
|
||||
onMount(() => window.addEventListener("message", handler))
|
||||
onCleanup(() => window.removeEventListener("message", handler))
|
||||
|
||||
const directory = () => {
|
||||
const dir = server.workspaceDirectory()
|
||||
if (!dir) return ""
|
||||
@@ -167,6 +196,7 @@ export const DataBridge: Component<{ children: any }> = (props) => {
|
||||
onOpenDiff={openDiff}
|
||||
onOpenUrl={openUrl}
|
||||
onOpenContent={openContent}
|
||||
onValidateFiles={validateFiles}
|
||||
>
|
||||
{props.children}
|
||||
</DataProvider>
|
||||
|
||||
@@ -962,6 +962,12 @@ export interface RemoteStatusMessage {
|
||||
connected: boolean
|
||||
}
|
||||
|
||||
export interface ValidateFilesResultMessage {
|
||||
type: "validateFilesResult"
|
||||
id: string
|
||||
existing: string[]
|
||||
}
|
||||
|
||||
export type ExtensionMessage =
|
||||
| ReadyMessage
|
||||
| FontSizeChangedMessage
|
||||
@@ -1108,3 +1114,4 @@ export type ExtensionMessage =
|
||||
| ExtensionDataReadyMessage
|
||||
| TelemetryStateMessage
|
||||
| RemoteStatusMessage
|
||||
| ValidateFilesResultMessage
|
||||
|
||||
@@ -142,6 +142,12 @@ export interface OpenContentRequest {
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface ValidateFilesRequest {
|
||||
type: "validateFiles"
|
||||
id: string
|
||||
paths: string[]
|
||||
}
|
||||
|
||||
export interface CancelLoginRequest {
|
||||
type: "cancelLogin"
|
||||
}
|
||||
@@ -1107,6 +1113,7 @@ export type WebviewMessage =
|
||||
| OpenAgentManagerRequest
|
||||
| OpenAdvancedWorktreeRequest
|
||||
| OpenFileRequest
|
||||
| ValidateFilesRequest
|
||||
| CancelLoginRequest
|
||||
| SetOrganizationRequest
|
||||
| WebviewReadyRequest
|
||||
|
||||
@@ -48,6 +48,8 @@ export type OpenDiffFn = (diff: {
|
||||
export type OpenUrlFn = (url: string) => void
|
||||
|
||||
export type OpenContentFn = (content: string, language?: string) => void // kilocode_change
|
||||
|
||||
export type ValidateFilesFn = (paths: string[]) => Promise<string[]> // kilocode_change
|
||||
// kilocode_change end
|
||||
|
||||
export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
@@ -61,6 +63,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
onOpenDiff?: OpenDiffFn // kilocode_change
|
||||
onOpenUrl?: OpenUrlFn // kilocode_change
|
||||
onOpenContent?: OpenContentFn // kilocode_change
|
||||
onValidateFiles?: ValidateFilesFn // kilocode_change
|
||||
}) => {
|
||||
return {
|
||||
get store() {
|
||||
@@ -75,6 +78,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
openDiff: props.onOpenDiff, // kilocode_change
|
||||
openUrl: props.onOpenUrl, // kilocode_change
|
||||
openContent: props.onOpenContent, // kilocode_change
|
||||
validateFiles: props.onValidateFiles, // kilocode_change
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user