Commit graph

123 commits

Author SHA1 Message Date
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
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!
f9b619dfae
Merge pull request #55504 from NousResearch/bb/desktop-split-prompt-body
refactor(desktop): decompose use-prompt-actions (slash + submit sub-hooks)
2026-06-30 03:22:43 -05:00
brooklyn!
90f59ecdbb
Merge pull request #55501 from NousResearch/bb/desktop-split-message-stream
refactor(desktop): split use-message-stream (utils + gateway-event sub-hook)
2026-06-30 03:22:21 -05:00
Brooklyn Nicholson
7337248a4c refactor(desktop): extract submit pipeline into use-prompt-actions/submit
After the slash dispatcher, the next-largest body unit was submitPromptText —
a ~280-line submit pipeline. Lift it into a colocated useSubmitPrompt sub-hook
(use-prompt-actions/submit.ts) with a typed SubmitPromptDeps object; body moves
verbatim. SubmitTextOptions moves to utils.ts (shared by submit + submitText).

Pure restructuring, no behaviour change (full use-prompt-actions suite green).
index.ts: 1,212 -> 937.
2026-06-30 03:15:10 -05:00
Brooklyn Nicholson
51a710e57e refactor(desktop): extract gateway-event dispatcher into its own sub-hook
The remaining bulk of useMessageStream was handleGatewayEvent — a ~550-line
event-type dispatcher. Lift it into a colocated useGatewayEventHandler sub-hook
(use-message-stream/gateway-event.ts): the values it closed over (sibling
streaming callbacks + the 3 stable refs the deps array omitted + options)
become a typed GatewayEventDeps object; the dispatcher body moves verbatim.

Pure restructuring, no behaviour change (utils tests still green). index.ts:
1,120 -> 540.
2026-06-30 03:11:14 -05:00
Brooklyn Nicholson
08c83d0555 refactor(desktop): extract slash dispatcher into use-prompt-actions/slash
The usePromptActions body's largest unit was executeSlashCommand — a ~530-line
`/command` dispatcher. Lift it into a colocated useSlashCommand sub-hook
(use-prompt-actions/slash.ts): the ~13 values it closed over become a typed
SlashCommandDeps object the parent passes in; the dispatcher body (and its inner
runSlash recursion) moves verbatim. SlashActionCtx (slash-only) moves with it.

Pure restructuring, no behaviour change (verified: full use-prompt-actions test
suite still green). index.ts: 1,772 -> ~1,250.
2026-06-30 03:03:45 -05:00
Brooklyn Nicholson
086343854d refactor(desktop): split use-message-stream into folder + utils
Extract the standalone gateway-event helpers (session-info patch derivation,
completion-error detection, todo-payload routing, delegate_task -> subagent
spec mapping, + the stream-flush/subagent-event constants) out of the
1,285-line hook into a colocated, tested use-message-stream/utils.ts. index.ts
keeps the stateful streaming hook and consumes the helpers.

Pure restructuring, no behaviour change; folder index keeps the import path
intact. index.ts: 1,285 -> ~1,120. Adds unit tests for the pure helpers.
2026-06-30 02:58:45 -05:00
Brooklyn Nicholson
ed47f2b4aa refactor(desktop): split use-session-actions into folder + utils
Extract the ~16 standalone helpers (message reconciliation, optimistic/resolved
session upserts, stored-session resolution, runtime-info application, error
classification) out of the 1,254-line god hook into a colocated, tested
use-session-actions/utils.ts. index.ts keeps the hook orchestrator (the
stateful action callbacks) and consumes the helpers.

Pure restructuring, no behaviour change; folder index keeps the import path
(`@/app/session/hooks/use-session-actions`) intact. index.ts: 1,254 -> ~950.
Adds unit tests for the pure helpers.
2026-06-30 02:54:46 -05:00
Brooklyn Nicholson
fa7bce0789 refactor(desktop): colocate hook/component families into scoped folders
Single-scoped helpers/sub-files were sitting flat in shared/grab-bag dirs.
Fold each family into its own folder (index = the export, dir resolution keeps
public import paths intact), dropping the now-redundant filename prefix:

- session/hooks/use-prompt-actions.ts (+ -utils, + tests)
  -> use-prompt-actions/{index,utils}.ts (+ tests)
