diff --git a/.github/workflows/rust-lite-ci.yml b/.github/workflows/rust-lite-ci.yml index b008bd6..965e750 100644 --- a/.github/workflows/rust-lite-ci.yml +++ b/.github/workflows/rust-lite-ci.yml @@ -4,14 +4,15 @@ on: pull_request: paths: - "rust/**" - - "lite-rs/**" - - "src/**" - "lite/**" - "herdr-plugin.toml" - - "docs/rust-lite-parity-proof.md" - - "scripts/parity-lite.py" - - "scripts/parity-lite.sh" - - "scripts/smoke-rust-lite.sh" + - "herdr-annotate.version" + - "scripts/fetch-herdr-annotate.*" + - "scripts/lite-goldens/**" + - "scripts/lite-regression.py" + - "scripts/lite-regression.sh" + - "scripts/smoke.sh" + - "scripts/stage-local.sh" - ".github/workflows/rust-lite-*.yml" merge_group: @@ -51,7 +52,7 @@ jobs: workspaces: rust - run: cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings - parity: + regression: strategy: fail-fast: false matrix: @@ -64,10 +65,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: workspaces: rust - - uses: oven-sh/setup-bun@v2 - with: - bun-version: 1.3.14 - uses: actions/setup-python@v5 with: python-version: "3.12" - - run: bash scripts/parity-lite.sh + - run: bash scripts/lite-regression.sh diff --git a/.github/workflows/rust-lite-release.yml b/.github/workflows/rust-lite-release.yml index e16bc4e..82392e2 100644 --- a/.github/workflows/rust-lite-release.yml +++ b/.github/workflows/rust-lite-release.yml @@ -16,7 +16,7 @@ jobs: run: | version=$(sed -n 's/^version = "\(.*\)"/\1/p' rust/Cargo.toml | head -1) test "rust-lite-v$version" = "${GITHUB_REF_NAME}" - test "$version" = "$(tr -d '[:space:]' < lite-rs/herdr-annotate.version)" + test "$version" = "$(tr -d '[:space:]' < herdr-annotate.version)" build: needs: verify diff --git a/.github/workflows/windows-full-ci.yml b/.github/workflows/windows-full-ci.yml index 8743e51..cdebe58 100644 --- a/.github/workflows/windows-full-ci.yml +++ b/.github/workflows/windows-full-ci.yml @@ -4,10 +4,13 @@ on: pull_request: paths: - "herdr-plugin.toml" + - "herdr-annotate.version" - "plannotator-tui.version" + - "scripts/fetch-herdr-annotate.*" - "scripts/fetch-plannotator-tui.*" - "scripts/plannotator-tui.sh" - "scripts/smoke.sh" + - "scripts/test-fetch-herdr-annotate.*" - "scripts/test-fetch-plannotator-tui.*" - "scripts/test-herdr-windows-plugin.ps1" - "scripts/test-http-server.py" @@ -33,6 +36,8 @@ jobs: python-version: "3.11" - name: local override and idempotence run: ./scripts/test-fetch-plannotator-tui.ps1 -Case LocalOverride + - name: native lite local override and idempotence + run: ./scripts/test-fetch-herdr-annotate.ps1 -Case LocalOverride - name: loopback download and checksum preservation run: ./scripts/test-fetch-plannotator-tui.ps1 -Case Download - uses: actions/checkout@v4 @@ -59,3 +64,4 @@ jobs: steps: - uses: actions/checkout@v4 - run: bash scripts/test-fetch-plannotator-tui.sh + - run: bash scripts/test-fetch-herdr-annotate.sh diff --git a/.gitignore b/.gitignore index 9096fb9..c074431 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,4 @@ -node_modules/ .DS_Store bin/* !bin/.gitkeep /rust/target/ -/lite-rs/bin/* -!/lite-rs/bin/.gitkeep diff --git a/README.md b/README.md index ef10a3b..910dd18 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,17 @@ Annotate inside [Herdr](https://github.com/herdrdev/herdr): comment on any termi ## Requirements - Herdr 0.8.0 or later -- [Bun](https://bun.sh/) - macOS, Linux, or Windows +There is no runtime to install. Both installs download a small prebuilt `herdr-annotate` binary and verify its SHA-256 checksum. + On Linux, install `wl-clipboard`, `xclip`, or `xsel` for clipboard access. -On Windows, native Herdr plugin support is preview/best-effort. Bun must be on `PATH`. Clipboard access uses PowerShell; no extra clipboard package is required. The install, keybinding, configuration check, reload, and use instructions below also apply on Windows. +On Windows, native Herdr plugin support is preview/best-effort. Clipboard access uses PowerShell; no extra clipboard package is required. The install, keybinding, configuration check, reload, and use instructions below also apply on Windows. ## Install -Pick one. Installing the other later just swaps it (same plugin id). +Pick one. Installing the other later just swaps it (same plugin id). An install stays on the commit it came from; run the same command again to move to the current release. Full @@ -211,20 +212,28 @@ Herdr Annotate reads text that Herdr copies to the system clipboard. The plugin ## Development +The plugin is a Rust binary in `rust/`. + ```sh -bun install -bun test -bun run typecheck -herdr plugin link "$PWD" +cargo test --manifest-path rust/Cargo.toml +cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings +bash scripts/stage-local.sh # build and stage bin/herdr-annotate.exe +herdr plugin link "$PWD" # or "$PWD/lite" for the Lite variant ``` +`herdr plugin link` does not run manifest build hooks, so stage the binary first. It also replaces +any existing `annotate` link; link the other directory to switch back. + +`bash scripts/lite-regression.sh` checks the runtime against goldens recorded from the retired Bun +runtime. [docs/lite-testing.md](docs/lite-testing.md) covers what it compares and everything else +that guards Lite. + To test a local plannotator-tui build instead of the pinned release, put it in `bin/` before linking: `PLANNOTATOR_TUI_BIN=/path/to/plannotator-tui bash scripts/fetch-plannotator-tui.sh`. -`herdr plugin link` replaces any existing `annotate` link; link the other directory to switch back. Before a release, `HERDR_SESSION= bash scripts/smoke.sh` installs fresh, upgrades -from the first shipped commit, installs lite, swaps to full, and opens the review pane, then restores -whatever was installed. +from the first shipped commit, installs lite, swaps to full, and opens the manager and review panes, +then restores whatever was installed. ## Neovim integration diff --git a/bun.lock b/bun.lock deleted file mode 100644 index f48dabc..0000000 --- a/bun.lock +++ /dev/null @@ -1,24 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "herdr-annotate", - "devDependencies": { - "@types/bun": "latest", - "typescript": "^5.9.2", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - - "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], - - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], - } -} diff --git a/docs/lite-testing.md b/docs/lite-testing.md new file mode 100644 index 0000000..921dc1a --- /dev/null +++ b/docs/lite-testing.md @@ -0,0 +1,83 @@ +# How Lite is tested + +Herdr Annotate Lite is one native Rust binary. Its behavior was defined by the Bun runtime this +repository shipped through 0.3.0, and that definition is still enforced, by recording rather than +by running both runtimes side by side. + +## Recorded goldens + +`scripts/lite-regression.py` drives all six entrypoints the Lite manifest declares and compares +every observable with a recording checked in under `scripts/lite-goldens/`. The recording was taken +from the Bun runtime at commit `2add0da`, the last commit that still contained `src/`, so a green +run means the native runtime still does what Bun did. + +```sh +bash scripts/lite-regression.sh +``` + +The wrapper builds and stages a release binary, then checks it. `rust-lite-ci` runs the same +command on Ubuntu and macOS. + +### What one run compares + +- Process cases: exit code, stdout, stderr, every fake `herdr` and clipboard call with its + arguments, notification arguments, clipboard bytes, pending bytes and mode, and the resulting + filesystem tree. +- Screen cases: real PTYs at 86x22 (editor) and 98x28 (manager). The ANSI model in the harness + ignores style escapes and retains the terminal cell grid, including wide-character continuation + cells. It snapshots the first frame and the frame after every input, and compares every OSC 52 + clipboard sequence the pane emitted, in order. Where a case compares clipboard bytes, the last + sequence's payload must equal the bytes the native writer was handed. +- Store cases: JSONL bytes, file modes, and leftover lock and temporary files after scripted editor + and manager mutations. +- `store.cross-read`: exports `scripts/lite-goldens/fixtures/bun-editor-annotations.jsonl`, the + exact record the Bun editor wrote in the recorded run, so the on-disk format stays readable. +- Manifests: `lite/herdr-plugin.toml` and the root Full manifest must agree on the header and on + every action and pane declaration, and neither may gate the annotation tools to a platform. +- Error catalog: every user-visible message the Bun runtime produced still exists in the native + source file that owns it. +- Key coverage: the run fails unless it sent every editor and manager key in the required set. + +The only normalized values are each run's temporary roots, the repository root, the per-user +handoff directory, generated UUIDs, generated ISO timestamps, and the pid and time components of +pending and temporary filenames. Seeded timestamps stay literal. Screen cells and clipboard bytes +are never normalized. + +### Clipboard adapters + +One clipboard operation walks the platform's candidate list until an adapter works, so the goldens +record the role (`clipboard-read`, `clipboard-write`) instead of the program and hold on every +platform. The adapters themselves, their arguments, and the fallback order are checked against +`CLIPBOARD_CANDIDATES` in the harness, which also requires a run to exercise both a first-adapter +success and the full chain. macOS therefore covers `pbpaste` and `pbcopy`, and Ubuntu covers +Wayland, then xclip, then xsel. + +### Re-recording + +`bash scripts/lite-regression.sh --record` rewrites the goldens from the current binary. Do that +only for a deliberate behavior change, and read the diff: every rewritten line is a behavior the +retired runtime no longer defines. + +## Everything else + +| Check | Command | +|---|---| +| Unit and subprocess tests | `cargo test --manifest-path rust/Cargo.toml` | +| Lints and formatting | `cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings`, `cargo fmt --manifest-path rust/Cargo.toml --check` | +| Manifest structure, including the Windows gates | `python3 scripts/test-windows-full-manifest.py` | +| Binary fetchers | `bash scripts/test-fetch-herdr-annotate.sh`, `bash scripts/test-fetch-plannotator-tui.sh`, and the `.ps1` cases in `windows-full-ci` | +| Install, upgrade, swap, and both panes in a live session | `HERDR_SESSION= bash scripts/smoke.sh` | + +## What retiring Bun cost + +The old harness ran Bun and Rust on every case in the same run, so a divergence surfaced the moment +either side moved. The comparison is now against a fixed recording: it still catches a native +regression, but it can no longer settle a *new* behavior against a live Bun reference, and a +deliberate re-record is a human decision instead of a diff against a second implementation. + +`docs/rust-lite-parity.md` and `docs/rust-lite-parity-proof.md` mapped the TypeScript call paths to +the Rust ones function by function. They are retired with the runtime they describe; read them at +commit `2add0da`. + +Windows stays outside the PTY harness. It is covered by the Rust tests, the manifest and fetcher +checks in `windows-full-ci`, and the manual checklist in `windows-full-acceptance.md`. diff --git a/docs/rust-lite-parity-proof.md b/docs/rust-lite-parity-proof.md deleted file mode 100644 index 08f523b..0000000 --- a/docs/rust-lite-parity-proof.md +++ /dev/null @@ -1,377 +0,0 @@ -# Rust Lite parity proof - -This is the re-runnable proof behind the Rust Lite evaluation. It maps the externally observable -TypeScript Lite surface to the Rust call path at function granularity and names the differential -case that compares the result. The current local result is: - -```text -Parity Lite: 622 observables compared, 145 screens diffed, zero divergences / 1 deliberate -``` - -Run it from the repository root with `bash scripts/parity-lite.sh`. The shell wrapper stages a fresh -release binary; `scripts/parity-lite.py` then runs the Bun entrypoints and the staged native binary -against separate copies of the same fixtures. A failure retains its temporary inputs and unified -diffs and prints their path. A green run removes its temporary workspace. - -## What the harness compares - -- Process cases compare exit code, stdout, stderr, every fake `herdr`/clipboard command and argument, - notification arguments, clipboard bytes, pending bytes and mode, and resulting filesystem trees. -- Manifest cases parse `lite/herdr-plugin.toml`, `lite-rs/herdr-plugin.toml`, and the root Full - manifest and compare the declared entrypoints field by field, so a new action cannot reach one - runtime only. -- Screen cases use real PTYs at 86×22 (editor) and 98×28 (manager). The ANSI parser at - `scripts/parity-lite.py:158` ignores style escapes but retains the terminal cell grid, including - wide-character continuation cells. It snapshots the initial frame and the frame after every input. -- Screen cases also retain the raw PTY byte stream and compare every OSC 52 clipboard sequence a - pane emitted, in order (`scripts/parity-lite.py:388`). Where a case compares clipboard bytes, the - payload of the last sequence is additionally required to equal the bytes the native writer received, - so a runtime cannot emit a different copy to the terminal than it wrote to the clipboard. -- Store cases byte-compare JSONL, modes, and leftover lock/temp files after scripted editor and - manager mutations. `store.cross-read` makes Bun export the Rust editor's store and Rust export the - Bun editor's store, then compares the Markdown and subprocess traces. -- The only normalized values are each case's deliberately different temporary root, generated UUIDs, - generated ISO timestamps, and pid/time components in pending and temporary filenames - (`scripts/parity-lite.py:481`). Seed timestamps are allow-listed and remain literal. Product files - are never rewritten or filtered. Screen cells and clipboard bytes are never normalized. -- The harness runs on the host's real adapter branch. macOS therefore proves `pbpaste`/`pbcopy` and - Ubuntu proves the Wayland → xclip → xsel chain. Windows is intentionally outside this PTY harness - and remains in the separate build/promotion track. - -The parity surface is the six commands wired by the Lite manifest. Rust's single-binary dispatch -usage error and `--version` output are packaging controls outside that surface; no valid manifest -invocation reaches them. Conversely, TypeScript dynamic-import loader failures have no native -counterpart because those modules are linked into the binary. Their underlying clipboard/store -failures do have mapped counterparts below. - -## Entrypoints - -### `capture` - -| Observable decision or effect | TypeScript call path | Rust call path | Mechanical evidence | -|---|---|---|---| -| Manifest invocation | `lite/herdr-plugin.toml:17` starts `bun ../src/capture.ts`. | `lite-rs/herdr-plugin.toml:27` starts `herdr-annotate capture`; `rust/src/main.rs:3` passes argv to `cli::run` at `rust/src/cli.rs:33`, then `capture` at `rust/src/cli.rs:65`. | Every `process.capture.*` case. | -| Context decode | Top-level `src/capture.ts:12` parses `HERDR_PLUGIN_CONTEXT_JSON`; invalid JSON falls back to an empty object. `parseInvocationContext` and `selectedTextFromInvocation` are `src/types.ts:44` and `src/types.ts:58`. | `invocation_context` at `rust/src/cli.rs:58`, then `parse_invocation_context` and `selected_text_from_invocation` at `rust/src/types.rs:85` and `rust/src/types.rs:101`. | `process.capture.context`, `process.capture.invalid-context`. | -| Required paths | `stateDir`/`pluginRoot` at `src/paths.ts:27` and `src/paths.ts:32`, checked at `src/capture.ts:21`. | `state_dir`/`plugin_root` at `rust/src/paths.rs:29` and `rust/src/paths.rs:36`, checked at `rust/src/cli.rs:69`. | `process.capture.missing-state`, `process.capture.missing-root`. | -| Selection precedence | Invocation selection at `src/capture.ts:15`; if absent, `takeHandoff` at `src/handoff.ts:24`; if absent/blank/stale, `readClipboard` at `src/clipboard.ts:48`. | The same branches at `rust/src/cli.rs:71`, through `take_default_handoff`/`take_handoff` at `rust/src/handoff.rs:60`/`:32`, then `read_clipboard` at `rust/src/clipboard.rs:93`. | `process.capture.context` seeds all three sources and leaves the skipped handoff untouched; `process.capture.handoff` seeds a competing clipboard; stale, blank, invalid-UTF-8, and clipboard cases exercise the remaining decisions. | -| Empty selection | `src/capture.ts:38` sends `Nothing to annotate`, creates no pending file, exits 0. | `rust/src/cli.rs:78` sends the same notification and returns success. | `process.capture.empty`. | -| Pending record | `src/capture.ts:42` creates the state directory; `:44` constructs the record; `:49` names it; `:52` writes it. | `rust/src/cli.rs:85` constructs it; `:92` names it; `write_pending` at `:247` writes it. | All successful capture cases compare literal JSON after generated time/name normalization and assert mode 0600. | -| Editor pane | `runHerdr` at `src/herdr.ts:11` receives the argv built at `src/capture.ts:54`. Failure removes pending at `:74`, then the catch at `:78` notifies, prints, and exits 1. | `run_herdr` at `rust/src/herdr.rs:22` receives the argv at `rust/src/cli.rs:97`. Failure removes pending at `:117`; `cli::run` notifies at `:35`; `main` prints/exits 1 at `rust/src/main.rs:3`. | `process.capture.context` compares success argv; `process.capture.open-failure` compares cleanup, notification, stderr, and exit. | - -The structural difference is exception flow versus `Result`. Both converge on the same process -contract: success and blank input exit 0; a defined failure produces one stderr line, one best-effort -`Annotate failed` notification, and exit 1. - -### `copy-context` - -| Observable decision or effect | TypeScript call path | Rust call path | Mechanical evidence | -|---|---|---|---| -| Manifest and state | `lite/herdr-plugin.toml:24` → top-level `src/export.ts:7`; state is required at `:8`. | `lite-rs/herdr-plugin.toml:34` → `rust/src/main.rs:3` → `cli::run` at `rust/src/cli.rs:33` → `copy_context` at `:124`. | Every `process.copy.*` case. | -| Load and ordering | `loadAnnotations` at `src/store.ts:37` locks/parses; `newestFirstAnnotations` at `:32` reverses a copy; `src/export.ts:10` propagates load failure. | `load_annotations` at `rust/src/store.rs:70` locks/parses; `newest_first_annotations` at `:65` reverses clones; `rust/src/cli.rs:126` propagates failure. | Empty, populated, invalid-store, busy-lock, and stale-lock cases. | -| Empty store | `src/export.ts:14` notifies `No annotations` / `There is nothing to copy yet.` and exits 0. | `rust/src/cli.rs:127` sends the same notification and returns success. | `process.copy.empty`, including the newly created state-directory mode. | -| Markdown and clipboard | `formatAnnotations` at `src/format.ts:44`, then `writeClipboard` at `src/clipboard.ts:63`. | `format_annotations` at `rust/src/format.rs:67`, then `write_clipboard` at `rust/src/clipboard.rs:110`. | `process.copy.single`, `process.copy.populated`, `process.copy.no-clipboard`, and `store.cross-read`; clipboard bytes are unnormalized. | -| Success/failure reporting | `src/export.ts:21` sends singular/plural `Annotations copied`; catch at `:25` sends `Copy failed`, prints, exits 1. | `rust/src/cli.rs:132` sends the same success notification; `cli::run` at `:38` sends `Copy failed`; `main` prints/exits 1. | Single and populated cases prove grammar; no-clipboard, invalid-store, busy-lock, and missing-state prove failure outputs and exits. | - -### `copy-archive` - -| Observable decision or effect | TypeScript call path | Rust call path | Mechanical evidence | -|---|---|---|---| -| Manifest and state | `lite/herdr-plugin.toml:31` → top-level `src/export-archive.ts:76` → `main` at `:45`; state is required at `:47`. | `lite-rs/herdr-plugin.toml:41` → `rust/src/main.rs:3` → `cli::run` at `rust/src/cli.rs:33` → `copy_archive` at `:183`; state is required at `:184`. | Every `process.copy-archive.*` case. | -| Shared workflow | `copyAndArchiveAnnotations` at `src/archive-workflow.ts:29` receives the same dependencies `src/manager.ts:228` injects, wired at `src/export-archive.ts:55`. | `copy_and_archive_annotations` at `rust/src/archive_workflow.rs:27` receives the dependencies `rust/src/manager.rs:556` injects, wired at `rust/src/cli.rs:191`. | `test/archive-workflow.test.ts` and `archive_workflow::tests` cover the workflow; `store.manager.copy-archive` covers the manager key; the process cases cover the action. | -| Operation order | Load, format newest first, clipboard write, `appendArchivedSet`, then `removeAnnotationsById` — `src/archive-workflow.ts:32`-`:52`. | The same order at `rust/src/archive_workflow.rs:38`-`:65`. | `process.copy-archive.populated` byte-compares both JSONL stores, modes, and leftover lock/temp files afterwards, plus the unnormalized clipboard bytes. | -| Empty store | The injected loader records the empty read at `src/export-archive.ts:58`; `copyArchiveReport` at `:39` notifies `No annotations` / `There is nothing to copy yet.` and exits 0, matching `export.ts`. | `Cell` flag at `rust/src/cli.rs:194`; `copy_archive_report` at `:170` sends the same notification and returns success. | `process.copy-archive.empty` compares the notification, exit 0, and the untouched state tree. | -| Success reporting | `copyArchiveReport` at `src/export-archive.ts:25` sends singular/plural `Annotations copied and archived`. | `copy_archive_report` at `rust/src/cli.rs:157` builds the same title and body; `copy_archive` notifies at `:207`. | `process.copy-archive.populated`; `test/export-archive.test.ts` and `cli::tests::copy_archive_maps_every_outcome_to_its_notification_and_exit_status` pin both grammars. | -| Failure reporting | A `stay_open` failure maps to `Copy and archive failed` at `src/export-archive.ts:42`; the retained-active partial maps to `Copy and archive incomplete` at `:32`. Both print the body and exit 1 at `:70`. | The same two arms at `rust/src/cli.rs:175` and `:165`; `copy_archive` returns the body at `:209` and `main` prints/exits 1. | `process.copy-archive.no-clipboard` and `process.copy-archive.missing-state` compare notification argv, stderr, exit code, and the unchanged stores; the retained-active arm is pinned by both unit tests. | - -### `manage` - -| Observable decision or effect | TypeScript call path | Rust call path | Mechanical evidence | -|---|---|---|---| -| Manifest and root | `lite/herdr-plugin.toml:38` → `src/open-manager.ts:5` → `pluginRoot` at `src/paths.ts:32`. | `lite-rs/herdr-plugin.toml:48` → `rust/src/main.rs:3` → `cli::run` at `rust/src/cli.rs:33` → `manage` at `:214` → `plugin_root` at `rust/src/paths.rs:36`. | `process.manage.success`, `process.manage.missing-root`. | -| Manager pane | The argv literal is `src/open-manager.ts:13`; `runHerdr` is `src/herdr.ts:11`. Errors notify/print/exit at `src/open-manager.ts:32`. | The argv literal is `rust/src/cli.rs:216`; `run_herdr` is `rust/src/herdr.rs:22`; `cli::run`/`main` handle notify, stderr, and exit. | Success, child-stderr failure, empty-child-stderr fallback, and missing-root cases. | - -### `editor` - -| Observable decision or effect | TypeScript call path | Rust call path | Mechanical evidence | -|---|---|---|---| -| Manifest and pending selection | `lite/herdr-plugin.toml:46` → top-level `src/editor.ts:17`. `invocationContext` at `:20`; pending-file parse at `:34`; fallback `pendingAnnotationFromInvocation` at `src/types.ts:65`; parsed-file canonicalization at `src/types.ts:79`. | `lite-rs/herdr-plugin.toml:56` → dispatcher → `editor::run` at `rust/src/editor.rs:371`; `pending_from_env` at `:345`; invocation fallback and pending parsing at `rust/src/types.rs:107` and `:119`. | Missing/invalid process cases; `screen.editor.pending-file-save`; the other editor PTY cases use invocation fallback. | -| Terminal and screen | `render` at `src/editor.ts:77` uses `sanitizeTerminalText`, `wrapText`, `layoutComment`, and width helpers; alternate-screen setup is `:207`. | `EditorApp::draw` at `rust/src/editor.rs:68` calls the corresponding helpers at `rust/src/format.rs:7`/`:26`, `rust/src/layout.rs:14`, and `rust/src/width.rs:35`; `editor::run` owns the Ratatui terminal. | Initial frame and every frame in `screen.editor.*`; fixed 86×22 cells include wide Hangul. | -| Input and save | Key dispatch is `src/editor.ts:172`, with `resolveEditKey` consulted at `:181` before the single-character arm; vertical movement is `:52`; `save` is `:122`; successful save renders, waits 250 ms, and exits. | `EditorApp::handle_key` at `rust/src/editor.rs:169`, with `resolve_edit_key` consulted at `:181` and applied by `apply_edit_action` at `:228`; vertical movement at `:252`; `save` at `:278`; `run` at `:371` renders the saved state, waits 250 ms, and exits. | `screen.editor.edit-save`, empty-save, missing-state, Esc, and Ctrl+C cases. Store bytes are compared after save. | -| Cleanup and signals | `cleanup`/`exit` at `src/editor.ts:110`/`:117`; SIGTERM/SIGHUP handlers at `:160`. | `Termination::install`/`requested` at `rust/src/termination.rs:17`/`:33`; the polling loop at `rust/src/editor.rs:371` reaches `ratatui::restore`. | `screen.editor.sigterm` compares exit 0 and the restored terminal grid. | - -### `manager` - -| Observable decision or effect | TypeScript call path | Rust call path | Mechanical evidence | -|---|---|---|---| -| Manifest, state, initial load | `lite/herdr-plugin.toml:54` → `requireStateDir` at `src/manager.ts:30`; `reloadActive`/`reloadArchives` at `:55`/`:66`. | `lite-rs/herdr-plugin.toml:64` → dispatcher → `manager::run` at `rust/src/manager.rs:724`; `ManagerApp::load` at `:63`; reload methods at `:80`/`:95`. | `process.manager.missing-state`; initial screens in all manager PTY cases. | -| Active screen | `render` at `src/manager.ts:198` → `renderActive` at `:95`, with `clipped` at `:77`, formatting helpers, newest-first state, source and timestamp metadata. | `ManagerApp::draw` at `rust/src/manager.rs:110` → `draw_active` at `:162`, with `clipped` at `:681` and `format_timestamp` at `:691`. | Every active-view snapshot, including the detail-width regression fixture in `screen.manager.all-views`. | -| Archive screen | `render` → `renderArchives` at `src/manager.ts:138`; archive annotations are previewed newest first. | `ManagerApp::draw` → `draw_archives` at `rust/src/manager.rs:300`; same preview ordering and clipping. | Every archive-view snapshot and scripted archive mutation. | -| Input and mutations | Top-level key dispatch is `src/manager.ts:404`; active/archive handlers are `:320`/`:346`; action functions are `:218`–`:318`. | `ManagerApp::handle_key` is `rust/src/manager.rs:434`; view handlers are `:466`/`:503`; action methods are `:547`–`:651`. | `screen.manager.all-views`, empty-actions, success-copy sessions, and the exit/signal sessions. Resulting JSONL and clipboard bytes are compared. | -| Cleanup and signals | `cleanup`/`exit` at `src/manager.ts:378`/`:385`; signal handlers at `:392`. | `Termination` at `rust/src/termination.rs:17`; polling/restore at `rust/src/manager.rs:724`. | `screen.manager.sighup` compares exit 0 and restored cells. | - -## Every editor key - -All key paths clear the prior status before acting. TypeScript dispatch is `src/editor.ts:172`; -Rust dispatch is `EditorApp::handle_key` at `rust/src/editor.rs:169`; both render again after the -transition. - -| Key | TypeScript → Rust call path | Required state/store/screen effect | Harness step | -|---|---|---|---| -| Character input, including wide text | `src/editor.ts:199` uses `Array.from` and `splice` → `rust/src/editor.rs:215` calls `insert` at `:247`. | Insert Unicode scalar(s) at cursor, advance by character count, render using cell width. | `screen.editor.edit-save`: `chars`, `chars-second-line`. | -| Enter | `src/editor.ts:196` → Rust `KeyCode::Enter` at `rust/src/editor.rs:214` → `insert`. | Insert `\n`, move cursor, preserve explicit blank/line layout. | `enter`. | -| Backspace | `src/editor.ts:180` → `rust/src/editor.rs:187`. | If cursor > 0, remove the character before it and move left; otherwise no change. | `backspace`. | -| Delete | `src/editor.ts:182` → `rust/src/editor.rs:193`. | Remove the character at cursor if present; cursor stays. | `delete`. | -| Left / Right | `src/editor.ts:184`/`:186` → `rust/src/editor.rs:198`/`:199`. | Move one character, clamped to `[0, length]`. | `left`, `right`. | -| Up / Down | `moveCursorVertical` at `src/editor.ts:52` → `move_cursor_vertical` at `rust/src/editor.rs:252`. | Preserve terminal-cell column as closely as possible on the adjacent line; clamp first/last row and never split a wide glyph. | `up`, `down`. | -| Home / End | `src/editor.ts:192`/`:194` → `rust/src/editor.rs:202`/`:207`. | Move to start/end of the current logical line. | `home`, `end`. | -| Word left (Option/Alt+Left, Ctrl+Left, Alt+B) | `resolveEditKey` at `src/edit-keys.ts:53` → `wordStart` at `:16`, applied at `src/editor.ts:182`. | `resolve_edit_key` at `rust/src/edit_keys.rs:111` → `word_start` at `:56`, applied at `rust/src/editor.rs:230`. | Skip whitespace back to the previous word start; a newline is crossed by itself in one step; a CJK run is one word. | `screen.editor.word-line-moves`: `alt-left`, `alt-b`, `ctrl-left`, `alt-left-word`, `alt-left-previous-word`, `alt-left-line-first-word`, `alt-left-over-newline`. | -| Word right (Option/Alt+Right, Ctrl+Right, Alt+F) | `resolveEditKey` at `src/edit-keys.ts:53` → `wordEnd` at `:25`, applied at `src/editor.ts:184`. | `resolve_edit_key` at `rust/src/edit_keys.rs:111` → `word_end` at `:71`, applied at `rust/src/editor.rs:231`. | Skip whitespace forward to the next word end; a newline is crossed by itself in one step. | `screen.editor.word-line-moves`: `alt-f`, `alt-right`, `ctrl-right`, `alt-right-over-newline`, `alt-right-next-word`. | -| Line start (Command/Super+Left, Ctrl+A) | `resolveEditKey` at `src/edit-keys.ts:53` (xterm modifier 9 via the raw sequence) → `lineStart` at `:33`, applied at `src/editor.ts:186`. | `resolve_edit_key` at `rust/src/edit_keys.rs:111` (`KeyModifiers::SUPER`) → `line_start` at `:86`, applied at `rust/src/editor.rs:232`. | Move to the first character of the current logical line without crossing the newline. | `screen.editor.word-line-moves`: `super-left`, `ctrl-a`. | -| Line end (Command/Super+Right, Ctrl+E) | `resolveEditKey` at `src/edit-keys.ts:53` → `lineEnd` at `:39`, applied at `src/editor.ts:188`. | `resolve_edit_key` at `rust/src/edit_keys.rs:111` → `line_end` at `:95`, applied at `rust/src/editor.rs:233`. | Move to the end of the current logical line without crossing the newline. | `screen.editor.word-line-moves`: `super-right`, `ctrl-e`. | -| Delete word (Ctrl+W, Alt/Option+Backspace) | `src/editor.ts:190` splices `wordStart(comment, cursor)..cursor`. | `rust/src/editor.rs:234` drains `word_start(..)..cursor`. | Kill back to the word start and leave the cursor there; at a line start the kill takes only the newline. | `screen.editor.word-line-kills`: `ctrl-w`, `alt-backspace-marker`, `alt-backspace-word`, `alt-backspace-newline`. | -| Delete line (Ctrl+U) | `src/editor.ts:194` splices `lineStart(comment, cursor)..cursor`. | `rust/src/editor.rs:239` drains `line_start(..)..cursor`. | Kill back to the line start and leave the cursor there; at a line start it removes nothing. | `screen.editor.word-line-kills`: `ctrl-u`, `ctrl-u-at-line-start`. | -| Ctrl+S | `src/editor.ts:175` → `save` at `:122`; Rust control branch at `rust/src/editor.rs:178` → `save` at `:278`. | Blank comment: `Write a comment before saving.` and remain. Missing state: `Plugin state directory is unavailable.` and remain. Store error: display it and remain. Success: append exact JSONL, display `Saved.`, wait 250 ms, cleanly exit 0. | `screen.editor.edit-save`, `empty-save-escape`, `missing-state`. | -| Esc | `src/editor.ts:179` → `exit`/`cleanup`; Rust `rust/src/editor.rs:186` sets quit and `run` restores. | No store write; cursor shown, screen restored, exit 0. | `screen.editor.empty-save-escape`. | -| Ctrl+C | `src/editor.ts:174` → `exit`/`cleanup`; Rust `rust/src/editor.rs:174` sets quit. | Same cancellation effect as Esc. | `screen.editor.control-c`. | - -### How each runtime decodes the word and line modifiers - -The two runtimes reach the same action from different modifier models, so the mapping is stated here -rather than inferred. - -- Node's readline folds xterm modifiers 3 and 9 into a single `key.meta` flag (`modifier & 10`), so - `src/edit-keys.ts:54` inspects the raw sequence with `/;9[A-Z~]$/` to tell Command from Option. -- Crossterm's `parse_modifiers` subtracts one and reads the result as a bit set, so modifier 3 is - `KeyModifiers::ALT` and modifier 9 is `KeyModifiers::SUPER`. `KeyModifiers::META` is bit 32, which - is modifier 33, never Command, so `rust/src/edit_keys.rs:111` gates line moves on `SUPER` alone. -- Crossterm parses an `ESC`-prefixed byte as the following event with `ALT` folded in, so `\x1bb` is - `Char('b')` plus `ALT` and `\x1b\x7f` is `Backspace` plus `ALT`, matching Node's `meta` forms. -- Word boundaries use JavaScript's `\s` class, not `char::is_whitespace`. The two sets differ at - U+0085 and U+FEFF, so `is_javascript_whitespace` at `rust/src/edit_keys.rs:30` spells the - TypeScript set out and `edit_keys::tests::javascript_whitespace_matches_the_typescript_class` - pins both scalars. - -## Every manager key in both views - -The common TypeScript dispatcher is `src/manager.ts:404`; Rust's is `rust/src/manager.rs:434`. -Each non-exit transition re-renders the full grid. Arrow Up/Down are aliases of `k`/`j` and are -also fed by `screen.manager.all-views`. - -| Key | Active view: TypeScript → Rust and effect | Archives view: TypeScript → Rust and effect | Evidence | -|---|---|---|---| -| `j` / Down | `handleActiveKey` `src/manager.ts:330` → `handle_active_key` `rust/src/manager.rs:478`; increment/clamp active selection, changing list highlight and detail. | `handleArchiveKey` `src/manager.ts:366` → `handle_archive_key` `rust/src/manager.rs:527`; increment/clamp archive selection and detail. | `active-j`, `active-arrow-down`, `archives-j`, `archives-arrow-down`. | -| `k` / Up | `src/manager.ts:328` → `rust/src/manager.rs:475`; decrement/saturate. | `src/manager.ts:364` → `rust/src/manager.rs:524`; decrement/saturate. | Corresponding `k` and arrow-up steps. | -| `y` | `copy` at `src/manager.ts:218` receives the selected annotation → `ManagerApp::copy` at `rust/src/manager.rs:547`; exact one-item Markdown is copied; success exits 0, an empty selection displays status. | Same helpers receive the selected archive's annotations in newest-first order (`src/manager.ts:368`, `rust/src/manager.rs:529`). | Success sessions `store.manager.active-y-success` / `archives-y-success` / `osc52-remote-copy`, and empty-actions. | -| `c` | `src/manager.ts:337` copies the displayed newest-first active list → `rust/src/manager.rs:492`; success exits. | Not handled by `src/manager.ts:346` or `rust/src/manager.rs:503`; clears any prior confirmation/status through normal dispatch, otherwise store/view unchanged. | `store.manager.active-c-success`, `osc52-remote-copy-all`, and `archives-c-ignored`. | -| `C` | `copyAndArchive` at `src/manager.ts:227` → `copyAndArchiveAnnotations` at `src/archive-workflow.ts:29`; Rust `rust/src/manager.rs:554` → `copy_and_archive_annotations` at `rust/src/archive_workflow.rs:27`. Order is load → copy → append archive → remove captured active IDs. Success exits; partial failure is reported without data loss. | Not handled; same no-op/confirmation-clear behavior as archive `c`. | Success and byte-diff in `store.manager.copy-archive`; `osc52-remote-copy-archive` proves the archive still runs when only the terminal copy landed; `archives-C-ignored`; workflow failure ordering has paired TS/Rust unit specs. | -| `d` | `deleteSelectedAnnotation` at `src/manager.ts:245` → `rust/src/manager.rs:575`; remove selected ID through locked atomic rewrite, reload, status `Annotation deleted.` | First press records the selected archive id and renders `Press d again…`; second matching press calls `deleteSelectedArchive` (`src/manager.ts:297`, `rust/src/manager.rs:639`) and atomically removes it. No selection displays `No archive selected.` Esc or another ordinary key cancels confirmation. | Active delete, archive confirm/cancel/double-confirm in all-views; empty-actions. | -| `D` | `src/manager.ts:321` / `rust/src/manager.rs:467`: first press renders `Press Shift+D again…`; second calls `clearActive` (`src/manager.ts:257`, `rust/src/manager.rs:589`), rewrites active JSONL empty, reloads, and displays `All active annotations cleared.` | Uppercase `D` is not an archive action; it cancels a pending archive confirmation like any non-`d` archive key, otherwise no store effect. | Active confirm/cancel/double-confirm and `archives-D-ignored` in all-views. | -| `r` | `reloadActive` (`src/manager.ts:55`, `rust/src/manager.rs:80`); success status `Reloaded.`, failure status is the store error. | `reloadArchives` (`src/manager.ts:66`, `rust/src/manager.rs:95`) with the same status rule. | Both reload steps in all-views; invalid/busy store process cases cover propagated store errors. | -| `u` | Not handled in active view; clears transient status/confirmation, leaves selection and stores unchanged. | `restoreSelectedArchive` at `src/manager.ts:271` → `restoreArchivedSet` at `src/archive-workflow.ts:71`; Rust `rust/src/manager.rs:606` → `restore_archived_set` at `rust/src/archive_workflow.rs:96`. Order is merge missing annotation IDs, then remove archive; partial removal failure keeps the archive and reports it. | `active-u-ignored`; archive restore in all-views; no-selection in empty-actions; paired workflow unit specs cover partial failures and concurrent active records. | -| Tab | `switchView` at `src/manager.ts:308` → `rust/src/manager.rs:651`; clear confirmation/status, switch view, reload destination store. | Same in reverse. | Both Tab directions in all-views and all archive exit sessions. | -| Esc | `src/manager.ts:406`: if confirming, clear confirmation/status and stay; otherwise cleanly exit. Rust `rust/src/manager.rs:440` is identical. | Same. | Active confirmation cancel and active exit; archive confirmation cancel and archive exit. | -| `q` | Common dispatcher exits 0 through cleanup. | Same. | Active `q` in all-views; `screen.manager.q-archives`. | -| Ctrl+C | Common dispatcher exits 0 through cleanup. | Same. | `screen.manager.control-c-active` and `control-c-archives`. | - -## Rendered and exported products - -| Product | TypeScript call path | Rust call path | Observable proof | -|---|---|---|---| -| Markdown export | `formatAnnotations` at `src/format.ts:44`; `fenceFor` at `:38`. | `format_annotations` at `rust/src/format.rs:67`; `fence_for` at `:52`. | Both emit `# Annotated context`, then newest-first `## Annotation N` sections. Optional source is `workspace_label / tab_label`; selected text and comment retain line breaks; selected text uses a backtick fence one longer than its longest run (minimum three); duplicate blank lines are collapsed; the document ends in exactly one `\n`. Populated/single copy and cross-read compare raw clipboard bytes using backticks, multiline text, and wide characters. | -| Terminal-safe text | `sanitizeTerminalText`/`wrapText` at `src/format.ts:5`/`:12`. | `sanitize_terminal_text`/`wrap_text` at `rust/src/format.rs:7`/`:26`. | Control characters are removed except newline/tab, tabs become four spaces, CRLF becomes LF, explicit newlines are preserved, and wrapping uses terminal cells. Editor and both manager views compare resulting cells. | -| Width and clipping | `charWidth`, `stringWidth`, `truncateToWidth` at `src/width.ts:37`/`:48`/`:55`; manager `clipped` at `src/manager.ts:77`. | `char_width`, `string_width`, `truncate_to_width` at `rust/src/width.rs:35`/`:47`/`:52`; manager `clipped` at `rust/src/manager.rs:681`. | Same zero-width controls/combining ranges and same wide ranges; truncation never splits a wide glyph and adds exactly one ellipsis cell. Wide input occurs in editor, list, detail, metadata, and archive snapshots. | -| Editor geometry | `render` at `src/editor.ts:77` and `layoutComment` at `src/layout.ts:9`. | `EditorApp::draw` at `rust/src/editor.rs:67` and `layout_comment` at `rust/src/layout.rs:14`. | At 86×22, identical selected-text cap/overflow marker, comment viewport, cursor cell, footer/status placement, and full clears. Every editor input has a post-step grid diff. | -| Manager geometry and labels | `render`/active/archive/footer at `src/manager.ts:198`/`:95`/`:138`/`:184`. | `ManagerApp::draw`/active/archive/footer at `rust/src/manager.rs:110`/`:162`/`:300`/`:409`. | At 98×28, identical 36%-clamped list, divider, selected marker/reverse cell region, detail width, newest-first labels, counts, metadata line, preview overflow, empty-state text, confirmation footer, help footer, and transient status. The archive clipping boundary that exposed the earlier one-cell defect is in every seeded archive screen. | - -## Filesystem effects - -| Observable | TypeScript call path | Rust call path | Exact product and evidence | -|---|---|---|---| -| State directory creation | `fs.mkdirSync(..., {recursive:true})` at `src/capture.ts:42` and `withStoreLock` at `src/store.ts:196`. | `create_dir_all` at `rust/src/cli.rs:84` and `create_private_dir_all` at `rust/src/store.rs:392`. | Process-default directory mode (0755 under harness umask 022), not forced 0700. `process.copy.empty` compares the directory mode. | -| Pending file | `src/capture.ts:44`/`:49`/`:52`. | `rust/src/cli.rs:85`/`:92` and `write_pending` at `:247`. | Name `pending--.json`; mode 0600 on creation; bytes are one JSON object plus `\n`; property order `selectedText`, `context`, `capturedAt`. Capture cases byte/mode-diff it. | -| Pending consumption | `src/editor.ts:34` reads/parses, then `fs.rmSync(...,{force:true})` at `:39`. | `pending_from_env` at `rust/src/editor.rs:345`, then `remove_pending_file` at `:362`. | Delete only after successful read and semantic parse; missing-at-delete is ignored; other deletion errors fail startup. `screen.editor.pending-file-save` compares the consumed tree and saved JSONL; Rust regression `pending_removal_is_forceful_like_typescript` pins the delete race. | -| Handoff take | `handoffPath`/`takeHandoff` at `src/handoff.ts:17`/`:24`. | `handoff_path`/`take_handoff` at `rust/src/handoff.rs:12`/`:32`. | `$XDG_RUNTIME_DIR` else temp + `herdr-annotate-/selection`; missing/stat failure means absent; regular files ≤15 s old are decoded as UTF-8 with replacement; stale and blank values are rejected; every found node is removed; non-NotFound removal failure propagates. Context-skipped handoff remains. Capture handoff cases diff pending bytes and the runtime tree. | -| Active append | `appendAnnotation` at `src/store.ts:42`. | `append_annotation` / `append_annotation_context_first` at `rust/src/store.rs:77`/`:82`, sharing `append_annotation_record` at `:97`. | Append one compact JSON object plus `\n`; mode 0600 on creation. Capture-file editor order is `selectedText,capturedAt,context,id,comment,createdAt`; direct invocation fallback preserves TypeScript's distinct `selectedText,context,capturedAt,id,comment,createdAt`. `screen.editor.pending-file-save.state` and `screen.editor.edit-save.state` byte-diff both orders; two Rust regressions pin them. | -| JSONL read | `loadJsonLines` at `src/store.ts:150` and parsers at `src/types.ts:92`/`:103`. | `load_json_lines` at `rust/src/store.rs:227` and parsers at `rust/src/types.rs:133`/`:145`. | Missing file = empty; empty lines skipped; any malformed/nonconforming non-empty line rejects the whole store; unknown fields tolerated by parsers. Invalid-store and paired unit cases cover both stores. | -| Active rewrite | Remove/merge at `src/store.ts:54`/`:70` → `replaceJsonLines` at `:175`. | `remove_annotations_by_id`/`merge_annotations` at `rust/src/store.rs:120`/`:136` → `replace_json_lines` at `:253`. | Retained append order; merge by id without duplicates; canonical saved field order; temporary `.--.tmp`, mode 0600, one newline per record (zero bytes when empty), then rename; temp removed on failure. Manager state byte diffs prove final files and absence of leftovers. | -| Archive rewrite | Append/remove at `src/store.ts:99`/`:111` → `replaceJsonLines`. | `append_archived_set`/`remove_archived_set` at `rust/src/store.rs:173`/`:182` → `replace_json_lines`. | Entire archives store is atomically replaced. Outer order is `version,id,archivedAt,annotations`; inner annotations are canonical; trailing newline and 0600 mode match. Copy/archive, restore, and permanent-delete scripts byte-diff the products. | -| Lock acquire | `withStoreLock`/`acquireStoreLock`/`createStoreLock` at `src/store.ts:196`/`:217`/`:241`. | `with_store_lock`/`acquire_store_lock`/`create_store_lock` at `rust/src/store.rs:285`/`:296`/`:333`. | Per-store `.annotations.lock` or `.archives.lock`; directory mode 0700; `owner` mode 0600 with `:\n`; exclusive directory creation. A fresh existing lock returns the exact busy error without mutation. | -| Lock steal | Staleness check/removal/retry at `src/store.ts:223`–`:238`, `isStaleLock` at `:261`. | Corresponding branch `rust/src/store.rs:313`–`:330`, `is_stale_lock` at `:345`. | Age ≥30 s is removed then acquired once; a competing recreation reports busy. `process.copy.busy-lock` and `stale-lock` compare exits, errors, and final lock trees. | -| Lock release | `releaseStoreLock` at `src/store.ts:269`, called in `finally` at `:210`. | `StoreLockLease::drop` at `rust/src/store.rs:52`. | Read current owner; remove recursively only if token still matches; ignore cleanup errors. Every state-compared completed store case asserts no owned lock remains; paired contention tests cover recovery. | - -## Process effects - -The editor pane argv is identical, including order: - -```text -herdr plugin pane open --cwd --plugin annotate --entrypoint editor \ - --placement popup --width 88 --height 24 \ - --env HERDR_ANNOTATE_PENDING= --focus -``` - -It is constructed at `src/capture.ts:54` and `rust/src/cli.rs:97` and compared by every successful -capture case. The manager argv is constructed at `src/open-manager.ts:13` and `rust/src/cli.rs:216`: - -```text -herdr plugin pane open --cwd --plugin annotate --entrypoint manager \ - --placement popup --width 100 --height 30 --focus -``` - -`runHerdr` (`src/herdr.ts:11`) and `run_herdr` (`rust/src/herdr.rs:22`) ignore stdin/stdout, capture -stderr, and use `HERDR_BIN_PATH` or `herdr`. Nonzero child status returns trimmed child stderr; if it -is empty or spawning fails, the exact fallback is `herdr failed`. Manage success, -stderr failure, and empty-stderr failure are differential cases. - -Notifications are best effort and never alter the primary exit. Both sides call: - -```text -herdr notification show [--body <body>] -``` - -The compared title/body pairs are `Nothing to annotate`, `Annotate failed`, `No annotations`, -`Annotations copied`, `Copy failed`, `Annotations copied and archived`, `Copy and archive failed`, -`Copy and archive incomplete`, and `Unable to open annotations`; their bodies are listed in the -error/reporting table below or generated from the exact annotation count. - -Clipboard candidates and arguments are defined at `src/clipboard.ts:13`/`:30` and -`rust/src/clipboard.rs:12`/`:46`: - -| Platform | Read order | Write order | -|---|---|---| -| macOS | `pbpaste` | `pbcopy` | -| Windows | `powershell.exe -NoProfile -NonInteractive -Command "Get-Clipboard -Raw"` | `powershell.exe -NoProfile -NonInteractive -Command "$input | Set-Clipboard"` | -| Linux/other Unix | `wl-paste --no-newline`; `xclip -selection clipboard -out`; `xsel --clipboard --output` | `wl-copy`; `xclip -selection clipboard -in`; `xsel --clipboard --input` | - -### Pane copies and OSC 52 - -Herdr 0.9.0 forwards OSC 52 sequences emitted by pane output to the viewing client, so a copy made -inside a pane can reach the clipboard of the machine the person is sitting at even when the plugin -runs on a different server. Only the manager pane can use this path: the global `copy-context` and -`copy-archive` actions run with piped stdout and no terminal, so their behavior is unchanged. - -| Behavior | TypeScript | Rust | Proof | -|---|---|---|---| -| Sequence encoding | `osc52ClipboardSequence` at `src/pane-clipboard.ts:18` builds `ESC ] 52 ; c ; <base64 of the UTF-8 bytes> BEL`. | `osc52_clipboard_sequence` at `rust/src/pane_clipboard.rs:42` over the same bytes, with a hand-rolled base64 at `:17`. | `test/pane-clipboard.test.ts` and `pane_clipboard::tests` assert the exact bytes for `hi` (`\x1b]52;c;aGk=\x07`), an empty string, and multi-byte UTF-8; `store.manager.*` compares the sequences observed on real PTYs. | -| Emission point | `paneClipboardWriter` at `src/pane-clipboard.ts:49` wraps `writeClipboard`; `src/manager.ts:41` injects it into both manager copy call sites. | `write_pane_clipboard` at `rust/src/pane_clipboard.rs:68` wrapped by `pane_clipboard_write` at `rust/src/manager.rs:703` and injected at `:548`/`:558`. | Both runtimes emit exactly once per writer call, after the native attempt and before the next frame, so the emitted sequence lists match step for step. | -| Native failure | Either destination landing is a successful copy (`src/pane-clipboard.ts:56`). A remote server commonly has no clipboard tool at all, and the sequence is the copy that actually reached the person, so it is not reported as a failure and does not stop a copy-and-archive before its archive step. | Identical rule at `rust/src/pane_clipboard.rs:75`. | The three `store.manager.osc52-remote-*` cases run with the native writer failing: the copy exits 0, `C` still writes its archive and clears the active list, and both runtimes emit the same sequence. | -| Neither destination | Only a copy that reached neither the clipboard nor the terminal fails, carrying the unchanged native error. | Same. | Paired unit specs; unreachable from a pane whose stdout is a live terminal. | -| Oversized payloads | `exceedsCommonOsc52Limit` at `src/pane-clipboard.ts:26`. | `exceeds_common_osc52_limit` at `rust/src/pane_clipboard.rs:47`. | Terminals commonly refuse a base64 payload over 74994 bytes. Both runtimes emit the full sequence anyway and never truncate a copy; the predicate exists so the limit is stated rather than silently applied. | - -Replacing one runtime's emitter with a no-op and rerunning the harness fails every manager copy -case, so these comparisons are not vacuously green. - -Because a pane's stdout is a live terminal, a manager copy no longer fails there. `No supported -clipboard writer is available` therefore remains reachable from the global actions, which have no -terminal, but not from the manager; the manager's copy-failure frames were removed from -`screen.manager.all-views` for that reason. - -Readers return the first exit-0 stdout, decoded with UTF-8 replacement. Writers pipe the exact UTF-8 -Markdown to stdin and accept the first exit-0 adapter. Child stdout/stderr is suppressed. The fake -adapters log every attempt and capture writer stdin; macOS and Ubuntu CI together prove both Unix -branches. Windows arguments are source-mapped and build-checked, not run by this harness. - -### Exit codes - -| Path | Exit | -|---|---| -| Successful capture, empty capture after notification, successful/empty copy, successful/empty copy-archive, successful manage pane open | 0 | -| Missing required env, no clipboard adapter, store parse/lock/access failure, or pane-open failure | 1 after one stderr line; action commands also attempt their failure notification | -| Editor/manager initialization error | 1 with stderr, no action-level notification | -| Editor Esc/Ctrl+C, manager Esc/q/Ctrl+C, editor SIGTERM, manager SIGHUP | 0 after terminal restoration | -| Successful editor save | 0 after final `Saved.` frame and 250 ms delay | -| Manager copy/copy+archive success | 0 after clipboard/store completion; a pane copy succeeds whenever the clipboard write or the OSC 52 emission lands | - -## Error and failure strings - -These are all Lite-authored error strings and templates. A `<CODE>` is the operating-system code -projected by `safeFileError` (`src/store.ts:285`, `rust/src/store.rs:396`); omitting the code leaves -the prefix exactly as shown. - -| Exact string or template | TypeScript emitter | Rust emitter | Proof | -|---|---|---|---| -| `HERDR_PLUGIN_STATE_DIR is not set` | `src/capture.ts:22`, `src/export.ts:9`, `src/export-archive.ts:48`, `src/manager.ts:33` | `rust/src/cli.rs:69`/`:125`/`:185`, `rust/src/manager.rs:725` | Missing-state process cases. | -| `HERDR_PLUGIN_ROOT is not set` | `src/capture.ts:24`, `src/open-manager.ts:7` | `rust/src/cli.rs:70`/`:215` | Missing-root cases. | -| `No supported clipboard reader is available` | `src/clipboard.ts:59` | `rust/src/clipboard.rs:106` | `process.capture.no-clipboard`. | -| `Missing pending annotation` | `src/editor.ts:32` | `rust/src/editor.rs:351` | `process.editor.missing-pending`. | -| `Pending annotation is invalid` | `src/editor.ts:37` | `rust/src/editor.rs:357` | `process.editor.invalid-pending`. | -| `Write a comment before saving.` | `src/editor.ts:125` | `rust/src/editor.rs:281` | Editor empty-save frame. | -| `Plugin state directory is unavailable.` | `src/editor.ts:131` | `rust/src/editor.rs:285` | Editor missing-state frame. | -| `Unable to save annotation[ (<CODE>)]` | `src/store.ts:48` | `rust/src/store.rs:100` | Store error catalog and paired store/editor tests. | -| `Unable to read annotations (invalid data)` / `Unable to read archives (invalid data)` | `src/store.ts:163`/`:166` | `rust/src/store.rs:245`/`:247` | Invalid active process case and paired active/archive tests. | -| `Unable to read annotations (<CODE>)` / `Unable to read archives (<CODE>)` | `src/store.ts:171` | `rust/src/store.rs:235`/`:240` | Source/error catalog; OS-specific access cases remain unit-level. | -| `Unable to access annotations[ (<CODE>)]` / `Unable to access archives[ (<CODE>)]` | `src/store.ts:204` | `rust/src/store.rs:290` | Static catalog; normal missing-dir creation is differential. | -| `Unable to lock annotations[ (<CODE>)]` / `Unable to lock archives[ (<CODE>)]` | `src/store.ts:230`/`:257` | `rust/src/store.rs:306`/`:326` | Static catalog and contention tests. | -| `Annotations are busy; try again.` / `Archives are busy; try again.` | `src/store.ts:224`/`:235` | `rust/src/store.rs:314`/`:324` | Active busy-lock differential plus paired per-store tests. | -| `Unable to update annotations[ (<CODE>)]` / `Unable to update archives[ (<CODE>)]` | prefixes supplied at `src/store.ts:135`/`:147` | `rust/src/store.rs:202`/`:223` | Static catalog and rewrite failure unit paths. | -| `Nothing to copy.` | `src/manager-copy.ts:18` | `rust/src/manager_copy.rs:20` | `screen.manager.empty-actions`. | -| `Nothing to copy and archive.` | `src/archive-workflow.ts:35` | `rust/src/archive_workflow.rs:44` | `screen.manager.empty-actions`. | -| `No archive selected.` | `src/manager.ts:274`/`:351` | `rust/src/manager.rs:505`/`:609` | `screen.manager.empty-actions`. | -| `Copied and archived, but active annotations remain: <store error>` | `src/manager.ts:239`, `src/export-archive.ts:35` | `rust/src/manager.rs:570`, `rust/src/cli.rs:167` | Paired workflow partial-failure tests plus static catalog. | -| `Annotations restored, but the archive remains: <store error>` | `src/manager.ts:289` | `rust/src/manager.rs:628` | Paired workflow partial-failure tests plus static catalog. | -| Child `herdr` stderr, or `herdr <argv> failed` | `src/herdr.ts:20` | `rust/src/herdr.rs:32`–`:39` | Manage child-stderr and empty-stderr cases; capture pane failure. | - -`Unable to save annotation.` (with a period) at `src/editor.ts:146` is specifically a Bun dynamic -module-loader failure. There is no corresponding runtime condition in the statically linked binary; -actual store-open/write failures take the mapped `Unable to save annotation[ (<CODE>)]` path on both -sides. JSON-parser and raw filesystem diagnostics emitted before a Lite-defined wrapper are supplied -by Bun or the Rust standard library, not authored stable strings; both processes fail and surface the -native diagnostic, but those platform/runtime wordings are not claimed as a portable Lite contract. - -## Test and harness coverage - -The existing behavior-spec mapping remains in `docs/rust-lite-parity.md`. The Rust unit modules mirror -all TypeScript spec files: `types`, `paths`, `handoff`, `format`, `width`, `layout`, `store`, -`archive-workflow`, and `manager-copy`. Rust additionally has `TestBackend` editor/manager frames and -subprocess command tests. The differential harness is independent of those expected-value tests: its -oracle is the current TypeScript implementation running on the same fixture. - -Notable differential groups: - -- `process.capture.*`: context > handoff > clipboard precedence; stale/blank/lossy-UTF-8 handoff; - invalid context; empty; missing adapter/env; pane success/failure and pending cleanup. -- `process.copy.*`: missing/empty/single/plural/invalid stores; writer failure; fresh/stale locks. -- `process.copy-archive.*`: missing/empty stores; a complete archive byte-compared afterwards; writer - failure leaving both stores untouched. -- `manifest.*`: both Lite manifests declare the same action and pane ids, titles, descriptions, - contexts, placements, geometry, platform gates, and per-runtime argv, and the harness drives every - declared entrypoint. -- `process.manage.*`, `process.editor.*`, `process.manager.*`: argv/error fallback and initialization. -- `screen.editor.*`: both pending sources, every requested edit key, save branches, cancel keys, and SIGTERM. - `screen.editor.word-line-moves` and `screen.editor.word-line-kills` send the raw byte sequence for - each word/line key (`\x1b[1;3D`, `\x1b[1;3C`, `\x1b[1;5D`, `\x1b[1;5C`, `\x1b[1;9D`, `\x1b[1;9C`, - `\x1bb`, `\x1bf`, `\x1b\x7f`, `\x17`, `\x15`, `\x01`, `\x05`) and type a marker character after - each one, because a compared screen holds cells and not the cursor. The saved store bytes are the - record of where both runtimes put the cursor. -- `screen.manager.*`: both views, every view-valid key, ignored cross-view keys by handler mapping, - both confirmation flows, empty actions, exit keys, and SIGHUP. -- `store.manager.*`: successful clipboard-only and copy/archive products; the three - `store.manager.osc52-remote-*` cases copy with the native writer failing, which is the - remote-server shape of issue #40; `store.cross-read` proves each implementation parses and exports - the other's editor-written record. - -The harness itself asserts the requested key-coverage set before it can print green. - -## Findings fixed while building the proof - -Each observed mismatch was fixed in a separate commit whose message cites this map section: - -- `b50f388`: missing store directories now use the process-default mode, matching Bun. -- `28e3131`: direct invocation fallback preserves its distinct TypeScript JSON property order. -- `1d75160`: handoff UTF-8 decoding uses replacement characters, matching Bun. -- `89e41d8`: SIGTERM/SIGHUP exits restore the terminal and return 0. -- `53ef811`: pending deletion ignores a missing-at-delete race like `{ force: true }`. -- `3cb49ec`: non-NotFound handoff deletion failures now propagate instead of silently falling back. - -No TypeScript source changed. - -## Deliberate divergences - -There is one. TypeScript delegates manager timestamps to `Date.prototype.toLocaleString()` and the -host's locale database (`src/manager.ts:134`, `:160`, `:171`). Rust parses into local time and emits -the en-US shape explicitly (`rust/src/manager.rs:691`). The persisted ISO timestamp, ordering, export, -and en-US display are identical. A non-en-US host can display localized punctuation/order in -TypeScript while Rust stays en-US. The harness pins UTC and en-US and compares the literal seeded -timestamp cells; it does not normalize them. - -Windows remains unexecuted by this Unix PTY harness, as required. It is a verification gap for the -separate promotion track, not an intentional product behavior difference. diff --git a/docs/rust-lite-parity.md b/docs/rust-lite-parity.md deleted file mode 100644 index 6039de3..0000000 --- a/docs/rust-lite-parity.md +++ /dev/null @@ -1,124 +0,0 @@ -# Rust Lite parity checklist - -This document maps the TypeScript Lite implementation at `origin/main` to the native evaluation -crate. Checked items have a Rust implementation and automated coverage. Live checks are recorded -separately so test coverage is not confused with behavior observed inside Herdr. - -## Entrypoints and plugin contract - -- [x] `capture.ts` → `herdr-annotate capture`: selection precedence is invocation context, fresh - one-shot handoff file, then platform clipboard; blank selections notify without failing; pending - JSON is mode 0600; pane-open failure removes it. -- [x] `export.ts` → `herdr-annotate copy-context`: loads newest first, formats Markdown, copies, and - sends the same singular/plural notifications and errors. -- [x] `export-archive.ts` → `herdr-annotate copy-archive`: injects the dependencies `manager.ts` passes - to `copyAndArchiveAnnotations`, maps the three outcomes to the same notifications, and exits 0 on an - empty store and 1 on a real failure. -- [x] `open-manager.ts` → `herdr-annotate manage`: requires `HERDR_PLUGIN_ROOT` and opens the same - focused 100×30 popup. -- [x] `editor.ts` → `herdr-annotate editor`: pending-file and invocation-context fallback, delete-on- - successful-read behavior, multiline Unicode editing, wide-cell cursor layout, Ctrl+S validation, - save delay, Esc/Ctrl+C cancellation, raw terminal restoration. -- [x] `edit-keys.ts` → `rust/src/edit_keys.rs`: the same `wordStart`/`wordEnd`/`lineStart`/`lineEnd` - boundaries, including JavaScript's `\s` class, one-step newline crossing, and CJK runs as single - words; the same key map for Option/Alt and Ctrl arrows, Command/Super arrows, Alt+B/Alt+F, - Ctrl+A/Ctrl+E, and the Ctrl+W/Alt+Backspace/Ctrl+U kills. -- [x] `manager.ts` → `herdr-annotate manager`: active/archive views, newest-first lists, detail panes, - navigation, copy one/all, copy-and-archive, delete, double-confirm clear, restore, double-confirm - permanent archive deletion, reload, status messages, Esc/Tab/q/Ctrl+C behavior. Every pane copy also - emits OSC 52 so Herdr 0.9.0 forwards it to the viewing client's terminal. -- [x] `lite-rs/herdr-plugin.toml` preserves plugin id `annotate`, action ids, pane ids, placements, - dimensions, contexts, and supported platform declarations. All commands are the one native binary. - `scripts/parity-lite.py` compares those declarations against `lite/herdr-plugin.toml` and the root - Full manifest field by field. - -## Modules and compatibility boundaries - -- [x] `types.ts`: retained Herdr fields, permissive context projection, required pending fields, - non-empty saved fields, complete version-1 archive validation, unknown-field tolerance. -- [x] `paths.ts`: state/root environment variables, Windows extended drive and UNC normalization, - `annotations.jsonl` and `archives.jsonl` names. -- [x] `store.ts`: append-order JSONL, exact camelCase field names and distinct TypeScript pending and - saved-record field orders, trailing newline, mode 0600, whole-store invalid-data rejection, ID - merge/remove, atomic temporary replace, per-store directory locks, owner tokens, 30-second stale - recovery, ownership-checked release. -- [x] `format.ts`: control sanitization, four-space tabs, CRLF normalization, explicit-newline and - terminal-cell wrapping, Markdown headings/source/fences/blank-line shape, safe longer backtick fence. -- [x] `width.ts` and `layout.ts`: the same wide ranges, zero-width controls/combining marks, non- - splitting truncation/wrapping, terminal-cell cursor coordinates. -- [x] `handoff.ts`: per-user runtime/temp path, 15-second freshness, read-once removal, blank rejection. -- [x] `clipboard.ts`: `pbpaste`/`pbcopy`; PowerShell raw get/set; Linux Wayland then xclip then xsel. -- [x] `pane-clipboard.ts`: identical `ESC ] 52 ; c ; <base64> BEL` bytes over the raw UTF-8 text, - emitted after the native write at the same two manager call sites, the same rule that either - destination landing is a successful copy, and the same advisory 74994-byte payload limit with no - truncation. -- [x] `herdr.ts`: `HERDR_BIN_PATH` override, stderr projection, best-effort notifications. -- [x] `archive-workflow.ts` and `manager-copy.ts`: operation order and partial-failure states, including - preserving a concurrently saved annotation. - -`plannotator-tui-schema` is intentionally not a dependency: its `Annotation` is the Full product's -document-anchor/API wire shape, not Lite's existing terminal-selection JSONL shape. - -## TypeScript test mapping - -| TypeScript spec | Rust counterpart | -|---|---| -| `test/types.test.ts` | `types::tests` (8 tests, including JSON-number and JS-trim edges) | -| `test/paths.test.ts` | `paths::tests` (2 tests; all five TS path cases covered) | -| `test/handoff.test.ts` | `handoff::tests` (4 tests, including future-clock skew) | -| `test/format.test.ts` | `format::tests` plus malformed-boundary store/type tests | -| `test/width.test.ts` | `width::tests` (3 grouped tests covering every assertion) | -| `test/layout.test.ts` | `layout::tests` (3 grouped tests covering every assertion) | -| `test/edit-keys.test.ts` | `edit_keys::tests` (14 tests, including the JavaScript whitespace class) | -| `test/store.test.ts` | `store::tests` (6 tests) | -| `test/archive-workflow.test.ts` | `archive_workflow::tests` (8 tests) | -| `test/export-archive.test.ts` | `cli::tests::copy_archive_maps_every_outcome_to_its_notification_and_exit_status` | -| `test/manager-copy.test.ts` | `manager_copy::tests` (3 tests) | -| `test/pane-clipboard.test.ts` | `pane_clipboard::tests` (8 tests, including base64 chunk padding) | - -Additional Rust-only coverage: - -- `editor::tests`: headless 86×22 `TestBackend` frame, Unicode edit keys, word/line moves and kills - wired through `handle_key`, empty-save validation, quit. -- `manager::tests`: headless 98×28 active/archive frames, newest-first detail, exact TypeScript detail - width at the clipping boundary, confirmation, real clear. -- `rust/tests/commands.rs`: subprocess-level capture/manage/copy-context commands with a fake Herdr, - including exact pane argv, pending JSON, notifications, and pending cleanup on pane-open failure. -- `types::tests::serialization_matches_the_typescript_pending_and_saved_field_order`: literal pending - and saved JSON byte shapes. - -## Distribution and verification - -- [x] Rust 2024, Rust 1.96, repository lint policy, pedantic Clippy with `-D warnings`, no production - `unwrap()`, release LTO/strip, and `cargo fmt`. -- [x] Checksummed release assets for macOS (Intel/Apple Silicon), Linux (x86_64/aarch64), and Windows - are defined by `.github/workflows/rust-lite-release.yml`. -- [x] Unix and PowerShell installers verify `SHA256SUMS`; `lite-rs/scripts/stage-local.sh` builds and - stages the binary before `herdr plugin link` (link intentionally does not run manifest build hooks). -- [x] `scripts/smoke-rust-lite.sh` refuses the default session, preserves/restores the globally - installed `annotate` plugin, checks the native manifest/binary, and renders/closes the manager - entrypoint through Herdr. -- [ ] Release download mode is defined and tested structurally, but cannot be exercised before an - unmerged evaluation tag/release exists. -- [ ] Windows behavior is covered by platform-specific code and Windows CI, but was not live-tested - on this macOS development host. - -## Live verification - -On 2026-08-29, `scripts/smoke-rust-lite.sh` completed with zero failures against only the disposable -`rust-lite-test` session on macOS. It built and staged the release binary, linked `lite-rs`, verified -all three action commands and the bundled version, rendered the native manager popup through Herdr, -and closed it. The previously installed GitHub `annotate` plugin was restored at its exact commit, -and the disposable session was stopped afterward. - -The capture/editor save flow, system clipboard adapters, archive mutations, and every manager key -path were verified by automated unit, subprocess, and `TestBackend` tests rather than mutating live -annotation or clipboard data. Windows remains CI-only until it is exercised on a Windows host. - -## Deliberate display-level difference - -JavaScript's `Date.toLocaleString()` delegates to the host's full locale database. The Rust manager -uses the same local timezone but an en-US-style `M/D/YYYY, h:mm:ss AM/PM` string. Persisted timestamps, -sorting, and export do not change; only manager timestamp presentation can differ for non-en-US users. - -No TypeScript source was changed. diff --git a/docs/windows-full-acceptance.md b/docs/windows-full-acceptance.md index e7bc10e..67ea213 100644 --- a/docs/windows-full-acceptance.md +++ b/docs/windows-full-acceptance.md @@ -1,8 +1,9 @@ # Windows Full manifest acceptance checklist Decision: add `windows-full/herdr-plugin.toml`. Keep the root manifest unchanged. -The new variant requires Herdr 0.9.0 and starts the native TUI directly. The Bun -launcher is rejected. `q` quits the TUI; Escape is not a quit binding. +The new variant requires Herdr 0.9.0 and starts the native TUI directly. No +launcher process may sit between Herdr and the TUI. `q` quits the TUI; Escape is +not a quit binding. Prepared 2026-09-07 against plugin commit `46cbf5dab1746dfeb46eb160c33990b0e0de15b0`, with TUI pin `0.6.0`. @@ -15,8 +16,8 @@ requirements, not completed verification. Every box starts unchecked. - [ ] Add a GitHub-installable `windows-full/` subdirectory with plugin id `annotate`, name `Annotate`, the root plugin version, `platforms = ["windows"]`, and `min_herdr_version = "0.9.0"`. Do not rewrite the manifest during build. -- [ ] Leave `herdr-plugin.toml`, `lite/herdr-plugin.toml`, and - `lite-rs/herdr-plugin.toml` unchanged, including their `0.8.0` minimums. Preserve +- [ ] Leave `herdr-plugin.toml` and `lite/herdr-plugin.toml` unchanged, including + their `0.8.0` minimums. Preserve the root Unix wrapper and its platform gates. Do not change the TUI development manifest as part of this addition. - [ ] Compare parsed action ids, titles, descriptions, and contexts with the root @@ -36,15 +37,15 @@ requirements, not completed verification. Every box starts unchecked. - [ ] Require exactly three panes: `editor` (Annotate, popup, 88×24), `manager` (Annotations, popup, 100×30), and one `doc` (Annotate, overlay). The `doc` command must be exactly `["./bin/plannotator-tui.exe", "herdr", "pane"]`. - No shell, Bun, PowerShell, command-string interpolation, or fallback launcher - may sit between Herdr's pane process and the TUI. + No shell, PowerShell, command-string interpolation, or fallback launcher may sit + between Herdr's pane process and the TUI. - [ ] Keep `open` and `open-link` as direct argv ending in `herdr open`, and `last` as direct argv ending in `herdr last`, all using `./bin/plannotator-tui.exe`. Preserve the `markdown-file` link handler's title, pattern, and `open-link` action. All actions, panes, the handler, and the build must be effective on Windows; no inherited Unix gate may disable them. -- [ ] Reuse the existing Bun Lite sources with paths valid from `windows-full/` - (as `lite/` does with `../src/`). Verify those paths from an installed checkout, +- [ ] Reuse the shared native runtime with paths valid from `windows-full/` + (as `lite/` does with `../bin/`). Verify those paths from an installed checkout, not just the repository root. Preserve shared annotation state and archive formats; changing variants must not migrate or clear them. - [ ] On isolated installs, Herdr 0.8.2 rejects the new variant with @@ -55,8 +56,8 @@ requirements, not completed verification. Every box starts unchecked. - [ ] Extend manifest checks and CI path filters to include `windows-full/**`. Preserve existing root/Lite/development-manifest assertions rather than changing them to expect Windows Full everywhere. Run onboarding section 8's - applicable static, unit, manifest, and Lite parity checks with zero unexpected - divergences, plus Unix fetcher regression checks. + applicable static, unit, manifest, and Lite regression checks with zero + unexpected failures, plus Unix fetcher regression checks. ## 2. PowerShell fetch contract @@ -132,10 +133,10 @@ does not establish that a real Herdr pane works. unchanged. Exercise `open`, `open-link` (including a percent-encoded `file://` path), and `last` with controlled context/transcript fixtures. Record fixture evidence separately from real-agent integration. -- [ ] Open Full with Bun absent from pane `PATH`, and with inert review-folder - `bunfig.toml`/`.env` fixtures that would change a Bun launcher. Full must render - the intended document without loading those files. With Bun restored, smoke - the reused Lite action/pane paths from the installed subdirectory. +- [ ] Open Full with inert review-folder `.env` fixtures that would change a + launcher process. Full must render the intended document without loading those + files. Smoke the reused Lite action and pane paths from the installed + subdirectory. - [ ] Send `q` through ConPTY input to the normal viewer. Assert TUI exit code zero, closure of the review pane, and a responsive original pane/client. Repeat in the default overlay and supported popup/split placements. Record Escape @@ -223,7 +224,8 @@ for its specific assertions; it does not qualify the complete user experience. > The installer downloads the pinned native Windows TUI and verifies its > SHA-256 checksum. Document and agent-reply review starts that executable - > directly. Bun must still be on `PATH` for the Lite annotation tools. + > directly. The Lite annotation tools run the native `herdr-annotate` binary the + > same install downloads. > Press `q` to close the TUI. Escape is not a TUI quit key. > > Native terminal interaction, real-agent delivery, outer clipboard behavior, diff --git a/lite-rs/herdr-annotate.version b/herdr-annotate.version similarity index 100% rename from lite-rs/herdr-annotate.version rename to herdr-annotate.version diff --git a/herdr-plugin.toml b/herdr-plugin.toml index 9148924..48d2136 100644 --- a/herdr-plugin.toml +++ b/herdr-plugin.toml @@ -1,37 +1,52 @@ id = "annotate" name = "Annotate" -version = "0.3.0" +version = "0.4.0" min_herdr_version = "0.8.0" description = "Comment on terminal selections and copy annotations as agent context." platforms = ["linux", "macos", "windows"] +# --------------------------------------------------------------------------------------- +# Terminal annotations, served by the native herdr-annotate runtime. A prebuilt binary is +# fetched into bin/ at build time on all three platforms. Herdr resolves explicit relative +# programs against $HERDR_PLUGIN_ROOT, and the two panes below are opened by the plugin itself +# with --cwd set to that root, so one argv-only command works everywhere. The `.exe` suffix is +# an ordinary executable filename on macOS and Linux and the required suffix on Windows. + +[[build]] +platforms = ["macos", "linux"] +command = ["bash", "scripts/fetch-herdr-annotate.sh"] + +[[build]] +platforms = ["windows"] +command = ["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", "scripts/fetch-herdr-annotate.ps1"] + [[actions]] id = "capture" title = "Annotate selection" description = "Open a comment dialog for the current terminal selection." contexts = ["pane"] -command = ["bun", "src/capture.ts"] +command = ["./bin/herdr-annotate.exe", "capture"] [[actions]] id = "copy-context" title = "Copy annotations as context" description = "Copy all saved annotations to the clipboard as Markdown." contexts = ["global"] -command = ["bun", "src/export.ts"] +command = ["./bin/herdr-annotate.exe", "copy-context"] [[actions]] id = "copy-archive" title = "Copy annotations as context and archive them" description = "Copy all saved annotations to the clipboard as Markdown, then archive them." contexts = ["global"] -command = ["bun", "src/export-archive.ts"] +command = ["./bin/herdr-annotate.exe", "copy-archive"] [[actions]] id = "manage" title = "Manage annotations" description = "Browse, copy, archive, restore, and delete annotations." contexts = ["global"] -command = ["bun", "src/open-manager.ts"] +command = ["./bin/herdr-annotate.exe", "manage"] [[panes]] id = "editor" @@ -39,7 +54,7 @@ title = "Annotate" placement = "popup" width = 88 height = 24 -command = ["bun", "src/editor.ts"] +command = ["./bin/herdr-annotate.exe", "editor"] [[panes]] id = "manager" @@ -47,7 +62,7 @@ title = "Annotations" placement = "popup" width = 100 height = 30 -command = ["bun", "src/manager.ts"] +command = ["./bin/herdr-annotate.exe", "manager"] # --------------------------------------------------------------------------------------- # Document review with plannotator-tui (https://github.com/plannotator/plannotator-tui). diff --git a/lite-rs/bin/.gitkeep b/lite-rs/bin/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/lite-rs/bin/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lite-rs/herdr-plugin.toml b/lite-rs/herdr-plugin.toml deleted file mode 100644 index bf3f1c7..0000000 --- a/lite-rs/herdr-plugin.toml +++ /dev/null @@ -1,64 +0,0 @@ -# Herdr Annotate Lite, native Rust evaluation build. -# Link locally after staging a build with scripts/stage-local.sh. - -id = "annotate" -name = "Annotate" -version = "0.3.0-rust.1" -min_herdr_version = "0.8.0" -description = "Comment on terminal selections and copy annotations as agent context. Native Rust evaluation build." -platforms = ["linux", "macos", "windows"] - -[[build]] -platforms = ["macos", "linux"] -command = ["bash", "scripts/fetch-herdr-annotate.sh"] - -[[build]] -platforms = ["windows"] -command = ["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", "scripts/fetch-herdr-annotate.ps1"] - -# Herdr resolves explicit relative programs against $HERDR_PLUGIN_ROOT. The common `.exe` -# suffix lets one argv-only manifest work on all three platforms; it is an ordinary executable -# filename on macOS/Linux and the required executable suffix on Windows. -[[actions]] -id = "capture" -title = "Annotate selection" -description = "Open a comment dialog for the current terminal selection." -contexts = ["pane"] -command = ["./bin/herdr-annotate.exe", "capture"] - -[[actions]] -id = "copy-context" -title = "Copy annotations as context" -description = "Copy all saved annotations to the clipboard as Markdown." -contexts = ["global"] -command = ["./bin/herdr-annotate.exe", "copy-context"] - -[[actions]] -id = "copy-archive" -title = "Copy annotations as context and archive them" -description = "Copy all saved annotations to the clipboard as Markdown, then archive them." -contexts = ["global"] -command = ["./bin/herdr-annotate.exe", "copy-archive"] - -[[actions]] -id = "manage" -title = "Manage annotations" -description = "Browse, copy, archive, restore, and delete annotations." -contexts = ["global"] -command = ["./bin/herdr-annotate.exe", "manage"] - -[[panes]] -id = "editor" -title = "Annotate" -placement = "popup" -width = 88 -height = 24 -command = ["./bin/herdr-annotate.exe", "editor"] - -[[panes]] -id = "manager" -title = "Annotations" -placement = "popup" -width = 100 -height = 30 -command = ["./bin/herdr-annotate.exe", "manager"] diff --git a/lite-rs/scripts/stage-local.sh b/lite-rs/scripts/stage-local.sh deleted file mode 100755 index 7ea9727..0000000 --- a/lite-rs/scripts/stage-local.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# Build and stage the native runtime for `herdr plugin link`, which intentionally skips -# manifest build hooks. Run from any directory in the checkout. -set -euo pipefail - -plugin_root="$(cd "$(dirname "$0")/.." && pwd)" -repository_root="$(cd "$plugin_root/.." && pwd)" -cargo build --manifest-path "$repository_root/rust/Cargo.toml" --release -HERDR_ANNOTATE_BIN="$repository_root/rust/target/release/herdr-annotate" \ - bash "$plugin_root/scripts/fetch-herdr-annotate.sh" -echo "staged $plugin_root/bin/herdr-annotate.exe" -echo "link with: herdr plugin link $plugin_root" diff --git a/lite/herdr-plugin.toml b/lite/herdr-plugin.toml index e6d5374..7337920 100644 --- a/lite/herdr-plugin.toml +++ b/lite/herdr-plugin.toml @@ -1,41 +1,57 @@ -# Herdr Annotate, lite: the TypeScript tools only, no plannotator-tui binary download. +# Herdr Annotate, lite: the terminal annotation tools only, no plannotator-tui binary download. # Install with: herdr plugin install plannotator/herdr-annotate/lite # The full plugin at the repository root adds document review with plannotator-tui. +# +# The build hook and every command reach one directory up, so the Lite variant shares the +# repository's single staged binary the way it used to share its TypeScript sources. Link a local +# build with scripts/stage-local.sh, which `herdr plugin link` cannot run for you. id = "annotate" name = "Annotate" -version = "0.3.0" +version = "0.4.0" min_herdr_version = "0.8.0" description = "Comment on terminal selections and copy annotations as agent context." platforms = ["linux", "macos", "windows"] +[[build]] +platforms = ["macos", "linux"] +command = ["bash", "../scripts/fetch-herdr-annotate.sh"] + +[[build]] +platforms = ["windows"] +command = ["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", "../scripts/fetch-herdr-annotate.ps1"] + +# Herdr resolves explicit relative programs against $HERDR_PLUGIN_ROOT, and the two panes below +# are opened by the plugin itself with --cwd set to that root. The common `.exe` suffix lets one +# argv-only manifest work on all three platforms; it is an ordinary executable filename on +# macOS/Linux and the required executable suffix on Windows. [[actions]] id = "capture" title = "Annotate selection" description = "Open a comment dialog for the current terminal selection." contexts = ["pane"] -command = ["bun", "../src/capture.ts"] +command = ["../bin/herdr-annotate.exe", "capture"] [[actions]] id = "copy-context" title = "Copy annotations as context" description = "Copy all saved annotations to the clipboard as Markdown." contexts = ["global"] -command = ["bun", "../src/export.ts"] +command = ["../bin/herdr-annotate.exe", "copy-context"] [[actions]] id = "copy-archive" title = "Copy annotations as context and archive them" description = "Copy all saved annotations to the clipboard as Markdown, then archive them." contexts = ["global"] -command = ["bun", "../src/export-archive.ts"] +command = ["../bin/herdr-annotate.exe", "copy-archive"] [[actions]] id = "manage" title = "Manage annotations" description = "Browse, copy, archive, restore, and delete annotations." contexts = ["global"] -command = ["bun", "../src/open-manager.ts"] +command = ["../bin/herdr-annotate.exe", "manage"] [[panes]] id = "editor" @@ -43,7 +59,7 @@ title = "Annotate" placement = "popup" width = 88 height = 24 -command = ["bun", "../src/editor.ts"] +command = ["../bin/herdr-annotate.exe", "editor"] [[panes]] id = "manager" @@ -51,4 +67,4 @@ title = "Annotations" placement = "popup" width = 100 height = 30 -command = ["bun", "../src/manager.ts"] +command = ["../bin/herdr-annotate.exe", "manager"] diff --git a/package.json b/package.json deleted file mode 100644 index b1971bc..0000000 --- a/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "herdr-annotate", - "version": "0.3.0", - "private": true, - "type": "module", - "scripts": { - "test": "bun test", - "typecheck": "bunx tsc --noEmit" - }, - "devDependencies": { - "@types/bun": "latest", - "typescript": "^5.9.2" - } -} diff --git a/rust/src/edit_keys.rs b/rust/src/edit_keys.rs index 94726de..391cf80 100644 --- a/rust/src/edit_keys.rs +++ b/rust/src/edit_keys.rs @@ -1,7 +1,8 @@ //! Word and line editing keys for the comment editor. //! -//! This mirrors `src/edit-keys.ts` character for character so both runtimes move, and kill, by the -//! same boundaries. The helpers work on the editor's `Vec<char>` buffer and its scalar cursor. +//! This mirrors the retired Bun runtime's `edit-keys.ts` character for character, so the editor +//! still moves, and kills, by the same boundaries. The helpers work on the editor's `Vec<char>` +//! buffer and its scalar cursor. use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; diff --git a/rust/src/editor.rs b/rust/src/editor.rs index 81860d2..8d56ff8 100644 --- a/rust/src/editor.rs +++ b/rust/src/editor.rs @@ -224,7 +224,7 @@ impl EditorApp { false } - /// Apply a word or line action, mirroring the `resolveEditKey` branches in `src/editor.ts`. + /// Apply a word or line action, mirroring the retired Bun editor's `resolveEditKey` branches. fn apply_edit_action(&mut self, action: EditAction) { match action { EditAction::WordLeft => self.cursor = word_start(&self.comment, self.cursor), diff --git a/lite-rs/scripts/fetch-herdr-annotate.ps1 b/scripts/fetch-herdr-annotate.ps1 similarity index 72% rename from lite-rs/scripts/fetch-herdr-annotate.ps1 rename to scripts/fetch-herdr-annotate.ps1 index 01d5a09..37a6a06 100644 --- a/lite-rs/scripts/fetch-herdr-annotate.ps1 +++ b/scripts/fetch-herdr-annotate.ps1 @@ -1,12 +1,14 @@ $ErrorActionPreference = "Stop" Set-Location (Join-Path $PSScriptRoot "..") -$version = (Get-Content "herdr-annotate.version" -Raw).Trim() +$versionContents = Get-Content -LiteralPath "herdr-annotate.version" -Raw +$version = if ($null -eq $versionContents) { "" } else { [string]$versionContents } +$version = $version.Trim() if (-not $version) { throw "herdr-annotate.version is empty" } New-Item -ItemType Directory -Force "bin" | Out-Null $destination = Join-Path "bin" "herdr-annotate.exe" $stamp = Join-Path "bin" "herdr-annotate.version" -$installed = if (Test-Path $stamp) { (Get-Content $stamp -Raw).Trim() } else { "" } +$installed = if (Test-Path -LiteralPath $stamp -PathType Leaf) { ([string](Get-Content -LiteralPath $stamp -Raw)).Trim() } else { "" } if ((Test-Path $destination) -and $installed -eq $version -and -not $env:HERDR_ANNOTATE_BIN) { Write-Output "herdr-annotate $version already installed" @@ -17,9 +19,9 @@ if ($env:HERDR_ANNOTATE_BIN) { if (-not (Test-Path $env:HERDR_ANNOTATE_BIN -PathType Leaf)) { throw "HERDR_ANNOTATE_BIN is not a file: $env:HERDR_ANNOTATE_BIN" } - Copy-Item -Force $env:HERDR_ANNOTATE_BIN "$destination.tmp" - Move-Item -Force "$destination.tmp" $destination - Set-Content -NoNewline $stamp $version + Copy-Item -Force -LiteralPath $env:HERDR_ANNOTATE_BIN -Destination "$destination.tmp" + Move-Item -Force -LiteralPath "$destination.tmp" -Destination $destination + Set-Content -LiteralPath $stamp -NoNewline -Value $version Write-Output "installed herdr-annotate from $env:HERDR_ANNOTATE_BIN (local build, stamped $version)" exit 0 } @@ -41,10 +43,10 @@ try { if (-not $line) { throw "$asset is not listed in $base/SHA256SUMS" } $expected = ($line -split "\s+")[0].ToLowerInvariant() $actual = (Get-FileHash -Algorithm SHA256 (Join-Path $temporary $asset)).Hash.ToLowerInvariant() - if ($actual -ne $expected) { throw "sha256 mismatch for $asset: expected $expected, got $actual" } + if ($actual -ne $expected) { throw "sha256 mismatch for ${asset}: expected $expected, got $actual" } Copy-Item -Force (Join-Path $temporary $asset) "$destination.tmp" - Move-Item -Force "$destination.tmp" $destination - Set-Content -NoNewline $stamp $version + Move-Item -Force -LiteralPath "$destination.tmp" -Destination $destination + Set-Content -LiteralPath $stamp -NoNewline -Value $version Write-Output "installed herdr-annotate $version ($target)" } finally { diff --git a/lite-rs/scripts/fetch-herdr-annotate.sh b/scripts/fetch-herdr-annotate.sh similarity index 100% rename from lite-rs/scripts/fetch-herdr-annotate.sh rename to scripts/fetch-herdr-annotate.sh diff --git a/scripts/lite-goldens/fixtures/bun-editor-annotations.jsonl b/scripts/lite-goldens/fixtures/bun-editor-annotations.jsonl new file mode 100644 index 0000000..c6f1a20 --- /dev/null +++ b/scripts/lite-goldens/fixtures/bun-editor-annotations.jsonl @@ -0,0 +1 @@ +{"selectedText":"selected wide 한글 text\nwith a second line","context":{"workspace_label":"api 한","tab_label":"server"},"capturedAt":"2026-09-10T05:23:55.654Z","id":"72a4a0d4-f9c9-4e02-afe0-a189fbdf3a6a","comment":"alpha 한글 é\nbt","createdAt":"2026-09-10T05:23:56.623Z"} diff --git a/scripts/lite-goldens/process.capture.blank-handoff.json b/scripts/lite-goldens/process.capture.blank-handoff.json new file mode 100644 index 0000000..dd0e2c8 --- /dev/null +++ b/scripts/lite-goldens/process.capture.blank-handoff.json @@ -0,0 +1,26 @@ +{ + "process.capture.blank-handoff.artifact": [ + "{", + " \"pending\": {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"clipboard after blank handoff\\\",\\\"context\\\":{},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"mode\": \"0600\",", + " \"name\": \"pending-<TIME>-<PID>.json\"", + " },", + " \"runtime\": \"[\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\".\\\"\\n },\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\"herdr-annotate-<UID>\\\"\\n }\\n]\\n\"", + "}" + ], + "process.capture.blank-handoff.exit": [ + "0" + ], + "process.capture.blank-handoff.processes": [ + "{\"args\":[],\"command\":\"clipboard-read\"}", + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"editor\",\"--placement\",\"popup\",\"--width\",\"88\",\"--height\",\"24\",\"--env\",\"HERDR_ANNOTATE_PENDING=<ROOT>/pending-<TIME>-<PID>.json\",\"--focus\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.blank-handoff.stderr": [ + "" + ], + "process.capture.blank-handoff.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.clipboard.json b/scripts/lite-goldens/process.capture.clipboard.json new file mode 100644 index 0000000..5d57212 --- /dev/null +++ b/scripts/lite-goldens/process.capture.clipboard.json @@ -0,0 +1,23 @@ +{ + "process.capture.clipboard.artifact": [ + "{", + " \"bytes\": \"{\\\"selectedText\\\":\\\"clipboard 한 selection\\\",\\\"context\\\":{},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"mode\": \"0600\",", + " \"name\": \"pending-<TIME>-<PID>.json\"", + "}" + ], + "process.capture.clipboard.exit": [ + "0" + ], + "process.capture.clipboard.processes": [ + "{\"args\":[],\"command\":\"clipboard-read\"}", + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"editor\",\"--placement\",\"popup\",\"--width\",\"88\",\"--height\",\"24\",\"--env\",\"HERDR_ANNOTATE_PENDING=<ROOT>/pending-<TIME>-<PID>.json\",\"--focus\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.clipboard.stderr": [ + "" + ], + "process.capture.clipboard.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.context.json b/scripts/lite-goldens/process.capture.context.json new file mode 100644 index 0000000..acc1a68 --- /dev/null +++ b/scripts/lite-goldens/process.capture.context.json @@ -0,0 +1,25 @@ +{ + "process.capture.context.artifact": [ + "{", + " \"pending\": {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"context selection 한글\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"mode\": \"0600\",", + " \"name\": \"pending-<TIME>-<PID>.json\"", + " },", + " \"runtime\": \"[\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\".\\\"\\n },\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\"herdr-annotate-<UID>\\\"\\n },\\n {\\n \\\"bytes\\\": \\\"lower-priority handoff\\\",\\n \\\"kind\\\": \\\"file\\\",\\n \\\"mode\\\": \\\"0644\\\",\\n \\\"path\\\": \\\"herdr-annotate-<UID>/selection\\\"\\n }\\n]\\n\"", + "}" + ], + "process.capture.context.exit": [ + "0" + ], + "process.capture.context.processes": [ + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"editor\",\"--placement\",\"popup\",\"--width\",\"88\",\"--height\",\"24\",\"--env\",\"HERDR_ANNOTATE_PENDING=<ROOT>/pending-<TIME>-<PID>.json\",\"--focus\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.context.stderr": [ + "" + ], + "process.capture.context.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.empty.json b/scripts/lite-goldens/process.capture.empty.json new file mode 100644 index 0000000..0a820ab --- /dev/null +++ b/scripts/lite-goldens/process.capture.empty.json @@ -0,0 +1,19 @@ +{ + "process.capture.empty.artifact": [ + "[]" + ], + "process.capture.empty.exit": [ + "0" + ], + "process.capture.empty.processes": [ + "{\"args\":[],\"command\":\"clipboard-read\"}", + "{\"args\":[\"notification\",\"show\",\"Nothing to annotate\",\"--body\",\"Select text in Herdr or copy text to the clipboard.\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.empty.stderr": [ + "" + ], + "process.capture.empty.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.handoff.json b/scripts/lite-goldens/process.capture.handoff.json new file mode 100644 index 0000000..e009aa7 --- /dev/null +++ b/scripts/lite-goldens/process.capture.handoff.json @@ -0,0 +1,25 @@ +{ + "process.capture.handoff.artifact": [ + "{", + " \"pending\": {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"handoff selection\\\\n\\\",\\\"context\\\":{},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"mode\": \"0600\",", + " \"name\": \"pending-<TIME>-<PID>.json\"", + " },", + " \"runtime\": \"[\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\".\\\"\\n },\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\"herdr-annotate-<UID>\\\"\\n }\\n]\\n\"", + "}" + ], + "process.capture.handoff.exit": [ + "0" + ], + "process.capture.handoff.processes": [ + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"editor\",\"--placement\",\"popup\",\"--width\",\"88\",\"--height\",\"24\",\"--env\",\"HERDR_ANNOTATE_PENDING=<ROOT>/pending-<TIME>-<PID>.json\",\"--focus\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.handoff.stderr": [ + "" + ], + "process.capture.handoff.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.invalid-context.json b/scripts/lite-goldens/process.capture.invalid-context.json new file mode 100644 index 0000000..5400121 --- /dev/null +++ b/scripts/lite-goldens/process.capture.invalid-context.json @@ -0,0 +1,23 @@ +{ + "process.capture.invalid-context.artifact": [ + "{", + " \"bytes\": \"{\\\"selectedText\\\":\\\"clipboard after invalid context\\\",\\\"context\\\":{},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"mode\": \"0600\",", + " \"name\": \"pending-<TIME>-<PID>.json\"", + "}" + ], + "process.capture.invalid-context.exit": [ + "0" + ], + "process.capture.invalid-context.processes": [ + "{\"args\":[],\"command\":\"clipboard-read\"}", + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"editor\",\"--placement\",\"popup\",\"--width\",\"88\",\"--height\",\"24\",\"--env\",\"HERDR_ANNOTATE_PENDING=<ROOT>/pending-<TIME>-<PID>.json\",\"--focus\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.invalid-context.stderr": [ + "" + ], + "process.capture.invalid-context.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.invalid-utf8-handoff.json b/scripts/lite-goldens/process.capture.invalid-utf8-handoff.json new file mode 100644 index 0000000..14440fa --- /dev/null +++ b/scripts/lite-goldens/process.capture.invalid-utf8-handoff.json @@ -0,0 +1,25 @@ +{ + "process.capture.invalid-utf8-handoff.artifact": [ + "{", + " \"pending\": {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"invalid-�-handoff\\\",\\\"context\\\":{},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"mode\": \"0600\",", + " \"name\": \"pending-<TIME>-<PID>.json\"", + " },", + " \"runtime\": \"[\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\".\\\"\\n },\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\"herdr-annotate-<UID>\\\"\\n }\\n]\\n\"", + "}" + ], + "process.capture.invalid-utf8-handoff.exit": [ + "0" + ], + "process.capture.invalid-utf8-handoff.processes": [ + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"editor\",\"--placement\",\"popup\",\"--width\",\"88\",\"--height\",\"24\",\"--env\",\"HERDR_ANNOTATE_PENDING=<ROOT>/pending-<TIME>-<PID>.json\",\"--focus\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.invalid-utf8-handoff.stderr": [ + "" + ], + "process.capture.invalid-utf8-handoff.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.missing-root.json b/scripts/lite-goldens/process.capture.missing-root.json new file mode 100644 index 0000000..24c7332 --- /dev/null +++ b/scripts/lite-goldens/process.capture.missing-root.json @@ -0,0 +1,16 @@ +{ + "process.capture.missing-root.exit": [ + "1" + ], + "process.capture.missing-root.processes": [ + "{\"args\":[\"notification\",\"show\",\"Annotate failed\",\"--body\",\"HERDR_PLUGIN_ROOT is not set\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.missing-root.stderr": [ + "HERDR_PLUGIN_ROOT is not set", + "" + ], + "process.capture.missing-root.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.missing-state.json b/scripts/lite-goldens/process.capture.missing-state.json new file mode 100644 index 0000000..22335f8 --- /dev/null +++ b/scripts/lite-goldens/process.capture.missing-state.json @@ -0,0 +1,16 @@ +{ + "process.capture.missing-state.exit": [ + "1" + ], + "process.capture.missing-state.processes": [ + "{\"args\":[\"notification\",\"show\",\"Annotate failed\",\"--body\",\"HERDR_PLUGIN_STATE_DIR is not set\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.missing-state.stderr": [ + "HERDR_PLUGIN_STATE_DIR is not set", + "" + ], + "process.capture.missing-state.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.no-clipboard.json b/scripts/lite-goldens/process.capture.no-clipboard.json new file mode 100644 index 0000000..cb685f0 --- /dev/null +++ b/scripts/lite-goldens/process.capture.no-clipboard.json @@ -0,0 +1,20 @@ +{ + "process.capture.no-clipboard.artifact": [ + "[]" + ], + "process.capture.no-clipboard.exit": [ + "1" + ], + "process.capture.no-clipboard.processes": [ + "{\"args\":[],\"command\":\"clipboard-read\"}", + "{\"args\":[\"notification\",\"show\",\"Annotate failed\",\"--body\",\"No supported clipboard reader is available\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.no-clipboard.stderr": [ + "No supported clipboard reader is available", + "" + ], + "process.capture.no-clipboard.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.open-failure.json b/scripts/lite-goldens/process.capture.open-failure.json new file mode 100644 index 0000000..1c661f0 --- /dev/null +++ b/scripts/lite-goldens/process.capture.open-failure.json @@ -0,0 +1,20 @@ +{ + "process.capture.open-failure.artifact": [ + "[]" + ], + "process.capture.open-failure.exit": [ + "1" + ], + "process.capture.open-failure.processes": [ + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"editor\",\"--placement\",\"popup\",\"--width\",\"88\",\"--height\",\"24\",\"--env\",\"HERDR_ANNOTATE_PENDING=<ROOT>/pending-<TIME>-<PID>.json\",\"--focus\"],\"command\":\"herdr-fake\"}", + "{\"args\":[\"notification\",\"show\",\"Annotate failed\",\"--body\",\"pane open failed\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.open-failure.stderr": [ + "pane open failed", + "" + ], + "process.capture.open-failure.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.capture.stale-handoff.json b/scripts/lite-goldens/process.capture.stale-handoff.json new file mode 100644 index 0000000..350df90 --- /dev/null +++ b/scripts/lite-goldens/process.capture.stale-handoff.json @@ -0,0 +1,26 @@ +{ + "process.capture.stale-handoff.artifact": [ + "{", + " \"pending\": {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"clipboard after stale handoff\\\",\\\"context\\\":{},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"mode\": \"0600\",", + " \"name\": \"pending-<TIME>-<PID>.json\"", + " },", + " \"runtime\": \"[\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\".\\\"\\n },\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\"herdr-annotate-<UID>\\\"\\n }\\n]\\n\"", + "}" + ], + "process.capture.stale-handoff.exit": [ + "0" + ], + "process.capture.stale-handoff.processes": [ + "{\"args\":[],\"command\":\"clipboard-read\"}", + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"editor\",\"--placement\",\"popup\",\"--width\",\"88\",\"--height\",\"24\",\"--env\",\"HERDR_ANNOTATE_PENDING=<ROOT>/pending-<TIME>-<PID>.json\",\"--focus\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.capture.stale-handoff.stderr": [ + "" + ], + "process.capture.stale-handoff.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy-archive.empty.json b/scripts/lite-goldens/process.copy-archive.empty.json new file mode 100644 index 0000000..fb75b97 --- /dev/null +++ b/scripts/lite-goldens/process.copy-archive.empty.json @@ -0,0 +1,21 @@ +{ + "process.copy-archive.empty.artifact": [ + "{", + " \"clipboard\": \"\",", + " \"state\": \"[\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0755\\\",\\n \\\"path\\\": \\\".\\\"\\n }\\n]\\n\"", + "}" + ], + "process.copy-archive.empty.exit": [ + "0" + ], + "process.copy-archive.empty.processes": [ + "{\"args\":[\"notification\",\"show\",\"No annotations\",\"--body\",\"There is nothing to copy yet.\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy-archive.empty.stderr": [ + "" + ], + "process.copy-archive.empty.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy-archive.missing-state.json b/scripts/lite-goldens/process.copy-archive.missing-state.json new file mode 100644 index 0000000..095b1cc --- /dev/null +++ b/scripts/lite-goldens/process.copy-archive.missing-state.json @@ -0,0 +1,16 @@ +{ + "process.copy-archive.missing-state.exit": [ + "1" + ], + "process.copy-archive.missing-state.processes": [ + "{\"args\":[\"notification\",\"show\",\"Copy and archive failed\",\"--body\",\"HERDR_PLUGIN_STATE_DIR is not set\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy-archive.missing-state.stderr": [ + "HERDR_PLUGIN_STATE_DIR is not set", + "" + ], + "process.copy-archive.missing-state.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy-archive.no-clipboard.json b/scripts/lite-goldens/process.copy-archive.no-clipboard.json new file mode 100644 index 0000000..64839d3 --- /dev/null +++ b/scripts/lite-goldens/process.copy-archive.no-clipboard.json @@ -0,0 +1,23 @@ +{ + "process.copy-archive.no-clipboard.artifact": [ + "{", + " \"clipboard\": \"# Annotated context\\n\\n## Annotation 1\\n\\nSource: api 한 / server\\n\\nSelected text:\\n\\n```\\nthird selection\\n```\\n\\nComment:\\n\\nthird comment\\n\\n## Annotation 2\\n\\nSource: api 한 / server\\n\\nSelected text:\\n\\n```\\nwide 한글 selection and enough text to exercise clipping at the detail edge\\n```\\n\\nComment:\\n\\nsecond comment\\n\\n## Annotation 3\\n\\nSource: api 한 / server\\n\\nSelected text:\\n\\n````\\nfirst selection with ``` ticks\\nand a second line\\n````\\n\\nComment:\\n\\nfirst comment\\nwith two lines\\n\",", + " \"state\": \"[\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0700\\\",\\n \\\"path\\\": \\\".\\\"\\n },\\n {\\n \\\"bytes\\\": \\\"{\\\\\\\"selectedText\\\\\\\":\\\\\\\"first selection with ``` ticks\\\\\\\\nand a second line\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-08T00:00:00.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-one\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"first comment\\\\\\\\nwith two lines\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-08T00:00:01.000Z\\\\\\\"}\\\\n{\\\\\\\"selectedText\\\\\\\":\\\\\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-09T10:11:12.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-two\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"second comment\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-09T10:11:13.000Z\\\\\\\"}\\\\n{\\\\\\\"selectedText\\\\\\\":\\\\\\\"third selection\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-10T20:21:22.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-three\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"third comment\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-10T20:21:23.000Z\\\\\\\"}\\\\n\\\",\\n \\\"kind\\\": \\\"file\\\",\\n \\\"mode\\\": \\\"0600\\\",\\n \\\"path\\\": \\\"annotations.jsonl\\\"\\n },\\n {\\n \\\"bytes\\\": \\\"{\\\\\\\"version\\\\\\\":1,\\\\\\\"id\\\\\\\":\\\\\\\"archive-old\\\\\\\",\\\\\\\"archivedAt\\\\\\\":\\\\\\\"2026-08-20T01:02:03.000Z\\\\\\\",\\\\\\\"annotations\\\\\\\":[{\\\\\\\"selectedText\\\\\\\":\\\\\\\"first selection with ``` ticks\\\\\\\\nand a second line\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-08T00:00:00.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-one\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"first comment\\\\\\\\nwith two lines\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-08T00:00:01.000Z\\\\\\\"}]}\\\\n{\\\\\\\"version\\\\\\\":1,\\\\\\\"id\\\\\\\":\\\\\\\"archive-new\\\\\\\",\\\\\\\"archivedAt\\\\\\\":\\\\\\\"2026-08-21T01:02:03.000Z\\\\\\\",\\\\\\\"annotations\\\\\\\":[{\\\\\\\"selectedText\\\\\\\":\\\\\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-09T10:11:12.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-two\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"second comment\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-09T10:11:13.000Z\\\\\\\"},{\\\\\\\"selectedText\\\\\\\":\\\\\\\"third selection\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-10T20:21:22.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-three\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"third comment\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-10T20:21:23.000Z\\\\\\\"}]}\\\\n\\\",\\n \\\"kind\\\": \\\"file\\\",\\n \\\"mode\\\": \\\"0600\\\",\\n \\\"path\\\": \\\"archives.jsonl\\\"\\n }\\n]\\n\"", + "}" + ], + "process.copy-archive.no-clipboard.exit": [ + "1" + ], + "process.copy-archive.no-clipboard.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "{\"args\":[\"notification\",\"show\",\"Copy and archive failed\",\"--body\",\"No supported clipboard writer is available\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy-archive.no-clipboard.stderr": [ + "No supported clipboard writer is available", + "" + ], + "process.copy-archive.no-clipboard.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy-archive.populated.json b/scripts/lite-goldens/process.copy-archive.populated.json new file mode 100644 index 0000000..468bcd2 --- /dev/null +++ b/scripts/lite-goldens/process.copy-archive.populated.json @@ -0,0 +1,22 @@ +{ + "process.copy-archive.populated.artifact": [ + "{", + " \"clipboard\": \"# Annotated context\\n\\n## Annotation 1\\n\\nSource: api 한 / server\\n\\nSelected text:\\n\\n```\\nthird selection\\n```\\n\\nComment:\\n\\nthird comment\\n\\n## Annotation 2\\n\\nSource: api 한 / server\\n\\nSelected text:\\n\\n```\\nwide 한글 selection and enough text to exercise clipping at the detail edge\\n```\\n\\nComment:\\n\\nsecond comment\\n\\n## Annotation 3\\n\\nSource: api 한 / server\\n\\nSelected text:\\n\\n````\\nfirst selection with ``` ticks\\nand a second line\\n````\\n\\nComment:\\n\\nfirst comment\\nwith two lines\\n\",", + " \"state\": \"[\\n {\\n \\\"kind\\\": \\\"dir\\\",\\n \\\"mode\\\": \\\"0700\\\",\\n \\\"path\\\": \\\".\\\"\\n },\\n {\\n \\\"bytes\\\": \\\"\\\",\\n \\\"kind\\\": \\\"file\\\",\\n \\\"mode\\\": \\\"0600\\\",\\n \\\"path\\\": \\\"annotations.jsonl\\\"\\n },\\n {\\n \\\"bytes\\\": \\\"{\\\\\\\"version\\\\\\\":1,\\\\\\\"id\\\\\\\":\\\\\\\"archive-old\\\\\\\",\\\\\\\"archivedAt\\\\\\\":\\\\\\\"2026-08-20T01:02:03.000Z\\\\\\\",\\\\\\\"annotations\\\\\\\":[{\\\\\\\"selectedText\\\\\\\":\\\\\\\"first selection with ``` ticks\\\\\\\\nand a second line\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-08T00:00:00.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-one\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"first comment\\\\\\\\nwith two lines\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-08T00:00:01.000Z\\\\\\\"}]}\\\\n{\\\\\\\"version\\\\\\\":1,\\\\\\\"id\\\\\\\":\\\\\\\"archive-new\\\\\\\",\\\\\\\"archivedAt\\\\\\\":\\\\\\\"2026-08-21T01:02:03.000Z\\\\\\\",\\\\\\\"annotations\\\\\\\":[{\\\\\\\"selectedText\\\\\\\":\\\\\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-09T10:11:12.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-two\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"second comment\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-09T10:11:13.000Z\\\\\\\"},{\\\\\\\"selectedText\\\\\\\":\\\\\\\"third selection\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-10T20:21:22.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-three\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"third comment\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-10T20:21:23.000Z\\\\\\\"}]}\\\\n{\\\\\\\"version\\\\\\\":1,\\\\\\\"id\\\\\\\":\\\\\\\"<UUID>\\\\\\\",\\\\\\\"archivedAt\\\\\\\":\\\\\\\"<TIMESTAMP>\\\\\\\",\\\\\\\"annotations\\\\\\\":[{\\\\\\\"selectedText\\\\\\\":\\\\\\\"first selection with ``` ticks\\\\\\\\nand a second line\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-08T00:00:00.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-one\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"first comment\\\\\\\\nwith two lines\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-08T00:00:01.000Z\\\\\\\"},{\\\\\\\"selectedText\\\\\\\":\\\\\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-09T10:11:12.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-two\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"second comment\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-09T10:11:13.000Z\\\\\\\"},{\\\\\\\"selectedText\\\\\\\":\\\\\\\"third selection\\\\\\\",\\\\\\\"capturedAt\\\\\\\":\\\\\\\"2026-08-10T20:21:22.000Z\\\\\\\",\\\\\\\"context\\\\\\\":{\\\\\\\"workspace_id\\\\\\\":\\\\\\\"workspace-1\\\\\\\",\\\\\\\"workspace_label\\\\\\\":\\\\\\\"api 한\\\\\\\",\\\\\\\"tab_id\\\\\\\":\\\\\\\"tab-1\\\\\\\",\\\\\\\"tab_label\\\\\\\":\\\\\\\"server\\\\\\\",\\\\\\\"focused_pane_id\\\\\\\":\\\\\\\"pane-1\\\\\\\",\\\\\\\"focused_pane_cwd\\\\\\\":\\\\\\\"/workspace\\\\\\\",\\\\\\\"focused_pane_agent\\\\\\\":\\\\\\\"codex\\\\\\\"},\\\\\\\"id\\\\\\\":\\\\\\\"ann-three\\\\\\\",\\\\\\\"comment\\\\\\\":\\\\\\\"third comment\\\\\\\",\\\\\\\"createdAt\\\\\\\":\\\\\\\"2026-08-10T20:21:23.000Z\\\\\\\"}]}\\\\n\\\",\\n \\\"kind\\\": \\\"file\\\",\\n \\\"mode\\\": \\\"0600\\\",\\n \\\"path\\\": \\\"archives.jsonl\\\"\\n }\\n]\\n\"", + "}" + ], + "process.copy-archive.populated.exit": [ + "0" + ], + "process.copy-archive.populated.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "{\"args\":[\"notification\",\"show\",\"Annotations copied and archived\",\"--body\",\"3 annotations copied as Markdown and archived.\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy-archive.populated.stderr": [ + "" + ], + "process.copy-archive.populated.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy.busy-lock.json b/scripts/lite-goldens/process.copy.busy-lock.json new file mode 100644 index 0000000..3ae150e --- /dev/null +++ b/scripts/lite-goldens/process.copy.busy-lock.json @@ -0,0 +1,31 @@ +{ + "process.copy.busy-lock.artifact": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0755\",", + " \"path\": \".\"", + " },", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0755\",", + " \"path\": \".annotations.lock\"", + " }", + "]", + "" + ], + "process.copy.busy-lock.exit": [ + "1" + ], + "process.copy.busy-lock.processes": [ + "{\"args\":[\"notification\",\"show\",\"Copy failed\",\"--body\",\"Annotations are busy; try again.\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy.busy-lock.stderr": [ + "Annotations are busy; try again.", + "" + ], + "process.copy.busy-lock.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy.empty.json b/scripts/lite-goldens/process.copy.empty.json new file mode 100644 index 0000000..97c539b --- /dev/null +++ b/scripts/lite-goldens/process.copy.empty.json @@ -0,0 +1,25 @@ +{ + "process.copy.empty.artifact": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0755\",", + " \"path\": \".\"", + " }", + "]", + "" + ], + "process.copy.empty.exit": [ + "0" + ], + "process.copy.empty.processes": [ + "{\"args\":[\"notification\",\"show\",\"No annotations\",\"--body\",\"There is nothing to copy yet.\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy.empty.stderr": [ + "" + ], + "process.copy.empty.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy.invalid-store.json b/scripts/lite-goldens/process.copy.invalid-store.json new file mode 100644 index 0000000..15e24df --- /dev/null +++ b/scripts/lite-goldens/process.copy.invalid-store.json @@ -0,0 +1,16 @@ +{ + "process.copy.invalid-store.exit": [ + "1" + ], + "process.copy.invalid-store.processes": [ + "{\"args\":[\"notification\",\"show\",\"Copy failed\",\"--body\",\"Unable to read annotations (invalid data)\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy.invalid-store.stderr": [ + "Unable to read annotations (invalid data)", + "" + ], + "process.copy.invalid-store.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy.missing-state.json b/scripts/lite-goldens/process.copy.missing-state.json new file mode 100644 index 0000000..5cb18ee --- /dev/null +++ b/scripts/lite-goldens/process.copy.missing-state.json @@ -0,0 +1,16 @@ +{ + "process.copy.missing-state.exit": [ + "1" + ], + "process.copy.missing-state.processes": [ + "{\"args\":[\"notification\",\"show\",\"Copy failed\",\"--body\",\"HERDR_PLUGIN_STATE_DIR is not set\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy.missing-state.stderr": [ + "HERDR_PLUGIN_STATE_DIR is not set", + "" + ], + "process.copy.missing-state.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy.no-clipboard.json b/scripts/lite-goldens/process.copy.no-clipboard.json new file mode 100644 index 0000000..f73a89e --- /dev/null +++ b/scripts/lite-goldens/process.copy.no-clipboard.json @@ -0,0 +1,65 @@ +{ + "process.copy.no-clipboard.artifact": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "", + "## Annotation 2", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "wide 한글 selection and enough text to exercise clipping at the detail edge", + "```", + "", + "Comment:", + "", + "second comment", + "", + "## Annotation 3", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "````", + "first selection with ``` ticks", + "and a second line", + "````", + "", + "Comment:", + "", + "first comment", + "with two lines", + "" + ], + "process.copy.no-clipboard.exit": [ + "1" + ], + "process.copy.no-clipboard.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "{\"args\":[\"notification\",\"show\",\"Copy failed\",\"--body\",\"No supported clipboard writer is available\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy.no-clipboard.stderr": [ + "No supported clipboard writer is available", + "" + ], + "process.copy.no-clipboard.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy.populated.json b/scripts/lite-goldens/process.copy.populated.json new file mode 100644 index 0000000..6eccba4 --- /dev/null +++ b/scripts/lite-goldens/process.copy.populated.json @@ -0,0 +1,64 @@ +{ + "process.copy.populated.artifact": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "", + "## Annotation 2", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "wide 한글 selection and enough text to exercise clipping at the detail edge", + "```", + "", + "Comment:", + "", + "second comment", + "", + "## Annotation 3", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "````", + "first selection with ``` ticks", + "and a second line", + "````", + "", + "Comment:", + "", + "first comment", + "with two lines", + "" + ], + "process.copy.populated.exit": [ + "0" + ], + "process.copy.populated.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "{\"args\":[\"notification\",\"show\",\"Annotations copied\",\"--body\",\"3 annotations copied as Markdown.\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy.populated.stderr": [ + "" + ], + "process.copy.populated.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy.single.json b/scripts/lite-goldens/process.copy.single.json new file mode 100644 index 0000000..9dc238a --- /dev/null +++ b/scripts/lite-goldens/process.copy.single.json @@ -0,0 +1,36 @@ +{ + "process.copy.single.artifact": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "````", + "first selection with ``` ticks", + "and a second line", + "````", + "", + "Comment:", + "", + "first comment", + "with two lines", + "" + ], + "process.copy.single.exit": [ + "0" + ], + "process.copy.single.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "{\"args\":[\"notification\",\"show\",\"Annotations copied\",\"--body\",\"1 annotation copied as Markdown.\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy.single.stderr": [ + "" + ], + "process.copy.single.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.copy.stale-lock.json b/scripts/lite-goldens/process.copy.stale-lock.json new file mode 100644 index 0000000..24b0078 --- /dev/null +++ b/scripts/lite-goldens/process.copy.stale-lock.json @@ -0,0 +1,25 @@ +{ + "process.copy.stale-lock.artifact": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0755\",", + " \"path\": \".\"", + " }", + "]", + "" + ], + "process.copy.stale-lock.exit": [ + "0" + ], + "process.copy.stale-lock.processes": [ + "{\"args\":[\"notification\",\"show\",\"No annotations\",\"--body\",\"There is nothing to copy yet.\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.copy.stale-lock.stderr": [ + "" + ], + "process.copy.stale-lock.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.editor.invalid-pending.json b/scripts/lite-goldens/process.editor.invalid-pending.json new file mode 100644 index 0000000..8feca4d --- /dev/null +++ b/scripts/lite-goldens/process.editor.invalid-pending.json @@ -0,0 +1,15 @@ +{ + "process.editor.invalid-pending.exit": [ + "1" + ], + "process.editor.invalid-pending.processes": [ + "" + ], + "process.editor.invalid-pending.stderr": [ + "Pending annotation is invalid", + "" + ], + "process.editor.invalid-pending.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.editor.missing-pending.json b/scripts/lite-goldens/process.editor.missing-pending.json new file mode 100644 index 0000000..89b1213 --- /dev/null +++ b/scripts/lite-goldens/process.editor.missing-pending.json @@ -0,0 +1,15 @@ +{ + "process.editor.missing-pending.exit": [ + "1" + ], + "process.editor.missing-pending.processes": [ + "" + ], + "process.editor.missing-pending.stderr": [ + "Missing pending annotation", + "" + ], + "process.editor.missing-pending.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.manage.failure-without-stderr.json b/scripts/lite-goldens/process.manage.failure-without-stderr.json new file mode 100644 index 0000000..95c0098 --- /dev/null +++ b/scripts/lite-goldens/process.manage.failure-without-stderr.json @@ -0,0 +1,17 @@ +{ + "process.manage.failure-without-stderr.exit": [ + "1" + ], + "process.manage.failure-without-stderr.processes": [ + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"manager\",\"--placement\",\"popup\",\"--width\",\"100\",\"--height\",\"30\",\"--focus\"],\"command\":\"herdr-fake\"}", + "{\"args\":[\"notification\",\"show\",\"Unable to open annotations\",\"--body\",\"herdr plugin pane open --cwd <ROOT> --plugin annotate --entrypoint manager --placement popup --width 100 --height 30 --focus failed\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.manage.failure-without-stderr.stderr": [ + "herdr plugin pane open --cwd <ROOT> --plugin annotate --entrypoint manager --placement popup --width 100 --height 30 --focus failed", + "" + ], + "process.manage.failure-without-stderr.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.manage.failure.json b/scripts/lite-goldens/process.manage.failure.json new file mode 100644 index 0000000..b16cb70 --- /dev/null +++ b/scripts/lite-goldens/process.manage.failure.json @@ -0,0 +1,17 @@ +{ + "process.manage.failure.exit": [ + "1" + ], + "process.manage.failure.processes": [ + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"manager\",\"--placement\",\"popup\",\"--width\",\"100\",\"--height\",\"30\",\"--focus\"],\"command\":\"herdr-fake\"}", + "{\"args\":[\"notification\",\"show\",\"Unable to open annotations\",\"--body\",\"manager open failed\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.manage.failure.stderr": [ + "manager open failed", + "" + ], + "process.manage.failure.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.manage.missing-root.json b/scripts/lite-goldens/process.manage.missing-root.json new file mode 100644 index 0000000..4094749 --- /dev/null +++ b/scripts/lite-goldens/process.manage.missing-root.json @@ -0,0 +1,16 @@ +{ + "process.manage.missing-root.exit": [ + "1" + ], + "process.manage.missing-root.processes": [ + "{\"args\":[\"notification\",\"show\",\"Unable to open annotations\",\"--body\",\"HERDR_PLUGIN_ROOT is not set\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.manage.missing-root.stderr": [ + "HERDR_PLUGIN_ROOT is not set", + "" + ], + "process.manage.missing-root.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.manage.success.json b/scripts/lite-goldens/process.manage.success.json new file mode 100644 index 0000000..560fcbf --- /dev/null +++ b/scripts/lite-goldens/process.manage.success.json @@ -0,0 +1,15 @@ +{ + "process.manage.success.exit": [ + "0" + ], + "process.manage.success.processes": [ + "{\"args\":[\"plugin\",\"pane\",\"open\",\"--cwd\",\"<ROOT>\",\"--plugin\",\"annotate\",\"--entrypoint\",\"manager\",\"--placement\",\"popup\",\"--width\",\"100\",\"--height\",\"30\",\"--focus\"],\"command\":\"herdr-fake\"}", + "" + ], + "process.manage.success.stderr": [ + "" + ], + "process.manage.success.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/process.manager.missing-state.json b/scripts/lite-goldens/process.manager.missing-state.json new file mode 100644 index 0000000..a3ad2be --- /dev/null +++ b/scripts/lite-goldens/process.manager.missing-state.json @@ -0,0 +1,15 @@ +{ + "process.manager.missing-state.exit": [ + "1" + ], + "process.manager.missing-state.processes": [ + "" + ], + "process.manager.missing-state.stderr": [ + "HERDR_PLUGIN_STATE_DIR is not set", + "" + ], + "process.manager.missing-state.stdout": [ + "" + ] +} diff --git a/scripts/lite-goldens/screen.editor.control-c.json b/scripts/lite-goldens/screen.editor.control-c.json new file mode 100644 index 0000000..2914131 --- /dev/null +++ b/scripts/lite-goldens/screen.editor.control-c.json @@ -0,0 +1,62 @@ +{ + "screen.editor.control-c.exit": [ + "0" + ], + "screen.editor.control-c.osc52": [ + "[]" + ], + "screen.editor.control-c.processes": [ + "" + ], + "screen.editor.control-c.screen-count": [ + "2" + ], + "screen.editor.control-c.screen.control-c": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.editor.control-c.screen.initial": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ] +} diff --git a/scripts/lite-goldens/screen.editor.edit-save.json b/scripts/lite-goldens/screen.editor.edit-save.json new file mode 100644 index 0000000..39f25ca --- /dev/null +++ b/scripts/lite-goldens/screen.editor.edit-save.json @@ -0,0 +1,342 @@ +{ + "screen.editor.edit-save.exit": [ + "0" + ], + "screen.editor.edit-save.osc52": [ + "[]" + ], + "screen.editor.edit-save.processes": [ + "" + ], + "screen.editor.edit-save.screen-count": [ + "13" + ], + "screen.editor.edit-save.screen.backspace": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " bt ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.chars": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.chars-second-line": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " beta ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.delete": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " bta ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.down": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " bt ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.end": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " bta ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.enter": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.home": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " beta ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.initial": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.left": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " bt ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.right": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " beta ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.screen.save": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " bt ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Saved. " + ], + "screen.editor.edit-save.screen.up": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha 한·글· é ", + " bt ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.edit-save.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"selected wide 한글 text\\\\nwith a second line\\\",\\\"context\\\":{\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_label\\\":\\\"server\\\"},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\",\\\"id\\\":\\\"<UUID>\\\",\\\"comment\\\":\\\"alpha 한글 é\\\\nbt\\\",\\\"createdAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/screen.editor.empty-save-escape.json b/scripts/lite-goldens/screen.editor.empty-save-escape.json new file mode 100644 index 0000000..f734a66 --- /dev/null +++ b/scripts/lite-goldens/screen.editor.empty-save-escape.json @@ -0,0 +1,86 @@ +{ + "screen.editor.empty-save-escape.exit": [ + "0" + ], + "screen.editor.empty-save-escape.osc52": [ + "[]" + ], + "screen.editor.empty-save-escape.processes": [ + "" + ], + "screen.editor.empty-save-escape.screen-count": [ + "3" + ], + "screen.editor.empty-save-escape.screen.empty-save": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Write a comment before saving. " + ], + "screen.editor.empty-save-escape.screen.escape": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.editor.empty-save-escape.screen.initial": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ] +} diff --git a/scripts/lite-goldens/screen.editor.missing-state.json b/scripts/lite-goldens/screen.editor.missing-state.json new file mode 100644 index 0000000..a98f3ed --- /dev/null +++ b/scripts/lite-goldens/screen.editor.missing-state.json @@ -0,0 +1,110 @@ +{ + "screen.editor.missing-state.exit": [ + "0" + ], + "screen.editor.missing-state.osc52": [ + "[]" + ], + "screen.editor.missing-state.processes": [ + "" + ], + "screen.editor.missing-state.screen-count": [ + "4" + ], + "screen.editor.missing-state.screen.char": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " x ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.missing-state.screen.escape": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.editor.missing-state.screen.initial": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.missing-state.screen.save": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " x ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Plugin state directory is unavailable. " + ] +} diff --git a/scripts/lite-goldens/screen.editor.pending-file-save.json b/scripts/lite-goldens/screen.editor.pending-file-save.json new file mode 100644 index 0000000..0916ef5 --- /dev/null +++ b/scripts/lite-goldens/screen.editor.pending-file-save.json @@ -0,0 +1,102 @@ +{ + "screen.editor.pending-file-save.exit": [ + "0" + ], + "screen.editor.pending-file-save.osc52": [ + "[]" + ], + "screen.editor.pending-file-save.processes": [ + "" + ], + "screen.editor.pending-file-save.screen-count": [ + "3" + ], + "screen.editor.pending-file-save.screen.chars": [ + " ", + " Selected text ", + " selection from pending file ", + " ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " pending comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.pending-file-save.screen.initial": [ + " ", + " Selected text ", + " selection from pending file ", + " ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.pending-file-save.screen.save": [ + " ", + " Selected text ", + " selection from pending file ", + " ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " pending comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Saved. " + ], + "screen.editor.pending-file-save.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"selection from pending file\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_label\\\":\\\"pending workspace\\\",\\\"tab_label\\\":\\\"pending tab\\\"},\\\"id\\\":\\\"<UUID>\\\",\\\"comment\\\":\\\"pending comment\\\",\\\"createdAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/screen.editor.sigterm.json b/scripts/lite-goldens/screen.editor.sigterm.json new file mode 100644 index 0000000..2e763bb --- /dev/null +++ b/scripts/lite-goldens/screen.editor.sigterm.json @@ -0,0 +1,62 @@ +{ + "screen.editor.sigterm.exit": [ + "0" + ], + "screen.editor.sigterm.osc52": [ + "[]" + ], + "screen.editor.sigterm.processes": [ + "" + ], + "screen.editor.sigterm.screen-count": [ + "2" + ], + "screen.editor.sigterm.screen.initial": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.sigterm.screen.sigterm": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ] +} diff --git a/scripts/lite-goldens/screen.editor.word-line-kills.json b/scripts/lite-goldens/screen.editor.word-line-kills.json new file mode 100644 index 0000000..4c645f0 --- /dev/null +++ b/scripts/lite-goldens/screen.editor.word-line-kills.json @@ -0,0 +1,462 @@ +{ + "screen.editor.word-line-kills.exit": [ + "0" + ], + "screen.editor.word-line-kills.osc52": [ + "[]" + ], + "screen.editor.word-line-kills.processes": [ + "" + ], + "screen.editor.word-line-kills.screen-count": [ + "18" + ], + "screen.editor.word-line-kills.screen.alt-backspace-marker": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.alt-backspace-newline": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gammaY ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.alt-backspace-word": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.chars": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.chars-second-line": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta epsilon ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.ctrl-a-line-start": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gammaZY ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.ctrl-u": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.ctrl-u-at-line-start": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gammaZY ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.ctrl-w": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.enter": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.initial": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.mark-tail": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " tail alpha beta gammaZY ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.mark-w": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta W ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.mark-x": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· X ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.mark-y": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " Y ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.mark-z": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gammaZY ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.screen.save": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " tail alpha beta gammaZY ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Saved. " + ], + "screen.editor.word-line-kills.screen.super-left-to-line-start": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " Y ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-kills.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"selected wide 한글 text\\\\nwith a second line\\\",\\\"context\\\":{\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_label\\\":\\\"server\\\"},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\",\\\"id\\\":\\\"<UUID>\\\",\\\"comment\\\":\\\"tail alpha beta gammaZY\\\",\\\"createdAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/screen.editor.word-line-moves.json b/scripts/lite-goldens/screen.editor.word-line-moves.json new file mode 100644 index 0000000..0e9dbb3 --- /dev/null +++ b/scripts/lite-goldens/screen.editor.word-line-moves.json @@ -0,0 +1,822 @@ +{ + "screen.editor.word-line-moves.exit": [ + "0" + ], + "screen.editor.word-line-moves.osc52": [ + "[]" + ], + "screen.editor.word-line-moves.processes": [ + "" + ], + "screen.editor.word-line-moves.screen-count": [ + "33" + ], + "screen.editor.word-line-moves.screen.alt-b": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 1epsilon ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-f": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 21epsilon ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-left": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta epsilon ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-left-line-first-word": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-left-over-newline": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-left-previous-word": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-left-word": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-right": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 21epsilon3 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-right-next-word": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 한·글· delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.alt-right-over-newline": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 한·글· delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.chars": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.chars-second-line": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta epsilon ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.ctrl-a": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 9한·글·8 delta 521epsilon346A ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.ctrl-e": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " B9한·글·8 delta 521epsilon346A ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.ctrl-left": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 21epsilon34 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.ctrl-right": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 521epsilon34 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.enter": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.initial": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-1": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 1epsilon ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-2": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 21epsilon ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-3": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 21epsilon3 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-4": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 21epsilon34 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-5": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 521epsilon34 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-6": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma ", + " 한·글· delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-7": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 한·글· delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-8": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 한·글·8 delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-9": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 9한·글·8 delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-a": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 9한·글·8 delta 521epsilon346A ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-b": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " B9한·글·8 delta 521epsilon346A ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.mark-c": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " B9한·글·8 delta 521epsilon346AC ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.save": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " B9한·글·8 delta 521epsilon346AC ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Saved. " + ], + "screen.editor.word-line-moves.screen.super-left": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 한·글·8 delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.screen.super-right": [ + " ", + " Selected text ", + " selected wide 한·글· text ", + " with a second line ", + " ", + " ", + " ", + " ", + " ", + " Comment ", + " alpha beta gamma7 ", + " 9한·글·8 delta 521epsilon346 ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " Ctrl+S save · Esc cancel · Enter new line " + ], + "screen.editor.word-line-moves.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"selected wide 한글 text\\\\nwith a second line\\\",\\\"context\\\":{\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_label\\\":\\\"server\\\"},\\\"capturedAt\\\":\\\"<TIMESTAMP>\\\",\\\"id\\\":\\\"<UUID>\\\",\\\"comment\\\":\\\"alpha beta gamma7\\\\nB9한글8 delta 521epsilon346AC\\\",\\\"createdAt\\\":\\\"<TIMESTAMP>\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/screen.manager.all-views.json b/scripts/lite-goldens/screen.manager.all-views.json new file mode 100644 index 0000000..06b6579 --- /dev/null +++ b/scripts/lite-goldens/screen.manager.all-views.json @@ -0,0 +1,879 @@ +{ + "screen.manager.all-views.clipboard": [ + "" + ], + "screen.manager.all-views.exit": [ + "0" + ], + "screen.manager.all-views.osc52": [ + "[]" + ], + "screen.manager.all-views.processes": [ + "" + ], + "screen.manager.all-views.screen-count": [ + "28" + ], + "screen.manager.all-views.screen.active-arrow-down": [ + " Annotations (3) newest first ", + " third selection │ Selected text ", + " › wide 한·글· selection and enough… │ wide 한·글· selection and enough text to exercise clipping at ", + " first selection with ``` ticks… │ the detail edge ", + " │ ", + " │ ", + " │ Comment ", + " │ second comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/9/2026, 10:11:13 AM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.all-views.screen.active-arrow-up": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.all-views.screen.active-clear": [ + " Annotations (0) newest first ", + " │ ", + " No active annotations. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " All active annotations cleared. " + ], + "screen.manager.all-views.screen.active-clear-cancel": [ + " Annotations (2) newest first ", + " › wide 한·글· selection and enough… │ Selected text ", + " first selection with ``` ticks… │ wide 한·글· selection and enough text to exercise clipping at ", + " │ the detail edge ", + " │ ", + " │ ", + " │ Comment ", + " │ second comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/9/2026, 10:11:13 AM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.all-views.screen.active-clear-confirm": [ + " Annotations (2) newest first ", + " › wide 한·글· selection and enough… │ Selected text ", + " first selection with ``` ticks… │ wide 한·글· selection and enough text to exercise clipping at ", + " │ the detail edge ", + " │ ", + " │ ", + " │ Comment ", + " │ second comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/9/2026, 10:11:13 AM ", + " │ ", + " Press Shift+D again to clear all active annotations · Esc cancel " + ], + "screen.manager.all-views.screen.active-clear-confirm-again": [ + " Annotations (2) newest first ", + " › wide 한·글· selection and enough… │ Selected text ", + " first selection with ``` ticks… │ wide 한·글· selection and enough text to exercise clipping at ", + " │ the detail edge ", + " │ ", + " │ ", + " │ Comment ", + " │ second comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/9/2026, 10:11:13 AM ", + " │ ", + " Press Shift+D again to clear all active annotations · Esc cancel " + ], + "screen.manager.all-views.screen.active-delete": [ + " Annotations (2) newest first ", + " › wide 한·글· selection and enough… │ Selected text ", + " first selection with ``` ticks… │ wide 한·글· selection and enough text to exercise clipping at ", + " │ the detail edge ", + " │ ", + " │ ", + " │ Comment ", + " │ second comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/9/2026, 10:11:13 AM ", + " │ ", + " Annotation deleted. " + ], + "screen.manager.all-views.screen.active-j": [ + " Annotations (3) newest first ", + " third selection │ Selected text ", + " › wide 한·글· selection and enough… │ wide 한·글· selection and enough text to exercise clipping at ", + " first selection with ``` ticks… │ the detail edge ", + " │ ", + " │ ", + " │ Comment ", + " │ second comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/9/2026, 10:11:13 AM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.all-views.screen.active-k": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.all-views.screen.active-quit": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.manager.all-views.screen.active-reload": [ + " Annotations (0) newest first ", + " │ ", + " No active annotations. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Reloaded. " + ], + "screen.manager.all-views.screen.active-to-archives": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.active-u-ignored": [ + " Annotations (0) newest first ", + " │ ", + " No active annotations. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.all-views.screen.archives-C-ignored": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.archives-D-ignored": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.archives-arrow-down": [ + " Archives (2) newest first ", + " 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " › 8/20/2026, 1:02:03 AM · 1 anno… │ 8/20/2026, 1:02:03 AM ", + " │ ", + " │ 1 annotation ", + " │ 1. first selection with ``` ticks and a second line ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.archives-arrow-up": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.archives-c-ignored": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.archives-delete": [ + " Archives (0) newest first ", + " │ ", + " No archived sets. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Archive permanently deleted. " + ], + "screen.manager.all-views.screen.archives-delete-cancel": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.archives-delete-confirm": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Press d again to permanently delete this archive · Esc cancel " + ], + "screen.manager.all-views.screen.archives-delete-confirm-again": [ + " Archives (1) newest first ", + " › 8/20/2026, 1:02:03 AM · 1 anno… │ Archived set ", + " │ 8/20/2026, 1:02:03 AM ", + " │ ", + " │ 1 annotation ", + " │ 1. first selection with ``` ticks and a second line ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Press d again to permanently delete this archive · Esc cancel " + ], + "screen.manager.all-views.screen.archives-j": [ + " Archives (2) newest first ", + " 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " › 8/20/2026, 1:02:03 AM · 1 anno… │ 8/20/2026, 1:02:03 AM ", + " │ ", + " │ 1 annotation ", + " │ 1. first selection with ``` ticks and a second line ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.archives-k": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.all-views.screen.archives-reload": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Reloaded. " + ], + "screen.manager.all-views.screen.archives-restore": [ + " Archives (1) newest first ", + " › 8/20/2026, 1:02:03 AM · 1 anno… │ Archived set ", + " │ 8/20/2026, 1:02:03 AM ", + " │ ", + " │ 1 annotation ", + " │ 1. first selection with ``` ticks and a second line ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " 2 annotations restored. " + ], + "screen.manager.all-views.screen.archives-to-active": [ + " Annotations (2) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.all-views.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.all-views.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"}\\n{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " },", + " {", + " \"bytes\": \"\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"archives.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/screen.manager.control-c-active.json b/scripts/lite-goldens/screen.manager.control-c-active.json new file mode 100644 index 0000000..a0bb864 --- /dev/null +++ b/scripts/lite-goldens/screen.manager.control-c-active.json @@ -0,0 +1,74 @@ +{ + "screen.manager.control-c-active.exit": [ + "0" + ], + "screen.manager.control-c-active.osc52": [ + "[]" + ], + "screen.manager.control-c-active.processes": [ + "" + ], + "screen.manager.control-c-active.screen-count": [ + "2" + ], + "screen.manager.control-c-active.screen.control-c": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.manager.control-c-active.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ] +} diff --git a/scripts/lite-goldens/screen.manager.control-c-archives.json b/scripts/lite-goldens/screen.manager.control-c-archives.json new file mode 100644 index 0000000..3f37fd9 --- /dev/null +++ b/scripts/lite-goldens/screen.manager.control-c-archives.json @@ -0,0 +1,104 @@ +{ + "screen.manager.control-c-archives.exit": [ + "0" + ], + "screen.manager.control-c-archives.osc52": [ + "[]" + ], + "screen.manager.control-c-archives.processes": [ + "" + ], + "screen.manager.control-c-archives.screen-count": [ + "3" + ], + "screen.manager.control-c-archives.screen.control-c": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.manager.control-c-archives.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.control-c-archives.screen.to-archives": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ] +} diff --git a/scripts/lite-goldens/screen.manager.empty-actions.json b/scripts/lite-goldens/screen.manager.empty-actions.json new file mode 100644 index 0000000..f3c1fa5 --- /dev/null +++ b/scripts/lite-goldens/screen.manager.empty-actions.json @@ -0,0 +1,297 @@ +{ + "screen.manager.empty-actions.clipboard": [ + "" + ], + "screen.manager.empty-actions.exit": [ + "0" + ], + "screen.manager.empty-actions.osc52": [ + "[]" + ], + "screen.manager.empty-actions.processes": [ + "" + ], + "screen.manager.empty-actions.screen-count": [ + "9" + ], + "screen.manager.empty-actions.screen.active-c": [ + " Annotations (0) newest first ", + " │ ", + " No active annotations. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Nothing to copy. " + ], + "screen.manager.empty-actions.screen.active-copy-archive": [ + " Annotations (0) newest first ", + " │ ", + " No active annotations. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Nothing to copy and archive. " + ], + "screen.manager.empty-actions.screen.active-y": [ + " Annotations (0) newest first ", + " │ ", + " No active annotations. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Nothing to copy. " + ], + "screen.manager.empty-actions.screen.archives-d": [ + " Archives (0) newest first ", + " │ ", + " No archived sets. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " No archive selected. " + ], + "screen.manager.empty-actions.screen.archives-u": [ + " Archives (0) newest first ", + " │ ", + " No archived sets. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " No archive selected. " + ], + "screen.manager.empty-actions.screen.archives-y": [ + " Archives (0) newest first ", + " │ ", + " No archived sets. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " Nothing to copy. " + ], + "screen.manager.empty-actions.screen.initial": [ + " Annotations (0) newest first ", + " │ ", + " No active annotations. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.empty-actions.screen.quit": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.manager.empty-actions.screen.to-archives": [ + " Archives (0) newest first ", + " │ ", + " No archived sets. │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "screen.manager.empty-actions.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/screen.manager.escape-active.json b/scripts/lite-goldens/screen.manager.escape-active.json new file mode 100644 index 0000000..d0d6ba8 --- /dev/null +++ b/scripts/lite-goldens/screen.manager.escape-active.json @@ -0,0 +1,74 @@ +{ + "screen.manager.escape-active.exit": [ + "0" + ], + "screen.manager.escape-active.osc52": [ + "[]" + ], + "screen.manager.escape-active.processes": [ + "" + ], + "screen.manager.escape-active.screen-count": [ + "2" + ], + "screen.manager.escape-active.screen.active-escape": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.manager.escape-active.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ] +} diff --git a/scripts/lite-goldens/screen.manager.escape-exit.json b/scripts/lite-goldens/screen.manager.escape-exit.json new file mode 100644 index 0000000..8881ef1 --- /dev/null +++ b/scripts/lite-goldens/screen.manager.escape-exit.json @@ -0,0 +1,104 @@ +{ + "screen.manager.escape-exit.exit": [ + "0" + ], + "screen.manager.escape-exit.osc52": [ + "[]" + ], + "screen.manager.escape-exit.processes": [ + "" + ], + "screen.manager.escape-exit.screen-count": [ + "3" + ], + "screen.manager.escape-exit.screen.archives-escape": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.manager.escape-exit.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.escape-exit.screen.to-archives": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ] +} diff --git a/scripts/lite-goldens/screen.manager.q-archives.json b/scripts/lite-goldens/screen.manager.q-archives.json new file mode 100644 index 0000000..5094eea --- /dev/null +++ b/scripts/lite-goldens/screen.manager.q-archives.json @@ -0,0 +1,104 @@ +{ + "screen.manager.q-archives.exit": [ + "0" + ], + "screen.manager.q-archives.osc52": [ + "[]" + ], + "screen.manager.q-archives.processes": [ + "" + ], + "screen.manager.q-archives.screen-count": [ + "3" + ], + "screen.manager.q-archives.screen.archives-q": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "screen.manager.q-archives.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.q-archives.screen.to-archives": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ] +} diff --git a/scripts/lite-goldens/screen.manager.sighup.json b/scripts/lite-goldens/screen.manager.sighup.json new file mode 100644 index 0000000..7ad315c --- /dev/null +++ b/scripts/lite-goldens/screen.manager.sighup.json @@ -0,0 +1,74 @@ +{ + "screen.manager.sighup.exit": [ + "0" + ], + "screen.manager.sighup.osc52": [ + "[]" + ], + "screen.manager.sighup.processes": [ + "" + ], + "screen.manager.sighup.screen-count": [ + "2" + ], + "screen.manager.sighup.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "screen.manager.sighup.screen.sighup": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ] +} diff --git a/scripts/lite-goldens/store.cross-read.json b/scripts/lite-goldens/store.cross-read.json new file mode 100644 index 0000000..818c3d5 --- /dev/null +++ b/scripts/lite-goldens/store.cross-read.json @@ -0,0 +1,33 @@ +{ + "store.cross-read.exit": [ + "0" + ], + "store.cross-read.markdown": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "selected wide 한글 text", + "with a second line", + "```", + "", + "Comment:", + "", + "alpha 한글 é", + "bt", + "" + ], + "store.cross-read.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "{\"args\":[\"notification\",\"show\",\"Annotations copied\",\"--body\",\"1 annotation copied as Markdown.\"],\"command\":\"herdr-fake\"}", + "" + ], + "store.cross-read.stderr": [ + "" + ] +} diff --git a/scripts/lite-goldens/store.manager.active-c-success.json b/scripts/lite-goldens/store.manager.active-c-success.json new file mode 100644 index 0000000..6f50912 --- /dev/null +++ b/scripts/lite-goldens/store.manager.active-c-success.json @@ -0,0 +1,147 @@ +{ + "store.manager.active-c-success.clipboard": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "", + "## Annotation 2", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "wide 한글 selection and enough text to exercise clipping at the detail edge", + "```", + "", + "Comment:", + "", + "second comment", + "", + "## Annotation 3", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "````", + "first selection with ``` ticks", + "and a second line", + "````", + "", + "Comment:", + "", + "first comment", + "with two lines", + "" + ], + "store.manager.active-c-success.exit": [ + "0" + ], + "store.manager.active-c-success.osc52": [ + "[", + " \"\\u001b]52;c;IyBBbm5vdGF0ZWQgY29udGV4dAoKIyMgQW5ub3RhdGlvbiAxCgpTb3VyY2U6IGFwaSDtlZwgLyBzZXJ2ZXIKClNlbGVjdGVkIHRleHQ6CgpgYGAKdGhpcmQgc2VsZWN0aW9uCmBgYAoKQ29tbWVudDoKCnRoaXJkIGNvbW1lbnQKCiMjIEFubm90YXRpb24gMgoKU291cmNlOiBhcGkg7ZWcIC8gc2VydmVyCgpTZWxlY3RlZCB0ZXh0OgoKYGBgCndpZGUg7ZWc6riAIHNlbGVjdGlvbiBhbmQgZW5vdWdoIHRleHQgdG8gZXhlcmNpc2UgY2xpcHBpbmcgYXQgdGhlIGRldGFpbCBlZGdlCmBgYAoKQ29tbWVudDoKCnNlY29uZCBjb21tZW50CgojIyBBbm5vdGF0aW9uIDMKClNvdXJjZTogYXBpIO2VnCAvIHNlcnZlcgoKU2VsZWN0ZWQgdGV4dDoKCmBgYGAKZmlyc3Qgc2VsZWN0aW9uIHdpdGggYGBgIHRpY2tzCmFuZCBhIHNlY29uZCBsaW5lCmBgYGAKCkNvbW1lbnQ6CgpmaXJzdCBjb21tZW50CndpdGggdHdvIGxpbmVzCg==\\u0007\"", + "]" + ], + "store.manager.active-c-success.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "" + ], + "store.manager.active-c-success.screen-count": [ + "2" + ], + "store.manager.active-c-success.screen.copy-all": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "store.manager.active-c-success.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "store.manager.active-c-success.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}\\n{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"}\\n{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " },", + " {", + " \"bytes\": \"{\\\"version\\\":1,\\\"id\\\":\\\"archive-old\\\",\\\"archivedAt\\\":\\\"2026-08-20T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"archive-new\\\",\\\"archivedAt\\\":\\\"2026-08-21T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"archives.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/store.manager.active-y-success.json b/scripts/lite-goldens/store.manager.active-y-success.json new file mode 100644 index 0000000..1fc56d1 --- /dev/null +++ b/scripts/lite-goldens/store.manager.active-y-success.json @@ -0,0 +1,117 @@ +{ + "store.manager.active-y-success.clipboard": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "" + ], + "store.manager.active-y-success.exit": [ + "0" + ], + "store.manager.active-y-success.osc52": [ + "[", + " \"\\u001b]52;c;IyBBbm5vdGF0ZWQgY29udGV4dAoKIyMgQW5ub3RhdGlvbiAxCgpTb3VyY2U6IGFwaSDtlZwgLyBzZXJ2ZXIKClNlbGVjdGVkIHRleHQ6CgpgYGAKdGhpcmQgc2VsZWN0aW9uCmBgYAoKQ29tbWVudDoKCnRoaXJkIGNvbW1lbnQK\\u0007\"", + "]" + ], + "store.manager.active-y-success.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "" + ], + "store.manager.active-y-success.screen-count": [ + "2" + ], + "store.manager.active-y-success.screen.copy-one": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "store.manager.active-y-success.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "store.manager.active-y-success.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}\\n{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"}\\n{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " },", + " {", + " \"bytes\": \"{\\\"version\\\":1,\\\"id\\\":\\\"archive-old\\\",\\\"archivedAt\\\":\\\"2026-08-20T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"archive-new\\\",\\\"archivedAt\\\":\\\"2026-08-21T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"archives.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/store.manager.archives-y-success.json b/scripts/lite-goldens/store.manager.archives-y-success.json new file mode 100644 index 0000000..a52d6e0 --- /dev/null +++ b/scripts/lite-goldens/store.manager.archives-y-success.json @@ -0,0 +1,161 @@ +{ + "store.manager.archives-y-success.clipboard": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "", + "## Annotation 2", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "wide 한글 selection and enough text to exercise clipping at the detail edge", + "```", + "", + "Comment:", + "", + "second comment", + "" + ], + "store.manager.archives-y-success.exit": [ + "0" + ], + "store.manager.archives-y-success.osc52": [ + "[", + " \"\\u001b]52;c;IyBBbm5vdGF0ZWQgY29udGV4dAoKIyMgQW5ub3RhdGlvbiAxCgpTb3VyY2U6IGFwaSDtlZwgLyBzZXJ2ZXIKClNlbGVjdGVkIHRleHQ6CgpgYGAKdGhpcmQgc2VsZWN0aW9uCmBgYAoKQ29tbWVudDoKCnRoaXJkIGNvbW1lbnQKCiMjIEFubm90YXRpb24gMgoKU291cmNlOiBhcGkg7ZWcIC8gc2VydmVyCgpTZWxlY3RlZCB0ZXh0OgoKYGBgCndpZGUg7ZWc6riAIHNlbGVjdGlvbiBhbmQgZW5vdWdoIHRleHQgdG8gZXhlcmNpc2UgY2xpcHBpbmcgYXQgdGhlIGRldGFpbCBlZGdlCmBgYAoKQ29tbWVudDoKCnNlY29uZCBjb21tZW50Cg==\\u0007\"", + "]" + ], + "store.manager.archives-y-success.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "" + ], + "store.manager.archives-y-success.screen-count": [ + "3" + ], + "store.manager.archives-y-success.screen.copy-archive": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "store.manager.archives-y-success.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "store.manager.archives-y-success.screen.to-archives": [ + " Archives (2) newest first ", + " › 8/21/2026, 1:02:03 AM · 2 anno… │ Archived set ", + " 8/20/2026, 1:02:03 AM · 1 anno… │ 8/21/2026, 1:02:03 AM ", + " │ ", + " │ 2 annotations ", + " │ 1. third selection ", + " │ 2. wide 한·글· selection and enough text to exercise clippin… ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " j/k · y copy · u restore · d twice delete · Tab active · q " + ], + "store.manager.archives-y-success.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}\\n{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"}\\n{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " },", + " {", + " \"bytes\": \"{\\\"version\\\":1,\\\"id\\\":\\\"archive-old\\\",\\\"archivedAt\\\":\\\"2026-08-20T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"archive-new\\\",\\\"archivedAt\\\":\\\"2026-08-21T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"archives.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/store.manager.copy-archive.json b/scripts/lite-goldens/store.manager.copy-archive.json new file mode 100644 index 0000000..aba6700 --- /dev/null +++ b/scripts/lite-goldens/store.manager.copy-archive.json @@ -0,0 +1,147 @@ +{ + "store.manager.copy-archive.clipboard": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "", + "## Annotation 2", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "wide 한글 selection and enough text to exercise clipping at the detail edge", + "```", + "", + "Comment:", + "", + "second comment", + "", + "## Annotation 3", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "````", + "first selection with ``` ticks", + "and a second line", + "````", + "", + "Comment:", + "", + "first comment", + "with two lines", + "" + ], + "store.manager.copy-archive.exit": [ + "0" + ], + "store.manager.copy-archive.osc52": [ + "[", + " \"\\u001b]52;c;IyBBbm5vdGF0ZWQgY29udGV4dAoKIyMgQW5ub3RhdGlvbiAxCgpTb3VyY2U6IGFwaSDtlZwgLyBzZXJ2ZXIKClNlbGVjdGVkIHRleHQ6CgpgYGAKdGhpcmQgc2VsZWN0aW9uCmBgYAoKQ29tbWVudDoKCnRoaXJkIGNvbW1lbnQKCiMjIEFubm90YXRpb24gMgoKU291cmNlOiBhcGkg7ZWcIC8gc2VydmVyCgpTZWxlY3RlZCB0ZXh0OgoKYGBgCndpZGUg7ZWc6riAIHNlbGVjdGlvbiBhbmQgZW5vdWdoIHRleHQgdG8gZXhlcmNpc2UgY2xpcHBpbmcgYXQgdGhlIGRldGFpbCBlZGdlCmBgYAoKQ29tbWVudDoKCnNlY29uZCBjb21tZW50CgojIyBBbm5vdGF0aW9uIDMKClNvdXJjZTogYXBpIO2VnCAvIHNlcnZlcgoKU2VsZWN0ZWQgdGV4dDoKCmBgYGAKZmlyc3Qgc2VsZWN0aW9uIHdpdGggYGBgIHRpY2tzCmFuZCBhIHNlY29uZCBsaW5lCmBgYGAKCkNvbW1lbnQ6CgpmaXJzdCBjb21tZW50CndpdGggdHdvIGxpbmVzCg==\\u0007\"", + "]" + ], + "store.manager.copy-archive.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "" + ], + "store.manager.copy-archive.screen-count": [ + "2" + ], + "store.manager.copy-archive.screen.copy-archive": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "store.manager.copy-archive.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "store.manager.copy-archive.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " },", + " {", + " \"bytes\": \"{\\\"version\\\":1,\\\"id\\\":\\\"archive-old\\\",\\\"archivedAt\\\":\\\"2026-08-20T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"archive-new\\\",\\\"archivedAt\\\":\\\"2026-08-21T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"<UUID>\\\",\\\"archivedAt\\\":\\\"<TIMESTAMP>\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"},{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"archives.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/store.manager.osc52-remote-copy-all.json b/scripts/lite-goldens/store.manager.osc52-remote-copy-all.json new file mode 100644 index 0000000..4f32f07 --- /dev/null +++ b/scripts/lite-goldens/store.manager.osc52-remote-copy-all.json @@ -0,0 +1,147 @@ +{ + "store.manager.osc52-remote-copy-all.clipboard": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "", + "## Annotation 2", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "wide 한글 selection and enough text to exercise clipping at the detail edge", + "```", + "", + "Comment:", + "", + "second comment", + "", + "## Annotation 3", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "````", + "first selection with ``` ticks", + "and a second line", + "````", + "", + "Comment:", + "", + "first comment", + "with two lines", + "" + ], + "store.manager.osc52-remote-copy-all.exit": [ + "0" + ], + "store.manager.osc52-remote-copy-all.osc52": [ + "[", + " \"\\u001b]52;c;IyBBbm5vdGF0ZWQgY29udGV4dAoKIyMgQW5ub3RhdGlvbiAxCgpTb3VyY2U6IGFwaSDtlZwgLyBzZXJ2ZXIKClNlbGVjdGVkIHRleHQ6CgpgYGAKdGhpcmQgc2VsZWN0aW9uCmBgYAoKQ29tbWVudDoKCnRoaXJkIGNvbW1lbnQKCiMjIEFubm90YXRpb24gMgoKU291cmNlOiBhcGkg7ZWcIC8gc2VydmVyCgpTZWxlY3RlZCB0ZXh0OgoKYGBgCndpZGUg7ZWc6riAIHNlbGVjdGlvbiBhbmQgZW5vdWdoIHRleHQgdG8gZXhlcmNpc2UgY2xpcHBpbmcgYXQgdGhlIGRldGFpbCBlZGdlCmBgYAoKQ29tbWVudDoKCnNlY29uZCBjb21tZW50CgojIyBBbm5vdGF0aW9uIDMKClNvdXJjZTogYXBpIO2VnCAvIHNlcnZlcgoKU2VsZWN0ZWQgdGV4dDoKCmBgYGAKZmlyc3Qgc2VsZWN0aW9uIHdpdGggYGBgIHRpY2tzCmFuZCBhIHNlY29uZCBsaW5lCmBgYGAKCkNvbW1lbnQ6CgpmaXJzdCBjb21tZW50CndpdGggdHdvIGxpbmVzCg==\\u0007\"", + "]" + ], + "store.manager.osc52-remote-copy-all.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "" + ], + "store.manager.osc52-remote-copy-all.screen-count": [ + "2" + ], + "store.manager.osc52-remote-copy-all.screen.copy-all": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "store.manager.osc52-remote-copy-all.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "store.manager.osc52-remote-copy-all.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}\\n{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"}\\n{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " },", + " {", + " \"bytes\": \"{\\\"version\\\":1,\\\"id\\\":\\\"archive-old\\\",\\\"archivedAt\\\":\\\"2026-08-20T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"archive-new\\\",\\\"archivedAt\\\":\\\"2026-08-21T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"archives.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/store.manager.osc52-remote-copy-archive.json b/scripts/lite-goldens/store.manager.osc52-remote-copy-archive.json new file mode 100644 index 0000000..356b347 --- /dev/null +++ b/scripts/lite-goldens/store.manager.osc52-remote-copy-archive.json @@ -0,0 +1,147 @@ +{ + "store.manager.osc52-remote-copy-archive.clipboard": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "", + "## Annotation 2", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "wide 한글 selection and enough text to exercise clipping at the detail edge", + "```", + "", + "Comment:", + "", + "second comment", + "", + "## Annotation 3", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "````", + "first selection with ``` ticks", + "and a second line", + "````", + "", + "Comment:", + "", + "first comment", + "with two lines", + "" + ], + "store.manager.osc52-remote-copy-archive.exit": [ + "0" + ], + "store.manager.osc52-remote-copy-archive.osc52": [ + "[", + " \"\\u001b]52;c;IyBBbm5vdGF0ZWQgY29udGV4dAoKIyMgQW5ub3RhdGlvbiAxCgpTb3VyY2U6IGFwaSDtlZwgLyBzZXJ2ZXIKClNlbGVjdGVkIHRleHQ6CgpgYGAKdGhpcmQgc2VsZWN0aW9uCmBgYAoKQ29tbWVudDoKCnRoaXJkIGNvbW1lbnQKCiMjIEFubm90YXRpb24gMgoKU291cmNlOiBhcGkg7ZWcIC8gc2VydmVyCgpTZWxlY3RlZCB0ZXh0OgoKYGBgCndpZGUg7ZWc6riAIHNlbGVjdGlvbiBhbmQgZW5vdWdoIHRleHQgdG8gZXhlcmNpc2UgY2xpcHBpbmcgYXQgdGhlIGRldGFpbCBlZGdlCmBgYAoKQ29tbWVudDoKCnNlY29uZCBjb21tZW50CgojIyBBbm5vdGF0aW9uIDMKClNvdXJjZTogYXBpIO2VnCAvIHNlcnZlcgoKU2VsZWN0ZWQgdGV4dDoKCmBgYGAKZmlyc3Qgc2VsZWN0aW9uIHdpdGggYGBgIHRpY2tzCmFuZCBhIHNlY29uZCBsaW5lCmBgYGAKCkNvbW1lbnQ6CgpmaXJzdCBjb21tZW50CndpdGggdHdvIGxpbmVzCg==\\u0007\"", + "]" + ], + "store.manager.osc52-remote-copy-archive.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "" + ], + "store.manager.osc52-remote-copy-archive.screen-count": [ + "2" + ], + "store.manager.osc52-remote-copy-archive.screen.copy-archive": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "store.manager.osc52-remote-copy-archive.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "store.manager.osc52-remote-copy-archive.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " },", + " {", + " \"bytes\": \"{\\\"version\\\":1,\\\"id\\\":\\\"archive-old\\\",\\\"archivedAt\\\":\\\"2026-08-20T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"archive-new\\\",\\\"archivedAt\\\":\\\"2026-08-21T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"<UUID>\\\",\\\"archivedAt\\\":\\\"<TIMESTAMP>\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"},{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"archives.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/lite-goldens/store.manager.osc52-remote-copy.json b/scripts/lite-goldens/store.manager.osc52-remote-copy.json new file mode 100644 index 0000000..1b73ccf --- /dev/null +++ b/scripts/lite-goldens/store.manager.osc52-remote-copy.json @@ -0,0 +1,117 @@ +{ + "store.manager.osc52-remote-copy.clipboard": [ + "# Annotated context", + "", + "## Annotation 1", + "", + "Source: api 한 / server", + "", + "Selected text:", + "", + "```", + "third selection", + "```", + "", + "Comment:", + "", + "third comment", + "" + ], + "store.manager.osc52-remote-copy.exit": [ + "0" + ], + "store.manager.osc52-remote-copy.osc52": [ + "[", + " \"\\u001b]52;c;IyBBbm5vdGF0ZWQgY29udGV4dAoKIyMgQW5ub3RhdGlvbiAxCgpTb3VyY2U6IGFwaSDtlZwgLyBzZXJ2ZXIKClNlbGVjdGVkIHRleHQ6CgpgYGAKdGhpcmQgc2VsZWN0aW9uCmBgYAoKQ29tbWVudDoKCnRoaXJkIGNvbW1lbnQK\\u0007\"", + "]" + ], + "store.manager.osc52-remote-copy.processes": [ + "{\"args\":[],\"command\":\"clipboard-write\"}", + "" + ], + "store.manager.osc52-remote-copy.screen-count": [ + "2" + ], + "store.manager.osc52-remote-copy.screen.copy-one": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ], + "store.manager.osc52-remote-copy.screen.initial": [ + " Annotations (3) newest first ", + " › third selection │ Selected text ", + " wide 한·글· selection and enough… │ third selection ", + " first selection with ``` ticks… │ ", + " │ ", + " │ ", + " │ Comment ", + " │ third comment ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ ", + " │ api 한· / server · 8/10/2026, 8:21:23 PM ", + " │ ", + " j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q " + ], + "store.manager.osc52-remote-copy.state": [ + "[", + " {", + " \"kind\": \"dir\",", + " \"mode\": \"0700\",", + " \"path\": \".\"", + " },", + " {", + " \"bytes\": \"{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}\\n{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"}\\n{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"annotations.jsonl\"", + " },", + " {", + " \"bytes\": \"{\\\"version\\\":1,\\\"id\\\":\\\"archive-old\\\",\\\"archivedAt\\\":\\\"2026-08-20T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"first selection with ``` ticks\\\\nand a second line\\\",\\\"capturedAt\\\":\\\"2026-08-08T00:00:00.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-one\\\",\\\"comment\\\":\\\"first comment\\\\nwith two lines\\\",\\\"createdAt\\\":\\\"2026-08-08T00:00:01.000Z\\\"}]}\\n{\\\"version\\\":1,\\\"id\\\":\\\"archive-new\\\",\\\"archivedAt\\\":\\\"2026-08-21T01:02:03.000Z\\\",\\\"annotations\\\":[{\\\"selectedText\\\":\\\"wide 한글 selection and enough text to exercise clipping at the detail edge\\\",\\\"capturedAt\\\":\\\"2026-08-09T10:11:12.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-two\\\",\\\"comment\\\":\\\"second comment\\\",\\\"createdAt\\\":\\\"2026-08-09T10:11:13.000Z\\\"},{\\\"selectedText\\\":\\\"third selection\\\",\\\"capturedAt\\\":\\\"2026-08-10T20:21:22.000Z\\\",\\\"context\\\":{\\\"workspace_id\\\":\\\"workspace-1\\\",\\\"workspace_label\\\":\\\"api 한\\\",\\\"tab_id\\\":\\\"tab-1\\\",\\\"tab_label\\\":\\\"server\\\",\\\"focused_pane_id\\\":\\\"pane-1\\\",\\\"focused_pane_cwd\\\":\\\"/workspace\\\",\\\"focused_pane_agent\\\":\\\"codex\\\"},\\\"id\\\":\\\"ann-three\\\",\\\"comment\\\":\\\"third comment\\\",\\\"createdAt\\\":\\\"2026-08-10T20:21:23.000Z\\\"}]}\\n\",", + " \"kind\": \"file\",", + " \"mode\": \"0600\",", + " \"path\": \"archives.jsonl\"", + " }", + "]", + "" + ] +} diff --git a/scripts/parity-lite.py b/scripts/lite-regression.py similarity index 60% rename from scripts/parity-lite.py rename to scripts/lite-regression.py index a074512..87ecb77 100755 --- a/scripts/parity-lite.py +++ b/scripts/lite-regression.py @@ -1,5 +1,10 @@ #!/usr/bin/env python3 -"""Deterministic differential harness for Herdr Annotate Lite.""" +"""Deterministic golden regression harness for Herdr Annotate Lite. + +The goldens under scripts/lite-goldens were recorded from the retired Bun runtime, so a green run +is the standing proof that the native runtime still behaves the way the Bun one did. --record +rewrites them from the current run and is only for a deliberate behavior change. +""" from __future__ import annotations @@ -43,18 +48,38 @@ UUID_PATTERN = re.compile( r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b" ) -TYPESCRIPT_SCRIPTS = { - "capture": "capture.ts", - "copy-context": "export.ts", - "copy-archive": "export-archive.ts", - "manage": "open-manager.ts", - "editor": "editor.ts", - "manager": "manager.ts", -} +# The six entrypoints the Lite manifest declares, and the whole surface this harness drives. +ENTRYPOINTS = ("capture", "copy-archive", "copy-context", "editor", "manage", "manager") +# The Full manifest at the repository root starts the binary in its own bin/; the Lite variant +# in lite/ shares that one staged binary, the way it shared the Bun sources. NATIVE_PROGRAM = "./bin/herdr-annotate.exe" +LITE_NATIVE_PROGRAM = "../bin/herdr-annotate.exe" PENDING_PATTERN = re.compile(r"pending-\d+-\d+\.json") TEMP_PATTERN = re.compile(r"\.(annotations|archives)-\d+-\d+\.tmp") -DELIBERATE_DIVERGENCES = ("manager timestamp locale outside en-US",) +HANDOFF_PATTERN = re.compile(r"herdr-annotate-\d+") +DELIBERATE_DIFFERENCES = ("manager timestamp locale outside en-US",) + +# One clipboard operation walks its platform's candidate list until an adapter works. The +# goldens record the role only, so they hold on every platform, and the chain itself is checked +# against this table instead. +CLIPBOARD_CANDIDATES = { + "darwin": { + "read": (("pbpaste", ()),), + "write": (("pbcopy", ()),), + }, + "linux": { + "read": ( + ("wl-paste", ("--no-newline",)), + ("xclip", ("-selection", "clipboard", "-out")), + ("xsel", ("--clipboard", "--output")), + ), + "write": ( + ("wl-copy", ()), + ("xclip", ("-selection", "clipboard", "-in")), + ("xsel", ("--clipboard", "--input")), + ), + }, +} WIDE_RANGES = ( (0x1100, 0x115F), @@ -103,6 +128,10 @@ class Step: data: bytes = b"" coverage: tuple[str, ...] = () process_signal: int | None = None + # The keystroke ends the process (a manager copy closes the pane). The screen is captured + # only after the exit has been observed and the output drained, so a slow runner cannot + # snapshot the frame before the terminal is restored. + exits: bool = False @dataclass @@ -112,51 +141,114 @@ class PtyResult: osc52: list[str] -class Proof: - def __init__(self, artifacts: Path) -> None: +class Goldens: + """Every recorded expectation, and the checks the harness states in its own source.""" + + def __init__(self, directory: Path, artifacts: Path, *, record: bool) -> None: + self.directory = directory self.artifacts = artifacts - self.observables = 0 + self.record = record + self.groups: dict[str, dict[str, list[str]]] = {} + self.seen: dict[str, set[str]] = {} + self.checked = 0 self.screens = 0 self.failures: list[str] = [] self.coverage: set[str] = set() @staticmethod - def _display(value: object) -> list[str]: + def _render(value: object, *, screen: bool = False) -> list[str]: + if screen: + return ["".join(cell or "·" for cell in row) for row in value] if isinstance(value, bytes): - return value.decode("utf-8", "backslashreplace").splitlines(keepends=True) - if isinstance(value, tuple) and value and isinstance(value[0], tuple): - return ["".join(cell or "·" for cell in row).rstrip() + "\n" for row in value] - return (json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n").splitlines( - keepends=True - ) - - def compare(self, name: str, typescript: object, rust: object, *, screen: bool = False) -> None: - self.observables += 1 + return value.decode("utf-8", "backslashreplace").split("\n") + if isinstance(value, str): + return value.split("\n") + return json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True).split("\n") + + def _group(self, group: str) -> dict[str, list[str]]: + if group not in self.groups: + path = self.directory / f"{group}.json" + recorded: dict[str, list[str]] = {} + if not self.record and path.exists(): + recorded = json.loads(path.read_text(encoding="utf-8")) + self.groups[group] = recorded + self.seen[group] = set() + return self.groups[group] + + def check(self, group: str, name: str, value: object, *, screen: bool = False) -> None: + """Compare one observable with its recording.""" + self.checked += 1 if screen: self.screens += 1 - if typescript == rust: + rendered = self._render(value, screen=screen) + recorded = self._group(group) + self.seen[group].add(name) + if self.record: + recorded[name] = rendered return - self.failures.append(name) - diff = "".join( + if name not in recorded: + self.fail(name, f"no golden is recorded in {group}.json\n") + return + if recorded[name] != rendered: + self.fail(name, self._diff(name, recorded[name], rendered, "golden", "actual")) + + def equal(self, name: str, expected: object, actual: object) -> None: + """Check one expectation this harness states itself, which is never recorded.""" + self.checked += 1 + if expected == actual: + return + self.fail( + name, + self._diff(name, self._render(expected), self._render(actual), "expected", "actual"), + ) + + @staticmethod + def _diff(name: str, left: list[str], right: list[str], left_name: str, right_name: str) -> str: + return "".join( difflib.unified_diff( - self._display(typescript), - self._display(rust), - fromfile=f"{name}.typescript", - tofile=f"{name}.rust", + [f"{line}\n" for line in left], + [f"{line}\n" for line in right], + fromfile=f"{name}.{left_name}", + tofile=f"{name}.{right_name}", ) ) - target = self.artifacts / "divergences" / f"{safe_name(name)}.diff" + + def fail(self, name: str, detail: str) -> None: + self.failures.append(name) + target = self.artifacts / "failures" / f"{safe_name(name)}.diff" target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(diff or f"TypeScript: {typescript!r}\nRust: {rust!r}\n", encoding="utf-8") - print(f"DIVERGENCE {name}\n{diff}", file=sys.stderr) + target.write_text(detail or f"{name} differs\n", encoding="utf-8") + print(f"FAILURE {name}\n{detail}", file=sys.stderr) def require_coverage(self, required: Iterable[str]) -> None: - missing = sorted(set(required) - self.coverage) - self.compare("screen.key-coverage", [], missing) + self.equal("screen.key-coverage", [], sorted(set(required) - self.coverage)) + + def finish(self) -> None: + """Write the recording, or report goldens this run never exercised.""" + if self.record: + self.directory.mkdir(parents=True, exist_ok=True) + for group, entries in self.groups.items(): + (self.directory / f"{group}.json").write_text( + json.dumps(entries, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + for path in sorted(self.directory.glob("*.json")): + if path.stem not in self.groups: + path.unlink() + return + for group, entries in self.groups.items(): + unused = sorted(set(entries) - self.seen[group]) + if unused: + self.fail(f"{group}.unused-goldens", "\n".join(unused) + "\n") + unused_groups = sorted( + path.stem for path in self.directory.glob("*.json") if path.stem not in self.groups + ) + if unused_groups: + self.fail("goldens.unused-groups", "\n".join(unused_groups) + "\n") class TerminalGrid: - """Small ANSI terminal model for the sequences emitted by Bun and Ratatui.""" + """Small ANSI terminal model for the sequences a Ratatui pane emits.""" def __init__(self, rows: int, cols: int) -> None: self.rows = rows @@ -362,6 +454,15 @@ def send(self, data: bytes) -> None: os.write(self.master, data) self.drain() + def wait_exit(self, timeout: float = 5.0) -> None: + deadline = time.monotonic() + timeout + while self.process.poll() is None and time.monotonic() < deadline: + self.drain(quiet=0.05, maximum=0.25) + if self.process.poll() is None: + raise RuntimeError("PTY command did not exit after an exiting step") + # Everything the process wrote on its way out, up to EOF. + self.drain(quiet=0.1, maximum=1.0) + def send_signal(self, process_signal: int) -> None: os.killpg(self.process.pid, process_signal) self.drain() @@ -487,6 +588,7 @@ def normalize_text(value: str, roots: Iterable[Path]) -> str: normalized = normalized.replace(root, "<ROOT>") normalized = PENDING_PATTERN.sub("pending-<TIME>-<PID>.json", normalized) normalized = TEMP_PATTERN.sub(r".\1-<PID>-<TIME>.tmp", normalized) + normalized = HANDOFF_PATTERN.sub("herdr-annotate-<UID>", normalized) normalized = UUID_PATTERN.sub("<UUID>", normalized) def timestamp(match: re.Match[str]) -> str: @@ -503,12 +605,30 @@ def read_process_log(path: Path, roots: Iterable[Path]) -> bytes: if not path.exists(): return b"" entries = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] + collapsed: list[dict[str, object]] = [] + for entry in entries: + # A clipboard operation is one entry however long this platform's candidate chain is. + role = entry.get("command") + if ( + collapsed + and isinstance(role, str) + and role.startswith("clipboard-") + and collapsed[-1].get("command") == role + ): + continue + collapsed.append(entry) raw = "\n".join( - json.dumps(entry, ensure_ascii=False, separators=(",", ":"), sort_keys=True) for entry in entries + json.dumps(entry, ensure_ascii=False, separators=(",", ":"), sort_keys=True) + for entry in collapsed ) return normalize_bytes((raw + ("\n" if raw else "")).encode("utf-8"), roots) +def adapter_log(process_log: Path) -> Path: + """Where the fake clipboard tools record the adapter chain each operation walked.""" + return process_log.with_suffix(".adapters.jsonl") + + def state_snapshot(path: Path, roots: Iterable[Path]) -> bytes: if not path.exists(): return b"<missing>\n" @@ -535,25 +655,37 @@ def create_fakes(directory: Path) -> Path: """#!/usr/bin/env python3 import json, os, pathlib, sys name = pathlib.Path(sys.argv[0]).name -log = pathlib.Path(os.environ["PARITY_PROCESS_LOG"]) -log.parent.mkdir(parents=True, exist_ok=True) -with log.open("a", encoding="utf-8") as output: - output.write(json.dumps({"command": name, "args": sys.argv[1:]}, ensure_ascii=False, separators=(",", ":")) + "\\n") + + +def append(variable, record): + path = os.environ.get(variable) + if not path: + return + target = pathlib.Path(path) + target.parent.mkdir(parents=True, exist_ok=True) + with target.open("a", encoding="utf-8") as output: + output.write(json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\\n") + + if name == "herdr-fake": - if os.environ.get("PARITY_HERDR_FAIL") == "1" and sys.argv[1:2] == ["plugin"]: - sys.stderr.write(os.environ.get("PARITY_HERDR_STDERR", "fake herdr failure") + "\\n") + append("LITE_PROCESS_LOG", {"command": name, "args": sys.argv[1:]}) + if os.environ.get("LITE_HERDR_FAIL") == "1" and sys.argv[1:2] == ["plugin"]: + sys.stderr.write(os.environ.get("LITE_HERDR_STDERR", "fake herdr failure") + "\\n") raise SystemExit(7) raise SystemExit(0) -reads = {"pbpaste", "wl-paste", "xclip-read", "xsel-read"} -writes = {"pbcopy", "wl-copy", "xclip-write", "xsel-write"} +reads = {"pbpaste", "wl-paste"} mode = "read" if name in reads or (name == "xclip" and "-out" in sys.argv) or (name == "xsel" and "--output" in sys.argv) else "write" +# The process log keeps the role only, so it is identical on every platform; the adapter log keeps +# the platform's real program and arguments for the chain check. +append("LITE_PROCESS_LOG", {"command": "clipboard-" + mode, "args": []}) +append("LITE_ADAPTER_LOG", {"role": mode, "adapter": name, "args": sys.argv[1:]}) data = sys.stdin.buffer.read() if mode == "write" else b"" -if mode == "write" and os.environ.get("PARITY_CLIPBOARD_OUTPUT"): - pathlib.Path(os.environ["PARITY_CLIPBOARD_OUTPUT"]).write_bytes(data) -if os.environ.get("PARITY_CLIPBOARD_FAIL") in (mode, "all"): +if mode == "write" and os.environ.get("LITE_CLIPBOARD_OUTPUT"): + pathlib.Path(os.environ["LITE_CLIPBOARD_OUTPUT"]).write_bytes(data) +if os.environ.get("LITE_CLIPBOARD_FAIL") in (mode, "all"): raise SystemExit(9) if mode == "read": - source = os.environ.get("PARITY_CLIPBOARD_INPUT") + source = os.environ.get("LITE_CLIPBOARD_INPUT") if source: sys.stdout.buffer.write(pathlib.Path(source).read_bytes()) raise SystemExit(0) @@ -567,17 +699,19 @@ def create_fakes(directory: Path) -> Path: class Harness: - def __init__(self, root: Path, rust_binary: Path, workspace: Path, proof: Proof) -> None: + def __init__(self, root: Path, binary: Path, workspace: Path, goldens: Goldens) -> None: self.root = root - self.rust_binary = rust_binary + self.binary = binary self.workspace = workspace - self.proof = proof + self.goldens = goldens self.fake_bin = create_fakes(workspace / "fake-bin") + self.chains: dict[str, set[tuple[tuple[str, tuple[str, ...]], ...]]] = { + "read": set(), + "write": set(), + } - def command(self, implementation: str, entrypoint: str) -> list[str]: - if implementation == "rust": - return [str(self.rust_binary), entrypoint] - return ["bun", str(self.root / "src" / TYPESCRIPT_SCRIPTS[entrypoint])] + def command(self, entrypoint: str) -> list[str]: + return [str(self.binary), entrypoint] def environment( self, @@ -597,24 +731,25 @@ def environment( "HERDR_BIN_PATH": str(self.fake_bin / "herdr-fake"), "HERDR_PLUGIN_CONTEXT_JSON": "{}", "XDG_RUNTIME_DIR": str(runtime), - "PARITY_PROCESS_LOG": str(process_log), - "PARITY_CLIPBOARD_INPUT": str(clipboard_input), - "PARITY_CLIPBOARD_OUTPUT": str(clipboard_output), + "LITE_PROCESS_LOG": str(process_log), + "LITE_ADAPTER_LOG": str(adapter_log(process_log)), + "LITE_CLIPBOARD_INPUT": str(clipboard_input), + "LITE_CLIPBOARD_OUTPUT": str(clipboard_output), "TZ": "UTC", "LANG": "en_US.UTF-8", "LC_ALL": "en_US.UTF-8", "TERM": "xterm-256color", } ) - for key in ("PARITY_CLIPBOARD_FAIL", "PARITY_HERDR_FAIL", "PARITY_HERDR_STDERR", "HERDR_ANNOTATE_PENDING"): + for key in ("LITE_CLIPBOARD_FAIL", "LITE_HERDR_FAIL", "LITE_HERDR_STDERR", "HERDR_ANNOTATE_PENDING"): env.pop(key, None) if extra: env.update(extra) return env - def run(self, implementation: str, entrypoint: str, env: Mapping[str, str]) -> CommandResult: + def run(self, entrypoint: str, env: Mapping[str, str]) -> CommandResult: result = subprocess.run( - self.command(implementation, entrypoint), + self.command(entrypoint), cwd=self.root, env=dict(env), stdout=subprocess.PIPE, @@ -623,50 +758,63 @@ def run(self, implementation: str, entrypoint: str, env: Mapping[str, str]) -> C ) return CommandResult(result.returncode, result.stdout, result.stderr) - def process_pair( + def record_chains(self, log: Path) -> None: + """Keep every adapter chain a case walked, for the platform chain check.""" + if not log.exists(): + return + entries = [json.loads(line) for line in log.read_text(encoding="utf-8").splitlines() if line] + chain: list[tuple[str, tuple[str, ...]]] = [] + role = "" + for entry in entries: + if chain and entry["role"] != role: + self.chains[role].add(tuple(chain)) + chain = [] + role = entry["role"] + chain.append((entry["adapter"], tuple(entry["args"]))) + if chain: + self.chains[role].add(tuple(chain)) + + def case_paths(self, name: str) -> tuple[Path, Path, Path, Path, Path]: + base = self.workspace / name + base.mkdir(parents=True) + runtime = base / "runtime" + runtime.mkdir() + clipboard_input = base / "clipboard-input" + clipboard_input.write_bytes(b"") + return ( + base / "state", + runtime, + base / "process.jsonl", + clipboard_input, + base / "clipboard-output", + ) + + def process_case( self, name: str, entrypoint: str, - setup: Callable[[str, Path, Path, Path, Path, Path], Mapping[str, str] | None], - inspect: Callable[[str, Path, Path, Path, Path, Path], object] | None = None, - ) -> tuple[Path, Path]: - case = self.workspace / name - results: dict[str, tuple[CommandResult, Path, Path, Path, Path, Path]] = {} - for implementation in ("typescript", "rust"): - base = case / implementation - state = base / "state" - runtime = base / "runtime" - log = base / "process.jsonl" - clipboard_input = base / "clipboard-input" - clipboard_output = base / "clipboard-output" - base.mkdir(parents=True) - runtime.mkdir() - clipboard_input.write_bytes(b"") - extra = setup(implementation, state, runtime, log, clipboard_input, clipboard_output) or {} - env = self.environment(state, runtime, log, clipboard_input, clipboard_output, extra) - results[implementation] = ( - self.run(implementation, entrypoint, env), - state, - runtime, - log, - clipboard_input, - clipboard_output, - ) - ts, rs = results["typescript"], results["rust"] - roots = [ts[1], ts[2], rs[1], rs[2], self.workspace] - self.proof.compare(f"{name}.exit", ts[0].exit_code, rs[0].exit_code) - self.proof.compare(f"{name}.stdout", normalize_bytes(ts[0].stdout, roots), normalize_bytes(rs[0].stdout, roots)) - self.proof.compare(f"{name}.stderr", normalize_bytes(ts[0].stderr, roots), normalize_bytes(rs[0].stderr, roots)) - self.proof.compare(f"{name}.processes", read_process_log(ts[3], roots), read_process_log(rs[3], roots)) + setup: Callable[[Path, Path, Path, Path, Path], Mapping[str, str] | None], + inspect: Callable[[Path, Path, Path, Path, Path], object] | None = None, + ) -> Path: + state, runtime, log, clipboard_input, clipboard_output = self.case_paths(name) + extra = setup(state, runtime, log, clipboard_input, clipboard_output) or {} + env = self.environment(state, runtime, log, clipboard_input, clipboard_output, extra) + result = self.run(entrypoint, env) + self.record_chains(adapter_log(log)) + roots = [state, runtime, self.workspace, self.root] + self.goldens.check(name, f"{name}.exit", result.exit_code) + self.goldens.check(name, f"{name}.stdout", normalize_bytes(result.stdout, roots)) + self.goldens.check(name, f"{name}.stderr", normalize_bytes(result.stderr, roots)) + self.goldens.check(name, f"{name}.processes", read_process_log(log, roots)) if inspect: - self.proof.compare( + self.goldens.check( + name, f"{name}.artifact", - inspect("typescript", *ts[1:]), - inspect("rust", *rs[1:]), + inspect(state, runtime, log, clipboard_input, clipboard_output), ) - return ts[1], rs[1] + return state - def pty_pair( + def pty_case( self, name: str, entrypoint: str, @@ -675,79 +823,55 @@ def pty_pair( rows: int, cols: int, seed: Callable[[Path], None], - extra: Mapping[str, str] - | Callable[[str, Path], Mapping[str, str]] - | None = None, + extra: Mapping[str, str] | Callable[[Path], Mapping[str, str]] | None = None, compare_state: bool = False, compare_clipboard: bool = False, - ) -> tuple[Path, Path]: - case = self.workspace / name - results: dict[str, tuple[PtyResult, Path, Path, Path, Path]] = {} - for implementation in ("typescript", "rust"): - base = case / implementation - state = base / "state" - runtime = base / "runtime" - log = base / "process.jsonl" - clipboard_input = base / "clipboard-input" - clipboard_output = base / "clipboard-output" - base.mkdir(parents=True) - runtime.mkdir() - clipboard_input.write_bytes(b"clipboard selection") - seed(state) - case_extra = extra(implementation, state) if callable(extra) else extra - env = self.environment( - state, runtime, log, clipboard_input, clipboard_output, case_extra - ) - session = PtySession(self.command(implementation, entrypoint), env, self.root, rows, cols) - session.wait_for(marker) - screens = [("initial", session.grid.snapshot())] - for step in steps: - self.proof.coverage.update(step.coverage) - if step.process_signal is None: - session.send(step.data) - else: - session.send_signal(step.process_signal) - screens.append((step.label, session.grid.snapshot())) - code = session.finish() - results[implementation] = ( - PtyResult(screens, code, osc52_sequences(bytes(session.raw))), - state, - runtime, - log, - clipboard_output, - ) - ts, rs = results["typescript"], results["rust"] - roots = [ts[1], ts[2], rs[1], rs[2], self.workspace] - self.proof.compare(f"{name}.exit", ts[0].exit_code, rs[0].exit_code) - self.proof.compare(f"{name}.screen-count", len(ts[0].screens), len(rs[0].screens)) - for (ts_label, ts_screen), (rs_label, rs_screen) in zip(ts[0].screens, rs[0].screens): - self.proof.compare(f"{name}.screen.{ts_label}.label", ts_label, rs_label) - self.proof.compare(f"{name}.screen.{ts_label}", ts_screen, rs_screen, screen=True) - self.proof.compare(f"{name}.processes", read_process_log(ts[3], roots), read_process_log(rs[3], roots)) - self.proof.compare(f"{name}.osc52", ts[0].osc52, rs[0].osc52) + ) -> Path: + state, runtime, log, clipboard_input, clipboard_output = self.case_paths(name) + clipboard_input.write_bytes(b"clipboard selection") + seed(state) + case_extra = extra(state) if callable(extra) else extra + env = self.environment(state, runtime, log, clipboard_input, clipboard_output, case_extra) + session = PtySession(self.command(entrypoint), env, self.root, rows, cols) + session.wait_for(marker) + screens = [("initial", session.grid.snapshot())] + for step in steps: + self.goldens.coverage.update(step.coverage) + if step.process_signal is None: + session.send(step.data) + if step.exits: + session.wait_exit() + else: + session.send_signal(step.process_signal) + screens.append((step.label, session.grid.snapshot())) + exit_code = session.finish() + osc52 = osc52_sequences(bytes(session.raw)) + self.record_chains(adapter_log(log)) + roots = [state, runtime, self.workspace, self.root] + self.goldens.check(name, f"{name}.exit", exit_code) + self.goldens.check(name, f"{name}.screen-count", len(screens)) + for label, screen in screens: + self.goldens.check(name, f"{name}.screen.{label}", screen, screen=True) + self.goldens.check(name, f"{name}.processes", read_process_log(log, roots)) + self.goldens.check(name, f"{name}.osc52", osc52) if compare_clipboard: - ts_clipboard = normalize_bytes(ts[4].read_bytes() if ts[4].exists() else b"", roots) - rs_clipboard = normalize_bytes(rs[4].read_bytes() if rs[4].exists() else b"", roots) - self.proof.compare(f"{name}.clipboard", ts_clipboard, rs_clipboard) + clipboard = normalize_bytes( + clipboard_output.read_bytes() if clipboard_output.exists() else b"", roots + ) + self.goldens.check(name, f"{name}.clipboard", clipboard) # Every pane copy reaches the viewing client too: the payload the terminal received is # byte-for-byte the text the native writer was handed. - def readable(value: bytes) -> str: - return value.decode("utf-8", "backslashreplace") - - self.proof.compare( + self.goldens.equal( f"{name}.osc52-payload", - [readable(ts_clipboard), readable(rs_clipboard)], - [ - readable(normalize_bytes(osc52_payload(ts[0].osc52), roots)), - readable(normalize_bytes(osc52_payload(rs[0].osc52), roots)), - ], + clipboard, + normalize_bytes(osc52_payload(osc52), roots), ) if compare_state: - self.proof.compare(f"{name}.state", state_snapshot(ts[1], roots), state_snapshot(rs[1], roots)) - return ts[1], rs[1] + self.goldens.check(name, f"{name}.state", state_snapshot(state, roots)) + return state -def pending_artifact(_implementation: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> object: +def pending_artifact(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> object: del runtime, log, source, sink files = sorted(state.glob("pending-*.json")) if len(files) != 1: @@ -760,27 +884,24 @@ def pending_artifact(_implementation: str, state: Path, runtime: Path, log: Path } -def pending_and_runtime_artifact( - implementation: str, - state: Path, +def pending_and_runtime_artifact(state: Path, runtime: Path, log: Path, source: Path, sink: Path, ) -> object: return { - "pending": pending_artifact(implementation, state, runtime, log, source, sink), + "pending": pending_artifact(state, runtime, log, source, sink), "runtime": state_snapshot(runtime, [runtime]).decode("utf-8"), } -def clipboard_artifact(_implementation: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> object: +def clipboard_artifact(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> object: del state, runtime, log, source return sink.read_bytes() if sink.exists() else b"" -def clipboard_and_state_artifact( - _implementation: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path +def clipboard_and_state_artifact(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> object: del runtime, log, source return { @@ -789,7 +910,7 @@ def clipboard_and_state_artifact( } -def no_pending_artifact(_implementation: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> object: +def no_pending_artifact(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> object: del runtime, log, source, sink return sorted(path.name for path in state.glob("pending-*.json")) if state.exists() else [] @@ -814,7 +935,7 @@ def run_process_layer(harness: Harness) -> None: def handoff_path(runtime: Path) -> Path: return runtime / f"herdr-annotate-{os.getuid() if hasattr(os, 'getuid') else 'user'}" / "selection" - def capture_context(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: + def capture_context(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: del log, sink state.mkdir() handoff = handoff_path(runtime) @@ -823,11 +944,11 @@ def capture_context(_impl: str, state: Path, runtime: Path, log: Path, source: P source.write_text("lower-priority clipboard", encoding="utf-8") return {"HERDR_PLUGIN_CONTEXT_JSON": context} - harness.process_pair( + harness.process_case( "process.capture.context", "capture", capture_context, pending_and_runtime_artifact ) - def capture_handoff(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def capture_handoff(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del log, sink state.mkdir() handoff = handoff_path(runtime) @@ -835,12 +956,11 @@ def capture_handoff(_impl: str, state: Path, runtime: Path, log: Path, source: P handoff.write_text("handoff selection\n", encoding="utf-8") source.write_text("lower-priority clipboard", encoding="utf-8") - harness.process_pair( + harness.process_case( "process.capture.handoff", "capture", capture_handoff, pending_and_runtime_artifact ) - def capture_stale_handoff( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def capture_stale_handoff(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> None: del log, sink state.mkdir() @@ -851,15 +971,14 @@ def capture_stale_handoff( os.utime(handoff, (stale, stale)) source.write_text("clipboard after stale handoff", encoding="utf-8") - harness.process_pair( + harness.process_case( "process.capture.stale-handoff", "capture", capture_stale_handoff, pending_and_runtime_artifact, ) - def capture_blank_handoff( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def capture_blank_handoff(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> None: del log, sink state.mkdir() @@ -868,15 +987,14 @@ def capture_blank_handoff( handoff.write_text(" \n\t", encoding="utf-8") source.write_text("clipboard after blank handoff", encoding="utf-8") - harness.process_pair( + harness.process_case( "process.capture.blank-handoff", "capture", capture_blank_handoff, pending_and_runtime_artifact, ) - def capture_invalid_utf8_handoff( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def capture_invalid_utf8_handoff(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> None: del log, sink state.mkdir() @@ -885,234 +1003,226 @@ def capture_invalid_utf8_handoff( handoff.write_bytes(b"invalid-\xff-handoff") source.write_text("clipboard after invalid handoff", encoding="utf-8") - harness.process_pair( + harness.process_case( "process.capture.invalid-utf8-handoff", "capture", capture_invalid_utf8_handoff, pending_and_runtime_artifact, ) - def capture_clipboard(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def capture_clipboard(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del runtime, log, sink state.mkdir() source.write_bytes("clipboard 한 selection".encode("utf-8")) - harness.process_pair("process.capture.clipboard", "capture", capture_clipboard, pending_artifact) + harness.process_case("process.capture.clipboard", "capture", capture_clipboard, pending_artifact) - def capture_invalid_context( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def capture_invalid_context(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> Mapping[str, str]: del runtime, log, sink state.mkdir() source.write_text("clipboard after invalid context", encoding="utf-8") return {"HERDR_PLUGIN_CONTEXT_JSON": "{broken"} - harness.process_pair( + harness.process_case( "process.capture.invalid-context", "capture", capture_invalid_context, pending_artifact ) - def capture_empty(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def capture_empty(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del runtime, log, sink state.mkdir() source.write_bytes(b" \n\t") - harness.process_pair("process.capture.empty", "capture", capture_empty, no_pending_artifact) + harness.process_case("process.capture.empty", "capture", capture_empty, no_pending_artifact) - def capture_no_clipboard(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: + def capture_no_clipboard(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: del runtime, log, source, sink state.mkdir() - return {"PARITY_CLIPBOARD_FAIL": "read"} + return {"LITE_CLIPBOARD_FAIL": "read"} - harness.process_pair("process.capture.no-clipboard", "capture", capture_no_clipboard, no_pending_artifact) + harness.process_case("process.capture.no-clipboard", "capture", capture_no_clipboard, no_pending_artifact) - def capture_open_failure(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: + def capture_open_failure(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: del runtime, log, source, sink state.mkdir() return { "HERDR_PLUGIN_CONTEXT_JSON": context, - "PARITY_HERDR_FAIL": "1", - "PARITY_HERDR_STDERR": "pane open failed", + "LITE_HERDR_FAIL": "1", + "LITE_HERDR_STDERR": "pane open failed", } - harness.process_pair("process.capture.open-failure", "capture", capture_open_failure, no_pending_artifact) + harness.process_case("process.capture.open-failure", "capture", capture_open_failure, no_pending_artifact) - def capture_missing_state( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def capture_missing_state(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> Mapping[str, str]: del state, runtime, log, source, sink return {"HERDR_PLUGIN_STATE_DIR": "", "HERDR_PLUGIN_CONTEXT_JSON": context} - harness.process_pair("process.capture.missing-state", "capture", capture_missing_state) + harness.process_case("process.capture.missing-state", "capture", capture_missing_state) - def capture_missing_root( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def capture_missing_root(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> Mapping[str, str]: del runtime, log, source, sink state.mkdir() return {"HERDR_PLUGIN_ROOT": "", "HERDR_PLUGIN_CONTEXT_JSON": context} - harness.process_pair("process.capture.missing-root", "capture", capture_missing_root) + harness.process_case("process.capture.missing-root", "capture", capture_missing_root) - def copy_empty(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def copy_empty(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del state, runtime, log, source, sink - harness.process_pair( + harness.process_case( "process.copy.empty", "copy-context", copy_empty, - lambda impl, state, runtime, log, source, sink: state_snapshot(state, [state]), + lambda state, runtime, log, source, sink: state_snapshot(state, [state]), ) - def copy_populated(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def copy_populated(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del runtime, log, source, sink seed_stores(state, archives=[]) - harness.process_pair("process.copy.populated", "copy-context", copy_populated, clipboard_artifact) + harness.process_case("process.copy.populated", "copy-context", copy_populated, clipboard_artifact) - def copy_single(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def copy_single(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del runtime, log, source, sink seed_stores(state, annotations=ANNOTATIONS[:1], archives=[]) - harness.process_pair("process.copy.single", "copy-context", copy_single, clipboard_artifact) + harness.process_case("process.copy.single", "copy-context", copy_single, clipboard_artifact) - def copy_no_clipboard(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: + def copy_no_clipboard(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: del runtime, log, source, sink seed_stores(state, archives=[]) - return {"PARITY_CLIPBOARD_FAIL": "write"} + return {"LITE_CLIPBOARD_FAIL": "write"} - harness.process_pair("process.copy.no-clipboard", "copy-context", copy_no_clipboard, clipboard_artifact) + harness.process_case("process.copy.no-clipboard", "copy-context", copy_no_clipboard, clipboard_artifact) - def copy_invalid(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def copy_invalid(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del runtime, log, source, sink state.mkdir() (state / "annotations.jsonl").write_text("{broken\n", encoding="utf-8") - harness.process_pair("process.copy.invalid-store", "copy-context", copy_invalid) + harness.process_case("process.copy.invalid-store", "copy-context", copy_invalid) - def copy_missing_state( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def copy_missing_state(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> Mapping[str, str]: del state, runtime, log, source, sink return {"HERDR_PLUGIN_STATE_DIR": ""} - harness.process_pair("process.copy.missing-state", "copy-context", copy_missing_state) + harness.process_case("process.copy.missing-state", "copy-context", copy_missing_state) - def fresh_lock(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def fresh_lock(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del runtime, log, source, sink state.mkdir() (state / ".annotations.lock").mkdir() - harness.process_pair( + harness.process_case( "process.copy.busy-lock", "copy-context", fresh_lock, - lambda impl, state, runtime, log, source, sink: state_snapshot(state, [state]), + lambda state, runtime, log, source, sink: state_snapshot(state, [state]), ) - def stale_lock(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: - fresh_lock(_impl, state, runtime, log, source, sink) + def stale_lock(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + fresh_lock(state, runtime, log, source, sink) stale = time.time() - 31 os.utime(state / ".annotations.lock", (stale, stale)) - harness.process_pair( + harness.process_case( "process.copy.stale-lock", "copy-context", stale_lock, - lambda impl, state, runtime, log, source, sink: state_snapshot(state, [state]), + lambda state, runtime, log, source, sink: state_snapshot(state, [state]), ) - def copy_archive_empty(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def copy_archive_empty(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del state, runtime, log, source, sink - harness.process_pair( + harness.process_case( "process.copy-archive.empty", "copy-archive", copy_archive_empty, clipboard_and_state_artifact, ) - def copy_archive_populated(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def copy_archive_populated(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del runtime, log, source, sink seed_stores(state) - harness.process_pair( + harness.process_case( "process.copy-archive.populated", "copy-archive", copy_archive_populated, clipboard_and_state_artifact, ) - def copy_archive_no_clipboard( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def copy_archive_no_clipboard(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> Mapping[str, str]: del runtime, log, source, sink seed_stores(state) - return {"PARITY_CLIPBOARD_FAIL": "write"} + return {"LITE_CLIPBOARD_FAIL": "write"} - harness.process_pair( + harness.process_case( "process.copy-archive.no-clipboard", "copy-archive", copy_archive_no_clipboard, clipboard_and_state_artifact, ) - def copy_archive_missing_state( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def copy_archive_missing_state(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> Mapping[str, str]: del state, runtime, log, source, sink return {"HERDR_PLUGIN_STATE_DIR": ""} - harness.process_pair("process.copy-archive.missing-state", "copy-archive", copy_archive_missing_state) + harness.process_case("process.copy-archive.missing-state", "copy-archive", copy_archive_missing_state) - def manage_success(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def manage_success(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del state, runtime, log, source, sink - harness.process_pair("process.manage.success", "manage", manage_success) + harness.process_case("process.manage.success", "manage", manage_success) - def manage_failure(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: + def manage_failure(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: del state, runtime, log, source, sink - return {"PARITY_HERDR_FAIL": "1", "PARITY_HERDR_STDERR": "manager open failed"} + return {"LITE_HERDR_FAIL": "1", "LITE_HERDR_STDERR": "manager open failed"} - harness.process_pair("process.manage.failure", "manage", manage_failure) + harness.process_case("process.manage.failure", "manage", manage_failure) - def manage_failure_without_stderr( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def manage_failure_without_stderr(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> Mapping[str, str]: del state, runtime, log, source, sink - return {"PARITY_HERDR_FAIL": "1", "PARITY_HERDR_STDERR": ""} + return {"LITE_HERDR_FAIL": "1", "LITE_HERDR_STDERR": ""} - harness.process_pair( + harness.process_case( "process.manage.failure-without-stderr", "manage", manage_failure_without_stderr ) - def manage_missing_root( - _impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path + def manage_missing_root(state: Path, runtime: Path, log: Path, source: Path, sink: Path ) -> Mapping[str, str]: del state, runtime, log, source, sink return {"HERDR_PLUGIN_ROOT": ""} - harness.process_pair("process.manage.missing-root", "manage", manage_missing_root) + harness.process_case("process.manage.missing-root", "manage", manage_missing_root) - def editor_missing(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: + def editor_missing(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> None: del runtime, log, source, sink state.mkdir() - harness.process_pair("process.editor.missing-pending", "editor", editor_missing) + harness.process_case("process.editor.missing-pending", "editor", editor_missing) - def editor_invalid(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: + def editor_invalid(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: del runtime, log, source, sink state.mkdir() pending = state / "invalid-pending.json" pending.write_text('{"selectedText":"only"}\n', encoding="utf-8") return {"HERDR_ANNOTATE_PENDING": str(pending)} - harness.process_pair("process.editor.invalid-pending", "editor", editor_invalid) + harness.process_case("process.editor.invalid-pending", "editor", editor_invalid) - def manager_missing(_impl: str, state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: + def manager_missing(state: Path, runtime: Path, log: Path, source: Path, sink: Path) -> Mapping[str, str]: del state, runtime, log, source, sink return {"HERDR_PLUGIN_STATE_DIR": ""} - harness.process_pair("process.manager.missing-state", "manager", manager_missing) + harness.process_case("process.manager.missing-state", "manager", manager_missing) EDITOR_REQUIRED = { @@ -1162,7 +1272,7 @@ def empty_manager_seed(state: Path) -> None: state.mkdir(parents=True, mode=0o700) -def pending_file_extra(_implementation: str, state: Path) -> Mapping[str, str]: +def pending_file_extra(state: Path) -> Mapping[str, str]: pending = state / "pending-input.json" pending.write_text( json.dumps( @@ -1179,7 +1289,7 @@ def pending_file_extra(_implementation: str, state: Path) -> Mapping[str, str]: return {"HERDR_ANNOTATE_PENDING": str(pending)} -def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: +def run_screen_and_store_layer(harness: Harness) -> Path: editor_context = { "HERDR_PLUGIN_CONTEXT_JSON": json.dumps( { @@ -1205,7 +1315,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: Step("down", b"\x1b[B", ("editor:down",)), Step("save", b"\x13", ("editor:ctrl-s",)), ] - editor_ts, editor_rs = harness.pty_pair( + editor_state = harness.pty_case( "screen.editor.edit-save", "editor", editor_steps, @@ -1256,7 +1366,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: Step("mark-c", b"C"), Step("save", b"\x13", ("editor:ctrl-s",)), ] - harness.pty_pair( + harness.pty_case( "screen.editor.word-line-moves", "editor", move_steps, @@ -1288,7 +1398,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: Step("mark-tail", b"tail ", ("editor:chars",)), Step("save", b"\x13", ("editor:ctrl-s",)), ] - harness.pty_pair( + harness.pty_case( "screen.editor.word-line-kills", "editor", kill_steps, @@ -1299,7 +1409,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: editor_context, compare_state=True, ) - harness.pty_pair( + harness.pty_case( "screen.editor.pending-file-save", "editor", [Step("chars", b"pending comment"), Step("save", b"\x13")], @@ -1310,7 +1420,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: pending_file_extra, compare_state=True, ) - harness.pty_pair( + harness.pty_case( "screen.editor.empty-save-escape", "editor", [ @@ -1323,7 +1433,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: editor_seed, editor_context, ) - harness.pty_pair( + harness.pty_case( "screen.editor.control-c", "editor", [Step("control-c", b"\x03", ("editor:ctrl-c",))], @@ -1333,7 +1443,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: editor_seed, editor_context, ) - harness.pty_pair( + harness.pty_case( "screen.editor.missing-state", "editor", [Step("char", b"x"), Step("save", b"\x13"), Step("escape", b"\x1b")], @@ -1343,7 +1453,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: editor_seed, {**editor_context, "HERDR_PLUGIN_STATE_DIR": ""}, ) - harness.pty_pair( + harness.pty_case( "screen.editor.sigterm", "editor", [Step("sigterm", process_signal=signal.SIGTERM)], @@ -1383,7 +1493,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: Step("archives-to-active", b"\t", ("manager:archives:Tab",)), Step("active-quit", b"q", ("manager:active:q",)), ] - harness.pty_pair( + harness.pty_case( "screen.manager.all-views", "manager", manager_steps, @@ -1394,7 +1504,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: compare_state=True, compare_clipboard=True, ) - harness.pty_pair( + harness.pty_case( "screen.manager.escape-exit", "manager", [ @@ -1406,7 +1516,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: 98, manager_seed, ) - harness.pty_pair( + harness.pty_case( "screen.manager.escape-active", "manager", [Step("active-escape", b"\x1b", ("manager:active:Esc",))], @@ -1415,7 +1525,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: 98, manager_seed, ) - harness.pty_pair( + harness.pty_case( "screen.manager.control-c-active", "manager", [Step("control-c", b"\x03", ("manager:active:Ctrl-C",))], @@ -1424,7 +1534,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: 98, manager_seed, ) - harness.pty_pair( + harness.pty_case( "screen.manager.control-c-archives", "manager", [ @@ -1436,7 +1546,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: 98, manager_seed, ) - harness.pty_pair( + harness.pty_case( "screen.manager.q-archives", "manager", [ @@ -1448,7 +1558,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: 98, manager_seed, ) - harness.pty_pair( + harness.pty_case( "screen.manager.sighup", "manager", [Step("sighup", process_signal=signal.SIGHUP)], @@ -1457,7 +1567,7 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: 98, manager_seed, ) - harness.pty_pair( + harness.pty_case( "screen.manager.empty-actions", "manager", [ @@ -1479,17 +1589,17 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: ) for key, steps in ( - ("active-y-success", [Step("copy-one", b"y", ("manager:active:y",))]), - ("active-c-success", [Step("copy-all", b"c", ("manager:active:c",))]), + ("active-y-success", [Step("copy-one", b"y", ("manager:active:y",), exits=True)]), + ("active-c-success", [Step("copy-all", b"c", ("manager:active:c",), exits=True)]), ( "archives-y-success", [ Step("to-archives", b"\t", ("manager:active:Tab",)), - Step("copy-archive", b"y", ("manager:archives:y",)), + Step("copy-archive", b"y", ("manager:archives:y",), exits=True), ], ), ): - harness.pty_pair( + harness.pty_case( f"store.manager.{key}", "manager", steps, @@ -1501,10 +1611,10 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: compare_clipboard=True, ) - harness.pty_pair( + harness.pty_case( "store.manager.copy-archive", "manager", - [Step("copy-archive", b"C", ("manager:active:C",))], + [Step("copy-archive", b"C", ("manager:active:C",), exits=True)], "Annotations (", 28, 98, @@ -1517,11 +1627,11 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: # issue #40: the native write fails, the OSC 52 sequence still reaches the viewing client, and the # copy is a success. `C` must therefore still archive and clear the active list. for key, steps in ( - ("osc52-remote-copy", [Step("copy-one", b"y", ("manager:active:y",))]), - ("osc52-remote-copy-all", [Step("copy-all", b"c", ("manager:active:c",))]), - ("osc52-remote-copy-archive", [Step("copy-archive", b"C", ("manager:active:C",))]), + ("osc52-remote-copy", [Step("copy-one", b"y", ("manager:active:y",), exits=True)]), + ("osc52-remote-copy-all", [Step("copy-all", b"c", ("manager:active:c",), exits=True)]), + ("osc52-remote-copy-archive", [Step("copy-archive", b"C", ("manager:active:C",), exits=True)]), ): - states = harness.pty_pair( + state = harness.pty_case( f"store.manager.{key}", "manager", steps, @@ -1529,99 +1639,124 @@ def run_screen_and_store_layer(harness: Harness) -> tuple[Path, Path]: 28, 98, manager_seed, - {"PARITY_CLIPBOARD_FAIL": "write"}, + {"LITE_CLIPBOARD_FAIL": "write"}, compare_state=True, compare_clipboard=True, ) if key != "osc52-remote-copy-archive": continue + # The copy succeeded on the OSC 52 path alone, so copy-and-archive must have gone on to # write its archive (a third, beside the two seeded) and clear the active list, rather than # stopping at the copy. - for implementation, state in zip(("typescript", "rust"), states): - def records(name: str, state: Path = state) -> int: - path = state / name - if not path.exists(): - return 0 - return len([line for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]) - - harness.proof.compare( - f"store.manager.{key}.archived.{implementation}", - {"archives": 3, "active": 0}, - {"archives": records("archives.jsonl"), "active": records("annotations.jsonl")}, - ) + def records(name: str, state: Path = state) -> int: + path = state / name + if not path.exists(): + return 0 + return len([line for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]) + + harness.goldens.equal( + f"store.manager.{key}.archived", + {"archives": 3, "active": 0}, + {"archives": records("archives.jsonl"), "active": records("annotations.jsonl")}, + ) - harness.proof.require_coverage(EDITOR_REQUIRED | MANAGER_REQUIRED) - return editor_ts, editor_rs + harness.goldens.require_coverage(EDITOR_REQUIRED | MANAGER_REQUIRED) + return editor_state -def cross_read(harness: Harness, typescript_state: Path, rust_state: Path) -> None: - case = harness.workspace / "store.cross-read" - outputs: dict[str, tuple[CommandResult, bytes, bytes]] = {} - for name, implementation, state in ( - ("typescript-reads-rust", "typescript", rust_state), - ("rust-reads-typescript", "rust", typescript_state), - ): - base = case / name - runtime = base / "runtime" - runtime.mkdir(parents=True) - log = base / "process.jsonl" - source = base / "clipboard-input" - source.write_bytes(b"") - sink = base / "clipboard-output" - env = harness.environment(state, runtime, log, source, sink) - result = harness.run(implementation, "copy-context", env) - outputs[name] = (result, sink.read_bytes() if sink.exists() else b"", read_process_log(log, [state, runtime])) - left = outputs["typescript-reads-rust"] - right = outputs["rust-reads-typescript"] - harness.proof.compare("store.cross-read.exit", left[0].exit_code, right[0].exit_code) - harness.proof.compare("store.cross-read.stderr", left[0].stderr, right[0].stderr) - harness.proof.compare("store.cross-read.markdown", left[1], right[1]) - harness.proof.compare("store.cross-read.processes", left[2], right[2]) - - -def verify_error_catalog(root: Path, proof: Proof) -> None: - pairs = { - "HERDR_PLUGIN_STATE_DIR is not set": ("src/capture.ts", "rust/src/cli.rs"), - "HERDR_PLUGIN_ROOT is not set": ("src/capture.ts", "rust/src/cli.rs"), - "No supported clipboard reader is available": ("src/clipboard.ts", "rust/src/clipboard.rs"), - "No supported clipboard writer is available": ("src/clipboard.ts", "rust/src/clipboard.rs"), - "Missing pending annotation": ("src/editor.ts", "rust/src/editor.rs"), - "Pending annotation is invalid": ("src/editor.ts", "rust/src/editor.rs"), - "Write a comment before saving.": ("src/editor.ts", "rust/src/editor.rs"), - "Plugin state directory is unavailable.": ("src/editor.ts", "rust/src/editor.rs"), - "Nothing to copy.": ("src/manager-copy.ts", "rust/src/manager_copy.rs"), - "Nothing to copy and archive.": ("src/archive-workflow.ts", "rust/src/archive_workflow.rs"), - "No archive selected.": ("src/manager.ts", "rust/src/manager.rs"), - "Unable to save annotation": ("src/store.ts", "rust/src/store.rs"), - "Unable to update annotations": ("src/store.ts", "rust/src/store.rs"), - "Unable to update archives": ("src/store.ts", "rust/src/store.rs"), - "Unable to read": ("src/store.ts", "rust/src/store.rs"), - "Unable to access": ("src/store.ts", "rust/src/store.rs"), - "Unable to lock": ("src/store.ts", "rust/src/store.rs"), - "are busy; try again.": ("src/store.ts", "rust/src/store.rs"), - "Copied and archived, but active annotations remain:": ( - "src/manager.ts", - "rust/src/manager.rs", - ), - "Annotations copied and archived": ("src/export-archive.ts", "rust/src/cli.rs"), - "Copy and archive failed": ("src/export-archive.ts", "rust/src/cli.rs"), - "Copy and archive incomplete": ("src/export-archive.ts", "rust/src/cli.rs"), - "copied as Markdown and archived.": ("src/export-archive.ts", "rust/src/cli.rs"), - "Annotations restored, but the archive remains:": ( - "src/manager.ts", - "rust/src/manager.rs", - ), - } - for message, (typescript_file, rust_file) in pairs.items(): - present = ( - message in (root / typescript_file).read_text(encoding="utf-8"), - message in (root / rust_file).read_text(encoding="utf-8"), +def verify_cross_read(harness: Harness, editor_state: Path) -> None: + """The runtime still reads a store the retired Bun editor wrote. + + The fixture beside the goldens is the exact `annotations.jsonl` the Bun editor produced in the + recorded `screen.editor.edit-save` run, so this stays a compatibility check after Bun is gone. + """ + fixture = harness.goldens.directory / "fixtures" / "bun-editor-annotations.jsonl" + if harness.goldens.record: + fixture.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(editor_state / "annotations.jsonl", fixture) + state, runtime, log, clipboard_input, clipboard_output = harness.case_paths("store.cross-read") + state.mkdir(mode=0o700) + shutil.copyfile(fixture, state / "annotations.jsonl") + (state / "annotations.jsonl").chmod(0o600) + result = harness.run( + "copy-context", + harness.environment(state, runtime, log, clipboard_input, clipboard_output), + ) + harness.record_chains(adapter_log(log)) + roots = [state, runtime, harness.workspace, harness.root] + group = "store.cross-read" + harness.goldens.check(group, f"{group}.exit", result.exit_code) + harness.goldens.check(group, f"{group}.stderr", normalize_bytes(result.stderr, roots)) + harness.goldens.check( + group, + f"{group}.markdown", + clipboard_output.read_bytes() if clipboard_output.exists() else b"", + ) + harness.goldens.check(group, f"{group}.processes", read_process_log(log, roots)) + + +def verify_clipboard_chains(harness: Harness) -> None: + """The chains the run walked are this platform's candidate list, in order. + + The goldens keep the clipboard role only so they hold on every platform. The adapter identity, + its arguments, and the fallback order are checked here instead, including that the run + exercised both a first-adapter success and the full chain. + """ + candidates = CLIPBOARD_CANDIDATES["darwin" if sys.platform == "darwin" else "linux"] + + def steps(chain: Sequence[tuple[str, Sequence[str]]]) -> list[list[object]]: + return [[adapter, list(arguments)] for adapter, arguments in chain] + + for role, expected in candidates.items(): + observed = sorted(harness.chains[role], key=len) + harness.goldens.equal( + f"clipboard.{role}.chains", + [steps(expected[: len(chain)]) for chain in observed], + [steps(chain) for chain in observed], ) - proof.compare( + harness.goldens.equal( + f"clipboard.{role}.lengths", + sorted({1, len(expected)}), + sorted({len(chain) for chain in observed}), + ) + + +# Every user-visible message the retired Bun runtime produced, and the native file that owns it. +ERROR_CATALOG = { + "HERDR_PLUGIN_STATE_DIR is not set": "rust/src/cli.rs", + "HERDR_PLUGIN_ROOT is not set": "rust/src/cli.rs", + "No supported clipboard reader is available": "rust/src/clipboard.rs", + "No supported clipboard writer is available": "rust/src/clipboard.rs", + "Missing pending annotation": "rust/src/editor.rs", + "Pending annotation is invalid": "rust/src/editor.rs", + "Write a comment before saving.": "rust/src/editor.rs", + "Plugin state directory is unavailable.": "rust/src/editor.rs", + "Nothing to copy.": "rust/src/manager_copy.rs", + "Nothing to copy and archive.": "rust/src/archive_workflow.rs", + "No archive selected.": "rust/src/manager.rs", + "Unable to save annotation": "rust/src/store.rs", + "Unable to update annotations": "rust/src/store.rs", + "Unable to update archives": "rust/src/store.rs", + "Unable to read": "rust/src/store.rs", + "Unable to access": "rust/src/store.rs", + "Unable to lock": "rust/src/store.rs", + "are busy; try again.": "rust/src/store.rs", + "Copied and archived, but active annotations remain:": "rust/src/manager.rs", + "Annotations copied and archived": "rust/src/cli.rs", + "Copy and archive failed": "rust/src/cli.rs", + "Copy and archive incomplete": "rust/src/cli.rs", + "copied as Markdown and archived.": "rust/src/cli.rs", + "Annotations restored, but the archive remains:": "rust/src/manager.rs", +} + + +def verify_error_catalog(root: Path, goldens: Goldens) -> None: + for message, source in ERROR_CATALOG.items(): + goldens.equal( f"errors.catalog.{safe_name(message)}", - (True, True), - present, + True, + message in (root / source).read_text(encoding="utf-8"), ) @@ -1636,99 +1771,143 @@ def manifest_entries(manifest: Mapping[str, object], table: str) -> dict[str, di } -def verify_manifests(root: Path, proof: Proof) -> None: - """Both Lite manifests must declare the same entrypoints, and the harness must drive them all.""" +LITE_UNIX_BUILD = ["bash", "../scripts/fetch-herdr-annotate.sh"] +LITE_WINDOWS_BUILD = [ + "powershell.exe", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + "../scripts/fetch-herdr-annotate.ps1", +] + + +def verify_manifests(root: Path, goldens: Goldens) -> None: + """The Lite variant and the Full plugin declare one entrypoint set, and the harness drives it. + + Lite is the same runtime reached from one directory down, so the two manifests must agree on + every declaration a user sees, and neither may gate the annotation tools to a platform. + """ def load(path: Path) -> dict[str, object]: with path.open("rb") as handle: return tomllib.load(handle) lite = load(root / "lite" / "herdr-plugin.toml") - native = load(root / "lite-rs" / "herdr-plugin.toml") full = load(root / "herdr-plugin.toml") + for field in ("id", "name", "version", "min_herdr_version", "description", "platforms"): + goldens.equal(f"manifest.{field}", full.get(field), lite.get(field)) + + goldens.equal( + "manifest.lite-build-commands", + [LITE_UNIX_BUILD, LITE_WINDOWS_BUILD], + [item.get("command") for item in lite.get("build", []) if isinstance(item, dict)], + ) + lite_actions = manifest_entries(lite, "actions") - native_actions = manifest_entries(native, "actions") lite_panes = manifest_entries(lite, "panes") - native_panes = manifest_entries(native, "panes") full_actions = manifest_entries(full, "actions") full_panes = manifest_entries(full, "panes") - proof.compare("manifest.action-ids", sorted(lite_actions), sorted(native_actions)) - proof.compare("manifest.pane-ids", sorted(lite_panes), sorted(native_panes)) - proof.compare( + goldens.equal( "manifest.harness-entrypoints", - sorted(TYPESCRIPT_SCRIPTS), + list(ENTRYPOINTS), sorted(set(lite_actions) | set(lite_panes)), ) + # Full adds the plannotator-tui review entrypoints; every Lite one must also be in Full. + goldens.equal( + "manifest.pane-ids", + sorted(lite_panes), + sorted(set(full_panes) & set(lite_panes)), + ) + goldens.equal( + "manifest.action-ids", + sorted(lite_actions), + sorted(set(full_actions) & set(lite_actions)), + ) - for table, lite_entries, native_entries, full_entries, fields in ( - ("action", lite_actions, native_actions, full_actions, ("title", "description", "contexts")), - ("pane", lite_panes, native_panes, full_panes, ("title", "placement", "width", "height")), + for table, lite_entries, full_entries, fields in ( + ("action", lite_actions, full_actions, ("title", "description", "contexts")), + ("pane", lite_panes, full_panes, ("title", "placement", "width", "height")), ): for identifier, entry in lite_entries.items(): - native_entry = native_entries.get(identifier, {}) full_entry = full_entries.get(identifier, {}) for field in fields: - proof.compare( + goldens.equal( f"manifest.{table}.{identifier}.{field}", - (entry.get(field), entry.get(field)), - (native_entry.get(field), full_entry.get(field)), + entry.get(field), + full_entry.get(field), ) - lite_command = entry.get("command", []) - proof.compare( + goldens.equal( f"manifest.{table}.{identifier}.command", - ( - [NATIVE_PROGRAM, identifier], - [part.replace("../src/", "src/") for part in lite_command], - ), - (native_entry.get("command"), full_entry.get("command")), + ([LITE_NATIVE_PROGRAM, identifier], [NATIVE_PROGRAM, identifier]), + (entry.get("command"), full_entry.get("command")), ) - proof.compare( + goldens.equal( f"manifest.{table}.{identifier}.platforms", - (None, None, None), - (entry.get("platforms"), native_entry.get("platforms"), full_entry.get("platforms")), + (None, None), + (entry.get("platforms"), full_entry.get("platforms")), ) def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(description="Check Herdr Annotate Lite against its goldens.") parser.add_argument("--root", type=Path, required=True) - parser.add_argument("--rust-binary", type=Path, required=True) - return parser.parse_args() + parser.add_argument("--binary", type=Path, required=True) + parser.add_argument( + "--goldens", + type=Path, + default=None, + help="the recorded expectations (default: scripts/lite-goldens beside this file)", + ) + parser.add_argument( + "--record", + action="store_true", + help="rewrite the goldens from this run, for a deliberate behavior change", + ) + arguments = parser.parse_args() + if arguments.goldens is None: + arguments.goldens = Path(__file__).resolve().parent / "lite-goldens" + return arguments def main() -> int: args = parse_args() os.umask(0o022) - workspace = Path(tempfile.mkdtemp(prefix="herdr-annotate-parity-")) - proof = Proof(workspace / "artifacts") + workspace = Path(tempfile.mkdtemp(prefix="herdr-annotate-lite-")) + goldens = Goldens(args.goldens.resolve(), workspace / "artifacts", record=args.record) try: - harness = Harness(args.root.resolve(), args.rust_binary.resolve(), workspace, proof) + harness = Harness(args.root.resolve(), args.binary.resolve(), workspace, goldens) print("== process layer") run_process_layer(harness) print("== screen and store layers") - typescript_state, rust_state = run_screen_and_store_layer(harness) - print("== cross-read, error catalog, and manifests") - cross_read(harness, typescript_state, rust_state) - verify_error_catalog(args.root.resolve(), proof) - verify_manifests(args.root.resolve(), proof) - if proof.failures: + editor_state = run_screen_and_store_layer(harness) + print("== cross-read, clipboard chains, error catalog, and manifests") + verify_cross_read(harness, editor_state) + verify_clipboard_chains(harness) + verify_error_catalog(args.root.resolve(), goldens) + verify_manifests(args.root.resolve(), goldens) + goldens.finish() + summary = ( + f"Lite regression: {goldens.checked} observables, {goldens.screens} screens, " + f"{len(DELIBERATE_DIFFERENCES)} deliberate difference" + ) + if goldens.failures: print( - f"Parity Lite: {proof.observables} observables compared, {proof.screens} screens diffed, " - f"{len(proof.failures)} divergences / " - f"{len(DELIBERATE_DIVERGENCES)} deliberate; artifacts: {workspace}", + f"{summary}, {len(goldens.failures)} failures; artifacts: {workspace}", file=sys.stderr, ) return 1 - print( - f"Parity Lite: {proof.observables} observables compared, {proof.screens} screens diffed, " - f"zero divergences / {len(DELIBERATE_DIVERGENCES)} deliberate" - ) + if args.record: + print(f"{summary}, recorded into {goldens.directory}") + else: + print(f"{summary}, zero failures") shutil.rmtree(workspace) return 0 - except Exception as error: # noqa: BLE001 - harness must retain evidence on any failure. - print(f"parity-lite: {error}; artifacts: {workspace}", file=sys.stderr) + except Exception as error: # noqa: BLE001 - the harness must retain evidence on any failure. + print(f"lite-regression: {error}; artifacts: {workspace}", file=sys.stderr) return 1 diff --git a/scripts/lite-regression.sh b/scripts/lite-regression.sh new file mode 100755 index 0000000..c61bd0f --- /dev/null +++ b/scripts/lite-regression.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Check the native Lite runtime against the goldens recorded from the retired Bun runtime. +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" + +for command in cargo python3; do + command -v "$command" >/dev/null 2>&1 || { + echo "lite-regression: required command is unavailable: $command" >&2 + exit 2 + } +done + +echo "== stage the native runtime" +bash "$root/scripts/stage-local.sh" >/dev/null + +exec python3 "$root/scripts/lite-regression.py" \ + --root "$root" \ + --binary "$root/bin/herdr-annotate.exe" \ + "$@" diff --git a/scripts/parity-lite.sh b/scripts/parity-lite.sh deleted file mode 100755 index 80edab1..0000000 --- a/scripts/parity-lite.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# Differential proof for the TypeScript and native Rust Lite implementations. -set -euo pipefail - -root="$(cd "$(dirname "$0")/.." && pwd)" - -for command in bun cargo python3; do - command -v "$command" >/dev/null 2>&1 || { - echo "parity-lite: required command is unavailable: $command" >&2 - exit 2 - } -done - -echo "== stage native Lite" -bash "$root/lite-rs/scripts/stage-local.sh" >/dev/null - -exec python3 "$root/scripts/parity-lite.py" \ - --root "$root" \ - --rust-binary "$root/lite-rs/bin/herdr-annotate.exe" diff --git a/scripts/smoke-rust-lite.sh b/scripts/smoke-rust-lite.sh deleted file mode 100755 index eb4aaf5..0000000 --- a/scripts/smoke-rust-lite.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# Smoke-test the native Lite build in a disposable named Herdr session. This links the -# evaluation manifest, confirms its binary-backed entrypoints, renders the manager pane, -# then restores the machine's prior `annotate` install. -# -# HERDR_SESSION=rust-lite-test bash scripts/smoke-rust-lite.sh -set -euo pipefail - -[ -n "${HERDR_SESSION:-}" ] || { echo "set HERDR_SESSION to a disposable named session" >&2; exit 2; } -[ "$HERDR_SESSION" != default ] || { echo "refusing to run against the default session" >&2; exit 2; } - -root="$(cd "$(dirname "$0")/.." && pwd)" -failures=0 - -plugin_json() { herdr plugin list --json | python3 -c ' -import json,sys -for p in json.load(sys.stdin)["result"]["plugins"]: - if p["plugin_id"] == "annotate": - print(json.dumps(p)); break'; } -field() { python3 -c "import json,sys; p=json.load(sys.stdin); print(eval(sys.argv[1]))" "$1"; } -check() { - if [ "$2" = "$3" ]; then - echo " ok $1: $2" - else - echo " FAIL $1: got '$2', want '$3'" >&2 - failures=$((failures+1)) - fi -} - -before="$(plugin_json || true)" -restore() { - echo "== restore" - herdr plugin uninstall annotate >/dev/null 2>&1 || true - if [ -z "$before" ]; then - echo " (nothing was installed)" - return - fi - kind="$(printf '%s' "$before" | field "p['source']['kind']")" - if [ "$kind" = github ]; then - owner="$(printf '%s' "$before" | field "p['source']['owner']")" - repo="$(printf '%s' "$before" | field "p['source']['repo']")" - subdir="$(printf '%s' "$before" | field "p['source'].get('subdir') or ''")" - commit="$(printf '%s' "$before" | field "p['source']['resolved_commit']")" - herdr plugin install "$owner/$repo${subdir:+/$subdir}" --ref "$commit" --yes >/dev/null - echo " restored $owner/$repo${subdir:+/$subdir}@${commit:0:7}" - else - prior_root="$(printf '%s' "$before" | field "p['plugin_root']")" - herdr plugin link "$prior_root" >/dev/null - echo " restored link $prior_root" - fi -} -trap restore EXIT - -echo "== build native Lite" -bash "$root/lite-rs/scripts/stage-local.sh" >/dev/null -binary="$root/rust/target/release/herdr-annotate" -check "binary version" "$("$binary" --version)" "herdr-annotate $(tr -d '[:space:]' < "$root/lite-rs/herdr-annotate.version")" - -echo "== link native manifest" -herdr plugin link "$root/lite-rs" >/dev/null -installed="$(plugin_json)" -check "plugin root" "$(printf '%s' "$installed" | field "p['plugin_root']")" "$root/lite-rs" -actions="$(herdr plugin action list --plugin annotate | python3 -c ' -import json,sys -print(",".join(sorted(a["action_id"] for a in json.load(sys.stdin)["result"]["actions"])))')" -check "actions" "$actions" "capture,copy-archive,copy-context,manage" -commands="$(printf '%s' "$installed" | python3 -c ' -import json,sys -p=json.load(sys.stdin) -print(",".join(sorted(a["command"][0] for a in p["actions"])))')" -check "native action commands" "$commands" "./bin/herdr-annotate.exe,./bin/herdr-annotate.exe,./bin/herdr-annotate.exe,./bin/herdr-annotate.exe" -check "bundled binary" "$("$root/lite-rs/bin/herdr-annotate.exe" --version)" "herdr-annotate $(tr -d '[:space:]' < "$root/lite-rs/herdr-annotate.version")" - -echo "== manager pane renders in $HERDR_SESSION" -herdr plugin pane open --plugin annotate --entrypoint manager --placement overlay --focus >/dev/null -pane="" -for _ in $(seq 1 20); do - pane="$(herdr pane list | python3 -c ' -import json,sys -ps=[p for p in json.load(sys.stdin)["result"]["panes"] if p.get("label") == "Annotations"] -print(ps[-1]["pane_id"] if ps else "")')" - [ -n "$pane" ] && break - sleep 0.25 -done -if [ -n "$pane" ] && herdr pane wait-output "$pane" --match "Annotations (" --timeout 8000 >/dev/null 2>&1; then - echo " ok manager pane $pane rendered" -else - echo " FAIL manager pane did not render" >&2 - [ -n "$pane" ] && herdr pane read "$pane" >&2 || true - herdr pane list >&2 || true - herdr plugin log list --plugin annotate --limit 5 >&2 || true - failures=$((failures+1)) -fi -[ -n "$pane" ] && herdr plugin pane close "$pane" >/dev/null 2>&1 || true - -echo "== result: $failures failure(s)" -[ "$failures" -eq 0 ] diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 5c3fc6f..be2acb1 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Smoke-test the plugin the way users get it: fresh install, upgrade from an old commit, -# lite, and the lite -> full swap, then exercise the review pane in a disposable Herdr -# session. Restores whatever `annotate` install was present before it ran. +# Smoke-test the plugin the way users get it: fresh install, upgrade from an old commit, lite, +# and the lite -> full swap, exercising both fetched binaries and both panes in a disposable +# Herdr session. Restores whatever `annotate` install was present before it ran. # # HERDR_SESSION=<disposable named session> bash scripts/smoke.sh [old-ref] # # Requires: herdr, python3, a running named session in HERDR_SESSION (never the default -# session; the pane test opens and closes panes in it). The plugin registry is global, so +# session; the pane tests open and close panes in it). The plugin registry is global, so # this replaces the machine's `annotate` install while it runs and puts it back at the end. set -euo pipefail @@ -22,11 +22,54 @@ for p in json.load(sys.stdin)['result']['plugins']: field() { python3 -c "import json,sys; p=json.load(sys.stdin); print(eval(sys.argv[1]))" "$1"; } actions() { herdr plugin action list --plugin annotate | python3 -c " import json,sys; print(','.join(sorted(a['action_id'] for a in json.load(sys.stdin)['result']['actions'])))"; } -bin_version() { - local root; root="$(plugin_json | field "p['plugin_root']")" - local bin="$root/bin/plannotator-tui.exe"; [ -x "$bin" ] && "$bin" --version | awk '{print $2}' || echo none +programs() { plugin_json | python3 -c " +import json,sys +print(','.join(sorted({a['command'][0] for a in json.load(sys.stdin)['actions']})))"; } +plugin_root() { plugin_json | field "p['plugin_root']"; } +# Lite installs from the lite/ subdirectory and shares the checkout's one staged bin/. +checkout_root() { + local root; root="$(plugin_root)" + case "$root" in */lite) printf '%s' "${root%/lite}" ;; *) printf '%s' "$root" ;; esac +} +tui_version() { + local bin; bin="$(plugin_root)/bin/plannotator-tui.exe" + [ -x "$bin" ] && "$bin" --version | awk '{print $2}' || echo none +} +# An old commit, and the Lite plugin root, carry only one of the two pins. +read_pin() { if [ -f "$1" ]; then tr -d '[:space:]' < "$1"; else echo none; fi; } +tui_pin() { read_pin "$(plugin_root)/plannotator-tui.version"; } +native_version() { + local bin; bin="$(checkout_root)/bin/herdr-annotate.exe" + [ -x "$bin" ] && "$bin" --version | awk '{print $2}' || echo none +} +native_pin() { read_pin "$(checkout_root)/herdr-annotate.version"; } +pane_id() { herdr pane list | python3 -c " +import json,sys +panes=[p for p in json.load(sys.stdin)['result']['panes'] if p.get('label')==sys.argv[1]] +print(panes[-1]['pane_id'] if panes else '')" "$1"; } +wait_pane() { + local pane="" + for _ in $(seq 1 20); do + pane="$(pane_id "$1")" + [ -n "$pane" ] && break + sleep 0.25 + done + printf '%s' "$pane" +} +# The manage action opens the manager as a popup, which `herdr pane list` does not report, so +# open it as an overlay with the same --cwd the action passes: the plugin root, which is what +# resolves the manifest's relative program for both variants. +manager_renders() { + local pane result=FAIL + herdr plugin pane open --plugin annotate --entrypoint manager --placement overlay \ + --cwd "$(plugin_root)" --focus >/dev/null 2>&1 || true + pane="$(wait_pane Annotations)" + if [ -n "$pane" ] && herdr pane wait-output "$pane" --match "Annotations (" --timeout 8000 >/dev/null 2>&1; then + result=ok + fi + [ -n "$pane" ] && herdr plugin pane close "$pane" >/dev/null 2>&1 || true + printf '%s' "$result" } -pin() { local root; root="$(plugin_json | field "p['plugin_root']")"; tr -d '[:space:]' < "$root/plannotator-tui.version" 2>/dev/null || echo none; } check() { if [ "$2" = "$3" ]; then echo " ok $1: $2"; else echo " FAIL $1: got '$2', want '$3'" >&2; failures=$((failures+1)); fi; } install() { herdr plugin uninstall annotate >/dev/null 2>&1 || true; herdr plugin install "$@" --yes >/dev/null; } failures=0 @@ -55,15 +98,16 @@ trap restore EXIT echo "== fresh install: full" install "$spec" check "actions" "$(actions)" "capture,copy-archive,copy-context,last,manage,open,open-link" -check "binary matches pin" "$(bin_version)" "$(pin)" +check "action programs" "$(programs)" "./bin/herdr-annotate.exe,./bin/plannotator-tui.exe" +check "review binary matches pin" "$(tui_version)" "$(tui_pin)" +check "native binary matches pin" "$(native_version)" "$(native_pin)" +check "manager pane" "$(manager_renders)" "ok" echo "== review pane opens with --cwd (the #7 regression)" work="$(mktemp -d)"; mkdir -p "$work/docs"; printf '# Smoke plan\n\nhello\n' > "$work/docs/plan.md" -before_panes="$(herdr pane list | python3 -c "import json,sys; print(len(json.load(sys.stdin)['result']['panes']))")" herdr plugin pane open --plugin annotate --entrypoint doc --placement overlay --cwd "$work" \ --env "PLANNOTATOR_TUI_FILE=$work/docs/plan.md" >/dev/null -pane="$(herdr pane list | python3 -c " -import json,sys; ps=[p for p in json.load(sys.stdin)['result']['panes'] if p.get('label')=='Annotate']; print(ps[-1]['pane_id'] if ps else '')")" +pane="$(wait_pane Annotate)" if [ -n "$pane" ] && herdr pane wait-output "$pane" --match "Smoke plan" --timeout 8000 >/dev/null 2>&1; then echo " ok pane $pane rendered the document" else @@ -74,20 +118,25 @@ rm -rf "$work" echo "== upgrade: $old_ref -> main" install "$spec" --ref "$old_ref" -old_bin="$(bin_version)"; echo " old binary $old_bin (pin $(pin))" +old_bin="$(tui_version)"; echo " old review binary $old_bin (pin $(tui_pin))" install "$spec" -check "binary replaced on upgrade" "$(bin_version)" "$(pin)" -[ "$old_bin" != "$(bin_version)" ] && echo " ok binary changed $old_bin -> $(bin_version)" || echo " note old and new pins are equal; replacement not exercised" +check "review binary replaced on upgrade" "$(tui_version)" "$(tui_pin)" +check "native binary installed on upgrade" "$(native_version)" "$(native_pin)" +[ "$old_bin" != "$(tui_version)" ] && echo " ok review binary changed $old_bin -> $(tui_version)" || echo " note old and new pins are equal; replacement not exercised" echo "== fresh install: lite" install "$spec/lite" check "actions" "$(actions)" "capture,copy-archive,copy-context,manage" -check "no binary" "$(bin_version)" "none" +check "action programs" "$(programs)" "../bin/herdr-annotate.exe" +check "no review binary" "$(tui_version)" "none" +check "native binary matches pin" "$(native_version)" "$(native_pin)" +check "manager pane" "$(manager_renders)" "ok" echo "== swap: lite -> full" install "$spec" check "actions" "$(actions)" "capture,copy-archive,copy-context,last,manage,open,open-link" -check "binary" "$(bin_version)" "$(pin)" +check "review binary" "$(tui_version)" "$(tui_pin)" +check "native binary" "$(native_version)" "$(native_pin)" echo "== result: $failures failure(s)" [ "$failures" -eq 0 ] diff --git a/scripts/stage-local.sh b/scripts/stage-local.sh new file mode 100755 index 0000000..6c572a5 --- /dev/null +++ b/scripts/stage-local.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Build and stage the native runtime for `herdr plugin link`, which intentionally skips +# manifest build hooks. Run from any directory in the checkout. +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" +cargo build --manifest-path "$root/rust/Cargo.toml" --release +HERDR_ANNOTATE_BIN="$root/rust/target/release/herdr-annotate" \ + bash "$root/scripts/fetch-herdr-annotate.sh" +echo "staged $root/bin/herdr-annotate.exe" diff --git a/scripts/test-fetch-herdr-annotate.ps1 b/scripts/test-fetch-herdr-annotate.ps1 new file mode 100644 index 0000000..58df5cc --- /dev/null +++ b/scripts/test-fetch-herdr-annotate.ps1 @@ -0,0 +1,95 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateSet("LocalOverride")] + [string]$Case +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest +if (Test-Path variable:PSNativeCommandUseErrorActionPreference) { + $PSNativeCommandUseErrorActionPreference = $false +} + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$testRoot = Join-Path $env:RUNNER_TEMP ("herdr annotate fetch " + $Case + " " + [guid]::NewGuid()) +$pluginRoot = Join-Path $testRoot "plugin root with spaces" +$pluginScripts = Join-Path $pluginRoot "scripts" +$fetcher = Join-Path $pluginScripts "fetch-herdr-annotate.ps1" +$destination = Join-Path $pluginRoot "bin/herdr-annotate.exe" +$stamp = Join-Path $pluginRoot "bin/herdr-annotate.version" +$version = (Get-Content -LiteralPath (Join-Path $repositoryRoot "herdr-annotate.version") -Raw).Trim() +$oldLocalOverride = [Environment]::GetEnvironmentVariable("HERDR_ANNOTATE_BIN", "Process") + +function Assert-True { + param([bool]$Condition, [string]$Message) + if (-not $Condition) { throw $Message } +} + +function Invoke-Fetcher { + $output = & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $fetcher *>&1 | + Out-String + [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = $output } +} + +function Assert-BytesEqual { + param([string]$Left, [string]$Right, [string]$Message) + $leftBytes = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Left)) + $rightBytes = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Right)) + Assert-True ($leftBytes -ceq $rightBytes) $Message +} + +try { + New-Item -ItemType Directory -Force $pluginScripts | Out-Null + Copy-Item -LiteralPath (Join-Path $repositoryRoot "scripts/fetch-herdr-annotate.ps1") -Destination $fetcher + Copy-Item -LiteralPath (Join-Path $repositoryRoot "herdr-annotate.version") -Destination $pluginRoot + + $sourceDirectory = Join-Path $testRoot "synthetic source with spaces" + $source = Join-Path $sourceDirectory "herdr-annotate local.exe" + New-Item -ItemType Directory -Force $sourceDirectory | Out-Null + Set-Content -LiteralPath $source -NoNewline -Value "local override bytes" + New-Item -ItemType Directory -Force (Split-Path -Parent $destination) | Out-Null + Set-Content -LiteralPath $destination -NoNewline -Value "old destination bytes" + Set-Content -LiteralPath $stamp -NoNewline -Value "old-version" + + $env:HERDR_ANNOTATE_BIN = $source + $result = Invoke-Fetcher + Assert-True ($result.ExitCode -eq 0) "local override failed: $($result.Output)" + Assert-BytesEqual $source $destination "local override bytes differ" + Assert-True ((Get-Content -LiteralPath $stamp -Raw) -ceq $version) "local stamp differs" + + $env:HERDR_ANNOTATE_BIN = $null + $beforeHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $destination).Hash + $result = Invoke-Fetcher + Assert-True ($result.ExitCode -eq 0) "idempotent run failed: $($result.Output)" + Assert-True ($result.Output -match "already installed") "idempotent run did not short-circuit: $($result.Output)" + Assert-True ( + (Get-FileHash -Algorithm SHA256 -LiteralPath $destination).Hash -ceq $beforeHash + ) "idempotent run replaced the destination" + + # A missing local build is fatal here: the annotation tools are this binary. + $env:HERDR_ANNOTATE_BIN = Join-Path $testRoot "missing explicit override.exe" + $result = Invoke-Fetcher + Assert-True ($result.ExitCode -ne 0) "missing explicit override exited successfully" + Assert-True ($result.Output -match "HERDR_ANNOTATE_BIN is not a file") "missing override error differs: $($result.Output)" + Assert-True ( + (Get-FileHash -Algorithm SHA256 -LiteralPath $destination).Hash -ceq $beforeHash + ) "missing override changed the destination" + + $env:HERDR_ANNOTATE_BIN = $null + Set-Content -LiteralPath (Join-Path $pluginRoot "herdr-annotate.version") -NoNewline -Value "" + $result = Invoke-Fetcher + Assert-True ($result.ExitCode -ne 0) "empty version pin exited successfully" + Assert-True ($result.Output -match "herdr-annotate.version is empty") "empty pin error differs: $($result.Output)" + Assert-True ( + (Get-FileHash -Algorithm SHA256 -LiteralPath $destination).Hash -ceq $beforeHash + ) "empty pin changed the destination" +} finally { + if ($null -eq $oldLocalOverride) { + $env:HERDR_ANNOTATE_BIN = $null + } else { + $env:HERDR_ANNOTATE_BIN = $oldLocalOverride + } + Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction SilentlyContinue +} + +exit 0 diff --git a/scripts/test-fetch-herdr-annotate.sh b/scripts/test-fetch-herdr-annotate.sh new file mode 100755 index 0000000..e282df1 --- /dev/null +++ b/scripts/test-fetch-herdr-annotate.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# The native Lite fetcher: local override, idempotence, and the two fatal inputs. Unlike the +# plannotator-tui fetcher, a failure here is fatal: the annotation tools are the binary. +set -euo pipefail + +repository_root="$(cd "$(dirname "$0")/.." && pwd)" +test_root="$(mktemp -d)" +trap 'rm -rf "$test_root"' EXIT +plugin_root="$test_root/plugin root with spaces" +source_root="$test_root/source binary with spaces" +mkdir -p "$plugin_root/scripts" "$plugin_root/bin" "$source_root" +cp "$repository_root/scripts/fetch-herdr-annotate.sh" "$plugin_root/scripts/" +cp "$repository_root/herdr-annotate.version" "$plugin_root/" +version="$(tr -d '[:space:]' < "$repository_root/herdr-annotate.version")" + +source_binary="$source_root/herdr-annotate local" +cat > "$source_binary" <<SOURCE +#!/usr/bin/env sh +printf 'herdr-annotate $version\n' +SOURCE +chmod +x "$source_binary" +printf 'old destination' > "$plugin_root/bin/herdr-annotate.exe" +chmod +x "$plugin_root/bin/herdr-annotate.exe" +printf 'old-version' > "$plugin_root/bin/herdr-annotate.version" + +HERDR_ANNOTATE_BIN="$source_binary" bash "$plugin_root/scripts/fetch-herdr-annotate.sh" +cmp "$source_binary" "$plugin_root/bin/herdr-annotate.exe" +test "$(cat "$plugin_root/bin/herdr-annotate.version")" = "$version" +test ! -e "$plugin_root/bin/herdr-annotate" +test "$("$plugin_root/bin/herdr-annotate.exe" --version)" = "herdr-annotate $version" + +output="$(bash "$plugin_root/scripts/fetch-herdr-annotate.sh")" +case "$output" in + *"already installed"*) ;; + *) echo "idempotent fetch did not short-circuit: $output" >&2; exit 1 ;; +esac + +before="$(cat "$plugin_root/bin/herdr-annotate.exe")" +if HERDR_ANNOTATE_BIN="$test_root/missing override" bash "$plugin_root/scripts/fetch-herdr-annotate.sh" 2>"$test_root/missing.err"; then + echo "a missing local override exited successfully" >&2; exit 1 +fi +grep -q "HERDR_ANNOTATE_BIN is not executable" "$test_root/missing.err" +test "$(cat "$plugin_root/bin/herdr-annotate.exe")" = "$before" + +printf '' > "$plugin_root/herdr-annotate.version" +if bash "$plugin_root/scripts/fetch-herdr-annotate.sh" 2>"$test_root/empty.err"; then + echo "an empty version pin exited successfully" >&2; exit 1 +fi +grep -q "herdr-annotate.version is empty" "$test_root/empty.err" +test "$(cat "$plugin_root/bin/herdr-annotate.exe")" = "$before" diff --git a/scripts/test-windows-full-manifest.py b/scripts/test-windows-full-manifest.py index 833bbff..75a2daf 100644 --- a/scripts/test-windows-full-manifest.py +++ b/scripts/test-windows-full-manifest.py @@ -9,8 +9,23 @@ PROGRAM = "./bin/plannotator-tui.exe" +NATIVE_PROGRAM = "./bin/herdr-annotate.exe" FULL_PLATFORMS = {"macos", "linux"} UNIX_BUILD = ["bash", "scripts/fetch-plannotator-tui.sh"] +NATIVE_UNIX_BUILD = ["bash", "scripts/fetch-herdr-annotate.sh"] +NATIVE_WINDOWS_BUILD = [ + "powershell.exe", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + "scripts/fetch-herdr-annotate.ps1", +] +NATIVE_ENTRIES = { + "actions": ("capture", "copy-context", "copy-archive", "manage"), + "panes": ("editor", "manager"), +} DISTRIBUTED_PANE = [ "sh", "-c", @@ -90,16 +105,41 @@ def check_top_level_windows(path: Path, manifest: dict[str, object]) -> None: fail(path, "top-level platforms must be macOS, Linux, and Windows") -def check_distributed(path: Path, version_path: Path) -> None: +def check_native_lite(path: Path, manifest: dict[str, object]) -> None: + """The Lite tools reach Windows too: one native argv, no shell, no platform gate.""" + for table, identifiers in NATIVE_ENTRIES.items(): + for identifier in identifiers: + item = entry(path, manifest, table, identifier) + command = item.get("command") + if command != [NATIVE_PROGRAM, identifier]: + fail(path, f"unexpected {table}.{identifier} argv: {command!r}") + if "platforms" in item: + fail(path, f"{table}.{identifier} is gated to {item['platforms']!r}") + if any("$" in argument for argument in command): + fail(path, f"interpolation found in {table}.{identifier}: {command!r}") + + +def check_distributed(path: Path, version_path: Path, native_version_path: Path) -> None: manifest = load(path) check_top_level_windows(path, manifest) + check_native_lite(path, manifest) if version_path.read_text(encoding="utf-8").strip() != "0.8.0": fail(version_path, "plannotator-tui.version is not 0.8.0") + if native_version_path.read_text(encoding="utf-8").strip() != "0.1.0": + fail(native_version_path, "herdr-annotate.version is not 0.1.0") build_entries = builds(path, manifest) - if len(build_entries) != 1: - fail(path, f"expected one Full build, found {len(build_entries)}") - build = build_entries[0] + if len(build_entries) != 3: + fail(path, f"expected three Full builds, found {len(build_entries)}") + native_unix, native_windows, build = build_entries + if platforms(path, manifest, native_unix) != FULL_PLATFORMS: + fail(path, f"native Unix build platforms are {platforms(path, manifest, native_unix)!r}") + if native_unix.get("command") != NATIVE_UNIX_BUILD: + fail(path, f"unexpected native Unix build argv: {native_unix.get('command')!r}") + if platforms(path, manifest, native_windows) != {"windows"}: + fail(path, "the native PowerShell build is not Windows-only") + if native_windows.get("command") != NATIVE_WINDOWS_BUILD: + fail(path, f"unexpected native Windows build argv: {native_windows.get('command')!r}") if platforms(path, manifest, build) != FULL_PLATFORMS: fail(path, f"Full build platforms are {platforms(path, manifest, build)!r}") if build.get("command") != UNIX_BUILD: @@ -166,7 +206,11 @@ def main() -> None: if len(sys.argv) > 2: raise SystemExit("usage: test-windows-full-manifest.py [development-manifest]") root = Path(__file__).resolve().parent.parent - check_distributed(root / "herdr-plugin.toml", root / "plannotator-tui.version") + check_distributed( + root / "herdr-plugin.toml", + root / "plannotator-tui.version", + root / "herdr-annotate.version", + ) if len(sys.argv) == 2: check_development(Path(sys.argv[1])) diff --git a/src/archive-workflow.ts b/src/archive-workflow.ts deleted file mode 100644 index 487f617..0000000 --- a/src/archive-workflow.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { ClipboardResult } from "./clipboard"; -import { formatAnnotations } from "./format"; -import { newestFirstAnnotations, type StoreResult } from "./store"; -import type { Annotation, ArchivedAnnotationSet } from "./types"; - -type ClipboardWriter = (text: string) => ClipboardResult<undefined>; - -interface CopyAndArchiveDependencies { - readonly loadActive: () => StoreResult<Annotation[]>; - readonly writeClipboard: ClipboardWriter; - readonly saveArchive: (archive: ArchivedAnnotationSet) => StoreResult<undefined>; - readonly removeActive: (annotationIds: readonly string[]) => StoreResult<undefined>; - readonly createArchiveId: () => string; - readonly now: () => string; -} - -interface RestoreArchiveDependencies { - readonly mergeActive: (annotations: readonly Annotation[]) => StoreResult<number>; - readonly removeArchive: (archiveId: string) => StoreResult<undefined>; -} - -/** The manager transition produced by a copy-and-archive attempt. */ -export type CopyAndArchiveOutcome = - | { readonly _tag: "close"; readonly archivedCount: number } - | { readonly _tag: "stay_open"; readonly message: string } - | { readonly _tag: "archived_active_retained"; readonly message: string }; - -/** Copy the active annotations, persist a recoverable archive, then remove those active IDs. */ -export function copyAndArchiveAnnotations( - dependencies: CopyAndArchiveDependencies, -): CopyAndArchiveOutcome { - const loaded = dependencies.loadActive(); - if (!loaded.ok) return { _tag: "stay_open", message: loaded.message }; - if (loaded.value.length === 0) { - return { _tag: "stay_open", message: "Nothing to copy and archive." }; - } - - const copied = dependencies.writeClipboard( - formatAnnotations(newestFirstAnnotations(loaded.value)), - ); - if (!copied.ok) return { _tag: "stay_open", message: copied.message }; - - const archive: ArchivedAnnotationSet = { - version: 1, - id: dependencies.createArchiveId(), - archivedAt: dependencies.now(), - annotations: loaded.value, - }; - const saved = dependencies.saveArchive(archive); - if (!saved.ok) return { _tag: "stay_open", message: saved.message }; - - const removed = dependencies.removeActive(loaded.value.map((annotation) => annotation.id)); - if (!removed.ok) { - return { _tag: "archived_active_retained", message: removed.message }; - } - - return { _tag: "close", archivedCount: loaded.value.length }; -} - -/** The manager transition produced by restoring one archived annotation set. */ -export type RestoreArchivedSetOutcome = - | { readonly _tag: "restored"; readonly restoredCount: number } - | { readonly _tag: "stay_open"; readonly message: string } - | { - readonly _tag: "restored_archive_retained"; - readonly restoredCount: number; - readonly message: string; - }; - -/** Merge an archived set into active annotations, then remove its recoverable archive record. */ -export function restoreArchivedSet( - archive: ArchivedAnnotationSet, - dependencies: RestoreArchiveDependencies, -): RestoreArchivedSetOutcome { - const merged = dependencies.mergeActive(archive.annotations); - if (!merged.ok) return { _tag: "stay_open", message: merged.message }; - - const removed = dependencies.removeArchive(archive.id); - if (!removed.ok) { - return { - _tag: "restored_archive_retained", - restoredCount: merged.value, - message: removed.message, - }; - } - - return { _tag: "restored", restoredCount: merged.value }; -} diff --git a/src/capture.ts b/src/capture.ts deleted file mode 100644 index 3e2d196..0000000 --- a/src/capture.ts +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bun -import fs from "node:fs"; -import path from "node:path"; -import { notify, runHerdr } from "./herdr"; -import { normalizeWindowsPath, pluginRoot, stateDir } from "./paths"; -import { - parseInvocationContext, - selectedTextFromInvocation, - type PendingAnnotation, -} from "./types"; - -try { - let context = parseInvocationContext(undefined); - let selectedText: string | undefined; - try { - const decoded: unknown = JSON.parse(process.env.HERDR_PLUGIN_CONTEXT_JSON ?? "{}"); - context = parseInvocationContext(decoded); - selectedText = selectedTextFromInvocation(decoded); - } catch {} - - const dir = stateDir(); - if (!dir) throw new Error("HERDR_PLUGIN_STATE_DIR is not set"); - const root = pluginRoot(); - if (!root) throw new Error("HERDR_PLUGIN_ROOT is not set"); - - if (!selectedText) { - // A program on the server (Neovim's mapping) may have handed the selection over in a - // file; that beats the clipboard, which a headless server does not have. - const { takeHandoff } = await import("./handoff"); - selectedText = takeHandoff(); - } - if (!selectedText) { - const { readClipboard } = await import("./clipboard"); - const clipboard = readClipboard(); - if (!clipboard.ok) throw new Error(clipboard.message); - selectedText = clipboard.value; - } - if (!selectedText.trim()) { - notify("Nothing to annotate", "Select text in Herdr or copy text to the clipboard."); - process.exit(0); - } - fs.mkdirSync(dir, { recursive: true }); - - const pending: PendingAnnotation = { - selectedText, - context, - capturedAt: new Date().toISOString(), - }; - const pendingPath = normalizeWindowsPath( - path.join(dir, `pending-${Date.now()}-${process.pid}.json`), - ); - fs.writeFileSync(pendingPath, `${JSON.stringify(pending)}\n`, { mode: 0o600 }); - - const opened = runHerdr([ - "plugin", - "pane", - "open", - "--cwd", - root, - "--plugin", - "annotate", - "--entrypoint", - "editor", - "--placement", - "popup", - "--width", - "88", - "--height", - "24", - "--env", - `HERDR_ANNOTATE_PENDING=${pendingPath}`, - "--focus", - ]); - if (!opened.ok) { - fs.rmSync(pendingPath, { force: true }); - throw new Error(opened.message); - } -} catch (error) { - const message = error instanceof Error ? error.message : String(error); - notify("Annotate failed", message); - console.error(message); - process.exit(1); -} diff --git a/src/clipboard.ts b/src/clipboard.ts deleted file mode 100644 index 6d4f1b9..0000000 --- a/src/clipboard.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { spawnSync } from "node:child_process"; - -interface ClipboardCommand { - command: string; - args: string[]; -} - -/** The result of an expected clipboard operation. */ -export type ClipboardResult<T> = - | { readonly ok: true; readonly value: T } - | { readonly ok: false; readonly message: string }; - -function readCommands(): ClipboardCommand[] { - if (process.platform === "darwin") return [{ command: "pbpaste", args: [] }]; - if (process.platform === "win32") { - return [ - { - command: "powershell.exe", - args: ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"], - }, - ]; - } - return [ - { command: "wl-paste", args: ["--no-newline"] }, - { command: "xclip", args: ["-selection", "clipboard", "-out"] }, - { command: "xsel", args: ["--clipboard", "--output"] }, - ]; -} - -function writeCommands(): ClipboardCommand[] { - if (process.platform === "darwin") return [{ command: "pbcopy", args: [] }]; - if (process.platform === "win32") { - return [ - { - command: "powershell.exe", - args: ["-NoProfile", "-NonInteractive", "-Command", "$input | Set-Clipboard"], - }, - ]; - } - return [ - { command: "wl-copy", args: [] }, - { command: "xclip", args: ["-selection", "clipboard", "-in"] }, - { command: "xsel", args: ["--clipboard", "--input"] }, - ]; -} - -/** Read text from the first clipboard adapter available on the current platform. */ -export function readClipboard(): ClipboardResult<string> { - for (const candidate of readCommands()) { - const result = spawnSync(candidate.command, candidate.args, { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - windowsHide: process.platform === "win32", - }); - if (result.status === 0 && typeof result.stdout === "string") { - return { ok: true, value: result.stdout }; - } - } - return { ok: false, message: "No supported clipboard reader is available" }; -} - -/** Write text through the first clipboard adapter available on the current platform. */ -export function writeClipboard(text: string): ClipboardResult<undefined> { - for (const candidate of writeCommands()) { - const result = spawnSync(candidate.command, candidate.args, { - input: text, - encoding: "utf8", - stdio: ["pipe", "ignore", "ignore"], - windowsHide: process.platform === "win32", - }); - if (result.status === 0) return { ok: true, value: undefined }; - } - return { ok: false, message: "No supported clipboard writer is available" }; -} diff --git a/src/edit-keys.ts b/src/edit-keys.ts deleted file mode 100644 index 3c1216e..0000000 --- a/src/edit-keys.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type readline from "node:readline"; - -export type EditAction = - | "word-left" - | "word-right" - | "line-start" - | "line-end" - | "delete-word" - | "delete-line" - | null; - -const isWordChar = (char: string | undefined): boolean => - char !== undefined && char !== "\n" && !/\s/.test(char); - -/** Index of the start of the word before `cursor` (readline backward-word). */ -export function wordStart(chars: readonly string[], cursor: number): number { - let index = Math.min(cursor, chars.length); - while (index > 0 && !isWordChar(chars[index - 1]) && chars[index - 1] !== "\n") index -= 1; - if (index > 0 && chars[index - 1] === "\n" && index === cursor) return index - 1; - while (index > 0 && isWordChar(chars[index - 1])) index -= 1; - return index; -} - -/** Index of the end of the word after `cursor` (readline forward-word). */ -export function wordEnd(chars: readonly string[], cursor: number): number { - let index = Math.max(0, cursor); - while (index < chars.length && !isWordChar(chars[index]) && chars[index] !== "\n") index += 1; - if (index < chars.length && chars[index] === "\n" && index === cursor) return index + 1; - while (index < chars.length && isWordChar(chars[index])) index += 1; - return index; -} - -export function lineStart(chars: readonly string[], cursor: number): number { - let index = cursor; - while (index > 0 && chars[index - 1] !== "\n") index -= 1; - return index; -} - -export function lineEnd(chars: readonly string[], cursor: number): number { - let index = cursor; - while (index < chars.length && chars[index] !== "\n") index += 1; - return index; -} - -/** - * Map a readline keypress to an editing action beyond single-character moves. - * - * Terminals encode Option/Alt as xterm modifier 3 and Command/Super as 9; Node's - * readline folds both into `key.meta`, so the raw sequence tells them apart. - * Ghostty (and most macOS terminals) rewrite Cmd+Backspace to Ctrl+U and - * Opt+Backspace to Ctrl+W, matching the readline line/word kill bindings. - */ -export function resolveEditKey(key: readline.Key): EditAction { - const sequence = key.sequence ?? ""; - const superModifier = /;9[A-Z~]$/.test(sequence); - - if (key.ctrl && key.name === "u") return "delete-line"; - if (key.ctrl && key.name === "w") return "delete-word"; - if (key.meta && key.name === "backspace") return "delete-word"; - if (key.ctrl && key.name === "a") return "line-start"; - if (key.ctrl && key.name === "e") return "line-end"; - - if (key.meta && key.name === "b") return "word-left"; - if (key.meta && key.name === "f") return "word-right"; - - if (key.name === "left" && (key.meta || key.ctrl)) return superModifier ? "line-start" : "word-left"; - if (key.name === "right" && (key.meta || key.ctrl)) return superModifier ? "line-end" : "word-right"; - - return null; -} diff --git a/src/editor.ts b/src/editor.ts deleted file mode 100644 index d05a744..0000000 --- a/src/editor.ts +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env bun -import crypto from "node:crypto"; -import fs from "node:fs"; -import readline from "node:readline"; -import { sanitizeTerminalText, wrapText } from "./format"; -import { stateDir } from "./paths"; -import { layoutComment } from "./layout"; -import { lineEnd, lineStart, resolveEditKey, wordEnd, wordStart } from "./edit-keys"; -import { charWidth, stringWidth, truncateToWidth } from "./width"; -import type { StoreResult } from "./store"; -import { - parsePendingAnnotation, - pendingAnnotationFromInvocation, - type Annotation, - type PendingAnnotation, -} from "./types"; - -const pendingPath = process.env.HERDR_ANNOTATE_PENDING; -let pending: PendingAnnotation; - -function invocationContext(): unknown { - try { - return JSON.parse(process.env.HERDR_PLUGIN_CONTEXT_JSON ?? "{}"); - } catch { - return {}; - } -} - -try { - const invocation = invocationContext(); - if (!pendingPath) { - const fallback = pendingAnnotationFromInvocation(invocation); - if (!fallback) throw new Error("Missing pending annotation"); - pending = fallback; - } else { - const decoded: unknown = JSON.parse(fs.readFileSync(pendingPath, "utf8")); - const parsed = parsePendingAnnotation(decoded); - if (!parsed) throw new Error("Pending annotation is invalid"); - pending = parsed; - fs.rmSync(pendingPath, { force: true }); - } -} catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); -} - -const out = (value: string) => process.stdout.write(value); -const comment: string[] = []; -let cursor = 0; -let status = ""; -let finished = false; - -function moveCursorVertical(delta: number): void { - const before = comment.slice(0, cursor).join(""); - const lines = before.split("\n"); - const row = lines.length - 1; - const col = stringWidth(lines[row] ?? ""); - const allLines = comment.join("").split("\n"); - const targetRow = Math.max(0, Math.min(allLines.length - 1, row + delta)); - let next = 0; - for (let index = 0; index < targetRow; index += 1) next += Array.from(allLines[index] as string).length + 1; - // Land on the character whose cell column is closest to the current one. - let offset = 0; - let used = 0; - for (const char of allLines[targetRow] as string) { - const width = charWidth(char); - if (used + width > col) break; - used += width; - offset += 1; - } - cursor = next + offset; -} - -function writeAt(row: number, col: number, text: string): void { - out(`\x1b[${row};${col}H${text}`); -} - -function render(): void { - const cols = Math.max(20, process.stdout.columns || 86); - const rows = Math.max(10, process.stdout.rows || 22); - const left = 3; - const innerWidth = Math.max(1, cols - 4); - const selectionRows = Math.max(3, Math.min(7, Math.floor((rows - 6) / 2))); - const editorRows = Math.max(1, rows - selectionRows - 5); - const wrappedSelection = wrapText(sanitizeTerminalText(pending.selectedText), innerWidth); - const selected = wrappedSelection.slice(0, selectionRows); - const editing = layoutComment(comment, cursor, innerWidth); - const editorStart = Math.max(0, editing.cursorRow - editorRows + 1); - const visibleEditor = editing.lines.slice(editorStart, editorStart + editorRows); - - out("\x1b[2J\x1b[H\x1b[?25l"); - writeAt(2, left, "\x1b[1mSelected text\x1b[0m"); - selected.forEach((line, index) => writeAt(3 + index, left, `\x1b[2m${line}\x1b[0m`)); - if (wrappedSelection.length > selectionRows) { - writeAt(2 + selectionRows, left + innerWidth - 1, "\x1b[2m…\x1b[0m"); - } - - const commentTitleRow = 3 + selectionRows; - writeAt(commentTitleRow, left, "\x1b[1mComment\x1b[0m"); - visibleEditor.forEach((line, index) => writeAt(commentTitleRow + 1 + index, left, line)); - - const footer = status || "Ctrl+S save · Esc cancel · Enter new line"; - writeAt(rows, left, `\x1b[2m${truncateToWidth(footer, innerWidth)}\x1b[0m`); - - const visualCursorRow = editing.cursorRow - editorStart; - if (visualCursorRow >= 0 && visualCursorRow < editorRows) { - writeAt(commentTitleRow + 1 + visualCursorRow, left + editing.cursorCol, "\x1b[?25h"); - } -} - -function cleanup(): void { - if (finished) return; - finished = true; - if (process.stdin.isTTY) process.stdin.setRawMode(false); - out("\x1b[?25h\x1b[2J\x1b[H\x1b[?1049l"); -} - -function exit(code: number): void { - cleanup(); - process.exit(code); -} - -async function save(): Promise<void> { - const value = comment.join("").trim(); - if (!value) { - status = "Write a comment before saving."; - render(); - return; - } - const dir = stateDir(); - if (!dir) { - status = "Plugin state directory is unavailable."; - render(); - return; - } - const annotation: Annotation = { - ...pending, - id: crypto.randomUUID(), - comment: value, - createdAt: new Date().toISOString(), - }; - let saved: StoreResult<undefined>; - try { - const { appendAnnotation } = await import("./store"); - saved = appendAnnotation(dir, annotation); - } catch { - status = "Unable to save annotation."; - render(); - return; - } - if (!saved.ok) { - status = saved.message; - render(); - return; - } - status = "Saved."; - render(); - setTimeout(() => exit(0), 250); -} - -process.on("exit", cleanup); -process.on("SIGTERM", () => exit(0)); -try { - process.on("SIGHUP", () => exit(0)); -} catch { - // SIGHUP is not available on every supported platform. -} -process.stdout.on("resize", render); - -readline.emitKeypressEvents(process.stdin, { escapeCodeTimeout: 20 } as any); -if (process.stdin.isTTY) process.stdin.setRawMode(true); -process.stdin.resume(); -process.stdin.on("keypress", (text: string, key: readline.Key) => { - status = ""; - if (key.ctrl && key.name === "c") return exit(0); - if (key.ctrl && key.name === "s") { - void save(); - return; - } - if (key.name === "escape") return exit(0); - const action = resolveEditKey(key); - if (action === "word-left") { - cursor = wordStart(comment, cursor); - } else if (action === "word-right") { - cursor = wordEnd(comment, cursor); - } else if (action === "line-start") { - cursor = lineStart(comment, cursor); - } else if (action === "line-end") { - cursor = lineEnd(comment, cursor); - } else if (action === "delete-word") { - const start = wordStart(comment, cursor); - comment.splice(start, cursor - start); - cursor = start; - } else if (action === "delete-line") { - const start = lineStart(comment, cursor); - comment.splice(start, cursor - start); - cursor = start; - } else if (key.name === "backspace") { - if (cursor > 0) comment.splice(--cursor, 1); - } else if (key.name === "delete") { - if (cursor < comment.length) comment.splice(cursor, 1); - } else if (key.name === "left") { - cursor = Math.max(0, cursor - 1); - } else if (key.name === "right") { - cursor = Math.min(comment.length, cursor + 1); - } else if (key.name === "up") { - moveCursorVertical(-1); - } else if (key.name === "down") { - moveCursorVertical(1); - } else if (key.name === "home") { - while (cursor > 0 && comment[cursor - 1] !== "\n") cursor -= 1; - } else if (key.name === "end") { - while (cursor < comment.length && comment[cursor] !== "\n") cursor += 1; - } else if (key.name === "return") { - comment.splice(cursor, 0, "\n"); - cursor += 1; - } else if (text && !key.ctrl && !key.meta) { - const inserted = Array.from(text); - comment.splice(cursor, 0, ...inserted); - cursor += inserted.length; - } - render(); -}); - -out("\x1b[?1049h"); -render(); diff --git a/src/export-archive.ts b/src/export-archive.ts deleted file mode 100644 index 37d4a2c..0000000 --- a/src/export-archive.ts +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bun -import crypto from "node:crypto"; -import { copyAndArchiveAnnotations, type CopyAndArchiveOutcome } from "./archive-workflow"; -import { writeClipboard } from "./clipboard"; -import { notify } from "./herdr"; -import { appendArchivedSet, loadAnnotations, removeAnnotationsById } from "./store"; - -/** The notification and exit status one copy-and-archive action reports. */ -export interface CopyArchiveReport { - readonly title: string; - readonly body: string; - readonly failure: boolean; -} - -/** - * Map one copy-and-archive outcome to the action's notification and exit status. - * - * `loadedEmpty` separates the nothing-to-do case from a real failure: both are `stay_open`, - * but an empty store is reported like `export.ts` and exits successfully. - */ -export function copyArchiveReport( - outcome: CopyAndArchiveOutcome, - loadedEmpty: boolean, -): CopyArchiveReport { - if (outcome._tag === "close") { - return { - title: "Annotations copied and archived", - body: `${outcome.archivedCount} annotation${outcome.archivedCount === 1 ? "" : "s"} copied as Markdown and archived.`, - failure: false, - }; - } - if (outcome._tag === "archived_active_retained") { - return { - title: "Copy and archive incomplete", - body: `Copied and archived, but active annotations remain: ${outcome.message}`, - failure: true, - }; - } - if (loadedEmpty) { - return { title: "No annotations", body: "There is nothing to copy yet.", failure: false }; - } - return { title: "Copy and archive failed", body: outcome.message, failure: true }; -} - -function main(): void { - const dir = process.env.HERDR_PLUGIN_STATE_DIR; - if (!dir) { - const message = "HERDR_PLUGIN_STATE_DIR is not set"; - notify("Copy and archive failed", message); - console.error(message); - process.exit(1); - } - - let loadedEmpty = false; - const outcome = copyAndArchiveAnnotations({ - loadActive: () => { - const loaded = loadAnnotations(dir); - if (loaded.ok && loaded.value.length === 0) loadedEmpty = true; - return loaded; - }, - writeClipboard, - saveArchive: (archive) => appendArchivedSet(dir, archive), - removeActive: (annotationIds) => removeAnnotationsById(dir, annotationIds), - createArchiveId: () => crypto.randomUUID(), - now: () => new Date().toISOString(), - }); - - const report = copyArchiveReport(outcome, loadedEmpty); - notify(report.title, report.body); - if (report.failure) { - console.error(report.body); - process.exit(1); - } -} - -if (import.meta.main) main(); diff --git a/src/export.ts b/src/export.ts deleted file mode 100644 index a4d6c1f..0000000 --- a/src/export.ts +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bun -import { writeClipboard } from "./clipboard"; -import { formatAnnotations } from "./format"; -import { notify } from "./herdr"; -import { loadAnnotations, newestFirstAnnotations } from "./store"; - -try { - const dir = process.env.HERDR_PLUGIN_STATE_DIR; - if (!dir) throw new Error("HERDR_PLUGIN_STATE_DIR is not set"); - const loaded = loadAnnotations(dir); - if (!loaded.ok) throw new Error(loaded.message); - const annotations = newestFirstAnnotations(loaded.value); - - if (annotations.length === 0) { - notify("No annotations", "There is nothing to copy yet."); - process.exit(0); - } - - const copied = writeClipboard(formatAnnotations(annotations)); - if (!copied.ok) throw new Error(copied.message); - notify( - "Annotations copied", - `${annotations.length} annotation${annotations.length === 1 ? "" : "s"} copied as Markdown.`, - ); -} catch (error) { - const message = error instanceof Error ? error.message : String(error); - notify("Copy failed", message); - console.error(message); - process.exit(1); -} diff --git a/src/format.ts b/src/format.ts deleted file mode 100644 index 3d9571f..0000000 --- a/src/format.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Annotation } from "./types"; -import { charWidth } from "./width"; - -/** Remove terminal control characters while retaining useful whitespace. */ -export function sanitizeTerminalText(text: string): string { - return text - .replace(/\t/g, " ") - .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ""); -} - -/** Wrap plain text to terminal-cell-width lines while preserving explicit newlines. */ -export function wrapText(text: string, width: number): string[] { - const safeWidth = Math.max(1, width); - const output: string[] = []; - for (const sourceLine of text.replace(/\r\n/g, "\n").split("\n")) { - const chars = Array.from(sourceLine); - if (chars.length === 0) { - output.push(""); - continue; - } - let line = ""; - let used = 0; - for (const char of chars) { - const width = charWidth(char); - if (used + width > safeWidth && line !== "") { - output.push(line); - line = ""; - used = 0; - } - line += char; - used += width; - } - output.push(line); - } - return output; -} - -function fenceFor(text: string): string { - const longest = Math.max(0, ...Array.from(text.matchAll(/`+/g), (match) => match[0].length)); - return "`".repeat(Math.max(3, longest + 1)); -} - -/** Format saved annotations as portable, agent-neutral Markdown context. */ -export function formatAnnotations(annotations: readonly Annotation[]): string { - const sections = annotations.map((annotation, index) => { - const source = [annotation.context.workspace_label, annotation.context.tab_label] - .filter(Boolean) - .join(" / "); - const fence = fenceFor(annotation.selectedText); - const metadata = source ? `\nSource: ${source}\n` : ""; - return [ - `## Annotation ${index + 1}`, - metadata, - "Selected text:", - "", - fence, - annotation.selectedText, - fence, - "", - "Comment:", - "", - annotation.comment, - ] - .filter((line, lineIndex, lines) => !(line === "" && lines[lineIndex - 1] === "")) - .join("\n"); - }); - return `# Annotated context\n\n${sections.join("\n\n")}\n`; -} diff --git a/src/handoff.ts b/src/handoff.ts deleted file mode 100644 index 88e3f34..0000000 --- a/src/handoff.ts +++ /dev/null @@ -1,45 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -/** - * A selection handed to `annotate.capture` by another program on the same machine as the - * Herdr server (Neovim, a script), for hosts where the plugin cannot read a clipboard: - * headless servers reached over SSH or `herdr --remote`. - * - * The writer puts the text in {@link handoffPath} and invokes the action; the action takes - * the file (reads and deletes it) when it is fresh. Anything older is a leftover, not a - * selection, and is ignored so a stale file can never be annotated by surprise. - */ -export const HANDOFF_MAX_AGE_MS = 15_000; - -/** `$XDG_RUNTIME_DIR` (per user, tmpfs) when set, else the system temp dir, plus the uid. */ -export function handoffPath(env: NodeJS.ProcessEnv = process.env): string { - const base = env.XDG_RUNTIME_DIR?.trim() || os.tmpdir(); - const uid = typeof process.getuid === "function" ? String(process.getuid()) : "user"; - return path.join(base, `herdr-annotate-${uid}`, "selection"); -} - -/** The handed-off text when the file exists and is fresh; the file is removed either way. */ -export function takeHandoff( - file: string = handoffPath(), - now: number = Date.now(), - maxAgeMs: number = HANDOFF_MAX_AGE_MS, -): string | undefined { - let stat: fs.Stats; - try { - stat = fs.statSync(file); - } catch { - return undefined; - } - let text: string | undefined; - if (stat.isFile() && now - stat.mtimeMs <= maxAgeMs) { - try { - text = fs.readFileSync(file, "utf8"); - } catch { - text = undefined; - } - } - fs.rmSync(file, { force: true }); - return text && text.trim() ? text : undefined; -} diff --git a/src/herdr.ts b/src/herdr.ts deleted file mode 100644 index 8cc6e5d..0000000 --- a/src/herdr.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { spawnSync } from "node:child_process"; - -const binary = process.env.HERDR_BIN_PATH ?? "herdr"; - -/** The result of invoking Herdr for an expected plugin operation. */ -export type HerdrResult = - | { readonly ok: true } - | { readonly ok: false; readonly message: string }; - -/** Invoke Herdr synchronously and return a safe error projection on failure. */ -export function runHerdr(args: readonly string[]): HerdrResult { - const result = spawnSync(binary, args, { - encoding: "utf8", - stdio: ["ignore", "ignore", "pipe"], - windowsHide: process.platform === "win32", - }); - if (result.status !== 0) { - return { - ok: false, - message: result.stderr?.trim() || `herdr ${args.join(" ")} failed`, - }; - } - return { ok: true }; -} - -/** Best-effort user notification; failures are intentionally non-fatal. */ -export function notify(title: string, body?: string): void { - const args = ["notification", "show", title]; - if (body) args.push("--body", body); - spawnSync(binary, args, { - encoding: "utf8", - stdio: ["ignore", "ignore", "pipe"], - windowsHide: process.platform === "win32", - }); -} diff --git a/src/layout.ts b/src/layout.ts deleted file mode 100644 index fc5ed5e..0000000 --- a/src/layout.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { charWidth } from "./width"; - -/** - * Lay the comment out in terminal cells and report where the cursor lands. - * - * The cursor column is measured in cells, not characters, so that wide - * characters do not push the reported position out of step with the screen. - */ -export function layoutComment( - comment: readonly string[], - cursor: number, - width: number, -): { lines: string[]; cursorRow: number; cursorCol: number } { - const safeWidth = Math.max(1, width); - const lines: string[] = [""]; - let row = 0; - let col = 0; - let cursorRow = 0; - let cursorCol = 0; - for (let index = 0; index <= comment.length; index += 1) { - const cells = index < comment.length ? charWidth(comment[index] as string) : 0; - // A wide character must not straddle the right edge. - if (col > 0 && col + cells > safeWidth) { - lines.push(""); - row += 1; - col = 0; - } - if (index === cursor) { - cursorRow = row; - cursorCol = col; - } - if (index === comment.length) break; - const char = comment[index] as string; - if (char === "\n") { - lines.push(""); - row += 1; - col = 0; - continue; - } - lines[row] += char; - col += cells; - } - return { lines, cursorRow, cursorCol }; -} diff --git a/src/manager-copy.ts b/src/manager-copy.ts deleted file mode 100644 index cc6b44d..0000000 --- a/src/manager-copy.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ClipboardResult } from "./clipboard"; -import { formatAnnotations } from "./format"; -import type { Annotation } from "./types"; - -type ClipboardWriter = (text: string) => ClipboardResult<undefined>; - -/** Whether the annotation manager should close or remain visible after a copy attempt. */ -export type ManagerCopyOutcome = - | { readonly _tag: "close" } - | { readonly _tag: "stay_open"; readonly message: string }; - -/** Format and copy annotations without changing the supplied annotations or their store. */ -export function copyAnnotations( - annotations: readonly Annotation[], - writeClipboard: ClipboardWriter, -): ManagerCopyOutcome { - if (annotations.length === 0) { - return { _tag: "stay_open", message: "Nothing to copy." }; - } - - const copied = writeClipboard(formatAnnotations(annotations)); - if (!copied.ok) { - return { _tag: "stay_open", message: copied.message }; - } - - return { _tag: "close" }; -} diff --git a/src/manager.ts b/src/manager.ts deleted file mode 100644 index 24e4b97..0000000 --- a/src/manager.ts +++ /dev/null @@ -1,429 +0,0 @@ -#!/usr/bin/env bun -import crypto from "node:crypto"; -import readline from "node:readline"; -import { copyAndArchiveAnnotations, restoreArchivedSet } from "./archive-workflow"; -import { writeClipboard } from "./clipboard"; -import { sanitizeTerminalText, wrapText } from "./format"; -import { stringWidth, truncateToWidth } from "./width"; -import { copyAnnotations } from "./manager-copy"; -import { emitToTerminal, paneClipboardWriter } from "./pane-clipboard"; -import { stateDir } from "./paths"; -import { - appendArchivedSet, - loadAnnotations, - loadArchivedSets, - mergeAnnotations, - newestFirstAnnotations, - newestFirstArchivedSets, - removeAnnotationsById, - removeArchivedSet, -} from "./store"; -import type { Annotation, ArchivedAnnotationSet } from "./types"; - -type ManagerView = "active" | "archives"; - -type Confirmation = - | { readonly _tag: "none" } - | { readonly _tag: "clear_active" } - | { readonly _tag: "delete_archive"; readonly archiveId: string }; - -function requireStateDir(): string { - const value = stateDir(); - if (!value) { - console.error("HERDR_PLUGIN_STATE_DIR is not set"); - process.exit(1); - } - return value; -} - -const dir = requireStateDir(); -const out = (value: string) => process.stdout.write(value); -const writePaneClipboard = paneClipboardWriter(writeClipboard, emitToTerminal); -let annotations: Annotation[] = []; -let archives: ArchivedAnnotationSet[] = []; -let activeSelected = 0; -let archiveSelected = 0; -let view: ManagerView = "active"; -let status = ""; -let confirmation: Confirmation = { _tag: "none" }; -let finished = false; - -function clampSelection(selected: number, length: number): number { - return Math.max(0, Math.min(selected, length - 1)); -} - -function reloadActive(): boolean { - const loaded = loadAnnotations(dir); - if (!loaded.ok) { - status = loaded.message; - return false; - } - annotations = newestFirstAnnotations(loaded.value); - activeSelected = clampSelection(activeSelected, annotations.length); - return true; -} - -function reloadArchives(): boolean { - const loaded = loadArchivedSets(dir); - if (!loaded.ok) { - status = loaded.message; - return false; - } - archives = newestFirstArchivedSets(loaded.value); - archiveSelected = clampSelection(archiveSelected, archives.length); - return true; -} - -function clipped(text: string, width: number): string { - const value = sanitizeTerminalText(text).replace(/\s+/g, " ").trim(); - if (stringWidth(value) <= width) return value; - return `${truncateToWidth(value, Math.max(0, width - 1))}…`; -} - -function writeAt(row: number, col: number, text: string): void { - out(`\x1b[${row};${col}H${text}`); -} - -function firstVisibleIndex(selected: number, length: number, visibleRows: number): number { - return Math.max(0, Math.min(selected - Math.floor(visibleRows / 2), length - visibleRows)); -} - -function countLabel(count: number): string { - return `${count} annotation${count === 1 ? "" : "s"}`; -} - -function renderActive( - rows: number, - listWidth: number, - detailLeft: number, - detailWidth: number, - listRows: number, -): void { - const first = firstVisibleIndex(activeSelected, annotations.length, listRows); - writeAt( - 1, - 2, - `\x1b[1mAnnotations (${annotations.length})\x1b[0m \x1b[2mnewest first\x1b[0m`, - ); - - if (annotations.length === 0) { - writeAt(3, 2, "\x1b[2mNo active annotations.\x1b[0m"); - return; - } - - annotations.slice(first, first + listRows).forEach((annotation, index) => { - const absolute = first + index; - const active = absolute === activeSelected; - const label = clipped(annotation.selectedText, listWidth - 4); - writeAt(2 + index, 2, `${active ? "\x1b[7m›" : " "} ${label}\x1b[0m`); - }); - - const current = annotations[activeSelected]; - if (!current) return; - const source = [current.context.workspace_label, current.context.tab_label] - .filter(Boolean) - .join(" / "); - writeAt(2, detailLeft, "\x1b[1mSelected text\x1b[0m"); - const selectedLines = wrapText(sanitizeTerminalText(current.selectedText), detailWidth).slice(0, 7); - selectedLines.forEach((line, index) => writeAt(3 + index, detailLeft, `\x1b[2m${line}\x1b[0m`)); - const commentRow = 4 + Math.max(3, selectedLines.length); - writeAt(commentRow, detailLeft, "\x1b[1mComment\x1b[0m"); - wrapText(sanitizeTerminalText(current.comment), detailWidth) - .slice(0, Math.max(1, rows - commentRow - 4)) - .forEach((line, index) => writeAt(commentRow + 1 + index, detailLeft, line)); - const metadata = [source, new Date(current.createdAt).toLocaleString()].filter(Boolean).join(" · "); - if (metadata) writeAt(rows - 2, detailLeft, `\x1b[2m${clipped(metadata, detailWidth)}\x1b[0m`); -} - -function renderArchives( - rows: number, - listWidth: number, - detailLeft: number, - detailWidth: number, - listRows: number, -): void { - const first = firstVisibleIndex(archiveSelected, archives.length, listRows); - writeAt( - 1, - 2, - `\x1b[1mArchives (${archives.length})\x1b[0m \x1b[2mnewest first\x1b[0m`, - ); - - if (archives.length === 0) { - writeAt(3, 2, "\x1b[2mNo archived sets.\x1b[0m"); - return; - } - - archives.slice(first, first + listRows).forEach((archive, index) => { - const absolute = first + index; - const active = absolute === archiveSelected; - const label = `${new Date(archive.archivedAt).toLocaleString()} · ${countLabel(archive.annotations.length)}`; - writeAt( - 2 + index, - 2, - `${active ? "\x1b[7m›" : " "} ${clipped(label, listWidth - 4)}\x1b[0m`, - ); - }); - - const current = archives[archiveSelected]; - if (!current) return; - writeAt(2, detailLeft, "\x1b[1mArchived set\x1b[0m"); - writeAt(3, detailLeft, `\x1b[2m${clipped(new Date(current.archivedAt).toLocaleString(), detailWidth)}\x1b[0m`); - writeAt(5, detailLeft, `\x1b[1m${countLabel(current.annotations.length)}\x1b[0m`); - const visibleAnnotations = newestFirstAnnotations(current.annotations); - const previewRows = Math.max(1, rows - 8); - visibleAnnotations.slice(0, previewRows).forEach((annotation, index) => { - const label = `${index + 1}. ${annotation.selectedText}`; - writeAt(6 + index, detailLeft, clipped(label, detailWidth)); - }); - if (visibleAnnotations.length > previewRows) { - writeAt(rows - 2, detailLeft, `\x1b[2m… ${visibleAnnotations.length - previewRows} more\x1b[0m`); - } -} - -function footerText(): string { - if (confirmation._tag === "clear_active") { - return "Press Shift+D again to clear all active annotations · Esc cancel"; - } - if (confirmation._tag === "delete_archive") { - return "Press d again to permanently delete this archive · Esc cancel"; - } - if (status) return status; - if (view === "active") { - return "j/k · y copy · c all · Shift+C copy+archive · d delete · Shift+D clear · Tab archives · q"; - } - return "j/k · y copy · u restore · d twice delete · Tab active · q"; -} - -function render(): void { - const cols = Math.max(50, process.stdout.columns || 98); - const rows = Math.max(14, process.stdout.rows || 28); - const listWidth = Math.max(22, Math.min(36, Math.floor(cols * 0.36))); - const detailLeft = listWidth + 3; - const detailWidth = Math.max(1, cols - detailLeft - 1); - const listRows = Math.max(1, rows - 4); - - out("\x1b[2J\x1b[H\x1b[?25l"); - for (let row = 2; row < rows; row += 1) { - writeAt(row, listWidth + 1, "\x1b[2m│\x1b[0m"); - } - if (view === "active") { - renderActive(rows, listWidth, detailLeft, detailWidth, listRows); - } else { - renderArchives(rows, listWidth, detailLeft, detailWidth, listRows); - } - writeAt(rows, 2, `\x1b[2m${clipped(footerText(), cols - 3)}\x1b[0m`); -} - -function copy(items: readonly Annotation[]): void { - const outcome = copyAnnotations(items, writePaneClipboard); - if (outcome._tag === "stay_open") { - status = outcome.message; - return; - } - exit(0); -} - -function copyAndArchive(): void { - const outcome = copyAndArchiveAnnotations({ - loadActive: () => loadAnnotations(dir), - writeClipboard: writePaneClipboard, - saveArchive: (archive) => appendArchivedSet(dir, archive), - removeActive: (annotationIds) => removeAnnotationsById(dir, annotationIds), - createArchiveId: () => crypto.randomUUID(), - now: () => new Date().toISOString(), - }); - if (outcome._tag === "close") exit(0); - if (outcome._tag === "archived_active_retained") { - reloadArchives(); - status = `Copied and archived, but active annotations remain: ${outcome.message}`; - return; - } - status = outcome.message; -} - -function deleteSelectedAnnotation(): void { - const target = annotations[activeSelected]; - if (!target) return; - const removed = removeAnnotationsById(dir, [target.id]); - if (!removed.ok) { - status = removed.message; - return; - } - if (!reloadActive()) return; - status = "Annotation deleted."; -} - -function clearActive(): void { - const removed = removeAnnotationsById( - dir, - annotations.map((annotation) => annotation.id), - ); - if (!removed.ok) { - status = removed.message; - return; - } - confirmation = { _tag: "none" }; - if (!reloadActive()) return; - status = "All active annotations cleared."; -} - -function restoreSelectedArchive(): void { - const target = archives[archiveSelected]; - if (!target) { - status = "No archive selected."; - return; - } - const outcome = restoreArchivedSet(target, { - mergeActive: (items) => mergeAnnotations(dir, items), - removeArchive: (archiveId) => removeArchivedSet(dir, archiveId), - }); - if (outcome._tag === "stay_open") { - status = outcome.message; - return; - } - const activeReloaded = reloadActive(); - const archivesReloaded = reloadArchives(); - if (!activeReloaded || !archivesReloaded) return; - if (outcome._tag === "restored_archive_retained") { - status = `Annotations restored, but the archive remains: ${outcome.message}`; - return; - } - status = outcome.restoredCount - ? `${countLabel(outcome.restoredCount)} restored.` - : "Archive removed; its annotations were already active."; -} - -function deleteSelectedArchive(archiveId: string): void { - const removed = removeArchivedSet(dir, archiveId); - if (!removed.ok) { - status = removed.message; - return; - } - confirmation = { _tag: "none" }; - if (!reloadArchives()) return; - status = "Archive permanently deleted."; -} - -function switchView(): void { - confirmation = { _tag: "none" }; - status = ""; - if (view === "active") { - view = "archives"; - reloadArchives(); - } else { - view = "active"; - reloadActive(); - } -} - -function handleActiveKey(text: string, key: readline.Key): void { - if (text === "D") { - if (confirmation._tag === "clear_active") clearActive(); - else confirmation = { _tag: "clear_active" }; - return; - } - - confirmation = { _tag: "none" }; - if (key.name === "up" || key.name === "k") { - activeSelected = Math.max(0, activeSelected - 1); - } else if (key.name === "down" || key.name === "j") { - activeSelected = Math.min(Math.max(0, annotations.length - 1), activeSelected + 1); - } else if (text === "C") { - copyAndArchive(); - } else if (key.name === "y") { - const current = annotations[activeSelected]; - copy(current ? [current] : []); - } else if (key.name === "c") { - copy(annotations); - } else if (key.name === "d") { - deleteSelectedAnnotation(); - } else if (key.name === "r") { - if (reloadActive()) status = "Reloaded."; - } -} - -function handleArchiveKey(text: string, key: readline.Key): void { - if (text === "d") { - const current = archives[archiveSelected]; - if (!current) { - confirmation = { _tag: "none" }; - status = "No archive selected."; - } else if ( - confirmation._tag === "delete_archive" && - confirmation.archiveId === current.id - ) { - deleteSelectedArchive(current.id); - } else { - confirmation = { _tag: "delete_archive", archiveId: current.id }; - } - return; - } - - confirmation = { _tag: "none" }; - if (key.name === "up" || key.name === "k") { - archiveSelected = Math.max(0, archiveSelected - 1); - } else if (key.name === "down" || key.name === "j") { - archiveSelected = Math.min(Math.max(0, archives.length - 1), archiveSelected + 1); - } else if (key.name === "y") { - const current = archives[archiveSelected]; - copy(current ? newestFirstAnnotations(current.annotations) : []); - } else if (key.name === "u") { - restoreSelectedArchive(); - } else if (key.name === "r") { - if (reloadArchives()) status = "Reloaded."; - } -} - -function cleanup(): void { - if (finished) return; - finished = true; - if (process.stdin.isTTY) process.stdin.setRawMode(false); - out("\x1b[?25h\x1b[2J\x1b[H\x1b[?1049l"); -} - -function exit(code: number): never { - cleanup(); - process.exit(code); -} - -reloadActive(); -reloadArchives(); -process.on("exit", cleanup); -process.on("SIGTERM", () => exit(0)); -try { - process.on("SIGHUP", () => exit(0)); -} catch { - // SIGHUP is not available on every supported platform. -} -process.stdout.on("resize", render); - -readline.emitKeypressEvents(process.stdin, { escapeCodeTimeout: 20 } as any); -if (process.stdin.isTTY) process.stdin.setRawMode(true); -process.stdin.resume(); -process.stdin.on("keypress", (text: string, key: readline.Key) => { - if (key.ctrl && key.name === "c") return exit(0); - if (key.name === "escape") { - if (confirmation._tag !== "none") { - confirmation = { _tag: "none" }; - status = ""; - render(); - return; - } - return exit(0); - } - if (key.name === "q") return exit(0); - if (key.name === "tab") { - switchView(); - render(); - return; - } - - status = ""; - if (view === "active") handleActiveKey(text, key); - else handleArchiveKey(text, key); - render(); -}); - -out("\x1b[?1049h"); -render(); diff --git a/src/open-manager.ts b/src/open-manager.ts deleted file mode 100644 index 5d5ebdb..0000000 --- a/src/open-manager.ts +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bun -import { notify, runHerdr } from "./herdr"; -import { pluginRoot } from "./paths"; - -const root = pluginRoot(); -if (!root) { - const message = "HERDR_PLUGIN_ROOT is not set"; - notify("Unable to open annotations", message); - console.error(message); - process.exit(1); -} - -const opened = runHerdr([ - "plugin", - "pane", - "open", - "--cwd", - root, - "--plugin", - "annotate", - "--entrypoint", - "manager", - "--placement", - "popup", - "--width", - "100", - "--height", - "30", - "--focus", -]); - -if (!opened.ok) { - notify("Unable to open annotations", opened.message); - console.error(opened.message); - process.exit(1); -} diff --git a/src/pane-clipboard.ts b/src/pane-clipboard.ts deleted file mode 100644 index c438cab..0000000 --- a/src/pane-clipboard.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Clipboard writes performed from inside a Herdr pane. - * - * Herdr 0.9.0 forwards OSC 52 sequences emitted by pane output to the viewing client, so a copy made - * inside a pane can reach the clipboard of the machine the person is sitting at rather than only the - * machine the plugin runs on. Panes emit the sequence in addition to the native clipboard write; the - * global actions run with piped stdout and no terminal, so they cannot use this path. - */ -import type { ClipboardResult } from "./clipboard"; - -/** - * Base64 payload size that terminals commonly refuse beyond. Herdr forwards whatever the terminal - * accepts, so this is advisory only: oversized text is still emitted in full, never truncated. - */ -export const OSC52_COMMON_PAYLOAD_LIMIT_BYTES = 74994; - -/** Build the OSC 52 sequence that sets the terminal clipboard selection to `text`. */ -export function osc52ClipboardSequence(text: string): string { - return `\x1b]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`; -} - -/** Whether the encoded payload is larger than terminals commonly accept. */ -export function exceedsCommonOsc52Limit(text: string): boolean { - return Buffer.from(text, "utf8").toString("base64").length > OSC52_COMMON_PAYLOAD_LIMIT_BYTES; -} - -/** - * Write the sequence to the pane's terminal, reporting whether it was written. - * - * Best effort: a closed or broken stdout must not throw out of a copy. - */ -export function emitToTerminal(sequence: string): boolean { - try { - process.stdout.write(sequence); - return true; - } catch { - return false; - } -} - -/** - * Wrap a native clipboard writer so every pane copy also reaches the viewing client's terminal. - * - * The native write is attempted first and the sequence is emitted afterwards. Either one landing is - * a successful copy: a remote server commonly has no clipboard tool at all, and the sequence is the - * copy that actually reached the person, so it must not be reported as a failure or block the rest - * of a copy-and-archive. Only a copy that reached neither destination fails, with the native error. - */ -export function paneClipboardWriter( - writeClipboard: (text: string) => ClipboardResult<undefined>, - emit: (sequence: string) => boolean, -): (text: string) => ClipboardResult<undefined> { - return (text: string) => { - const native = writeClipboard(text); - const emitted = emit(osc52ClipboardSequence(text)); - if (native.ok || emitted) return { ok: true, value: undefined }; - return native; - }; -} diff --git a/src/paths.ts b/src/paths.ts deleted file mode 100644 index b93dbeb..0000000 --- a/src/paths.ts +++ /dev/null @@ -1,45 +0,0 @@ -import path from "node:path"; - -/** Remove Windows extended-path prefixes that Bun and CreateProcess cannot use as a cwd. */ -export function normalizeWindowsPath(value: string): string; -export function normalizeWindowsPath(value: undefined): undefined; -export function normalizeWindowsPath(value: string | undefined): string | undefined { - if (!value) return value; - - if (value.startsWith("\\\\?\\")) { - const withoutPrefix = value.slice(4); - return withoutPrefix.slice(0, 4).toUpperCase() === "UNC\\" - ? `\\\\${withoutPrefix.slice(4)}` - : withoutPrefix; - } - - if (value.startsWith("//?/")) { - const withoutPrefix = value.slice(4); - return withoutPrefix.slice(0, 4).toUpperCase() === "UNC/" - ? `//${withoutPrefix.slice(4)}` - : withoutPrefix; - } - - return value; -} - -/** Return Herdr's plugin-owned state directory when the runtime supplied one. */ -export function stateDir(): string | undefined { - return process.env.HERDR_PLUGIN_STATE_DIR || undefined; -} - -/** Return Herdr's plugin root in a form that Bun and CreateProcess can use as a cwd. */ -export function pluginRoot(): string | undefined { - const value = process.env.HERDR_PLUGIN_ROOT; - return value ? normalizeWindowsPath(value) : undefined; -} - -/** Resolve the JSONL store inside an already parsed plugin state directory. */ -export function annotationsPath(dir: string): string { - return path.join(dir, "annotations.jsonl"); -} - -/** Resolve the archived-set JSONL store inside the plugin state directory. */ -export function archivesPath(dir: string): string { - return path.join(dir, "archives.jsonl"); -} diff --git a/src/store.ts b/src/store.ts deleted file mode 100644 index 06213fa..0000000 --- a/src/store.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { randomUUID } from "node:crypto"; -import fs from "node:fs"; -import path from "node:path"; -import { annotationsPath, archivesPath } from "./paths"; -import { - parseAnnotation, - parseArchivedAnnotationSet, - type Annotation, - type ArchivedAnnotationSet, -} from "./types"; - -type StoreName = "annotations" | "archives"; - -type LockCreation = - | { readonly _tag: "acquired" } - | { readonly _tag: "exists" } - | { readonly _tag: "error"; readonly message: string }; - -interface StoreLockLease { - readonly path: string; - readonly owner: string; -} - -const staleLockMilliseconds = 30_000; - -/** The result of an expected annotation-store operation. */ -export type StoreResult<T> = - | { readonly ok: true; readonly value: T } - | { readonly ok: false; readonly message: string }; - -/** Present append-ordered annotations with the most recently saved first. */ -export function newestFirstAnnotations(annotations: readonly Annotation[]): Annotation[] { - return annotations.slice().reverse(); -} - -/** Load the complete active store, rejecting malformed records instead of dropping data. */ -export function loadAnnotations(dir: string): StoreResult<Annotation[]> { - return withStoreLock(dir, "annotations", () => loadAnnotationsUnlocked(dir)); -} - -/** Append one annotation without rewriting existing records. */ -export function appendAnnotation(dir: string, annotation: Annotation): StoreResult<undefined> { - return withStoreLock(dir, "annotations", () => { - try { - fs.appendFileSync(annotationsPath(dir), `${JSON.stringify(annotation)}\n`, { mode: 0o600 }); - return { ok: true, value: undefined }; - } catch (error) { - return { ok: false, message: safeFileError("Unable to save annotation", error) }; - } - }); -} - -/** Remove selected annotation IDs without racing concurrent annotation saves. */ -export function removeAnnotationsById( - dir: string, - annotationIds: readonly string[], -): StoreResult<undefined> { - return withStoreLock(dir, "annotations", () => { - const loaded = loadAnnotationsUnlocked(dir); - if (!loaded.ok) return loaded; - const removed = new Set(annotationIds); - return replaceAnnotationsUnlocked( - dir, - loaded.value.filter((annotation) => !removed.has(annotation.id)), - ); - }); -} - -/** Merge annotations into the active list without duplicating existing annotation IDs. */ -export function mergeAnnotations( - dir: string, - annotations: readonly Annotation[], -): StoreResult<number> { - return withStoreLock(dir, "annotations", () => { - const loaded = loadAnnotationsUnlocked(dir); - if (!loaded.ok) return loaded; - const existingIds = new Set(loaded.value.map((annotation) => annotation.id)); - const additions = annotations.filter((annotation) => !existingIds.has(annotation.id)); - if (additions.length === 0) return { ok: true, value: 0 }; - const replaced = replaceAnnotationsUnlocked(dir, [...loaded.value, ...additions]); - if (!replaced.ok) return replaced; - return { ok: true, value: additions.length }; - }); -} - -/** Present append-ordered archive sets with the most recently archived first. */ -export function newestFirstArchivedSets( - archives: readonly ArchivedAnnotationSet[], -): ArchivedAnnotationSet[] { - return archives.slice().reverse(); -} - -/** Load complete, parsed annotation sets from the archive store. */ -export function loadArchivedSets(dir: string): StoreResult<ArchivedAnnotationSet[]> { - return withStoreLock(dir, "archives", () => loadArchivedSetsUnlocked(dir)); -} - -/** Atomically append one complete set to the archive store. */ -export function appendArchivedSet( - dir: string, - archive: ArchivedAnnotationSet, -): StoreResult<undefined> { - return withStoreLock(dir, "archives", () => { - const loaded = loadArchivedSetsUnlocked(dir); - if (!loaded.ok) return loaded; - return replaceArchivedSetsUnlocked(dir, [...loaded.value, archive]); - }); -} - -/** Permanently remove one archived set by its archive ID. */ -export function removeArchivedSet(dir: string, archiveId: string): StoreResult<undefined> { - return withStoreLock(dir, "archives", () => { - const loaded = loadArchivedSetsUnlocked(dir); - if (!loaded.ok) return loaded; - return replaceArchivedSetsUnlocked( - dir, - loaded.value.filter((archive) => archive.id !== archiveId), - ); - }); -} - -function loadAnnotationsUnlocked(dir: string): StoreResult<Annotation[]> { - return loadJsonLines(annotationsPath(dir), "annotations", parseAnnotation); -} - -function replaceAnnotationsUnlocked( - dir: string, - annotations: readonly Annotation[], -): StoreResult<undefined> { - return replaceJsonLines( - dir, - annotationsPath(dir), - "annotations", - annotations, - "Unable to update annotations", - ); -} - -function loadArchivedSetsUnlocked(dir: string): StoreResult<ArchivedAnnotationSet[]> { - return loadJsonLines(archivesPath(dir), "archives", parseArchivedAnnotationSet); -} - -function replaceArchivedSetsUnlocked( - dir: string, - archives: readonly ArchivedAnnotationSet[], -): StoreResult<undefined> { - return replaceJsonLines(dir, archivesPath(dir), "archives", archives, "Unable to update archives"); -} - -function loadJsonLines<T>( - file: string, - label: string, - parse: (value: unknown) => T | undefined, -): StoreResult<T[]> { - if (!fs.existsSync(file)) return { ok: true, value: [] }; - try { - const records: T[] = []; - for (const line of fs.readFileSync(file, "utf8").split("\n").filter(Boolean)) { - let decoded: unknown; - try { - decoded = JSON.parse(line); - } catch { - return { ok: false, message: `Unable to read ${label} (invalid data)` }; - } - const record = parse(decoded); - if (!record) return { ok: false, message: `Unable to read ${label} (invalid data)` }; - records.push(record); - } - return { ok: true, value: records }; - } catch (error) { - return { ok: false, message: safeFileError(`Unable to read ${label}`, error) }; - } -} - -function replaceJsonLines( - dir: string, - file: string, - temporaryLabel: string, - records: readonly unknown[], - errorMessage: string, -): StoreResult<undefined> { - const temporary = path.join(dir, `.${temporaryLabel}-${process.pid}-${Date.now()}.tmp`); - try { - const contents = records.map((record) => JSON.stringify(record)).join("\n"); - fs.writeFileSync(temporary, contents ? `${contents}\n` : "", { mode: 0o600 }); - fs.renameSync(temporary, file); - return { ok: true, value: undefined }; - } catch (error) { - try { - fs.rmSync(temporary, { force: true }); - } catch {} - return { ok: false, message: safeFileError(errorMessage, error) }; - } -} - -function withStoreLock<T>( - dir: string, - store: StoreName, - operation: () => StoreResult<T>, -): StoreResult<T> { - try { - fs.mkdirSync(dir, { recursive: true }); - } catch (error) { - return { ok: false, message: safeFileError(`Unable to access ${store}`, error) }; - } - - const acquired = acquireStoreLock(path.join(dir, `.${store}.lock`), store); - if (!acquired.ok) return acquired; - - try { - return operation(); - } finally { - releaseStoreLock(acquired.value); - } -} - -function acquireStoreLock(lock: string, store: StoreName): StoreResult<StoreLockLease> { - const owner = `${process.pid}:${randomUUID()}`; - const created = createStoreLock(lock, store, owner); - if (created._tag === "acquired") return { ok: true, value: { path: lock, owner } }; - if (created._tag === "error") return { ok: false, message: created.message }; - - if (!isStaleLock(lock)) { - return { ok: false, message: `${capitalized(store)} are busy; try again.` }; - } - - try { - fs.rmSync(lock, { recursive: true, force: true }); - } catch (error) { - return { ok: false, message: safeFileError(`Unable to lock ${store}`, error) }; - } - - const retried = createStoreLock(lock, store, owner); - if (retried._tag === "exists") { - return { ok: false, message: `${capitalized(store)} are busy; try again.` }; - } - if (retried._tag === "error") return { ok: false, message: retried.message }; - return { ok: true, value: { path: lock, owner } }; -} - -function createStoreLock(lock: string, store: StoreName, owner: string): LockCreation { - let directoryCreated = false; - try { - fs.mkdirSync(lock, { mode: 0o700 }); - directoryCreated = true; - fs.writeFileSync(path.join(lock, "owner"), `${owner}\n`, { mode: 0o600 }); - return { _tag: "acquired" }; - } catch (error) { - if (hasFileErrorCode(error, "EEXIST")) { - return { _tag: "exists" }; - } - if (directoryCreated) { - try { - fs.rmSync(lock, { recursive: true, force: true }); - } catch {} - } - return { _tag: "error", message: safeFileError(`Unable to lock ${store}`, error) }; - } -} - -function isStaleLock(lock: string): boolean { - try { - return Date.now() - fs.statSync(lock).mtimeMs >= staleLockMilliseconds; - } catch { - return false; - } -} - -function releaseStoreLock(lease: StoreLockLease): void { - try { - const currentOwner = fs.readFileSync(path.join(lease.path, "owner"), "utf8").trim(); - if (currentOwner !== lease.owner) return; - fs.rmSync(lease.path, { recursive: true, force: true }); - } catch {} -} - -function capitalized(value: string): string { - return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`; -} - -function hasFileErrorCode(error: unknown, code: string): boolean { - return error instanceof Error && "code" in error && error.code === code; -} - -function safeFileError(prefix: string, error: unknown): string { - if (error instanceof Error && "code" in error && typeof error.code === "string") { - return `${prefix} (${error.code})`; - } - return prefix; -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index ac8f277..0000000 --- a/src/types.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** Herdr invocation fields retained as useful annotation provenance. */ -export interface InvocationContext { - workspace_id?: string; - workspace_label?: string; - tab_id?: string; - tab_label?: string; - focused_pane_id?: string; - focused_pane_cwd?: string; - focused_pane_agent?: string; -} - -/** Clipboard text and provenance waiting for a user comment. */ -export interface PendingAnnotation { - selectedText: string; - context: InvocationContext; - capturedAt: string; -} - -/** A saved annotation with a non-empty user comment. */ -export interface Annotation extends PendingAnnotation { - id: string; - comment: string; - createdAt: string; -} - -/** One recoverable set of annotations moved out of the active list. */ -export interface ArchivedAnnotationSet { - readonly version: 1; - readonly id: string; - readonly archivedAt: string; - readonly annotations: readonly Annotation[]; -} - -function isRecord(value: unknown): value is Record<string, unknown> { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function optionalString(record: Record<string, unknown>, key: string): string | undefined { - const value = record[key]; - return typeof value === "string" ? value : undefined; -} - -/** Parse untrusted Herdr context JSON into the small provenance shape this plugin stores. */ -export function parseInvocationContext(value: unknown): InvocationContext { - if (!isRecord(value)) return {}; - return { - workspace_id: optionalString(value, "workspace_id"), - workspace_label: optionalString(value, "workspace_label"), - tab_id: optionalString(value, "tab_id"), - tab_label: optionalString(value, "tab_label"), - focused_pane_id: optionalString(value, "focused_pane_id"), - focused_pane_cwd: optionalString(value, "focused_pane_cwd"), - focused_pane_agent: optionalString(value, "focused_pane_agent"), - }; -} - -/** Read a non-empty terminal selection from Herdr's plugin invocation context. */ -export function selectedTextFromInvocation(value: unknown): string | undefined { - if (!isRecord(value)) return undefined; - const selectedText = optionalString(value, "selected_text"); - return selectedText?.trim() ? selectedText : undefined; -} - -/** Build a pending annotation from selected text supplied by a Herdr pane invocation. */ -export function pendingAnnotationFromInvocation( - value: unknown, - capturedAt = new Date().toISOString(), -): PendingAnnotation | undefined { - const selectedText = selectedTextFromInvocation(value); - if (!selectedText) return undefined; - return { - selectedText, - context: parseInvocationContext(value), - capturedAt, - }; -} - -/** Parse a pending-annotation file, returning undefined when required fields are invalid. */ -export function parsePendingAnnotation(value: unknown): PendingAnnotation | undefined { - if (!isRecord(value)) return undefined; - const selectedText = optionalString(value, "selectedText"); - const capturedAt = optionalString(value, "capturedAt"); - if (selectedText === undefined || capturedAt === undefined) return undefined; - return { - selectedText, - capturedAt, - context: parseInvocationContext(value.context), - }; -} - -/** Parse one persisted JSONL record, returning undefined for malformed records. */ -export function parseAnnotation(value: unknown): Annotation | undefined { - const pending = parsePendingAnnotation(value); - if (!pending || !isRecord(value)) return undefined; - const id = optionalString(value, "id"); - const comment = optionalString(value, "comment"); - const createdAt = optionalString(value, "createdAt"); - if (!id || !comment?.trim() || !createdAt) return undefined; - return { ...pending, id, comment, createdAt }; -} - -/** Parse one persisted archive record without accepting partial annotation sets. */ -export function parseArchivedAnnotationSet(value: unknown): ArchivedAnnotationSet | undefined { - if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.annotations)) { - return undefined; - } - const id = optionalString(value, "id"); - const archivedAt = optionalString(value, "archivedAt"); - if (!id || !archivedAt || value.annotations.length === 0) return undefined; - - const annotations: Annotation[] = []; - for (const item of value.annotations) { - const annotation = parseAnnotation(item); - if (!annotation) return undefined; - annotations.push(annotation); - } - - return { version: 1, id, archivedAt, annotations }; -} diff --git a/src/width.ts b/src/width.ts deleted file mode 100644 index 1d2ca2a..0000000 --- a/src/width.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Terminal cell widths. - * - * East Asian wide and fullwidth characters occupy two terminal cells. Counting - * them as one shifts every column the editor computes, which misplaces the - * cursor and lets IME preedit overlays land inside existing text. - */ - -const WIDE_RANGES: readonly (readonly [number, number])[] = [ - [0x1100, 0x115f], // Hangul Jamo initial consonants - [0x2e80, 0x303e], // CJK radicals, Kangxi, CJK symbols and punctuation - [0x3041, 0x33ff], // Hiragana through CJK compatibility - [0x3400, 0x4dbf], // CJK unified ideographs extension A - [0x4e00, 0x9fff], // CJK unified ideographs - [0xa000, 0xa4cf], // Yi syllables - [0xa960, 0xa97f], // Hangul Jamo extended-A - [0xac00, 0xd7a3], // Hangul syllables - [0xf900, 0xfaff], // CJK compatibility ideographs - [0xfe10, 0xfe19], // Vertical forms - [0xfe30, 0xfe6f], // CJK compatibility forms, small form variants - [0xff00, 0xff60], // Fullwidth forms - [0xffe0, 0xffe6], // Fullwidth signs - [0x1f300, 0x1f64f], // Emoji - [0x1f900, 0x1f9ff], // Supplemental symbols and pictographs - [0x20000, 0x3fffd], // CJK extensions B and beyond -]; - -function isWide(codePoint: number): boolean { - for (const [start, end] of WIDE_RANGES) { - if (codePoint < start) return false; - if (codePoint <= end) return true; - } - return false; -} - -/** Cells occupied by a single character. Control characters count as zero. */ -export function charWidth(char: string): number { - const codePoint = char.codePointAt(0); - if (codePoint === undefined) return 0; - if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0)) return 0; - // Combining marks attach to the preceding character. - if (codePoint >= 0x0300 && codePoint <= 0x036f) return 0; - if (codePoint >= 0x200b && codePoint <= 0x200f) return 0; - return isWide(codePoint) ? 2 : 1; -} - -/** Cells occupied by a string. */ -export function stringWidth(text: string): number { - let total = 0; - for (const char of text) total += charWidth(char); - return total; -} - -/** Longest prefix of `text` that fits in `width` cells without splitting a character. */ -export function truncateToWidth(text: string, width: number): string { - if (width <= 0) return ""; - let used = 0; - let output = ""; - for (const char of text) { - const next = charWidth(char); - if (used + next > width) break; - output += char; - used += next; - } - return output; -} diff --git a/test/archive-workflow.test.ts b/test/archive-workflow.test.ts deleted file mode 100644 index 2035748..0000000 --- a/test/archive-workflow.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { copyAndArchiveAnnotations, restoreArchivedSet } from "../src/archive-workflow"; -import { - appendAnnotation, - appendArchivedSet, - loadAnnotations, - loadArchivedSets, - mergeAnnotations, - removeAnnotationsById, - removeArchivedSet, -} from "../src/store"; -import type { Annotation, ArchivedAnnotationSet } from "../src/types"; - -const directories: string[] = []; - -function temporaryDirectory(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "herdr-annotate-workflow-")); - directories.push(dir); - return dir; -} - -function annotation(id: string): Annotation { - return { - id, - selectedText: `selection ${id}`, - comment: `comment ${id}`, - capturedAt: "2026-08-08T00:00:00Z", - createdAt: "2026-08-08T00:00:01Z", - context: {}, - }; -} - -function archive(annotationIds: readonly string[]): ArchivedAnnotationSet { - return { - version: 1, - id: "archive-one", - archivedAt: "2026-08-26T23:32:00Z", - annotations: annotationIds.map(annotation), - }; -} - -afterEach(() => { - for (const dir of directories.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); -}); - -describe("copy and archive", () => { - test("copies newest first, saves the complete set, then removes active IDs", () => { - const active = [annotation("one"), annotation("two")]; - const events: string[] = []; - let clipboardText = ""; - let savedArchive: ArchivedAnnotationSet | undefined; - let removedIds: readonly string[] = []; - - const outcome = copyAndArchiveAnnotations({ - loadActive: () => { - events.push("load"); - return { ok: true, value: active }; - }, - writeClipboard: (text) => { - events.push("copy"); - clipboardText = text; - return { ok: true, value: undefined }; - }, - saveArchive: (saved) => { - events.push("archive"); - savedArchive = saved; - return { ok: true, value: undefined }; - }, - removeActive: (ids) => { - events.push("remove"); - removedIds = ids; - return { ok: true, value: undefined }; - }, - createArchiveId: () => "archive-one", - now: () => "2026-08-26T23:32:00Z", - }); - - expect(outcome).toEqual({ _tag: "close", archivedCount: 2 }); - expect(events).toEqual(["load", "copy", "archive", "remove"]); - expect(clipboardText.indexOf("selection two")).toBeLessThan( - clipboardText.indexOf("selection one"), - ); - expect(savedArchive).toEqual(archive(["one", "two"])); - expect(removedIds).toEqual(["one", "two"]); - }); - - test("does not archive or clear when clipboard writing fails", () => { - const events: string[] = []; - const outcome = copyAndArchiveAnnotations({ - loadActive: () => ({ ok: true, value: [annotation("one")] }), - writeClipboard: () => ({ ok: false, message: "Clipboard unavailable" }), - saveArchive: () => { - events.push("archive"); - return { ok: true, value: undefined }; - }, - removeActive: () => { - events.push("remove"); - return { ok: true, value: undefined }; - }, - createArchiveId: () => "archive-one", - now: () => "2026-08-26T23:32:00Z", - }); - - expect(outcome).toEqual({ _tag: "stay_open", message: "Clipboard unavailable" }); - expect(events).toEqual([]); - }); - - test("does not clear active annotations when archive storage fails", () => { - let removed = false; - const outcome = copyAndArchiveAnnotations({ - loadActive: () => ({ ok: true, value: [annotation("one")] }), - writeClipboard: () => ({ ok: true, value: undefined }), - saveArchive: () => ({ ok: false, message: "Archive unavailable" }), - removeActive: () => { - removed = true; - return { ok: true, value: undefined }; - }, - createArchiveId: () => "archive-one", - now: () => "2026-08-26T23:32:00Z", - }); - - expect(outcome).toEqual({ _tag: "stay_open", message: "Archive unavailable" }); - expect(removed).toBeFalse(); - }); - - test("reports that active data remains when clearing fails after archival", () => { - let archived = false; - const outcome = copyAndArchiveAnnotations({ - loadActive: () => ({ ok: true, value: [annotation("one")] }), - writeClipboard: () => ({ ok: true, value: undefined }), - saveArchive: () => { - archived = true; - return { ok: true, value: undefined }; - }, - removeActive: () => ({ ok: false, message: "Active store unavailable" }), - createArchiveId: () => "archive-one", - now: () => "2026-08-26T23:32:00Z", - }); - - expect(archived).toBeTrue(); - expect(outcome).toEqual({ - _tag: "archived_active_retained", - message: "Active store unavailable", - }); - }); -}); - -describe("restore archive", () => { - test("merges annotations before removing the archived set", () => { - const events: string[] = []; - const outcome = restoreArchivedSet(archive(["one", "two"]), { - mergeActive: () => { - events.push("merge"); - return { ok: true, value: 2 }; - }, - removeArchive: () => { - events.push("remove"); - return { ok: true, value: undefined }; - }, - }); - - expect(outcome).toEqual({ _tag: "restored", restoredCount: 2 }); - expect(events).toEqual(["merge", "remove"]); - }); - - test("keeps the archive when restoring active annotations fails", () => { - let removed = false; - const outcome = restoreArchivedSet(archive(["one"]), { - mergeActive: () => ({ ok: false, message: "Active store unavailable" }), - removeArchive: () => { - removed = true; - return { ok: true, value: undefined }; - }, - }); - - expect(outcome).toEqual({ _tag: "stay_open", message: "Active store unavailable" }); - expect(removed).toBeFalse(); - }); - - test("reports a retained archive after active annotations were restored", () => { - const outcome = restoreArchivedSet(archive(["one"]), { - mergeActive: () => ({ ok: true, value: 1 }), - removeArchive: () => ({ ok: false, message: "Archive unavailable" }), - }); - - expect(outcome).toEqual({ - _tag: "restored_archive_retained", - restoredCount: 1, - message: "Archive unavailable", - }); - }); -}); - -test("archives, clears, and restores a set through the real stores", () => { - const dir = temporaryDirectory(); - appendAnnotation(dir, annotation("one")); - appendAnnotation(dir, annotation("two")); - - const archived = copyAndArchiveAnnotations({ - loadActive: () => loadAnnotations(dir), - writeClipboard: () => ({ ok: true, value: undefined }), - saveArchive: (set) => appendArchivedSet(dir, set), - removeActive: (ids) => removeAnnotationsById(dir, ids), - createArchiveId: () => "archive-one", - now: () => "2026-08-26T23:32:00Z", - }); - - expect(archived).toEqual({ _tag: "close", archivedCount: 2 }); - expect(loadAnnotations(dir)).toEqual({ ok: true, value: [] }); - const storedArchives = loadArchivedSets(dir); - expect(storedArchives.ok && storedArchives.value.map(({ id }) => id)).toEqual(["archive-one"]); - if (!storedArchives.ok) return; - const storedArchive = storedArchives.value[0]; - if (!storedArchive) return; - - const restored = restoreArchivedSet(storedArchive, { - mergeActive: (items) => mergeAnnotations(dir, items), - removeArchive: (archiveId) => removeArchivedSet(dir, archiveId), - }); - - expect(restored).toEqual({ _tag: "restored", restoredCount: 2 }); - const active = loadAnnotations(dir); - expect(active.ok && active.value.map(({ id }) => id)).toEqual(["one", "two"]); - expect(loadArchivedSets(dir)).toEqual({ ok: true, value: [] }); -}); - -test("copy and archive preserves an annotation saved after the active snapshot", () => { - const dir = temporaryDirectory(); - appendAnnotation(dir, annotation("snapshot")); - - const outcome = copyAndArchiveAnnotations({ - loadActive: () => loadAnnotations(dir), - writeClipboard: () => ({ ok: true, value: undefined }), - saveArchive: (set) => { - const saved = appendArchivedSet(dir, set); - if (!saved.ok) return saved; - return appendAnnotation(dir, annotation("concurrent")); - }, - removeActive: (ids) => removeAnnotationsById(dir, ids), - createArchiveId: () => "archive-one", - now: () => "2026-08-26T23:32:00Z", - }); - - expect(outcome).toEqual({ _tag: "close", archivedCount: 1 }); - expect(loadAnnotations(dir)).toEqual({ ok: true, value: [annotation("concurrent")] }); - const storedArchives = loadArchivedSets(dir); - expect( - storedArchives.ok && storedArchives.value[0]?.annotations.map(({ id }) => id), - ).toEqual(["snapshot"]); -}); diff --git a/test/edit-keys.test.ts b/test/edit-keys.test.ts deleted file mode 100644 index d10a17d..0000000 --- a/test/edit-keys.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { lineEnd, lineStart, resolveEditKey, wordEnd, wordStart } from "../src/edit-keys"; - -const chars = (text: string) => Array.from(text); - -describe("word boundaries", () => { - test("wordStart skips trailing spaces then the word", () => { - const c = chars("foo bar "); - expect(wordStart(c, c.length)).toBe(4); - expect(wordStart(c, 4)).toBe(0); - expect(wordStart(c, 0)).toBe(0); - }); - - test("wordStart stops at a newline before crossing it", () => { - const c = chars("foo\nbar"); - expect(wordStart(c, 4)).toBe(3); - expect(wordStart(c, 3)).toBe(0); - }); - - test("wordEnd skips leading spaces then the word", () => { - const c = chars(" foo bar"); - expect(wordEnd(c, 0)).toBe(5); - expect(wordEnd(c, 5)).toBe(9); - expect(wordEnd(c, 9)).toBe(9); - }); - - test("wordEnd stops after a newline", () => { - const c = chars("foo\nbar"); - expect(wordEnd(c, 3)).toBe(4); - }); - - test("CJK runs count as one word", () => { - const c = chars("한글 테스트"); - expect(wordStart(c, c.length)).toBe(3); - expect(wordEnd(c, 0)).toBe(2); - }); -}); - -describe("line boundaries", () => { - test("lineStart and lineEnd stay within the current line", () => { - const c = chars("ab\ncd\nef"); - expect(lineStart(c, 4)).toBe(3); - expect(lineEnd(c, 4)).toBe(5); - expect(lineStart(c, 0)).toBe(0); - expect(lineEnd(c, 8)).toBe(8); - }); -}); - -describe("resolveEditKey", () => { - test("readline kill bindings (Cmd/Opt+Backspace via Ghostty)", () => { - expect(resolveEditKey({ ctrl: true, name: "u", sequence: "\x15" })).toBe("delete-line"); - expect(resolveEditKey({ ctrl: true, name: "w", sequence: "\x17" })).toBe("delete-word"); - expect(resolveEditKey({ meta: true, name: "backspace", sequence: "\x1b\x7f" })).toBe("delete-word"); - }); - - test("Option arrows move by word", () => { - expect(resolveEditKey({ meta: true, name: "left", sequence: "\x1b[1;3D" })).toBe("word-left"); - expect(resolveEditKey({ meta: true, name: "right", sequence: "\x1b[1;3C" })).toBe("word-right"); - expect(resolveEditKey({ meta: true, name: "b", sequence: "\x1bb" })).toBe("word-left"); - expect(resolveEditKey({ meta: true, name: "f", sequence: "\x1bf" })).toBe("word-right"); - }); - - test("Command arrows move to line edges", () => { - expect(resolveEditKey({ meta: true, name: "left", sequence: "\x1b[1;9D" })).toBe("line-start"); - expect(resolveEditKey({ meta: true, name: "right", sequence: "\x1b[1;9C" })).toBe("line-end"); - }); - - test("plain keys are left to the editor", () => { - expect(resolveEditKey({ name: "left", sequence: "\x1b[D" })).toBeNull(); - expect(resolveEditKey({ name: "backspace", sequence: "\x7f" })).toBeNull(); - expect(resolveEditKey({ name: "a", sequence: "a" })).toBeNull(); - }); -}); diff --git a/test/export-archive.test.ts b/test/export-archive.test.ts deleted file mode 100644 index b9c1f20..0000000 --- a/test/export-archive.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { copyAndArchiveAnnotations } from "../src/archive-workflow"; -import { copyArchiveReport } from "../src/export-archive"; -import type { Annotation, ArchivedAnnotationSet } from "../src/types"; - -function annotation(id: string): Annotation { - return { - id, - selectedText: `selection ${id}`, - comment: `comment ${id}`, - capturedAt: "2026-08-08T00:00:00Z", - createdAt: "2026-08-08T00:00:01Z", - context: {}, - }; -} - -interface Dependencies { - readonly active?: readonly Annotation[]; - readonly loadMessage?: string; - readonly clipboardMessage?: string; - readonly saveMessage?: string; - readonly removeMessage?: string; -} - -/** Drive the workflow with mocked dependencies, then report it like `src/export-archive.ts` does. */ -function report(dependencies: Dependencies) { - const active = dependencies.active ?? []; - let loadedEmpty = false; - const outcome = copyAndArchiveAnnotations({ - loadActive: () => { - if (dependencies.loadMessage) return { ok: false, message: dependencies.loadMessage }; - if (active.length === 0) loadedEmpty = true; - return { ok: true, value: [...active] }; - }, - writeClipboard: () => - dependencies.clipboardMessage - ? { ok: false, message: dependencies.clipboardMessage } - : { ok: true, value: undefined }, - saveArchive: (_archive: ArchivedAnnotationSet) => - dependencies.saveMessage - ? { ok: false, message: dependencies.saveMessage } - : { ok: true, value: undefined }, - removeActive: () => - dependencies.removeMessage - ? { ok: false, message: dependencies.removeMessage } - : { ok: true, value: undefined }, - createArchiveId: () => "archive-one", - now: () => "2026-08-26T23:32:00.000Z", - }); - return copyArchiveReport(outcome, loadedEmpty); -} - -describe("copy-archive action reporting", () => { - test("an empty store reports nothing to copy and succeeds", () => { - expect(report({ active: [] })).toEqual({ - title: "No annotations", - body: "There is nothing to copy yet.", - failure: false, - }); - }); - - test("a complete archive reports the singular and plural counts", () => { - expect(report({ active: [annotation("one")] })).toEqual({ - title: "Annotations copied and archived", - body: "1 annotation copied as Markdown and archived.", - failure: false, - }); - expect(report({ active: [annotation("one"), annotation("two")] })).toEqual({ - title: "Annotations copied and archived", - body: "2 annotations copied as Markdown and archived.", - failure: false, - }); - }); - - test("a failed clipboard write fails with the clipboard message", () => { - expect( - report({ active: [annotation("one")], clipboardMessage: "clipboard write failed" }), - ).toEqual({ - title: "Copy and archive failed", - body: "clipboard write failed", - failure: true, - }); - }); - - test("a failed load or save fails without the empty-store wording", () => { - expect(report({ loadMessage: "annotations.jsonl is unreadable" })).toEqual({ - title: "Copy and archive failed", - body: "annotations.jsonl is unreadable", - failure: true, - }); - expect(report({ active: [annotation("one")], saveMessage: "archives.jsonl is busy" })).toEqual({ - title: "Copy and archive failed", - body: "archives.jsonl is busy", - failure: true, - }); - }); - - test("retained active annotations report the partial archive and fail", () => { - expect( - report({ active: [annotation("one")], removeMessage: "store is busy" }), - ).toEqual({ - title: "Copy and archive incomplete", - body: "Copied and archived, but active annotations remain: store is busy", - failure: true, - }); - }); -}); diff --git a/test/format.test.ts b/test/format.test.ts deleted file mode 100644 index 476e599..0000000 --- a/test/format.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { formatAnnotations, sanitizeTerminalText, wrapText } from "../src/format"; -import { parseAnnotation, parsePendingAnnotation } from "../src/types"; - -describe("wrapText", () => { - test("wraps and preserves explicit newlines", () => { - expect(wrapText("abcdef\nxy", 3)).toEqual(["abc", "def", "xy"]); - }); -}); - -test("terminal display strips control characters", () => { - expect(sanitizeTerminalText("safe\u001b[2J\ttext\nnext")).toBe("safe[2J text\nnext"); -}); - -describe("persisted annotation parsing", () => { - test("rejects malformed boundary input", () => { - expect(parsePendingAnnotation({ selectedText: 42 })).toBeUndefined(); - expect(parseAnnotation({ selectedText: "text", capturedAt: "now", context: {} })).toBeUndefined(); - }); -}); - -describe("formatAnnotations", () => { - test("formats source, selection, and comment as agent context", () => { - const output = formatAnnotations([ - { - id: "one", - selectedText: "failed to connect", - comment: "Check the database first.", - capturedAt: "2026-08-08T00:00:00Z", - createdAt: "2026-08-08T00:00:01Z", - context: { workspace_label: "api", tab_label: "server" }, - }, - ]); - expect(output).toContain("# Annotated context"); - expect(output).toContain("Source: api / server"); - expect(output).toContain("failed to connect"); - expect(output).toContain("Check the database first."); - }); - - test("uses a longer fence when the selection contains backticks", () => { - const output = formatAnnotations([ - { - id: "one", - selectedText: "```example```", - comment: "Fence safely.", - capturedAt: "2026-08-08T00:00:00Z", - createdAt: "2026-08-08T00:00:01Z", - context: {}, - }, - ]); - expect(output).toContain("````\n```example```\n````"); - }); -}); - -describe("wrapText with wide characters", () => { - test("wraps on cells so lines fit the box", () => { - expect(wrapText("한글한글", 4)).toEqual(["한글", "한글"]); - }); - - test("never splits a wide character across lines", () => { - expect(wrapText("한글한", 5)).toEqual(["한글", "한"]); - }); - - test("mixes narrow and wide characters", () => { - expect(wrapText("a한b한", 4)).toEqual(["a한b", "한"]); - }); -}); diff --git a/test/handoff.test.ts b/test/handoff.test.ts deleted file mode 100644 index 49818d3..0000000 --- a/test/handoff.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { HANDOFF_MAX_AGE_MS, handoffPath, takeHandoff } from "../src/handoff"; - -function tempFile(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "herdr-annotate-handoff-")); - return path.join(dir, "selection"); -} - -describe("handoffPath", () => { - test("prefers XDG_RUNTIME_DIR and is per user", () => { - const p = handoffPath({ XDG_RUNTIME_DIR: "/run/user/1000" }); - expect(p.startsWith(path.join("/run/user/1000", "herdr-annotate-"))).toBe(true); - expect(path.basename(p)).toBe("selection"); - }); - test("falls back to the temp dir", () => { - expect(handoffPath({}).startsWith(os.tmpdir())).toBe(true); - }); -}); - -describe("takeHandoff", () => { - test("returns a fresh file's text and removes it", () => { - const file = tempFile(); - fs.writeFileSync(file, "hello\nworld\n"); - expect(takeHandoff(file)).toBe("hello\nworld\n"); - expect(fs.existsSync(file)).toBe(false); - }); - test("ignores and removes a stale file", () => { - const file = tempFile(); - fs.writeFileSync(file, "old"); - const later = Date.now() + HANDOFF_MAX_AGE_MS + 1000; - expect(takeHandoff(file, later)).toBeUndefined(); - expect(fs.existsSync(file)).toBe(false); - }); - test("ignores blank text and a missing file", () => { - const file = tempFile(); - fs.writeFileSync(file, " \n"); - expect(takeHandoff(file)).toBeUndefined(); - expect(takeHandoff(file)).toBeUndefined(); - }); -}); diff --git a/test/layout.test.ts b/test/layout.test.ts deleted file mode 100644 index 77be267..0000000 --- a/test/layout.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { layoutComment } from "../src/layout"; - -const chars = (text: string): string[] => Array.from(text); - -describe("layoutComment", () => { - test("reports the cursor column in cells, not characters", () => { - const comment = chars("한글"); - expect(layoutComment(comment, comment.length, 40).cursorCol).toBe(4); - }); - - test("counts narrow characters as one cell", () => { - const comment = chars("abc"); - expect(layoutComment(comment, comment.length, 40).cursorCol).toBe(3); - }); - - test("mixes narrow and wide characters", () => { - const comment = chars("a한b"); - expect(layoutComment(comment, comment.length, 40).cursorCol).toBe(4); - expect(layoutComment(comment, 2, 40).cursorCol).toBe(3); - }); - - test("never lets a wide character straddle the right edge", () => { - const comment = chars("한글한"); - const result = layoutComment(comment, comment.length, 5); - expect(result.lines).toEqual(["한글", "한"]); - expect(result.cursorRow).toBe(1); - expect(result.cursorCol).toBe(2); - }); - - test("keeps explicit newlines", () => { - const comment = chars("한\n글"); - const result = layoutComment(comment, comment.length, 40); - expect(result.lines).toEqual(["한", "글"]); - expect(result.cursorRow).toBe(1); - expect(result.cursorCol).toBe(2); - }); -}); diff --git a/test/manager-copy.test.ts b/test/manager-copy.test.ts deleted file mode 100644 index 0f732d3..0000000 --- a/test/manager-copy.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { copyAnnotations } from "../src/manager-copy"; -import type { Annotation } from "../src/types"; - -function annotation(id: string): Annotation { - return { - id, - selectedText: `selection ${id}`, - comment: `comment ${id}`, - capturedAt: "2026-08-08T00:00:00Z", - createdAt: "2026-08-08T00:00:01Z", - context: {}, - }; -} - -describe("annotation manager copying", () => { - test("closes after a successful copy without changing annotations", () => { - const annotations = [annotation("one"), annotation("two")]; - const original = structuredClone(annotations); - let clipboardText = ""; - - const outcome = copyAnnotations(annotations, (text) => { - clipboardText = text; - return { ok: true, value: undefined }; - }); - - expect(outcome).toEqual({ _tag: "close" }); - expect(clipboardText).toContain("selection one"); - expect(clipboardText).toContain("selection two"); - expect(annotations).toEqual(original); - }); - - test("stays open and reports a clipboard failure", () => { - const outcome = copyAnnotations([annotation("one")], () => ({ - ok: false, - message: "Clipboard unavailable", - })); - - expect(outcome).toEqual({ _tag: "stay_open", message: "Clipboard unavailable" }); - }); - - test("stays open when there is nothing to copy", () => { - let writes = 0; - const outcome = copyAnnotations([], () => { - writes += 1; - return { ok: true, value: undefined }; - }); - - expect(outcome).toEqual({ _tag: "stay_open", message: "Nothing to copy." }); - expect(writes).toBe(0); - }); -}); diff --git a/test/pane-clipboard.test.ts b/test/pane-clipboard.test.ts deleted file mode 100644 index 3b9160b..0000000 --- a/test/pane-clipboard.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - OSC52_COMMON_PAYLOAD_LIMIT_BYTES, - exceedsCommonOsc52Limit, - osc52ClipboardSequence, - paneClipboardWriter, -} from "../src/pane-clipboard"; - -describe("OSC 52 encoding", () => { - test("emits the exact bytes for plain text", () => { - const sequence = osc52ClipboardSequence("hi"); - - expect(sequence).toBe("\x1b]52;c;aGk=\x07"); - expect([...Buffer.from(sequence, "utf8")]).toEqual([ - 0x1b, 0x5d, 0x35, 0x32, 0x3b, 0x63, 0x3b, 0x61, 0x47, 0x6b, 0x3d, 0x07, - ]); - }); - - test("emits an empty payload for an empty string", () => { - expect(osc52ClipboardSequence("")).toBe("\x1b]52;c;\x07"); - }); - - test("encodes the raw UTF-8 bytes of multi-byte text", () => { - expect(osc52ClipboardSequence("한글 · é")).toBe("\x1b]52;c;7ZWc6riAIMK3IMOp\x07"); - expect(osc52ClipboardSequence("한글 · é")).toBe( - `\x1b]52;c;${Buffer.from("한글 · é", "utf8").toString("base64")}\x07`, - ); - }); - - test("reports oversized payloads without truncating them", () => { - const text = "a".repeat(OSC52_COMMON_PAYLOAD_LIMIT_BYTES); - - expect(exceedsCommonOsc52Limit("hi")).toBe(false); - expect(exceedsCommonOsc52Limit(text)).toBe(true); - expect(osc52ClipboardSequence(text)).toContain(Buffer.from(text, "utf8").toString("base64")); - }); -}); - -describe("pane clipboard writer", () => { - test("emits the sequence after a successful native write", () => { - const order: string[] = []; - const write = paneClipboardWriter( - (text) => { - order.push(`native:${text}`); - return { ok: true, value: undefined }; - }, - (sequence) => { - order.push(`emit:${sequence}`); - return true; - }, - ); - - expect(write("hi")).toEqual({ ok: true, value: undefined }); - expect(order).toEqual(["native:hi", "emit:\x1b]52;c;aGk=\x07"]); - }); - - test("a copy the terminal received succeeds even when the native write failed", () => { - const emitted: string[] = []; - const write = paneClipboardWriter( - () => ({ ok: false, message: "No supported clipboard writer is available" }), - (sequence) => { - emitted.push(sequence); - return true; - }, - ); - - expect(write("hi")).toEqual({ ok: true, value: undefined }); - expect(emitted).toEqual(["\x1b]52;c;aGk=\x07"]); - }); - - test("a copy that reached neither destination fails with the native error", () => { - const write = paneClipboardWriter( - () => ({ ok: false, message: "No supported clipboard writer is available" }), - () => false, - ); - - expect(write("hi")).toEqual({ - ok: false, - message: "No supported clipboard writer is available", - }); - }); -}); diff --git a/test/paths.test.ts b/test/paths.test.ts deleted file mode 100644 index f68f806..0000000 --- a/test/paths.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { normalizeWindowsPath, pluginRoot } from "../src/paths"; - -describe("normalizeWindowsPath", () => { - test("removes an extended drive-path prefix", () => { - expect(normalizeWindowsPath(String.raw`\\?\C:\foo`)).toBe(String.raw`C:\foo`); - }); - - test("converts an extended UNC path to an ordinary UNC path", () => { - expect(normalizeWindowsPath(String.raw`\\?\UNC\server\share\foo`)).toBe( - String.raw`\\server\share\foo`, - ); - }); - - test("leaves ordinary Windows and Unix paths unchanged", () => { - expect(normalizeWindowsPath(String.raw`C:\foo`)).toBe(String.raw`C:\foo`); - expect(normalizeWindowsPath("/home/user/plugin")).toBe("/home/user/plugin"); - }); - - test("handles empty and missing values", () => { - expect(normalizeWindowsPath("")).toBe(""); - expect(normalizeWindowsPath(undefined)).toBeUndefined(); - }); - - test("also handles slash-separated extended paths", () => { - expect(normalizeWindowsPath("//?/C:/foo")).toBe("C:/foo"); - expect(normalizeWindowsPath("//?/UNC/server/share/foo")).toBe("//server/share/foo"); - }); -}); - -describe("pluginRoot", () => { - test("returns an unset value as undefined and normalizes an extended root", () => { - const original = process.env.HERDR_PLUGIN_ROOT; - try { - delete process.env.HERDR_PLUGIN_ROOT; - expect(pluginRoot()).toBeUndefined(); - process.env.HERDR_PLUGIN_ROOT = String.raw`\\?\C:\plugin`; - expect(pluginRoot()).toBe(String.raw`C:\plugin`); - } finally { - if (original === undefined) delete process.env.HERDR_PLUGIN_ROOT; - else process.env.HERDR_PLUGIN_ROOT = original; - } - }); -}); diff --git a/test/store.test.ts b/test/store.test.ts deleted file mode 100644 index e982720..0000000 --- a/test/store.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { - appendAnnotation, - appendArchivedSet, - loadAnnotations, - loadArchivedSets, - mergeAnnotations, - newestFirstAnnotations, - newestFirstArchivedSets, - removeAnnotationsById, - removeArchivedSet, -} from "../src/store"; -import type { Annotation, ArchivedAnnotationSet } from "../src/types"; - -const directories: string[] = []; - -function temporaryDirectory(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "herdr-annotate-store-")); - directories.push(dir); - return dir; -} - -function annotation(id: string): Annotation { - return { - id, - selectedText: `selection ${id}`, - comment: `comment ${id}`, - capturedAt: "2026-08-08T00:00:00Z", - createdAt: "2026-08-08T00:00:01Z", - context: {}, - }; -} - -function archive(id: string, annotationIds: readonly string[]): ArchivedAnnotationSet { - return { - version: 1, - id, - archivedAt: `2026-08-26T23:32:0${id.length}Z`, - annotations: annotationIds.map(annotation), - }; -} - -afterEach(() => { - for (const dir of directories.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); -}); - -describe("annotation store", () => { - test("presents the newest appended annotation first without mutating storage order", () => { - const stored = [annotation("one"), annotation("two"), annotation("three")]; - expect(newestFirstAnnotations(stored).map(({ id }) => id)).toEqual(["three", "two", "one"]); - expect(stored.map(({ id }) => id)).toEqual(["one", "two", "three"]); - }); - - test("appends and loads records", () => { - const dir = temporaryDirectory(); - expect(appendAnnotation(dir, annotation("one")).ok).toBeTrue(); - expect(appendAnnotation(dir, annotation("two")).ok).toBeTrue(); - const loaded = loadAnnotations(dir); - expect(loaded.ok && loaded.value.map(({ id }) => id)).toEqual(["one", "two"]); - }); - - test("removes only the requested annotation IDs", () => { - const dir = temporaryDirectory(); - appendAnnotation(dir, annotation("one")); - appendAnnotation(dir, annotation("two")); - appendAnnotation(dir, annotation("three")); - - expect(removeAnnotationsById(dir, ["one", "three"]).ok).toBeTrue(); - const loaded = loadAnnotations(dir); - expect(loaded.ok && loaded.value.map(({ id }) => id)).toEqual(["two"]); - expect(removeAnnotationsById(dir, ["two"]).ok).toBeTrue(); - const cleared = loadAnnotations(dir); - expect(cleared.ok && cleared.value).toEqual([]); - }); - - test("merges restored annotations without duplicate IDs", () => { - const dir = temporaryDirectory(); - appendAnnotation(dir, annotation("one")); - - const merged = mergeAnnotations(dir, [annotation("one"), annotation("two")]); - expect(merged).toEqual({ ok: true, value: 1 }); - expect(loadAnnotations(dir)).toEqual({ - ok: true, - value: [annotation("one"), annotation("two")], - }); - - expect(removeAnnotationsById(dir, ["one"]).ok).toBeTrue(); - expect(loadAnnotations(dir)).toEqual({ ok: true, value: [annotation("two")] }); - }); - - test("rejects malformed active data instead of dropping it", () => { - const dir = temporaryDirectory(); - fs.writeFileSync(path.join(dir, "annotations.jsonl"), "{broken\n"); - expect(loadAnnotations(dir)).toEqual({ - ok: false, - message: "Unable to read annotations (invalid data)", - }); - }); - - test("fails safely on contention and recovers an abandoned lock", () => { - const dir = temporaryDirectory(); - const lock = path.join(dir, ".annotations.lock"); - fs.mkdirSync(lock); - - expect(appendAnnotation(dir, annotation("one"))).toEqual({ - ok: false, - message: "Annotations are busy; try again.", - }); - expect(loadAnnotations(dir)).toEqual({ - ok: false, - message: "Annotations are busy; try again.", - }); - - const stale = new Date(Date.now() - 31_000); - fs.utimesSync(lock, stale, stale); - expect(appendAnnotation(dir, annotation("one")).ok).toBeTrue(); - expect(loadAnnotations(dir)).toEqual({ ok: true, value: [annotation("one")] }); - }); -}); - -describe("archive store", () => { - test("persists complete sets and presents the newest archive first", () => { - const dir = temporaryDirectory(); - const first = archive("one", ["annotation-one"]); - const second = archive("two", ["annotation-two", "annotation-three"]); - - expect(appendArchivedSet(dir, first).ok).toBeTrue(); - expect(appendArchivedSet(dir, second).ok).toBeTrue(); - const loaded = loadArchivedSets(dir); - expect(loaded).toEqual({ ok: true, value: [first, second] }); - expect(loaded.ok && newestFirstArchivedSets(loaded.value).map(({ id }) => id)).toEqual([ - "two", - "one", - ]); - }); - - test("permanently removes only the selected archived set", () => { - const dir = temporaryDirectory(); - appendArchivedSet(dir, archive("one", ["annotation-one"])); - appendArchivedSet(dir, archive("two", ["annotation-two"])); - - expect(removeArchivedSet(dir, "one").ok).toBeTrue(); - const loaded = loadArchivedSets(dir); - expect(loaded.ok && loaded.value.map(({ id }) => id)).toEqual(["two"]); - }); - - test("rejects malformed archive data instead of partially loading it", () => { - const dir = temporaryDirectory(); - fs.writeFileSync(path.join(dir, "archives.jsonl"), "{broken\n"); - expect(loadArchivedSets(dir)).toEqual({ - ok: false, - message: "Unable to read archives (invalid data)", - }); - }); -}); diff --git a/test/types.test.ts b/test/types.test.ts deleted file mode 100644 index ba1e0f8..0000000 --- a/test/types.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - parseArchivedAnnotationSet, - pendingAnnotationFromInvocation, - selectedTextFromInvocation, -} from "../src/types"; - -function persistedAnnotation(id: string): Record<string, unknown> { - return { - id, - selectedText: `selection ${id}`, - comment: `comment ${id}`, - capturedAt: "2026-08-08T00:00:00Z", - createdAt: "2026-08-08T00:00:01Z", - context: {}, - }; -} - -describe("selectedTextFromInvocation", () => { - test("returns Herdr's terminal selection without changing it", () => { - expect(selectedTextFromInvocation({ selected_text: " selected text\n" })).toBe( - " selected text\n", - ); - }); - - test("ignores missing, invalid, and empty selections", () => { - expect(selectedTextFromInvocation({})).toBeUndefined(); - expect(selectedTextFromInvocation({ selected_text: 42 })).toBeUndefined(); - expect(selectedTextFromInvocation({ selected_text: " \n\t" })).toBeUndefined(); - expect(selectedTextFromInvocation(null)).toBeUndefined(); - }); -}); - -describe("pendingAnnotationFromInvocation", () => { - test("builds the editor fallback with invocation context", () => { - expect( - pendingAnnotationFromInvocation( - { - selected_text: "selected text", - workspace_id: "workspace-1", - focused_pane_cwd: "C:\\work", - }, - "2026-08-27T00:00:00Z", - ), - ).toEqual({ - selectedText: "selected text", - context: { - workspace_id: "workspace-1", - focused_pane_cwd: "C:\\work", - }, - capturedAt: "2026-08-27T00:00:00Z", - }); - }); - - test("returns undefined for a missing or empty invocation selection", () => { - expect(pendingAnnotationFromInvocation({}, "2026-08-27T00:00:00Z")).toBeUndefined(); - expect( - pendingAnnotationFromInvocation({ selected_text: " \n" }, "2026-08-27T00:00:00Z"), - ).toBeUndefined(); - }); -}); - -describe("parseArchivedAnnotationSet", () => { - test("parses a complete versioned archive", () => { - const parsed = parseArchivedAnnotationSet({ - version: 1, - id: "archive-one", - archivedAt: "2026-08-26T23:32:00Z", - annotations: [persistedAnnotation("one")], - }); - - expect(parsed?.id).toBe("archive-one"); - expect(parsed?.annotations.map(({ id }) => id)).toEqual(["one"]); - }); - - test("rejects empty, partial, and unknown archive versions", () => { - expect( - parseArchivedAnnotationSet({ - version: 1, - id: "empty", - archivedAt: "2026-08-26T23:32:00Z", - annotations: [], - }), - ).toBeUndefined(); - expect( - parseArchivedAnnotationSet({ - version: 1, - id: "partial", - archivedAt: "2026-08-26T23:32:00Z", - annotations: [persistedAnnotation("one"), { id: "broken" }], - }), - ).toBeUndefined(); - expect( - parseArchivedAnnotationSet({ - version: 2, - id: "future", - archivedAt: "2026-08-26T23:32:00Z", - annotations: [persistedAnnotation("one")], - }), - ).toBeUndefined(); - }); -}); diff --git a/test/width.test.ts b/test/width.test.ts deleted file mode 100644 index 06ef5a5..0000000 --- a/test/width.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { charWidth, stringWidth, truncateToWidth } from "../src/width"; - -describe("charWidth", () => { - test("gives Hangul syllables two cells", () => { - expect(charWidth("한")).toBe(2); - expect(charWidth("ㄱ")).toBe(2); - }); - - test("gives CJK and fullwidth forms two cells", () => { - expect(charWidth("漢")).toBe(2); - expect(charWidth("あ")).toBe(2); - expect(charWidth("A")).toBe(2); - }); - - test("gives Latin and punctuation one cell", () => { - expect(charWidth("a")).toBe(1); - expect(charWidth("·")).toBe(1); - }); - - test("gives control characters no cells", () => { - expect(charWidth("\u0007")).toBe(0); - }); -}); - -describe("stringWidth", () => { - test("adds the cells of each character", () => { - expect(stringWidth("한글abc")).toBe(7); - expect(stringWidth("")).toBe(0); - }); -}); - -describe("truncateToWidth", () => { - test("never splits a wide character", () => { - expect(truncateToWidth("한글", 3)).toBe("한"); - expect(truncateToWidth("한글", 4)).toBe("한글"); - }); - - test("returns nothing when no cells are available", () => { - expect(truncateToWidth("한", 0)).toBe(""); - }); -}); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 2353c80..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "noEmit": true, - "types": ["bun"] - }, - "include": ["src/**/*.ts", "test/**/*.ts"] -}