Commit Graph

28243 Commits

Author SHA1 Message Date
Zheqing Zeng 37bccf343e fix(macos): scrub gate env, refuse EACCES, normalize marker paths
Review fixes from kokhlo's live-hardware review:

- The boot-gate probe now runs with PYTHONHOME / PYTHONPATH /
  PYTHONSTARTUP / __PYVENV_LAUNCHER__ scrubbed: an inherited
  PYTHONHOME=<venv> boots a staged copy that would otherwise die with
  "No module named 'encodings'", papering over the exact prefix
  failure the gate exists to catch.
- OSError is split by errno: ENOENT/ENOEXEC (fixtures, foreign-arch
  images) still skip; EACCES after our own chmod now refuses the
  install instead of silently accepting a broken copy.
- Marker writes and both marker comparisons go through os.path.realpath,
  so the managed-runtime layout (cpython-3.11-macos-* symlinked to
  cpython-3.11.15-macos-*) no longer reports stale on a fresh install.

Tests: +3 (env-scrub spy, EACCES refusal, symlinked-home state).
100 passed in the module + doctor neighborhoods.
2026-08-28 09:05:19 +05:30
Zheqing Zeng 1c92b95a12 test(macos): cover every boot-gate refusal branch directly
The gate is the never-brick guarantee, so each direction gets its own
test: nonzero exit (dyld/encodings crash), build-time prefix leak,
timeout, and the deliberate OSError skip (a binary that cannot execute
here means the symlinked venv was equally dead — installing cannot make
things worse).
2026-08-28 09:05:19 +05:30
Zheqing Zeng aa72df4b42 fix(macos): re-land dylib-complete TCC interpreter anchor
The first landing (#95131/#95478, reverted in #95563) copied the
uv-store interpreter into venv/bin/python so TCC grants would stick
to a stable path. On real Macs that copy bricked every hermes command
two ways: dynamically-linked builds died in dyld because
@executable_path/../lib/libpython resolved into venv/lib/ (#95425),
and alias symlinks to the copy made CPython getpath lose the venv
prefix (#95541, ModuleNotFoundError: encodings).

Re-land:

- Keep the signed real-file copy of bin/python (identifier-pinned
  via _macos_sign_managed_python).
- Materialize python3 / python3.N as real-file copies, never
  symlinks. Copies boot on every build we could reproduce and keep
  the TCC identity.
- Hardlink store libpython* into venv/lib/ when present (copy across
  devices). Existing LC_RPATH already points there.
- Pre-install boot gate: launch the staged copy, demand encodings
  plus the venv prefix, abort and leave the live venv untouched
  on failure.

Doctor reports/installs the new anchor (the revert-era heal is
removed). Update refreshes it after a successful code swap. Tests
cover layout, idempotence, predecessor-symlink repair, libpython
hardlink, boot-gate refusal, and a macos_only real-interpreter E2E.

Closes #95596.
2026-08-28 09:05:19 +05:30
kshitijk4poor f4ae8958b5 chore: map zengzheqing noreply email for attribution (PR #96647) 2026-08-28 09:05:19 +05:30
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 b61408e95e feat(commands): attach desktop slash metadata to the registry
New commands and plugins declare argument_mode and desktop availability
on CommandDef / register_command. commands.catalog ships that map so
desktop does not need a second command list.
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
Victor Kyriazakos 5cc47c994b fix(cron): resolve api_server host for the manual-run forward (bind parity)
The forward dialed a hardcoded 127.0.0.1. The api_server adapter binds
extra.host -> API_SERVER_HOST -> 127.0.0.1, so mirror that chain when
dialing. Wildcard binds (0.0.0.0/::) listen on loopback, so keep dialing
loopback for those; bracket bare IPv6 literals.
2026-08-27 19:52:17 -07:00
Victor Kyriazakos 8e8112687b fix(cron): don't reference nonexistent 'hermes cron trigger' in relay-fronted errors
The CLI has no 'trigger' subcommand ('trigger' is only an alias of the
cronjob TOOL's run action). Point operators at the real remediation:
start the gateway; its ticker owns relay-fronted delivery and fires the
job on schedule.
2026-08-27 19:52:17 -07:00
Victor Kyriazakos ad0e522362 fix(cron): forward manual run to the gateway for relay-fronted delivery (NS-773)
A manual 'hermes cron run' on a relay-fronted target has no live relay adapter
and no standalone sender, so it now forwards to the running gateway's
POST /api/jobs/{id}/run (marks due for the gateway ticker, which delivers via
the live relay adapter). Gateway unreachable -> the accurate 'start the gateway
or use cron trigger' error. Native topologies are untouched.
2026-08-27 19:52:17 -07:00
Victor Kyriazakos 2d1d65de46 fix(cron): accurate error for relay-fronted delivery with no live gateway (NS-773)
A manual in-process 'hermes cron run' has no live relay adapter, but the
delivery loop fell through to the native standalone path and hit the native
configured/enabled gate, misdiagnosing relay-fronted platforms ('not
configured/enabled') whose credential lives in the connector. Now, when
resolve_delivery_transport finds no transport AND the platform is in
relay_fronted_platforms(), emit the accurate 'start the gateway or use cron
trigger' remediation and skip the native gate. Native topologies unchanged.
2026-08-27 19:52:17 -07: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
Brooklyn Nicholson baaf304992 feat(desktop): in-app tips
An ambient rotation that points at parts of the app you may not have found yet
— one accent bubble, an arrow, and an outline around the subject. No scrim and
no spotlight: a tip is a pointer beside your work, not a modal in front of it,
so it takes no focus, owns no Esc, and blocks nothing.

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

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

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

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

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

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

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

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

LruCache is the same move for a different leak: katex-memo.ts had a private
one with a hardcoded ceiling, and the bot rail had two Maps that never
evicted. One shared class, exported, katex's twin deleted.
2026-08-27 21:46:40 -05:00
Gille 6f8be61516 fix(desktop): prevent Bots home flash during chat switch 2026-08-27 19:44:14 -07:00
Teknium 420e156bf3 refactor(agent): single owner for Responses route predicates
Follow-up to the #96217 salvage: the codex/xai/github route checks were
re-implemented inline at four sites (codex_responses_adapter helpers,
chat_completion_helpers kwargs build, _is_openai_codex_backend, the
run_agent silent-reject hint). Consolidate them into
classify_responses_route() / ResponsesRouteFlags in
codex_responses_adapter and migrate every site — backend-identity
predicate class (#22548/#70893/#59561/#72468).

Host checks use exact-host-or-subdomain semantics, never substring
matching.
2026-08-27 18:56:21 -07:00
686f6c61 e6b4f3750b fix(agent): count native Responses preflight against pruned wire
Automatic preflight used the full durable transcript even when the
Codex Responses request would prune around a native compaction
checkpoint. That false-triggered a 600s local summary against history
the main request never sent. Estimate the converted, checkpoint-pruned
payload when native compaction is eligible, and keep the generic
estimate as the conservative fallback.
2026-08-27 18:56:21 -07:00
Brooklyn Nicholson 0fececa733 chore(desktop): lint the bot-mode modules clean
The plugin shipped as bundled JavaScript, so eslint never saw it. Now that it
is .tsx under src/, the whole ruleset applies: sorted JSX props, curly braces,
statement padding, unused imports.

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

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

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

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

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

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

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

Pull the splash's lettering out of `intro.tsx` into a `Wordmark`
primitive so a second caller cannot fork it, and move the typeface,
weight and tracking into `.wordmark` beside the `.fit-text` rules they
travel with — as utilities they were one class-merge or one missed scan
away from silently rendering the wordmark in body text. `Wordmark` takes
its width, because fit-text sizes to fill and a short name set at the
splash's full width comes out enormous.
2026-08-27 19:08:28 -05:00
Brooklyn Nicholson 01a3e9a44c fix(cli): give a profile created without a clone a usable model block
Creating a bot from the desktop dialog builds the profile tree but no
config.yaml, so the profile resolves no provider and its first turn dies
with "No LLM provider configured" — created, but unable to run. Every bot
made that way was dead on arrival.

Seed the active profile's model block at creation. It is a copy, not a
link: profiles stay independent islands and editing either afterwards never
touches the other. "Fresh" means fresh skills and SOUL, not unreachable.
2026-08-27 19:08:28 -05:00
Brooklyn Nicholson 67854b50b0 fix(desktop): bot chats share core's unread and drop the branch rail
Bot Mode kept its own unread map, so a bot's dot and the session dot could
disagree about the same chat. Route it through core's store instead, keyed
by the canonical chat's stored id. A hidden session has no listed row to
read a profile from, so the writers take an explicit profile hint rather
than assuming the live gateway's — otherwise the marker persists into a
bucket that never held it.

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

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

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

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

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

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

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

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

No behavior change. 16,193 lines become 16,050 of real TSX.
2026-08-27 19:08:27 -05:00
Ben Barclay 6dcebea7fc Merge pull request #96341 from fangliquanflq/fix/computer-use-notarised-cua-paths
fix(computer-use): launch notarised CUA Driver from standard macOS installs
2026-08-28 09:04:46 +10:00
Gille 4956ff0cb9 fix(cli): keep journey labels readable 2026-08-27 17:45:43 -05:00
Mariano Nicolini c38d62aefe feat(nous): tell a governed org its model choice is restricted
The gateway omits a policy-blocked model from `/v1/models` rather than
marking it, so after the preceding commits a restricted model is simply
absent from the pickers. That reads as "Hermes does not support this"
instead of "your organization disallows it".

Show one line when the org is governed, in the two flows where a user
picks a model. It enumerates nothing: model policy is an allowlist, so an
org admitting a handful of models blocks the whole rest of the catalog,
and graying hundreds of rows would be a worse UI than omitting them.

Driven by the `policy_present` claim, which is tri-state — the line shows
only when it is explicitly true, because an absent claim means an older
mint rather than an unrestricted org. The claim is stamped at mint time,
so the line can lag a policy change by up to the access token's lifetime.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:17:56 -03:00