Commit Graph

4143 Commits

Author SHA1 Message Date
Brooklyn Nicholson baaf304992 feat(desktop): in-app tips
An ambient rotation that points at parts of the app you may not have found yet
— one accent bubble, an arrow, and an outline around the subject. No scrim and
no spotlight: a tip is a pointer beside your work, not a modal in front of it,
so it takes no focus, owns no Esc, and blocks nothing.

It only speaks when the app is genuinely quiet — nothing streaming, no dialog,
menu or tour up, window focused, a few seconds since the last keystroke — and
walks the catalog in order rather than shuffling, so tips arrive as a tour of
neighbouring parts of the app instead of unrelated ones. A tip with nothing on
screen to point at is skipped, not waited for.

Closing one with its ✕ retires it for good. That is the whole reason the ✕ is a
heavier gesture than letting the bubble time out, and Settings → Appearance is
the only way back — alongside the switch that turns the feature off entirely.
2026-08-27 21:50:18 -05:00
Brooklyn Nicholson 46512ee1d6 feat(desktop): give the app's main surfaces durable handles
Tours and tips both address elements by selector, and everything they most want
to point at — the composer, the model pill, the nav rows, the profile rail, the
right-pane toggle — was reachable only by icon, position, or a translated
aria-label. None of those survive a re-render, a theme, or a locale change.

Two handles are needed per surface, not one, because where an arrow points and
what an outline wraps are different questions: a nav row's label carries the
`data-tour` handle so an arrow lands at the end of the word, and defers the
outline to the row via `data-tip-arrow-only`.

The collector also has to skip panes hidden by the keep-alive stack. An inactive
tab stays mounted under `visibility: hidden` to keep its scroll position, so its
rect is identical to the live tab's and no geometry test separates them — which
is how a tour could spotlight a background tab's composer.
2026-08-27 21:50:18 -05:00
Brooklyn Nicholson 50f816abaf feat(desktop): add an accent variant to the popover primitive
The default popover is glass over the app's own chrome, which is right for
something the user opened and wrong for something the app said. The accent
variant fills the same box — same arrow, same placement engine — with a solid
brand colour so an unprompted surface reads as the app speaking.

Filling it with `primary` directly doesn't work across themes: a pale accent is
a perfectly valid primary (imported VS Code themes love a pastel), and the
honest `primaryForeground` for one is near-black, so the loud surface comes out
a pastel card whispering. `--dt-primary-solid` deepens the hue until a light
foreground clears AA — a no-op on an accent that is already deep, and darkening
only, so the hue survives.
2026-08-27 21:50:18 -05:00
Brooklyn Nicholson 355be02793 style(desktop): prettier over this branch's own eslint --fix output
The curly pass left one-line { return x } bodies behind; prettier expands
them. Formatting only.
2026-08-27 21:47:23 -05:00
Brooklyn Nicholson d7f6ef8a17 fix(desktop): three latent bugs in the bot rail, and the dead declarations
groupChatSyncMemberKey keyed on a field the descriptors never carry, so
every member hashed to the empty string and the round engine saw one member
where there were several; it keys on botRosterKey now, which is durable
across machines. botMetaWriteAt grew an entry per write and never dropped
one — it prunes past 60s, which is longer than the echo it exists to
suppress. aliasRouteIndex replaced the whole map on rebuild, so a slower
rebuild finishing second clobbered a newer one; a generation token means
only the newest result lands. Regression tests for each.

Dead since the split: EYE_X/EYE_Y, generatedSessionTitle, enabledMcp,
groupChatSyncDeletedRevision — each down to a declaration with no reader.
The bot source-status labels were half-localized, so they moved onto one
helper here rather than in the i18n pass. no-redeclare is disabled inline
over the two overload pairs it misreads, rather than in the shared config.
2026-08-27 21:47:04 -05:00
Brooklyn Nicholson 99cae5b9f6 refactor(desktop): put the bot dialogs' one-offs on the shared primitives
A raw checkbox in the routine editor where the SDK already exports one. The
same resizable-panel style block inline in three dialogs, now a
ResizableFrame beside the other dialog parts. Four inline styles that were
Tailwind spelled longhand — model-picker's was literally flex flex-col
gap-2. The twenty that remain are computed grid columns and avatar sizes,
which have to stay inline.
2026-08-27 21:46:58 -05:00
Brooklyn Nicholson fa236b31ff i18n(desktop): localize the strings the bot rail still hardcoded
Both roster filter menus, the avatar picker's tabs. Renamed group.newDesc to
group.manageDesc since it describes managing an existing group, not making
one.