- components/assistant-ui/thread* + assistant/system/user message renderers
  -> assistant-ui/thread/{index,content,status,message-parts,timestamp,types,
     list,timeline,timeline-data,assistant-message,system-message,user-message,
     user-edit-composer,user-message-text} (+ tests)
- components/assistant-ui/tool-fallback(+model)/tool-approval
  -> assistant-ui/tool/{fallback,fallback-model,approval} (+ tests)

Pure move + import rewrites; no behaviour change. App-wide shared primitives
(markdown-text, directive-text, tooltip-icon-button, clarify-tool, ansi-text,
message-render-boundary) stay flat. desktop-controller intentionally left in
app/ (route root; foldering would churn ~80 relative imports for no gain).
2026-06-30 02:42:07 -05:00
brooklyn!
28ba01c603
Merge pull request #55459 from NousResearch/bb/desktop-split-prompt-actions
refactor(desktop): extract use-prompt-actions standalone helpers into utils
2026-06-30 02:19:12 -05:00
Brooklyn Nicholson
bde2dc1051 refactor(desktop): extract use-prompt-actions standalone helpers into utils
The usePromptActions hook is the textbook "god hook" AGENTS.md warns against.
As a first, safe slice, pull its module-level standalone helpers (no closure
over hook state) into a focused, testable use-prompt-actions-utils.ts sibling:

- error classifiers: isSessionNotFoundError, isSessionBusyError,
  isProviderSetupError, inlineErrorMessage
- session-busy retry: withSessionBusyRetry (+ its constants)
- attachment IO: base64FromDataUrl, imageFilenameFromPath,
  readImageForRemoteAttach, readFileDataUrlForAttach, friendlyRemoteAttachError
- misc: delay, isSessionIdCandidate, blobToDataUrl, renderCommandsCatalog,
  slashStatusText, appendText, visibleUserOrdinal, visibleUserIndexAtOrdinal,
  the _submitInFlight guard set, and the GatewayRequest type

Pure restructuring, no behavior change; the usePromptActions and
uploadComposerAttachment exports (and their import paths) are unchanged. Adds
unit tests for the pure helpers. use-prompt-actions.ts: 1,956 -> 1,772.
2026-06-30 01:38:58 -05:00
Brooklyn Nicholson
25c7900fb5 refactor(desktop): thin desktop-controller by extracting session-list actions
DesktopController is a route root that had grown a controller's worth of
session-list plumbing inline. Extract the cohesive fetch/paging cluster into
a focused hook and a tested pure helper, per AGENTS.md's "keep route roots
thin" guidance:

- use-session-list-actions.ts: refreshSessions / loadMoreSessions /
  loadMoreSessionsForProfile / loadMoreMessagingForPlatform / refreshCronJobs
  (plus the private cron/messaging refreshers, sessionsToKeep, and the
  excluded-source constants)
- desktop-controller-utils.ts: pure sameCronSignature helper (+ unit tests)

Pure restructuring, no behavior change. desktop-controller.tsx: 1,441 -> 1,233.
2026-06-30 01:34:12 -05:00
Brooklyn Nicholson
09abbf8a63 feat(desktop): mirror voice.auto_tts into an $autoSpeakReplies store 2026-06-29 15:22:37 -05:00
Brooklyn Nicholson
adacb16d62 fix(desktop): make agent terminal tabs fully readable
Register read-only agent terminals with the same renderer-side terminal reader
as user terminals so read_terminal works on whichever tab is active.

Also bring agent xterm rendering closer to user-terminal parity (unicode 11,
web links, font weights/spacing) and make the gateway sink wiring resilient if
only one terminal event sink was already installed.
2026-06-28 21:18:49 -05:00
Brooklyn Nicholson
e117cfdff0 feat(desktop): live agent terminals + agent-driven tab close
Make the read-only agent terminal mirrors stream in real time and give
the agent a desktop-only way to dismiss its own tabs.

- Stream background output live: the local reader used a blocking
  read(4096) that buffered small periodic output until EOF, so agent
  tabs only "filled in" at process exit. Switch to buffer.read1(4096)
  (decoded) for incremental chunks.
- Route agent.terminal.output / terminal.close to the window that owns
  the process (its gateway session) instead of an empty session id, so
  events actually reach the desktop renderer.
