Commit Graph

4143 Commits

Author SHA1 Message Date
Brooklyn Nicholson 508b3cf65d feat(desktop): count unread sessions from the shared status map
The titlebar badge needs the same unread answer the green dots use, without
double-counting lineage aliases that are not listed rows.
2026-08-20 00:42:21 -05:00
Brooklyn Nicholson cce0427905 fix(desktop): authenticate gated file downloads like REST
saveGatewayFile rode the OAuth cookie partition even when hermes:api already
held a native bearer, so listing worked and Download 401'd.

Co-authored-by: 686f6c61 <github@00b.tech>
2026-08-20 00:39:56 -05:00
Brooklyn Nicholson b7b6ee0118 fix(desktop): name the gated file-download auth decision
Downloads have to present the same bearer-vs-cookie choice as oauth REST.
A cookie-only save against a cookieless native session is the Files-panel 401.

Co-authored-by: 686f6c61 <github@00b.tech>
2026-08-20 00:39:56 -05:00
Brooklyn Nicholson ddb305c602 fix(desktop): give mermaid diagrams a pixel size in the overlay and on copy
Mermaid emits width="100%". Inside the zoom viewer's shrink-to-fit grid
that percentage can collapse, and svgSize's parseFloat("100%") made a
100px PNG so copy fell back to raw SVG text.

Co-authored-by: Robert Mohid <rmohid@gmail.com>
2026-08-20 00:31:43 -05:00
Brooklyn Nicholson 6a3a1f411b fix(desktop): mermaid zoom overlay body collapsed to zero height
The Dialog shell is a fixed-height flex column, but the body had no
flex-1. The toolbar is absolutely positioned, so the in-flow stage had
nothing to resolve against and clipped the SVG.