The getPluginCtx()?.i18n.t() sites only guarded the context, not i18n on it
— a plugin context without the bundle threw mid-render and painted an empty
rail. Guarded both.
2026-08-27 21:46:54 -05:00
Brooklyn Nicholson 716564531b fix(desktop): bound the two bot-rail caches that grew for the window's life
petFrameCache is keyed by spritesheet URL over a 4500-pet gallery and holds
decoded PNG data URLs, so scrolling pinned every pet you passed until the
window closed. Capped at 120 — five pages, so scrolling back stays instant
and a miss only re-pays the fetch and crop. relayAgentsCache already swept
stale ids, but the sweep sits behind an early return that a shrink to one
connection skips; capped at 32, safe because every live connection is
rewritten each cycle so eviction can only reach ids that stopped being
fetched.

relay.ts also carried eight loose module-level lets, the state outlier
across the plugin's modules. Nothing renders from them, so they stay module
scope — collapsed into one record rather than moved to a store.
2026-08-27 21:46:49 -05:00
Brooklyn Nicholson ff5c5b4ae8 refactor(desktop): compose the shared lead cell instead of copying it
Six verbatim copies of the leading-glyph box down to two owners: transcript
lines get SCAFFOLD_GLYPH_CLASS from scaffold-row, sidebar rows get
SIDEBAR_ROW_LEAD. The bot rail's GatewayKindGlyph was a fork of core's
ConnectionGlyph down to the icon set, so it wraps the real one now and the
duplicate kind-to-icon tables are gone.
2026-08-27 21:46:45 -05:00
686f6c61 6ac193e02b fix(desktop): refetch Bot Chat on roster reopen instead of idle snapshot
forceResume already requested a main-route resume, but Bot Chat is a
tile. Reopening reused the warm cached transcript and skipped REST, so
cron bot-chat deliveries that landed while the panel was closed stayed
invisible until app restart. Refresh the tile transcript on explicit
open and merge the persisted tail into the cache.
2026-08-27 19:46:43 -07:00
Brooklyn Nicholson 610d1ed0da refactor(desktop): give the sidebar row geometry and a bounded cache one owner
Row geometry lived inside chrome.tsx as private consts, so anything that
wanted to line up with a session row copied the literals instead — the lead
cell alone appeared verbatim in six files. Its own comment warns that owning
height anywhere else makes rows float 1-2px off sessions, which is exactly
what a copy invites. Split the measurements into row-geometry.ts, keep the
public names re-exported from chrome.tsx, and put the lead cell and
ConnectionGlyph on the plugin SDK so a plugin composes the same box rather
than re-deriving it. ConnectionGlyph takes a className now so a caller can
tint it without forking the component.

LruCache is the same move for a different leak: katex-memo.ts had a private
one with a hardcoded ceiling, and the bot rail had two Maps that never
evicted. One shared class, exported, katex's twin deleted.
2026-08-27 21:46:40 -05:00
Gille 6f8be61516 fix(desktop): prevent Bots home flash during chat switch 2026-08-27 19:44:14 -07:00
Brooklyn Nicholson 0fececa733 chore(desktop): lint the bot-mode modules clean
The plugin shipped as bundled JavaScript, so eslint never saw it. Now that it
is .tsx under src/, the whole ruleset applies: sorted JSX props, curly braces,
statement padding, unused imports.

Three of the ref writes the atom-mirror rule flagged are the cases its own
comment carves out — a scroll-position tracker fed by a DOM listener, a
previous-value tracker for the hidden -> visible edge (lagging a render IS its
contract), and a timer handle cleared on unmount. Those get the documented
disable. The fourth was a genuine mirror: McpSetupButton copied its profile
prop into a ref every render so two callers could read it. They read the prop
directly now, and the ref holds only the profile the component creates on
demand for the New Bot flow.

no-redeclare counts a TypeScript overload signature as a redeclaration of its
implementation, which is what botSelectionKey and botMetaKey tripped. Swapped
for the TS-aware version alongside the no-undef swap already there; hermes-ink,
whose vendored yoga bindings merge a const and a type under one name, extends
its existing carve-out to the new rule name.
2026-08-27 20:05:36 -05:00
Brooklyn Nicholson 32ca343c83 fix(desktop): /new inside a bot chat compared against a property that does not exist
A bot's canonical chat is the relationship — /new inside one would fork it into
a scratch session, so the composer reroutes /new to /compact there. The guard
deciding "is this chat the canonical one" read host.activeSessionId, which is
not on the host: the real atoms are host.state.activeSessionId (runtime id) and
host.state.focusedStoredSessionId (stored id). The optional chain swallowed it,
the comparison ran against null every turn, and /new reset forever-chats for as
long as the guard shipped.

It reads the focused STORED id now, which is the id space canonical_session
reports in. The comparison itself moves into isCanonicalChatOnScreen so a test
can drive it — matching either the durable registry row or the
compression-lineage tip, since a compacted Bot Chat is on screen under its tip
id while the registry still names it by the root.
2026-08-27 20:05:32 -05:00
Brooklyn Nicholson d3df1a36a8 test(desktop): port the bot-mode suite off the .mjs vm harness
The old harness sliced source text out of plugin.js, re-evaluated the
fragments through vm.runInNewContext, and in a good number of files simply
regex-matched the source for a symbol name. Nothing it asserted survived the
split into modules, and its blind spot was load-bearing: the /new guard
regex-matched clean for as long as it shipped dead.

These are the same contracts driven against the real modules through the real
imports, colocated beside the code they cover. Files whose only content was a
source regex or a re-implementation of the function under test are dropped
rather than translated.
2026-08-27 20:05:28 -05:00
Brooklyn Nicholson e4bd1a0a78 feat(desktop): give a bot's empty chat its own face and name
An untouched bot chat was blank: core's splash stands down for any
session that exists, and nothing took its place. Claim the new
`chat.empty` slot and title the chat with the bot's face above its name
in the splash's lettering, so an empty conversation still says whose it
is. Rendering "HERMES AGENT" there would have been the wrong identity
for the surface.

The transcript hands its slot the RUNTIME session id while a canonical
Bot Chat is keyed by its stored one — the two id spaces behind the
#93080 misroute — so identity resolves through the focus store the rest
of the plugin already trusts. Metadata is read with `botRosterMeta`
rather than by name: it is keyed by the route it came from, so a by-name
read misses the entry a bot's avatar and rename actually live under.

The name, not the stack, sits on the center line; the face hangs above
it by half its own block.
2026-08-27 19:08:28 -05:00
Brooklyn Nicholson 45176219a2 feat(desktop): let a plugin title an empty chat it owns
Core has one empty state, the intro splash, and it belongs to a fresh
draft with no session selected. A session that exists but has nothing in
it yet falls outside that, and whoever opened it is the only one who
knows what should stand in the gap — core has no business learning a
bot's face and name.

Add `chat.empty` as a contribution area, resolved by the transcript the
same way `transcript.directives` resolves an inline widget. A
contribution mounts for every empty session and renders nothing for the
ones it does not own, so it can subscribe to its own stores and appear
once they load.

Pull the splash's lettering out of `intro.tsx` into a `Wordmark`
primitive so a second caller cannot fork it, and move the typeface,
weight and tracking into `.wordmark` beside the `.fit-text` rules they
travel with — as utilities they were one class-merge or one missed scan
away from silently rendering the wordmark in body text. `Wordmark` takes
its width, because fit-text sizes to fill and a short name set at the
splash's full width comes out enormous.
2026-08-27 19:08:28 -05:00
Brooklyn Nicholson 67854b50b0 fix(desktop): bot chats share core's unread and drop the branch rail
Bot Mode kept its own unread map, so a bot's dot and the session dot could
disagree about the same chat. Route it through core's store instead, keyed
by the canonical chat's stored id. A hidden session has no listed row to
read a profile from, so the writers take an explicit profile hint rather
than assuming the live gateway's — otherwise the marker persists into a
bucket that never held it.

The composer also hands a blank repoPath to the branch/worktree rail in a
bot chat. The row already hides itself without a repo and stops probing git
and GitHub with it, so one prop does what a second composer would have.
2026-08-27 19:08:28 -05:00
Brooklyn Nicholson 35b53af10d fix(desktop): stop the main zone vanishing behind Bot Mode
Contributions could scope themselves to a workspace, and Bot Mode scoped
every sessions pane out of the center. Close the last bot chat and the main
zone had nothing left to render, so it collapsed and the sidebar stretched
across where the app used to be. Bot chats are ordinary tabs in the main
strip, beside session tabs, so the filter goes: workspace mode stays as a
SIGNAL consumers adapt to, never a decision about whether a pane renders.

