Files

10 KiB

Session handoff — 2026-06-02 (55)

Fifty-fifth handover. One issue resolved: #11 — copy the output panel to the system clipboard. New ADR-0041; new copy app-command; first native-clipboard dependency (arboard). The session is a clean run of the full phased method: all four design axes escalated to and decided by the user (twice — a first round of four, then a second round of two once a hard technical constraint surfaced), /runda on the design before any code, test-first build, full-stack PTY verification at the end.

§1. State at handoff

Branch: main. HEAD 1ea376b (handoff-54). 3 commits ahead of origin/main at session start — the three session-54 commits (4cd574b, 516848f, 1ea376b); handoff-53's #15/#16 and earlier are now pushed (origin advanced since handoff-54 was written).

This session's #11 work is committed on top (see the commit that accompanies this handoff). Tests: 2151 passing / 0 failing / 1 ignored (full cargo test; lib-only 1578). Baseline at session start was 2134 / 0 / 1+17 new tests, no regressions, no new skips. The 1 ignored is the same long-standing ```ignore doctest in src/friendly/mod.rs. Clippy: clean (nursery, all targets, -D warnings). Push is the user's step.

§2. #11 — copy the output panel to the clipboard (ADR-0041)

Friction removed: filing a bug report meant terminal-selecting the output panel and fighting wrapping/borders. Now: a typed command.

Command (app-level, sigil-free, both modes — ADR-0003 registry):

Form Copies
copy, copy all the whole output panel
copy last from the most recent echo line to the end

copy last boundary is fuzzy by design: app commands push echo-less [system] lines, so "from the last echo" reaches past them to the previous DSL command — documented + accepted in ADR-0041.

Mechanism — OSC 52 and native arboard, ALWAYS BOTH. The deciding constraint (surfaced in escalation round 2): OSC 52 acceptance is undetectable — the terminal sends no ack — so a literal "fall back when OSC 52 is unsupported" cannot be built. So both fire every time: emit the OSC 52 escape (ESC ]52;c;<base64>BEL; tmux-passthrough wrapped when $TMUX set), then a best-effort native write whose failure is ignored (headless host → OSC 52 carried it). Identical content, so the local double-write is harmless. OSC 52 needs no new dep (base64 + crossterm already present); it works over SSH.

Format — plain text verbatim as rendered: each line's [tag] body ✓/✗ joined by \n, without the viewport's right-edge padding or soft-wrapping (full logical lines, reflow-friendly, no trailing whitespace). A drift-lock test (ui.rs plain_text_matches_rendered_line_content) pins OutputLine:: plain_text to the concatenated span content of render_output_line, so the copy can never silently diverge from the screen.

Files (6 new, 18 modified):

  • src/clipboard.rs (new) — pure osc52_sequence / emit_osc52 / in_tmux, the NativeClipboard trait + deliver, and the SystemClipboard arboard impl (lazy-init, kept alive — X11 serves the selection from a thread owned by the handle).
  • src/action.rsAction::CopyToClipboard(String).
  • src/app.rsOutputLine::plain_text, App::copy_text, handle_copy_command, dispatch arm + 6 Tier-1 tests.
  • src/dsl/command.rs / dsl/mod.rsAppCommand::Copy { scope } + CopyScope { All, Last } + re-export + source mapping.
  • src/dsl/grammar/app.rs + grammar/mod.rsCOPY CommandNode (copy [all|last], unknown→copy.unknown), REGISTRY entry.
  • src/runtime.rs — long-lived SystemClipboard + the Action::CopyToClipboard arm (thin call to clipboard::deliver).
  • src/ui.rs — the drift-lock test.
  • src/friendly/strings/en-US.yaml + friendly/keys.rscopy.done/copy.nothing/copy.unknown, help.app.copy, parse.usage.copy, CLI --help banner line.
  • src/completion.rscopy added to the app-keyword test list (the candidate itself is REGISTRY-driven).
  • tests/typing_surface/{mod,app_commands}.rs + 4 new snapshots.
  • Cargo.toml / Cargo.lockarboard --no-default-features.
  • docs/adr/0041-* (new), docs/adr/0003-* (registry note), docs/adr/README.md (index + 0003 note), docs/requirements.md (new V6, done).

Coverage (17): clipboard module 6 (exact OSC 52 bytes, base64 round-trip incl. non-ASCII, tmux wrap, deliver both-paths + writer-error-still-fires-native); app 6 (all / bare-=-all / last-slice / empty-nothing / no-echo-nothing / unknown-target-friendly-error); drift-lock 1; typing-surface 4 (copy / copy all / copy last parse, copy offers all+last).

Full-stack PTY verification (the real binary, not a mock): drove target/debug/rdbms-playground through a pseudo-terminal, typed create table … then copy, and confirmed the OSC 52 escape reached the tty with a base64 payload decoding to the exact on-screen content (echo + + [system] structure + box-drawing table). This is the end-to-end proof the unit seams don't give on their own.

