Commit graph

2049 commits

Author SHA1 Message Date
Brooklyn Nicholson
fb5efe917a feat(desktop): parse the @kind: prefix as a browse scope
detectTrigger split an `@folder:apps/desk` token into one opaque query, so
every consumer downstream had to re-parse the prefix — or, more often, treat
it as characters the user was expected to maintain by hand.

Split it into `scope` + `value` at the source, behind a known-kinds list so a
handle like @teknium1: or a host:port stays ordinary text, and add
openDirectiveScope() for callers that need to know the caret is sitting in an
empty scope.
2026-07-30 04:52:06 -05:00
hermes-seaeye[bot]
11089899fb
fmt(js): npm run fix on merge (#74740)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-30 09:42:51 +00:00
brooklyn!
94924c6430
Merge pull request #74674 from NousResearch/bb/yolo-palette
Put YOLO in ⌘K and show each toggle's live state
2026-07-30 04:25:18 -05:00
brooklyn!
1ae1eb47bc
Merge pull request #74729 from NousResearch/bb/no-auto-filetree
Opening the diff pane no longer opens the file tree
2026-07-30 04:23:18 -05:00
Brooklyn Nicholson
7996ec66cd test(desktop): match the switch and disclosure labels to what they render
These assert the accessible name, and the name now states the action rather
than the word toggle: an enabled toolset offers "Turn X toolset off", a
collapsed project offers "Show X sessions".
2026-07-30 04:18:17 -05:00
Brooklyn Nicholson
6cb1aa44ed fix(desktop): opening the diff pane no longer opens the file tree
`revealTreePane` un-collapsed a pane's column by calling the side's bound
store setter. On the right that setter IS `setFileBrowserOpen` — the file
tree's own toggle — so anything sharing that column dragged the tree open
with it: ⌘G on the diff, a session tile reveal, `focus_pane`.

Un-collapse the column directly instead. The tree now opens only when its
own toggle is pressed. `revealPreview` had a local workaround for exactly
this, which the fix makes redundant.
2026-07-30 04:16:34 -05:00
brooklyn!
dd241cf0cd
Merge pull request #74644 from NousResearch/bb/double-tap-heart
feat(desktop): double-click a message to heart it
2026-07-30 03:56:01 -05:00
Brooklyn Nicholson
640056704c Merge remote-tracking branch 'origin/main' into bb/yolo-palette
# Conflicts:
#	apps/desktop/src/app/command-palette/index.tsx
2026-07-30 03:49:03 -05:00
hermes-seaeye[bot]
9accf79d83
fmt(js): npm run fix on merge (#74702)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-30 08:19:58 +00:00
Brooklyn Nicholson
129b4e7fef feat(desktop): make the ⌘K row note a shared primitive with a state variant
A toggle's on/off isn't a passive fact like a version string — the row is about
to change it. It keeps the label's color so the two read as one phrase, and
earns its separation from a faint underline instead of going muted. Both
variants live in floating-hud.ts beside the other shared HUD chrome.

That underline needs room to exist: flex items are blockified, so the note's
truncate/overflow-hidden bites, and the app's global 0.25rem offset sits at the
bottom edge of a text-xs line box. The state variant drops truncate (one word,
nothing to clip) and pulls the offset to 2px.

Flipping a setting isn't navigation either, so toggles keep the palette open the
way the theme and color-mode rows already do, and selecting a keepOpen row bumps
a counter that rebuilds the groups so the note can't report the state it left.

Also: yolo is lowercase, and logs gets its own icon instead of borrowing the
YOLO bolt.
2026-07-30 03:18:13 -05:00
Brooklyn Nicholson
2c71a37ad1 fix(desktop): clear double-click heart lint errors 2026-07-30 03:16:42 -05:00
Brooklyn Nicholson
b69d4e5a78 feat(desktop): double-click a message to heart it
The iMessage gesture, on the same opt-in toggle as the rest of reactions —
double-click any message and it gets a heart; double-click again and it comes
off. Off by default, and while it's off the message root carries no listener
at all.

The gesture is deliberately narrow about what it claims: only a true
double-click (detail === 2, so a triple-click to select the paragraph doesn't
re-toggle), and never over an element where a double-click already means
something — links, buttons, inputs, code blocks. It clears the word selection
the browser just made, since the tapback is what the gesture meant.

Reaction state for the handler is read lazily off the message runtime at
event time rather than subscribed to, mirroring how the footer already reads
its text: the handler renders nothing, so subscribing the message root to
every reaction change would be cost for no paint.
2026-07-30 03:16:42 -05:00
Brooklyn Nicholson
9d589b92d3 refactor(desktop): one hook for a message's reactions
The assistant footer and the user bubble each carried the same block: two
metadata reads, the three-store merge, and a local-first toggle that paints
before it persists. Same code, two files, and the next surface that wants to
react would have been a third copy.

useMessageReactions owns it now, with commitReaction as the single write path
so every caller applies identical tapback semantics.
2026-07-30 03:16:41 -05:00
brooklyn!
d437d3e54d
Merge pull request #74665 from NousResearch/bb/palette-perf
perf(desktop): ⌘K opens instantly, whatever else the shell is doing
2026-07-30 03:08:58 -05:00
Brooklyn Nicholson
0481766561 perf(desktop): make ⌘K cost nothing until it opens
⌘K is an overlay that is stateful to itself — pressing it owes the user a
frame immediately, whatever else the shell is doing. It was not built that
way.

`CommandPalette` is mounted for the life of the app, and its body ran
unconditionally: a dozen store subscriptions (connection, desktop version,
client + backend update status/apply, keybinds, worktrees, theme, i18n),
three `useQuery`s, and the group builders that assemble a few hundred rows.
`<Portal>` renders nothing while closed, so none of it was ever visible —
but all of it still ran. An in-flight update rewrites `$updateApply` on
every progress line, and each of those rebuilt the entire row set for a
surface nobody could see.

Split the body into `CommandPaletteBody`, mounted only while the palette is
on screen. A closed palette is now one store subscription. The body is keyed
by open count, so per-open state (search, sub-page) resets by remount and
the explicit close-reset effect goes away, and `mounted` lags `open` by the
150ms exit animation so Radix can still play `data-[state=closed]` instead
of the overlay vanishing.

Rows additionally move behind `useDeferredValue` in their own memo
component. Because that component mounts with the portal, the deferred
initial value applies per open: the first commit is the frame + input, and
the several-hundred-row list arrives in an interruptible follow-up render
rather than blocking the frame the keypress asked for. The empty state is
suppressed while rows are still pending so opening doesn't flash "no
results".

The `enabled: open` gates on the three queries are dropped — the component
only exists when open, so they are inherently lazy, and react-query still
serves a reopen from cache while revalidating.
2026-07-30 03:01:05 -05:00
Brooklyn Nicholson
09fa5c063c fix(desktop): make ⌘K group order the priority it already was
Group order is the only tiebreaker rankGroups has (stable sort), and ties are
the common case: "yolo" matches both "Toggle YOLO" and a worktree named
bb/yolo-palette as a whole word. Branch rows sat second and won, burying the
command under a list that grows with whatever's checked out.

The order now reads as the priority: where you're going, what you can do, what
you can configure, then the typed-only lists, and branches last.

Also pulls the muted detail back to ~4px — it reads as a suffix of the label,
not another flex item at the row's icon-to-label gap.
2026-07-30 03:00:58 -05:00
Brooklyn Nicholson
3198ed7cc3 refactor(desktop): name value-taking setters set*, not toggle*
toggleSkill(name, enabled) is a setter wearing a toggle's name. A toggle takes
no argument; anything handed an explicit value is a set.
2026-07-30 02:41:41 -05:00
Brooklyn Nicholson
f975837054 fix(desktop): name the direction in labels that describe live state
The sidebar rail, project disclosures, and the toolset switch now read "Show" or
"Hide" from what's actually on screen, and a failed write says which direction
failed instead of "Failed to toggle". All five locales.
2026-07-30 02:41:41 -05:00
Brooklyn Nicholson
b86ae0d108 feat(desktop): put YOLO in ⌘K and show each toggle's live state
YOLO had a status-bar zap and a slash command but was never registered as a
palette contribution, so ⌘K couldn't reach it.

Adding it exposed the wider gap: "Toggle status bar" doesn't say which way it
will go. Rows already carry a muted `detail` slot, so paletteToggle fills it
with on/off — no new chrome, and the verb stays. Status bar, logs, and layout
edit mode go through the same helper.
2026-07-30 02:41:34 -05:00
Brooklyn Nicholson
975f4ef38d perf(desktop): budget the transcript live tail in parts, not turns
The content-visibility virtualization from #66470 stopped engaging on agent
sessions. Its live tail — the newest turns kept always-rendered so a turn is
only virtualized once its height has settled — was sized as a raw count of 6
turns, while everything else in this file budgets in rendered PARTS
(RENDER_BUDGET=300, FIRST_PAINT_BUDGET=20).

Those units diverge badly on agent transcripts. A chat turn is 2-6 parts, but a
turn with tool calls is 50-200, so "6 turns" can exempt the entire visible
transcript. Measured on a 5-tile window (7/3/5/3/2 groups per tile): zero
content-visibility containers were active anywhere, and every Radix overlay
open paid the full whole-document style recalc that #66470 exists to avoid
(~610ms of a ~700ms open, in a handful of enormous recalcs rather than any
long task).

Size the tail by parts instead, clamped to [2, 6] turns. The floor keeps the
streaming turn rendered when turns are huge, preserving the anti-drift
guarantee; the ceiling stops a tail of tiny turns from reaching further back
than the old turn-count policy did, so no transcript shape renders more than
before. `liveTailStart` replaces the per-row `isVirtualizedGroup` predicate and
is computed once per render off the weighted groups.

Parts left always-rendered, real transcript shapes:

| shape                        | before | after |
|------------------------------|--------|-------|
| agent tile (7 tool-heavy)    |    690 |   270 |
| agent tile (5 turns)         |    535 |   225 |
| long agent session (40)      |    720 |   240 |
| long chat (40 short turns)   |     24 |    24 |
2026-07-30 02:38:41 -05:00
hermes-seaeye[bot]
fed48cf5b0
fmt(js): npm run fix on merge (#74677)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-30 07:36:23 +00:00
brooklyn!
866c9adae3
Merge pull request #74651 from NousResearch/bb/composer-paste-directives
Paste directives into the composer
2026-07-30 02:25:09 -05:00
brooklyn!
a2a74d7acf
Merge pull request #74623 from NousResearch/bb/open-project
feat(desktop): ⌘O open folder as project, projects in ⌘K
2026-07-30 02:12:22 -05:00
brooklyn!
05f5df6bdb
Merge pull request #74640 from NousResearch/bb/undo-close-tab-focus
fix(desktop): focus the tab restored by undo-close (⌘⇧T)
2026-07-30 02:06:29 -05:00
brooklyn!
1e2d55072c
Merge pull request #74642 from NousResearch/bb/sidebar-icon-clip
fix(desktop): sidebar labels truncate instead of pushing header icons out
2026-07-30 02:04:54 -05:00
Brooklyn Nicholson
422ecfe1da feat(desktop): hydrate commands when repainting inert composer text
Two repaint sites hand the editor text that is finished rather than
mid-keystroke: the main composer's programmatic draft writes (restore,
insert, history recall) and the inline edit composer opening a sent
message. Both now render with `trailingCommitted`, so a command ending
that text chips instead of reading as a half-typed token — the edit
composer in particular showed plain text for a message the transcript
had just rendered with a pill.

Regression tests cover the paste path: a command ending the paste, one
named mid-prose beside a ref, a path left alone, a paste landing against
a word, and one landing after an existing chip.
2026-07-30 02:00:22 -05:00
Brooklyn Nicholson
ccca952b92 feat(desktop): chip pasted directives in the composer
`appendComposerContents` — the one builder every paste goes through —
only ever chipped `@kind:value` refs. Slash commands had a single
leading-token special case in `renderComposerContents`, which paste
doesn't call, so a pasted `/clean` landed as dead text while the same
text typed by hand became a pill.

Both directive kinds now hydrate from one ordered span walk, with `@`
refs winning a tie so a slash inside a quoted ref value stays part of
that value. Paste additionally scans as inert text: a command ending the
paste is complete rather than half-typed, and the insertion point's own
token boundary decides the leading token, so `foo` + `/clean` stays
`foo/clean`.

`textBeforeCaret`'s chip-atomic serialization moves to rich-editor as
`serializeTextBefore` — the paste path needs the same "a chip edge is a
token boundary" reading that trigger detection does.
2026-07-30 02:00:22 -05:00
Brooklyn Nicholson
c5a68213fa feat(desktop): recognize slash commands in text the composer didn't watch typed
The composer chips a `/command` when it's picked or accepted from the
popover. Text that arrives whole — a paste, a restored draft, an undo
step — never passes through that path, so nothing recognizes the
commands in it.

Extract that recognition into a scanner that answers on the same terms
the typed path uses: no-arg commands only, no paths, built-ins as
invocations while skills may also be named mid-prose, and a trailing
token still-typed unless the caller says the text is inert.
2026-07-30 02:00:22 -05:00
brooklyn!
fa8b959b92
Merge pull request #74630 from NousResearch/bb/update-restart-race
fix(update): GUI update self-deadlocks against its own lock — every retry fails with "Hermes is still running"
2026-07-30 01:43:38 -05:00
Brooklyn Nicholson
3209236ae3 fix(desktop): fade the ⌘-held project label
The 'New session in <project>' preview is a note about what Enter will do,
not the row's name — so it takes text-muted-foreground/80, the same muted
tone the palette's detail notes already use.
2026-07-30 01:20:08 -05:00
Brooklyn Nicholson
958ab818b8 fix(desktop): sidebar labels truncate instead of pushing header icons out
Flex containers around section/lane labels kept their default min-width:auto,
so a long project title refused to shrink at narrow sidebar widths and shoved
the trailing action icons (caret, +, kebab, branch) past the edge. Give every
header label min-w-0 so its truncate can engage, pin shrink-0 on the caret at
the primitive level and on SidebarSectionMeta, and clip LaneLabel's pinned
tail inside the label. Icons now stay visible at any width.
2026-07-30 01:19:31 -05:00
Brooklyn Nicholson
ba194c1d19 feat(desktop): File > Open Folder… menu item
No accelerator (⌘O stays a rebindable renderer keybind, matching New
Window's rationale); clicking routes hermes:open-folder-requested through
the preload bridge to the same openFolderAsProject flow.
2026-07-30 01:15:01 -05:00
Brooklyn Nicholson
a11611af39 feat(desktop): projects in the command palette
⌘K gains a Projects group carrying each project's own sidebar codicon.
Selecting one is a pure scope switch; holding ⌘/⌃ previews the variant —
the label swaps to 'New session in <project>' beside a ⌘↵ chip — and
⌘-Enter runs it. A pinned row opens the native picker, and typing an
absolute path offers the same upsert inline. modLabel/comboHint live on
PaletteItem, so the next modifier-variant row gets both for free.
2026-07-30 01:15:01 -05:00
Brooklyn Nicholson
5affcd6bf4 feat(desktop): open a folder as a project with ⌘O
workspace.openFolder (default mod+o, the editor-standard open-folder chord)
runs openFolderAsProject: pick a folder, enter the project that already owns
it or create one named after the folder, scope the sidebar, and land on a
fresh session draft anchored there. A stale backend without the projects.*
RPC still gets the workspace session, with a warning.

StartWorkSessionRequest grows an openTab flag so these opens-from-nowhere
stack a tab instead of spending an occupied main, and goToProject/
resolveNewSessionCwd share one projectRootCwd resolver.
2026-07-30 01:15:01 -05:00
Brooklyn Nicholson
7556c51234 fix(desktop): focus the tab restored by undo-close (⌘⇧T)
Adoption alone is silent, so reopenLastClosedTile only restored placement
and left the tab behind the still-fronted workspace. Focus it after open.
2026-07-30 01:14:11 -05:00
brooklyn!
382282d5a0
Merge pull request #74610 from NousResearch/bb/checkbox-indeterminate
fix(desktop): stop the checkbox painting the check and dash at once
2026-07-30 01:06:49 -05:00
brooklyn!
eefcc098a7
Merge pull request #74611 from NousResearch/bb/composer-strips-outside
Move the composer strips out of the pop-out drag region
2026-07-30 01:05:45 -05:00
brooklyn!
fa183062e4
Merge pull request #74533 from NousResearch/bb/emoji-reactions
feat: iMessage-style emoji reactions on desktop — opt-in, two-way, persistent, model-aware
2026-07-30 01:02:14 -05:00
Brooklyn Nicholson
8c76fe19f8 fix(update): let the GUI updater's hermes update child pass the lock it already holds
The cross-process update lock (fe8e4d93d) made the in-progress marker
mutually exclusive across every update entrypoint — but the Tauri
updater holds that marker for its WHOLE run and then spawns
hermes update as a child stage. The child read the marker, found its
own parent's live pid, refused with exit 2, and the GUI mapped that to
"Hermes is still running. Close all Hermes windows and try the update
again." Retry spawns a fresh updater that deadlocks against itself the
same way, so every GUI-driven update dead-ends on the failure screen
with no winnable retry (observed: three consecutive self-refusals in
bootstrap-installer.log within 90 seconds).

Hand the claim off explicitly: update_child_env exports
HERMES_UPDATE_HANDOFF_PID naming the updater's own pid, and
UpdateLock.acquire treats a live holder matching that pid as the lock
we are already running under — run without claiming, and release
leaves the parent's marker untouched. The env var alone grants
nothing: the pid must also be the live marker owner, so a stale or
forged value cannot bypass the lock, and a dashboard-spawned
hermes update (no handoff env) is still refused exactly as before.
2026-07-30 00:51:33 -05:00
brooklyn!
98e43be8e0
Merge pull request #74602 from NousResearch/bb/pointer-intent
Keyboard-first pickers: give typing focus back, ignore a parked cursor
2026-07-30 00:41:45 -05:00
Brooklyn Nicholson
e17d058269 refactor(desktop): move the composer strips out of the drag region
The micro-action pills, the status stack, and the underside strip all
rendered inside the composer root. The pop-out drag region is an
`absolute inset-0` child of that root, so everything alongside it was
inside the grab area by construction — hovering a pill hatched the
composer and a press between two badges started a peel-out drag.

That was being patched at the gesture instead of the structure: a
`composer-no-drag` exclusion in gestureTargetOk, a matching guard on the
double-click toggle, and a strip that juggled z-index and pointer-events
to climb back over a region painting on top of it.

Introduce a dock column that owns the composer's position and stacks its
children in flow, bottom-anchored:

    composer-dock
    ├── micro-action strip
    ├── status stack
    ├── composer            <- drag region lives in here, and only here
    └── underside strip

The strips are siblings of the composer rather than children, so landing
in the grab area is impossible rather than excluded. gestureTargetOk and
the double-click handler go back to what they were, and the shared strip
constant drops to a bare flex row.

Two things fall out of the move:

Alignment stops needing a magic number. The strips sat in different
containing blocks — one in the stack's absolute lane resolving against
the root's padding box, one in the root's content box — and the root
carries `padding-inline: 5px` for the grab margin, which is the 5px the
pills hung left by. One parent and a shared `px-[5px]` line both strips
up with the surface directly.

The stack stops measuring itself. It published
--status-stack-measured-height only because it was out of flow and the
composer's measurement couldn't see it. As a dock child it's covered by
the dock's own height, so the var, the ResizeObserver effect, and the
detached-node cleanup it needed all go, and thread clearance reads one
number instead of summing two.
2026-07-30 00:18:29 -05:00
Brooklyn Nicholson
f790d5a6ce fix(desktop): stop the checkbox painting the check and dash at once
codicon.css styles glyphs through `.codicon[class*='codicon-']`, a
two-class selector that outranks Tailwind's single-class `hidden`. The
indicator stacks a check and a dash and hides one per state, so neither
was ever hidden and the box rendered both glyphs side by side, spilling
past its 16px bounds in every state including unchecked.

Use the important modifier on both display utilities so state, not
stylesheet order, decides which glyph shows.
2026-07-30 00:17:26 -05:00
hermes-seaeye[bot]
ba7d214b6a
fmt(js): npm run fix on merge (#74605)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-30 05:13:41 +00:00
Brooklyn Nicholson
745d1383cb fix(deps): add frimousse + emojibase to the lockfile without pruning other platforms
npm install on macOS dropped all 26 @esbuild/* cross-platform entries (443
lines) — that breaks Linux/Windows installs. Restored main's lockfile and
merged in only the three genuinely new entries.
2026-07-30 00:08:28 -05:00
Brooklyn Nicholson
fec1ac0a7a feat(desktop): reactions are opt-in under Settings → Appearance, off by default
One lever, every surface. The renderer toggle persists locally and mirrors
into display.message_reactions; the backend gates the agent's
react_to_message tool (check_fn) and the model-context annotation on the same
key, and the ':' composer trigger reads the store at detection time. Off
means off everywhere: no ☺ slot, no right-click picker, no :shortcode:
popover, no agent reactions, and the model hears nothing — while reactions
already persisted keep rendering so history doesn't lose data. Also fixes the
import-order lint error CI flagged in composer/index.tsx.
2026-07-30 00:08:28 -05:00
Brooklyn Nicholson
a90ccd46b7 feat(desktop): :shortcode: emoji completions in both composers
A third trigger kind beside @ and / — same detection, same popover, same
commit path. :jo opens 😂/🤣/… fed by the bundled emojibase shortcode data
(search hits shortcodes first, then tags and labels); picking inserts the
emoji character as plain inline text, not a chip. Boundary-anchored with a
two-char minimum so localhost:8080, timestamps, and :D never trigger it.
Wired in the main composer and the edit composer's duplicated trigger loop.
2026-07-30 00:08:28 -05:00
Brooklyn Nicholson
6ec319f530 feat(desktop): two-way reaction UI — tapback pill, frimousse picker, live paint
One slot, Slack-style: on assistant rows the picker trigger and the landed
reaction are the same far-right element, so reacting never shifts layout
(empty → ☺ following the action bar's hover fade; reacted → the emoji, always
visible, always full-strength). User bubbles react via right-click and show
the badge beneath, in the checkpoint row's register.

- Clicks paint instantly from a local nanostores overlay — no round-trip in
  the loop; the RPC persists behind it and rolls back visibly on rejection
- Agent reactions land via the message.reaction event into an overlay keyed
  by DURABLE row id, so the end-of-turn resume (which regenerates renderer
  ids and rebuilds from in-memory history) can't clobber the paint
- Full picker is frimousse behind the six-emoji quick row, fed from bundled
  emojibase-data served at ./emojibase by a small vite plugin (offline, no
  CDN), with Slack-style alternating cell tints keyed off the codepoint
- Reaction picker opts out of the shared popover glass: solid surface so
  15%-alpha hover tints stay readable
- react_to_message tool blocks are suppressed in the transcript (like todo):
  the reaction appearing IS the UI; failures still render
- rowId reaches rehydrated messages from both transcript shapes (gateway
  row_id, REST numeric id)
2026-07-30 00:08:28 -05:00
Brooklyn Nicholson
aaf3298d61 fix(desktop): pickers stop eating the keyboard and the pointer
Committing a model with Enter left focus on the pill (Radix restores it
to the trigger), so the next thing typed went nowhere instead of into
the message being written. And with the cursor parked over the list,
rows re-flowing under it as the query narrowed hover-stole the selection
mid-type — Enter then committed whatever the mouse happened to be over.

Both surfaces adopt the shared primitives: the model menu and every cmdk
list (palette, model dialog, session picker, searchable selects) go
pointer-inert until the mouse moves, and the model menu plus the command
palette hand typing focus back on close. The release defers a frame and
yields when something editable already claimed focus, so a palette
action that opens a dialog or navigates keeps its own focus.

Replaces the model menu's focus/blur highlight gate — pointer intent is
the real signal, so the keyboard highlight no longer flickers off when
Radix moves DOM focus onto a hovered row.
2026-07-30 00:06:52 -05:00
Brooklyn Nicholson
913882cdbb feat(desktop): keyboard-first overlay primitives
Two rules every hotkey-opened, search-driven overlay needs, in one place
so each picker doesn't reinvent them:

usePointerQuiet — a mouse that is merely PRESENT is not a mouse in use.
A list that opens under a parked cursor, or re-flows under one as its
filter narrows, fires pointerenter on whatever row slides beneath; menus
that select on hover take that as intent and steal the row you typed
toward. The pointer stays inert until it actually moves (or scrolls),
then hover works for the rest of the overlay's life.

releaseTypingFocus — dismissing the overlay ends its claim on the
keyboard. Handlers subscribe once, so the primitive stays ignorant of
what typing means on any given surface.
2026-07-30 00:06:52 -05:00
Brooklyn Nicholson
0ad06ae9da fix(desktop): keep ⌘1-9 working when the pointer is off the panes
Hover-first targeting (#74447) made the tab verbs read the hovered zone
else the focused one. But ⌘1-9 and ⌃Tab then took that single answer as
final: with the pointer over the sidebar, the titlebar, or any non-zone
chrome, the resolver returned null and the keys did nothing at all.

Turn the resolver into an eligibility ladder — hovered, then focused,
then the workspace's zone — where each rung must actually satisfy the
verb (a real tab strip for the number keys, a chat strip for ⌃Tab and
the ⌘W / ⌘T family) to claim the keystroke. Pointing at nothing now
falls through to focus, and a fresh window with no interaction yet falls
through to main, so the keys always land somewhere sensible.

This also fixes the narrower case the old code shared with ⌘W: hovering
a zone that is NOT a tab strip (a lone file tree) used to swallow the
key rather than handing it to the next rung.

⌘W / ⌘T already laddered to the workspace, so they keep their behavior
and simply share the one resolver again.
2026-07-29 23:58:41 -05:00