Skip to content

Live file watching for the Code tab (server streaming + client transport insulation) #616

@srid

Description

@srid

Make the Code tab (right-panel diff/browse) stay current as the agent edits files. This combines two workstreams that only pay off together:

  • Server side — the outstanding scope of Phase 5 of Code tab: diff review & file browser #514 ("Live file watching"): add streaming endpoints that emit snapshot-then-deltas on fs.watch events, scoped to the terminal's repo root with a debounce.
  • Client sideitem 1 of Code-tab subsystem: three structural fixes from Hickey+Lowy review #615 (CodeTab transport insulation): route CodeTab's RPC call sites through the existing stream namespace in packages/client/src/rpc/rpc.ts instead of client.* directly, so ClientRetryPlugin handles reconnects and the snapshot-then-deltas contract is consumed via createSubscription (the pattern already used by preferences, activity, session, metadata).

Shipping them separately: item 1 of #615 alone is a pure alias with zero user value (the endpoints stay pull-shaped); Phase 5 of #514 alone leaves the client on client.* and would have to be retrofitted. Shipping together: one PR, one structural migration.

Note (post-#708, the @pierre/diffs + @pierre/trees migration). That PR removed fs.listDir and the oldContent/newContent fields of GitDiffOutput (zero callers after Pierre took over). Scope below has been updated accordingly: fs.onDirChange is dropped in favor of fs.onListAllChange, and git.onDiffChange's payload is slimmer.

Scope

Server

Add streaming variants in packages/server/src/router.ts + packages/common/src/contract.ts:

  • git.onStatusChange(input) — yields current GitStatusOutput on subscribe, then re-emits on fs.watch events that affect the tracked/untracked set.
  • git.onDiffChange(input) — yields current GitDiffOutput for a (repoPath, filePath, mode, oldPath?) tuple, then re-emits on writes to the file. Payload is now slimmer (just oldFileName / newFileName / hunks) since oldContent / newContent were dropped in feat(code-tab): swap file tree + diff view for @pierre/trees and @pierre/diffs #708.
  • fs.onListAllChange(input) — yields current FsListAllOutput (flat path list) on subscribe, then re-emits when the working-tree path set changes (file added / removed / renamed, respecting .gitignore). Replaces what was originally scoped as fs.onDirChange — Pierre's @pierre/trees consumes a flat path list, not per-directory entries.
  • fs.onFileChange(input) — yields current FsReadFileOutput for a file, then re-emits on writes.

Implementation:

  • One shared fs.watch per repo root, refcounted by active subscriber count (integration-perf rule: no per-subscription watchers). Scope to the repo root; filter events by the subscriber's path-of-interest.
  • Debounce window ~150ms trailing-edge (matches the DEBOUNCE_MS in watchGitHead and the TRANSCRIPT_DEBOUNCE_MS pattern in agent session watchers).
  • Each handler yields snapshot first, then deltas — per rules/streaming.md invariant 2. Re-subscribe on reconnect (handled automatically by ClientRetryPlugin) restores full snapshot with no delta replay.
  • .git/ excluded from the watch (constant noise from git's own plumbing).

Two spots to decide:

  • Delta vs full replacement shape. git.onStatusChange could yield full GitStatusOutput each time (implicit replacement, simplest, matches onMetadataChange pattern) or {kind:"snapshot", ...} | {kind:"delta", ...}. Default to implicit replacement for all four endpoints — deltas only pay off if a single response's bytes dominate, which they don't for status/list-all; they might for onFileChange if large files are in play. Start implicit; add discriminated-union later if profiling shows it matters. (See client-side note below for fs.onListAllChange — Pierre's incremental mutation API is the natural delta consumer there.)
  • Pull endpoints stay or go. Keep git.status/git.diff and fs.listAll/fs.readFile as one-shot RPCs alongside the streaming variants — the streaming ones are additive. After the CodeTab migration, audit and delete pull endpoints with no remaining consumers (likely all four; worktree* ops stay one-shot regardless).

Client

packages/client/src/rpc/rpc.ts — add the four streaming entries to the stream namespace (with STREAM_RETRY context so reconnects work).

packages/client/src/right-panel/CodeTab.tsx and BrowseFileView.tsx — replace each createResource with createSubscription(() => stream.xxx(input), { onError }). Today there are three subscriptions in CodeTab.tsx (status, allPaths, diff) and one in BrowseFileView.tsx (readFile). Remove the refresh button (handleRefresh + refetchStatus / refetchDiff / refetchAll) — live watching makes manual refresh redundant. Wire onError to toast.error per rules/code-police-rules.md:subscription-must-surface-errors.

Reactive derivation becomes automatic: select-a-file → selectedPath() changes → subscription input changes → SolidJS re-subscribes → new snapshot arrives. No manual refetch plumbing.

Pierre incremental-mutation optimization (second pass). Pierre's FileTree exposes tree.add(path), remove(path), move(from, to), batch(ops). For fs.onListAllChange specifically, the first pass keeps things simple: full-replacement snapshots dispatched into tree.resetPaths(paths) (matches the current pattern). The second pass, only if the all-paths reset proves expensive on big repos, switches that one endpoint to discriminated-union deltas ({kind:"snapshot"} | {kind:"add", path} | {kind:"remove", path} | {kind:"move", from, to}) and dispatches into Pierre's per-path methods. Don't speculate the optimization in v1.

E2e

Update packages/tests/features/code-tab.feature to assert that a write from outside the right panel is reflected in the diff list / file tree within a bounded wait, without clicking refresh.

Sequencing

  • Pre-impl Hickey+Lowy pass (mandatory) on the concrete plan before any code — two recent issues (Structural cleanup pass before codebase grows (Hickey + Lowy review) #601 items 3 and 5) were whole-codebase-sweep false positives rescued by focused pre-impl review; item 4 (refactor(git): kolu-git owns the resolve+watch compose loop #613) had its design upgraded from option A to option B the same way. This node's plan sketches a shape; the pre-impl pass ranks it against alternatives and catches leaks.
  • First milestone: ship the server streaming endpoints + client stream.* wrapper + CodeTab migration in one PR. E2e in the same PR.
  • Follow-ups: kill the pull endpoints if no consumer remains; switch fs.onListAllChange to discriminated-union deltas + Pierre per-path mutations if profiling warrants.

Out of scope

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions