Commit graph

789 commits

Author SHA1 Message Date
Brooklyn Nicholson
5a6720b884 fix(desktop,tui-gateway,zai): stop thinking-off from reverting to medium
A Z.ai desktop user reported thinking reverting to medium after one turn,
burning ~200% of a week's credits in 4 days despite reasoning_effort: false
in config.yaml. Four compounding bugs:

- _session_info reported reasoning_effort "" for disabled reasoning,
  indistinguishable from unset — the desktop adopted it after the first
  turn, wiping its sticky "thinking off" pick so every later chat
  reverted to the default effort.
- config.set key=reasoning always wrote agent.reasoning_effort to global
  config.yaml, so every desktop model-menu selection (preset.effort ??
  'medium') clobbered the user's configured value. Now session-scoped
  like the messaging gateway's /reasoning, landing on
  create_reasoning_override so lazily-built sessions keep it too.
- YAML `reasoning_effort: false`/`off`/`no` (boolean False) was coerced
  to "" by every loader's `str(x or "")`, silently re-enabling thinking.
  parse_reasoning_effort now treats False/"false"/"disabled" as
  {"enabled": False}; loaders (tui gateway, gateway, cli, cron,
  delegate) pass the raw value through. The desktop config reader also
  crashed on the boolean (false.trim()), aborting voice/STT settings.
- The zai provider profile never sent thinking on the wire, and GLM-4.5+
  defaults to thinking ON server-side — so disabling reasoning was a
  silent no-op on direct Z.ai, the actual token burner. The profile now
  emits extra_body.thinking {"type": "enabled"|"disabled"} for
  thinking-capable GLM models, mirroring the DeepSeek profile.