A structural floor backs it up — a subtree hosting the registered main pane
never collapses, whatever happens to its tabs. The shared "+" now falls
through to an ordinary session when the selected owner has no route of its
own, instead of refusing with a toast.

Also lets a pane contribution declare defaultCollapsed, so Bot Mode's
scheduled-jobs pane arrives as the right-edge rail tab rather than an open
zone. It applies when the pane enters the tree, so a user's expand persists
and is never overruled on a later boot.
2026-08-27 19:08:28 -05:00
Brooklyn Nicholson 4fcd16e224 feat(desktop): widen the plugin SDK to what Bot Mode had to reinvent
Every primitive Bot Mode hand-rolled was one the app already had but never
exported: the Panel master-detail family, SessionStatusDot, ColorSwatches +
PROFILE_SWATCHES, RowButton, DisclosureCaret, formatAgo, translateNow, and
the unread store behind the dot. Export them, with docblocks that say which
hand-rolled shape each one replaces so the next plugin doesn't repeat it.

warmAgent/ensureAgent accept an undefined connectionId alongside null, since
a roster row's is optional and both mean "no explicit source".
2026-08-27 19:08:28 -05:00
Brooklyn Nicholson 5afa487e93 refactor(desktop): rebuild Bot Mode on the app's design system
Bot Mode arrived as a 16,933-line plugin.js that reimplemented most of the
app: its own scroll container, color palette, status dots, empty states,
buttons, time formatting and cron surface, none of which could follow the
theme. Split it into 41 focused modules and route every one of those through
the primitives core already ships, so a Bot row now renders the same
SessionStatusDot, swatches and age labels as the session row beside it.

User-facing strings move into a plugin locale bundle instead of sitting
inline, and the UI settles on "bot" as the noun (model-facing prompt text
still says "agent"). The codemod scaffolding that drove the jsx() -> TSX
conversion retires with the conversion.
2026-08-27 19:08:28 -05:00
Brooklyn Nicholson b2e5b1d420 refactor(desktop): convert Bot Mode from hand-written jsx() calls to TSX
hermes-bots/plugin.js was 16,193 lines of hand-written JSX compiler output
— it imported { jsx, jsxs } from 'react/jsx-runtime' and called them
directly. Because the file was .js, eslint (scoped to plugins/**/*.{ts,tsx})
and tsc (allowJs: false) both skipped it entirely, so none of the design
system, import-fence, or type rules that govern the rest of the app ever
reached the largest UI surface we ship on by default.

Converting it back to JSX is an inverse-compile, not a rewrite, so it is
done by script rather than by hand:

- scripts/codemod/dejsx.mjs rewrites jsx()/jsxs() calls into JSX elements.
  735 conversions, none skipped. Comments between children become
  {/* … */} containers, since a bare // in children position is text.
- scripts/codemod/verify.mjs proves the result. It recompiles the .tsx
  through esbuild — an implementation independent of the codemod — and
  compares it to the original after normalizing away esbuild's own
  rewrites (quote style, void 0, numeric format, string/template folding,
  export hoisting) and alpha-renaming every binding per scope. Output:
  IDENTICAL across 333,711 normalized characters.