- Add close_terminal: a HERMES_DESKTOP-gated tool (sibling of
  read_terminal) that drops a process's read-only tab WITHOUT killing it
  via process_registry.on_close; output keeps buffering and the user can
  reopen from the status stack.
- ⌘W now closes a focused agent tab: mark the agent instance
  data-terminal and focus it on activation so isFocusWithin routes there.
- ensureTerminal() no longer spawns an extra user shell when a tab
  already exists (e.g. opening a background task from the status stack).
2026-06-28 21:15:14 -05:00
Brooklyn Nicholson
520212cc59 feat(desktop): stream agent terminal output live instead of polling
Replace the 5s output_tail poll (which often showed nothing) with a real push
stream. The process registry gains an on_output sink called from its reader
threads with each chunk; the tui_gateway wires it to emit agent.terminal.output
{process_id, chunk} (write_json is _stdout_lock-guarded, so emitting from the
reader thread is safe). The desktop routes chunks by process id straight into
the read-only agent xterm via a small writer registry, with a capped backlog so
a tab opened mid-stream (or reopened) replays what it missed.

Drops the fragile poll/tail path: no session-key matching, no truncation, no
lag — full-fidelity ANSI, env-agnostic (local/docker/ssh).
2026-06-28 19:33:43 -05:00
Brooklyn Nicholson
cd5fb760a5 fix(desktop): restore cross-wired runtime-id guard on session resume
resumeSession's warm-cache fast-path once again trusted the
storedSessionId -> runtimeId -> ClientSessionState mapping without
checking the cached state still BELONGS to the session being resumed. A
pooled profile backend that gets idle-reaped and respawned re-mints
runtime ids, so a recycled id resolves to a live-but-DIFFERENT session's
cache entry and paints the wrong transcript under the current route:
click thread A, a totally different thread (often from another worktree)
loads. The session.usage 404 guard only catches a fully-dead id; a
recycled-live id 200s, so the fast-path happily served the stale cache.

