Commit graph

1028 commits

Author SHA1 Message Date
Teknium
ebd737f4d9 fix(mcp): close hosted OAuth lifecycle gaps 2026-07-17 04:50:47 -07:00
Teknium
6045529724 fix(mcp): harden hosted OAuth across profiles and clients 2026-07-17 04:50:47 -07:00
Gille
531e5763e8
fix(desktop): hide Windows updater console during handoff (#66040)
* fix(desktop): hide Windows updater console (#56884)

* test(desktop): cover hidden updater handoffs behaviorally

---------

Co-authored-by: Kyssta <218078013+kyssta-exe@users.noreply.github.com>
2026-07-16 22:50:40 -04:00
brooklyn!
c856f36459
perf(desktop): kill the layout-thrash cascade on session switch (#66033)
Follow-up to #65890 (router transitions off) and #65898 (structural
compare + first-paint budget): profiling the switch path on real 1000+-
message sessions with a new CDP harness showed the remaining freeze is
NOT markdown rendering — it's a forced-reflow cascade from mount-time
layout reads interleaved with style writes across the transcript's
layout effects, plus the first-paint budget cut landing too late to
stop the full-budget commit.

Measured on the two largest local sessions (996 and 1363 messages),
main-thread longtask totals per switch: warm 2450ms -> 557ms and
1158ms -> 194ms; first paint 1690ms -> 444ms. Harness:
scripts/profile-session-switch.mjs (same CDP family as
profile-real-stream.mjs).

- use-resize-observer: drop the synchronous initial callback and ride
  the observer's spec-guaranteed first delivery instead (same frame,
  after layout, before paint). The sync call ran while the commit's
  layout was dirty, so every size read in a callback forced a full
  reflow — with one instance per user bubble (measureClamp read
  scrollHeight, then WROTE --human-msg-full, re-dirtying layout for the
  next bubble), the switch commit thrashed for over a second. Inside RO
  timing the same reads are free. Composer metrics (2x
  getBoundingClientRect + documentElement style writes) rides the same
  fix.
- Same class, same fix at the remaining call sites profiling surfaced:
  ExpandableBlock and TerminalOutput (dozens per tool-heavy transcript)
  now measure/pin via RO initial delivery; the tool-window and
  thinking-preview pins drop their sync pin() call; the thread
  timeline's initial active-tick compute joins its existing
  scroll-time rAF batching so back-to-back transcript updates coalesce.
- thread/list: cut the render budget in the RENDER phase (state-from-
  props adjustment) instead of the post-commit layout effect. The
  effect-time cut was too late — on a warm switch React first built and
  committed the full 300-part tree, then re-rendered at 60, then bumped
  back to 300, so the expensive commit still happened (and on a cold
  switch the bump rAF usually fired while the transcript was still
  empty, so the prefetched messages rendered at full budget anyway).
  The render-phase cut restarts the component before any child renders;
  a second trigger handles the cold path where messages land later
  under the same sessionKey.
- thread/list: backfill 60 -> 300 inside startTransition so the older
  turns' markdown+shiki render is interruptible background work instead
  of a synchronous freeze one frame after the switch paints. Functional
  Math.max so an urgent "Show earlier" click can't be rebased back down.
- composer focus: skip the rAF/timeout focus retries when the element
  is already focused — focus() runs the full focusing steps (forcing
  layout) even on the active element, ~585ms per switch on a large
  dirty DOM.
- Replace the tautological render-budget test (it re-declared the
  constants locally and asserted 60 < 300) with behavior tests of the
  now-exported buildGroups + firstVisibleGroupIndex.

Verification: apps/desktop `npx tsc --noEmit` clean; full
`npx vitest run` 210 files / 1763 passed; manual CDP check confirms the
deferred backfill commits the full transcript, stays pinned to bottom,
and "Show earlier" still pages.
2026-07-16 22:46:08 -04:00
nousbot-eng
56e2ba5e79
fmt(js): npm run fix on merge (#66013)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 00:58:06 +00:00
nousbot-eng
36bf3c2673
fmt(js): npm run fix on merge (#66010)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 00:51:29 +00:00
HexLab
3f199f5c51
fix(desktop): don't latch remote backend boot failures so remote gateway reconnect recovers (#65756) 2026-07-16 20:45:43 -04:00
brooklyn!
432fca55a7
fix(desktop): drain queued prompts for background sessions (#66001)
Co-authored-by: Jakub Wolniewicz <4850809+frizikk@users.noreply.github.com>
2026-07-16 20:39:55 -04:00
nousbot-eng
10b6d1a910
fmt(js): npm run fix on merge (#65986)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-16 23:19:03 +00:00
ethernet
39a93dc633
fix(desktop): follow compression's stored-id rotation to prevent thread reload (#65984)
Auto-compression ends the SessionDB session and forks a continuation,
rotating the stored session id. The gateway emits `session.info` with
the new `stored_session_id`, and the desktop's cache entry was updated
via `ensureSessionState` — but the URL route and `$selectedStoredSessionId`
never followed the rotation.

On the next send, `getRuntimeIdForStoredSession(oldStoredId)` returned
null (the cache entry's `storedSessionId` no longer matched the old id),
so `routedSessionNeedsResume` evaluated true, triggering a full
`session.resume` + REST transcript prefetch — the whole thread reloaded.

Fix: a new `$activeSessionStoredId` atom is set in `ensureSessionState`
when the active session's stored id changes. A `useEffect` in
`use-session-actions` subscribes to it and re-anchors the route +
selection (`setSelectedStoredSessionId` + `navigate(replace: true)`),
and cleans up the stale stored→runtime mapping.

`replace: true` because it's the same conversation — compression is
transparent to the user, so back-button stays correct.
2026-07-16 23:12:55 +00:00
nousbot-eng
75467998f9
fmt(js): npm run fix on merge (#65974)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-16 22:32:53 +00:00
ethernet
f08b1f3445
feat(desktop): button tooltip keybind hints + keybinds settings tab + unified worktree dialog (#65204)
* feat(desktop): add useKeybindHint hook and TipKeybindLabel

Add a shared hook that reads the current keybind combo for an action
id from the $bindings store (rebindable) or KEYBIND_READONLY (fixed),
returning a formatted string or null when unbound.

Add TipKeybindLabel — a convenience component that auto-reads both
its label (from i18n) and keybind combo from the action registry.
Pass only actionId for the common case; pass text to override when
the tooltip is context-dependent.

* fix(desktop): replace native title= on buttons with themed Tip

Migrate all <button>/<Button> elements using the native HTML title=
attribute to the instant, themed <Tip> component. Native tooltips are
unstyled, delayed (~500ms OS default), and visually inconsistent with
the app's instant themed tooltips.

Also adds <Tip> wrappers to icon-only buttons that were missing
tooltips entirely (dialog close, search clear, overlay close,
master-detail pane controls, keybind panel rebind/reset buttons).

Adds an enforcement test (no-native-title.test.ts) that scans all
.tsx files for <button>/<Button> with title= and fails if any are
found. Updates DESIGN.md with the icon-only button tooltip rule and
keybind hint guidance.

* feat(desktop): wire keybind hints into button tooltips

Add actionId to TitlebarTool, StatusbarItem, and SidebarNavItem so
their tooltips show the current keybind combo via TipKeybindLabel.
Fix the hardcoded NEW_SESSION_KBD in the sidebar to read from
$bindings so it stays live on rebind.

Wired surfaces:
- Titlebar: sidebar toggle, flip panes, keybinds, settings
- Statusbar: terminal toggle (view.showTerminal)
- Status stack: open agents button (nav.agents)
- Sidebar nav: new session, skills, messaging, artifacts

* feat(desktop): move keybind panel to settings tab with search filter

Move the keyboard shortcuts panel from a Radix Dialog into a proper
Settings tab (/settings?tab=keybinds). The ⌘/ shortcut and titlebar
keyboard button now navigate to this settings tab instead of toggling
a dialog. Adds a search filter to filter shortcuts by label.

- New: src/app/settings/keybind-settings.tsx (extracted from keybind-panel.tsx)
- Delete: src/app/shell/keybind-panel.tsx (dialog wrapper removed)
- Remove: $keybindPanelOpen atom and toggle/open/close functions
- Add: IconKeyboard to lib/icons.ts
- i18n: keybinds.search + settings.nav.keybinds (en, zh, zh-hant, ja)

* refactor(desktop): unify worktree dialog into shared WorktreeDialog

Extract the worktree creation dialog from StartWorkButton (sidebar) into a
shared WorktreeDialog component. Both the sidebar's StartWorkButton and the
composer's CodingStatusRow now use the same dialog, eliminating the duplicated
UI.

The shared dialog keeps the sidebar version's full feature set:
- BaseBranchPicker (filterable base branch combobox)
- Convert mode (check out an existing branch into a worktree)
- Sanitized branch name input

The coding row passes repoPath (from cwd) and onOpenWorktree (which carries
the composer draft to the new session) so the unified dialog works everywhere
there's a repo, not just inside an entered project.
2026-07-16 18:26:21 -04:00
nousbot-eng
f1315ae91e
fmt(js): npm run fix on merge (#65971)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-16 22:26:09 +00:00
ethernet
0f05aaa2bf
perf(desktop): make session switching snappy on large transcripts (#65898)
Switching between chat sessions in the desktop app froze for up to ~1–2s on
large transcripts. Profiling the switch path surfaced three main-thread
blockers, fixed here minimally and without changing behavior.

1. JSON.stringify deep-compare (worst case). chatMessagesEquivalent compared
   message parts with JSON.stringify(a) === JSON.stringify(b) on every switch.
   On image-/large-blob-bearing transcripts this serialized every part twice
   and cost well over a second. Replaced with a structural compare that never
   stringifies: array-level identity fast-path, per-part reference fast-path,
   then type-aware field comparison. The compare's only consumer asks "did the
   transcript change, should I setMessages?", so it is deliberately
   conservative — a false-negative just causes one extra idempotent
   setMessages, while a false-positive (the unsafe direction) is avoided.

2. Scroll-settle loop. thread-list ran a requestAnimationFrame settle loop up
   to 90 frames (or 5 stable frames) on every sessionKey change, each frame
   forcing a synchronous layout read + write — racing the markdown paint for
   up to ~1.5s. A normal synchronous switch stabilizes within a couple
   frames, so the ceiling is now 2 stable frames / 15 max.

3. Synchronous first paint of up to 300 parts. On switch, thread-list reset
   the render budget to the full RENDER_BUDGET=300, so up to 300 parts went
   through markdown + shiki syntax-highlighting synchronously on the switch
   commit. It now paints a small FIRST_PAINT_BUDGET=60 first, then bumps to
   the full 300 in a requestAnimationFrame after the first commit.

Salvaged from PR #49807 by professorpalmer — re-applied to the restructured
file layout (use-session-actions/utils.ts, thread/list.tsx) and tests merged
into the existing utils.test.ts.

Co-authored-by: Cary Palmer <professorpalmer@users.noreply.github.com>
2026-07-16 18:19:41 -04:00
ethernet
42bd4368ae fix(desktop): sidebar status indicators lag for background sessions
The sidebar working dot didn't update for background sessions until the
user opened them. Two coupled causes:

1. The gateway's session.info event payload omitted stored_session_id,
   so the desktop app had no way to map a background session's runtime
   id to its stored id. Without the stored id, setSessionWorking(null,
   ...) was a no-op — the $workingSessionIds atom never updated.

2. The running→busy transition in the session.info handler was gated on
   `apply` (active session only). The gate correctly scopes view-only
   side effects (setCurrentModel, setCurrentCwd, etc.) to the focused
   chat, but the per-session busy state drives the sidebar indicator and
   must reach every session. updateSessionState only mutates the
   per-runtime cache entry, and syncSessionStateToView already guards
   the view publish to the active session, so ungating is safe.

Fix: add stored_session_id to _session_info() in tui_gateway/server.py,
add the field to GatewayEventPayload, pass it to updateSessionState in
the session.info handler, and ungate the running→busy transition.
2026-07-16 17:08:08 -04:00
nousbot-eng
74fc222f17
fmt(js): npm run fix on merge (#65912)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-16 19:57:39 +00:00
ethernet
f1af945f6c
fix(desktop): slow session switch (#65890)
react-router v7's HashRouter wraps every route state update in
React.startTransition() by default. In React 19's concurrent renderer,
transitions are non-urgent — React can yield mid-render and come back
later. When the app is under load (streaming token deltas, gateway
events, store updates from active sessions), those higher-priority
updates keep interrupting the transition, starving the route change commit.

This matches every symptom of the session-switch lag:
- Main thread is free (animations run, clicks work) — startTransition
  defers, it does not block
- navigate() does not take effect for seconds — the transition keeps
  getting interrupted by higher-priority updates
- Worse under load — more concurrent re-renders = more interruptions
- The whole UI (sidebar + main pane) does not update — the entire route
  change is one transition, so nothing commits until it finishes

Pass useTransitions={false} to HashRouter so route state updates are
synchronous at default React priority instead of deferred transition
priority. navigate() now commits and paints immediately.

See: react-router v7 chunk-BIP66BKV.js HashRouter implementation.
2026-07-16 15:34:53 -04:00
Andry Lloyd Paez
b0ca12192e
fix(desktop): restore closed main window on second launch (#64800)
* fix(desktop): restore closed main window on second launch

* fix(desktop): reset deep-link readiness when main window closes
2026-07-16 15:33:38 -04:00
brooklyn!
2655c725cc
fix(desktop): refresh default-derived composer model (#65896)
Co-authored-by: Koho Zheng <koho.jung@outlook.com>
2026-07-16 15:23:13 -04:00
UnathiCodex
bcf0d74572
fix(desktop): preserve zoom across display moves (#65874)
Reassert the persisted webContents zoom after BrowserWindow moved, covering Windows monitor transitions where Chromium recalculates display scaling and drops the user-selected zoom.
2026-07-16 19:21:43 +00:00
Koho Zheng
0f6abc73a8 fix(desktop): refresh default-derived composer model 2026-07-16 15:15:36 -04:00
Gille
c387be08b9
fix(desktop): serialize git status refreshes (#65341) 2026-07-16 15:05:14 -04:00
Brooklyn Nicholson
ee8275a8b2 test(desktop): port backend-connection-state test to vitest
Rebased onto current main, where the electron test harness migrated from
node:test to vitest (test:desktop:platforms = `vitest run --project electron`,
auto-discovering electron/*.test.ts). Swap the node:test import for vitest and
drop the .ts-extension import hack; the obsolete package.json node --test list
edit is dropped in the cherry-pick resolution since vitest auto-discovers.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
2026-07-16 14:55:58 -04:00
Gille
71fa56e8ac fix(desktop): restore cloud reconnect action 2026-07-16 14:53:11 -04:00
Gille
783003179a fix(desktop): ignore stale backend exits 2026-07-16 14:53:11 -04:00
geoffreybutler94
f0ff8d5097
fix(desktop): preserve routed session on profile rebind (#65283)
Co-authored-by: geoffreybutler94 <257877469+geoffreybutler94@users.noreply.github.com>
2026-07-16 18:33:00 +00:00
ethernet
659d1123c4
fix(desktop): model picker reverts in existing threads (#65777)
selectModel in use-model-controls captured activeSessionId as a closure
prop, but the actions bag in wiring.tsx mutates in place to keep a stable
identity for memoized surfaces. The modelMenuContent useMemo captures
selectModel once when the gateway first opens (before any session is
active) and never re-evaluates, so clicking a model in an existing thread
goes through a stale closure with activeSessionId=null — the pick is
treated as UI-only, config.set is never sent, and the next session.info
event clobbers the optimistic update back to the session's real model.

Drop the activeSessionId prop entirely. All three callbacks now read
$activeSessionId.get() live from the store, matching the pattern
refreshCurrentModel already followed. This is the correct contract for
the actions bag's in-place mutation: callbacks read live state from the
store, not from captured props.
2026-07-16 16:17:41 +00:00
Siddharth Balyan
7d8c499893
fix(desktop): preserve node-pty helper in packaged app (#65611)
Guard staged node-pty ASAR path rewrites so already-unpacked paths are
not rewritten twice. Normalize spawn-helper to mode 0755 in both the
prebuild and locally compiled build/Release staging paths.

Add behavioral coverage for both unpacked path forms and both helper
layouts.

Co-authored-by: zhouwei <zwcf5200@163.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-07-16 11:58:50 +00:00
github-actions[bot]
75f45a0692
fmt(js): npm run fix on merge (#65229)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-15 21:40:56 +00:00
ethernet
09f8a8268c test: port workspace-level JS tests to a new js-tests workspace package 2026-07-15 17:24:12 -04:00
ethernet
2f3007ff51 test: port JS/package.json invariant tests from Python to vitest
The CI change classifier routes package.json / package-lock.json /
.ts/.tsx changes to the frontend lane, not the Python lane. Four
Python tests asserted about these JS-side artifacts, so a PR touching
only those files would skip the Python suite — the regression goes
green on the PR and red on main (where the classifier fails open).

Ported all four to vitest so they run in the correct CI lane:

  tests/test_package_json_lazy_deps.py
    → apps/desktop/electron/package-json-lazy-deps.test.ts
    (camofox is lazy, agent-browser is eager, lockfile clean)

  tests/test_desktop_electron_pin.py
    → apps/desktop/electron/desktop-electron-pin.test.ts
    (electron dep is exact, matches build.electronVersion, lockfile agrees)

  tests/test_assistant_ui_tap_compat.py
    → apps/desktop/electron/assistant-ui-tap-compat.test.ts
    (@assistant-ui cluster shares one tap version + semver helper)

  tests/test_dashboard_sidecar_close_on_disconnect.py
    → web/src/lib/chat-sidebar-session-params.test.ts
    (sidecar session.create opts into close_on_disconnect + profile)

The ChatSidebar test was regex-matching .tsx source text (the
source-reading anti-pattern). Extracted sidecarSessionCreateParams()
from the component's effect into an exported pure function so the
test calls real code instead of pattern-matching a string.

Verified: 8 electron tests + 76 web tests pass; both typecheck clean.
2026-07-15 17:24:12 -04:00
ethernet
93808ca6a7 fix(desktop): resolve eslint errors in composer-input-sanitize.ts
The hoisted shared eslint config catches pre-existing no-useless-escape
and no-control-regex errors in apps/desktop that were only fixed in web/
in the previous commit.

- no-useless-escape: remove unnecessary \) escapes inside character classes
- no-control-regex: add eslint-disable-next-line comments for intentional
  \x1b terminal escape byte matching (same pattern as web/src/lib/pty-mobile-input.ts)
2026-07-16 01:42:02 +05:30
ethernet
3102fc9a66 fix(shared): add missing 'fix' script alias
apps/shared had lint:fix but not the 'fix' alias that other workspaces
have. The js-tests check job runs 'npm run fix' as a second step, so
this workspace was failing with 'Missing script: fix'.
2026-07-16 01:42:02 +05:30
ethernet
214cbf77f0 refactor(lint): hoist shared eslint + prettier config to root 2026-07-16 01:42:02 +05:30
ethernet
b80b52aa46
feat(desktop): add background-task indicator to sidebar session rows (#65174)
* feat(desktop): add background-task indicator to sidebar session rows

A session with a live terminal(background=true) process but no active LLM
turn now shows a pulsing gray dot in the sidebar — distinct from the accent
pulse of an active turn and the steady amber/green of needs-input/unread.

New $backgroundRunningSessionIds computed atom joins
$backgroundStatusBySession (runtime-keyed) with $sessionStates
(runtime→stored) to produce stored session ids the sidebar row can match
against — same pattern as $attentionSessionIds and $unreadFinishedSessionIds.

Also refactors SidebarRowDot from a 3-branch if-else chain into a
table-driven priority array of DotState entries. Each state declares its
active flag, className, ariaLabel, and title in one place — adding a new
indicator state is now one array entry instead of another conditional.

* fix(desktop): keep pulse-dot before:bg-* static so Tailwind emits it

The PING() helper interpolated the color into `before:bg-${color}`, but
Tailwind v4 only generates utilities it finds as complete static strings — a
template-composed `before:bg-(--ui-accent)` / `before:bg-muted-foreground/50`
is never emitted, so both pulse rings (working + new background) lost their
halo color. Make PING a static scaffold and write the before:bg color inline
per variant.

---------

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-07-15 20:02:23 +00:00
ethernet
4e7b0389ea
fix(desktop): match sessions by git branch in ctrl-k palette and sidebar search (#65172)
SessionInfo already carries git_branch from the backend, but neither the
ctrl-k command palette nor the sidebar search function included it in
their matching fields. Typing a branch name matched nothing.

- session-search.ts: add session.git_branch to sessionMatchesSearch
- command-palette/index.tsx: thread git_branch through SessionEntry and
  into the keywords array for both sessions and archived sessions groups
- session-search.test.ts: cover full, partial, and "main" branch matches
2026-07-15 15:37:12 -04:00
ethernet
c1945d410b
fix(desktop): panel layout toggle bugs — terminal reveal, side collapse in column-root layouts, zone-menu restore (#65162)
* fix(desktop): un-minimize zone when revealing a collapsed tool panel

`revealTreePane()` fronted the pane's tab but never un-minimized its zone
when the pane lived in a shared zone (terminal + logs, or a tool panel
stacked with the workspace). The shared-zone branch of `setPaneCollapsed`
routes opens through `revealTreePane` instead of `toggleTreeGroupMinimized`,
so the zone stayed collapsed and the terminal appeared to "close but not
open" on Ctrl+` — and tab clicks needed a double-click because the first
click's `restoreTreePane` → opener → listener → `revealTreePane` chain
didn't un-minimize either.

The fix: `revealTreePane` now restores a minimized zone before fronting
the pane, chaining the un-minimize + activate into a single `commit` so
the second op sees the updated tree.

* fix(desktop): side collapse in column-root layouts + restore after zone-menu minimize

Two follow-up fixes for the panel layout:

1. Ctrl+B / Ctrl+J sidebar toggles didn't work in the Terminal deck (and
   Quad) layout. The side-collapse system (treeSideOfPane, paneRootSide,
   layoutHasRootSide, and the renderer's semanticSides) only operated on the
   ROOT split when it was a row — but Terminal deck's root is a column, so the
   side columns (sessions/files) nested inside a child row were invisible to
   the collapse system. Extracted a rootRow() helper that finds the row
   containing main (the root itself for row layouts, or the row child of a
   column root), and a rootRow prop propagated through TreeNode → TreeSplit so
   semanticSides fires on the right split regardless of root orientation.

2. Clicking the "logs" tab in a minimized terminal/logs zone needed a
   double-click when the zone was minimized via the zone menu (right-click →
   minimize), not the toggle. restoreTreePane called the opener
   ($logsOpen.set(true)) and returned early — but when the store was already
   true, nanostores don't fire the listener, so setPaneCollapsed/revealTreePane
   never ran and the zone stayed minimized. Now restoreTreePane also
   un-minimizes directly + calls revealTreePane when the zone is still
   minimized after the opener runs.
2026-07-15 15:37:09 -04:00
Brooklyn Nicholson
a4c9b069dc fix(desktop): stop reasoning text flashing/re-typing on every delta
Reasoning ("Thinking") rendered through MarkdownTextContent with a smooth
typewriter reveal. TextMessagePartProvider mints a fresh part object on every
text change, and useSmooth resets its reveal to empty whenever the part
identity changes — so each reasoning delta restarted the animation from the
first character (h / hell / hello wo ...). Token-streaming reasoners
(R1/Qwen/GLM/Claude thinking) fire a delta per token and flash hard; GPT-5's
coarse reasoning summary updates too rarely to notice, which is why it looked
fine.

Drop smooth on the reasoning path so it plain-appends, exactly like the
assistant answer already does. Removes the now-dead smooth plumbing too.
2026-07-15 14:35:15 -04:00
Brooklyn Nicholson
7a5a6ef99b fix(desktop): keep generated images out of tool overflow
Generalize the clarify opt-out into an UNBOUNDABLE_TOOLS set so a run
containing image_generate also stays a plain, fully-visible stack instead
of collapsing into the bounded window, where the max-height + gradient mask
clipped the image the same way it clipped clarify forms.
2026-07-15 14:08:35 -04:00
ethernet
587e76fbc2 feat(desktop): green unread dot for background-finished sessions
When an agent turn finishes while the user is viewing a different
session, the sidebar now shows a steady green dot on that session —
distinct from the blue pulsing dot of a running turn and the gray dot
of an idle one. Opening the session clears the indicator.

The unread state is ephemeral renderer-side state, matching the
existing $workingSessionIds and $attentionSessionIds pattern: no
persistence, no backend involvement, wiped on gateway-mode switch.

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: dschnurbusch <dschnurbusch@users.noreply.github.com>
Co-authored-by: Flow Digital Inc. <flow-digital-ny@users.noreply.github.com>
2026-07-15 13:51:41 -04:00
Brooklyn Nicholson
5c03e27ce2 Merge remote-tracking branch 'origin/main' into bb/contrib-areas
# Conflicts:
#	apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts
2026-07-15 13:22:08 -04:00
Teknium
fcdc10a0f3 fix(moa): reject half-filled MoA saves at the API boundary and hold desktop autosave until slots are complete
Follow-up hardening on top of #64158 (@DavidMetcalfe):

Backend (the root-cause fix):
- hermes_cli/moa_config.py: add validate_moa_payload() — strict write-time
  counterpart to the deliberately tolerant normalize_moa_config(). Flags
  half-filled slots, empty reference lists, recursive moa slots, naming the
  exact preset/slot.
- hermes_cli/web_server.py: PUT /api/model/moa validates before normalizing
  and returns 422 with the specific problems instead of silently swapping the
  user's preset for hardcoded defaults (#64156). Also declares
  fanout / reference_max_tokens / reasoning_effort on the Pydantic payload so
  client round-trips no longer erase hand-set values.

Desktop:
- Replace sanitize-then-send with hold-while-incomplete: the debounced
  autosave is deferred (not repaired) while any slot is half-filled, and
  flushes once the model pick completes the edit. Mid-edit UI state is never
  repainted by a save response (generation guard covers held edits too).
- updateMoaSlot only clears the model when the provider actually changed.
- Explicit preset ops (set default / add / delete) cancel the pending
  autosave and invalidate in-flight responses so the two writers can't race.
- Stable row keys (preset+index) so mid-edit rows don't remount; cleared
  model shows the 'Model' placeholder instead of vanishing.

Both TS clients' MoaConfigResponse types now declare the round-tripped
fields (fanout, reference_max_tokens, reasoning_effort).

Tests: 12 new backend unit tests (validate_moa_payload contract incl.
validate/normalize agreement), 3 new web_server endpoint tests (422 on
half-filled ref/aggregator, fanout round-trip), 3 new desktop vitest cases
(autosave held while half-filled, flush on completion, same-provider
reselect no-op). E2E validated against a live TestClient with isolated
HERMES_HOME: bug sequence now 422s with config untouched.

Fixes #64156
2026-07-15 09:50:08 -07:00
David Metcalfe
a61a0bc019 fix(desktop): prevent MoA autosave defaults explosion from half-filled slots
Three fixes in model-settings.tsx:

1. Filter incomplete slots before autosave: sanitizeMoaRefsForSave() strips
   reference slots with empty model before any autosave (the 600ms debounce
   was sending half-filled provider-but-no-model slots to the backend, where
   _clean_slot rejects them and _normalize_preset falls back to hardcoded
   defaults). Presets with zero valid refs keep their empty reference_models
   array rather than being silently dropped. The aggregator slot is also
   sanitized when its model is empty.

2. Add withActive() to MoA provider dropdowns: the reference and aggregator
   provider Selects filtered to authenticated-only, so unauthenticated
   current values (e.g. openai-codex) rendered blank. Mirror the existing
   pattern from the model Select.

3. Add generation counter to scheduleMoaSave(): stale save responses could
   overwrite newer state. Bump a counter on each save and skip setMoa/setError
   if a newer save was scheduled in the meantime.
2026-07-15 09:50:08 -07:00
ethernet
1600008ab0 fix(desktop): show +/- summary on collapsed review folders
ReviewDirRow never rendered a DiffCount, so collapsed folders gave no
indication of the additions/deletions inside them. The tree builder
already aggregates added/removed onto directory nodes (verified by
tree-data tests) — this just renders them, matching the file rows.
2026-07-15 11:57:33 -04:00
ethernet
1d48863b85 fix(desktop): inline git identity in worktree tracking test for CI
CI runners have no global git identity. The remote repo seed commit
needs inline -c user.email/user.name flags, matching the pattern
already used by ensureGitRepo.
2026-07-15 11:46:29 -04:00
ethernet
678b86df1c fix(desktop): worktree from origin/main should not set up upstream tracking
When "new worktree" branches off a remote-tracking ref like origin/main,
`git worktree add -b <branch> <dir> origin/main` auto-sets upstream
tracking (branch → origin/main), producing `branch:origin/main` in branch
listings. The user wants a standalone local branch — like `git checkout
origin/main && git checkout -b branch` — not one silently wired to the
remote. Add `--no-track` when the base is an `origin/` ref. Local branch
bases are unaffected (they never triggered tracking).
2026-07-15 11:46:29 -04:00
HexLab98
2388e0687b test(input): preservation regressions and prompt.submit boundary (#62557)
Add cases for mid-string markers, trailing punctuation, and insufficient
tail repeats; verify prompt.submit passes sanitized text to run_conversation.
2026-07-15 07:39:42 -07:00
HexLab98
1011cd24e2 fix(input): strip bracketed-paste leaks before prompt persistence (#62557)
Extract shared hermes_cli/input_sanitize.py (bracketed-paste wrapper stripping
and terminal ~[[e artifact suffix collapse) and wire it into prompt.submit
and the Desktop composer so corrupted user text is cleaned before
messages.content is persisted.
2026-07-15 07:39:42 -07:00
Brooklyn Nicholson
b5aef05e2c fix(desktop): layout reset reopens collapsed sidebars
resetLayoutTree() claims to "restore everything" but never reopened collapsed
SIDES. A sidebar hidden before a reset survived it, so the next ⌘B toggled from
that stale-hidden state into a SHOW — and the user's hide never persisted across
reload. Reopen every bound side through its store so the toggles stay truthful.

Adds a store-level regression test (real modules, re-import = reload) covering
hide→reload and the reset→hide→reload repro.
2026-07-15 03:30:25 -04:00
Brooklyn Nicholson
305e26558e fix(desktop): composer progressively collapses on narrow tiles
Drive the composer's layout off its OWN measured width (the existing
ResizeObserver, so it reacts per-tile, not per-viewport) instead of only
stacking or overflowing:

- >=440px: full inline row, full model label.
- 320-440px: still inline, but the model pill sheds its label for its chevron
  icon (~120px back) so the controls stop crowding the placeholder.
- <320px: stacks to the two-row layout, pill stays iconized.

Adds COMPOSER_COMPACT_PILL_PX above the stack breakpoint and a `compactPill`
signal from useComposerMetrics, wired to the model pill's existing compact mode.
2026-07-15 03:17:50 -04:00