Also: /new (session reset) now carries reasoning_config across the
rebuild like model_override; config.get reasoning prefers the session's
live value and maps a config False to "none"; Settings shows "Off"
instead of a blank select for hand-written false.
2026-07-02 15:23:47 -05:00
Tranquil-Flow
c3f06a8fda fix(desktop): refresh profile rail after deletion (#49289) 2026-07-02 15:18:49 -05:00
liuhao1024
c5e8a60b0a fix(desktop): skip ensureBackend after profile-delete teardown to prevent respawn loop
When the renderer sends a DELETE /api/profiles/{name} request, the IPC
handler tears down the profile's pool backend (or primary backend) via
prepareProfileDeleteRequest.  However, the very next line calls
ensureBackend(profile), which spawns a fresh pool backend for the just-
deleted profile.  The new backend's startup path calls ensure_hermes_home(),
which recreates the profile directory — defeating the deletion and leaving
the process as a zombie.

On the next Desktop restart the cycle repeats: the profile directory exists,
the Desktop spawns a backend, the backend recreates the directory after
deletion, and PIDs accumulate indefinitely.

Fix: make prepareProfileDeleteRequest return the torn-down profile name.
The IPC handler uses this to route the DELETE to the primary backend
instead of spawning a new pool backend for the deleted profile.

Fixes #52279
2026-07-02 15:18:49 -05:00
Jaaneek
5ef0b8acb0 feat(auth): make xAI Grok OAuth device-code-only, drop loopback login
Replace the loopback/PKCE-callback server and manual-paste fallback with
the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The
flow works in headless/SSH/container sessions with no 127.0.0.1 listener,
shrinking the local attack surface.

- Poll the token endpoint with server-provided interval, honoring
  slow_down and expires_in; store tokens with auth_mode
  oauth_device_code.
- Adaptive proactive refresh skew for short-lived device-code JWTs;
  rotated tokens sync back to auth.json, the global root store, and the
  credential pool (no refresh-token replay).
- Clear source suppression on successful re-login (CLI + dashboard) and
  drop the duplicate dashboard pool entry so exactly one seeded
  device_code entry exists.
- Use the shared device_code source name for consistency with the
  nous/codex device-code providers.
- Desktop: remove the loopback OAuth flow states and dead type variants;
  pkce providers' sign-in URL selection is unchanged.
- Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted
  --manual-paste flag from documented commands.
2026-07-02 13:17:41 -07:00
brooklyn!
6cffc37b5a
feat(desktop): collapse profile rail to a select past 13 profiles (#57306)
The colored-square rail stops scaling once a user racks up many profiles:
tiny drag targets and an endless horizontal scroll strip. Past a threshold
(13) the rail swaps the squares for a compact select dropdown — same active
tint + initial glyph, minus the drag-reorder / long-press-recolor / per-row
context menu that only make sense at small counts. Two render paths behind
one flag; the left default↔all toggle, the "+" create button, and Manage
stay put in both. Rename/delete/color remain reachable via Manage.
2026-07-02 19:15:07 +00:00
helix4u
a9b5598909 fix(desktop): load remote model options before session
Both Desktop picker surfaces (status-bar model menu, settings/onboarding
dialog) only asked the connected gateway's model.options once a session
existed; before that they fell back to the Desktop REST/global options, which
can't see virtual providers a remote gateway exposes — including the MoA
presets from #53817. Centralize the fetch rule in requestModelOptions(): prefer
the connected gateway whenever one exists (no session_id needed — the RPC
resolves disk config), REST only when no gateway is connected.

The status-bar MoA preset section now renders from the same model.options
payload (the virtual `moa` provider row) instead of the local /api/model/moa
REST config, so remote presets appear correctly; the row is filtered out of
the main provider groups so presets don't list twice. Preset selection keeps
the persistent switchTo path from #56417 and drops the vestigial session gate —
like regular model rows, a pre-session pick ships on the next session.create.

Fixes #53817.

Rebased and reconciled with #56417 (persistent MoA selection), which landed
after this PR was opened and covered its one-shot-/moa half.
2026-07-02 12:50:46 -05:00
Brooklyn Nicholson
fe82b3a774 fix(desktop): read attachment previews local-first in remote mode
attachImagePath fetched its thumbnail through readDesktopFileDataUrl, which in
remote mode routes every read to the gateway fs bridge. Paperclip picks,
clipboard saves, and OS drops always produce paths on the LOCAL machine, so the
gateway read 404s — toasting "image preview failed" and dropping the thumbnail
even though the attach itself works (upload reads local bytes via the Electron
bridge). Read the local bridge first and fall back to the remote facade, which
still serves in-app drags from the remote project tree. Local mode is
unchanged (the facade already reads locally there).

Follow-up to #56572, which restored the remote paperclip picker and made this
path reachable from the picker as well.
2026-07-02 12:36:03 -05:00
helix4u
c19bfb50ad fix(desktop): restore remote file picker attachments 2026-07-02 12:32:30 -05:00
Brooklyn Nicholson
8da0a56ba8 style(desktop): fix pre-existing import-order lint in use-prompt-actions 2026-07-02 12:28:30 -05:00
Brooklyn Nicholson
931e2356af feat(desktop): /journey opens the memory graph overlay instead of printing text 2026-07-02 12:27:52 -05:00
Brooklyn Nicholson
42ca438131 style(desktop): fix import ordering + padding lint in remote-artifact files 2026-07-02 12:22:47 -05:00
helix4u
03406ae255 fix(desktop): restore remote artifact rendering 2026-07-02 12:22:47 -05:00
David Metcalfe
9738870489 fix(desktop): call checkUpdates() in startUpdatePoller so version pill auto-populates
startUpdatePoller() only called checkBackendUpdates() — never checkUpdates().
The statusbar version pill reads $updateStatus (set by checkUpdates()), so the
commit-behind counter stayed null after restart. It only appeared when the user
manually clicked the pill, which triggered checkUpdates() via openUpdateOverlayFor.

Added void checkUpdates() in three places alongside the existing
checkBackendUpdates() calls:
- On startup in startUpdatePoller()
- In the 30-minute setInterval callback
- In the onFocus handler

checkUpdates() uses the Electron IPC bridge (local git check), not the gateway,
so no mode gating is needed. The existing $updateChecking atom guard prevents
double-fire on overlap.

Fixes #53079
2026-07-02 11:52:57 -05:00
Brooklyn Nicholson
fb44b519d7 fix(desktop): parse multiline slash commands + hand degenerate payloads back
parseSlashCommand used /^(\S+)\s*(.*)$/ where `.` can't cross a newline and
`$` anchors end-of-string, so any slash command whose arg contained a newline
(/goal <multi-line text>, a skill command with a long pasted context) failed
the whole match, parsed as an empty name, and rendered "empty slash command"
while the payload vanished — cleared from the composer and absent from the
Up-arrow history ring, which only derives from sent user messages.

- name now splits on any whitespace ([\s\S]* arg), matching the CLI and the
  gateway's split(maxsplit=1); multiline args flow to slash.exec intact
- the residual empty-name branch (bare "/", "/ text") restores the submitted
  text to the composer draft instead of eating it

Fixes #41323. Fixes #55510.
2026-07-02 11:36:21 -05:00
teknium1
36b7e5e9cc fix(desktop): guard configured-cwd override against active sessions
Follow-up to the #39227 salvage: config refreshes fire mid-session too
(gateway events, settings saves), so applying terminal.cwd
unconditionally would yank the workspace out from under an attached
session. Gate the override on activeSessionIdRef like the sibling
reasoning/tier settings, keep branch refresh on the live cwd, and add
coverage for the active-session path. Also lint-polish the new test
file (typed config mock, prettier formatting).
2026-07-02 01:58:19 -07:00
Sahibzada Allahyar
d2de9580e1 fix(desktop): prefer configured workspace cwd 2026-07-02 01:58:19 -07:00
Teknium
9be292f1e6
fix(desktop): make MoA preset selection persistent, not one-shot (#54670) (#56417)
The MoA preset section in the composer model dropdown presented presets like
persistent model selections, but selecting one dispatched the one-shot `/moa`
command (command.dispatch name=moa) — it ran a single turn through MoA and then
silently reverted to the prior model. The user saw MoA context for one message,
then it vanished with no indication.

Route MoA preset selection through the same persistent path real provider
selections use: onSelectModel({ model: preset, provider: 'moa' }) →
config.set model="<preset> --provider moa" → the gateway's switch_model. The
check mark now reflects the real current selection (currentProvider === 'moa'
&& currentModel === preset) instead of transient local state, and the
now-unused activeMoaPreset state is removed.

Tests: new model-menu-panel.test.tsx (2) — selecting a preset calls
onSelectModel with provider 'moa' (persistent), and the check renders on the
active preset. tsc -b clean.
2026-07-01 06:40:20 -07:00
Teknium
1641441837
fix(desktop): don't false-timeout long prompt.submit turns (MoA, deep reasoning) (#56411)
prompt.submit is fire-and-forget — turn completion is signaled by stream /
message.complete events, not the RPC return — but it inherited the generic 30s
default RPC timeout. A turn that legitimately takes >30s to ACK (MoA presets
running references + aggregator in series, deep reasoning, large tool chains)
popped a false 'request timed out: prompt.submit' toast at 30s while the turn
was still running and streamed its real answer in 60-120s later (#55024).

Add PROMPT_SUBMIT_REQUEST_TIMEOUT_MS (1_800_000 = the backend's
agent.gateway_timeout ceiling) and pass it on all four prompt.submit call sites
(submit, resume-recovery retry, regenerate, rewind), mirroring the existing
SESSION_LIST_REQUEST_TIMEOUT_MS opt-out precedent. Widen the GatewayRequest
type (+ the inline requestGateway prop type) to carry the optional timeoutMs the
runtime impl already accepts.

Tests: use-prompt-actions/index.test.tsx 34/34 pass; tsc -b clean.
2026-07-01 06:33:47 -07:00
brooklyn!
44ddc552f5
Merge pull request #56029 from NousResearch/fix/desktop-drop-folder-attach 2026-06-30 22:08:21 -05:00
emozilla
a488fcf107 fix(desktop): detect dropped folders so they attach as @folder refs
Dragging a folder from Explorer/Finder into the composer failed with "file
not found on gateway and no data_url provided", on local gateways too.

extractDroppedFiles tagged every OS drop as a File-bearing entry, so
partitionDroppedFiles routed the folder to the upload pipeline and
file.attach tried to read a directory's bytes — a directory has none, and
there is no data_url to send. This regressed in 4906dcfc25, which routed
OS drops through file.attach to reach a remote gateway but did not exclude
directories, which also carry a File handle.

Detect directories at drop time with DataTransferItem.webkitGetAsEntry(),
the only synchronous way to tell a dropped folder from a file. A dropped
directory now becomes a path-only entry with isDirectory set, which routes
to a @folder: ref exactly like the folder picker, instead of the file
upload path that cannot stage a directory.

Process transfer.items before transfer.files: webkitGetAsEntry lives only
on items, and claiming the folder's path there first lets the files
fallback dedup skip the same entry (Chromium lists a dropped folder in
both). Path-based dedup and the getPathForFile resolution are preserved.
2026-06-30 22:36:37 -04:00
Brooklyn Nicholson
c6ba4b229e fix(desktop): lift memory-graph dark-mode line + outline alpha
Dark-mode connector lines and ring outlines read too faint. Double the
two live knobs: MODE_DEFAULTS.dark.lineAlpha 0.12->0.24 and
RING_PARAMS.dark.ringAlpha 0.03->0.06. (MODE_DEFAULTS.ringAlpha is dead;
the outline is drawn from RING_PARAMS.)
2026-06-30 16:44:04 -05:00
brooklyn!
b77ebb5999
Merge pull request #55871 from NousResearch/bb/desktop-composer-trigger
refactor(desktop): extract the composer trigger/completion engine into useComposerTrigger
2026-06-30 15:31:23 -05:00
Brooklyn Nicholson
e774d7fbf9 refactor(desktop): extract the composer trigger/completion engine into useComposerTrigger
The last welded composer engine. The `@`/`/` trigger state, detection
(refreshTrigger), the adapter-driven item list + its effects, popover selection,
closeTrigger, commitTypedSlashDirective, and the contentEditable chip insertion
(replaceTriggerWithChip) move verbatim into hooks/use-composer-trigger.ts behind a
hook that takes the editor refs + the two completion sources (at/slash). ChatBar's
input/keydown/keyup paths + the popover render consume the returned API; the
keydown navigation block stays in place (no key-handling restructure), and
triggerKeyConsumedRef is exposed so keyup still skips its post-consume refresh.

ChatBar 1,248 → 1,047. Behaviour-preserving: typecheck 0 errors, eslint clean,
and the composer DOM repro suite (slash-nav, enter-submit, IME composition,
trigger-popover) is green — the documented IME/caret/focus edge fixes ride along
verbatim. (The 1 attachments.test.tsx failure is pre-existing on main.)
2026-06-30 15:27:36 -05:00
brooklyn!
7098c2b71e
Merge pull request #55868 from NousResearch/bb/desktop-fallback-model
refactor(desktop): split tool fallback-model into a folder with leaf modules
2026-06-30 15:26:26 -05:00
Brooklyn Nicholson
322138df51 refactor(desktop): split tool fallback-model into a folder with leaf modules
fallback-model.ts (1,696) folded into assistant-ui/tool/fallback-model/ with
three cohesive, self-contained leaf modules extracted (verbatim moves):

- types.ts (83)   — the shared tool-view types/interfaces.
- format.ts (133) — pure value formatting/parsing (isRecord, compactPreview,
  clampForDisplay, prettyJson, parseMaybeObject, unwrapToolPayload, numberValue,
  contextValue, formatDurationSeconds).
- targets.ts (75) — url/path/preview detection + disclosure ids (looksLikeUrl,
  findFirstUrl, hostnameOf, isPreviewableTarget, toolPart/GroupDisclosureId).

index.ts (1,434) keeps the tool-specific assembler (TOOL_META, titles, the count
machinery, subtitle/detail/diff, buildToolView) and re-exports the leaf modules,
so consumers importing `./fallback-model` are unchanged (folder index resolution)
— no importer or channel edits needed. The count/result/detail helpers reach
across each other around buildToolView, so they stay together to avoid a circular
split; the three leaves are the clean cut.

Behaviour-preserving: typecheck 0 errors, eslint clean, fallback-model test 24/26
(the 2 browser_navigate title failures are pre-existing on main — `hostnameOf`
intentionally includes the pathname; verified identical on the un-split file).
2026-06-30 15:24:33 -05:00
Brooklyn Nicholson
05ed553c53 fix(journey): satisfy desktop eslint sort rules in star map
The merged #55859 left the star-map NodeContextMenu import and the
canvas onContextMenu prop out of perfectionist's required order, failing
`npm run lint` in the desktop workspace. Reorder both.
2026-06-30 15:16:18 -05:00
Brooklyn Nicholson
bb67dad07a feat(journey): edit/delete in TUI overlay and desktop star map
TUI /journey gets d/e with confirm + $EDITOR; desktop gets a right-click
context menu with inline edit modal. Both refresh the graph after mutation.
Extract openInEditor into the shared TUI editor helper.
2026-06-30 15:07:24 -05:00
Brooklyn Nicholson
9e4ed4d7a9 feat(installer): redesign the Tauri setup shim — design system, OS theme, granular updates
Bring Hermes-Setup.exe's UI onto the shared design tokens (self-contained, no
desktop-component coupling) and add two capabilities:

- design: flat stage rows (running step opaque, rest muted), neutral check /
  destructive cross, running fourier-flow Loader, hairline --stroke-nous
  borders, fill-less log panel; ported BrandMark (nous-girl) + HackeryButton +
  Loader standalone; re-synced button variants; de-boxed success/failure.
- theme: follow the OS light/dark via the authoritative Tauri window theme
  (theme.ts + onThemeChanged, core🪟allow-theme), with Nous dark seed
  colors in styles.css so the --ui-*/--dt-* chain derives correctly.
- updates: split the monolithic "Updating" bar into handoff -> download ->
  rebuild (+ install on macOS) stages via a shared update_stages() builder, a
  live elapsed timer on the running stage, and a dev-only fake-boot preview
  (gated on import.meta.env.DEV, stripped from the shipped bundle).
2026-06-30 14:46:28 -05:00
Brooklyn Nicholson
8fe8c2d6c4 style(desktop): bring the install & update overlays onto the design system
Align the first-launch install overlay and the in-app update overlay to
apps/desktop/DESIGN.md, reusing in-bundle primitives only (no new deps):

- install overlay: Loader2 spinners -> Loader (fourier-flow); emerald check /
  AlertTriangle -> neutral Codicon check + canonical ErrorIcon; de-boxed the
  failure block; hairline (--stroke-nous) command/code chips; --ui-* tokens;
  BrandMark header; flat stage rows (only the active step opaque).
- update overlay: drop the redundant DialogContent border (base Dialog already
  supplies shadow-nous + --stroke-nous); de-box the changelog; hairline +
  primary-flash manual command block; on-brand BrandMark for "all set".
2026-06-30 14:46:28 -05:00
Brooklyn Nicholson
b48fcfa8bd test(desktop): unit-cover the composer URL-dialog engine
Adds hooks/use-composer-url-dialog.test.tsx (renderHook): @url: directive
fallback, host onAddUrl preference + clear/close, and the blank-input no-op.
First unit coverage for an extracted composer engine — previously none of this
logic was testable while welded into the DOM-coupled ChatBar.
2026-06-30 14:27:19 -05:00
Brooklyn Nicholson
8795dfa331 refactor(desktop): extract composer pop-out engine into useComposerPopout
Moves the docked↔floating state, dock/float/toggle actions, drag-gesture wiring,
and the on-screen re-clamp effect out of ChatBar into
hooks/use-composer-popout.ts, verbatim. ChatBar passes its composerRef in and
consumes the returned popout state/handlers; the secondary-window gate and the
shared persisted atom stay encapsulated in the hook.
2026-06-30 14:26:39 -05:00
Brooklyn Nicholson
a8f9d089f1 refactor(desktop): extract composer placeholder logic into useComposerPlaceholder
Moves the resting-placeholder state + the conversation-change re-roll effect +
the disabled/reconnecting/starting derivation out of ChatBar into
hooks/use-composer-placeholder.ts, verbatim. The hook owns its own i18n + browse
reset; ChatBar just reads the derived string.
2026-06-30 14:25:21 -05:00
Brooklyn Nicholson
2a84bcb149 refactor(desktop): extract composer "Add URL" dialog into useComposerUrlDialog
Moves the URL dialog's open/value state, autofocus-on-open effect, and submit
(host onAddUrl or an @url: directive) out of ChatBar into
hooks/use-composer-url-dialog.ts, verbatim. ChatBar just wires the returned
openUrlDialog into the context menu and the state into <UrlDialog>.
2026-06-30 14:24:32 -05:00
Brooklyn Nicholson
e009ba57ac refactor(desktop): extract global Esc-to-cancel into useComposerEscCancel
Moves the chat-focused Esc-cancel listener (the latest-handler ref + the
register-once window keydown effect) out of ChatBar into
hooks/use-composer-esc-cancel.ts, verbatim. Encapsulating the latest-closure ref
inside its own hook is the first of the plan's "delete the latest-closure refs"
cleanups: it's no longer a loose ref in the 1.4k-line component, just an
implementation detail of a focused side-effect hook keyed on busy/awaitingInput/
onCancel.
2026-06-30 14:23:35 -05:00
Brooklyn Nicholson
5fdc2acedc refactor(desktop): extract composer branch/worktree engine into useComposerBranch
Moves the CodingStatusRow hand-offs (openInWorktree + branch-off / convert /
list / switch) out of ChatBar into hooks/use-composer-branch.ts, verbatim. The
hook depends only on cwd + draftRef + clearDraft (backend coupling via the
projects store); nothing about ChatBar's render. Dead projects/composer-store
imports drop out of index.tsx.
2026-06-30 14:22:54 -05:00
Brooklyn Nicholson
18d54bf0fd refactor(desktop): split onboarding overlay god file into onboarding/ folder
desktop-onboarding-overlay.tsx (1,291 lines) folded into components/onboarding/
as a cohesive feature folder. Behaviour-preserving — every move is verbatim;
typecheck + lint + tests green.

- index.tsx (665) — overlay shell, Picker, Header/Preparing, API-key catalog +
  ApiKeyForm; re-exports the provider API the settings page consumes.
- flow.tsx (364) — OAuth flow panels (FlowPanel, steps, DeviceCode, CodeBlock,
  ConfirmingModelPanel, DocsLink, Status).
- providers.tsx (118) — provider rows + display/sort (FeaturedProviderRow,
  ProviderRow, KeyProviderRow, providerTitle, sortProviders).
- glyph.tsx (170) — the decode/scramble animation toolkit (pure leaf).

Importers (desktop-controller, providers-settings) repointed to
@/components/onboarding; the overlay test moved to onboarding/index.test.tsx.
2026-06-30 13:21:49 -05:00
brooklyn!
f99ba56df4
Merge pull request #55331 from xxxigm/fix/desktop-projects-stale-backend
fix(desktop): handle stale backend when creating projects (#54999)
2026-06-30 11:47:04 -05:00
xxxigm
83e10a6777 fix(desktop): detect stale backend for projects.create
Probe the projects.* RPC surface, block create with a clear update hint,
and avoid the raw "unknown method" toast. Includes i18n for en, zh, ja,
and zh-hant.

Fixes NousResearch/hermes-agent#54999
2026-06-30 21:23:38 +07:00
xxxigm
217b3c283a test(desktop): cover projects RPC capability probing
Regression tests for stale-backend detection when projects.create is
missing from an older backend that still reports the same semver.
2026-06-30 21:23:38 +07:00
brooklyn!
1d495cfbbf
Merge pull request #55226 from NousResearch/bb/desktop-memory-graph
feat(desktop): memory graph — playable timeline of memories + skills over time
2026-06-30 04:36:17 -05:00
brooklyn!
6d20ac4c85
Merge pull request #55500 from NousResearch/bb/desktop-composer-draft
perf+refactor(desktop): de-entangle the composer into isolated engine hooks
2026-06-30 04:35:28 -05:00
Brooklyn Nicholson
aa07400e1a chore(desktop): keep draft persist effect deps clean
Replace direct queueEditRef reads in cleanup/pagehide with a mirrored local ref so hook deps stay stable and eslint-clean.
2026-06-30 04:33:08 -05:00
Brooklyn Nicholson
9998ff4cbe fix(desktop): persist live composer draft before swap/reload
Sync the contentEditable text before stash-on-scope-change and pagehide so pending rAF draft flushes cannot drop the newest keystrokes.
2026-06-30 04:32:39 -05:00
Brooklyn Nicholson
2f46fde3f5 fix(desktop): keep queued composer edit ref in sync
Update the shared queued-edit ref synchronously with React state so draft persistence sees the correct edit mode while loading and restoring queued prompts. Also drop the accidental node_modules symlink from the PR.
2026-06-30 04:27:22 -05:00
Brooklyn Nicholson
e5253d852b fix(desktop): tree-kill Windows terminal descendants
Ensure Windows desktop and local terminal teardown kill full process trees so Git Bash descendants cannot survive wrapper exits and accumulate across retries.
2026-06-30 04:23:27 -05:00
Brooklyn Nicholson
94d70dee54 perf(desktop): stop ChatBar re-rendering on cross-session status/queue churn
Audit follow-up. ChatBar subscribed to the whole `$statusItemsBySession` (a
computed that rebuilds the entire map) + `$previewStatusBySession` maps just to
derive a boolean, so every per-item status mutation (a subagent tick, the 5s
background poll) and every OTHER session's change re-rendered the ~1.4k
component. The queue hook likewise subscribed to the whole `$queuedPromptsBySession`
map.

- Add `useSessionStatusPresence` — a coarse edge (useSyncExternalStore) that
  flips only when the stack shows/hides; ChatBar uses it for the styling
  data-attr instead of the two map subscriptions.
- Add generic `useSessionSlice(store, key)` — subscribes to one session's array,
  bailing out when other sessions churn (the plain atom keeps per-key refs
  stable). The queue hook now reads its slice through it.

Result: ChatBar re-renders only when the stack's presence flips or this session's
queue changes — not on background/subagent status streaming or other sessions.

Verified: typecheck clean, 0 lint errors, composer tests 39/40 (pre-existing
attachments failure unrelated).
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
33d91029b2 perf+fix(desktop): coalesce composer paste/input flush; scope dock glow to thread
Two composer fixes:

- **Paste/input lag** — `flushEditorToDraft` serializes the whole editor
  (`composerPlainText` is O(n)); running it on every event during a burst
  (holding a key, or holding Cmd+V into a growing editor) was O(n²). Coalesce
  the input/paste path to one flush per animation frame. Lossless: the
  contentEditable DOM is the source of truth and submit + the compositionend /
  keydown paths re-read it synchronously (those stay immediate).
- **Detached-composer dock glow** — was `fixed inset-x-0` (full viewport, spilled
  under the sessions sidebar). Switched to `absolute inset-x-0`, so it anchors to
  the chat-column root the docked composer centers in — the glow now spans only
  the thread area, matching the actual dock target.

Verified: typecheck clean, 0 lint errors, composer DOM repro tests pass.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
773a3703bf refactor(desktop): extract composer submit engine into useComposerSubmit
Lift the submit orchestration out of ChatBar into
composer/hooks/use-composer-submit.ts: `submitDraft` (the one decision tree —
queue-edit save · slash-now-while-busy · queue · drain · send · stop),
`dispatchSubmit` (the shared send-with-restore primitive + the external-submit
listener), and `steerDraft`.

This is the seam where the draft and queue engines meet; it now reads both clean
APIs as explicit inputs instead of closing over inline state. ChatBar is left as
a thin coordinator that owns the shared `queueEditRef` and wires the four engines
(draft · queue · submit · metrics/voice/drop) into render.

Behaviour-identical (verbatim move). Verified: typecheck clean, composer DOM
repro tests (enter-submit, IME, slash-now, steer, drain) pass.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
4c4b790f11 refactor(desktop): extract composer queue engine into useComposerQueue
Lift the queue subsystem out of ChatBar into composer/hooks/use-composer-queue.ts:
the per-session queue-store binding + queuedPrompts, in-place queued-prompt
editing (begin/step/exit), the shared drain lock + send-then-remove sequence,
manual send-now, bounded auto-drain, and the three queue effects (re-key migrate,
idle auto-drain, queue-edit cleanup).

It consumes the draft API (draftRef/clearDraft/loadIntoComposer/focusInput) and
writes the coordinator-owned `queueEditRef` the draft engine reads — so the
draft↔queue coupling is two explicit deps, not an inline tangle. `steerDraft`
and the chat-focus Esc-cancel stay in ChatBar (not queue-internal).

Behaviour-identical (verbatim move). Verified: typecheck clean, composer DOM
repro tests + queue/edit paths pass.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
9ee7333e5b refactor(desktop): extract composer draft engine into useComposerDraft
De-entangle the draft spine: lift the source-of-truth engine (the imperative
composer-runtime subscription, edit primitives, focus, edge selectors, and
per-session load/clear/stash/restore) out of ChatBar into
composer/hooks/use-composer-draft.ts.

The draft↔queue cycle is broken by making `queueEditRef` a coordinator-owned
ref ChatBar threads into the hook (explicit dep, not an implicit shared global).
The contentEditable *event* handlers stay in ChatBar (they bridge into the
trigger engine) and drive the primitives the hook exposes.

Behaviour-preserving (verbatim move); typing perf preserved. Verified: typecheck
clean, composer DOM repro tests (enter-submit, IME, slash-nav) + text-guard pass.
2026-06-30 04:19:10 -05:00