Straight regression, not a new bug. f7bf74064 ("reject cross-wired
runtime-id cache on session resume") landed takeWarmCache() + its
regression test; 62af32efe ("keep active sessions aligned with cwd"),
rebased off a stale branch, restructured resumeSession and silently
reverted both 29 minutes later -- the exact stale-branch squash clobber
AGENTS.md warns about ("Squash merges from stale branches silently
revert recent fixes").

Re-apply the whole-class fix on top of the current cwd-aligned code:
takeWarmCache() validates state.storedSessionId === storedSessionId at
BOTH cache reads (the early transcript-keep decision and the fast-path),
purging a cross-wired mapping on a miss so it falls through to a full
resume that rebinds a correct runtime id. Restore the two regression
tests guarding it.

Tests: resumeSession warm-cache mapping integrity -- a cross-wired
mapping is rejected + purged (the bug), a correctly-wired cache is still
served with no needless refetch (no perf regression).

Co-authored-by: professorpalmer <professorpalmer@users.noreply.github.com>
2026-06-28 18:23:09 -05:00
Brooklyn Nicholson
19bae1b9e0 test(desktop): assert new backend sessions carry workspace cwd
Pin the desktop-to-gateway cwd handoff: createBackendSessionForSend must pass
the current workspace cwd into session.create so the backend registers the
session cwd before the agent/tools run.
2026-06-28 14:44:28 -05:00
brooklyn!
23021be26e
Merge pull request #52656 from helix4u/fix-desktop-empty-resume-view
fix(desktop): retry empty resumed transcripts
2026-06-28 11:57:57 -05:00
Teknium
163cb24d45
feat(moa): render reference-model blocks in TUI and desktop, not just CLI (#53855)
The MoA reference-block display (each reference model's output shown as a
labelled thinking block before the aggregator responds) previously existed
only in the classic CLI. The facade already emits moa.reference / moa.aggregating
through tool_progress_callback; this wires the TUI and desktop consumers.

- tui_gateway/server.py: _on_tool_progress relays moa.reference (label / text /
  index / count) and moa.aggregating to the Ink/desktop client as their own
  events.
- ui-tui: gatewayTypes adds the two event shapes; createGatewayEventHandler
  routes them; turnController.recordMoaReference pushes a committed
  thinking-style segment tagged with the source model. Shown regardless of
  showReasoning — references ARE the mixture-of-agents process the user opted
  into, not ordinary reasoning. moa.aggregating is a status-only transition
  (no transcript entry).
- apps/desktop: use-message-stream appends each reference as a labelled
  reasoning chunk via the existing reasoning disclosure; GatewayEventPayload
  gains label/index/aggregator.

Tests: tui_gateway emit (3), Ink handler render + showReasoning-independence +
aggregating-no-segment (3). TUI typecheck/lint clean; desktop typecheck/lint
clean.
2026-06-27 18:46:20 -07:00
Brooklyn Nicholson
54b50037e1 fix(desktop): treat a pending prompt as paused-on-you, not working
A clarify/approval/sudo/secret prompt blocks the turn on the user, but the UI
treated it as an in-flight turn: the "thinking" timer kept ticking and Esc
interrupted the run — discarding a question you might want to come back to. Add
$activeSessionAwaitingInput (the pet's awaitingInput concept, scoped to the
active session) and use it to suppress the stall indicator and disarm Esc while a
prompt waits. Clear the session's prompts (and needsInput) on Stop and on turn
end so a resolved/aborted turn can't leave a dead panel or a stuck "needs input"
dot.
2026-06-26 03:55:34 -05:00
Brooklyn Nicholson
62fe9fd101 style(desktop,tui): fix all lint/type/formatting issues
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:

- Run prettier across both trees (printWidth/wrap drift; prettier is not
  CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
  import/export sorting.
- Manual fixes for non-auto-fixable rules:
  - remove unused node:net import in electron/main.cjs (uses Electron net)
  - replace inline `typeof import(...)` annotations with top-level
    `import type * as EnvModule` in two ui-tui test files
  - scoped eslint-disable no-control-regex on intentional sentinel/ANSI
    regexes (mathUnicode.ts, text.ts)
  - resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
    deps, collapse redundant session.* members, and justified disables on
    settings mount-only data-load effects to preserve run-once behavior

No behavior changes; test pass/fail counts are unchanged from the main
baseline.
2026-06-26 01:04:33 -05:00
Brooklyn Nicholson
62af32efe7 feat(desktop): keep active sessions aligned with cwd 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
f7bf740640 fix(desktop): reject cross-wired runtime-id cache on session resume
resumeSession's warm-cache fast-path trusted the
storedSessionId -> runtimeId -> ClientSessionState mapping without
checking the cached state still BELONGS to the session being resumed.
A pooled profile backend that gets idle-reaped and respawned
(pruneSecondaryGateways) re-mints runtime ids, so a recycled id can
resolve to a live-but-DIFFERENT session's cache entry. The only
existing guard was a session.usage 404 -- that catches a fully-dead
runtime id, but a recycled id still 200s, so the fast-path happily
painted the wrong transcript under the current route (open chat A,
chat B loads).

Fold the belongs-to check into a single takeWarmCache() helper used at
BOTH cache reads -- the early transcript-keep decision and the fast-path
itself -- so a cross-wired entry can't even briefly flash a stale
transcript before the full resume repaints. On a mismatch the helper
purges both stale map entries and reports a miss, falling through to a
full resume that rebinds a correct runtime id. The full-resume path
already guards its final paint with isCurrentResume(), so only the
cached fast-path was missing the belongs-to check.

Pre-existing bug from the initial desktop app (#20059); not introduced
by the session-switch perf work (#49807), which left these lines
untouched.

Tests: two cases in use-session-actions.test.tsx driven through a
harness that owns the two cache maps -- a cross-wired mapping is
rejected + purged (the bug), and a correctly-wired cache still serves
from memory with no needless refetch (no perf regression).

Supersedes #50464 by @professorpalmer, reimplemented to also guard the
early transcript-keep read (whole-class fix, not just the fast-path).

Co-authored-by: professorpalmer <professorpalmer@users.noreply.github.com>
2026-06-25 16:11:18 -05:00
helix4u
5191ebba22 fix(desktop): retry empty resumed transcripts 2026-06-25 13:36:19 -06:00
Brooklyn Nicholson
1ca1f9f2c7 refactor(tui_gateway): DRY the deferred-session paths
Collapse the duplicated cold-resume / lazy-watch / create scaffolding into
shared helpers: _deferred_session_record (the live-session dict minus the
agent), _lazy_resume_info (the not-yet-built session.info), _claim_or_reuse_live
(lock + double-checked register-or-reuse), and _schedule_agent_build (the
pre-warm timer). Net -12 lines, three copies of the ~30-key session dict and
the lazy-info block down to one each. No behavior change.
2026-06-25 14:03:03 -05:00
Brooklyn Nicholson
3bf00e459a perf(desktop): make deferred resume the default, not an opt-in flag
Per review: gating the faster path behind a `defer_build` flag that the
only caller always sends is pointless. Flip it — `session.resume` now
defers the agent build by default for every caller (desktop + Ink TUI);
a caller that needs the agent built synchronously passes `eager_build:
true` (used by the build-race test). The desktop no longer sends a flag.

While verifying the flip, fixed two real parity gaps the deferred path
had vs the old eager (`_init_session`) path:

- `_enable_gateway_prompts()` was never called on a deferred resume, so
  approvals/clarify wouldn't route through the gateway prompt callbacks.
- `_start_agent_build` never wired `background_review_callback` /
  `memory_notifications`, so a deferred-built session's self-improvement
  "💾 …" summary leaked to stdout instead of rendering in-transcript.
  Wiring it there also fixes it for `session.create` sessions, which
  build through the same path.

ACP is unaffected (it uses its own session_manager, not this RPC); the
Ink TUI already consumes the same lazy `info` shape from session.create
and upgrades on the later `session.info` event.
2026-06-25 14:03:03 -05:00
Brooklyn Nicholson
c4c590e4a1 perf(desktop): make session switching fast under load
Switching sessions in the desktop app could freeze the whole UI for
several seconds on heavy, tool-rich chats. Root causes and fixes:

- Cold `session.resume` built the AIAgent (MCP discovery, prompt/skill
  build) *before* returning, and the desktop awaits that RPC before it
  paints — so the entire switch blocked on the build. Add an opt-in
  `defer_build` resume path (the contract `session.create` already uses):
  return the full display transcript immediately, register an upgradable
  live session, and pre-warm the agent on a short timer. The persisted
  runtime identity (model/provider/base_url/api_mode/reasoning/tier) is
  restored on the deferred build so it can't drop the provider.

- Nothing bounded how many in-memory agents accumulate; a user who
  reconnects often piled up detached sessions for the full 6h TTL. Add a
  soft LRU cap (`max_live_sessions`, default 16) that evicts the
  least-recently-active DETACHED sessions (no live client) — never a
  running, awaiting-input, mid-build, or live-transport one. Reopening
  re-resumes from disk.

- On the prefetch-hit cold-resume path, skip rebuilding a throwaway
  merged-message array (and its 1000-entry Map) when the prefetch already
  painted the exact transcript; the downstream sameMessageList guard
  already drops the publish, so it was pure main-thread cost.

The desktop opts into `defer_build` for every non-watch cold resume; the
eager path stays for CLI/TUI and existing callers.
2026-06-25 14:03:03 -05:00
xxxigm
7e2db0a140 fix(desktop): stop refText crash on undefined composer attachment holes
A session switch or draft restore can leave undefined/null holes in the
composer attachments array. AttachmentList was guarded against this in
#49624, but the sibling submit path was not: submitPromptText maps the
same array through attachmentDisplayText/optimisticAttachmentRef and
buildContextText (a.kind / a.label / a.refText), so a hole threw
"Cannot read properties of undefined (reading 'refText')" — an uncaught
renderer error that blanks the chat pane and shows "Desktop app link
offline".

Close the whole bug class:
- attachmentDisplayText / optimisticAttachmentRef no-op on a falsy
  attachment (shared chokepoint, also protects thread.tsx drop handler).
- submitPromptText filters falsy entries from the source array, and
  buildContextText filters its (possibly post-sync) input before reading
  fields.
2026-06-24 18:22:01 -07:00
brooklyn!
7157b213f5
Merge pull request #47959 from NousResearch/bb/pets-gen
Pet generation: frame-perfect hatch flow, backend picker, CPU-safe chroma, and CI-hardening
2026-06-24 19:41:34 -05:00
Brooklyn Nicholson
1fe013ee16 feat(pets): polish generate flow and reduce hatch CPU pressure
Ship the final pet-generation UX polish (provider picker behavior, step-2 cancel flow, banner integration, and visual consistency) and make saturated-chroma background removal C-op driven so hatch processing no longer hammers the machine during long runs.
2026-06-24 19:08:06 -05:00
Brooklyn Nicholson
93192059c9 fix(desktop): let the session watchdog heal a stuck "looping" turn
The 8-minute stream-silence watchdog only removed a stuck session from
$workingSessionIds (the sidebar dot). The composer's busy state lives in
the session-state cache and was never cleared, so a hung or looping turn
that never delivered its terminal event — including an old session
re-opened while the backend still reports it "running" — stayed wedged on
"Thinking" / Stop indefinitely.

Have the watchdog notify subscribers when it force-clears a session, and
subscribe from the session-state cache to also drop that session's
busy/awaiting/needsInput flags. updateSessionState re-syncs $busy when the
healed session is the one on screen, so the composer recovers instead of
spinning forever.

Frontend-only safety net; doesn't touch the turn lifecycle. The backend
root (a stale in-memory session["running"] surviving a dead turn thread
and re-arming busy on every resume) is a separate follow-up.
2026-06-24 18:36:17 -05:00
Brooklyn Nicholson
e495b33bf1 Merge remote-tracking branch 'origin/main' into bb/pets-merge
# Conflicts:
#	hermes_cli/commands.py
#	tui_gateway/server.py
2026-06-23 19:05:22 -05:00
Brooklyn Nicholson
d0af7fc954 feat(desktop): detect tool previews into composer status stack
Register previewable artifacts from the tool row, feed a session-scoped store,
and render compact rows above the composer. Remove the inline preview card.
2026-06-22 19:22:11 -05:00
Brooklyn Nicholson
cb17a9efb2 fix(desktop): stop auto-opening tool previews
Drop gateway-event preview registration so HTML artifacts from tool results
no longer pop the rail. De-dupe the inline preview card label.
2026-06-22 19:21:20 -05:00
Brooklyn Nicholson
5342eccf12 Merge remote-tracking branch 'origin/main' into bb/pets 2026-06-22 05:25:49 -05:00
Brooklyn Nicholson
86b990fe0f feat(desktop): floating pet, pop-out overlay + Cmd+K picker
Add the in-window floating pet (sprite, speech bubble, contact shadow,
profile-scoped, resize-safe) and a pop-out always-on-top overlay window
with gestures and notifications. Add the Cmd+K pet picker page plus the
appearance gallery and size slider in settings. Includes the pet stores,
electron overlay wiring, i18n strings, and store tests.
2026-06-20 14:18:40 -05:00
Harish Kukreja
1b7b4d138a
fix(desktop): handle slash exec dispatch payloads (#49358) 2026-06-19 21:11:16 -05:00
Teknium
b936f92b25
fix(desktop): render send/prefill directive notices (/goal, /undo) (#49073)
The desktop slash dispatcher dropped the `notice` field on `send` and
never handled `prefill` directives at all. `/goal <text>` returns
{type: send, notice: "⊙ Goal set …", message} from command.dispatch —
the desktop submitted the goal text as a plain prompt with no feedback,
so the goal looked like it did nothing. `/undo` returns a prefill
directive that fell through to "invalid response".

- types: add `notice?` to SendCommandDispatchResponse; add
  PrefillCommandDispatchResponse to the union.
- parseCommandDispatch: keep `notice` on send, parse prefill.
- runExec dispatcher: render the notice as a system line before acting,
  and handle prefill by dropping the message into the composer for
  editing (mirrors the TUI's createSlashHandler).

Tests: parseCommandDispatch send-notice / prefill cases.
2026-06-19 07:28:50 -07:00
Brooklyn Nicholson
51ee5b2c94 fix(desktop,tui): surface self-improvement review summary + honor memory_notifications
The "💾 Self-improvement review" summary (skill/memory updated) was invisible
on two surfaces:

- Desktop Electron app had no review.summary event handler — skill/memory
  writes happened silently. Now appends a persistent system message to the
  transcript (matching the Ink TUI's persistent-line semantics, not a
  transient toast that can be missed).
- tui_gateway (backs both 'hermes --tui' and the desktop) never read
  display.memory_notifications, so it always behaved as 'on' and ignored a
  user who set 'off'/'verbose'. Added _load_memory_notifications() (mirrors
  the messaging gateway's bool->str normalization, defaults to 'on') and
  wired it to agent.memory_notifications, matching gateway/run.py and the CLI.

Delivery chain now reaches all surfaces:
background_review.py -> background_review_callback -> review.summary event ->
desktop transcript / Ink TUI line / gateway message / CLI print.
2026-06-18 13:22:12 -05:00
Austin Pickett
016bce1a09
fix(desktop): recover stranded session windows when resume fails (#47655)
* fix(desktop): recover stranded session windows when resume fails

Opening a session in a new window (or any routed resume) could latch the
thread loader on "session" forever — the reported "stays stuck loading,
even after a nap" bug. Two compounding causes:

1. use-session-actions.resumeSession's catch ran the REST transcript
   fallback OUTSIDE its own try. When session.resume rejected AND the
   fallback also threw (the common case on a wedged/unreachable backend),
   the throw skipped setMessages and left activeSessionId null with an
   empty transcript — exactly the state the loader gates on
   (messagesEmpty && !activeSessionId), with no terminal/error state.

2. use-route-resume's self-heal could never re-fire: resumeSession sets
   selectedStoredSessionIdRef synchronously at entry (before failing), so
   stuckOnRoutedSession stays false, and on an already-open idle window
   neither pathnameChanged nor gatewayBecameOpen fire again. The window
   never retried — naps, focus, nothing recovered it.

Fix:
- Wrap the REST fallback in its own try so a fallback failure can't strand
  the loader.
- Add $resumeFailedSessionId: armed on terminal resume failure, cleared at
  the next resume's entry (and left clear on success).
- use-route-resume gains a bounded backoff auto-retry (4 attempts, 1s→8s)
  that re-resumes while the routed session matches the failure flag, with a
  fire-time liveness recheck so a recovered session isn't double-resumed.

Regression tests cover: fallback-wrap arming the flag without throwing,
flag cleared on success, retry fires on backoff, no retry for a
non-routed/recovered session, and the retry cap.

* feat(desktop): show error + manual Retry when resume retries exhaust

When a stranded session window's bounded auto-retry gives up (gateway
resume RPC + REST fallback fail through all MAX_RESUME_RETRIES attempts),
the loader latched forever. Add a $resumeExhaustedSessionId atom armed at
the give-up point so the chat view swaps the perpetual spinner for an
explicit error state + manual Retry button. Retry / reconnect / reselect
clears the latch and resets the auto-retry counter for a fresh cycle; a
route-change away from the stranded session also clears it.

Distinct from $resumeFailedSessionId (armed during the backoff window) so
the error UI only appears once auto-recovery has actually given up, not
mid-retry. Adds i18n strings across en/ja/zh/zh-hant and 3 tests covering
latch-arms-on-exhaustion, stays-clear-while-retries-remain, and
clears-on-route-change.

* fix(desktop): address review on stranded-resume recovery layer

Follow-up to review on #47655 (PR head 253bfc0e3). Four issues on the
recovery layer:

1. (blocking) Arm $resumeFailedSessionId only when the transcript is still
   empty after the REST fallback ($messages.get().length === 0), matching the
   atom's documented contract and the loader's messagesEmpty gate. Previously
   armed on any resume-RPC reject regardless of fallback outcome, so a window
   that recovered its history via REST still auto-retried and, on exhaustion,
   blanked the visible transcript behind the error overlay.

2. Reset the bounded-retry attempt counter on the $resumeExhaustedSessionId
   armed->cleared edge so a manual Retry / reconnect / reselect on the SAME
   stranded session gets a fresh backoff cycle, not a single one-shot attempt
   that immediately re-arms the error. (Keyed on the exhausted latch rather
   than the resumeFailedSessionId null->value transition the review suggested:
   the auto-retry loop itself toggles resumeFailedSessionId every cycle, so
   keying the reset there would defeat the MAX_RESUME_RETRIES cap. Only
   resumeSession clears the exhausted latch, making its clear edge the
   unambiguous manual-retry signal.)

3. Advance retryAttemptRef only when the timer actually dispatches a resume,
   not at schedule time. Prevents unrelated dep changes during the 1s-8s
   backoff window (transient gatewayState flip, non-stable resumeSession) from
   burning attempts and hitting MAX with fewer than 4 real resume attempts.

4. Drop unrelated blank-line-only insertions in store/session.ts and
   use-session-actions.ts to keep the diff tight.

Tests: +3 (RPC-fails-REST-succeeds-no-arm; manual-retry-fresh-cycle;
no-attempts-burned-on-dep-churn). All 19 resume tests + full session-hook
suite (65) pass; tsc --noEmit clean.

---------

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-06-17 17:33:53 -04:00
Brooklyn Nicholson
435c706e8e fix(desktop): stop a failed turn leaking into every other thread
A turn that ends in an error (e.g. an out-of-funds state) was being
re-rendered in unrelated threads. On a warm thread switch the on-screen
`$messages` still belongs to the previously viewed thread, and
`flushPendingViewState` fed it into `preserveLocalAssistantErrors`, which
grafted the prior thread's failed turn onto the newly opened one. Because
the polluted view then became the next switch's baseline, the error
cascaded into every thread the user visited.

Only carry local errors across a view flush when the on-screen baseline is
the same session being flushed; the cached state we publish already retains
that session's own errors. Also surface the turn error as a global toast
even when the failing turn ran in a background thread, since the error
blocks all subsequent interactions until the user acts.
2026-06-17 05:07:48 -07:00
brooklyn!
c6e99ab375
Merge pull request #46959 from NousResearch/bb/composer-model-selector
feat(desktop): composer model selector, per-model presets & external-provider disconnect
2026-06-16 09:55:57 -05:00
Brooklyn Nicholson
cb6b4127e7 refactor(desktop): make composer model picker sticky session state
The picker no longer touches the profile default. Model/effort/fast live as
plain UI state persisted in localStorage, so a pick follows across Cmd+N and
restarts instead of snapping back. New chats ship that state through
session.create as per-session overrides; live chats still scope switches to the
current session. Settings -> Model remains the only surface that writes the
profile default.

The gateway now accepts those session.create overrides, builds the agent with
them directly, reflects them in the immediate session.info payload, and writes
the chat's own model_config into the lazy DB row so reconnect/resume restores
that chat instead of the global default.
2026-06-16 09:50:07 -05:00
Teknium
a68ac0c49a
feat(desktop): allow /browser connect on a local gateway (#47245)
* fix(skills): guard recursive skill delete against tree-escape

Port from Kilo-Org/kilocode#11240. Their issue #11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.

* feat(desktop): allow /browser connect on a local gateway

/browser was hardcoded as terminal-only in the desktop slash palette, so
the chat GUI rejected it with "only available in the terminal interface."
The TUI already drives the live CDP connection via the browser.manage RPC.

Wire the same RPC into the desktop dispatcher as a /browser action handler,
gated to local-gateway connections ($connection.mode !== 'remote'). connect
mutates BROWSER_CDP_URL (and may launch Chrome) in the gateway process, so
it's only meaningful when that process runs on this machine; a remote
gateway gets a clear "local gateway only" message instead.
2026-06-16 09:03:43 -05:00
Brooklyn Nicholson
67233d1c2a fix(desktop): sync new sessions across windows
Broadcast session-list mutations from scratch windows so the main sidebar refreshes without manual reloads.
2026-06-15 20:59:57 -05:00
Austin Pickett
ed20f5ed06
fix(desktop): let explicit model switches escape broken config providers (#42241) (#46796)
When a desktop/dashboard session had no agent built yet and the user explicitly
picked a provider in the model picker, config.set('model', ...) would first try
to initialize the agent from the (possibly broken) config default provider —
failing before the user's explicit switch could take effect, trapping them on a
misconfigured default.

config.set now pre-parses the model flags: if an explicit --provider is present
and no agent exists yet, it skips the default-provider agent build and routes
straight through _apply_model_switch with the explicit provider. _apply_model_switch
gained a parsed_flags passthrough (avoids double-parsing) and only falls back to
resolve_runtime_provider(requested=None) when no explicit provider was given.

The desktop hook now sends config.set instead of slash.exec for active-session
model changes, so errors from the selected provider surface to the user instead
of being swallowed.

Co-authored-by: rodboev <rod.boev@gmail.com>
2026-06-15 15:36:51 -04:00