Move the per-project SESSIONS list out of the Agent Manager sidebar and
behind a per-project history button that opens the sessions view scoped to
that project. Each session row offers direct actions to resume it in a new
worktree or in the project's local tabs, and sessions are no longer part of
the sidebar keyboard navigation order.
* feat(ui): parse file-path candidates with line and column suffixes
Replace the regex-based parseFilePath with extractSuffix and
normalizeCandidatePath helpers, and make extractFilePathFromHref return
structured { path, line, column } data instead of a bare string.
This shifts inline-code detection from "does this string look like a
file?" to "extract a candidate path so the filesystem can confirm it
later", which also handles :line / :line:col and :start-end line ranges,
a/ and b/ diff prefixes, and Windows drive paths. Unit tests are
rewritten to cover the new helpers and the structured href result.
* feat(ui): render and open clickable file links
Mark every inline code span as a file-link candidate during markdown
rendering, then validate candidates against the filesystem after render
and promote confirmed files to clickable links. Results are cached so
morphdom re-renders during streaming resolve without extra round-trips,
and unconfirmed candidates fall back to plain code.
Clicking a confirmed link (or a markdown link whose href is a file path)
opens the file at the referenced line/column. Markdown file-path links
also get distinct styling.
* fix(ui): tighten file-link detection and streaming validation
Address kilo-code-bot review on #11219:
- preserve top-level a/ and b/ directories instead of treating them as diff
prefixes for every candidate
- only treat markdown hrefs as file targets when the href still looks like a
file path after removing query/fragment and parsing line suffixes
- observe text and file-link attribute mutations during streaming so morphdom
in-place updates re-run validation
* fix(ui): preserve common extensionless file hrefs
Address kilo-code-bot review on #11219: keep the href guard that avoids
routing extensionless docs links through openFile, but allow common
extensionless workspace files (LICENSE, Dockerfile, Makefile) to use the
file-open path. Mirror the helper in kilo-ui and cover the allowlist plus
unknown extensionless rejection in file-path tests.
* fix(ui): scope file-link validation to session and harden against races
Addresses review feedback on PR #11219 (marius-kilocode):
- validateFiles requests now carry an explicit sessionID; the extension
resolves the workspace directory from that id instead of its own
mutable currentSession, so a session switch mid-request can't validate
candidates against the wrong worktree.
- Introduce a module-level file-link-validator singleton shared by every
TextPartDisplay instance: in-flight requests for the same path are
coalesced instead of duplicated, and the validation cache lives outside
component state so virtualization unmount/remount no longer discards it
and repeats filesystem checks for unchanged history.
- Bound filesystem check concurrency in file-links.ts (mapLimit, cap 8)
instead of firing an unbounded Promise.all per validateFiles call.
- Guard DOM promotion against stale responses: only mutate a candidate
element if it's still connected and still represents the path that was
validated, so a slow response can't clobber a newer candidate that
reused the same node during streaming.
- A validateFiles timeout now rejects instead of resolving with [], and
is retried with backoff before giving up as an unconfirmed (not
negative) result, so it's never cached as "file doesn't exist".
* test(ui): update bidi contract test for file-link candidate markup
The codespan renderer added by this PR marks non-trivial inline code
spans as file-link candidates, so the plain <code dir="auto"> markup
this test expected no longer matches. Update the expectation to the
candidate-wrapped output while still asserting dir="auto" is preserved.
* refactor(ui): harden and de-duplicate clickable file-link validation
Follow-up review fixes on PR #11219:
- Escape the `data-file-candidate` attribute value in the codespan
renderer. It's derived from raw model output, so a stray `"` could
break out of the attribute (neutralized by the DOMPurify pass, but now
fixed at generation time for correctness + defense-in-depth). Adds
escapeAttribute() to the shared file-path module.
- Only treat code spans that look like file paths (an extension or a
known extensionless name) as candidates, so bare identifiers like
`useState`/`null` no longer trigger filesystem probes during
streaming. Exposes looksLikeFilePath() for this.
- De-duplicate file-path.ts: expose it via the `@opencode-ai/ui/file-path`
export and import it from kilo-ui instead of keeping a byte-identical
copy with dead exports. Deletes packages/kilo-ui/src/file-path.ts.
- Keep target/rel on file-path markdown links so the shared opencode
consumer's navigation/security behavior is unchanged; Kilo's click
handler still intercepts via preventDefault.
- Cache negative validation results with a short TTL so a file created
mid-session becomes clickable on the next scan (positives never
expire). Retry/TTL are now tunable via checkFile options.
- Filter the MutationObserver so promote()'s own class/attribute writes
no longer schedule an extra no-op validation pass.
- Add unit tests for looksLikeFilePath, escapeAttribute, the codespan
candidate/escaping behavior, and the validator's dedup/batching/cache/
TTL/retry paths.
* fix(ui): scope file-link open to its session and harden rendering
Thread an explicit sessionID through the click-to-open path (OpenFileFn,
openFile webview message, editor-actions) so opening a validated file link
resolves against the session it was rendered for, mirroring validateFiles and
avoiding the wrong worktree during an Agent Manager session switch.
Also: re-scan file-link candidates once streaming stops so candidates left
'unknown' after a mid-stream validation timeout get a final pass; make the
validator cache LRU (refresh recency on read) instead of FIFO; and escape
href/title in the markdown link renderer as defense-in-depth alongside
DOMPurify.
* feat(ui): make extensionless and bare-token file references clickable
The candidate heuristic previously required a dotted basename or one of three hardcoded extensionless names (Dockerfile/LICENSE/Makefile), so real files like 'install', 'run-script', or 'configure' were never validated and stayed plain.
Split the heuristic: keep looksLikeFilePath strict for markdown-link hrefs (decided statically, no filesystem check, so a false positive would hijack a doc route), and add a permissive looksLikeCandidate for inline code spans, which ARE validated against the filesystem afterward. A code span now qualifies unless it is empty or contains code punctuation, so extensionless files and separator paths become clickable while expressions like useState() stay plain.
To keep the broader candidate set from multiplying probes, defer candidate validation until a message finishes streaming instead of running it on every morphdom frame; completed history still validates on mount.
* refactor(ui): isolate file-link negative cache from confirmed positives
Addresses kilo-code-bot's suggestion on the widened looksLikeCandidate heuristic: with nearly every bare token now probed, transient negatives (useState, null, ...) dominated the single fixed-size LRU and could evict confirmed-file entries, forcing re-probes on revisit.
Split the validator cache into a positives Set (confirmed files, never expire) and a negatives Map (TTL'd), each LRU-bounded independently (2000 / 1000). Negative-probe churn now only evicts other negatives, a real clickable file is never pushed out by non-file noise, and eviction stays O(1) with no scan.
* feat(ui): validate file links during streaming with a per-element settle debounce
Replaces the defer-until-complete pass with incremental validation so links appear while a message streams (requested in review), without the partial-path probe storm that defer avoided.
Because morphdom keeps the same <code> node as a healed streamed path grows (src/fo -> src/foo.ts), each candidate carries a per-element debounce: a probe fires only after that element's path has been stable for 400ms, and re-arms whenever the path changes, so intermediate partials are superseded before hitting the filesystem. Settled candidates behind the streaming frontier light up mid-stream; on completion all pending candidates flush immediately. Combined with the split positive/negative caches, a growing token now costs one probe for its final value instead of one per frame.
---------
Co-authored-by: Sylwester Liljegren <sylwester.liljegren@softronic.se>