Commit Graph

4143 Commits

Author SHA1 Message Date
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
hermes-seaeye[bot] 4cbbe11ffc fmt(js): npm run fix on merge (#96837)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-28 03:15:46 +00:00
brooklyn! 4bd2793367 feat(desktop): default the in-app tip rotation on (#96831)
Made opt-in when it fired a tip 45 seconds into a launch and another
every six minutes, which is a cadence that owes you a choice. The pacing
has since become a settling delay of five to ten minutes per launch and
a six-hour cooldown persisted across them — roughly a tip a day, weeks
to walk the catalog. At that weight the switch has nothing left to
protect anyone from, and a discovery feature nobody meets is one nobody
has. The switch stays for whoever still wants it off.
2026-08-28 03:10:46 +00:00
hermes-seaeye[bot] 7e7fc6445b fmt(js): npm run fix on merge (#96833)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-28 03:10:27 +00:00
Brooklyn Nicholson 595ee92289 fix(commands): put desktop slash metadata on the CommandDef
Inference is now any args_hint without subcommands → text. Mixed is the
only remaining hint-token path. desktop= and the few argument_mode
overrides live on the registry entry; the side tables are gone. Catalog
aliases get their own dict copy. Composer tests seed the catalog so
/goal stays mixed without an overlay row.
2026-08-27 22:05:40 -05:00
Brooklyn Nicholson 60f58249e5 fix(desktop): resolve slash commands from the catalog
The overlay table is only actions, pickers, and RPCs. Completions group
by backend kind, and argument mode comes from the warmed catalog so
/review and plugin commands stay typeable.
2026-08-27 22:05:40 -05:00
Brooklyn Nicholson e870d3fde3 fix(desktop): don't let slash Space steal a leftover highlight
Space and Enter should complete a prefix (/com → /compress) but keep an
exactly typed name, so leftover /compress cannot replace /review.
2026-08-27 22:05:40 -05:00
Brooklyn Nicholson bbcf5691a6 Merge origin/main into bb/bot-mode-design-system
main's Bots-home flash fix (6f8be61) landed in plugin.js, which this branch
deletes. Both files stay deleted: the guard it adds (botOpenInFlight gating
botsHomeMayOpen) protects a surface this branch removes, and the two sites
where it renamed the generation bump to cancelBotOpen already bump here via
bumpBotOpenGeneration in shared.ts.
2026-08-27 21:52:40 -05:00
Brooklyn Nicholson 198a69421c feat(desktop): pace the tip rotation in hours, not minutes
A first tip 45 seconds in and one every six minutes after walks the
whole catalog in an hour, which is the cadence of a notification rather
than a nicety. Games get this right by being almost absent: a tip while
you settle in, then nothing for the rest of the day.

Two clocks now have to agree. A per-launch settling delay of five to ten
minutes means opening the app is never met with a bubble, and a six-hour
cooldown persisted across launches means quitting and reopening isn't a
way to farm them — the old schedule lived in the effect and re-armed on
every mount. Flipping the switch on skips the settling delay and offers
immediately, since that clock guards a launch you came into with a
purpose, not a deliberate opt-in.

An agent tip starts the cooldown too: whoever just pointed at something,
the user has had their one interruption for a while.
2026-08-27 21:50:18 -05:00
Brooklyn Nicholson 0357982696 feat(desktop): make the idle tip rotation opt-in, ungate the tool
The two halves of tips were behind one switch, which meant the app
volunteering commentary at idle shipped on by default. Split them along
the line that matters: the rotation talks unprompted, so it now waits to
be asked for, while an agent tip stays ungated like the tour it mirrors
— Hermes raises one mid-conversation, in answer to something the user
said.

Drops the tool's config gate along with the config key it read. The
renderer mirrored that key with config.set, which has no branch for it
and answered "unknown config key" into a swallowed catch, so the opt-out
never reached the backend in the first place.
2026-08-27 21:50:18 -05:00
Brooklyn Nicholson 911c6c50d3 feat(tools): let Hermes point at one thing with the tip tool
The quiet sibling of `tour`, in the same `desktop_ui` toolset and reading the
same `tour(action='targets')` discovery call: one bubble with an arrow, for a
sentence that would be clearer with a finger on the thing it's about. Dimming
the whole app to say "the model name is a button" is the wrong weight.

Fire-and-forget rather than a round-trip, because a tip is not a question and
blocking the turn on one would stall the reply it belongs to. The renderer
enforces the user's opt-out itself, so a stale config read can never put a
bubble on a screen that asked for none.
2026-08-27 21:50:18 -05:00