Skip to content

feat(macos): on-demand OpenCode rewrite shortcuts (last dictation and selection) - #63

Open
qretsar wants to merge 14 commits into
anomalyco:mainfrom
qretsar:feat/rewrite-last-dictation
Open

qretsar wants to merge 14 commits into
anomalyco:mainfrom
qretsar:feat/rewrite-last-dictation

Conversation

@qretsar

@qretsar qretsar commented Sep 1, 2026

Copy link
Copy Markdown

Closes #62

Summary

Two shortcuts built entirely on the existing OpenCode rewrite machinery, addressing the lost-flow-of-thought problem after accidental or messy captures (#62):

  1. Option-Shift-O, Rewrite last dictation (src/suppression.rs, src/parakeet.rs): re-submits the last completed transcript through Profiles::rewrite_cancellable (existing processor queue: bounded admission, ordered output, Escape cancellation) and pastes the result at the cursor. Always generates with the matching mode's configured model, even when the automatic mode-processing toggle is off. Failures paste nothing. The rewritten output never becomes the new last dictation, so Option-Shift-V keeps returning the original. Duplicate presses cancel-and-replace the in-flight rewrite.
  2. Option-Shift-R, Rewrite selected text: captures the current selection through Accessibility (the same call Voice Action uses) and does the same over the selection. Requires a non-empty selection and OpenCode; failures paste nothing.
  3. Keyboard layout serialization fix (src/keyboard.rs): concurrent TISCopyCurrentKeyboardLayoutInputSource calls abort the whole process (confirmed via a macOS crash report and reproduced by the new tests under parallel execution); TIS access is now serialized behind a lock and the cache is filled on cold misses.

Both shortcuts are user-configurable in Settings with the standard capture UI, conflict validation, and load-time disable when they would shadow an existing binding. History records rewrite outputs under a new Rewrite kind; AGENTS.md gains the matching invariant bullets.

Verification

  • New regression tests: rewrite model gate, generation-while-disabled, missing-transcript rejection, cancel-and-replace duplicate rejection, selection submission, RewriteLast/Selection paste semantics (Standalone, last-dictation untouched, Rewrite history kind), suppression matching, keyboard layout concurrency, Settings conflicts and voice-action interplay
  • Hardware (MacBook Pro M1, Right Command dictation): Option-Shift-O rewrites the last capture in place flow, Option-Shift-R replaces a text selection, Escape cancels cleanly
  • cargo fmt --check
  • cargo test --locked --bin voice-control (one pre-existing unrelated failure: personal_commands::tests::clean_workspace_provisioning_preserves_user_config_on_refresh, fails on clean main in this environment)
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • git diff --check

A note from me

I use HEX every single day and I am grateful for it, it genuinely changed how I work. I do not care about contributor credit at all; the feature is what matters to me. If you would rather design or land this yourselves, or if you would prefer I stop opening PRs on your repository, that is completely fine and there are no hard feelings. Happy to close this and keep using the app either way.

@qretsar
qretsar force-pushed the feat/rewrite-last-dictation branch 2 times, most recently from 8d594f9 to 0f88105 Compare September 3, 2026 20:42

@kitlangton kitlangton left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. On-demand rewriting is useful, and the existing ordered processor/output queue is the right place for it. I found two behavioral blockers before landing it:

  • The output worker publishes state.last_transcript before finish_output pastes. A concurrent Rewrite Last can therefore accept text from a paste that later fails or is cancelled. Restoring the snapshot afterward cannot retract the already accepted rewrite. Please publish the shared last transcript only after successful output, and cover the in-flight paste failure/cancellation case.
  • Both shortcuts default to active on new installs and upgrades, including when OpenCode is absent. Please make these explicitly configured/opt-in so upgrading does not silently reserve two more global shortcuts.

Also, rewrite_generates_even_when_live_processing_is_disabled returns at the initial cancellation check, before profile selection or generation. It does not establish its named behavior; that needs a controlled generation boundary. The new in-process native layout test should use the existing subprocess layout harness rather than introducing process-global layout initialization into the parallel unit suite. Please add the affected feature map as well.

Add an Option-Shift-O shortcut that re-runs the last completed dictation
through OpenCode and pastes the cleaned-up result at the cursor. Unlike
live mode processing, the rewrite always generates with the matching
mode's configured model even when automatic processing is off; failures
paste nothing, the rewritten output never becomes the new last dictation,
and the job flows through the existing processor queue so ordering,
bounded admission, cancellation, and history capture are inherited.

The shortcut is user-configurable from Settings with conflict validation,
disables itself on load when it would shadow an existing binding, and
shows the processing indicator through the standard Submitted -> Stage
pipeline. Also fixes keyboard.rs layout-query concurrency (previous
commit) that the new tests surfaced.

AGENTS.md documents the invariant.
- Serialize HIToolbox keyboard-layout queries behind a lock: concurrent
  TISCopyCurrentKeyboardLayoutInputSource calls abort the process, which
  the new tests hit deterministically under parallel execution.
- Drop the stored processor sender before joining processor workers so
  shutdown cannot block forever on a channel that never disconnects.
- Publish Paste/Send text into the shared last-transcript mirror before
  the paste runs and restore it on failure, so a rewrite submitted while
  a paste is still in flight rewrites the transcript the user just heard.
- Reject a second rewrite while one is already generating instead of
  queueing duplicate generations of the same text.
- Report cancellation before the model gate, cover edit- and
  meeting-conflict cases in tests, and reword the normalized-settings
  log message.
- Surface user cancellation of a rewrite as a clean cancel instead of a
  failed rewrite (error tone) in the processor error path.
- Fill the keyboard layout cache on cold misses so every key_code_for
  caller benefits from the serialized TIS scan.
- Give rewrites their own DictationPhase::Rewritten so the activity feed
  no longer claims the dictation was merely pasted again.
- Cover RewriteLast in the Voice Action takeover test.
Add a user-configurable Option-Shift-R shortcut that captures the
selected text in the focused app (Accessibility, same as Voice Action),
runs it through the on-demand OpenCode rewrite pipeline, and pastes the
result over the selection. Requires a non-empty selection and OpenCode;
failures paste nothing and never touch the last dictation.

Also upgrades RewriteLast to cancel-and-replace: a second press cancels
the in-flight rewrite and starts a fresh one instead of erroring.
Block Paste and Send behind a barrier, call rewrite_last while output is
still running, then finish with success, error, or cancel-before-commit.
Rewrite must enqueue only the previously published transcript in every
case; shared state publishes the new text only after a successful paste.
Move the concurrent key_code_for stress case into keyboard_layout so
process-global HIToolbox init stays out of the parallel unit suite.
Rewrite last/selection stay inactive until assigned. Ordinary Paste and
Send publish the shared last transcript only after successful output.
No migration flag: the feature never shipped, so missing fields default
to None like a fresh install.
Configure runtime hotkeys before matching Option-Shift-O, assert None
bindings are neither matched nor suppressed, and check PasteLast
conflicts only when rewrite is actually saved.
Treat only a committed in-flight rewrite as blocking. Escape or
cancel_latest already marks the job cancelled while OrderedOutputs may
still hold that Cancelled event behind an earlier paste; a second press
must start a fresh rewrite instead of reporting already in progress.
Disable sets the hotkey to None and persists; reassign is via capture,
not a preserved inactive binding.
@qretsar
qretsar force-pushed the feat/rewrite-last-dictation branch from 0f88105 to 1ae046c Compare September 15, 2026 08:21
@qretsar

qretsar commented Sep 15, 2026

Copy link
Copy Markdown
Author

Thanks for the careful review. Both blockers and the test findings are addressed on the branch, which is now rebased on current main.

Publish after successful output. The output worker no longer writes state.last_transcript before the paste runs. The pre-publish and snapshot-restore path is gone; the shared value is written back only after finish_output reports a successful Paste or Send. A rewrite submitted while a paste is still in flight reads the previously published text, so a paste that later fails or gets cancelled can never feed a rewrite. Covered by failed_paste_does_not_publish_last_transcript and rewrite_last_during_in_flight_paste_sees_only_previously_published_text across Paste and Send, success, error, and cancel-before-commit. Testing the cancellation path also surfaced a second-press wedge: after Escape cancelled a buffered rewrite, the guard treated "already cancelled" as "already in progress" and blocked fresh rewrites until the queue drained. That is fixed and covered by rewrite_last_restarts_after_escape_cancels_a_buffered_rewrite (3f88667, c18ade4, f487140).

Opt-in shortcuts. Both rewrite hotkeys now default to None in the serde defaults, AppSettings::default, and the runtime projection. Nothing is reserved until a shortcut is explicitly assigned in Settings, and None stays out of runtime matching, suppression, and conflict normalization. Covered by missing_fields_receive_defaults, rewrite_shortcuts_only_enter_runtime_when_explicitly_saved, and unconfigured_rewrite_shortcuts_are_neither_matched_nor_suppressed. There is no migration flag because the feature has never shipped in a release, so no distributed settings file can carry the old defaults (888a2e9, 1a4b419).

Generation boundary test. Added a rewrite_cancellable_with seam so the test drives profile and model selection down to a fake generation boundary and asserts generation ran while live processing is off, instead of returning at the first cancellation check (7d0585c).

Layout test. Moved into the existing subprocess harness in tests/keyboard_layout.rs as the serialized-queries case; the in-process initialization is gone from the parallel unit suite (3de7390).

Feature map. Added the On-Demand Rewrites section in docs/features/dictation.md and updated the AGENTS.md invariants to state the opt-in and publish-after-success behavior (074ce3d, 1ae046c).

481 tests pass locally; the only failure is the pre-existing personal_commands provisioning environment issue on this machine, unrelated to this PR.

Document that Escape or cancel_latest leaves a cancelled rewrite buffered
until OrderedOutputs drains, and a second press starts a fresh rewrite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

On-demand OpenCode rewrite: shortcut to rewrite the last dictation or the selected text

2 participants