§3. The arboard decision — current state (READ if revisiting)

arboard 3.6.1 (1Password, MIT OR Apache-2.0), added --no-default-features. Two deliberate choices are flagged to the user as open-for-correction — they were made on low-stakes, principle-aligned grounds and explicitly surfaced, not silently decided:

  1. No wayland-data-control feature → X11-only native on Linux. That feature ~doubles the dep tree (~30 crates: wl-clipboard-rs, wayland-*, quick-xml, nom, petgraph). Since "always do both" makes OSC 52 the universal path (covers native-Wayland regardless) and most Wayland desktops run XWayland (so plain x11rb works), the feature was omitted to keep the dependency surface minimal (secure-by-default). If a concrete native-Wayland-without-OSC-52 need appears, add the feature — one line in Cargo.toml.

  2. Copy is verbatim on-screen, including the per-line [system] tags (the PTY payload shows [system] ┌──…). This is the literal reading of the user's "exactly what's rendered" choice. If tag stripping on table rows is wanted, that's a plain_text change (+ re-baseline the drift-lock test).

Security posture (re-run before any dep bump): arboard introduces zero new vulnerabilitiescargo audit, osv-scanner, and grype are all clean for arboard's tree (log / parking_lot / percent-encoding / x11rb, with gethostname/rustix already ours). Write-only (no OSC 52 read — the exfiltration vector is out of play). gitleaks + trivy clean on the new code.

Pre-existing, orthogonal, FLAGGED not fixed: osv-scanner / cargo audit report RUSTSEC-2025-0067 (libyml) and RUSTSEC-2025-0068 (serde_yml). These are a pre-existing direct dependency (Cargo.toml serde_yml = "0.0.12"libyml), not pulled by arboard (cargo tree -i serde_yml shows only rdbms-playground as parent), known since handoff-14. cargo audit treats them as allowed warnings (exit 0). Out of scope for #11; fixing means migrating off serde_yml (the YAML lib behind project.yaml) — its own task, needs user sign-off.

§4. Process notes (what worked)

  • Escalate genuine forks — don't decide. Six questions across two rounds. Round 1: mechanism / scope / trigger / format. Round 2 was forced by honesty — the chosen "OSC 52 + native fallback" is not literally buildable (no ack), so the coordination had to be re-decided (→ "always do both") and the trigger confirmed. Surfacing the constraint beat shipping a thing that didn't match the label.
  • /runda on the design. The DA pass on ADR-0041 found: the copy last boundary fuzziness (→ documented), the "exactly on screen" overclaim (→ reworded to "as rendered, not padded"), and the full-stack-coverage obligation (→ deliver both-paths test + PTY run). Folded in before any code.
  • Test-first + drift-lock. The renderer composes the visible line at render time (tag, running:✓/✗, teaching prefix), so copying raw OutputLine.text would be wrong; plain_text mirrors the renderer and the drift-lock test guarantees they stay in step.
  • Verify by running the app. The PTY harness proved the seam the unit tests can't (OSC 52 bytes actually reaching a real tty).

§5. What's open

Bug reports / enhancements

#11 is resolved this session. The handoff-50 bug-list trio (#10/#11/#14) is now fully closed. No open issues remain on the recent list — confirm on GitHub after push.

Other tracks (from requirements.md)

  • Track 2 Iter 6 leftovers: history.log-based persistent input-history hydration polish (§12), migration-framework exercise.
  • C3a (modify relationship), C4 (m:n convenience).
  • H1 friendly DB-error layer (partial); tutorial/lesson system (needs its own ADR); V4 session-log + Markdown export (the broader output-panel UX — V6/clipboard is a complementary "get the session out" path now shipped); I1/I1b multi-line + readline; I3 tab-completion polish; I4 highlighting beyond input echo.
  • Pre-existing security debt: migrate off serde_yml/libyml (RUSTSEC-2025-0067/0068) — needs user sign-off (§3).

§6. How to take over

  1. Read this note, then CLAUDE.md, then docs/requirements.md (V6 now done), then docs/adr/README.md and any ADR you'll touch (ADR-0041 is new; ADR-0003 gained a registry note).
  2. The codebase is on main; this session's #11 commit sits on top of the 3 unpushed session-54 commits. Closing #11 on GitHub is pending the push (the user's step).
  3. If revisiting the clipboard: §3 has the two open-for-correction arboard decisions (no-Wayland-feature; verbatim-with-tags) and the security posture. Re-run the scanners before any dep bump.
  4. Honour the process pins: escalate genuine forks (and surface technical constraints that contradict a chosen option's label), /runda the design and implementation, test-first with a drift-lock where a copy mirrors a renderer, and verify by running the real app (the PTY harness pattern is reusable for any terminal-side behaviour).