Co-authored-by: Anuvrat Rastogi <anuvrat.rastogi@sap.com>
2026-08-20 00:31:43 -05:00
hermes-seaeye[bot] e1d54926a1 fmt(js): npm run fix on merge (#90536)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 05:18:09 +00:00
Brooklyn Nicholson 9c12d2d6c3 feat(desktop): unfocused session panes recede
With two sessions tiled side by side nothing said which one you were in —
both painted at full strength, both composers looked live. The unfocused
surface now fades and desaturates as one layer (thread, timeline rail,
composer, header together), so the focused conversation is the one with
colour in it. Light and dark carry their own opacity; a single pane never
dims, since focus falls back to the primary's selection.

The sidebar gains the matching half: every session open in a pane keeps the
active band, the unfocused ones at reduced strength through their own mixed
token — a colour rather than row opacity, which would have dimmed the title
and status dot with it.
2026-08-20 00:12:20 -05:00
Brooklyn Nicholson db72b41485 fix(desktop): scrollbars stop carrying the theme accent
The thumb mixed from --dt-midground, so every list had a small tinted bar in
its corner competing with real accent-coloured UI. A new --dt-scrollbar-thumb
derives from the same colour with chroma forced to zero, keeping each theme's
lightness — so the thumb still sits correctly against its own surfaces, just
without the hue. Alpha steps and the Firefox fallbacks are unchanged.
2026-08-20 00:12:20 -05:00
Brooklyn Nicholson 83fac2cf2d feat(desktop): one theme list in the palette, with a mode toggle inside it
The picker split every palette across a Light and a Dark group, so a
built-in appeared twice and picking one silently set the mode too. It now
mirrors Appearance settings: light/dark/system rows, then every theme once,
applied on top of whichever mode is selected.

Mode rows preview on highlight like theme rows already did — system resolves
through the live prefers-color-scheme query, so it previews what committing
it would actually give you.
2026-08-20 00:12:20 -05:00
hermes-seaeye[bot] 7156d0d658 fmt(js): npm run fix on merge (#90523)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 05:00:29 +00:00
Brooklyn Nicholson a662d08f37 refactor(desktop): replace every window.confirm with the shared dialog
Ten prompts — deleting sessions, cron jobs, credentials, endpoints and
providers, plus the settings and memory resets — were raw Chromium modals:
unstyled, blocking, and nothing like the rest of the app. Lint now rejects
the native globals so they can't come back.
2026-08-19 23:52:23 -05:00
Brooklyn Nicholson 21198d9401 feat(desktop): add confirm() as the imperative front door to ConfirmDialog
Handlers that need the answer inline had no way to reach the shared dialog
without hoisting state and a JSX mount into their component, so they all
reached for window.confirm instead. This mirrors notify(): a store action
carries the question, one host at the shell renders the real ConfirmDialog.
2026-08-19 23:52:23 -05:00
Brooklyn Nicholson 5df9cd27ea feat(desktop): let ConfirmDialog carry a secondary action
The worktree removal prompt offers a third way out — hide the lane but leave
the worktree on disk — which is why it was still hand-rolled. One optional
slot between Cancel and Confirm covers it, and it keeps Confirm as the
focused button so Enter still means the destructive action.
2026-08-19 23:52:23 -05:00
Brooklyn Nicholson 1e26c02de6 refactor(desktop): route cron delete and review revert through ConfirmDialog
Both were hand-rolled copies of the shared confirm — same two-button shape,
same busy/close beat — and neither answered Enter. Folding them in drops the
duplication and picks up the focus fix.
2026-08-19 23:52:23 -05:00
Brooklyn Nicholson bb0e9ee95a fix(desktop): confirm dialogs take focus so Enter confirms
The delete-session dialog opted out of Radix's autofocus, which left focus
on the sidebar row that opened it — Enter re-activated the row instead of
confirming, and ConfirmDialog's Enter handler never saw the key.

ConfirmDialog now focuses its own Confirm button on open. The existing Enter
test fired the key at the dialog node, so it passed over the bug; it now
fires at whatever actually holds focus.
2026-08-19 23:52:23 -05:00
Brooklyn Nicholson bfcfdb30d1 test(desktop): prove the status bar keeps its own right-click menu
The unit test covers the primitive contract — the marker survives Radix's
asChild Slot merge. This adds the end-to-end half: mount the real coordinator
next to the real status bar, right-click it, and assert the customize menu
opens while the app fallback stays shut. That is the assertion that fails on
a build where the two halves drift apart, and it holds regardless of how the
ownership marker is spelled.

Drops the hand-stamped DOM fixture that asserted the coordinator honors an
attribute the test itself wrote.

Co-authored-by: huklaa <huklaa@users.noreply.github.com>
2026-08-19 23:51:10 -05:00
aydnOktay 2d6d7c550f fix(desktop): keep Radix context menus when asChild overwrites data-slot
The app-wide context-menu coordinator recognizes surfaces that own a Radix
menu by `[data-slot="context-menu-trigger"]`. Radix `asChild` merges as
mergeProps(slotProps, childProps), so a child that sets its own `data-slot`
wins and the marker never reaches the DOM. The status bar footer is
`data-slot="statusbar"`, so the coordinator swallowed its right-click and
showed the window-verbs fallback instead — leaving every default-hidden
status bar item, the context meter included, unreachable from the UI.

Stamp a dedicated `data-hermes-context-menu-trigger` after `{...props}` on
ContextMenuTrigger and bail on that marker. Any asChild surface with its own
`data-slot` is covered, not just the status bar.
2026-08-19 23:51:10 -05:00
Teknium 6851841112 fix(bot-mode): group chat opens as one room pane, not two (#89788)
Opening a Bot Mode group chat painted the room twice — once as a main-window
workspace tab (host.openWorkspace) and once as the in-panel fallback, because
the Bots pane rendered off $groupChatWorkspace alone. Two live panes with
independent drafts drove one shared engine, and the roster disappeared behind
the duplicate.

The in-panel room is the fallback surface, not a second copy: it now renders
only while no main tab owns the group. The selection atom stays set either way
so the roster row still highlights, and desktops without the door — or whose
door throws — keep the in-pane room.

Consolidates #89881, #90274 and #90398, which fixed the same bug.

Closes #89788

Co-authored-by: helix4u <helix4u@users.noreply.github.com>
2026-08-19 23:12:25 -05:00
Brooklyn Nicholson 145cd763ca feat(desktop): drag markdown table columns to resize them
A colgroup of percentages is the only state, so widths never touch the
cells: one <col> per column, table-layout fixed, and the browser does the
rest. A drag moves one seam and the pair either side trade width, so the
table box never changes size mid-drag — no reflow of the message around
it, no scrollbar appearing under the pointer.

Handles are markup inside each <th>; the table listens once and resolves
the grabbed seam from the DOM, so there is no context, no per-column
component, and no index threading. Tables stay in auto layout until they
are resized, and double-clicking a seam hands them back to it — the same
reset gesture the pane sashes use.

On a 43-row table a 40-step drag mutates 78 col[style] attributes and
touches no cell.
2026-08-19 23:10:25 -05:00
Brooklyn Nicholson e361e70b3b feat(desktop): keep markdown table column widths across turns and sessions
A markdown table has no id — it is re-parsed from text on every render, so
any resize state hung off the transcript dies on the next turn. Key the
record by a hash of the header row instead: the same table resolves to the
same key after a re-render, a session switch, or a reload, without the
transcript carrying anything.

Widths are percentages of the table box, never pixels, so a restored table
stays fluid in a narrow pane. The namespace is deliberately disposable —
one key, 64 entries, 7-day expiry, swept on first access. Losing it costs
one drag.
2026-08-19 23:10:25 -05:00
hermes-seaeye[bot] a72c9ca248 fmt(js): npm run fix on merge (#90461)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 03:21:47 +00:00
Brooklyn Nicholson d15cd18fa1 feat(desktop): hide the Thinking toggle where a disable is rejected
The picker offered an off switch for every reasoning model, including routes
whose upstream answers a disable with HTTP 400 — so "thinking off" was a
control that could not work. Carry the catalog's mandatory verdict through
model.options as can_disable_reasoning and hide the toggle when it is false.

Effort levels are left alone. The catalog's supported_efforts under-reports
what the Portal serves (z-ai/glm-5.3 publishes max, high, low yet honors
minimal at its lowest thinking), so filtering the scale by it would hide
levels that work.
2026-08-19 22:14:56 -05:00
Owenz-creator 8408edcfb5 fix(bot-mode): protect ordinary sessions from hide sweep 2026-08-19 19:30:28 -07:00
Teknium d604ba6585 fix(bot-mode): running kanban/tool workers now light the Bots roster (#90268)
Worker sessions are deny-listed out of every conversation list, so a
profile grinding through a 30-minute kanban task read idle ('3 hr ago')
with no ACTIVE NOW entry the entire run.

- tui_gateway/methods_profiles.py: profiles.list rows gain worker_session
  — the newest kanban/tool row (id, source, title, last_active). Workers
  heartbeat last_activity_at every <=60s while running (#72016), so the
  field stays fresh exactly while work is happening. last_session keeps
  its deny-list contract; include_sessions:false omits the field; older
  clients ignore it.
- hermes-bots plugin: workerActiveAt() (150s window, one missed heartbeat
  of slack) feeds ACTIVE NOW, the row pulse dot ('Working on a task right
  now'), and the row age label while a worker runs. Chat semantics are
  untouched when no worker is live.
- Tests: 4 new pytest (real SessionDB on temp HERMES_HOME), 2 new node
  behavior tests; sabotage-verified.

Session-list visibility of workers (the issue's first half) is left as-is
by design — auto-resume and shared lists must keep excluding workers; the
roster signal was the actionable gap.
2026-08-19 19:30:12 -07:00
Teknium 3d62508240 style: sort sidebar-archive import per perfectionist/sort-imports 2026-08-19 19:25:55 -07:00
Teknium 3e05033275 fix(desktop): deleting an archived session no longer leaves a ghost row that spins forever
Archived rows render from $archivedSessions (their own capped store —
they're excluded from $sessions by design), but removeSession only pruned
$sessions. Deleting from the Archived filter left the row in place; a
click on it resumed a hard-deleted id: resume 404 -> goneSessionVerdict
saw the row still listed -> 'retry' -> unrecoverable spinner.

removeSession now resolves the row from either store, evicts both
optimistically, restores the archived row on RPC failure, and forwards
the archived row's owning profile to deleteSession.
2026-08-19 19:25:55 -07:00
Teknium b2ea0f3810 fix(desktop): gateway restart no longer clickable-by-mistake next to reconnect
Community report (X @Cobalt_Peak): Reconnect and Restart gateway in the
statusbar gateway popover rendered the same RefreshCw icon side by side,
so users triggered full gateway restarts when they meant to reconnect.

- Restart now uses a Power icon with a destructive hover tint
- Moved restart to the end of the row, after the system-panel button,
  behind a visual divider separating it from the benign actions
2026-08-19 19:25:37 -07:00
Teknium 5ead089775 fix(desktop): cron panel empty states stop suggesting a broader search when no search is active
Both cron empty states used search-flavored copy unconditionally; a fresh
panel with zero jobs and no query told users 'Try a broader search
query'. Copy now follows the query state, reusing existing i18n keys.
2026-08-19 19:24:32 -07:00
Teknium 20059cbc69 fix(desktop): sidebar search results no longer show raw >>>term<<< FTS markers
The backend's session search wraps matched terms in sqlite snippet()
delimiters '>>>'/'<<<' (hermes_state_search.py). The sidebar rendered the
snippet as plain text via searchResultToSession(), so searching 'foo'
painted rows literally titled '>>>foo<<<'. Strip the markers before the
snippet becomes the row preview.
2026-08-19 19:24:22 -07:00
hermes-seaeye[bot] fab8479aa0 fmt(js): npm run fix on merge (#90408)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 01:14:22 +00:00
Brooklyn Nicholson 22b81836d2 style(desktop): prettier 2026-08-19 20:01:40 -05:00
Brooklyn Nicholson 12d438ceb3 fix(desktop): keep the two-argument call shape for session RPCs without a deadline
Threading timeoutMs/signal through requestForSessionProfile and
requestGatewayForProfile handed every session-scoped RPC a trailing
`undefined, undefined`. Only the plugin host bridge actually supplies those,
so the rest of the app's calls changed observed arity for no reason — and the
resume/activate paths assert on the exact call shape.

Forward the deadline args only when the caller set them; the plugin bridge
keeps the full four-argument route it needs.
2026-08-19 20:01:40 -05:00
EndeavorYen 0734cfd319 fix(desktop): keep chrome API home when opening a Bot Chat
Opening a plugin/Bot Mode session is navigation, not a workspace switch.
keepAllProfilesScope (default true) now dials the named backend without
moving $activeGatewayProfile or setApiRequestProfile. Session-owned RPCs
still route to the session owner. Pass false to switch chrome and collapse
the Sessions sidebar.
2026-08-19 20:01:40 -05:00
EndeavorYen 2367b90b9f fix(desktop): keep Sessions workspace when opening a Bot Chat
Bot Mode passed keepAllProfilesScope:false, which re-homed the sidebar
onto the bot profile. That profile forever-chat is hidden, so Sessions
and the roster looked empty. Opening a bot is navigation, not a workspace
switch. Also restore all-profiles when the bot backend is already live.

Related: #89789
2026-08-19 20:01:40 -05:00
chelsealong 6ec4aa8c3a fix(desktop): move the hydration-timeout retry into host.openSession
A review of the previous commit found that retrying at the plugin layer
(openStoredBotChat catching and re-calling host.openSession) didn't fix
the reported bug: host.openSession's own catch block unconditionally
calls setResumeExhaustedSessionId on a hydration timeout before
rethrowing, and only an explicit resumeSession() (the manual Retry
button) clears that latch for the currently-routed session. A
plugin-side retry is a different code path that can hydrate the
transcript fine while the full-screen "Couldn't load this session"
overlay stays latched over it.

host.openSession now takes a retryHydrationTimeoutOnce option and
retries the open+hydration-wait internally, before the latch is ever
set, so a successful retry never arms the overlay. openStoredBotChat
just opts in via that option.
2026-08-19 20:01:40 -05:00
Jack Lau 3a50a6bea8 fix(desktop): bound the profile-activation half of a Bot Chat wake
host.openSession awaited ensureGatewayProfile with no deadline. That await
gates waitForFocusedSessionHydration, which arms the only timer on the path,
so a profile dial that never settles left the open pending for the life of the
window: the pane froze with no error, no Retry and - the part that made this
hard to recognise - no timeout either. The gateway log signature is a bare
`ws accepted` with no matching `ws closed`.

Bound the activation with its own copy of the wake budget rather than folding
it into the hydration one. A cold profile backend can legitimately spend most
of the hydration budget painting a large transcript, and that race is already
tight enough to lose, so charging activation to the same clock would trade a
wedge for a regression. The timeout reuses the hydration message prefix on
purpose - openSession keys the core stranded-session surface off it - and the
[bot-wake] support log now names which phase expired, so a stuck dial is not
read as a slow transcript.

Scoped to callers that passed awaitHydration. A plain open never asked for a
deadline and has nowhere to render one, so its behaviour is unchanged.

Two existing tests counted microtask ticks between the call and the core open.
The bounded activation adds a tick, so they now flush a macrotask instead,
which asserts the same thing without depending on the await count.

Refs #89556
2026-08-19 20:01:40 -05:00
hermes-seaeye[bot] ad889540f2 fmt(js): npm run fix on merge (#90384)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 00:25:17 +00:00
Brooklyn Nicholson de322448ac feat(desktop): move the HUD's way out onto the bar and drop the strip above it
The exit chip floated over the composer in a 26px transparent strip reserved
for it (--hud-chip-strip), hidden until you hovered the bar. Under glass that
strip is bare untinted material across the top of the HUD — a band of chrome
above the surface, present in every state, holding a control you cannot see.

It rides the composer's controls row now, next to send. That costs no
reserved space and takes about 120 lines of CSS with it: the chip needed its
own placement, hover reveal, leave-hold, and an opaque card to stay legible
over an unknown desktop. None of that applies to a button on the bar, which
is already our surface — the problem was the placement, not the control.

Trade-off worth naming: the way out is now always visible in the HUD rather
than revealed on hover. It is one more permanent glyph on a Spotlight bar, in
exchange for an escape hatch that no longer depends on discovering it.
2026-08-19 19:12:09 -05:00
Brooklyn Nicholson 701314c6dd feat(desktop): fold the HUD's voice controls into one menu
Dictation, spoken replies, the wake word and start-conversation were four
separate icon buttons in a Spotlight bar a few hundred pixels wide — most of
the row spent on toggles that are set once and rarely touched. In the HUD
they collapse into a single menu; the docked composer has the width and
keeps them inline, same controls and same state.

The trigger is not a static glyph. It reports the loudest live voice state —
recording, transcribing, listening for the wake word, speaking replies — and
lights while any is on, because a folded menu that looked idle with the mic
open would be a worse trade than the space it saves. The three toggles are
checkbox rows that hold the menu open on select, so the state you just
changed is the state you can see.

The shared control class names move to a module of their own so the row and
the menus it renders can wear them without importing each other, and the
pressed-toggle tint stops being written out at each of its four sites.
2026-08-19 19:12:09 -05:00
Brooklyn Nicholson cba8efce21 feat(desktop): paint the HUD band as the app's thread surface under Glass
The band wore its own card tint at a hardcoded 80/92%, so a HUD beside the
docked window read as a lookalike rather than the same surface, and the Tint
slider moved one and not the other. It now paints --ui-bg-chrome at
--translucency-glass-keep: one painter, one token, one lever.

That needed the setting and the surface rewrite to stop being one flag.
data-hermes-glass means "this window's field surfaces may be rewritten" and
is deliberately false in the HUD, which owns its own backgrounds; the new
data-hermes-glass-on means "the user's Glass setting is live" and is
published everywhere, along with the tint number the band reads.

The 0.5rem side inset drops to zero while glass is on. It exists to keep an
opaque sheet clear of the bar's corner controls, but the frost is the whole
window — an inset sheet left a hairline of bare untinted material down both
sides.

An open completion drawer now drops the frost along with the band it belongs
to. The drawer takes the band to 25% and blurs it while the native material
stayed at full strength, which is the same bare slab in a different
disguise. It mounts without a focus change, so it is observed rather than
passed in, coalesced to a frame because the shell mutates with every
streamed token.
2026-08-19 19:12:09 -05:00
Brooklyn Nicholson 7f9e79b2e1 feat(desktop): back the HUD band with the same window material the app uses
The HUD asked for vibrancy directly and always with the 'hud' material —
one of the two rungs the macOS census rejected, because it collapses into
under-window on blur and so changed the frost the moment another app took
focus. It also ignored the translucency setting entirely: Glass off still
frosted, and Windows got nothing at all.

hudFrostFor is the mapping for a transparent window, beside vibrancyFor in
the shared module both processes read. Two gates give it its answer: the
renderer's report that the band actually covers the window, and the user's
Glass setting. Off resolves to no material rather than a resting one, since
a transparent window has no opaque page to hide an unwanted frost behind.

Windows 11 rides setBackgroundMaterial through the same call, so the HUD
follows the frost ladder on both platforms. Main self-diffs and keys the
latch to the window, so a Settings change re-frosts a live HUD, a tint drag
touches nothing native, and a HUD respawned on another profile is not
mistaken for the window that already carried the material.
2026-08-19 19:12:09 -05:00
Brooklyn Nicholson 7f3d255931 test(desktop): cover the terminal overlay hiding on an unfocused tab switch 2026-08-19 18:31:57 -05:00
Brooklyn Nicholson 2473e56859 fix(desktop): stand the terminal overlay down when its tab loses focus
The persistent terminal is a position:fixed overlay that chases its slot's
rect, and the whole tracker — visibility included — was gated behind the
renderer pause. Switching tabs while the window is unfocused therefore left
the overlay parked over the zone at full opacity with pointerEvents:auto, so
the chat underneath was unreachable until something refocused the window.

Visibility is correctness rather than perf, so sample it on every wake even
while paused; the rect chase, which is the part that forces layout, stays
gated.
2026-08-19 18:31:57 -05:00
Teknium 271e49a8ff fix(bot-mode): accept both host.connections() shapes in the Create-on picker normalize
The SDK now returns the registry rows per its documented contract
(salvaged #89893), while desktops predating the SDK unwrap resolve the
raw registry envelope. The plugin normalize accepts both, so the picker
works across the transition; regression test updated to pin the
dual-shape normalize.
2026-08-19 16:10:10 -07:00
hukla a46fe01251 chore(sdk): remove unrelated session helper from #89893 2026-08-19 16:10:10 -07:00
hukla b40d2019e8 fix(sdk): preserve primary in registered connections 2026-08-19 16:10:10 -07:00
hukla aa2cec721f test(sdk): cover registry primary connection mapping 2026-08-19 16:10:10 -07:00
hukla c825be4c77 test(sdk): cover connection registry list contract 2026-08-19 16:10:10 -07:00
hukla 6ad587236f fix(sdk): return registered connection list 2026-08-19 16:10:10 -07:00
Teknium 010c9925e3 fix(bot-mode): roster age, pulse, unread, and sort now see canonical Bot Chat activity
The canonical Bot Chat is hidden from session lists by design, so
profiles.list's last_session never advances when you message a bot there.
PR #88690 moved the roster PREVIEW to preferred_session but left every
activity signal on last_session — a bot you just messaged showed '6d ago',
never pulsed, never badged, and sorted below stale bots.

New botActivitySession(bot) helper returns the fresher of preferred_session
(the pinned Bot Chat, resolved precisely by the backend) and last_session
(newest visible conversation). All four activity sites key off it now:

- row age label (relativeTime)
- active-now pulse dot + activeBots strip
- unread watermark + activity toast preview
- roster recency sort (activityOf)

Older gateways without the preferred_session resolver degrade to
last_session exactly as before. Backend untouched.

Tests: extracted the real helper into the vm harnesses (no stub drift),
5 new behavior tests; sabotage-verified they fail against the old code.
2026-08-19 16:04:13 -07:00