From 20befa13209bfc0ae0141ff3fa089bcfe31f2570 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 22 Sep 2026 20:53:49 -0700 Subject: [PATCH] docs: terminal file viewer spec review and implementation plan Review the approved design against tauri 2.11.0, xterm 5.5.0, Vite 6.4.1 and the tree: record the verified multi-window, capability, CSP and exec facts, choose the save mechanism (stage in /tmp, swap in as the container user), make polling a sha256sum exec, store realpath-resolved targets, and pin the exact viewer capability set. Add the task-by-task plan with its parallel groups and the verification commands that work here. Co-Authored-By: Claude Fable 5.1 --- .../plans/2026-09-22-terminal-file-viewer.md | 3238 +++++++++++++++++ .../2026-09-22-terminal-file-viewer-design.md | 347 ++ 2 files changed, 3585 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-22-terminal-file-viewer.md create mode 100644 docs/superpowers/specs/2026-09-22-terminal-file-viewer-design.md diff --git a/docs/superpowers/plans/2026-09-22-terminal-file-viewer.md b/docs/superpowers/plans/2026-09-22-terminal-file-viewer.md new file mode 100644 index 0000000..1479b6a --- /dev/null +++ b/docs/superpowers/plans/2026-09-22-terminal-file-viewer.md @@ -0,0 +1,3238 @@ +# Terminal File Viewer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** File paths in terminal output become clickable; a click opens the file from the project's container in its own OS window with a CodeMirror 6 editor that highlights the target line, live-reloads on a 2 s poll, and saves with hash-based conflict detection. + +**Architecture:** A pure matcher plus an xterm `ILinkProvider` find paths in the terminal buffer and call one Rust command, `open_file_viewer`, which probes the candidate container paths in a single exec and opens a `file-viewer-` window on a second Vite entry (`viewer.html`). A managed `ViewerRegistry` maps window labels to targets; every viewer command takes `window: tauri::Window` and operates only on its caller's entry, so no viewer command ever accepts a path. Reads use the existing archive-API fetch; polls are one `sha256sum` exec; saves stage bytes in `/tmp` via the existing tar upload and then let a `sh` script running as the container user do the hash check, `chmod --reference` and `mv -f`. + +**Tech Stack:** Rust (tauri 2.11, bollard 0.18, sha2, base64, uuid), React 19 + TypeScript, @xterm/xterm 5.5, CodeMirror 6 (`@codemirror/*` 6.x, `@lezer/highlight`), Vite 6 multi-page, Vitest + jsdom. + +**Spec:** `docs/superpowers/specs/2026-09-22-terminal-file-viewer-design.md` — read it first, including "Decisions made during review", which records why each mechanism below is the one chosen. + +## Global Constraints + +- **User-approved decisions are fixed:** one window per click, editable, CodeMirror 6, probe-roots resolution, 2 s polling, no autosave, "Changed on disk" conflict banner, 20-window cap, AppManifest lockdown deferred. +- **Design tokens only.** All colour from CSS custom properties in `app/src/index.css` (`--bg-primary`, `--bg-secondary`, `--border-color`, `--text-primary`, `--text-secondary`, `--text-disabled`, `--accent`, `--accent-emphasis`, `--warning`, `--warning-muted`, `--success`, `--radius-control`, `--radius-panel`). Filled buttons use `--accent-emphasis`. Never `focus:outline-none`, never `disabled:opacity-50`. +- **Use `components/ui` primitives** (`Button`, `StatusIndicator`) — do not hand-roll replacements. Status is never colour alone. +- **Frontend types in `lib/types.ts` must match Rust structs field-for-field** (snake_case both sides). +- **Every `#[tauri::command]` goes into `generate_handler![]` in `lib.rs`, one fully-qualified path per line.** The test `every_command_is_registered_exactly_once` (`lib.rs:759`) fails otherwise. +- **No viewer command accepts a path or a label argument.** Commands take `window: tauri::Window` and gate on `window.label()`. +- **`viewer.html` must not contain an inline `