Skip to content

feat(ui): validate inline code spans against filesystem to enable cli… - #10340

Closed
sylwester-liljegren wants to merge 1 commit into
Kilo-Org:mainfrom
sylwester-liljegren:feat/file-links-fs-validation
Closed

feat(ui): validate inline code spans against filesystem to enable cli…#10340
sylwester-liljegren wants to merge 1 commit into
Kilo-Org:mainfrom
sylwester-liljegren:feat/file-links-fs-validation

Conversation

@sylwester-liljegren

Copy link
Copy Markdown
Contributor

…ckable file links

Make file references in agent responses clickable by validating inline code spans against the filesystem. Code spans that match real files in the workspace become clickable links that open the file at the referenced line. Non-existent paths remain as plain code.

The implementation introduces a two-phase approach: markdown rendering marks all code spans as file-link candidates, then post-render validation checks each candidate against the workspace filesystem. Confirmed files are promoted to clickable links with cached results to avoid redundant checks during re-renders.

Changes include:

  • New extractSuffix() and normalizeCandidatePath() utilities for parsing and normalizing candidate paths
  • Updated extractFilePathFromHref() to return structured path/line/column data
  • Post-render validation in message-part component with caching
  • VS Code extension filesystem stat checks and fallback workspace search for dead links
  • Message protocol extensions for validateFiles/validateFilesResult round-trips
  • CSS styling for file-path links with hover effects
  • Comprehensive test updates for new path parsing logic

Context

File references in agent responses (e.g. src/foo.ts:42, LICENSE, .gitignore) were not reliably clickable. The original approach used regex-based auto-detection on inline code spans, which produced false positives (Effect.gen, @tsconfig/bun, .env when it doesn't exist) and false negatives (extensionless files like LICENSE, Makefile). This PR replaces regex heuristics with filesystem validation — the extension stat-checks every code span candidate against the workspace, so only real files become clickable links.

Implementation

Every backtick code span is a candidate. The marked.tsx codespan renderer strips an optional :line[-range][:col] suffix, normalizes the remaining text into a workspace-relative path (prepending ./ for bare names like LICENSE), and renders the element with a file-link-candidate CSS class and data-file-candidate attribute.

Post-render validation via extension round-trip. After each markdown render, message-part.tsx collects all candidate paths from the DOM, sends them in a single validateFiles message to the extension. KiloProvider.handleValidateFiles stat-checks each path against the workspace root, rejects directories (FileType.Directory), and responds with the list of paths that are actual files.

Promote or strip. On response, candidates that exist as files get promoted to .file-link (clickable, dotted underline). Candidates that don't exist get their marker attributes removed and stay as plain <code>. Results are cached in a Map<string, boolean> so that when morphdom re-renders the DOM during streaming (resetting elements back to file-link-candidate), the effect can immediately re-promote from cache without another round-trip.

Fallback for handleOpenFile. If the exact path doesn't exist when clicked (edge case), the extension searches the workspace by filename, offers a QuickPick for multiple matches, or shows a "File not found" warning.

Markdown links also supported. The link renderer in marked.tsx detects file-path hrefs via extractFilePathFromHref and applies a file-path-link class with matching visual styling. The click handler destructures { path, line, column } from the href.

Tradeoffs:

  • Every code span triggers a stat check, batched per render. Typical responses have 20-50 spans; each stat is microseconds on SSD. The round-trip is a single postMessage per render cycle.
  • Brief flash possible: code spans render as plain code, then confirmed files "light up" when the validation response arrives (typically <100ms). The cache eliminates this on re-renders.
  • Zero prompt files modified — no upstream merge conflict risk from prompt changes.

Screenshots

N/A

How to Test

  1. Open any project in VS Code with the extension loaded
  2. Start a new Kilo Code session and ask: "Give me a tour of this project's structure. Mention specific files with line numbers, extensionless files like LICENSE or Makefile, dotfiles like .gitignore, and also mention things that aren't files like Effect.gen or @tsconfig/bun"
  3. In the response, verify:
    • Files that exist (e.g. packages/opencode/src/index.ts, LICENSE, .gitignore) appear with dotted underlines and are clickable — clicking opens the file in the editor
    • Files with line numbers (e.g. src/foo.ts:42) open at the correct line
    • Non-existent paths (e.g. .env if not present) and non-file code spans (Effect.gen, @tsconfig/bun, useState) render as plain monospace code with no underline
    • Directories mentioned by the AI are not clickable
  4. During streaming, verify links remain clickable (morphdom cache test) — click a file link while the response is still generating
  5. Click a file link that doesn't exist at the exact path — verify the "File not found" warning or QuickPick appears

}, 3000)
})
}
window.addEventListener("message", (event) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Missing onCleanup for window.addEventListener — potential memory leak

The event listener registered here is never removed. Although DataBridge is a top-level component that currently never unmounts during the session, the lack of cleanup is a latent leak: if the component ever remounts (e.g. hot module reload, future refactors, tests) a new listener accumulates while the old one is never removed, causing duplicate validateFilesResult handling.

Consistent with the pattern on lines 322–323 (another window.addEventListener in the same file), wrap this in an onMount/onCleanup pair:

onMount(() => {
  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 ?? [])
      }
    }
  }
  window.addEventListener("message", handler)
  onCleanup(() => window.removeEventListener("message", handler))
})


