feat(automation): reserved ${terminal.*}/${time} tokens and snippet insertion - #93
Merged
rockyway merged 4 commits intoSep 13, 2026
Merged
Conversation
…nsertion in messages
Send to Terminal and Send to Webhook messages can now name the watched terminal
that fired the rule and the moment the send ran, with the same ${name} grammar
$1/${file} already use:
${terminal.id} the tm- id of the source terminal
${terminal.title} the label the tab strip / picker / activity log show ("" if none)
${terminal.cwd} shell-reported OSC cwd, else the process cwd; looked up only
when a substituting message names it, on a blocking worker
${time} local wall-clock when the send runs, YYYY-MM-DD HH:mm:ss
The values come from the pending send's own source terminal (PendingSend.pair.tm
and the decide-time label), never a destination, for immediate, parked and
schedule sends. A pattern's own named group of the same name wins over a reserved
name. One Reserved bag is built per crossing in run_crossing, so both
destinations render the same ${time}. Dry run renders real values; TS and Rust
save validation accept the reserved names (a schedule rule may use them without
a pattern) and still reject ${nope}. The shared grammar fixture gains the rows.
Both message editors get the four chips and an "Insert snippet" button that
opens the existing Snippets flyout (ContextMenu standaloneSubmenu) in a new
pickOnly mode; a picked snippet is spliced verbatim at the caret, the draft is
dirtied through the normal patch, a use is recorded, and the substitute flag is
left alone. The webhook panel now restores focus/caret after an insert like the
action panel does (shared panels/caretInsert.ts). The picker menu sits above the
editor overlay via a ContextMenu className.
Plan 042 (termflow-fabric docs/plan/042-automation-terminal-tokens-and-snippet-insert.md).
… paste-safe values, Custom JSON escaping Architect + spec-vs-implementation audits of 0cc3846, all findings fixed as one class: - `EngineHost::cwd_for` is keyed by the crossing's PROCESS (`send.pair.pc`), not its leaf: `leaf_to_process` is overwritten on every spawn, and `run_webhook` has no restart guard, so a leaf lookup after a Ctrl+R inside a parked wait paired run A's `$1` with run B's directory. AppState resolves through the roster's shared `proc_snapshot` instead of a fresh `System::new_all()` per crossing. Pinned by a webhook test that replaces the process under the same leaf and expects the gone process's cwd to render empty (mutant-checked). - `began` is taken in `run_crossing` before the bag is built, so the echo/settle window still covers the cwd scan; a `JoinError` from the blocking lookup is logged, not swallowed. - `Reserved::time_from_ms` converts UTC→local from `at_ms` with no second clock read; `schedule.rs`'s "only local-time conversion" claim corrected in place. - `Reserved::for_send` scrubs control characters (ESC/CR/LF…) from title and cwd — both can be spelled by a person, an API caller or a directory name, and would close the bracketed paste early and submit the rest. The id and time are ours and untouched. - Custom webhook bodies: substituted values are written as JSON string fragments (`subst::ValueEscape::JsonString`, chosen by `automation_webhook::value_escape`; renderer mirror `webhookValueEscape` feeds the panel preview and the save-time JSON check). A Windows cwd or a quoted title no longer produces a body that saves but posts as invalid JSON. Scope change agreed with Tam; recorded in plan 042. - Picker returns focus to its button on close instead of parking it on <body>. - Tests: caret restore pinned in both panels (chip and snippet paths), outside-click dismissal, JSON-escape unit + engine tests on both sides.
Review 220 (codex sol) found no code defect; every finding was an oracle a
wrong implementation would pass, or a sentence the code did not support.
Each is fixed as a class, at every site of its shape:
- `${time}` was asserted by SHAPE at four sites (subst unit test, the
schedule send, the webhook crossing, the dry run). A fixed timestamp of
the right shape passed all four. Now: the unit test pins the value
against chrono's other local-time path; the engine sends are bracketed
between two clock reads formatted by chrono directly (not by
`time_from_ms`, so the bounds do not move with a mutant); the dry run
asserts the exact rendering of its caller's `now_ms`.
- The Custom-JSON validation test could not tell a validator that
substituted from one that did not — the untouched body is valid JSON
too. The shared sample values now carry a `"` (title) and a `\` (cwd)
in BOTH layers, so only substituted-and-escaped parses, and the tests
read the rendering itself (`rendered_webhook_body`;
`renderWebhookBodyForValidation` exported for its test) and parse it
back to the raw samples.
- The no-pattern exemption was probed with one of four names, in both
validators. Now all four, at both destinations, with the names spelled
out (a loop over the constant would shrink with a trimmed constant);
plus a subst test that `RESERVED_NAMES` and `Reserved::get` agree.
- The TS shadowing test read `{ time: '42' }` through a flat sample map,
which cannot say which path resolved it. It now pins the observable
difference: a declared-but-absent group is empty text, an undeclared
reserved name is a placeholder.
- `build_reserved`'s doc claimed the cwd "a shell was in when it printed
the match"; the lookup runs when the send runs, after any parked delay.
Corrected in place.
Mutants killed and reverted: fixed-string `time_from_ms` (4 tests),
Custom provider rendered raw (Rust + TS), `is_reserved` = terminal.id
only (Rust ×3, TS ×4), TS preview taking the reserved path first.
…real caret after review round 2
Review 221 (codex sol, whole branch, second independent pass) again found
no code defect. Its three oracle findings, fixed as classes:
- The picker test named "opens above the editor" observed only the class.
jsdom cannot paint, so the order is pinned where it is decided: a test
reads both stylesheets and asserts the picker's z-index is greater than
the editor's (z-index 1000 on the picker now fails it).
- The shared scanner fixture had no unterminated `${` row and no unknown
dotted name. Both added; `rendered: null` means "recognised, resolved by
nothing", honoured by both consumers (a scanner that emits a token to end
of input, or a resolver that renders an unknown name as "", fails).
- The caret tests spied on the range the panel ASKED for. The test host now
applies every dispatch through `draftReducer`, and both tests read the
selection the textarea holds after the longer value rendered — mid-text,
because a programmatic value set parks the caret at the end and an insert
at the end could not tell a restored caret from none (a synchronous
restore before the re-render now fails four tests).
Plus the coverage gap round 1 and 2 both named: `AppState::cwd_for` gets
its own tests (OSC answered without a scan; a silent shell falls back to
the scan of the test's own process; unknown process is None), gated on
`integration-tests` like every `mock_app()` test.
rockyway
deleted the
feature/automation-reserved-tokens-snippet-insert
branch
September 13, 2026 19:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plan 042 — reserved message tokens and snippet insertion for automations.
${terminal.id},${terminal.title},${terminal.cwd},${time}are accepted next to$0/$1/${name}in both Send-to-Terminal and Send-to-Webhook messages (opt-insubstituteflag, unchanged). They resolve to the source (watched) terminal of the match — also for parked after-match sends and for schedule sends that have no captures. A pattern's own named group shadows a reserved name; an unknown${x}still errors.subst.rs) mirrored in TS (automationTokens.ts), shared fixtureautomationTokenCases.json. The reserved bag is built once per crossing inrun_crossing(beganis taken before it, so the settle window is not shortened); cwd is process-keyed (EngineHost::cwd_for(pc): OSC-reported cwd first, sharedproc_snapshotscan fallback), so a leaf restarted between match and send yields""rather than the new shell's directory. Title/cwd are scrubbed of control characters before they can reach a paste.ValueEscape::JsonString, mirrored bywebhookValueEscape); preset providers stay raw because they serialise the finished message as a JSON value themselves. Validation, dry-run and the editor preview all know the reserved names (chips in both editors).pickOnlymode and splices the snippet text verbatim at the caret (caretInsert.ts) without sending; dismiss leaves the message unchanged and focus returns to the button.Plan:
termflow-fabric/docs/plan/042-automation-terminal-tokens-and-snippet-insert.md(implementer reports 070/071, handoff 072, external reviews 220/221 in the same repo).Verification
cargo test(src-tauri): 1038 passed ·bun run typecheckclean ·bun run test: 254 suites / 4488 tests ·bun run buildok (pre-existing size warnings only) · clippy: no warnings in touched files.cargo test --features integration-tests --no-runcompiles (the newAppState::cwd_fortests run on the Linux CI leg).5ad6984,f4c3f1f) and mutation-checked — 11 mutants killed and reverted (fixed-string${time}, Custom rendered raw in both layers, one-name exemption in both layers, reserved-path-first preview, picker z-index below the editor, caret restored before the re-render, unterminated${emitting a token in both scanners, unknown dotted name rendering as"").GUI runbook pending
Plan 042 §7 (11 steps; step 7 checks that the picker paints above the editor). Merge after sign-off.
Noted, out of scope
SendTo::All("Every watched terminal") is a dead radio — the engine never readssend_to. Backlog item in plan §9.