Two rewrite classes are semantics-preserving but not byte-identical, so
they are named and counted rather than hidden: 12 spread-children folds
(JSX has no spread-children syntax; children={[...xs]} can only be written
{xs}, which React flattens identically and which is the idiom the whole
ecosystem writes) and 1 redundant key prop (passed both in props and as
the third argument; React's jsx runtime never copies key into props).

No behavior change. 16,193 lines become 16,050 of real TSX.
2026-08-27 19:08:27 -05:00
Finn763 253b9d78c1 fix(desktop): keep bot chat focused when clicking the Bots pane (#96062)
Clicking a bot row moved the layout interaction tracker to the sidebar
group, so $focusedStoredSessionId fell back to the primary selection —
which is null in Bot Mode, because bot chats open as tiles and never set
$selectedStoredSessionId. The Bots plugin reads that null 'focused'
edge as 'the chat lost the center', releases its open claim, and the
Bots home re-asserts over the still-visible chat: the UI jumps to the
list instead of staying in the chat.

$focusedStoredSessionId now answers from the main zone's active tile in
Bot Mode before falling back to the selection, so a chrome-sidebar
click no longer fabricates a null edge; a genuinely closed chat (no
tile in main) still surfaces null and the home returns as before.

Regression tests cover the sidebar-click case (red before the fix),
plus guards for the closed-chat and sessions-mode derivations.
2026-08-27 13:48:57 -07:00
Teknium 9a9e9074cb style: sort MINIMIZED_TRACK import (perfectionist lint) for salvaged #95956 2026-08-27 13:48:45 -07:00
Thomas Bekkers dbca7a4f02 fix(hermes-bots): keep the Cronjobs tile registered while it holds focus in Bot Mode
Clicking the Cronjobs tile shifts focus onto the tile itself, momentarily
dropping bot-chat workspace ownership — syncRoutinesPane then unregistered
the pane out from under the user's own click, with no way back. Keep the
tile while Bot Mode is on screen and the tile is the focused surface;
leaving Bot Mode still unregisters as designed. Live-verified.
2026-08-27 13:48:45 -07:00
Thomas Bekkers 584f3a748b fix(desktop): keep a restore tab when a pane or strip collapses (#91223)
Hiding the Sessions/Bots strip, or tapping the header of a lone docked
tile (Cronjobs and any plugin pane beside the workspace), left no mouse
path back: the restore menu lived on chrome the gesture just unmounted,
and a row-collapsed rail could size to 0px.

Treat hide-only chrome as stranded so `never` cannot hide those chips.
Stop collapsing on header tap (chevron only). Size a minimized zone to
MINIMIZED_TRACK and keep the horizontal strip when two or more tabs
remain.
2026-08-27 13:48:45 -07:00
Brooklyn Nicholson a24c12d14f fix(desktop): gate transcript budget cap so Show earlier works
The render-phase cap snapped a visible pane's Show-earlier growth back
on the next render, so the button did nothing. Clamp only hot-hidden
panes, and grow the DOM budget when expanding the store window too.

Supersedes #87686.

Co-authored-by: Kirk <317508070+chukirk-svg@users.noreply.github.com>
Co-authored-by: Per0 <175494353+Per0-1@users.noreply.github.com>
2026-08-27 14:51:13 -05:00
hermes-seaeye[bot] ca2a0d4d6f fmt(js): npm run fix on merge (#96506)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-27 16:29:27 +00:00
HexLab98 c086bb6f71 test(desktop): cover Voxtral JSON unwrap on client-direct STT
Pin the Groq plain-text path and the Mistral envelope so dictation
keeps spoken words, not the raw transcription object, in the composer.
2026-08-27 11:23:40 -05:00
HexLab98 bc737576ba fix(desktop): unwrap Mistral Voxtral JSON in client-direct STT
Mistral ignores response_format=text and returns the full transcription
object. Desktop was dumping that JSON into the composer; pull .text out
so dictation shows the spoken words instead.
2026-08-27 11:23:40 -05:00
fangliquanflq f54d015470 fix(desktop): retain remote owner after session resume 2026-08-27 11:22:35 -05:00
Gille 46f091b93e fix(desktop): recover cloud auth through portal (#96170) 2026-08-27 11:22:09 -05:00
Teknium dcabb39ab0 test(desktop/bots): drop unused prompt params in empty-sentinel harness (lint) 2026-08-27 03:57:25 -07:00
RibatTRW f05fec3565 fix(desktop,bots): render "(empty)" sentinel as a friendly message in group chat
The agent loop writes an internal "(empty)" sentinel when the
nudge/prefill/empties/fallback ladder all fail. The gateway converts it
into a user-friendly notice at delivery, but the desktop group-chat
bridge appended the raw sentinel into the room log (seen posting
"(empty)" in a Bot Mode group room), and it synced to the shared
ui_meta for mobile.

Normalize at the single choke point, appendGroupChatEntry, mirroring
gateway/run.py substitution so group chat and gateway surfaces show the
same text. (pass)/empty silence semantics unchanged. Includes a
regression test proven to fail on the pre-fix code.

Fixes #94308
2026-08-27 03:57:25 -07:00
chelsealong 42e0b5f24f test(desktop/bots): pin harvestStrandedGroupReply's rescued-delivery path
Address review feedback on #94386: the new tests only exercised
runGroupChatMemberTurn's use of pickGroupTurnReply. Add the analogous
case for harvestStrandedGroupReply (substantive answer -> synthetic
continuation nudge -> (pass) tail) and document the pass-only tie-break
(newest wins) in pickGroupTurnReply's docstring.
2026-08-27 03:57:25 -07:00
chelsealong 8d412e67ba fix(desktop/bots): keep a substantive group reply after a synthetic (pass)
runGroupChatMemberTurn (and harvestStrandedGroupReply) selected only the
last assistant message in a finished turn. A Codex intent-ack continuation
nudge can land a complete, substantive room answer and then get a
synthetic "(pass)" reply to the nudge itself — the terminal message picked
by the old scan, which silently discarded the real answer (#94376).

Both call sites now scan the messages appended this turn for the last
substantive (non-pass) assistant reply, falling back to a pass only when
no substantive answer exists in that window.
2026-08-27 03:57:25 -07:00
Teknium b00e71dc93 test(desktop): lock the Stop button to room.running and the stop primitive
Source-contract tests (the group-room-ux pattern): the workspace renders
the Stop button only while room.running, wires it to stopGroupThread
(not the #94570 per-member interrupt spray), and carries no hardcoded
CJK label.
2026-08-27 03:56:37 -07:00
Lancaster.Q c5e0def79b feat(desktop): Stop button for a running group-chat round (#94570)
Salvaged from #94570 (@ShonnQ): the Activity bar gains a Stop button
while a round is running (room.running), plus an inline Stop on the
expanded 'working' activity row. Rewired from the original per-member
session.interrupt spray onto the stopGroupThread primitive so the round
loop actually stops (epoch bump + holds + on-turn interrupt) instead of
marching to the next member; labels are plain English like the rest of
the plugin's UI strings.

Co-authored-by: Hermes Agent <agent@nousresearch.com>
2026-08-27 03:56:37 -07:00
Teknium 1b575c65ab fix(desktop): real stop primitive for group-chat rounds (#91868, #94569)
stopGroupThread(group, thread, members?) is the room's first true
cancellation primitive: it bumps the room epoch (the driving loop bails
at its next member boundary), sets #93129 holds for every member (no
future turns until an explicit release), records a 'stopped' activity
event on the new epoch, and sends session.interrupt to the member
currently on turn via its own route — previously the plugin issued zero
interrupt RPCs, so 'stop' meant waiting out the in-flight model call.

The runGroupChatMemberTurnLeased poll loop now abandons a turn whose
dispatch epoch went stale WHILE its member is held — the stop signature.
An ordinary newer-send epoch bump without a hold still polls to
completion so late work keeps landing (#93127 commit check unchanged).
2026-08-27 03:56:37 -07:00
hermes-seaeye[bot] 8d30c20449 fmt(js): npm run fix on merge (#96263)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-27 10:16:14 +00:00
Teknium 1ae2c2b171 fix(bots): label cap-forced drive exits distinctly from consensus settle (#94478)
Follow-up to the #94755 salvage: every runGroupChatRounds exit recorded
'settled', so a room that died at the round/message/continuation cap looked
identical to genuine consensus. Track the exit kind and record 'capped'
(with label + glyph) when a cap ended the drive, and pin the exit-path
wiring with source-contract tests.
2026-08-27 03:10:13 -07:00
beplee 411f9c2f44 fix(bots): bound continuation rounds + index-order mention tracking (#94755 review)
- GROUP_CHAT_MAX_CONTINUATIONS=2 caps continuation rounds independently of
  the message cap, so pathological @mention chains can't consume the room's
  whole budget on handoffs.
- unaddressedGroupMentions now orders by log INDEX instead of entry id:
  ids are UUIDs (groupChatEntryId), not monotonic — string comparison could
  both re-drive answered members and miss stranded ones.
- New unaddressed-mentions.test.mjs exercises the REAL function via the
  vm-slice pattern (replacing concept-only helpers) and pins the ordering
  fix with a UUID-vs-log-order case.
2026-08-27 03:10:13 -07:00
beplee 24a5b6ecb7 fix(bots): drive cited member after an unanswered @mention handoff
In Bot Mode group chats, a member reply that @mentions a teammate never
drove the cited bot when the current round went quiet: the
'spokeThisRound === 0' early exit treated a zero-reply round as 'everyone
passed' and settled the room, even though the reply's @mention was
pending. The same silent settle happened whenever GROUP_CHAT_MAX_ROUNDS
or GROUP_CHAT_MAX_MESSAGES landed between the mention and the next
round (#94478).

Fix:
- unaddressedGroupMentions() detects member-to-member citations in the
  thread whose cited member has not posted anything after the citing
  entry (self-mentions excluded; user sends re-drive everyone anyway).
- The quiet-round exit now checks for such pending handoffs and runs one
  bounded continuation round driving exactly those cited members — same
  holds/stranded/epoch/cap machinery as ordinary rounds, so nothing new
  is trusted.
- If the continuation also produces nothing (pass, failure, or cap), the
  room settles as before; behavior only changes where a bot was actually
  called and never answered.

Regression tests in plugins tests family (2 new); full hermes-bots
plugin suite stays green (558/558).

Fixes #94478
2026-08-27 03:10:13 -07:00
Casey beb212dcc5 desktop: fix two managed-SSH-spawn bugs that break every fresh remote backend
1. Quoting: the spawn payload wrapped expandRemotePath() output -- already
   a shell-quoted fragment like "$HOME"'/...' -- in shq() again, so the
   reservation/lock/owner_file variables hold the quote characters
   literally and every mkdir "$reservation" fails forever (~5 min per
   attempt spinning in the reservation loop while holding the box-global
   update mutex; queued spawns starve behind it). The same double quoting
   sits in the stale-reaper identity guards, making every reap REFUSE.
   The lockfile-reuse path masks the bug for existing backends, so it
   only bites on fresh spawns.
2. Bashism: lockfile publication used ${var//__PID__/$child} -- bash-only
   substitution in a payload run under plain sh (dash on Ubuntu), which
   aborts the script AFTER the serve was spawned. The client then saw an
   unknown failure, ran its error cleanup (deleting the token file), and
   the just-booted serve died on the missing token -- orphaning one serve
   per attempt. Replaced with a POSIX sed substitution.

Adds two regression tests: payload variables must keep $HOME expandable
(no re-quoting), and the pid substitution must be POSIX sh. Both fail
against the previous code; all 89 remote-lifecycle tests pass with the
fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 02:48:21 -07:00
Teknium 9faa685385 feat(desktop): read-only stored-transcript resume + legacy owner-backfill trigger (#94724)
The fail-closed owner ladder (#95407) is correct for new sessions, but
legacy unowned rows on registry-topology installs dead-ended in
SessionOwnerResolutionError (reporter's Error B) with their transcripts
fully intact in state.db.

- resolveLegacyOwnerBackfillScope: pick the single-match store for the
  server-side owner backfill at enumeration time (serving registered
  connection / primary pool); fail closed on multi-candidate topologies.
- maybeBackfillLegacySessionOwners: one-shot per scope per renderer,
  fire-and-forget from the #95407 stamp path, logs the stamped count.
- Read-only stored-transcript resume: when session.resume fails closed,
  fetch the transcript over id-only REST (ambient first, then registered
  backends, read-only probes only) and open the session as a read-only
  transcript instead of dead-ending; sends are refused with a notice and
  a later successful live resume clears the latch. Wired into the main
  pane resume recovery and the session-tile delegate (which now runs the
  same fail-closed owner gate as the RPC dispatcher).

Refs #94724
2026-08-27 02:17:56 -07:00
Teknium 65974a3e7c feat(desktop): browser_exec rows use the leading # comment as their title, matching CLI/TUI (#96093) 2026-08-27 02:17:13 -07:00
kshitijk4poor 9f05b06589 Revert "Merge pull request #94245 from kshitijk4poor/feat/gw-event-replay"
This reverts commit df7d7f6e8d, reversing
changes made to 1a66134404.
2026-08-27 11:26:57 +05:30
kshitij df7d7f6e8d Merge pull request #94245 from kshitijk4poor/feat/gw-event-replay
feat(gateway): slim WS-only server — remove FastAPI/uvicorn from desktop boot path
2026-08-27 11:22:41 +05:30
hermes-seaeye[bot] 36b0a96dcb fmt(js): npm run fix on merge (#96076)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-27 04:43:58 +00:00
Teknium ff03d46eef test(desktop): method-aware gateway mock for the refresh-reconcile confirm interaction test
The reconcile-to-guarded-model interaction test's requestGateway mock
must only answer config.set with the confirm handshake — the panel's
model.options read rides the same dispatcher and was eating the
first mocked response.
2026-08-26 21:38:41 -07:00