Skip to content

(sessions): list the files a session touched, including outside any repository (#309) - #423

Merged
devsuitup merged 4 commits into
mainfrom
feat/309-touched-files
Oct 3, 2026
Merged

devsuitup merged 4 commits into
mainfrom
feat/309-touched-files

Conversation

@devsuitup

@devsuitup devsuitup commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

What

A Touched tab in the file panel (header toggle next to Changes) lists the files a local session's file tools touched, from its own transcript and its subagents'. It works outside any git repository. Closes #309.

  • Source: assistant tool_use blocks named Edit, Write, MultiEdit, NotebookEdit (file_path, or notebook_path). Parent transcript plus the subagents found by enumerateSessionFiles (both layouts).
  • Each row is checked against the disk when the list is built: present, gone, not-file, unreadable (also: no answer in 3 s), refused. A gone file says so instead of failing to open.
  • Rows are merged per path: tools, call count, and who made the calls (session, or subagent type and short id).
  • The tab states its coverage where it is read (#touched-coverage): it is a lower bound, Bash-made changes are not listed. The text is in the DOM while loading, on error and when the list is empty, and a test pins it. An empty list reads "No files touched by the file tools".
  • Not in the PR: Changes panel untouched; no Bash parsing; no attribution between sessions sharing a directory.

Trust

Paths in a transcript are attacker-influenced (a sandboxed session writes its own).

  • Relative paths resolve only against a cwd accepted by verifiedTranscriptCwd ((sessions): trust a transcript cwd only if it encodes back to its folder (#385) #419), per transcript: a subagent in a worktree leaves its relative paths unresolved, shown with the reason and not openable.
  • Not followed, listed as unresolved (C0, DEL, C1, U+2028/2029 and every \p{Cf} count as control characters: bidi overrides and isolates, zero-width, tag characters; the unresolved text shows them as visible escapes and paths render in a bidi isolate): UNC / \\?\ / \\.\ forms (a stat there reaches the network), a rooted path with no drive, a drive-relative path, ~, control characters, over 4096 characters.
  • Every resolved path goes through isSensitivePathAsync before its stat, both under one 3 s timeout (a timeout is unreadable); after 8 timed-out checks no new file-system call is issued and the rest are unreadable, so planted paths on an offline drive cannot starve the thread pool; a hit, or a check that throws, lists the row as refused and never stats it. Nothing is read by the listing.
  • Opening is not new: a click calls readFileForPanel (read-file-for-panel and its guards) and the ordinary file tab, only for a row that is both openable and present. A source check pins that the view calls no other window.api method than sessionTouchedFiles and readFileForPanel.
  • The cached folder must be a plain name before it is joined to the projects directory; a sub: id and a remote folder are refused.
  • Bounds: 500 resolved + 500 unresolved rows (omitted is a counter), concurrency 8, 256 MiB of transcript in total (coverage.truncated), a tool-call line over 4 MiB and any line over 32 MiB skipped and counted (coverage.skippedLines).

Choices the issue left open

  • Placement: its own tab and toggle, not a section of Changes (changing Changes is out of scope).
  • Remote sessions: the issue is silent, so local only; the IPC answers reason: 'remote' and the tab shows it.
  • Refresh: on open and on the refresh button only, not on each busy-to-idle edge (a refresh reads the transcripts and stats up to 500 paths).
  • A row opens the plain file viewer, not the Changes diff.
  • A transcript is a record of intent: a refused write is listed (shown gone if the file does not exist). Tool results are not read.

Tests

  • test/session-touched-files.test.js (37): extraction (four tools, ignored tools and user turns, malformed lines), path resolution (also with path.win32), the walk over real temp directories (outside-repo file, subagents in both layouts, other sessions excluded, merge, gone / not-file / unreadable / refused, relative paths with and without a verified cwd, caps, byte budget, stat timeout and concurrency), and target resolution.
  • test/dom-file-panel-touched.test.js (16, jsdom, through the real file-panel.js): the toggle and rows, the coverage statement in every state, state labels, no click on a non-openable row, unresolved rows, a refused read, notes for omitted / subagents / malformed / truncated, markup shown as text, stale open, hiding the previous container.
  • test/touched-files-wiring.test.js (5): main handler guard wiring, preload channel, script order, the view's API surface.
  • test/header-controls.test.js and test/terminal-manager-harness.js updated for the new control.
  • Mutations, one per guard (34 run, all killed except one equivalent): notebook key, assistant-only, ~, UNC form, relative without verified cwd, control characters, sensitive refusal, fail-closed sensitivity, folder containment, verified cwd, stat concurrency, stat timeout, byte budget, own-subagents filter, file cap, dedupe, malformed count, gone vs unreadable, not-file, session id validation, remote refusal, openable flag, present state, coverage note, open stale guard, text vs HTML for both path kinds, click guard, refused read, truncated and omitted notes, previous-container hiding, main handler's guard. The one survivor is the first of two length checks on a raw path, which the second check after resolution makes equivalent.
  • Review follow-up mutations, all killed: whole-check timeout, timed-out gate, \p{Cf}, C1, visible escape, per-line bound, bidi isolate CSS, skipped-lines note.
  • task -d <worktree> check: lint 0 errors (349 warnings, none in the new files), 3101 + 120 tests, 0 failures.

Not verified

…epository

Changes answers what differs in a working tree, not what a session touched:
a file outside any repository, a directory with no git, a tree shared by
several sessions and a change since committed or reverted all escape it.

The new Touched tab reads the session's transcript and its subagents' for
Edit, Write, MultiEdit and NotebookEdit calls, checks each path against the
disk (present, gone, not a file, unreadable, refused) and states in the
panel that it is a lower bound: Bash-made changes are not listed.

Paths come from transcripts, so they are treated as untrusted: relative
paths resolve only against a cwd that verifiedTranscriptCwd accepts, UNC,
device and drive-relative forms are not followed, credential locations are
listed as refused without a stat, and a click goes through
read-file-for-panel and its guards.

Closes #309
@devsuitup

Copy link
Copy Markdown
Owner Author

Reviewing 97272d0 (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Adversarial review at 97272d0: no security blocker on IPC input or opening. The renderer sends only a session id and main derives the folder. A click opens only through readFileForPanel and its guards, and nothing persists the probe results. Being fixed before merge: (F1, reproduced) isSensitive runs outside the 3 s timeout, so an offline mapped drive can hang the listing and starve the fs thread pool. (F2, reproduced) bidi, C1 and format characters pass, so a ‮ path displays reversed before a click. Also: an uncapped overflow Set, a 32 MiB per-line parse cap, comment length and the eslint block scope.

…rmat characters

The sensitivity guard ran outside the stat timeout, so a path on an offline
mapped drive could hold the listing forever. The guard and the stat now run
under one timeout, a timeout reads as unreadable, and after a few timed-out
checks no new call is issued, which bounds the threads planted paths can hold.

Paths with bidi overrides or isolates, marks, zero-width, C1 or other format
characters are no longer resolved, and an unresolved path shows them as
visible escapes inside a bidi isolate. A tool-call line over 4 MiB is skipped
and counted, the overflow past the row cap is a counter, and the lint
exceptions for the new view are narrowed to its three hooks.

Refs #309
@devsuitup

Copy link
Copy Markdown
Owner Author

Reviewing 06eeda7 (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Re-review at 06eeda7 (delta from 97272d0): 0 blocking. The guard and the stat run under one timeout, and the timed-out gate is per refresh: mutating it away issues 60 calls instead of about 10. C0, C1, U+2028/2029 and \p{Cf} (bidi, BOM, zero-width, tag characters) are refused. They render escaped, astral code points included, and both row types get unicode-bidi: isolate. The 4 MiB skip is counted in the summary. The eslint exemptions are narrowed to /* exported */. Minor: the gate is order-dependent, since 8 slow paths early in the list mark the rest unreadable (a documented trade-off). A path cut at 300 characters can leave a lone surrogate, which renders as U+FFFD (display only).

The timer that bounds a disk check was unref'd, so a check that never
answers left nothing alive: on Node 20 and 22 the test runner saw an empty
event loop with a pending promise and cancelled the file. The timer is now
referenced, and still cleared as soon as the race settles.

Refs #309
@devsuitup

Copy link
Copy Markdown
Owner Author

CI green at 6edb32d. The Node 20/22 cancellation came from a timer.unref() in withTimeout: a check that never answers left the event loop empty. The fix drops the unref, and the timer is still cleared once the race settles. Reproduced under Node 22 with the CI command (c8), red at 06eeda7 and green after. Ready to merge.

@devsuitup
devsuitup merged commit ec76431 into main Oct 3, 2026
11 checks passed
@devsuitup
devsuitup deleted the feat/309-touched-files branch October 3, 2026 07:23
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.

(sessions): list the files a session touched, including outside any repository

1 participant