Repository navigation
feat(shortcut): single/double press detection for Ctrl+G - #31
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#30) CopyFromForeground() sleeps ~150ms (Ctrl+C + wait). Capturing on every press ate into the 200ms detection window, leaving only ~50ms margin. Use atomic.Bool to capture only on the first press of each cycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#30) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ble shortcuts (#30) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…gurable shortcuts (#30) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hook (#30) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ents (#30) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ok (#30) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cutPyramidize$, add settings fields (#30) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…corder (#30) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs fixed: - Auto-repeat: track trigger keyup in double-tap state machine. Only accept second tap after the trigger key has been physically released and pressed again (tapWaitRelease → tapWaitSecond phases). - Recording conflict: add SetPaused() to service interface. Recorder component pauses the hook during capture mode so keypresses pass through to the webview instead of being intercepted. - Diagnostic logging: add Debug-level trace logs in hook callback for modifier tracking, tap phases, and combo matching. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FixComponent is only mounted on the /fix route. When the app is hidden or on another route, the shortcutFix$ subscription didn't exist, so the silent fix never ran. Move it to ShellComponent which is always mounted — read clipboard, enhance, write back, paste to foreground. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#30) Outlook uses delayed clipboard rendering — the fixed 150ms sleep wasn't enough for the clipboard to be populated. Now polls GetClipboardSequenceNumber in 25ms intervals up to 1s timeout. Fast apps still finish in ~25-50ms; slow apps like Outlook get the full second. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the shortcut combo includes Shift or Alt (e.g. Ctrl+Shift+G for pyramidize), those modifiers are still physically held when CopyFromForeground sends Ctrl+C. The target app sees Ctrl+Shift+C instead of Ctrl+C, which Outlook and other apps silently ignore. Fix: inject Shift-up and Alt-up events before Ctrl+C (and Ctrl+V) so the receiving app always gets a clean Ctrl+C/Ctrl+V regardless of which modifiers the shortcut combo used. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…wn (#30) Gives the user time to release modifier keys before CopyFromForeground runs, reducing the chance of Ctrl+Shift+C instead of Ctrl+C. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 tasks
Open
3 tasks
With a NULL hWnd, SetTimer ignores the nIDEvent argument and returns a newly generated ID, so KillTimer(NULL, 1) never killed anything and every first tap leaked a periodic timer that kept posting WM_TIMER forever. The stray timer fired a silent fix milliseconds into the next first tap, so a hold-Ctrl double-tap ran fix twice and Pyramidize was unreachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two fix events used to start two concurrent read -> enhance -> write -> paste cycles, and the second paste landed on top of the first result. A guard flag now drops the shortcut while a cycle is running and logs a warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closed
4 tasks
A SetTimer(NULL, ...) timer belongs to the thread that created it, so the KillTimer calls that Unregister, UpdateConfig and SetPaused made from their own goroutines could never kill it — and clearing timerID there dropped the only handle, leaking the periodic timer. Those callers now post wmReset and the message loop does the reset on the pump thread, which also removes the cross-goroutine writes to tapState and indepPending. The config fields stay as they are; they are already guarded by mu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4 tasks
The backend has no HTTP timeout yet (#32), so a hung enhance call would leave silentFixInFlight set and swallow every later shortcut. A 120 s safety timer now releases the guard and logs a warning; it is cleared in the finally block. The timeout is an overridable field so the tests need not wait for it. Also on the Windows side: when SetTimer fails the service now stays in tapWaitRelease with timerID == 0 instead of going idle, so auto-repeat is absorbed rather than firing a fix per repeat, the trigger keyup then starts over instead of arming pyramidize, and the modifier-release path only fires fix while a timer is actually pending so the failure path cannot fire twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pires (#30) When the timer expired while the trigger key was still down, the state machine dropped to tapIdle and every auto-repeat keydown looked like a fresh first tap, so holding the shortcut fired one fix per delay interval. A new tapFiredWaitRelease phase fires the fix once and swallows the trigger key until its keyup, which returns to tapIdle; a modifier release in that phase no longer fires a second fix. The phase is reached from the hook's own keydown/keyup events — tapWaitRelease already means "trigger physically down" — and it also replaces the timerID == 0 marker used on the SetTimer-failure path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0xMMA
added a commit
that referenced
this pull request
Sep 17, 2026
…housekeeping items Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Sep 18, 2026
0xMMA
added a commit
that referenced
this pull request
Sep 18, 2026
…5 blocked by @angular/build peer) (#84) * chore(frontend): upgrade Vitest, Playwright, jsdom and Prettier Refs #35, E4 step 3. devDependencies only — Angular, PrimeNG and TypeScript stay where they are and wait for #31. @playwright/test 1.58.2 -> 1.63.0 jsdom 28.1.0 -> 30.1.0 prettier 3.8.1 -> 3.9.8 vitest 4.0.8 -> 5.0.1 One commit rather than one per tool: no changelog needed adapting. No config key changed, no spec changed, and the three things that looked like they might need work turned out not to. VITEST 5 IS OUTSIDE ANGULAR'S DECLARED PEER RANGE `@angular/build:unit-test` is what `ng test` drives, and it declares vitest as an optional peer at `^4.0.8` — on every 21.2.x and on 22.1.8, the newest published. So this pairing is unsupported upstream, not merely untested, and `npm install` prints `ERESOLVE overriding peer dependency`. Shipped anyway, on evidence rather than optimism: `npm ci` from a clean node_modules resolves it and exits 0, all 14 files and 198 tests pass, the test count is identical to vitest 4, and a deliberately broken assertion still fails one test — so the suite is running and biting, not passing for the wrong reason. The caveat is written into .claude/rules/testing.md next to the runner, because the peer warning in an install log is otherwise the kind of thing someone "fixes" by downgrading the wrong package. PRETTIER 3.9 REFORMATS NOTHING HERE The brief asked for the formatting change to be kept out of the diff if 3.9 moved anything. It does not: 3.8 and 3.9 report the same 30 files under `--list-different`, and the sets are identical. The repo is simply not Prettier-formatted and Prettier is not enforced anywhere, so there is no formatting commit and no .prettierrc pin. JSDOM 30 CHANGED NOTHING WE DEPEND ON `matchMedia`, `ResizeObserver` and `IntersectionObserver` are still undefined — checked twice, once against the library and once with a throwaway spec inside the running test environment, because those are not the same thing. The polyfill rule in .claude/rules/testing.md stands and now says it was verified against 30, so the next bump does not re-derive it. PLAYWRIGHT 1.63 Every config key still applies: 58 passed and 3 skipped, exercising webServer, baseURL, screenshot, trace and the chromium project. One full run of four had `router-leave-settings` fail; it passes 3 of 3 in isolation and 58/58 on the next two full runs. That is the flake class the config's own comment describes and `retries: 1` in CI exists for. Not attributed to 1.63 — establishing that would need the same repetition on 1.58, which was not done. Co-authored-by: Michael <0xMMA@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * chore(frontend): keep vitest on 4.x; correct what the rules said about isolation Review round on #84. Two of my own claims in .claude/rules/testing.md were wrong, and the vitest 5 bump costs more than it buys. VITEST STAYS ON 4.1.11, NOT 5.0.1 The evidence I shipped it on was about the test RUN — clean `npm ci`, identical test count, a broken assertion still failing — and all of that holds. It missed the thing developers actually do: `npm install` fails outright. npm error code ERESOLVE npm error Found: vitest@5.0.1 ... peerOptional vitest@"^4.0.8" from @angular/build Not a warning, an error with a non-zero exit, for every package anyone tries to add, until they know to pass --legacy-peer-deps. CI runs `npm ci` everywhere, so CI would never have caught it — my green pipeline was not evidence about this. A test-runner major is not worth breaking local installs for, so the bump stops at 4.1.11: inside the declared peer range, `npm install --dry-run` clean, 198 tests green, and `clearMocks` still defaults to false as it did on 4.0.8. The conditions for moving to 5 are written down instead of lost: the builder has to declare `^5`, and the same change has to handle vitest 5 flipping `clearMocks` to true by default — the builder does not set it, so a mock's calls stop surviving into the next test and an assertion on call counts that should fail could start passing. SPEC FILES ARE NOT ISOLATED, AND I WROTE THE OPPOSITE I claimed each spec file gets its own environment. The builder sets `isolate: false`, so files in one fork share a process, a globalThis and one jsdom window. Measured: 11 of 14 files landed in a single fork and saw `ResizeObserver` and `matchMedia` already defined on entry, set by two files that ran before them. That also explains the leak I reported as "observed once, did not reproduce in ten runs" — it reproduces perfectly, it just depends on which fork a file lands in. The advice was right and the reason was wrong, which is the more damaging half: it told the next author that cross-file state cannot happen. The jsdom section is corrected the same way. `matchMedia`, `ResizeObserver` and `IntersectionObserver` are undefined in jsdom 30 and in a spec run on its own — but in a full run a spec may see them anyway, so a green suite is not proof that your stub is there. The rule now says to run a new or changed spec on its own once, with the command to do it. Co-authored-by: Michael <0xMMA@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Michael <0xMMA@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Resolves the settings model conflict (shortcut fields + Models map) and regenerates the frontend bindings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The spec read `[data-testid="shortcut-input"]`, a plain text input this branch
replaced with the mode toggle and the recorder component. `inputValue()` on a
locator that matches nothing does not fail — it waits, so the test spent the
full 60s timeout before CI called it red.
What it is really for is that settings reached the UI at all, so it now checks
that through the elements that exist: the mode toggle renders, the Fix
recorder shows `ctrl+g` as the recorder formats it ("Ctrl + G"), and the
Pyramidize section is absent because double-tap mode has one shortcut.
The delay slider is asserted too, and not as decoration: it exists only in
double-tap mode, so without it the assertion that Pyramidize is hidden would
pass just as happily in a mode where nothing renders at all. Browser mode
defaults to double_tap (`wails.service.ts`, BROWSER_MODE_DEFAULTS) — asserted
rather than assumed.
Retrying assertions throughout, no `.count()` or `.textContent()` sampled once:
that is the flake #85 was about, and the same trap in a different shape is what
made this test hang instead of fail.
Three local runs, 7 passed each, 1.0s for this test against the 60s it used to
burn. Each new assertion confirmed to bite by inverting it.
Co-authored-by: Michael <0xMMA@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 24, 2026
0xMMA
added a commit
that referenced
this pull request
Oct 5, 2026
…104) * fix(shell): show the version with exactly one "v" in the sidebar Release builds are stamped with the git tag (v3.6.0), so the template's hardcoded "v" produced "vv3.6.0". versionLabel() (core/version-label.ts) now adds the prefix only to a bare version number: tags and git-describe output keep their own "v", a dev build shows "dev" rather than "vdev", and a lone commit hash from `git describe --always` stays as it is. The About tab uses the same label, so both places read alike. Closes #21 * fix(settings): stop offering AWS Bedrock until it works Bedrock is a stub that only returns "not yet supported", yet it sat in the Active Provider dropdown with its own key field. It is gone from both, and README, CLAUDE.md and the website no longer list it. The Go stub and the settings value stay so #23 can bring it back. A settings file that already names bedrock is left as it is: switching it would send the user's text to a service they never chose. Settings and Pyramidize both show "Choose a provider" with a note that AWS Bedrock is not available yet. Pyramidize also stays disabled, skips the key banner and model probe for it, and so never reaches the raw "unsupported provider" error. Picking any provider clears the note. Closes #22 * fix(settings): remove the unstyled light and system themes Only the dark theme is styled. Choosing Light dropped PrimeNG into its unstyled light mode, and System did nothing. The theme control is gone (a dropdown with one entry is not a choice), and applyTheme() keeps .app-dark whatever theme_preference says, so a value saved by an older version cannot unstyle the app either. The field stays in the settings model and is saved unchanged, for the light theme in #25. The dark-mode E2E spec gains a test that serves a stored "light" through the Wails runtime endpoint and asserts the page stays dark with no theme control. Closes #24 * fix(settings): keep toggle switches at full size beside long hints The Sensitive Logging row puts a long hint next to its switch, and the flex row squeezed the switch (30px at 1280px wide, 10px at 600px), so the knob slid past the track's edge. The switch no longer shrinks; a Playwright test at a 600px viewport asserts every toggle row's switch keeps its 40px width. Closes #26 * fix(ui): put .app-dark on <html> so dark tokens reach components PrimeNG declares component tokens on :root as aliases of the theme tokens (the select panel's background is var(overlay background)), and an alias resolves on the element that declares it. With .app-dark on <body>, <html> never saw the dark theme, so every aliased token came out light: the disabled Pyramidize global-instruction input (slate-200), every p-select dropdown panel (white), tab bars, and the dim slate key labels on the AI Providers tab. The class now sits on <html> (index.html, applyTheme). The existing overrides in styles.scss are kept so no colour that was right changes; the card background moves to :root.app-dark so PrimeNG's own :root card token cannot raise the settings card off the page. Disabled pInputText, pTextarea and p-select keep a dark zinc-900 surface with muted text, rather than Aura's zinc-700 block. Two dark-mode E2E tests pin the disabled-input and dropdown-panel colours. Closes #28 * fix(pyramidize): keep Apply secondary until an instruction is typed Apply wore the same accent as the Pyramidize button even while it was disabled, so a button that could do nothing competed with the page's main action. It is now secondary while the global instruction is empty or whitespace, and turns primary as soon as the user types one. Closes #29 * fix(pyramidize): keep an unavailable provider out of canvas refinement With a saved AWS Bedrock provider, the Pyramidize button was already guarded, but the Editor canvas is editable before any Pyramidize: typing a global instruction enabled Apply, and Apply, Ctrl+Enter or the selection bubble ended in a raw "unsupported provider: bedrock" error. Apply now stays secondary and disabled, the bubble's Apply is disabled, and both methods return early. The note on the page now points to Settings, since a choice made on the page lasts for the session only. Also: the settings spec's no-AWS-key test asserts the provider keys that must be there, so an unrendered tab cannot pass it, and the lookup uses Object.hasOwn so an inherited property name cannot match. Refs #22 * feat(settings): show and switch the provider in use on AI Providers Every provider card on the AI Providers tab now says whether KeyLint uses it and offers a one-click switch. A provider that cannot work yet stays selectable and its card says why. The General tab's dropdown is replaced by a read-only pointer to the tab where the choice is made. An environment-supplied key now names its variable on the card. * test(e2e): cover switching providers against a fake settings backend * refactor(settings): place provider card import clear of #31's lines * fix(settings): apply a save in memory only once it is on disk Save swapped the live settings before writing the file, so a failed write still switched the provider for Fix and Pyramidize while the settings screen put its marker back. Also reword the missing Anthropic key error, which pointed at the removed Active Provider dropdown. * fix(settings): harden the provider switch after review Keep presets added this visit, hold Save and Reset during a switch, drop a stale Ollama warning when the URL changes, move focus to the marked card or the AI Providers tab, and word the summary so it does not promise more than Pyramidize's own session choice delivers. * feat(settings): add SetActiveProvider and write settings atomically SetActiveProvider saves the provider alone under saveMu, validated against the providers the screen offers. Settings are written to a temp file and renamed over settings.json, so an interrupted write can no longer leave a truncated file that stops the app from starting. * feat(settings): switch provider alone and let Pyramidize follow it Use this calls SetActiveProvider, so other pending edits stay pending. Switch, Save and Reset lock each other; Save shows its errors. The marker moves once saved, focus goes to the card heading and a live region announces it. Pyramidize re-adopts the Settings provider on every visit unless the user picked another there, which it now shows as a session override with a way back. * fix(settings): lock single-field writes and retry a held rename settings.Update holds saveMu from read to write; SetActiveProvider, CompleteSetup and the preset and threshold writers use it, so none can undo a switch landing meanwhile. On Windows the final rename retries for about 2 s while another process holds settings.json open. * fix(settings): tidy switch focus, announcements and session override Pyramidize drops an override once Settings agrees with it and treats a pick forced by an unusable Settings provider as no override. A failed switch is announced once and focuses the error when no card is in use. A switch refreshes the target's status and clears a stale Save error. Specs reset Pyramidize's module state, so test order no longer matters. --------- Co-authored-by: Michael <0xMMA@users.noreply.github.com>
Brings in settings atomic writes and settings.Update, provider switching (#104), the dev channel (#107) incl. workflows, the shell footer (#106), Go 1.27 and the CI apt fixes. Conflicts: shell keeps the subscription list and silent-fix guard next to the dev-channel update title; wails service keeps setShortcutPaused next to the dev-channel calls; settings imports ToggleSwitch and SliderModule. Bindings regenerated. E2E settings stubs gain the shortcut fields the backend now returns.
Nothing emitted settings:changed, so the hot-reload never ran and a new combo needed a restart. Settings now calls an OnSaved hook after every write; main.go re-applies the shortcuts when they changed, installing the hook if startup registration had failed. Save refuses a shortcut change the hook cannot use (unparseable, no Ctrl/Alt/Win, duplicate, bad mode or delay), load() replaces such values from the file, and the shortcut_key migration is dropped: Ctrl+G was hard-wired up to v4.5.0-beta, so the legacy field never held a live value.
The shell navigates to Pyramidize on the shortcut, but the page subscribed only after its async setup, so the event was already gone and the page opened empty. WailsService keeps the shortcut pending until a page takes it.
Leaving the page or switching mode while recording left the hook paused until restart. Shift+digit now records the digit, not the symbol.
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.
Closes #30
Summary
Started as a 200 ms double-press detector on top of
RegisterHotKey; that cannot see a second tap while Ctrl is held, so the branch replaced the input capture with a low-level keyboard hook and made shortcuts configurable.WH_KEYBOARD_LLhook (internal/features/shortcut/service_windows.go) tracks modifier state, suppresses the matched trigger key so the foreground app never sees it, and runs the double-tap state machine on the hook thread.double_tap(single tap → fix; second tap within the delay while modifiers are held → pyramidize; releasing the modifier ends the wait early) andindependent(two separate bindings, each fires on keyup).shortcut_mode,shortcut_fix,shortcut_pyramidize,shortcut_double_tap_delay(100–500 ms, default 200); legacyshortcut_keyis ignored (Ctrl+G was hard-wired up to v4.5.0-beta, so it never held a live value); unusable values in the file are replaced on load and refused on Save. Key-combo parser with VK mapping (keycombo.go+ tests). Saved shortcuts apply immediately (settingsOnSavedhook inmain.go).frontend/src/app/features/settings/shortcut-recorder/); recording pauses the hook.shortcut:fix/shortcut:pyramidizereplaceshortcut:triggered; frontend exposesshortcutFix$/shortcutPyramidize$; the silent-fix subscription moved toShellComponentso it works on every route; Pyramidize no longer captures the clipboard on a single press.Design:
docs/superpowers/specs/2026-04-06-keyboard-hook-configurable-shortcuts-design.md(supersedes the double-press spec in the same folder).Test plan
go test ./internal/...(CI)v0.0.0-pr.31, always the latest CI run of this branch; Developer options are unlocked by tapping the version). Fallback: theKeyLint-windows-amd64-setupartifact of the latest CI run.version string— obsolete: the merge of main (2026-10-08) superseded that build; the dev-channel build carries the 2026-09-17 fixes and main's newer work.v4.4.3-beta-35-ga04faa3settings.json(v4.5.0-beta, onlyshortcut_key) loads without error and Ctrl+G worksUpdate 2026-09-17
Root cause of the double-fire.
SetTimerwith a NULLhWndignores thenIDEventargument and returns a newly generated timer ID; the code passed a constant1and killed the timer with that same1, soKillTimernever matched. BecauseSetTimertimers are periodic, every first tap leaked a timer that kept postingWM_TIMERforever. A stray timer then fired a silent fix milliseconds into the next first tap, the second tap was treated as a fresh first tap, and the second paste landed after the first paste's collapsed caret — so hold-Ctrl double-tap ran fix twice and Pyramidize was unreachable.What changed.
fix(shortcut): kill the double-tap timer by its real Win32 ID (#30)— the service now stores the IDSetTimerreturns, routes every kill site through astopTimer()helper (keyup, modifier release, second tap,UpdateConfig,SetPaused,Unregister), ignoresWM_TIMERwhosewParamis not the current ID, and kills the timer when it expires. IfSetTimerever fails it logs a warning and degrades to a plain single-tap fix instead of swallowing the key. The bogusdoubleTapTimerIDconstant is gone.fix(shell): ignore shortcut while a silent fix is in flight (#30)—ShellComponent.silentFix()now guards against re-entry, so two fix events can no longer run two concurrent read → enhance → write → paste cycles and paste on top of each other. Covered by a new Vitest case (138 tests green).fix(shortcut): reset double-tap state on the pump thread (#30)— aSetTimer(NULL, …)timer belongs to the thread that created it, so theKillTimercalls made fromUnregister,UpdateConfigandSetPausedon their own goroutines could never kill it; those callers now post awmResetmessage and the message loop does the reset on the pump thread, which also removes the cross-goroutine writes totapState/indepPending.fix(shell): release silent-fix guard after a safety timeout (#30)— a 120 s safety timer releasessilentFixInFlightif the enhance call never settles (the backend HTTP timeout lands in feat(provider): Claude Code CLI as first-class provider (claude -p) #32), plus reject-path and timeout-path tests (140 green); on the Windows side a failedSetTimernow stays intapWaitReleaseso auto-repeat cannot fire one fix per repeat.fix(shortcut): absorb held trigger key after the double-tap window expires (#30)— holding the shortcut down used to fire one fix per delay interval, because the expiring timer dropped the state machine totapIdleand every auto-repeat keydown read as a fresh first tap; a newtapFiredWaitReleasephase fires the fix once and swallows the trigger key until its keyup.Verification.
GOOS=windows go vet ./...clean apart from three pre-existingpossible misuse of unsafe.Pointerwarnings, Windows cross-compile with mingw succeeds,go test ./internal/...andcd frontend && npm testgreen on Linux.Manual checklist reset. All Windows manual items above are unchecked again and must be re-run against the artifact from the CI run for
a04faa3— no earlier artifact carries this fix. (2026-10-08: use the dev-channel build instead, see the test plan.)Update 2026-10-08
Merged
origin/main(settings atomic writes +settings.Update, provider switching #104, dev channel #107, shell footer #106, Go 1.27, CI apt fixes). Conflicts in shell,wails.service, settings imports and bindings; both sides kept. Then fixed what the merge review found:fix(shortcut): apply saved shortcuts at once, refuse unusable ones— nothing ever emittedsettings:changed, so the hot-reload never ran. Settings now calls anOnSavedhook after every write;main.gore-applies shortcuts when they changed (and installs the hook if startup registration failed).Saverefuses an unusable shortcut change;load()repairs unusable values; theshortcut_keymigration is gone (it never fired, and firing it would have applied unvalidated free text).fix(enhance): load clipboard for a shortcut sent from another page— Pyramidize by shortcut from any page but/enhanceopened empty: the event fired before the page subscribed.WailsServicenow keeps it pending until a page takes it.fix(settings): resume shortcuts when the recorder goes away— leaving Settings mid-recording left the hook paused until restart; Shift+digit records the digit.🤖 Generated with Claude Code