Skip to content

File peek phase 2: clicking and reading, a plain text viewer end to end #78

Description

@karngyan

Phase 2 of file peek. Phase 1 (#55) put the verbs on the wire and in the daemon; this phase puts a client in front of them and makes the feature real: hover a path in terminal output, it underlines if it exists, click it, read the file. Plain text only, zero new dependencies. Design: docs/superpowers/specs/2026-08-11-file-peek-design.md, sections "The browser" and "The viewer". Contract: spec/protocol.md.

Where it stands

The branch feat/file-peek-ui (on origin) holds the first two of eleven planned tasks: the paths.ts candidate matcher and the wrapped-line assembly, both with tests, plus the phase 2 plan doc docs/superpowers/plans/2026-08-11-file-peek-phase-2-clicking-and-reading.md. Start by rebasing that branch onto main and rereading the plan's own corrections (its last commit is "Correct the plan where execution proved it wrong").

Scope

Client methods. FlueClient gains stat(id, paths), read(id, path), and cancel(ref), with promises correlated by reqId, chunk routing by ref for binary frame 0x02, and the abandon dance for a viewer closed before its file answer lands (mirror attach's abandon in web/src/client/client.ts).

The detectLinks seam. web/src/emulator/types.ts gains detectLinks(detector: LinkDetector | null): void where a detector is {find(line), verify(candidates), open(candidate)}. xterm.ts implements it with registerLinkProvider. Nothing above the seam learns that xterm exists.

Wrapped lines. The link provider walks isWrapped backward and forward, assembles the logical line, matches against that, and maps ranges back to screen coordinates. A 90-character path on an 80-column phone spans two buffer rows; getting this wrong ships a feature that silently fails on the device it matters most on.

Candidates. web/src/lib/paths.ts: absolute (/x, ~/x), relative with a separator (./x, ../x, a/b), and bare filenames with an extension (CLAUDE.md). Trailing .,;:)]}"' stripped, optional :line[:col] captured. Generous matching on purpose; verification decides the underline.

Verification on hover. One stat per hovered line carrying every candidate on it. Cache per session and raw text: 30 s for a hit, 2 s for a miss. The short negative lifetime is deliberate: an agent says it is writing src/foo.ts, you hover a moment before it exists, and a long negative cache leaves the path dead for the session.

The viewer. A Radix dialog over the session in web/src/files/. Windowed rendering (monospace means fixed line height, so arithmetic, not a library), chunks paint as they arrive, header shows name, resolved directory, size, and a copy button, truncation is stated, and a :line suffix scrolls to the line and marks it. Plain text; Shiki and images are phase 3.

Tests

  • paths.test.ts: candidates, punctuation stripping, :line:col, wrapped-line assembly.
  • client.test.ts: stat, read, and cancel promises; chunk routing by ref; abandon before file.
  • Viewer: windowing, the truncation notice.
  • e2e (web/e2e): one relayed read of a real file. The 1 MiB cap and chunk pacing only exist for real over the relay, so that is the only place they are genuinely proven.
  • New web sources feed the Tailwind prose scanner through styles.build.test.ts; see the notes at the top of web/src/styles.css before writing comments.

Commands: cd web && pnpm vitest run && pnpm run lint, go test ./... untouched but run anyway.

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