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) — pureosc52_sequence/emit_osc52/in_tmux, theNativeClipboardtrait +deliver, and theSystemClipboardarboard impl (lazy-init, kept alive — X11 serves the selection from a thread owned by the handle).src/action.rs—Action::CopyToClipboard(String).src/app.rs—OutputLine::plain_text,App::copy_text,handle_copy_command, dispatch arm + 6 Tier-1 tests.src/dsl/command.rs/dsl/mod.rs—AppCommand::Copy { scope }+CopyScope { All, Last }+ re-export + source mapping.src/dsl/grammar/app.rs+grammar/mod.rs—COPYCommandNode (copy [all|last], unknown→copy.unknown), REGISTRY entry.src/runtime.rs— long-livedSystemClipboard+ theAction::CopyToClipboardarm (thin call toclipboard::deliver).src/ui.rs— the drift-lock test.src/friendly/strings/en-US.yaml+friendly/keys.rs—copy.done/copy.nothing/copy.unknown,help.app.copy,parse.usage.copy, CLI--helpbanner line.src/completion.rs—copyadded 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.lock—arboard--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:
-
No
wayland-data-controlfeature → 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 plainx11rbworks), 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 inCargo.toml. -
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 aplain_textchange (+ re-baseline the drift-lock test).
Security posture (re-run before any dep bump): arboard introduces
zero new vulnerabilities — cargo 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.
/rundaon the design. The DA pass on ADR-0041 found: thecopy lastboundary fuzziness (→ documented), the "exactly on screen" overclaim (→ reworded to "as rendered, not padded"), and the full-stack-coverage obligation (→deliverboth-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 rawOutputLine.textwould be wrong;plain_textmirrors 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
- Read this note, then
CLAUDE.md, thendocs/requirements.md(V6 now done), thendocs/adr/README.mdand any ADR you'll touch (ADR-0041 is new; ADR-0003 gained a registry note). - 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). - 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.
- Honour the process pins: escalate genuine forks (and surface
technical constraints that contradict a chosen option's label),
/rundathe 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).