mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge pull request #479 from Kilo-Org/feat/clickable-filenames-461-404
feat(vscode): make filenames in chat clickable to open editor tab
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 (
|
||||
<DataProvider data={data()} directory="" onPermissionRespond={respond} onSyncSession={sync}>
|
||||
<DataProvider data={data()} directory="" onPermissionRespond={respond} onSyncSession={sync} onOpenFile={open}>
|
||||
{props.children}
|
||||
</DataProvider>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Show when={throttledText()}>
|
||||
<div data-component="text-part">
|
||||
<div data-slot="text-part-body">
|
||||
<Markdown text={throttledText()} cacheKey={part.id} />
|
||||
<Markdown text={throttledText()} cacheKey={part.id} onClick={handleMarkdownClick} /> {/* kilocode_change */}
|
||||
<div data-slot="text-part-copy-wrapper">
|
||||
<Tooltip
|
||||
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
||||
@@ -751,10 +768,19 @@ ToolRegistry.register({
|
||||
subtitle: props.input.filePath ? getFilename(props.input.filePath) : "",
|
||||
args,
|
||||
}}
|
||||
// kilocode_change start
|
||||
onSubtitleClick={
|
||||
data.openFile && props.input.filePath ? () => data.openFile!(props.input.filePath) : undefined
|
||||
}
|
||||
// kilocode_change end
|
||||
/>
|
||||
<For each={loaded()}>
|
||||
{(filepath) => (
|
||||
<div data-component="tool-loaded-file">
|
||||
<div
|
||||
data-component="tool-loaded-file"
|
||||
classList={{ clickable: !!data.openFile }} // kilocode_change
|
||||
onClick={() => data.openFile?.(filepath)} // kilocode_change
|
||||
>
|
||||
<Icon name="enter" size="small" />
|
||||
<span>
|
||||
{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 (
|
||||
<BasicTool
|
||||
{...props}
|
||||
@@ -1119,11 +1153,27 @@ ToolRegistry.register({
|
||||
<div data-slot="message-part-title-area">
|
||||
<div data-slot="message-part-title">
|
||||
<span data-slot="message-part-title-text">{i18n.t("ui.messagePart.title.edit")}</span>
|
||||
<span data-slot="message-part-title-filename">{filename()}</span>
|
||||
{/* kilocode_change start */}
|
||||
<span
|
||||
data-slot="message-part-title-filename"
|
||||
classList={{ clickable: !!data.openFile }}
|
||||
onClick={handleFileClick}
|
||||
>
|
||||
{filename()}
|
||||
</span>
|
||||
{/* kilocode_change end */}
|
||||
</div>
|
||||
<Show when={props.input.filePath?.includes("/")}>
|
||||
<div data-slot="message-part-path">
|
||||
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
|
||||
{/* kilocode_change start */}
|
||||
<span
|
||||
data-slot="message-part-directory"
|
||||
classList={{ clickable: !!data.openFile }}
|
||||
onClick={handleFileClick}
|
||||
>
|
||||
{getDirectory(props.input.filePath!)}
|
||||
</span>
|
||||
{/* kilocode_change end */}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<BasicTool
|
||||
{...props}
|
||||
@@ -1172,11 +1230,27 @@ ToolRegistry.register({
|
||||
<div data-slot="message-part-title-area">
|
||||
<div data-slot="message-part-title">
|
||||
<span data-slot="message-part-title-text">{i18n.t("ui.messagePart.title.write")}</span>
|
||||
<span data-slot="message-part-title-filename">{filename()}</span>
|
||||
{/* kilocode_change start */}
|
||||
<span
|
||||
data-slot="message-part-title-filename"
|
||||
classList={{ clickable: !!data.openFile }}
|
||||
onClick={handleFileClick}
|
||||
>
|
||||
{filename()}
|
||||
</span>
|
||||
{/* kilocode_change end */}
|
||||
</div>
|
||||
<Show when={props.input.filePath?.includes("/")}>
|
||||
<div data-slot="message-part-path">
|
||||
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
|
||||
{/* kilocode_change start */}
|
||||
<span
|
||||
data-slot="message-part-directory"
|
||||
classList={{ clickable: !!data.openFile }}
|
||||
onClick={handleFileClick}
|
||||
>
|
||||
{getDirectory(props.input.filePath!)}
|
||||
</span>
|
||||
{/* kilocode_change end */}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -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({
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
<span data-slot="apply-patch-file-path">{file.relativePath}</span>
|
||||
{/* kilocode_change start */}
|
||||
<span
|
||||
data-slot="apply-patch-file-path"
|
||||
classList={{ clickable: !!data.openFile }}
|
||||
onClick={(e: MouseEvent) => {
|
||||
if (!data.openFile) return
|
||||
e.stopPropagation()
|
||||
data.openFile(file.filePath)
|
||||
}}
|
||||
>
|
||||
{file.relativePath}
|
||||
</span>
|
||||
{/* kilocode_change end */}
|
||||
<Show when={file.type !== "delete"}>
|
||||
<DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} />
|
||||
</Show>
|
||||
|
||||
@@ -52,6 +52,8 @@ export type SessionHrefFn = (sessionID: string) => string
|
||||
|
||||
export type SyncSessionFn = (sessionID: string) => void | Promise<void>
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -461,6 +461,26 @@ async function highlightCodeBlocks(html: string): Promise<string> {
|
||||
|
||||
export type NativeMarkdownParser = (markdown: string) => Promise<string>
|
||||
|
||||
// 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 `<a href="${href}"${titleAttr} class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`
|
||||
},
|
||||
// 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 `<code class="file-link" data-file-path="${file.path}"${lineAttr}${colAttr}>${text}</code>`
|
||||
}
|
||||
return `<code>${text}</code>`
|
||||
},
|
||||
// kilocode_change end
|
||||
},
|
||||
},
|
||||
markedKatex({
|
||||
|
||||
Reference in New Issue
Block a user