One config key everywhere (#41531): the same display.timestamps that stamps
[HH:MM] on classic-CLI labels now gates the desktop transcript's timeline
timestamps and renders dim [HH:MM] labels on TUI user/assistant rows.
- desktop: $displayTimestamps store fed from config.yaml via
use-hermes-config; TimelineTimestamp renders nothing while the key is off
(the default). Hover tooltips with the exact time stay ungated (#70450).
- TUI: tui_gateway forwards each persisted row's timestamp in the display
projection; toTranscriptMessages threads it as Msg.createdAt; live rows
are stamped at append (the #82840 rule); MessageLine shows a dim [HH:MM]
above user/assistant rows when display.timestamps is on.
- No new config keys, no HERMES_* env vars; display-only, prompt-cache safe.
- Add submitting state + IME composition guard to prevent double-submit
- submitEdit() sets latch, then clears after 200ms timeout
- handleKeyDown() guards on composing so IME Enter doesn't submit
- Shift+Enter inserts newline without submitting
- Test validates Enter calls onEdit, latch clears for second session
Fixes#70771
Signed-off-by: xrwang8 <xrwang8@gmail.com>
The inline edit composer caps height at max-h-48 but had no overflow
rule, so long prompts were clipped with no way to reach the tail.
Add overflow-y-auto to match the main composer editor.
Collapsed tool zones (terminal/logs) kept a down chevron after minimize,
so the restore affordance looked like another collapse. Point the icon in
the action direction — down when expanded, up when collapsed — matching
master-detail collapsible detail headers. Same fix for floating panes.
(cherry picked from commit 3392aeb9dba0ed9e9cabfb96b6010e46fa453ca1)
Widen the composer-side IME fixes to the inline edit composer: the same
missed-compositionend wedge and post-compositionend keyCode 229 Enter
apply to its handleKeyDown path.
Check event.nativeEvent.isComposing in Enter-to-submit branches so CJK (Japanese, Chinese, Korean) and other compositional input methods commit the candidate instead of firing the send handler.
Applied to all five sites in the desktop renderer that currently handle Enter with no composition guard:
- chat composer main submit and trigger popover (apps/desktop/src/app/chat/composer/index.tsx)
- message edit composer submit and trigger popover (apps/desktop/src/components/assistant-ui/thread.tsx)
- onboarding API key and auth code inputs (apps/desktop/src/components/desktop-onboarding-overlay.tsx)
- session rename input (apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx)
Fixes#37483
Deleting a session in the desktop app fired instantly on click — the CLI
path (hermes sessions delete) asks y/N by default, so one misclick (Archive
and Delete sit right next to each other) permanently destroyed a conversation
with no dialog and no undo (#61470).
Route every delete entry point (sidebar rows, tab menus, the chat header,
context menus — all share useSessionActions) through a shared
DeleteSessionDialog built on ConfirmDialog. ConfirmDialog gains an
onOpenAutoFocus prop so dialogs with no input keep focus off the close
button (a11y).
Tests: menu delete now asks; cancel keeps the session; Enter confirms;
Escape cancels; delete item disabled without onDelete; the same guard
applies via SessionContextMenu.
pet.info accepts knownRevision; when it matches the active sheet's
revision the multi-MB spritesheetBase64 is elided and
spritesheetUnchanged=true is returned. The desktop floating pet passes
the revision it already holds and keeps its cached bytes, so backstop
refreshes no longer resend ~3.2MB frames over the WS (write-loop stalls,
disconnect storms). Legacy callers omitting knownRevision get the full
payload unchanged.
Event-capable backends no longer polled pet.info, so a cold-start
fail-open enabled:false left the mascot hidden until Settings re-seeded
the store. Keep a slow backstop and short startup retries.
Petdex spritesheets are 192x208px illustration frames, not pixel art.
The desktop canvas drew them with imageSmoothingEnabled=false
(nearest-neighbour), so zoomed pets looked blocky. Enable bicubic
smoothing so scaled frames stay clean at any zoom.
High-DPI backing-store sizing is addressed separately in #83276.
DPR-sized canvas backing store separated from CSS footprint, tracking
zoom/display changes live. Rendering-fix subset of PR #75307; the
overlay-placement feature portion is out of scope here.
Covers the devicePixelRatio half of #83216.
The pane-resize sash's 9px grab band was centered on the split boundary,
so ~4.5px reached into the leading pane and sat exactly on top of the
session list's 4px scrollbar — the pointer always hit the sash (cursor
flipped to col-resize) and the scrollbar thumb was unclickable/undraggable.
Make the grab band asymmetric: 1px into the leading pane, 7px into the
trailing one. The scrollbar regains its full hit area while the sash stays
an easy 8px target; the hairline and hover strip are repositioned onto the
actual boundary.
Fixes#79157
Regression coverage for #68634. The indicator family mounts only on the
thread's any-role tail (ba756333): a running bubble that is not the tail
stays silent, even when only a user or system row trails it, and the
optimistic-placeholder flow renders exactly one status row, the
placeholder's own.
Mutation-checked: relaxing the mount gate to a last-assistant walk fails
the two silence cases, and removing the gate fails all five.
Clarify prompts share the same emitted-while-detached failure class the
pending-approval replay fixed: `clarify.request` rides `_block()`'s pending
registry, so a client whose transport was down when the event fired never
sees the question and the agent thread stays parked until timeout.
Widen the resume snapshot the same way:
- tui_gateway/server.py: `_live_session_payload` now carries
`pending_clarify` — a read-only snapshot of the clarify prompt still
blocking the session, scoped to the owning runtime sid. The registry stays
authoritative; the embedded request_id resolves via clarify.respond.
- Desktop resume paths (`use-session-actions`) restore the parked clarify
into the clarify store (multi_select preserved) and flag needsInput,
mirroring restorePendingApproval on both the activate and resume paths.
- pending_approval replay now also forwards the queue-injected request_id so
the restored prompt responds with exact-request correlation.
- Tests: server-side replay + scoping test; harmonized the #82087 replay
test with the request_id `_ApprovalEntry` now injects.
Correlate approval requests, reject stale responses, replay pending approvals after reconnect or session resume, and preserve fail-closed timeout behavior.
Two sibling sites still treated only 'completed'/'failed' as terminal:
- delegate-model.ts settled result rows as 'completed' for ANY status other
than 'failed', so a delegate result row with status 'timeout' or 'error'
(the statuses tools/delegate_tool.py actually emits on child timeout or
crash) rendered behind a green check. Settled rows now map ok/completed
to completed and everything else to failed.
- subagents.ts asStatus accepted a literal 'queued' payload status even on
a subagent.complete event, leaving the row active forever. The fail-closed
branch now runs before the queued fallback, so completion events always
settle.
Persist backend ownership for reliable cleanup, park inactive panes, and
evict unreferenced transcripts so Desktop stays responsive over long sessions.
💘 Generated with Crush
Assisted-by: Crush:gpt-5.6
Renaming a session from its row menu opened the rename dialog, but the
menu's close restored focus to its trigger — the session row's own
<button> — instead of leaving it in the dialog input. Focus sat on the
row, so Space toggled the row (selecting/deselecting the session) and the
arrow keys moved the list rather than the text caret; you couldn't type a
space or move within the name.
Thread onCloseAutoFocus through the shared ActionsMenu / ActionsContextMenu
primitives and suppress that one focus-restore when the rename item is the
action that closed the menu, so the dialog input keeps focus. Every other
action leaves the restore untouched. Mirrors the project menu's existing
appearance-popover guard.
resolveAgentAvatar cached null permanently (window lifetime), so a bot
whose avatar reached the asset store moments after its first notice
rendered — freshly created bots, art backfills in flight — kept the 🤖
glyph until an app restart even though profiles.get_asset had the pfp
(user report: brand-new bots' notices never picked up their faces).
Hits stay cached for the window; misses re-probe after 30s. Same
dedupe/inflight behavior otherwise.
The sending bot's chat showed inter-agent deliveries as raw terminal
tool rows (the hermes -p … chat … command + output transcript) —
plumbing, not conversation. When a terminal call matches the delivery
convention (-p <agent> chat … -q "Message from …", the shape from
#85855), it now renders as compact centered notices instead:
'Messaging <agent>…' while running, 'Messaged <agent>' on completion,
and — when the quiet run returns the recipient's reply — a 'Message
from <agent>' notice with the text behind a 'show message' expander.
Avatar resolution reuses the #85855 helper (now exported); glyph
fallback everywhere it can't resolve. Failed commands keep the real
terminal row (debuggable). Ordinary terminal calls untouched.
Sender + receiver now speak one visual language: the exchange is a
pair of timeline events on both ends (Grok-bots parity), with #85884
collapsing the receiving side's reply.
agent-delivery tests 6/6; thread suite 20 files / 117 tests green.
The recipient's reply to an inter-agent delivery rendered as a full
assistant message, so the receiving bot's chat read like a normal
human conversation. The exchange is an EVENT in that bot's timeline,
not conversation content: when the immediately preceding user message
is an inter-agent delivery (AGENT_MESSAGE_RE, shipped in #85855), the
reply now renders as a compact centered 'Replied to <sender>' notice
with the full text behind a 'show reply' expander — mirroring the
delivery notice above it. Never collapses while streaming (progress
stays visible); ordinary assistant messages untouched.
Thread suite 19 files / 111 tests green.
* feat(desktop): render agent-to-agent messages as attributed cards, not user bubbles
Bot-to-bot deliveries arrive on the user role (alternation requires
it) but are not the human speaking — they rendered as if the user
typed them. Detect the delivery prefix ('Message from 🤖 <sender>: …',
emoji-less, and the legacy bracket form) and render an attributed
inter-agent card: left-aligned, robot + sender header, 'agent message'
label, body through the same minimal markdown pipeline. Anchored regex
cannot fire mid-prose. Same pattern as ProcessNotificationNote.
Presentational only; content/roles/caching untouched.
* reshape: inter-agent card -> Grok-style compact timeline notice
Per maintainer screenshot: the delivery renders as a subtle centered
'🤖 Message from <sender>' notice (ProcessNotificationNote's shape),
with the delivered text behind a 'show message' expander instead of a
full-width card. The recipient's reply remains a normal assistant
message below it.
* feat: sender avatar on the inter-agent notice
The delivery prefix may carry the sender's profile handle —
'Message from 🤖 <Display Name> (@<handle>): …'. The notice resolves
it through profiles.list (has_avatar) + profiles.get_asset and renders
the sender's actual avatar in place of the 🤖 glyph, with module-level
memoization (one resolution per sender per window) and inflight
de-dup. Glyph fallback covers handle-less prefixes, older gateways
without profiles.*, avatarless profiles, and failures. 'hermes'
resolves to the primary profile by convention. Tests 6/6.
* lint: sort the $gateway import (perfectionist/sort-imports)
Picking a colon-less completion row (agent profile mentions from
complete.path, e.g. '@mr-tester') fell through serialize()'s typed-
reference branch: classify() marks colon-less entries type 'simple'
with insertId = text, so the serializer minted '@simple:`@mr-tester`'
— which rendered as a weird chip in the composer AND broke downstream
@mention routing (the backtick-quoted form no longer parses as a bare
mention). Colon-less rawText now inserts verbatim, matching what @diff
and @staged already did via the empty-insertId path.
Test: mention rows serialize to plain @name and commit to the editor
without an @simple: wrapper (directive-label 5/5).
Closing a pane contributed by a plugin used to disable the entire plugin,
unloading every one of its contributions. For a plugin that owns several
independent panes (e.g. Bot Mode's Cronjobs pane alongside its Bots roster
and composer middleware), closing one pane silently killed the rest.
Now: closing one pane of a multi-pane plugin dismisses only that pane; the
plugin stays enabled and its other panes/commands/middleware keep working.
Reset layout restores dismissed contributed panes. A single-pane plugin
keeps the existing symmetric behavior (Close disables the plugin, with
Settings -> Plugins as the recovery path).
Adds regression coverage for both cases.
MessageRenderBoundary re-throws anything that is not the transient
assistant-ui lookup race, by design, so a RangeError raised inside
Streamdown's render unwinds all the way to the workspace boundary and
replaces the entire app with "workspace failed to render". The message
is replayed from the session on every reload, so Retry lands on the same
content and fails the same way — the app is bricked, not glitching.
Wrap the markdown surface itself, so one bad message degrades to the
existing HugeTextFallback (readable, already used for oversized text)
while the rest of the transcript stays alive. The boundary sits on
MarkdownTextSurface rather than any single caller because the crash is a
property of the content, not of which part carries it: the same payload
arrives as an assistant answer, as reasoning, or in tool output, and all
of them render through here.
Tests drive the real component with both known overflow shapes and fail
with the reported RangeError when either half of the fix is reverted.
The depth clamp handles the raw-HTML cause; deeply nested block
structure recurses in mdast-to-hast where no HTML guard can reach it,
which is why the boundary is not redundant.
Co-authored-by: Gille <helix4u@users.noreply.github.com>
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.
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.