// File existence validation for code span candidates
const pending = new Map<string, (existing: string[]) => void>()
let counter = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: let counter = 0 — prefer const with a ref object per the project style guide

The style guide says to avoid let statements. Since counter is mutated in place, use a single-element object instead:

Suggested change
let counter = 0
const counter = { n: 0 }

And update the increment to const id = \vf-${++counter.n}``.

// Candidates that exist as files get promoted to .file-link; others stay plain code.
// Results are cached so morphdom re-renders can be resolved immediately.
let bodyRef: HTMLDivElement | undefined
const cache = new Map<string, boolean>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: cache is never cleared — unbounded memory growth during long sessions

The Map<string, boolean> accumulates one entry per unique candidate path encountered across the lifetime of this component instance. In a long session with many assistant messages referencing many distinct paths, this can grow large. Consider bounding the cache size (e.g. LRU with a cap of ~500 entries), or clearing it when the session changes.

elements.get(p)!.push(el)
}
if (!pending.length || !data.validateFiles) return
data.validateFiles(pending).then((existing) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Unhandled rejection from validateFiles promise

If data.validateFiles rejects (e.g. the extension returns an error or the promise is rejected for any reason), the rejection is silently swallowed here. Add a .catch to avoid unhandled promise rejections:

Suggested change
data.validateFiles(pending).then((existing) => {
data.validateFiles(pending).then((existing) => {

Change to:

    data.validateFiles(pending)
      .then((existing) => {
        const set = new Set(existing)
        for (const p of pending) cache.set(p, set.has(p))
        for (const [p, els] of elements) {
          for (const el of els) promote(el, p, set.has(p))
        }
      })
      .catch(() => { /* validateFiles timed out or failed — candidates stay as plain code */ })

@kilo-code-bot

kilo-code-bot Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

The previous 4 issues are all resolved. Two new issues found in the incremental diff (KiloProvider.ts refactor + new file-links.ts).

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-vscode/src/KiloProvider.ts 2989 Missing .catch() on handleValidateFiles — unhandled rejection if validateFiles or postMessage throws; webview silently times out after 3s
packages/kilo-vscode/src/kilo-provider/file-links.ts 39 findFiles().then() and show() calls have no .catch() — errors from openTextDocument, showTextDocument, and findFiles are silently swallowed
Previously Resolved Issues (carried forward)
Previous Issue Status
App.tsx — Missing onCleanup for window.addEventListener (WARNING) ✅ Fixed — onMount/onCleanup added
App.tsxlet counter = 0 instead of ref object (SUGGESTION) ✅ Fixed — changed to const counter = { n: 0 }
message-part.tsx — Unbounded cache Map (WARNING) ✅ Fixed — MAX_CACHE = 500 cap with FIFO eviction added
message-part.tsx — Unhandled rejection from validateFiles (SUGGESTION) ✅ Fixed — .catch() added
Files Reviewed (2 files changed in incremental diff)
  • packages/kilo-vscode/src/KiloProvider.ts — 1 issue (missing .catch() on handleValidateFiles)
  • packages/kilo-vscode/src/kilo-provider/file-links.ts — 1 issue (missing .catch() on async chains)

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-4.6 · 126,811 tokens

@sylwester-liljegren
sylwester-liljegren force-pushed the feat/file-links-fs-validation branch 2 times, most recently from 02bb498 to 7c08bb7 Compare May 17, 2026 23:58
const root = this.getWorkspaceDirectory(this.currentSession?.id)
fileLinks
.validateFiles(root, paths)
.then((existing) => this.postMessage({ type: "validateFilesResult", id, existing }))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Missing .catch() on handleValidateFiles — unhandled promise rejection risk

If fileLinks.validateFiles() rejects unexpectedly (e.g. an unhandled vscode API error), or if this.postMessage() throws, the rejection is silently lost. The webview's validateFiles call will then time out after 3 seconds and resolve to [], which is recoverable — but the silent failure makes debugging harder and causes unnecessary 3-second delays on every affected render during that error condition.

Add a .catch() to log the failure:

fileLinks
  .validateFiles(root, paths)
  .then((existing) => this.postMessage({ type: "validateFilesResult", id, existing }))
  .catch((err) => console.error("[Kilo New] handleValidateFiles failed:", err))

() => show(uri),
() => {
const name = filePath.split(/[\\/]/).pop() || filePath
vscode.workspace.findFiles(`**/${name}`, "**/node_modules/**", 5).then((matches) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: findFiles().then() has no .catch() — errors silently swallowed

If vscode.workspace.findFiles() rejects (e.g. workspace not ready, cancellation), the error is silently lost. Same issue exists on the show() calls at lines 35 and 41 — openTextDocument().then(showTextDocument) rejections are also unhandled.

Add .catch() handlers:

vscode.workspace.findFiles(`**/${name}`, "**/node_modules/**", 5).then((matches) => {
  // ... existing logic ...
}).catch((err) => console.error("[Kilo New] findFiles failed:", err))

And on the show helper:

const show = (target: vscode.Uri) =>
  vscode.workspace.openTextDocument(target)
    .then((doc) => vscode.window.showTextDocument(doc, opts))
    .catch((err) => console.error("[Kilo New] openFile show failed:", err))

@sylwester-liljegren
sylwester-liljegren force-pushed the feat/file-links-fs-validation branch from 7c08bb7 to 75e6767 Compare May 18, 2026 00:10
…ckable file links

Make file references in agent responses clickable by validating inline code spans against the filesystem. Code spans that match real files in the workspace become clickable links that open the file at the referenced line. Non-existent paths remain as plain code.

The implementation introduces a two-phase approach: markdown rendering marks all code spans as file-link candidates, then post-render validation checks each candidate against the workspace filesystem. Confirmed files are promoted to clickable links with cached results to avoid redundant checks during re-renders.

Changes include:
- New `extractSuffix()` and `normalizeCandidatePath()` utilities for parsing and normalizing candidate paths
- Updated `extractFilePathFromHref()` to return structured path/line/column data
- Post-render validation in message-part component with caching
- VS Code extension filesystem stat checks and fallback workspace search for dead links
- Message protocol extensions for validateFiles/validateFilesResult round-trips
- CSS styling for file-path links with hover effects
- Comprehensive test updates for new path parsing logic
@johnnyeric

Copy link
Copy Markdown
Contributor

Hey @sylwester-liljegren, thanks for the detailed write-up. The context and testing steps are helpful.

One thing: this does affect visible UI behavior, so I don’t think screenshots are N/A here.

Could you please add before/after screenshots or a short screen recording showing:

  • existing file references rendered as clickable links
  • non-file inline code remaining as normal code
  • a click opening the referenced file / line
  • ideally one example during streaming, since the PR mentions morphdom/cache behavior

This would make the UI impact much easier to review.

Also, since this is a fairly large change with explicit trade-offs, touching rendering, protocol messages, filesystem validation, and click behavior, could you please consider splitting this PR into smaller reviewable pieces if possible?

For example, the path parsing/tests, the protocol/filesystem validation, and the UI rendering/click behavior could potentially be reviewed separately. That would make it much easier for us to review carefully and give concrete feedback.

@sylwester-liljegren

Copy link
Copy Markdown
Contributor Author

Hey @sylwester-liljegren, thanks for the detailed write-up. The context and testing steps are helpful.

One thing: this does affect visible UI behavior, so I don’t think screenshots are N/A here.

Could you please add before/after screenshots or a short screen recording showing:

  • existing file references rendered as clickable links
  • non-file inline code remaining as normal code
  • a click opening the referenced file / line
  • ideally one example during streaming, since the PR mentions morphdom/cache behavior

This would make the UI impact much easier to review.

Also, since this is a fairly large change with explicit trade-offs, touching rendering, protocol messages, filesystem validation, and click behavior, could you please consider splitting this PR into smaller reviewable pieces if possible?

For example, the path parsing/tests, the protocol/filesystem validation, and the UI rendering/click behavior could potentially be reviewed separately. That would make it much easier for us to review carefully and give concrete feedback.

Good points, @johnnyeric . I thought first that the modifications would only change what code spans were rendered as clickable links or not, hence this decision of not showing any screenshots. But I can fix this (in one of the smaller PRs to be created as per your recommendation).

I'll fix this during this week whenever I get the chance. After I have created the smaller PRs, then this initial PR may be closed in favor of those smaller PRs.

@sylwester-liljegren

Copy link
Copy Markdown
Contributor Author

@johnnyeric Alright, I have created two separate PR:s for this: #11218 and #11219.

#11218 focuses on the protocol for file link validation using the local filesystem, whereas #11219 focuses on path parsing and UI rendering in VS Code. This way, these two parts can be reviewed isolated from each other in their own PR.

Keep in mind that in the current state, #11219 contains not only path parsing and UI rendering changes, but also changes seen in #11218, which might make that PR look big initially. However, after #11218 has been merged, the number of files in #11219 will collapse to 7 files, making it easier to review #11219 later on.

So you may begin reviewing #11218, whereas I will gather some visuals for how the changes in #11219 will impact the UI as per your recommendation. As soon as #11218 has been merged and #11219 has been supplied with visuals, I'll open it for external review.

My recommendation is to keep this PR opened in the mean time until the new PRs have been merged/closed in case another organisation of PRs is desired for better review conditions.

@github-actions

Copy link
Copy Markdown
Contributor

To stay organized pull requests are automatically closed after 30 days of inactivity. If the pull request is still relevant please reopen it or create a fresh new one.

@github-actions github-actions Bot closed this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants