Renames the openai-codex provider's display label across the CLI
(hermes model picker, provider labels), the dashboard OAuth accounts
catalog, and the Desktop onboarding + settings provider pickers.
Slug, aliases, and auth flows are unchanged.
Hovering a sidebar session row (one-line or card) now shows the complete
title in a styled tooltip — but only when the title actually overflows
its label, so fully visible titles never grow a redundant tip.
New OverflowTip primitive in ui/tooltip.tsx: a controlled Tip that
measures scrollWidth vs clientWidth on pointerenter and arms a 600ms
deliberate-hover delay only when the content is truncated. Community
request via @fhreire on X.
Adds a 'Scheduled jobs' row to the sidebar's top nav (below Artifacts,
watch codicon, wired to the existing nav.cron keybind action) so the
cron overlay opens from the same rail as New session / Capabilities /
Messaging / Artifacts.
Inside the cron overlay, the list rail now also shows the Automation
Blueprint recipes below the jobs (same search box filters both).
Clicking a recipe opens the create dialog pre-seeded to that
blueprint's typed-slot form via a new optional blueprintKey on the
create EditorState. Catalog fetch reuses the ['cron-blueprints'] query
key, so no extra request.
i18n: sidebar.nav.cron added to en/zh/zh-hant/ja (ar already had it).
ensureGatewayProfile narrows the Sessions sidebar to the activated
profile as a side effect, so every cross-profile open from a plugin
surface silently locked the user into that profile's session list. A
plugin-driven open is a navigation, not a scope choice: openSession now
restores the unified all-profiles view after a cross-profile activation
(keepAllProfilesScope, default true; pass false for the old narrowing).
Same-profile opens write no scope at all.
Final-diff simplify/review pass findings on #83785:
- Consent gate (confirm_truncate -> 4029) now checked BEFORE target
resolution, restoring the pre-PR precedence: an unconfirmed submit
carrying truncation params refuses without paying the durable-transcript
read or heal-stamping live history dicts, and an unconfirmed out-of-range
ordinal returns 4029 (not 4018). Malformed params still refuse first
with 4004. Regression test added (spy DB asserts zero reads pre-consent;
mutation-checked against the previous commit).
- _coerce_truncate_ordinal generalized to _coerce_truncate_int(param_name):
the row_id branch was inlining the exact bool-guard + int() -> 4004
pattern the helper had just extracted.
- Deleted the dead user_indices re-read after _resolve_truncate_row_id
(heal mutates dicts in place; the filter output is identical) and the
duplicate range check that had deadened the pre-existing guard.
- Desktop: exported isVisibleUserMessage from use-prompt-actions/utils and
used it in visibleUserOrdinal / visibleUserIndexAtOrdinal /
rebindSurvivorRowIds — one predicate for the ordinal parity all three
depend on instead of three verbatim copies.
- Docs: programmatic-integration.md documents survivor_user_row_ids.
check:lint failed on the two sort-named-imports errors (survivorRowIdsFrom
before type SurvivorUserRowIds) introduced by the rebind commit; also adds
the blank line eslint wanted in the new test helpers.
Review follow-up (StanleyStetson + egilewski on #83785/#83202): a successful
rewind's replace_messages(archive_dropped=True) re-inserts the surviving
prefix as NEW SQLite rows. Gateway memory picks up the fresh _row_id stamps
via lastrowid, but the Desktop's surviving bubbles kept their pre-rewind
ChatMessage.rowId — so a second rewind/edit/regenerate of an older surviving
turn sent a stale truncate_before_row_id and was (correctly) refused with
4018 until a transcript reload. Fail-closed stays untouched, per both
reviews; the fix is rebinding, not ordinal fallback.
Server: prompt.submit now returns survivor_user_row_ids (fresh post-rewrite
ids of surviving visible user turns, in visible-user-ordinal order) on both
the inline and compute-host paths whenever a durable truncation committed.
Desktop: runRewindSubmit surfaces the field; restore/edit/reload on both the
primary chat and session tiles rebind surviving user bubbles positionally
(same visible-user filter the ordinal math uses) and clear any rowId they
cannot rebind — a cleared id degrades to the ordinal path instead of a 4018.
Absent field (older gateway) leaves state untouched.
Tests: consecutive-rewind regression on a real SessionDB (stale id 4018s,
returned id succeeds; mutation-checked) + vitest for survivorRowIdsFrom /
rebindSurvivorRowIds (rebind, null-clear, past-end clear, hidden skip,
identity preservation).
Address rewinds/edits via SQLite messages.id (truncate_before_row_id)
instead of shifting user ordinals. Resolve against in-memory stamps,
then durable session history when live turns drop _row_id; refuse
unknown durable targets with 4018 (no ordinal fallback) and 4030 on
ordinal/row_id mismatch. Stamp _row_id on insert, load row ids on
resume paths, send rowId from Desktop, filter renderer-synthetic ids,
and stop silently resending failed targeted edits without truncation.
Add production-shaped SessionDB tests for resolve and fail-closed paths.
Fixes#82959
The Project-grouping flag was one global bool while the grouping beneath it
was already stored per scope (workspace vs all-profiles). Picking Project
inside a workspace therefore dragged the all-profiles view into the project
tree and vice versa — "I have to re-set grouping every time I switch."
The flag now lives per scope like its sibling grouping atoms (the flat key
keeps its historical name so existing choices survive), setSidebarGrouping
writes to the scope it just switched INTO when Profile flips the view, and
reset clears both scopes.
scrollbar-overlay opts out of the themed thin scrollbar; on Windows there
are no native overlay scrollbars, so Chromium painted the classic
always-visible gutter instead — a permanent scrollbar next to the recents
list. The themed fade bar reserves its 4px on every platform but stays
invisible until hover, and the wrapper no longer stacks a second scroller,
which is what the overlay class was originally working around.
The card prop was gated off whenever Project grouping was active, so the
Inbox style toggle silently did nothing there. It is a render variant, not
a grouping: project lanes and overview previews now render the same card
the flat list does.
Also mirrors the section's real virtualization inputs (projectOverview /
entered-project content, not the persistent agentProjectTree cache) when
deciding the wrapper's scroll classes, and stops gating SCROLL_Y on that
parallel guess — the section is the single authority on which scroller
lives, so the recents pane can no longer end up with no scroller at all
(the "no sessions under Updated grouping after toggling settings" blank).
Third draft provider: recurring phrasing in the draft ("every morning",
"daily", "each week") offers a Schedule-this pill. Click prefixes the
draft with an explicit scheduling instruction and the agent creates the
job via its cronjob tool on send — the pill never schedules anything
itself. Proper-noun guard keeps titles like "the Daily Prophet" quiet;
hyphen-as-word-char keeps "weekly-report.pdf" quiet.
Two new sources on the suggestion bus, one per provider shape:
- skill (draft): the draft names an enabled skill (whole-word,
4+ chars), so offer to lead the message with its /command. Invoke
prefixes the draft via the new 'prefix' insert mode and stands down
once the draft starts with a slash; skill_manage invalidates the
cached index alongside the slash-completion cache.
- repair (event): an mcp__ tool call failing with auth/connection-
shaped output offers a one-click reconnect for that server, fed from
the gateway tool.complete handler. Reconnect runs the shared OAuth
flow with server-side cancel and reloads live tools before claiming
success; a later successful call to the same server withdraws the
offer on its own.
The bus now keeps a session-scoped declined ledger: a pill the user
watched appear and let die three times stops re-offering for the rest
of the session. Acting on a pill clears its count, so a suggestion
that was taken can come back for the next trigger. In-memory on
purpose — a fresh session is a fresh chance.
Desktop plugins reach the backend exclusively through the generic ws
JSON-RPC door (host.request), but profile enumeration/creation only
existed on the dashboard REST router, which plugins cannot reach — so
anything 'one chat per agent profile'-shaped (bot rosters, profile
pickers, team panes) was impossible to build as a plugin.
- tui_gateway/methods_profiles.py: new @method handlers
* profiles.list — profiles + optional last_session preview per profile
(mirrors session.list's kanban/tool deny-list; best-effort per-profile
state.db probe degrades to null instead of failing the call)
* profiles.create — ws twin of POST /api/profiles (clone_from/clone_all/
no_skills/description), plus optional SOUL.md content and a best-effort
model+provider pin; mirrors the CLI flow (seed skills, safe alias)
Both run on the RPC pool, not the WS reader thread (list_profiles walks
skill trees; create copies bundles).
- SDK: host.openSession(id, { profile, intent }) — open a stored session
the way core surfaces do, soft-swapping to the owning profile's backend
first (ensureGatewayProfile), and host.newChat(profile) — fresh draft in
a named profile (same door as the sidebar's per-profile '+').
- Docs: desktop-plugin-sdk.md gains both surfaces.
First consumer: a Grok Bot-style 'Bots' roster plugin (one persistent
chat per agent profile with a New Agent dialog) built on exactly these
four doors.
The pill strip from the inline-MCP work is worth more than one source, so
the MCP-specific store splits into two layers with the same UX contract
(session-scoped, capped, self-limiting, one-click with narrated
idle→working→done):
- store/composer-suggestions.ts — the bus. Draft providers register into
the existing debounced sampler; event providers push/withdraw directly.
Offerings merge (event before draft), dedupe by provider-namespaced key,
and keep reference identity on no-ops.
- store/suggestion-providers/mcp.ts — the founding provider, behavior
unchanged: directory keyword/host matching, configured-server exclusion,
one-click connect with OAuth cancel + config rollback.
- composer/suggestion-pills.tsx — the generic strip; phases and cancel
live here, action/rollback/toasts stay with the provider's invoke.
No new pills yet — this is the seam for them.
The card reads the labelled choices off the gateway request rather than
the raw tool args -- the backend applies the label there, and the card
only mounts once the request exists, so the args are a hydration-race
fallback. RECOMMENDED_LABEL and bareChoice live in the clarify store so
the component and the choice-length guard share one definition; without
the guard a long option could be dropped for length the label added.
A renderer-local directory of official hosted MCP remotes (URL-only,
vendor-documented endpoints — deliberately not the reviewed install
catalog) powers keyword and pasted-link suggestions: typing jira or
pasting a *.atlassian.net URL floats an 'Add Atlassian' pill in the
composer's micro-action strip. Matching is whole-word/phrase (unicode
boundaries) plus strict host-suffix on links, host hits outrank
keywords, capped at two, debounced 600ms, and excludes servers already
in mcp_servers. Pills are session-scoped like the micro-action badges
and self-limiting rather than dismissible — they exist only while a
trigger is in the draft. A click drafts the setup request; the agent's
setup_mcp card carries the consent. Brand glyphs extracted from the
mcp-tab into lib/mcp-brands (shared, monochrome marks follow the theme
so GitHub/Notion/Vercel survive dark mode).
The card follows the approval bar's consent vocabulary (primary-tinted
action + ghost decline, ⌘⏎/Esc with clarify's focus-stand-down rule) on
clarify's widget shell. Install prefers the reviewed catalog entry (env
prompts inline, background installs polled to completion) and falls back
to the desktop suggestion directory via the validated add-server POST +
OAuth; success reloads live MCP tools before unblocking the agent so it
resumes with the tools it was just promised. Esc stays live mid-flight
as cancel — the abandoned flow aborts at its next poll and a post-write
cancel rolls the config entry back. Typing while the card is pending
declines it and sends normally (skipClarifyRequest's pattern), and the
request/tool.start rows merge on the server arg so reconnects can't
double-render the card.
The one-line session row gets the exact treatment the inbox card's title
already has: hovering a truncated title glides the clipped tail into view —
one direction at constant speed, a short hold at each end, then a snap back.
Same armMarquee/disarmMarquee handlers, same CSS, so overflow is measured on
pointerenter, short titles never move, hover state lives in DOM attributes
(no re-render of the memoized row), and reduced motion disables it.
Typing while the turn was parked on a blocking prompt routed the text
through steer (session.redirect), which sat undelivered behind the
blocked tool batch — nothing rendered, and stopping the turn to force it
through resolved the prompt to empty and ended the turn as the literal
"Operation interrupted." row, eating the message.
Clarify already had a carve-out (typing skips the question and steers)
because a real message IS an answer to a clarify. Approval/sudo/secret
have no such answer path, so the busy submit now queues the words as the
next turn instead: the prompt stays answerable, the queue drains on
settle, and the busy button advertises queue rather than steer while one
is pending. Slash commands still execute inline, and another session's
prompt never affects this one.
Right-clicking anywhere the app owns no menu of its own — the titlebar
gutter, an empty pane body, the sidebar background — did nothing at all.
Electron's native handler bails on non-editable, non-selected content by
design, so those surfaces had no menu to fall back to.
Wrap the shell in a fallback context menu carrying the verbs that belong
to the window rather than to a row: new session, new window, command
palette, toggle the status bar, settings, update Hermes. Every row reuses
the store action and the copy its Cmd+K twin already uses, so the two
can't drift.
A guard on an inner element keeps it a fallback: a right-click that lands
inside a surface with its own context menu, on an editable, or on a live
selection stops propagating before Radix's trigger sees it, leaving that
surface's menu — or Electron's native edit menu — in charge.
A pasted GitHub PR comment deep link (#discussion_r… / #issuecomment-…)
now lands as a typed review attachment instead of a bare url chip. The
card attaches optimistically and resolves through gh in the background —
author, file:line anchor, body, and the diff hunk — expanding at send
into an anchored fenced block, so "address this" carries exactly what
"this" is. When gh can't answer (offline, unauthenticated, foreign repo,
remote gateway) the card downgrades to the plain url ref and nothing is
lost.
Queued turns could only wait for the settle or interrupt the turn to jump
the line. Text-only queue entries now carry a steer action while the agent
is busy: the entry rides the existing mid-turn redirect (no interrupt, no
drain lock), is consumed only when the gateway accepts it, and lifts a
park so the rest of the queue keeps flowing. Slash commands and entries
with attachments keep their existing semantics.
An inbox card whose session is working through a todo list shows its "X/Y"
fraction at the far right of the footer line, updating live as items
complete. The count projects the runtime-keyed todo map to stored session
ids through the same lineage-alias fallback the working/attention
projections use, emits pre-rendered strings so unchanged rows never repaint,
and skips cancelled items on both sides of the fraction.
Hovering a truncated inbox-card title glides the clipped tail into view —
one direction at constant speed, a short hold at each end, then a snap back
to the start. Overflow is measured on pointerenter and the animation arms
only when the text actually clips, so short titles never wiggle. State lives
in DOM attributes and CSS variables, so a hover never re-renders the
memoized row, and the blanket reduced-motion override already disables it.
A new "Inbox style" toggle in the sidebar filter menu renders the flat
recents list as cards: a workspace header line (project when it resolves,
else the cwd leaf, else Home) with the age at its right edge, the title
grouped with a one-line last-message preview, and a model + size footer.
The preview line ships on by default and has its own Show-menu toggle,
offered only while Inbox style is active — the one-line row has nowhere
to put it.
A render variant, deliberately not a grouping — it composes with whichever
grouping is active and only the flat recents list opts in; pinned, project,
and messaging surfaces keep the one-line row. Spacing hangs off a single
--card-gap variable; the title/preview pair is one grouped cell with its own
tighter internal gap. The age and kebab sit in flow inside the header line
rather than a full-height side column, so title, preview, and footer span
the card's entire width.
The card's project label reads through a selector that resolves the label
string, so tree polls with fresh atom identity repaint only rows whose label
actually changed.
The virtualized recents list sat inside a wrapper that was itself a themed
scroller, so two 4px classic-scrollbar gutters stacked and every row ended
8px short of the sidebar edge the pinned list reaches. Drop the wrapper's
scroller when the virtual list owns scrolling, use the overlay scrollbar on
the virtual scroller (zero gutter, native fade), and neutralize both axes of
the wrapper overflow — `overflow-y-visible` next to `overflow-x-hidden`
computes to `auto` and still reserved a gutter.
The idle dot variant had no background of its own, so a settled session
outside any project rendered a literally invisible dot — the row read as
missing its status indicator next to its neighbours. Fall back to the
faintest filled grey; a project color still wins when there is one.
Todo, subagent, background, and preview titles in the composer status
stack were capped at an arbitrary max-w-[18rem], ellipsizing long items
way before the row ran out of space. The spans already live in the
shared StatusRow's min-w-0 flex-1 content slot, so plain truncate gives
correct overflow at the actual row edge — drop the cap.
The branch's PR chip carried its own pull-request icon next to the row's
branch glyph, so the strip opened with two git marks in a row. The chip now
takes showIcon, and the coding row renders it glyph-less and ahead of the
branch name — the leading branch icon covers both, and the row reads
icon → #number → branch. Sidebar rows keep the full chip.
The background review's summary fell through SystemMessage's generic branch: centered, 60% width, muted grey — while the memory tool row it reports on wears the gold-to-purple legendary chrome. Type the event with a review: marker at the gateway handler (same convention as steer: / slash:) and give it the brain glyph, gradient label and purple detail, left-aligned in the reading column.
* fix(desktop): keep config/structured code blocks fenced instead of unwrapping to prose
The desktop markdown preprocessor has a "prose fence" heuristic that
strips the fence off blocks it thinks are wrapped prose. Its
`proseLines >= 3 && codeSignals === 0` rule fires on ANY 3+ line
plaintext block with no JS/SQL tokens -- which is exactly what an SSH
config, a .env dump, or any INI/key-value listing looks like. The result
was that a fenced ```-block of SSH config rendered as a flat paragraph
instead of a code block.
Add isLikelyStructuredText() and use it as a veto in both
isLikelyProseFence() and isLikelyProseCodeBlock(): a block is treated as
structured (and kept fenced) when it has indented continuation lines, or
when it has no sentence-ending punctuation and a majority of lines are
`Key value` / `Key: value` directives. Real wrapped prose has
sentence-shaped lines and no per-line indentation, so it still unwraps as
before. The bullet-prose case in isLikelyProseCodeBlock is checked first
so markdown bullet lists remain prose.
Tests: markdown-code.test.ts gains SSH-config / flat-config / .env
regression cases for both functions, plus direct isLikelyStructuredText
coverage, and re-asserts that genuine paragraph prose still unwraps.
* fix(desktop): tighten config-line detection to not match punctuation-less prose
The first CONFIG_LINE_RE matched any 'word word' line, so a wrapped prose
fragment with no sentence punctuation (e.g. 'the quick brown fox jumps')
was misread as a config directive and its fence kept. Split into an
explicit-separator form (Key: value / Key = value) plus a short 2-3 token
'Key value' directive form; a real sentence line has more tokens, so
punctuation-less prose is no longer treated as config.
The first-run provider picker showed Fireworks AI alongside Nous Portal
before the user opened the 'Other providers' disclosure. Only Nous Portal
should be visible up front; Fireworks now lives inside the expanded list
but keeps its #1 position there (Nous -> Fireworks ordering preserved).
The custom-endpoint REST handlers ran bare load_config/save_config, so
every add/activate/delete landed in the process-level default profile
regardless of which profile the desktop settings UI was targeting. A
provider added under a non-default profile silently went to default:
visible only in default-bound sessions, absent everywhere else, and
un-addable to another profile without hand-editing its config.yaml.
Scope all four handlers (list/upsert/activate/delete) to the requested
profile via _config_profile_scope, matching /api/config, and spread the
active profile into the four hermes.ts wrappers alongside their existing
validateCustomEndpoint sibling.