Repository navigation
feat!: evolve Flash modes, plugins, and dev tooling - #8
Conversation
The core hard-coded ten terminal bundle ids (TerminalBundles) to decide where Command chords would type text, where pixel wheels are refused and what `only_terminals` matches. No protocol identifies a terminal emulator, so the list is now data: a manifest's `terminal_emulators`, unioned across plugins into TerminalEmulators when each plugin snapshot publishes (before the snapshot is visible, so selectors resolved against it see the same set). The bundled manifest-only terminals plugin declares the common emulators; a user adds one with a plugin of their own. The source registry asks the declared set on each activation check instead of freezing a copy at init.
WebBrowsers listed 22 browser bundle ids, and the core checked them (or a com.apple. prefix) to decide how to treat an app's accessibility. AppTraits now reads what an app is from its bundle, once per app and cached: a web browser declares the http and https URL schemes; Gecko ships Contents/MacOS/XUL; Chromium (browsers, Electron and CEF apps) ships renderer helper apps; Flutter ships FlutterMacOS.framework. - a browser's web areas are pages (semantic allowlist); any other app's are its own interface, where pressable containers are targets. - the enhanced-UI wake goes to the runtimes that gate their tree on it, Chromium and Flutter; AppKit, SwiftUI and UIKit apps, Apple's or not, never get the flag. It used to reach every non-com.apple app. - Gecko's scoped accessibility (GeckoAccessibility, was FirefoxAccessibility) and the window mover's exception key on the engine, so every Gecko build is covered, not three bundle ids. - AccessibilityWaker (was ChromiumAccessibilityWaker) also warms the traits cache off main for running and launched apps. - the walker threads one WalkEnvironment instead of five parameters.
AppMonitor excluded com.apple.Notes by bundle id from background model warming and observed it with the reduced notification set. The exclusion is now a manifest field, `on_demand_hints`, unioned across plugins like `terminal_emulators`; the bundled defaults plugin declares Notes. Both declared sets share one DeclaredApps registry type, replaced whole on each plugin snapshot and readable from any thread.
Every hint activation reassigned overlay.overlayConfig from the loaded config, and its didSet unconditionally bumped statusBarLayoutRevision (invalidating the memoized status-bar layout) and re-stroked the active window border. The value only changes at launch and on config reload, so the per-activation pushes are gone and the didSet ignores an equal value.
A keydown after an app switch no notification reported reconciles the frontmost app from inside the synchronous tap callback, and refreshing its effective mappings reached Register/UnregisterEventHotKey there. apply(mode:) now swaps the table the tap reads synchronously and leaves the Carbon registrations, which only matter without the tap, to the next main turn, coalesced.
Refresh reasons were strings that drove control flow: every AX
notification built "ax:<name>", then hasPrefix("ax:") and string
compares against "maintenance" and "queued" decided throttling,
priority and speculation. ModelRefreshReason carries those decisions as
properties; the log string is built only when logged.
normalModeCaptureRecoveryRecaptureToken was never set, so cancelNormalModeCaptureRecovery always returned early at its five call sites. normalModeCaptureRecoveryShouldRetry, its delay table and normalModeShouldRecaptureAfterActionDispatch (a plain wrapper) were only called from tests; commandLineEntryIsAllowed always returned true and commandLineExitMode always NORMAL. The wrapper's test now exercises normalModeShouldOwnKeyboardInput, which it wrapped.
filter(prefix:hints:) keyed a fresh dictionary by each chip's label, but labels are unique within a set, so every visible chip paid a string hash and a miss on every keystroke. It now renders the visible labels directly, and builds the visible-index set only while debug bounds are drawn.
The decision lived in overlapping branches of keyboardTapShouldSwallow, with the native-owner condition spelled two ways and secure input checked at three sites. KeyboardCaptureTap.decide now maps state to a Decision (pass, swallow, swallow if an INSERT chord is mapped, swallow if a native-surface key is mapped), pinned by a table test; the tap callback evaluates only the effects the decision asks for, in the same cheapest-first order.
Five copies read NSScreen.screens for the primary display's height, including the AX walker and the scroller, which run off main (gg / G now do): NSScreen is main-affine. ScreenSpace caches the height, reads it on main at launch and on every screen-parameter change (its observer is registered first), and serves every thread from the cache.
The previous-tab action went over the wire as tab_prev while the verb and every sibling spell previous out (tab_move_previous, pane_previous), and the host accepted both spellings when reading a manifest's `actions`, an alias the project forbids. The wire name is now tab_previous in the host and the tmux plugin; capabilities derive from SourceAction.byWireName instead of a hand-kept string switch, and a manifest declaring an unknown action is rejected at load. BREAKING CHANGE: third-party plugins must declare and handle `tab_previous` instead of `tab_prev`.
AppClickDecision bundled four Bools (release capture, enter INSERT,
suspend for a native surface, dismiss hints) of which three combinations
were reachable, and MenuBarClickDecision always suspended. They are now
AppClickDecision { ignore, suspendForNativeSurface(dismissHints:),
handOffToInsert } and PointerDecision.menuBar(dismissHints:); the handler
switches over them instead of testing each flag.
The 1...999 repeat-count bound was spelled in three places, the post-action recapture (force when focus may move, longer ramp) was copied in the key-window and mapping dispatch paths, and pointer mode kept a private copy of suspendNormalCaptureForNativeSurface. Each now exists once: RepeatCount.clamp, recaptureAfterNormalAction and the shared suspend.
A status-bar hover preview only closed when the pointer left its segment, so it lingered over whatever the user clicked or switched to. A press in another app, the desktop or the menu bar (the global pointer monitor), an app switch and a focused-window change now close it, and cancel a terminal popup still waiting out its hover dwell. The hover gate keeps it shut until the pointer leaves the segment. Focused popups already close when they lose key; pinned terminal popups stay.
The HTTP inspector accepted any request from a loopback peer, so a web page could rebind its own hostname to 127.0.0.1 and read /state (the clipboard history, hint labels) and /logs through the browser. It now serves only requests whose Host names this listener (127.0.0.1, localhost or [::1] on its port), and its log sink follows the configured level instead of forcing every trace message on hot paths to be built. Logs no longer carry what the user typed: the tap-to-route latency line dropped its key code, NORMAL's input trace its characters, and a dropped hint commit its target's accessibility label (warn level, on disk by default). The tap must never record keys (hard constraint 2).
…cal subcommands A focus change emitted core:apps.changed with the full running-apps list even when no app launched or quit, and the windows plugin re-walked every app's accessibility tree on each: 2,587 warm refreshes, about 600 s of walking over five hours of logs. The event now fires only when the set of (pid, bundle id) changes; the cached list plugins replay at startup still tracks every snapshot. Commands matched their registration case-insensitively but forwarded the typed case, so `:processes Refresh` reached the plugin as an unknown subcommand; the plugin now gets the names it registered.
- A failed source action keeps the plugin's reason and the source that claimed it (SourceActionResult.failureReason/source), logged with it. - Every plugin request completion logs its id, method, elapsed time and outcome at debug; timeouts and late replies carry the id; perform is no longer exempt from the slow-request warning. - A malformed reply fails the request it answers at once instead of leaving its caller to wait out the deadline. - Plugin stderr is logged as whole, capped lines at a bounded rate, with a count of what was suppressed; exits say signal or exit status. - Per-plugin CPU% was about 42x too low on Apple silicon: rusage CPU time is Mach ticks, now converted (MachTime). - A run-loop observer reports every main-thread busy stretch over 250 ms at any log level (the ping watchdog stays debug-only, and is now toggled after the inspector sink changes, not one reload late). - FlashLog: `fields` is lazy like the message, sinks take a minimum level, `error()` exists, and `emits`/`wouldEmit` are one function.
A bare Escape while a status-bar hover preview shows now closes it in either base mode and is swallowed, so the app doesn't also get it; the command line and a hint session keep their own Escape, and the key is not routed on to a mapping or the NORMAL interpreter. enter_normal_mode closes the preview too. Decided in KeyboardCaptureTap.decide (closeEphemeralPopup), pinned by its table test.
A routed key, a native hotkey, a physical click Flash acts on, a CLI verb or a submitted command line now starts an interaction with a short trace id (Trace), carried on every log line it causes: dispatch, the source-action chain (re-entered on each source's completion), delayed key sends, and plugin requests, whose envelope carries `trace`. The SDK runs each request handler inside that id and stamps it on the `log` notifications the handler emits, so plugin lines join the host's: `rg '"trace":"<id>"' flash.log` reads one interaction across processes. - protocol.json pins the id pattern; Swift and Rust parity tests check it. - The inspector serves /logs?trace=<id> and /traces (recent interactions: origin, duration, lines, worst level, participating sources), computed from its log ring on demand. - docs/observability.md describes the log format, tracing, inspector endpoints, stall detection, plugin diagnostics and privacy rules.
A plugin whose only resident surface is `status` (its `listen` feeding those segments) is now resident only while the enabled status bar, its options or a popup references one of its segments; otherwise it is on-demand and spawns on its first perform. A config reload that starts showing it spawns it; one that stops keeps the running process. With the bar off (the default), cpu, memory, disks, power, caffeinate, feed and aiproviders no longer spawn at launch.
Every request envelope now carries `deadline_ms`, what is left of the host's wait. The Rust SDK drops a `search` or `hints` handler still running a tenth of it (at most 50 ms) before the deadline, with the subprocesses and host calls it awaits, and answers `deadline exceeded` while the host still listens, logging it under the request's trace. `host.ax_snapshot` takes an optional `deadline_ms`: past it the walk answers with the nodes reached so far and `truncated: true`. The vscode plugin bounds its walk to 300 ms so its hints fit the 500 ms deadline.
A bang with a query opened the provider's page, then 2.5 s later pressed Return in whatever app had focus — a terminal the user switched to would run its pending command line. `host.open` now names the app a URL was handed to (`bundle_id`), and the plugin presses Return only while that app is still frontmost.
Every five minutes the plugin copied Firefox's and Chrome's databases, queried them and republished the catalog. A refresh now stamps each store file (length, mtime) and the recency window's day, and does nothing when all match the published catalog's; a database whose files are unchanged reuses its private copy instead of copying it again.
`caffeinate -w <plugin pid>` exits with the plugin, so a crash or a SIGKILL past the shutdown grace no longer leaves an ownerless assertion keeping the Mac awake. `on`/`toggle` with a minutes argument that is not a whole number (`-5`, `1h`) now fails with the usage line instead of silently keeping the Mac awake indefinitely.
The browsers and firefox plugins scripted every running browser on any focus change, app launch or quit — hundreds of milliseconds of AppleScript per event, for events in unrelated apps. The SDK's new `AppWatch` passes only focus moving into, within or out of a watched app, one launching or quitting, a change in their running instances, and a flashlight session opening; the 10 s poll catches the rest.
A plugin polling a slow or failing program logged `[plugin] subprocess slow` on every tick (1,583 lines from the browsers plugin). Each kind of warning — per program, slow or timed out — now logs once a minute, with `suppressed` counting the ones held back. A failed osascript logs `[plugin] osascript failed` with the AppleScript error number alone (never its message), which is what shows the browsers plugin's refreshes failing with -10004.
`label` carries the same rotating headlines and still prefix as `summary`, without the outbound arrow or inline preview; title and domain form one link to the item. Binding it hands hover to the surrounding template, so a configuration can wrap it in its own popup (a terminal running any feed reader).
A full-screen program keeps its last row for messages and prompts and leaves it blank otherwise (newsboat's message line). A terminal popup's hover preview now clips that row while it is empty, as the pager path already did; a focused or pinned popup keeps it for `/` input and errors, and a row with text is never clipped.
A committed left-click on a link or a named range action already answered why the popup was open, so the hovered popup closes and stays shut until the pointer leaves its region. Pinned popups and clicks with no handler are left alone.
A focused terminal popup stayed up after the user went back to the app it covered: Flash asked to activate with `NSApp.activate()`, which macOS 14+ declines while another app is frontmost, so the covered app stayed active and bringing it forward again (a click into it, or `open` raising it for a link) posted no app switch for Flash to see. Focusing a popup now activates Flash from the frontmost app (`activate(from:options:)`), so any app coming forward is an app switch and the popup closes on it, leaving TERMINAL. Verified on the resident: with Firefox frontmost, a focused btop popup makes Flash frontmost and closes when Firefox is raised again.
Add `flash gesture --fingers=2..5 --motion=<motion>`, which performs what that trackpad gesture does under the default macOS settings: secondary click, smart zoom, zoom, rotate, history, Look Up, Mission Control, App Exposé, Spaces, the app launcher and show desktop. Zoom, rotate and smart zoom post real gesture events; the Dock surfaces use CoreDockSendNotification, resolved at run time. Add the `look_up` app action (Control-Command-D by default) and `--gesture=look_up|smart_zoom` on mouse_target, mouse_bigram and mouse_grid. In NORMAL a count before a mouse finisher is a finger count: `2f` secondary-clicks, `3f` looks up and `2df` smart-zooms at the pick. Mission Control, including `system_key --key=mission_control`, now asks the Dock: the posted Mission Control key reached nothing on macOS 26.
The login-shell resolver waited for its shell's exit and output on two blocks of the global concurrent queue. With that pool saturated, a busy launch or a parallel test run, neither block ran and the resolution timed out after ten seconds, keeping the seeded environment. Exit now arrives through the process's termination handler and stdout drains on a dedicated thread.
Flash takes activation for its own typing surfaces, and leaving one does not always give it back: right after launch Flash was frontmost while Firefox looked focused. In INSERT every key the tap passes, Command chords included, then reached Flash instead of the app. Flash now hands activation back whenever it shows INSERT or becomes active in it, unless About, a hint session or a physical click still owns activation. `flash status` (schema 4) adds what decides a chord's fate: frontmost_app, flash_active, chord_routing (what the tap does with a modified chord now) and held_modifiers (a stuck modifier rides every keystroke). Running it while a shortcut fails pins the cause. BREAKING CHANGE: `flash status --json` reports schema 4 with four new keys.
Pin mise tools and lock Swift, Rust, JavaScript, and Python dependencies. Refresh Ghostty, Electron, and tmux oracle baselines; adapt status formatting and Tokio registrations to the new upstream behavior. BREAKING CHANGE: status formatting now follows tmux 3.8 for anchored substitutions and shell quoting.
|
@greptileai please review the latest changes on ab/dev. |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Pin rustfmt and clippy alongside Rust so fresh CI runners can execute the plugin gate with the same toolchain as local development.
|
@greptileai please review the latest commit (0e71fce) on this PR. |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Page clipboard history within the plugin wire limit and expose the retained count through the inspector. Show a compact connection indicator, keep navigation labels on one line, and make Escape and the shortcut sheet easier to use.
Open linked onboarding items directly, connect configuration origins and provenance to the live editor, and replace repeated TOML buttons with compact copy controls. Remove the redundant Flash Help breadcrumb on Home.
|
@greptileai please review the latest commit (edbe2ee) on this PR. |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
@greptileai please review the latest commit (f14de76) on this PR. |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
The flashlight sorted the default pool by a hardcoded category ladder (tmux windows > running apps > installed apps > browser tabs) before match score, so an exactly typed tab title lost to every app the query matched partially unless the query happened to be a title prefix. Rank by how the query hits the row's own name first (exact > prefix > word > substring > secondary/fuzzy), then by source precedence, then by score and frecency. The ladder is gone: the within-tier order now comes from the manifest `sources[].priority` (tmux windows `important`, apps and tabs `high`), the alive bonus and `[flashlight.precedence]` overrides. Multi-word queries match title words in any order, and `app_open --name` ranks plugin locations with the same scorer instead of taking the first substring hit. BREAKING CHANGE: `[flashlight.precedence]` weights now order rows inside one match tier only; the strict category ladder no longer exists.
A mapping on a named key kept whichever alias spelled it (`<esc>` stayed `esc`, `<return>` stayed `return`) while the interpreter emitted `escape` for Escape and nothing at all for Return, keypad Enter, Delete, the arrows, Home, End and the Page keys. The DRAG table's Return (drop) and Escape (cancel) and VISUAL's Escape therefore never fired: the key was swallowed and the only way out was the NORMAL chord. Every named key now has one atom, shared by the mapping parser and the interpreter, and keypad Enter is Return to both. Entering DRAG is now the `enter_drag_mode` verb (`--secondary`, `--middle`), a toggle that grabs or drops, and it ships unmapped: the `D` and `cD` defaults are gone. A hold from INSERT (an all-mode chord) takes the keyboard as NORMAL entry does and its release lands in NORMAL. A pointer move warps at once on every path, so the next keystroke of a held gesture reads the pointer where the previous one left it. BREAKING CHANGE: `D` and `cD` no longer enter DRAG; bind `enter_drag_mode`.
|
@greptileai please review the latest commit (a56c920) on this PR. |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Clear inherited shortcut modifiers from held mouse events so Control-based drag shortcuts remain primary drags. Show plugin details in a dismissible dialog, simplify the verbs guide within the shared docs layout, and correct help that implied colon was bound by default.
Add Vim-style registers, clear successful drag yanks, and harden mark restoration. Show register state in help and keep clipboard history in the resident. BREAKING CHANGE: VISUAL mode and the clipboard plugin are removed. Configure clipboard history under [clipboard].
Rename the mode, command, config scope, status, help, and inspector. Register Command-Control-H by default and gate the toggle to NORMAL, INSERT, and HOLD. BREAKING CHANGE: enter_drag_mode, [mode.drag], --mode=drag, and the drag status value become their hold equivalents.
Summary
ab/devwork into one PR: keyboard modes and mappings, hints and grid behavior, plugin actions, status and popup surfaces, the inspector/help experience, and release and development workflows.AsyncFdregistration for current Rust dependencies.TOMLbuttons with copy icons.Test plan
swift format lint --strict --recursive Sources Tests Package.swift./Scripts/check-guardrails.shswift test(2,042 tests) and./Scripts/test-plugins.sh --lane all./Scripts/test-status-format-oracle.py(1,201 cases)npm ciuv run --locked mkdocs build --strict./Scripts/install.sh --dev; one signed resident and successfulflash hints_dismissdispatchflash.tomlpassesflash config_check; resident reports zero config diagnostics on the installeda56c920buildaggr,Cld,Cdx,Grk,CPU,MEM,NET, andBATonce each, with noFEEDlabel.No other PR needs consolidation: PRs #1–#7 are already merged, and this is the only open PR.