Commit Graph

3953 Commits

Author SHA1 Message Date
hermes-seaeye[bot] d7c0fb9d66 fmt(js): npm run fix on merge (#97706)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-29 05:56:24 +00:00
Brooklyn Nicholson 178c23fb27 fix(desktop): open HUD links in the system browser
The HUD has no in-app browser, so a click tried to paint a webview
into the transparent overlay (OAuth). Hand those links to the OS,
mount the context menu, and skip preview-tile docking.
2026-08-29 00:51:43 -05:00
Brooklyn Nicholson 240790af60 fix(desktop): let HUD prompts take clicks on solid X11
Ignore-mouse cannot restore on X11, so a visible band that still has
pointer-events:none swallows clarify options and links. Held prompts
and solid-window bands now take the pointer without composer focus.
2026-08-29 00:51:43 -05:00
hermes-seaeye[bot] ee742fe1bc fmt(js): npm run fix on merge (#97642)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-29 03:51:25 +00:00
Brooklyn Nicholson d9d1ee8357 fix(desktop): board switcher crashed on every render
The rename and settings dialogs stay mounted while closed, so they render
with a null board on every pass. Their mutation callbacks read `board!.slug`,
and the React Compiler lifts a callback's property reads into its render-time
dependency check — so the read escaped the closure and dereferenced null
immediately on mount, taking the whole contribution down behind its error
boundary.

The non-null assertion never guarded anything; it erases at compile time.
Resolve the slug null-safely in the component body instead, which is also
the form the compiler can hoist safely.

Only the bare-lambda shape is affected: the inline `useMutation({ mutationFn })`
this replaced memoized on the whole `board` object and kept the read inside
the closure, so the regression arrived with the extraction into
`useBoardWrite`, not with the feature.
2026-08-28 22:45:28 -05:00
hermes-seaeye[bot] cb77fcb008 fmt(js): npm run fix on merge (#97638)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-29 03:43:33 +00:00
Brooklyn Nicholson 36620578f0 test(desktop): follow the model and path menu labels to sentence case
These query by visible text, so the casing pass moved the labels out
from under them.
2026-08-28 22:38:01 -05:00
Brooklyn Nicholson 4054d54926 refactor(desktop): menu labels are bare verbs in sentence case
Per-item menus across the app say "Rename", "Delete", "Export",
"Archive" — the row already names what you are acting on. A handful of
places had drifted to verb+noun or Title Case, so the same action read
differently depending on where you found it.

Sessions and projects now say "Rename…" like profiles and the file tree
already did. The per-profile context menu says "Export…"; the noun stays
on the profiles-list button and the native file-dialog title, which
stand alone. Bots drop "Delete Group" and "Edit Profile" for "Delete"
and "Edit…". Title Case gives way to sentence case in the file menu,
review tree, and model menu.

Nouns are kept wherever they carry weight: dialog titles, icon-button
tooltips, "Remove worktree" (its menu also has a plain "Remove"), and
"Open Bot Chat", which names the canonical session titled exactly that.
2026-08-28 22:38:01 -05:00
Brooklyn Nicholson 72cf8d1fac feat(desktop): export, import, rename and delete a board from the switcher
The board switcher could create and configure boards but not move,
rename, or remove one. Rename technically existed, buried as a field
inside "Settings…", which is why it read as missing; it now has its own
entry and the settings dialog is left owning scope alone.

Delete archives rather than erases — the board's directory moves to
boards/_archived/ and the toast names the path — and never appears for
`default`, which the backend refuses to remove.

The three dialogs had grown three copies of the same shell, the same
"invalidate the list and close" mutation tail, and the same name field,
so those are shared now instead of parallel-implemented.
2026-08-28 22:38:01 -05:00
Brooklyn Nicholson c57f8ad4e1 feat(desktop): PluginOs gains native save/open file pickers
Plugins had no sanctioned way to ask for a file path — the OS door
carried notify, openExternal, revealPath and writeClipboard, so anything
needing a dialog had to reach around the SDK for window.hermesDesktop.

pickSavePath and pickOpenPath wrap the existing selectSavePath /
selectPaths IPC with the door's usual contract: resolve null when the
bridge is missing or the user cancels, never throw at the plugin.
2026-08-28 22:38:01 -05:00
hermes-seaeye[bot] ac6c8028e0 fmt(js): npm run fix on merge (#97517)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-28 23:47:58 +00:00
Brooklyn Nicholson 2a36a71578 fix(desktop): stop the project tree strobing while it re-probes an unreadable root
An unreadable root self-heals on a 3s timer, so the probe runs for as long as
the pane is open. Every forced reload cleared `rootError`, emptied `data` and
dropped `resolvedCwd` before reading, so each tick rendered "unreadable" →
blank → "unreadable" and flickered the header's project name with it. A local
ENOENT resolves well inside the 180ms skeleton delay, so the gap paints as a
bare blank frame rather than a loading state.

Re-reading the same root now probes underneath what is on screen; only a
different root, or the same path from a different backend, clears first.
2026-08-28 18:42:51 -05:00
Brooklyn Nicholson 0401e08884 fix(desktop): don't claim a stranger's cwd as the selected session's workspace
An unnamed `session.info` was treated as describing whatever the pane had
selected. The gateway stamps `stored_session_id: session_key or ""`, so every
not-yet-persisted session emits one, and `broadcast_session_info` / the
approvals loop re-emit for every live session at once. An unscoped event
applies exactly when no session is active, so with nothing selected each of
those repointed `$currentCwd` and claimed it for the null selection — the file
tree, coding rail and statusbar painted a folder no selected conversation
owned, until the next `releaseWorkspaceCwdOwner` dropped the claim and they
un-painted it.

Require the event to be bound to the pane's own runtime before an absent id
reads as the selection. The case the allowance exists for — a lazy session that
is the pane's runtime but is not persisted yet — still adopts and owns its cwd.
2026-08-28 18:42:51 -05:00
Brooklyn Nicholson e60983a697 fix(desktop): let the HUD drag onto another monitor
Composer drag added renderer CSS-pixel deltas onto a window AppKit
clamps to the current display, so the bar could not follow the cursor
onto a second monitor (and drifted on mixed-DPI Windows). Track the OS
cursor in main and lift that clamp.

Co-authored-by: Biotrioo <biotrioo@protonmail.com>
2026-08-28 15:33:19 -05:00
brooklyn! a73b14c438 Merge pull request #96726 from NousResearch/bb/bot-mode-design-system 2026-08-28 15:06:08 -05:00
Teknium 15eb5caf7a fix(install): Node 24.0–24.10 no longer passes the gates only to die at npm EBADENGINE
The locked dependency tree now carries @babel/* 8.x, which requires
node ^22.18.0 || >=24.11.0. Our engines.node arm said ^24.0.0 and the
installer gates (node_satisfies_build / Test-NodeVersionOk) accepted any
Node 24 — so a system Node 24.0–24.10 cleared every gate we own and then
failed 'npm install' with EBADENGINE under engine-strict=true.

- Raise the 24 arm to ^24.11.0 in root + desktop package.json and the
  package-lock.json mirrors
- Tighten node_satisfies_build (install.sh) and Test-NodeVersionOk
  (install.ps1) to 24.11+; update user-facing wording
- Add invariant tests: every engines.node arm floor must satisfy every
  locked dependency's engines.node, and the installer gates must encode
  the same floors as the manifest — so the next babel-style floor bump
  turns into a CI red instead of a user install outage
- docs: correct stale 'Node.js v22' provisioning claim
2026-08-28 12:20:40 -07:00
hermes-seaeye[bot] 9048530318 fmt(js): npm run fix on merge (#97358)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-28 18:38:38 +00:00
Brooklyn Nicholson a792d0794f fix(desktop): fill session-switch backfill in two frames instead of ten
FIRST_PAINT_BUDGET 20 + BACKFILL_STEP 60 prepended the rest of a 600-unit
page across ~10 visible commits. A 290-unit step keeps the interruptible
commits and removes the strobe.
2026-08-28 13:33:53 -05:00
Brooklyn Nicholson d229648511 fix(desktop): keep the session loader up while known history is empty
Brand-new drafts are empty on purpose. A routed session the list already
knows has messages must not drop the loader just because a runtime id is
bound — that is the blank frame during an unproven warm hold and a cold
switch.
2026-08-28 13:33:53 -05:00
Brooklyn Nicholson b6eb17d01c fix(desktop): hold unproven warm transcripts off the view
A compressed runtime cache is a legal tail, not display history. Publishing
it on session switch then replacing it with the persisted lineage is the
warm-path flicker. Gate that paint on persisted-display provenance and keep
the previous/empty view until REST authority lands.

Co-authored-by: xrbs00 <178640517+xrbs00@users.noreply.github.com>
2026-08-28 13:33:53 -05:00
Brooklyn Nicholson 21cc469eb1 Merge remote-tracking branch 'origin/main' into bb/bot-mode-design-system
# Conflicts:
#	apps/desktop/src/plugins/hermes-bots/plugin.js
#	apps/desktop/src/plugins/hermes-bots/tests/group-chat.test.mjs
#	apps/desktop/src/sdk/profile-routing.test.ts
2026-08-28 12:05:11 -05:00
Brooklyn Nicholson 7584260842 docs(bots): name the room budget as the seam the limits PRs hook
GROUP_CHAT_MAX_ROUNDS and its four siblings carry over at the values
plugin.js shipped, so no rebase inherits a behavior change on top of a
rewrite. Making them configurable is live contributor work — #92213 for
per-room limits, #96842 for config plus a token budget — and both want the
same single seam, so say so where the constants are instead of adding a
config hook this PR has no consumer for.
2026-08-28 12:02:23 -05:00
Brooklyn Nicholson ebb20910fa fix(bots): scope a freshly created bot chat to the bots workspace
Creating a bot opened its chat with the workspace fields omitted, because
they were spread only when the caller passed a staleness probe — and the
create path is the one caller that has none. The composer reads that scope to
stand its branch rail down in a companion chat, so a just-created bot showed
the git rail until the next click reopened the same row scoped. Live-verified
on Linux against a real backend, and carried over from the old plugin.js
rather than introduced by the rewrite.

The probe answers whether to navigate. What the session IS never depended on
it: a freshly minted Bot Chat is a bot's chat no matter who asked for it. With
the gate gone all four openers in this file are the same call, so they become
one.
2026-08-28 12:02:23 -05:00
Brooklyn Nicholson a7db531a2a i18n(desktop): move the Bot Mode kickoff and the residual owned strings into the bundle
The intro a new bot is born with was the first line of its forever-chat and
shipped in English, so a non-English user met their bot in a foreign language
and the bot's reply followed the prompt's language. It now resolves through
the plugin bundle in all four locales. Attribution — the other half of #91827
— still needs the lazy or silent birth that issue proposes, since
prompt.submit IS the user-turn API; the intro itself stays, per AGENTS.md.

The rest is the class the review named rather than only the lines it cited:
every user-visible string the group room and the bot-scoped cron pane own now
lives in the bundle. Where core already ships the vocabulary in every locale —
Remove, weekday names, Daily/Hourly — the plugin reuses it instead of shipping
a second, worse translation. The frequency and weekday option lists stop being
module consts frozen at import, which pinned whichever locale loaded first.

Prompts addressed to a model, cron syntax, and the 'You' author marker stay
hardcoded on purpose, each for a stated reason, recorded in the bundle header.
2026-08-28 12:02:16 -05:00
xxxigm 76da7ff087 fix(desktop): keep This-device Default on the local source (#97038)
* fix(desktop): keep This-device Default on the local source

Selecting Default while This device is active took the legacy profile
door, which is also the window-primary key. On a VPS-primary desktop
that activated the remote gateway, so Bots showed the wrong Current
Gateway.

* test(desktop): pin Default on This device away from the window primary
2026-08-28 07:21:50 -07:00
Teknium 9f2ab334c8 fix(desktop): route the MCP health sweep and command palette through getServers
Two sibling readers of raw config.mcp_servers duplicated their own (weaker)
shape guard: mcp-health.ts guarded the map but still passed null entries to
isUrlServer (crash on .url read), and the command palette re-implemented the
map check inline. Both now go through getServers(), the single choke point
that drops malformed entries, so a null entry can't crash the sweep and the
palette lists exactly the servers the MCP tab shows.

Also records the contributor email mapping for the salvaged commits.

Follow-up to the cherry-picked #94338.
2026-08-28 06:33:40 -07:00
Nacho Chiappero 49761cebad test(desktop): pin that field-level junk in an mcp_servers entry survives
`getServers` filters on whole-entry shape only — an entry with a junk field
(`{ command: 42, enabled: 'yes' }`) is still handed to the readers, which
coerce and tolerate. Pin that so a later tightening of `isEntry` can't
quietly start dropping entries that merely carry a bad field.

Raised in review on #94338.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 06:33:40 -07:00
Nacho Chiappero 13afe9e9e1 fix(desktop): drop malformed mcp_servers entries instead of crashing
An `mcp_servers` key left without a value parses as `null`, and the MCP tab
reads `enabled` straight off every entry (`serverEnabled`), so a single
dangling key threw during render and took the whole Capabilities workspace
with it — including the screen you would use to fix the config.

Filter non-object entries in `getServers`, the one choke point every reader
goes through, so a hand-edited config degrades to a missing row instead of a
dead pane. The backend already refuses to write such an entry
(`_replace_mcp_servers`), so this only has to survive a config edited
outside the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 06:33:40 -07:00
Teknium fe576ba48e fix(desktop): a 'This device' Capabilities pick reaches the local machine again under a remote registry primary
Since 1e9a12a71 (v0.20.6), a remote/cloud/ssh registry PRIMARY makes
globalRemoteActive() true, so the ambient v1 route — and with it every
unpinned Capabilities read — resolves to the remote gateway. The only
road back to this machine is an explicit connectionId:'local' pin, but
capabilityScoped() deliberately DROPPED that pin (pre-#91564, absent id
always meant the local pool), and profileScopeKey() collapsed
'local::<profile>' to the bare profile key.

Consequences on any desktop whose registry primary is remote:
- Capabilities -> MCP showed the REMOTE host's mcp_servers under every
  scope; locally configured (and connected) MCP servers vanished from
  the UI entirely.
- Picking 'default - This device' in the scope selector was a silent
  no-op: the collapsed cache key equaled the current scope key, so
  changeScope() early-returned and the selector snapped back.

Fix: capabilityScoped() forwards EVERY non-empty connection id, 'local'
included — Electron's apiRequestRegistryConnectionId/ensureRegistryBackend
already own 'local' pins (forced-local pooled child) and this is the
documented contract there. profileScopeKey() namespaces every explicit
pin ('local::<profile>') so a This-device pick and the ambient path never
share a cache row. profiles.ts profileOwnerScoped, which hand-patched
this exact hole for profile mutations, reduces to a named alias.

Live A/B (Electron + CDP, remote registry primary, local-only servers in
local config): v0.20.6 = local servers invisible, local pick no-op;
fixed = 'This device' lists local-server-alpha/beta, remote scope
unchanged.
2026-08-28 06:33:35 -07:00
fangliquan 4a7df1d070 fix(dashboard): align supported Node engine lines 2026-08-28 05:12:33 -07:00
Teknium d22e8e4022 fix(bots): restore the #97008 session contracts on the rebuilt modules
createCanonicalChat sends follow_profile_config and ensureGroupChatSession
sends room_plumbing + follow_profile_config again, as main's plugin.js did
before the rebuild. Without them, bot sessions created by this branch fell
back to the server's legacy title heuristics (exact 'Bot Chat' title;
hidden + 'Group: ' prefix) — the exact dependence the explicit contracts
were introduced to replace. Contract-shape tests pin both params.
2026-08-28 05:11:09 -07:00
hermes-seaeye[bot] d5d80963fa fmt(js): npm run fix on merge (#97119)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-28 12:05:29 +00:00
Finn763 108783b44d fix(desktop): drop legacy tiles missing connectionId and guard partial routes
Enough1122 review of #94426:

- Legacy Bot tiles persisted before ownerRoute.connectionId existed carry no
  connection id, so the local-delete branch (`ownerConnection === 'local'`)
  never matched them and they resurrected the deleted profile on relaunch.
  Treat the empty connection id as the local connection.
- A route without profile now throws instead of silently falling into the
  local-delete branch and dropping nothing remotely owned.
- Pin the 'local' connection spelling with tests covering the legacy
  (no id), canonical ('local'), and divergent ('Local') spellings.
- Comment the live-atom filter arms by bucket and unify the duplicated
  #94235 call-site comments in sdk/index.ts and delete-profile-dialog.tsx.
2026-08-28 04:58:53 -07:00
Finn763 e3c56f7dd9 fix(desktop): scope tile drops to the deleting connection and normalize route identity
Address review findings on #94426:
- Non-route (local profile) deletes now require the tile's owner
  connectionId to be 'local' — a same-named bot on another connection
  keeps its tile and live conversation.
- Route identity (profile/targetProfile) goes through normalizeProfileKey
  like the non-route name, so whitespace-padded identities match on both
  paths; profile case stays exact, consistently on both paths.
- Test lint: drop the unused dropTilesForProfile value import (tests call
  it via the fresh module namespace) and replace the forbidden typeof
  import() type annotation with a type-only namespace import. New tests:
  same-name-other-connection survival, whitespace normalization, and
  case-exact identity. eslint: 0 errors; typecheck clean.
2026-08-28 04:58:53 -07:00
Finn763 ae6d3880ae fix(desktop): drop persisted tiles of deleted profiles so bots cannot resurrect
A Bot Mode bot cloned via 'Clone from profile' resurrects after
deletion: the desktop keeps the bot's chat tile in local storage
(bot-meta cleanup removes the roster entry, but the persisted tile
survives). On relaunch the tile restores, re-dials the deleted
profile's backend, and ensure_hermes_home() re-creates the profile
directory the delete just removed — the empty skeleton with no
config.yaml reported in #94235.

dropTilesForProfile() removes a deleted profile's session-tile
bucket and every Bot Mode tile whose ownerRoute points at it
(exact connection + backend target when a source-scoped route is
given) from memory and local storage, with discard (no Cmd+Shift+T
undo) semantics. Wired into both delete doors: the core
DeleteProfileDialog and the SDK host.deleteProfile path the Bots
plugin uses.

Regression tests cover local and source-scoped routes; verified red
with the cleanup disabled.
2026-08-28 04:58:53 -07:00
David Tyler 84e17db0bd fix(bot-mode): canonical bot DMs always follow the profile's current config
Bot-Mode canonical chats (the ONE forever DM per bot) and room plumbing
sessions are plugin-owned scratch conversations. They are now created with
an explicit follow_profile_config contract, persisted in the session row's
model_config, so session.resume rebuilds from the member profile's CURRENT
config instead of restoring the stored model/provider pin from an old row.

That stale pin is what left bot DMs stuck on a dead provider (e.g. 'out of
Nous credits' after the profile was switched to ollama-cloud) while the
same bot worked fine in rooms — the mirror image of the room-plumbing bug
(#89497 class). Normal 1:1 user chats keep the stored-runtime restore:
opening an older chat must show the model it actually used.

- tui_gateway/methods_session.py: accept follow_profile_config on session.create
- tui_gateway/server.py: persist the marker in the row; skip stored-runtime
  overrides on resume when present
- apps/desktop/src/plugins/hermes-bots/plugin.js: send the contract from
  createCanonicalChat and ensureGroupChatSession
- tests: backend override + row-persist coverage; desktop source-contract
  coverage for both session kinds
2026-08-28 02:59:17 -07:00
David Tyler 316e51ae72 fix(bot-mode): room plumbing sessions always follow the profile's current config
Room member sessions in Bot Mode are per-member scratch conversations
inside a group chat. session.resume restored their stored model/provider
pin from the row's model_config, so a room bot stayed stuck on whatever
provider was pinned when the row was first written — even after the
profile was switched. Every room message then failed on the stale
provider (e.g. 'out of Nous credits' after switching a profile from
Nous to ollama-cloud) while the same bot worked fine in DMs.

Add an explicit room_plumbing contract:
- session.create accepts room_plumbing: true, persisted in model_config
- _stored_session_runtime_overrides() returns {} for marked rows, so a
  room session always rebuilds from the member profile's CURRENT config
- hidden + 'Group:' title shape is kept as a legacy fallback for rows
  created by older desktop builds that never sent the marker; hidden
  non-room chats keep the stored-runtime restore
- Desktop Bot Mode sends room_plumbing: true when creating the hidden
  per-member room sessions

Fixes #89497
2026-08-28 02:59:17 -07:00
hermes-seaeye[bot] d3ccc09dc2 fmt(js): npm run fix on merge (#96951)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-28 07:13:47 +00:00
Mike DeMott dd5481aa40 refactor: centralize fast compression controls 2026-08-28 12:38:49 +05:30
Mike DeMott 213ae08e7a perf(compression): add guarded fast summary lane 2026-08-28 12:38:49 +05:30
brooklyn! 387c73b19a i18n(desktop): translate Bot Mode into ja, zh, and zh-hant (#96878)
The English bundle on the parent left the other locales to fall through.
Ship them the same way kanban does, using core's nouns, so a language
switch no longer paints the Bots rail in English.
2026-08-27 23:41:34 -05:00
Brooklyn Nicholson 71afc8155e docs(desktop): record why the Bots-home new-chat refusal is gone
Deleting the Bots home deleted the dead end that "Select a Bot or group
first." was apologizing for: with nothing selected the center is now an
ordinary session and + works there. The refusal still stands for the cases
that remain — a group room, and a selected row too orphaned to route.
2026-08-27 23:23:47 -05:00
Brooklyn Nicholson 832ec82f75 fix(desktop): mount every chat.empty contributor, not just the first
Ownership of an empty transcript is per session and is not known until each
plugin has loaded its own data — which is why the slot mounts contributors
rather than resolving a claim up front. Taking only contributions[0] then let
a plugin that DECLINES a session suppress the one that owns it: silently,
permanently, and only for the users whose installed plugins happen to
register in that order.

Every registration is mounted and answers for itself. Declining is free; two
claiming one session render both, a visible conflict instead of a silent drop.
2026-08-27 23:23:47 -05:00
Brooklyn Nicholson 0f4d4d4d8b fix(desktop): subscribe the bot-chat flag to every store it reads
The composer subscribed to $sessionStates while isBotChatSession reads the
scope set — and, to resolve a live id to the stored one it is filed under,
$sessionTiles too. It stayed roughly right only because $sessionStates
republishes per streaming token, so the stale answer was overwritten within a
frame. A quiet session had no such luck.

Adds useStoresSelector beside useStoreSelector for the general case: a scalar
whose inputs span several stores, recomputed when any notifies, still
re-rendering only when the scalar changes.
2026-08-27 23:23:43 -05:00
Brooklyn Nicholson 19ff8e66f3 test(desktop): port the bot-meta v1 -> v2 migration suite
The migration, its commit marker, and the rollback around it are all still
live in data.ts, but the suite covering them went out with the vm/regex
harness and was never ported — leaving only the happy-path commit ordering
asserted incidentally by bot-delete.

Nine cases: the sole-local topology gate (and the two refusals — multi-source,
and a batch where any one profile has no local route), the marker rule that
makes v2 authoritative (committed, markerless, crash-window), and the three
failed-commit paths (roll back to the last committed generation, clear both
keys when there is none, survive a failed cleanup).

migratedLocalRoutes is module-private, so where the old suite asserted the
map's size this one asserts what the map is for: no route adopted, nothing
written. Mutation-checked — dropping the marker rule fails three of them.
2026-08-27 23:23:43 -05:00
Brooklyn Nicholson 8ab34a76d0 fix(desktop): staleness-probe the adopt-on-conflict canonical open
The adopt branch runs a full extra round-trip past the point every sibling
open in createCanonicalChat is probed at — registry miss, mint, title
conflict, re-consult — so it is the likeliest of them to land after the user
has clicked another bot, and it was the only one that navigated unguarded.

Adopting the winner still settles identity, which is always correct to
return; only the workspace steal is gated.
2026-08-27 23:23:38 -05:00
Brooklyn Nicholson 008bc186ca fix(desktop): a bot row click returns to its open tabs instead of re-opening a closed Bot Chat
Ports 7c91079 onto the split modules — it landed on main in plugin.js, which
this branch deletes.

Every roster click resolved the bot's canonical Bot Chat by name and opened
it as a tab. Nothing records a tab close (this plugin keeps no closed set;
core's tile bucket only forgets), so a Bot Chat the user had closed came back
beside every newer thread on every bot switch. A click is now "go to this
bot": when the bot's workspace already holds tabs, the one the user last had
active is fronted and no chat is resolved or opened. The forever-chat opens
only when the bot has nothing open, or on the explicit ask — a new "Open Bot
Chat" row-menu item.

The claim such a click records carries only the fronted tab, so the reclaim
listener skips it and cannot resurrect the closed chat. focusExistingBotTab
is feature-detected, so an older shell keeps opening the canonical chat.

Dropped from the port: the Bots home "Open chat" button, a surface this
branch removes. The .mjs test is replaced by a real one — its two
source-reading cases become a render of the menu item in bot-row.test.tsx
and, for the reclaim guard, the claim-shape invariant the guard reads.
Stubbing usePluginI18n there also means the row's localized labels render as
text in tests instead of empty.
2026-08-27 22:51:39 -05:00
Brooklyn Nicholson 6559448306 Merge origin/main into bb/bot-mode-design-system
Keeps plugin.js and its new .mjs test deleted. main's closed-chat fix
(7c91079) landed in both; it is a real behaviour change, so the commit that
follows ports it onto the split modules rather than dropping it with the
files. Its core half — focusWorkspaceOwnerSessionTile and the
host.focusOpenWorkspaceSession verb — merged cleanly and is used as-is.
2026-08-27 22:51:30 -05:00
Zeus-Deus 7c910793bf fix(desktop): a bot row click returns to its open tabs instead of re-opening a closed Bot Chat
In Bot Mode every roster click resolved the bot's canonical "Bot Chat" by
name and opened it as a tab. Nothing records a tab close (the plugin keeps no
closed set; core's tile bucket only forgets), so a Bot Chat the user had
closed came back beside every newer thread on every bot switch — close it,
start a new thread, visit another bot, come back: two tabs again, forever.

A row click is now "go to this bot": when the bot's workspace already holds
tabs, the one the user last had active is fronted and no chat is resolved or
opened. The canonical chat is opened only when the bot has nothing open, or
on the explicit asks — a new "Open Bot Chat" row-menu item and the Bots home
"Open chat" button (`openRosterBot(bot, { canonical: true })`).

- session-states: `focusWorkspaceOwnerSessionTile(ownerKey)` fronts the
  owner's remembered-active tile (else its most recent) and reports it.
- sdk: `host.focusOpenWorkspaceSession(ownerKey)` exposes it to plugins;
  feature-detected in the plugin so older shells keep the canonical open.
- hermes-bots: `focusExistingBotTab` short-circuits `openRosterBot`; the
  claim it records carries only the fronted tab, and the session.reclaimed
  re-resume now skips such claims so it cannot resurrect the closed chat.

Tests: vitest for the core helper, a node test for the click path (open tabs
win, nothing open → canonical, explicit canonical, older shell, throwing
host), and an e2e that seeds two bots with real "Bot Chat" rows, closes one,
starts a thread, switches bots and back, and asserts the Bot Chat stays
closed until asked for explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 20:30:49 -07:00
brooklyn! 1acd5bb02b feat(desktop): make tips and guided tours both opt-out (#96835)
Tours had no switch at all, and the tips switch only covered the app's
own rotation — so "I don't want these" was answerable for half of one
feature and none of the other. Both are now a row in Settings →
Appearance, on by default, and off means off for Hermes as well: an
agent tip is dropped at the bridge and a tour request is refused in
words, so the agent hears that the walkthrough didn't run instead of
narrating a spotlight nobody can see.

Renames $tipRotationEnabled to $tipsEnabled to match what it now
governs. The storage key keeps the old name on purpose — renaming it
would read as unset for anyone who had already turned tips off, and
silently turning them back on is the one outcome worth avoiding.

The switches stop at the app's edge for now: the tools are still in the
model's schema and the calls simply don't land. Withdrawing them
entirely needs the setting to reach the backend, which is the next PR.
2026-08-27 22:17:00 -05:00