Commit Graph

325 Commits

Author SHA1 Message Date
hermes-seaeye[bot]
0f1668ef76 fmt(js): npm run fix on merge (#108753)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-12 02:33:53 +00:00
Teknium
9c021a6bde fix(desktop): Bot Mode pet picker selects locally hatched pets via pet.thumb
Selecting a self-generated pet in the Bot avatar picker always failed with
"Could not load that pet — try another", and its tile showed only a name.
Locally hatched pets have no petdex manifest entry, so pet.gallery reports an
empty spritesheetUrl; the picker cropped frame 0 client-side from that URL and
bailed on the empty string. The same raw CDN fetch also lacked a User-Agent,
which the petdex CDN rejects with 403 (#90465), so manifest pets could fail on
the same path.

Route tile rendering and selection through the gateway's pet.thumb RPC, which
already backs the Settings pet picker: it crops frame 0 server-side from the
installed sheet on disk (or the host-validated CDN URL for uninstalled pets)
and returns a same-origin PNG data URI. The client cache is keyed by slug,
evicts failures so a blip never poisons a tile, and races a 15s deadline so a
hung RPC cannot park a pending promise forever.

Based on analysis from PR #90931, whose target (plugin.js) has since been
decomposed into pet.tsx.

Co-authored-by: m1k3s0 <44042865+m1k3s0@users.noreply.github.com>
2026-09-11 19:28:29 -07:00
hermes-seaeye[bot]
7469c0f2a5 fmt(js): npm run fix on merge (#108522)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-11 19:00:50 +00:00
KoNit-K
3b45681c25 fix(desktop): qualify group (you) by connection, not bare profile name
Same-named defaults on different connections were mislabeled as (you) in
member room-delta prompts. Compare speaker/viewer with connection source
identity so only the true self gets the suffix.

Fixes #106851

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 16:39:27 +02:00
Teknium
52fb4e0877 fix(desktop): read Bot Mode avatars over the active socket, not one dial per bot
useRoster repaints every 5s and hands pullServerAvatars the active-source
rows. Since the multi-source merge (ed20a6f01a) every such row is
sourceScoped, so the avatar sync branch chose requestForBot and dialed each
bot's OWN backend to read a profile-directory PNG: a fresh WebSocket with
one JSON-RPC message, torn down at refcount 0, per bot per tick (#99336's
"ws accepted / ws closed messages=1" every ~5.2s on background profiles),
and for every bot with no running backend a pool spawn that waits out
POOL_SLOT_WAIT_MS (30s) and is re-queued by the next paint, forever, once
the pool is full (#102913's per-bot "waiting for a free local slot ...
timed out" cadence). The loop was self-sustaining because the plugin's own
160px face raster is deliberately not parked in $botMeta, so the empty
image slot re-fetched it on every tick.

Assets are files under the profile directory; the gateway that just
answered profiles.list reads them for any of its profiles. Route the three
avatar RPCs through host.request like the roster query itself, and remember
face-only answers so a row is fetched once, not once per tick.

Not changed: relay.ts (its loops dedupe to one route per registered
connection and return early below two connections, so a single-connection
desktop never issues a relay RPC), and useRoster's own profiles.list, which
already rides the active socket via requestForBot({name}).
2026-09-11 06:21:24 -07:00
Teknium
e4080b381b fix(desktop): roster avatar sync no longer dials a backend per registered profile at launch
The first Bot Mode roster paint after launch ran pullServerAvatars over every
row, and for a source-scoped row (every row on a local-primary desktop once
host.agents annotates the roster) each profiles.get_asset / set_asset went
through requestForBot -> host.requestProfile -> requestGatewayForAgent, i.e. a
(connectionId, profile) secondary that spawns that profile's pooled backend.
With ~60 registered profiles and 3 warm slots this queued 56 background spawns
at boot; each queued dial then rode reconnectSecondary's backoff until the
stall budget parked it (#107969), so the pool never drained and desktop.log
filled with "waiting for a free local slot" (#102978).

The active gateway's own profiles.list already produced these rows by reading
every local profile directory; get_asset/set_asset are the same directory
reads addressed by name. Route both through host.request on the active socket
with the row's backend profile name (route.targetProfile, so managed aliases
still resolve). No secondary socket, no pool slot, no spawn.

Cross-connection (remoteSource) rows never reached this path: pullServerAvatars
is fed activeSourceRoster, which filters them out.
2026-09-11 06:21:24 -07:00
hermes-seaeye[bot]
efca39279a fmt(js): npm run fix on merge (#108214)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-11 12:53:17 +00:00
xxxigm
5e9157658a fix(desktop): stage group-chat PDFs through file.attach
Direct chat already uploads PDFs into the session workspace. Group turns
called pdf.attach instead, which needs pdftoppm and swallowed failures, so
bots saw the filename and no file. Stage PDFs the same way as other files,
and name a failed attach in that member's prompt.
2026-09-11 07:30:43 -05:00
xxxigm
8184051be7 test(bot-mode): pin group PDF staging to the 1:1 file.attach contract
Group PDFs currently only hit pdf.attach, so a member prompt can name the
file while the session workspace never receives it. These tests require the
same file.attach + @file: ref path 1:1 chat uses, and a named failure when
that staging throws.
2026-09-11 07:30:43 -05:00
brooklyn!
8429a54bac fix(desktop): track the exact group member through presence and stop
Keep a runtime turn descriptor, feed its roster key into row mood and activity filtering, and release only the completing invocation’s presence. Stop uses the captured owner rather than a name lookup.

Co-authored-by: Tuna Dev <tuancookiez@gmail.com>
2026-09-11 07:09:01 -05:00
brooklyn!
6fa8518b44 fix(desktop): reject unsupported group slash commands before delivery
Preserve drafts and attachments through the existing nothing-sent contract. Port the command-shaped guard to the shared send boundary with behavioral coverage and localized feedback.

Co-authored-by: ClintonEmok <54935030+ClintonEmok@users.noreply.github.com>
2026-09-11 07:09:01 -05:00
Erosika
2954fc7c55 fix(desktop): keep the relay deliver timeout beside its call
relay-deliver-budget.test.ts reads relay.ts and expects RELAY_DELIVER_TIMEOUT_MS within 400 characters of the bot_relay.deliver call. The comment above the new from_connection parameter pushed it past that window. The parameter names say what the gateway does with them.
2026-09-10 10:27:07 -07:00
Erosika
3db4defcc1 fix(bot-mode): qualify a relayed author from the local connection too
delivery_turn_author kept the bare bot:<profile> id when the sender's connection was the Desktop's own "local", so a DM relayed from that machine collided with the recipient's profile of the same name. A relayed DM always crosses gateways, so the connection id is now part of the id whenever the Desktop sends one, and only the direct message_agent path in tools/bot_mode_dm.py stays bare. The relay.ts and session_auto_continue.py comments added earlier are cut to one line each.
2026-09-10 10:27:07 -07:00
Erosika
d3c8bacbe3 fix(bot-mode): qualify relayed authors with the sender's connection id
A relayed DM stamped bot:<profile> on the recipient turn, so an ops profile on another machine and the local ops profile shared one author id. The Desktop now forwards from_connection with each bot_relay.deliver, and delivery_turn_author builds bot:<connection>/<profile> for it while the Desktop's own gateway ("local") keeps the bare id. An api author object accepts an optional origin string that yields the same shape.
2026-09-10 10:27:07 -07:00
Erosika
16d15869ba feat(bot-mode): carry the sender through local and relay deliveries
a bot dm arrived as an ordinary user message. the only trace of the sender
was the "Message from" text prefix, which the model reads and nothing else
does. the recipient's memory provider saw its own configured user.

message_agent now passes the sender as {"id": "bot:<profile>", "name":
<handle>, "is_bot": true} to the delivery runner (--author <json>), which sets
HERMES_TURN_AUTHOR on the recipient one-shot only. the -Q turn reads it and
passes turn_author into run_conversation. the desktop relay forwards the
envelope's from_profile/from_handle to bot_relay.deliver, which sets the same
variable on its delivery turn. the runner drops any inherited author first so
a delivery without one stays unattributed. the text prefix is unchanged.
2026-09-10 10:27:07 -07:00
Erosika
b1915cb02d fix(bot-mode): keep the relay waiter watching past the Desktop deliver deadline
The sender-side waiter gave up at 900s while the Desktop held bot_relay.deliver open for 1500s, so a turn finishing between minute 15 and minute 25 wrote a reply nobody read. REPLY_WAIT_SECONDS now rebuilds the Desktop budget from the same numbers and waits 60s past it. The two turn constants move into tools/bot_relay.py so the gateway handler and the waiter share one definition.
2026-09-10 10:18:27 -07:00
Teknium
ff93f0ee9b fix(desktop): desktop plugins are app-level, not per profile; catalog gets a drag sash; Accent Picker moves to its own repo
Three things Teknium hit on the merged Plugins page:

1. Desktop plugins vanished on profile switch. `fs-ipc.ts::localPluginsRoot`
   resolved `<HERMES_HOME>/profiles/<active>/desktop-plugins` for a named
   Desktop profile, so a disk-installed plugin only existed under the profile
   it was installed from. Desktop plugins extend the app, not an agent; the
   root is now `<HERMES_HOME>/desktop-plugins` regardless of profile, gateway
   or remote machine (`electron/desktop-plugins-root.ts`), with a one-time
   migration that lifts any per-profile folders into the app root (root copy
   wins on collision). Agent-plugin and logs roots stay profile-scoped.

   On the page, the profile selector now renders INSIDE the framed Agent
   plugins block as its header, and Desktop plugins sit outside that frame,
   so the scope boundary is visible without reading the blurb.

2. The catalog viewport could not be resized. It gets the same top-edge drag
   sash as the Skills hub picker (persisted height, double-click resets,
   clamped so the lists above keep real height; iframe pointer-events off
   while dragging).

3. Accent Picker, a theme-authoring toy shipped off by default, is removed
   from the bundled set and published as a standalone desktop plugin at
   https://github.com/NousResearch/hermes-desktop-accent-picker (same source,
   esbuild-bundled plugin.js importing only the three loader-resolvable
   specifiers). Docs point there.
2026-09-10 08:19:55 -07:00
Teknium
61afcde8f9 refactor(desktop): one Plugins surface — Capabilities → Plugins owns agent + desktop plugins, install, and the catalog
Plugins were split across two pages that each showed half the picture:
Settings → Plugins listed desktop plugins plus "Install from Git" and a
pointer saying agent plugins live elsewhere; Capabilities → Plugins listed
agent plugins plus the catalog picker but knew nothing about desktop
plugins. A user asking "what extends my Hermes and where do I add more?"
had to visit both and still could not see the whole set in one place.

Capabilities → Plugins is now THE plugins page:

- Agent plugins section (scoped to the profile selector) with the
  "Install from Git" button in its header — installs target the scoped
  profile, not whichever one is active.
- Desktop plugins section beneath it (same for every profile), with the
  folder/rescan controls and the "agent half missing here" drift chip,
  whose repair also lands in the SCOPED profile.
- The catalog picker underneath, unchanged.

Settings → Plugins is removed. `/settings?tab=plugins[&plugin=…]` and the
existing `?tab=mcp` redirect share one table (`settings/moved-tabs.ts`) so
old bookmarks and palette links land on the same row on the new page.
Command palette: plugins moved from the Settings group to the Capabilities
group; installed-plugin rows deep-link to `/skills?tab=plugins&plugin=…`.
Dead `settings.plugins.agent.*` and `settings.nav.plugins` i18n keys dropped;
docs and in-code pointers say Capabilities → Plugins.
2026-09-10 02:30:50 -07:00
brooklyn!
c7676fc6b7 docs(desktop): explain radio artist lookups and privacy 2026-09-09 23:32:55 -05:00
brooklyn!
983f0c2870 feat(desktop): link radio credits directly to artist profiles 2026-09-09 23:32:55 -05:00
brooklyn!
a6102b8d80 docs(desktop): document Radio controls and privacy 2026-09-09 22:57:27 -05:00
brooklyn!
3f53719c1d feat(desktop): ship Radio as an opt-in SDK plugin 2026-09-09 22:57:27 -05:00
hermes-seaeye[bot]
c076d653a2 fmt(js): npm run fix on merge (#106065)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-08 21:28:40 +00:00
hermes-seaeye[bot]
e9bb6e86fb fmt(js): npm run fix on merge (#106039)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-08 21:23:02 +00:00
Teknium
bc952a78ce fix: integrate group attention with room ordering and working avatars 2026-09-08 13:54:58 -07:00
Adolanium
ab6f4c5408 fix(desktop): show the focused bot's working think pose
Port Adolanium's focused-turn pose from Hermes-Bot-Mode#101 and
hermes-agent#88134 to the current typed Bot Mode implementation.
Match the busy signal's connection-qualified focused owner rather than
the gateway socket, retain worker activity, and ease transitions in
elapsed time on the existing shared face clock.

Includes owner-isolation and animated-pose invariants, both proven red
on origin/main, and native Electron before/after verification against
a real temporary Hermes backend with held loopback inference.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-09-08 13:38:53 -07:00
Teknium
9d66e76c5d feat(desktop): let users order Group Chat rooms
Add Move up/down controls for actual rooms without changing bot or folder
ordering. Preserve default pin/activity ordering until an explicit move,
retain hidden room slots, and persist Desktop-local order through room
updates, mirror merges, and hydration. No membership or routing writes.

Adapted narrowly from the group ordering idea in archived
NousResearch/Hermes-Bot-Mode#105 by @onuraycicek; rename already exists.

Co-authored-by: Onur Aycicek <onur.m.aycicek@gmail.com>
2026-09-08 13:37:19 -07:00
Teknium
acbe72ed1f fix: keep Bot Mode pet selection rings inside the gallery 2026-09-08 13:35:51 -07:00
Teknium
f9993d5be2 refactor: extract group member phases and roster presentation 2026-09-08 13:29:18 -07:00
Teknium
e1c8764d18 fix: fence retired group member failure cues 2026-09-08 13:29:18 -07:00
Teknium
613ec20304 fix(bot-mode): keep pending group attention bound to its live room
Complete the PR #101452 salvage, preserving sprmn24 authorship. Credit @kokhlo PR #101591 for independently diagnosing the in-flight rename race; use scoped authoritative room bindings rather than persistent aliases. Keep mention attention independent and retire late work after disband.
2026-09-08 13:29:18 -07:00
sprmn24
ad08688bc6 fix(bot-mode): derive group clarify/approval attention from $groupClarify instead of duplicating it into $groupNeedsYou
$groupNeedsYou was written by two independent sources: an @user mention
(appendGroupChatEntry) and, until now, syncGroupClarify whenever a member
blocked on a clarify or approval. Nothing kept the two in sync, so every
path that consumed a clarify -- answering it, the server resolving it,
disbanding or renaming the room -- left a stale badge lit with nothing
behind it, because none of them cleared the copy syncGroupClarify had
written. A naive fix (writing false back into $groupNeedsYou on every
clarify cleanup path) would have created the opposite bug: clearing an
unrelated, still-unresolved @user mention.

$groupClarify is already the source of truth for pending clarify/
approval attention. syncGroupClarify no longer writes $groupNeedsYou; a
new groupHasPendingClarify(clarifies, group) derives the same signal
from a $groupClarify snapshot. It's intentionally pure -- the caller
(roster-pane) subscribes to $groupClarify itself via useValue and passes
the live snapshot in, so the subscription actually drives the
recalculation instead of existing only to force a re-render. A prompt
resolving, being answered, or its room being disbanded all self-correct
through the existing $groupClarify cleanup paths, with nothing left to
keep in sync.

Rename needed its own fix: the old clearGroupClarify(oldName) call on
rename dropped a clarify-only room's attention entirely, since clarify no
longer lives in $groupNeedsYou to be carried over by the existing
old->new key swap there. New renameGroupClarify(oldName, newName) re-keys
matching mirrors onto the new name in two passes -- unrelated entries
first, migrated entries last -- so a stale mirror already stranded at the
destination key (left behind by a known, separate in-flight-poll race)
can never clobber the just-migrated current prompt.

The roster now reads groupNeedsYou[group] || groupHasPendingClarify(...)
and subscribes to both stores so either one repaints the row.

Tests: multi-clarify sequencing, mention+clarify independence (resolving
one must not clear the other), server-side resolution cleanup, rename
migration, rename destination-collision (red-green verified against the
prior single-pass implementation), a GroupRow badge render test, and a
subscription harness using the real stores/useValue/helper end-to-end.

(cherry picked from commit e5038f12ccc1a4d1fb56ecc5f0e1a884f1e8b795)
2026-09-08 13:29:18 -07:00
Gille
9a017b9691 fix(desktop): refresh busy bot chat on explicit open 2026-09-07 23:16:13 -07:00
Teknium
3a7bf7455f fix: keep Bot Mode group replies visible in arrival order 2026-09-07 22:03:12 -07:00
Filipe Bezerra
e3ba651b6d fix(desktop): relay MCP OAuth through client-local callbacks 2026-09-07 06:10:28 -07:00
kshitijk4poor
40da71dbb4 docs(kanban): name the legacy-attachments cutoff (#35395) in the type comment
Desktop AGENTS.md requires a compat fallback to be 'tied to an identified
older runtime' so a future cleanup knows when it can be deleted. The
original comment said only 'older backend plugins'; name the actual
boundary: backends before #35395 (May 2026) omit the attachments key.

Review follow-up on the salvage of #104529.
2026-09-07 13:07:32 +05:30
Gille
0d7af8962f fix(desktop): tolerate legacy Kanban task attachment responses 2026-09-07 13:07:32 +05:30
Gille
96ed0e71ea fix(bot-mode): match scoped bot selection in reset guard 2026-09-05 16:17:23 -07:00
Teknium
f43b976209 fix(ci-fallout): repoint the in-container docker test, two windows-only tests and the desktop exe-integrity patches to the defining modules; name bot_relay turn timeout/attempts as constants so the Desktop mirror test reads them instead of counting subprocess.run sites 2026-09-03 17:28:02 -07:00
cmyyy
3ea71a47b3 fix(desktop): refresh Bot Chat transcript when a roster click fronts an already-open tab
A roster click on a bot whose canonical Bot Chat is already open only
fronted the tile: the pane kept whatever transcript it last painted,
which can predate rows the bot wrote while the user was elsewhere (a
cron delivery, a teammate's message_agent, another bot's turn). The
stale snapshot persisted until the next user turn — #95600's forceResume
only covered the not-yet-open registry path.

Reuse refreshOpenBotChat (the #99393 reclaim mechanism) on the fronted
branch so forceResume re-pulls the latest transcript. Regression test
pins the behavior: fronting an open Bot Chat now requests the canonical
registry open.
2026-09-03 01:10:16 -07:00
Teknium
bd6cc48b94 fix(desktop): annotate the resolvable target for aliased LOCAL rows too
The contributor fix covers remote rows. The reporter's video shows the
sibling shape: with a remote gateway active, the LOCAL twin carries the
'default-this-device' alias, and message_agent's local resolver only
knows bare profile names / 'hermes'. Emit the same target annotation
whenever a local row's alias differs from its resolvable handle.
2026-09-03 00:18:31 -07:00
liuhao1024
2e542c92e6 fix(desktop): annotate canonical relay targets for remote @mentions
The Bot Mode mention middleware built message_agent targets from
botHandle(), which prefers a roster row's source-qualified UI alias
("default-vera"). Neither resolver accepts that form — the relay matches
canonical handle/profile (± @connection-id) and the local path a bare
profile name or "hermes" — so remote handoffs died with "No teammate
named" before enqueue.

Annotate the canonical form instead: profile@connection-id for remote
rows, canonical bare handle (default→hermes) for local ones. Pin the
profile@connection form on the relay side too, so the emitted target
stays inside the documented resolver contract (#97678).
2026-09-03 00:18:31 -07:00
hermes-seaeye[bot]
bcef556b00 fmt(js): npm run fix on merge (#101481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-02 17:18:47 +00:00
Teknium
e9dd0bf5d5 feat(desktop): polish bot roster sections — dialog rename, Undo delete, Esc-cancel drag, nested under gateways (salvage #100745)
Follow-up on @fortun8te's user-made roster sections:

- Sections start empty: no seeded General/Workforce/Clients. With no
  sections created the roster renders exactly as before.
- New section and Rename go through one Dialog + Input + Cancel/Save
  (the app's session-rename shape) instead of an inline caret; the row
  menu's "New section…" files the bot as it creates.
- Delete needs no confirmation: bots return to Unassigned and the toast
  offers Undo (restores the section in its slot and refiles its bots).
- Drag: single-row drag under a private MIME type, every valid target
  shows a faint outline while a drag is live, the hovered target lights
  up, the source section refuses the drop, Escape cancels, and the moved
  row no longer stays faded after it remounts under its new section.
- Multi-select (cmd/shift-click, querySelectorAll shift-range) dropped:
  the roster has no selection model. Per-bot saveBotMeta writes run in
  sequence, one per profile (membership IS a field on each profile).
- Section heading reuses RosterSectionHeader (gains `action` /
  `onDoubleClick`), so user sections fold and look like the gateway
  headings; ⋯ menu and right-click drive the same Rename / Move up /
  Move down / Delete. Empty sections show a dashed "Drag bots here" slot.
- Composes with gateway buckets: sections nest INSIDE each connection
  bucket, indented under a hairline rail (membership lives in the bot's
  profile on that gateway); empty sections repeat there only mid-drag.
- Full i18n parity (en / ja / zh / zh-hant) for every new string; icon
  toggle and the storage-async plumbing removed.
- Tests trimmed to the three invariants (membership persists through
  saveBotMeta + reload, remainder = Unassigned, delete returns bots +
  undo) plus a live Electron e2e covering the whole flow.
- Docs: "Organize bots into sections" in user-guide/bot-mode.md.
2026-09-02 06:06:32 -07:00
Michael Knaap
bd9955d529 fix(desktop): section rename — Escape cancels, Enter commits once
Closing the rename field unmounts the input, and the unmount can still
fire onBlur, which committed the draft the user had just asked to throw
away with Escape. Enter also called commit() directly and then again from
blur. Route both through blur with a cancelled flag so the commit runs
exactly once and Escape never renames.
2026-09-02 06:06:32 -07:00
Michael Knaap
3d0ac691af feat(desktop): user-made sections in the bot roster, with drag-and-drop filing
The roster already has sections, but only automatic ones: one per gateway
connection plus the group-chat bucket. Those answer "where does this bot
run", which is not the question being asked when someone wants two client
bots filed together under "Clients" and the internal ones under "Team".

This adds a second axis that composes with the first: gateway sections keep
the top level whenever more than one connection is showing, and user
sections group the flat list underneath.

Design choices, each deliberate:

- Membership lives on the BOT (`ui_meta.sectionId`), not as a member list on
  the section. A bot can only be in one place, deleting a section cannot
  orphan anybody, and the assignment rides the same profile.yaml sync every
  other bot setting already uses, so it follows the profile to another
  machine. Section records (id, name, icon) live in plugin storage.
- "Unassigned" is not a section. It is whatever is left, always drawn last,
  and it is where members of a deleted section land. No record, so nothing
  to keep in sync.
- Three gestures, one rule: drag a row onto a section heading; cmd/ctrl-click
  and shift-click build a multi-selection (shift ranges in DOCUMENT order,
  anchored Finder-style); and the row's context menu gets "Move to section…"
  with the same targets the drag would use. Dragging a row that is part of
  the selection drags the whole selection.
- The drag uses a private MIME type, so a bot dropped on the composer or the
  transcript is simply not a valid payload there instead of pasting its key
  as text.
- Section headings rename inline (double-click / menu), reorder, hide their
  glyph, and delete (keeping their bots). Right-click and the ⋯ button open
  the same menu so neither can drift.

With no sections created the roster renders exactly as before.

Tests: user-sections.test.ts covers the pure model (normalisation, grouping
with unknown/deleted sections falling to Unassigned, drag payload
round-trip). The existing hermes-bots suite passes; tsc and eslint clean.
2026-09-02 06:06:32 -07:00
Teknium
d1f8c2ea11 test(desktop): group chat picker row label opts out of min-width:auto so names truncate
Renders CreateGroupChatDialog with a long-named bot and asserts the row
label carries min-w-0 (and the inner text column keeps min-w-0 flex-1 +
truncate). Sabotage-verified: fails against the pre-#90624 markup with
'expected [...] to include min-w-0'.
2026-09-02 05:42:14 -07:00
Jay
c65c79a4a8 fix(bot-mode): group chat picker rows overflow and scroll names out of view
The New Group Chat picker's rows are `label` flex containers holding a
`min-w-0 flex-1` text column whose two lines are `truncate`. The label
itself has no `min-w-0`, so as a flex item it keeps its `auto` minimum
width and cannot shrink below its content. `truncate` therefore never
fires: the row grows to fit the longest secondary line instead, which is
`@handle · in "Group A", "Group B", …` and so scales with how many groups
a bot already belongs to.

The row is a grid item inside a Radix ScrollArea, whose viewport wraps
children in a `display: table` div that sizes to content, so the whole
list widens rather than clipping.

Measured on a 414px viewport with one bot in three groups:

  wrapper width  593  (viewport 414)
  widest row     585
  overflows      yes

Nothing is visibly wrong until the first click. The checkbox now sits
past the right edge, so focusing it scrolls it into view: `scrollLeft`
jumps 0 → 178.38 (= 593 − 414) and every row shifts to `left: -162px`,
clipping the bot names from the left — the user clicks a name and the
names disappear. The scroll offset persists after unchecking, until the
dialog is remounted.

Adding `min-w-0` to the label lets it shrink, so `truncate` engages as
the markup already intended. Same viewport, same data:

  wrapper width  414  (unchanged display: table)
  widest row     406
  overflows      no
  scrollLeft after clicking a row  0 → 0

`display: table` on the ScrollArea wrapper is untouched; the fix works
with it rather than around it. Verified against a packaged build via CDP,
before and after, on identical roster data.

No test. The rule for this is "extract the logic into a small
pure/DI-testable function and call it for real", but there is no logic
here — `min-w-0` is a class name, and the behaviour under test belongs to
the layout engine. jsdom does not lay out, so a unit test cannot observe
the overflow; the Playwright suite could, but has no bots-roster fixture,
which is a large scaffold to hang off a one-class change. A source-regex
assertion would pass without ever laying anything out, which is precisely
the false confidence AGENTS.md describes. The before/after measurements
above are offered as the evidence instead — happy to add a Playwright
case if you'd rather have the fixture.
2026-09-02 05:42:14 -07:00
Teknium
209de12d5f fix(desktop): Bot Mode tabs caption a Bot Chat with the bot's name, not "Bot Chat"
Every bot's canonical chat is stored under the same title ("Bot Chat" — the
name the gateway resolves it by, and an invariant roster-actions.ts's stale-tile
probe and #90102 rely on), so the main tab strip captioned every open bot chat
identically and two bots' tabs were indistinguishable (#99152).

Fix at the presentation layer, leaving the stored title and tabTitle untouched:
- workspace-scope.ts gains `$workspaceOwnerLabels` + `workspaceOwnerTitle()`:
  a bots-mode tab whose resolved title still equals its registered placeholder
  reads its owner's label instead. Side threads / Sessions tabs are untouched.
- session-tile.tsx captions tiles through it (and the drag payload); the main
  `workspace` tab (controller.tsx) does the same via `$botChatScopes`, the
  bot-mode scope the main tab was last opened under (it has no tile).
- The hermes-bots roster publishes displayName() per owner key through the new
  `host.setWorkspaceOwnerLabel` (feature-detected), so renames follow.

Supersedes #99177, which set tabTitle at open time — that reverts after mount
because tileTitle() prefers the stored row's title once the hidden row is
upserted, and breaks the `workspaceTabTitle === 'Bot Chat'` invariant.

Tests: one unit test on workspaceOwnerTitle() (bot chat → bot name; side
thread / sessions tab / unlabeled owner untouched) and one Electron e2e
(tab strip reads "Alpha", not "Bot Chat"); both fail on main, pass here.

Closes #99152
Supersedes #99177

Co-authored-by: twotnguyen <nguyenngoctinh011258@gmail.com>
2026-09-02 05:38:10 -07:00
Gille
2599793271 fix(bot-mode): hand off group tabs to remote bots 2026-09-02 05:36:54 -07:00