Group the flat recents list and entered-project lanes by recency: an
unlabelled head of the newest run of sessions (cut at a real break in
activity, sized toward the most recent handful), then one divider per
coarse calendar range — Earlier today / Yesterday / Earlier this week /
Last week / Earlier this month / month / month + year. Empty ranges are
skipped, the first rendered group is never labelled, branch clusters
never split, and hand-ordered lists / pinned / project previews stay
divider-free.
forkBranch called session.create without a profile, so in app-global
remote mode (one backend serving every profile) a branch of a
non-default-profile chat silently landed on the launch profile — the
"session jumps between profiles after branching" bug. New chats already
carry their profile (desktopSessionCreateParams, #39993); branching was
the remaining create callsite without it.
Thread the parent's owning profile through both branch entry points:
branchStoredSession already resolved it (#67603) but dropped it before
the create; branchCurrentSession now resolves the open chat's profile
via the same cache → active → cross-profile ladder as resume. forkBranch
swaps the live gateway onto that profile and passes it on session.create,
which also makes the optimistic sidebar row's profile stamp correct.
Supersedes #40788 — same invariant, rebuilt for the current forkBranch/
branchStoredSession split.
Co-authored-by: Dusk1e <yusufalweshdemir@gmail.com>
Flips the default fan-out cadence from per_iteration (advisors re-run on
every tool iteration, multiplying advisor spend by tool-loop depth) to
user_turn (advisors run once on the first message of each user turn; the
acting aggregator works the rest of the tool loop with that turn's
advice). Until per-mode benchmarks justify a costlier default, MoA
defaults to the cheapest, lowest-impact cadence (#67199).
One default for everyone — no split legacy/new-preset semantics; presets
that want per-step advising set fanout: per_iteration explicitly. All
three modes (user_turn / per_iteration / every_n:N) remain selectable;
every_n:1 still collapses to per_iteration (semantic identity), while
unparseable values now fall to user_turn (the default).
Docs updated with a default-change note; the per-iteration rerun test
pins its mode explicitly.
Co-authored-by: skyer-flyyy <188930297+skyer-flyyy@users.noreply.github.com>
Follow-ups for salvaged #53784:
- reference_timeout now defaults to None = no per-preset override, so the
reference fan-out inherits auxiliary.moa_reference.timeout (900s default)
via call_llm's own per-task timeout resolution. The PR's 30.0s default
would have cut off long-thinking advisors mid-response, and its 300s max
cap capped legitimate explicit values — both removed. Explicit per-preset
values are still honored as-is.
- _is_failed_reference also treats '[skipped: …]' recursion-guard notes as
internal sentinels, keeping them out of both aggregator prompts.
- Dashboard/desktop TS types updated to number | null; web_server validator
accepts null/empty as 'inherit'.
Follow-up for salvaged PR #59753 rebased over the per-slot
reasoning_effort feature: _clean_slot now round-trips reasoning_effort
AND enabled together; add a normalize→normalize regression test, update
the validate/normalize agreement contract for the canonical enabled
default, restore the desktop per-slot toggle test on the current
autosave editor, and map oppenheimor's contributor email.
Frontend consumers for the events added by PR #59646: the TUI shows a
replace-in-place 'MoA: refs k/n' activity line (swapped for 'MoA:
aggregating…' on the aggregator phase), and desktop streams '◇ MoA refs
k/n' lines into the reasoning disclosure, self-cleaned by the first
moa.reference block.
Salvaged from PR #59743. Original author email was malformed
(sr@samirusani, not resolvable to a GitHub account), so the commit is
re-authored with credit via trailer.
Co-authored-by: Sami Rusani <samrusani@users.noreply.github.com>
Every moa.reference event called appendReasoningDelta(..., replace=true),
which wipes ALL existing reasoning-type message parts and seeds exactly one
new part. With two or more MoA reference models, each later reference
erased the reasoning disclosure built by earlier references, so only the
last advisor's output ever stayed visible instead of one labelled block per
reference (contradicting the multi-reference visibility behavior from
#53855).
Only the first reference (index <= 1, or missing) now replaces — preserving
the original "clear stale reasoning from before this turn" behavior. Every
later reference accumulates via the existing queue-then-flush path instead,
applied immediately since each reference arrives as one complete block
rather than incremental tokens.
Fixes#64658
The renderer showed two assistant bubbles for one turn — a partial
streamed copy plus the clean final copy (#63679). A reload fixed it, so
the persisted transcript was correct; this was live reconciliation.
Root cause is in completeAssistantMessage (use-message-stream/index.ts).
message.interim fires for BOTH verify-on-stop candidates AND ordinary
tool-call turns (tui_gateway _load_interim_assistant_messages), and the
interim seal clears streamId + sets interimBoundaryPending. So at
message.complete the streamId fast-path is skipped and it enters the
fallback. There the settle-onto-interim branch was gated on
responsePreviewed — true ONLY for verify-on-stop. A normal tool-call
turn whose final text matched its sealed interim satisfied neither
settle branch and fell through to append a brand-new bubble. Two rows,
distinct ids, id-based dedup cannot collapse them → renders twice.
Fix: settle onto the sealed interim whenever the final CONTINUES it —
final == interim, final starts with interim (streamed + trailing delta),
or interim starts with final (streaming dropped characters). This is
gated on existing.interim so it only ever collapses a genuine sealed
interim, and a genuinely DIFFERENT final still appends as its own
bubble. responsePreviewed is retained as an OR so the verify-on-stop
continuation-budget case (final text rewritten, no shared prefix) still
settles as before.
The prior test that asserted a non-previewed identical interim+final
produced TWO bubbles was encoding the bug; rewritten to assert one.
Added coverage: prefix-extended non-previewed final collapses; a
genuinely different final still appends (no over-collapse).
Community analysis on the issue (seedSeenBubbleKeys / kind-guard) was
against a pre-refactor v0.7.0 bundle — those symbols no longer exist;
this is the current-code root cause.
Co-authored-by: SHL0MS <SHL0MS@users.noreply.github.com>
Follow-up to the salvaged PR #39439:
- Replace the hardcoded English gateway-auth summary with a
notifications.errors.gatewayAuthFailed i18n key (en/ja/zh/zh-hant + types)
- Fix the malformed ['"'] regex character classes (duplicate quote) in
notifications.ts error matchers
- Add regression test: gateway_auth_failed maps to the gateway auth
summary, provider invalid_api_key still maps to the OpenAI summary (#39365)
The api_server adapter returned error code "invalid_api_key" for
API_SERVER_KEY authentication failures, which the Desktop error
classifier misidentified as a provider (OpenRouter/OpenAI) key
problem — showing "OpenRouter API key missing" when the real issue
was gateway auth.
Changes:
- gateway/platforms/api_server.py: return "gateway_auth_failed" code
with descriptive message for API_SERVER_KEY auth failures
- apps/desktop/src/store/notifications.ts: add "gateway_auth_failed"
handler before "invalid_api_key" to show correct error message
- agent/error_classifier.py: add "gateway_auth_failed" to auth patterns
- tests: update test_session_api.py to expect new error code
Fixes#39365
* fix(desktop): hide persisted agent-only history scaffolding
Filter verification-stop nudges and context-compaction handoffs at the
stored-history mapper boundary. Preserve a real reply when a compaction
handoff shares its stored message.
* test(desktop): build persisted E2E sessions through the real agent
Drive tui_gateway.entry over its stdio JSON-RPC transport against the mock
provider, wait for real completion events, and persist normal session history
through AIAgent and SessionDB. Migrate resume and hidden-history coverage,
including real compression and live verify-on-stop scaffolding, then remove
the unused direct SessionDB import scripts.
* fix(desktop): use the provisioned Python for real-session E2Es
Run the stdio gateway through uv's synced project environment outside the
Nix dev shell, while retaining the fully provisioned Nix Python when the
shell advertises HERMES_PYTHON_SRC_ROOT.
* fix(nix): expose the provisioned Python environment to uv
Mark the Nix-built Python environment active in the dev shell so the shared
E2E session builder can always run through `uv run --active --no-sync`.
* fix(timeline): persist typed display events
* fix(timeline): strip display-only fields from provider payloads, preserve through rewrites, fix /resume display history
Three review findings from PR #69771:
1. Provider payload leak: display_kind and display_metadata were forwarded
to the provider API as unknown message fields. Strict OpenAI-compatible
backends can reject the next request after a model switch or resumed
typed event. Strip both from the per-request api_msg copy in
conversation_loop alongside the existing api_content pop.
2. Rewrite/import data loss: _insert_message_rows preserved display_kind
but silently dropped display_metadata. After replace_messages,
archive_and_compact, or session import, async-delegation completion
events lost their task counts and fell back to generic display text.
Add display_metadata to the INSERT columns and bind tuple.
3. CLI /resume stale recap: startup --resume A set _resume_display_history
from A's lineage. A subsequent in-session /resume B loaded B only into
conversation_history via get_messages_as_conversation, leaving the stale
A display projection. _display_resumed_history preferentially read the
stale attribute, showing A's recap for B. Switch /resume to
get_resume_conversations and update _resume_display_history alongside
conversation_history.
Tests: 890 Python (5 files), 35 desktop TS — all green.
* feat(tui): render typed display events as ◈ markers in the Ink TUI
The TUI was not handling display_kind at all — model switch markers and
async delegation completions rendered as opaque user messages with the
full [System: ...] text, and hidden compaction handoffs were visible.
Wire display_kind through the full TUI chain:
- _history_to_messages (tui_gateway/server.py) forwards display_kind
and display_metadata to the gateway transcript payload.
- GatewayTranscriptMessage (gatewayTypes.ts) gains both fields.
- Msg.kind (types.ts) gains 'event' value.
- toTranscriptMessages (domain/messages.ts) maps:
- hidden → skip entirely
- model_switch → event "model changed"
- async_delegation_complete → event "N background agents finished"
(or "background agent work finished" without metadata)
- messageGroup (blockLayout.ts) routes event to its own group, with
SELF_SPACED + PAINTS_TRAILING_GAP so it owns its margins.
- messageLine.tsx renders event-kind as a dim ◈ marker with no gutter,
matching the CLI's ◈ event rendering.
- 4 new TUI tests for hidden/model_switch/async_delegation mapping.
TUI typecheck: clean. TUI lint: 0 errors (2 pre-existing warnings).
TUI tests: 9 passed (1 pre-existing failure on main, unrelated).
Guard the globalThis gateway-state container and the survivor park/adopt
calls on the import.meta.hot literal (not the runtime hmrActive() helper),
so Vite dead-code-eliminates every HMR path in production. Prod now uses a
plain module-local singleton — no globalThis, no Symbol.for — and the
survivor module drops out of the bundle entirely. Verified: gatewayRegistryState,
gatewaySurvivor, and import.meta.hot are all absent from the prod build.
Removes the now-unused hmrActive() export.
Park the live primary gateway socket on Fast Refresh dispose and re-adopt it
on remount so dev UI edits don't tear down the WebSocket. Hold gateway store
singletons on globalThis + self-accept HMR on store/gateway.ts. Prod strips
import.meta.hot — live unmount unchanged.
Two fixes for desktop hands-free voice:
- The live speech session bound to the first assistant bubble with text, so
a tool-calling turn spoke only the opening narration and silently dropped
every later interim AND the final answer. The conversation selector now
aggregates all unspoken assistant bubbles in order (turn-scoped speech);
auto-speak keeps its latest-reply-only behavior.
- The speak-stream WS producer blocked forever on the text queue, so a
narration line with no trailing whitespace ("Let me check.") sat in the
sentence chunker until end-of-turn — spoken long after the tool finished,
with the UI stuck on "Preparing audio…". Mirror the CLI speaker's idle
flush: sentence-terminated buffers flush after 0.5s of producer silence,
anything else after ~2s; open <think> blocks are never flushed.
Ctrl+Shift+C (and window.__creditsDemo()) steps the full credit-notice
lifecycle (usage 50->75->90, grant-spent, depleted/restored) through the real
gateway event fan-out via a new emitLocalGatewayEvent, so the toast/native/
billing-invalidation paths are testable without hitting real usage bands.
Installed only under import.meta.env.DEV, so it's tree-shaken from production.
The x-nous-credits-* headers are best-effort and can drift out of sync,
notably in team/org accounts where another member's spend moves the shared
balance without touching this client's headers. The billing endpoint is the
source of truth, so the page no longer trusts a cache: staleTime 0 +
refetchOnMount 'always' force a fresh fetch on every open and focus (still
polling 30s while mounted). The credits.* invalidation nudge still pulls a
crossing in immediately.
Three fixes to how agent credit notices render as toasts:
- Strip the leading severity glyph (the toast already draws a kind icon, so the
raw text doubled it). Native OS notifications keep the glyph (no icon there).
- Icon top-margin is now 0.42ch (font-relative) instead of a fixed rem.
- Band-color the $used figure (semibold) by $used/$cap: muted <75%,
--ui-orange >=75%, --ui-red >=90% (depleted red, restored green), reusing the
existing --ui-* usage palette. Icon shares the accent.
- Split a trailing '. detail' into a muted secondary line (title+description
convention) instead of an inline middot.
Generic 'accentColor' + 'meta' slots on the notification; degrades gracefully
when a notice has no figure.
Round out the credit-notice handling from the previous commit with the two
optional pieces from the issue:
- Native OS notification for the urgent pair. `credits.depleted` /
`credits.restored` also fire an Electron notification when Hermes is
backgrounded, via a new `credits` NativeNotificationKind (the existing
five didn't fit) with its own toggle in Settings → Notifications (the
panel is data-driven off NATIVE_NOTIFICATION_KINDS, so the toggle and
i18n are the only additions). The escalating usage line and grant-spent
notice stay in-app toasts only. Dispatch is `global` (account-wide, not
session-bound) and gated by the user's prefs + backgrounded check.
- Billing-page nudge. A `credits.*` crossing invalidates the
`['billing','state']` query so Settings → Billing reflects the change
immediately instead of waiting up to 30s for its poll.
`nativeNoticeInput()` is a pure mapping (urgent-key gate → native input),
unit-tested directly; the gateway-event branch does the localized-title
lookup and gated dispatch. i18n added for all four locales.
The desktop renderer had no handler for the `notification.show` /
`notification.clear` WS events, so every credit-usage notice the backend
sends (`agent/credits_tracker.py` → `tui_gateway/server.py`) was silently
dropped. Credit warnings like "• Credits 50% used · $220.00 cap" never
appeared, even though the Ink TUI renders them in its status bar.
Add the two missing branches to the gateway-event dispatcher, delegating
to a small, pure-testable module:
- `store/agent-notices.ts` — `noticeToToast()` maps a notice to a toast
(level → toast kind, sticky → durationMs 0, ttl → ttl_ms), and uses the
notice `key` as the toast id. Re-emitting the same key REPLACES the
toast, so the credits 50→75→90 line escalates in place instead of
stacking, and a key-matched `notification.clear` maps straight to
`dismissNotification(key)`.
- The notice `text` already carries its own glyph (• ⚠ ✕ ✓), so no toast
icon is added.
- Notices are account-wide, so the toast shows regardless of which
session is focused.
The Ink TUI (`ui-tui/src/app/turnController.ts`) is the reference for the
latest-wins / sticky-vs-ttl / key-matched-clear behavior.
Export `NotificationInput` so the mapping's return type can be named.
* fix(desktop): improve fallback tool-call details
Show failed image-generation calls through the normal fallback row, remove duplicate normal-mode web-search JSON, and format Technical Mode payloads as readable JSON.
* feat(desktop): render terminal tool calls as transcripts
Show terminal commands with a prompt and exit status, then reveal ANSI-safe stdout and stderr in the expanded tool row.
* fix(desktop): reconcile tool calls by command
Match context-only tool starts with command-bearing completions when their IDs differ, preventing stale duplicate terminal rows. Show the web-search query above its result cards.
`resumeTile` — the cold-resume path for a session opened in a tile / split
pane — resumed with `{ session_id, cols }` and read messages with no profile,
so a tile opening a session from another profile let the gateway fall back to
the launch-profile DB and fork the conversation into the wrong profile: the
same cross-profile bleed the recovery resumes had (#67603), just a sibling
call path. Resolve the owning profile via the shared `resolveSessionProfile`
and carry it on both the transcript prefetch and the resume RPC.
Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com>
Narrow the setup-error matcher, match the server's empty-key contract,
and route gateway-event plus create/resume/branch through one
credential-warning policy. Preserve configured=true only for
non-authoritative transport fallback.
Co-authored-by: Yingliang Zhang <zhangyingliang@outlook.com>
Co-authored-by: Brandon R <kingdomwarrior23@gmail.com>
* fix(desktop): stop model-switch dup + route recovery resumes to the owning profile
Fixes two Desktop session-reconciliation symptoms from #67603.
Symptom 1 — duplicated user bubble after a model switch. The gateway
persists model-switch / personality notices as role=user `[System: …]`
rows (tui_gateway/server.py) so strict OpenAI-compatible providers don't
reject a non-leading system message (#48338). `preserveLocalPendingTurnMessages`
paired local optimistic rows with the stored transcript by user-role
ordinal, so a marker between two real user turns shifted every later
ordinal and the optimistic row was re-appended at the bottom. The single
trailing-marker case is already covered by the compression-era
`latestAuthoritativeUser` guard, but two switches around one turn (marker
before AND after the committed prompt) still duplicated it. Exclude
`[System:` bookkeeping markers from ordinal pairing on both sides.
Symptom 2 — a session appearing under two profiles. The main resume path
already resolves a session's owning profile via `resolveStoredSession`
(cache → active backend → cross-profile probe), but the recovery
`session.resume` calls (stale runtime id, session-not-found, wedged loop,
redirect) omitted `profile`, so the gateway fell back to the launch-profile
DB and forked the conversation into the wrong profile. Route every recovery
resume — and an uncached right-click branch — through the same resolver so
the profile is carried even for sessions outside the paginated sidebar
window (the cache-miss gap).
Tests: discriminating two-switch marker test (fails before, passes after);
cache-hit + cross-profile cache-miss coverage for the recovery resume and
for branching an uncached session.
Supersedes #68665 and #63590.
Closes#67603.
Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com>
Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com>
* fix(desktop): scope the remembered session id per profile
A single global `hermes.desktop.lastSessionId` key remembered ONE session
across every profile, so a relaunch or cold start under profile B would try
to restore a session owned by profile A — reinforcing the impression that a
conversation had bled between profiles (#67603, second symptom).
Key the remembered id by the session's owning profile (resolved from the
session row's `profile`, falling back to the active gateway profile), read it
back for the active profile on restore, and clear an exhausted session under
its owner. The default profile keeps the original unsuffixed key so existing
installs' remembered session survives the upgrade.
Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com>
* fix(gateway): hide [System:] bookkeeping markers from every transcript
Model-switch and personality notices are persisted as role=user `[System: …]`
rows so strict providers accept them mid-history, but they are model-facing
runtime metadata, not user turns. `_history_to_messages` — the single display
projection every client reads — passed them straight through, so on resume or
reload they rendered as a fake user bubble in the desktop, TUI, CLI, and web
transcripts.
Drop them in that projection. The raw marker stays in `session["history"]`
for the model, so nothing changes for inference; only the display loses a row
that never belonged to the user. This also removes the stored marker from the
payload the desktop reconciles against, killing the ordinal shift that
duplicated the optimistic prompt (#67603) at its source — the desktop-side
marker exclusion remains as a fallback for older backends.
Co-authored-by: Dolverin <Dolverin@users.noreply.github.com>
---------
Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com>
Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com>
Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com>
Co-authored-by: Dolverin <Dolverin@users.noreply.github.com>
A single global `hermes.desktop.lastSessionId` key remembered ONE session
across every profile, so a relaunch or cold start under profile B would try
to restore a session owned by profile A — reinforcing the impression that a
conversation had bled between profiles (#67603, second symptom).
Key the remembered id by the session's owning profile (resolved from the
session row's `profile`, falling back to the active gateway profile), read it
back for the active profile on restore, and clear an exhausted session under
its owner. The default profile keeps the original unsuffixed key so existing
installs' remembered session survives the upgrade.
Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com>
Fixes two Desktop session-reconciliation symptoms from #67603.
Symptom 1 — duplicated user bubble after a model switch. The gateway
persists model-switch / personality notices as role=user `[System: …]`
rows (tui_gateway/server.py) so strict OpenAI-compatible providers don't
reject a non-leading system message (#48338). `preserveLocalPendingTurnMessages`
paired local optimistic rows with the stored transcript by user-role
ordinal, so a marker between two real user turns shifted every later
ordinal and the optimistic row was re-appended at the bottom. The single
trailing-marker case is already covered by the compression-era
`latestAuthoritativeUser` guard, but two switches around one turn (marker
before AND after the committed prompt) still duplicated it. Exclude
`[System:` bookkeeping markers from ordinal pairing on both sides.
Symptom 2 — a session appearing under two profiles. The main resume path
already resolves a session's owning profile via `resolveStoredSession`
(cache → active backend → cross-profile probe), but the recovery
`session.resume` calls (stale runtime id, session-not-found, wedged loop,
redirect) omitted `profile`, so the gateway fell back to the launch-profile
DB and forked the conversation into the wrong profile. Route every recovery
resume — and an uncached right-click branch — through the same resolver so
the profile is carried even for sessions outside the paginated sidebar
window (the cache-miss gap).
Tests: discriminating two-switch marker test (fails before, passes after);
cache-hit + cross-profile cache-miss coverage for the recovery resume and
for branching an uncached session.
Supersedes #68665 and #63590.
Closes#67603.
Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com>
Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com>
- Replace undefined CSS class with Tailwind v4's
so long choice text wraps properly
instead of overflowing the button container.
- Add validation that strips non-string items,
blanks, newlines, and text >200 chars — preventing garbage/JSON
arrays from rendering as raw values.
- Add diagnostic logging at both the gateway event
handler and the tool-args parser when choices are dropped, so
malformed payloads are no longer silent.
- Apply in both the gateway event handler
() and the inline tool-args parser
() for consistent defense in depth.
- Add unit tests for covering null, non-array,
mixed-type, blank, multiline, and overlong inputs.
Closes#69122
Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
* fix(desktop): keep clarify lifecycle when tool progress is off
* fix(desktop): render clarify prompt from the request event
Re-authored onto the current use-message-stream/gateway-event.ts (the
original patched the pre-split use-message-stream.ts). When the tool.start
row that normally mounts the inline clarify UI is missed (stream reconnect
/ hydration race), upsert a stable pending clarify tool row from
clarify.request itself so the prompt stays answerable; a real
tool.start/complete with the same request id merges rather than duplicates.
Co-authored-by: 정수환 <centerid@naver.com>
* chore(contributors): map centerid@naver.com -> lidises
Attribution mapping for the salvaged #47544 commit.
* fix(desktop): correlate clarify rows by question so hydration can't duplicate
The hydrated row (from clarify.request's request_id) and the real tool.start
row (the model's tool_call_id) have different ids, so id-only matching appended
a second clarify card in the normal path (caught by the BLOCKING_CLARIFY e2e:
'question' resolved to 2 elements). Add 'question' to the tool match-value keys
so a clarify upsert merges into the existing pending clarify row regardless of
id (same request<->args correlation ClarifyToolPending already uses); when no
row exists yet (reconnect/hydration) it still creates one.
---------
Co-authored-by: 정수환 <centerid@naver.com>
`ExpandableBlock` used a single full-width `absolute inset-x-0 bottom-0`
button that was BOTH the fade overflow cue and the expand/collapse control.
It sat on top of the whole 28px bottom strip, so it swallowed:
- the horizontal scrollbar of a wide code block (couldn't scroll sideways), and
- pointer/drag events on the block's last line (couldn't select/copy the tail).
Split the two responsibilities: the full-width fade stays as a pure
`pointer-events-none` cue, and the only clickable target is a compact toggle
pinned to the right edge (`pointer-events-auto`), clear of the draggable
scrollbar track. The inner container gains `overflow-x-auto` so wide code
gets a working scrollbar. Applies uniformly to code cards and the plain-text
fallback — no per-call prop needed.
Regression test asserts the pointer-events contract (fade `pointer-events-none`
+ full-width, toggle `pointer-events-auto` + right-pinned, not `inset-x-0`) and
that the toggle still flips `aria-expanded`.
Supersedes #69558, #69428Fixes#69168
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: BerneYue <xiongyue_hnu@163.com>
Keep Enter as the Cursor-style live-turn steering gesture and assign
Ctrl/Cmd+Enter to queue the current draft. Align keybind settings,
tooltips, translations, and the Electron queue-boundary coverage.