* feat(desktop): add useKeybindHint hook and TipKeybindLabel
Add a shared hook that reads the current keybind combo for an action
id from the $bindings store (rebindable) or KEYBIND_READONLY (fixed),
returning a formatted string or null when unbound.
Add TipKeybindLabel — a convenience component that auto-reads both
its label (from i18n) and keybind combo from the action registry.
Pass only actionId for the common case; pass text to override when
the tooltip is context-dependent.
* fix(desktop): replace native title= on buttons with themed Tip
Migrate all <button>/<Button> elements using the native HTML title=
attribute to the instant, themed <Tip> component. Native tooltips are
unstyled, delayed (~500ms OS default), and visually inconsistent with
the app's instant themed tooltips.
Also adds <Tip> wrappers to icon-only buttons that were missing
tooltips entirely (dialog close, search clear, overlay close,
master-detail pane controls, keybind panel rebind/reset buttons).
Adds an enforcement test (no-native-title.test.ts) that scans all
.tsx files for <button>/<Button> with title= and fails if any are
found. Updates DESIGN.md with the icon-only button tooltip rule and
keybind hint guidance.
* feat(desktop): wire keybind hints into button tooltips
Add actionId to TitlebarTool, StatusbarItem, and SidebarNavItem so
their tooltips show the current keybind combo via TipKeybindLabel.
Fix the hardcoded NEW_SESSION_KBD in the sidebar to read from
$bindings so it stays live on rebind.
Wired surfaces:
- Titlebar: sidebar toggle, flip panes, keybinds, settings
- Statusbar: terminal toggle (view.showTerminal)
- Status stack: open agents button (nav.agents)
- Sidebar nav: new session, skills, messaging, artifacts
* feat(desktop): move keybind panel to settings tab with search filter
Move the keyboard shortcuts panel from a Radix Dialog into a proper
Settings tab (/settings?tab=keybinds). The ⌘/ shortcut and titlebar
keyboard button now navigate to this settings tab instead of toggling
a dialog. Adds a search filter to filter shortcuts by label.
- New: src/app/settings/keybind-settings.tsx (extracted from keybind-panel.tsx)
- Delete: src/app/shell/keybind-panel.tsx (dialog wrapper removed)
- Remove: $keybindPanelOpen atom and toggle/open/close functions
- Add: IconKeyboard to lib/icons.ts
- i18n: keybinds.search + settings.nav.keybinds (en, zh, zh-hant, ja)
* refactor(desktop): unify worktree dialog into shared WorktreeDialog
Extract the worktree creation dialog from StartWorkButton (sidebar) into a
shared WorktreeDialog component. Both the sidebar's StartWorkButton and the
composer's CodingStatusRow now use the same dialog, eliminating the duplicated
UI.
The shared dialog keeps the sidebar version's full feature set:
- BaseBranchPicker (filterable base branch combobox)
- Convert mode (check out an existing branch into a worktree)
- Sanitized branch name input
The coding row passes repoPath (from cwd) and onOpenWorktree (which carries
the composer draft to the new session) so the unified dialog works everywhere
there's a repo, not just inside an entered project.
Switching between chat sessions in the desktop app froze for up to ~1–2s on
large transcripts. Profiling the switch path surfaced three main-thread
blockers, fixed here minimally and without changing behavior.
1. JSON.stringify deep-compare (worst case). chatMessagesEquivalent compared
message parts with JSON.stringify(a) === JSON.stringify(b) on every switch.
On image-/large-blob-bearing transcripts this serialized every part twice
and cost well over a second. Replaced with a structural compare that never
stringifies: array-level identity fast-path, per-part reference fast-path,
then type-aware field comparison. The compare's only consumer asks "did the
transcript change, should I setMessages?", so it is deliberately
conservative — a false-negative just causes one extra idempotent
setMessages, while a false-positive (the unsafe direction) is avoided.
2. Scroll-settle loop. thread-list ran a requestAnimationFrame settle loop up
to 90 frames (or 5 stable frames) on every sessionKey change, each frame
forcing a synchronous layout read + write — racing the markdown paint for
up to ~1.5s. A normal synchronous switch stabilizes within a couple
frames, so the ceiling is now 2 stable frames / 15 max.
3. Synchronous first paint of up to 300 parts. On switch, thread-list reset
the render budget to the full RENDER_BUDGET=300, so up to 300 parts went
through markdown + shiki syntax-highlighting synchronously on the switch
commit. It now paints a small FIRST_PAINT_BUDGET=60 first, then bumps to
the full 300 in a requestAnimationFrame after the first commit.
Salvaged from PR #49807 by professorpalmer — re-applied to the restructured
file layout (use-session-actions/utils.ts, thread/list.tsx) and tests merged
into the existing utils.test.ts.
Co-authored-by: Cary Palmer <professorpalmer@users.noreply.github.com>
The sidebar working dot didn't update for background sessions until the
user opened them. Two coupled causes:
1. The gateway's session.info event payload omitted stored_session_id,
so the desktop app had no way to map a background session's runtime
id to its stored id. Without the stored id, setSessionWorking(null,
...) was a no-op — the $workingSessionIds atom never updated.
2. The running→busy transition in the session.info handler was gated on
`apply` (active session only). The gate correctly scopes view-only
side effects (setCurrentModel, setCurrentCwd, etc.) to the focused
chat, but the per-session busy state drives the sidebar indicator and
must reach every session. updateSessionState only mutates the
per-runtime cache entry, and syncSessionStateToView already guards
the view publish to the active session, so ungating is safe.
Fix: add stored_session_id to _session_info() in tui_gateway/server.py,
add the field to GatewayEventPayload, pass it to updateSessionState in
the session.info handler, and ungate the running→busy transition.
react-router v7's HashRouter wraps every route state update in
React.startTransition() by default. In React 19's concurrent renderer,
transitions are non-urgent — React can yield mid-render and come back
later. When the app is under load (streaming token deltas, gateway
events, store updates from active sessions), those higher-priority
updates keep interrupting the transition, starving the route change commit.
This matches every symptom of the session-switch lag:
- Main thread is free (animations run, clicks work) — startTransition
defers, it does not block
- navigate() does not take effect for seconds — the transition keeps
getting interrupted by higher-priority updates
- Worse under load — more concurrent re-renders = more interruptions
- The whole UI (sidebar + main pane) does not update — the entire route
change is one transition, so nothing commits until it finishes
Pass useTransitions={false} to HashRouter so route state updates are
synchronous at default React priority instead of deferred transition
priority. navigate() now commits and paints immediately.
See: react-router v7 chunk-BIP66BKV.js HashRouter implementation.
Reassert the persisted webContents zoom after BrowserWindow moved, covering Windows monitor transitions where Chromium recalculates display scaling and drops the user-selected zoom.
Rebased onto current main, where the electron test harness migrated from
node:test to vitest (test:desktop:platforms = `vitest run --project electron`,
auto-discovering electron/*.test.ts). Swap the node:test import for vitest and
drop the .ts-extension import hack; the obsolete package.json node --test list
edit is dropped in the cherry-pick resolution since vitest auto-discovers.
Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
selectModel in use-model-controls captured activeSessionId as a closure
prop, but the actions bag in wiring.tsx mutates in place to keep a stable
identity for memoized surfaces. The modelMenuContent useMemo captures
selectModel once when the gateway first opens (before any session is
active) and never re-evaluates, so clicking a model in an existing thread
goes through a stale closure with activeSessionId=null — the pick is
treated as UI-only, config.set is never sent, and the next session.info
event clobbers the optimistic update back to the session's real model.
Drop the activeSessionId prop entirely. All three callbacks now read
$activeSessionId.get() live from the store, matching the pattern
refreshCurrentModel already followed. This is the correct contract for
the actions bag's in-place mutation: callbacks read live state from the
store, not from captured props.
Guard staged node-pty ASAR path rewrites so already-unpacked paths are
not rewritten twice. Normalize spawn-helper to mode 0755 in both the
prebuild and locally compiled build/Release staging paths.
Add behavioral coverage for both unpacked path forms and both helper
layouts.
Co-authored-by: zhouwei <zwcf5200@163.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
The CI change classifier routes package.json / package-lock.json /
.ts/.tsx changes to the frontend lane, not the Python lane. Four
Python tests asserted about these JS-side artifacts, so a PR touching
only those files would skip the Python suite — the regression goes
green on the PR and red on main (where the classifier fails open).
Ported all four to vitest so they run in the correct CI lane:
tests/test_package_json_lazy_deps.py
→ apps/desktop/electron/package-json-lazy-deps.test.ts
(camofox is lazy, agent-browser is eager, lockfile clean)
tests/test_desktop_electron_pin.py
→ apps/desktop/electron/desktop-electron-pin.test.ts
(electron dep is exact, matches build.electronVersion, lockfile agrees)
tests/test_assistant_ui_tap_compat.py
→ apps/desktop/electron/assistant-ui-tap-compat.test.ts
(@assistant-ui cluster shares one tap version + semver helper)
tests/test_dashboard_sidecar_close_on_disconnect.py
→ web/src/lib/chat-sidebar-session-params.test.ts
(sidecar session.create opts into close_on_disconnect + profile)
The ChatSidebar test was regex-matching .tsx source text (the
source-reading anti-pattern). Extracted sidecarSessionCreateParams()
from the component's effect into an exported pure function so the
test calls real code instead of pattern-matching a string.
Verified: 8 electron tests + 76 web tests pass; both typecheck clean.
The hoisted shared eslint config catches pre-existing no-useless-escape
and no-control-regex errors in apps/desktop that were only fixed in web/
in the previous commit.
- no-useless-escape: remove unnecessary \) escapes inside character classes
- no-control-regex: add eslint-disable-next-line comments for intentional
\x1b terminal escape byte matching (same pattern as web/src/lib/pty-mobile-input.ts)
apps/shared had lint:fix but not the 'fix' alias that other workspaces
have. The js-tests check job runs 'npm run fix' as a second step, so
this workspace was failing with 'Missing script: fix'.
* feat(desktop): add background-task indicator to sidebar session rows
A session with a live terminal(background=true) process but no active LLM
turn now shows a pulsing gray dot in the sidebar — distinct from the accent
pulse of an active turn and the steady amber/green of needs-input/unread.
New $backgroundRunningSessionIds computed atom joins
$backgroundStatusBySession (runtime-keyed) with $sessionStates
(runtime→stored) to produce stored session ids the sidebar row can match
against — same pattern as $attentionSessionIds and $unreadFinishedSessionIds.
Also refactors SidebarRowDot from a 3-branch if-else chain into a
table-driven priority array of DotState entries. Each state declares its
active flag, className, ariaLabel, and title in one place — adding a new
indicator state is now one array entry instead of another conditional.
* fix(desktop): keep pulse-dot before:bg-* static so Tailwind emits it
The PING() helper interpolated the color into `before:bg-${color}`, but
Tailwind v4 only generates utilities it finds as complete static strings — a
template-composed `before:bg-(--ui-accent)` / `before:bg-muted-foreground/50`
is never emitted, so both pulse rings (working + new background) lost their
halo color. Make PING a static scaffold and write the before:bg color inline
per variant.
---------
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
SessionInfo already carries git_branch from the backend, but neither the
ctrl-k command palette nor the sidebar search function included it in
their matching fields. Typing a branch name matched nothing.
- session-search.ts: add session.git_branch to sessionMatchesSearch
- command-palette/index.tsx: thread git_branch through SessionEntry and
into the keywords array for both sessions and archived sessions groups
- session-search.test.ts: cover full, partial, and "main" branch matches
* fix(desktop): un-minimize zone when revealing a collapsed tool panel
`revealTreePane()` fronted the pane's tab but never un-minimized its zone
when the pane lived in a shared zone (terminal + logs, or a tool panel
stacked with the workspace). The shared-zone branch of `setPaneCollapsed`
routes opens through `revealTreePane` instead of `toggleTreeGroupMinimized`,
so the zone stayed collapsed and the terminal appeared to "close but not
open" on Ctrl+` — and tab clicks needed a double-click because the first
click's `restoreTreePane` → opener → listener → `revealTreePane` chain
didn't un-minimize either.
The fix: `revealTreePane` now restores a minimized zone before fronting
the pane, chaining the un-minimize + activate into a single `commit` so
the second op sees the updated tree.
* fix(desktop): side collapse in column-root layouts + restore after zone-menu minimize
Two follow-up fixes for the panel layout:
1. Ctrl+B / Ctrl+J sidebar toggles didn't work in the Terminal deck (and
Quad) layout. The side-collapse system (treeSideOfPane, paneRootSide,
layoutHasRootSide, and the renderer's semanticSides) only operated on the
ROOT split when it was a row — but Terminal deck's root is a column, so the
side columns (sessions/files) nested inside a child row were invisible to
the collapse system. Extracted a rootRow() helper that finds the row
containing main (the root itself for row layouts, or the row child of a
column root), and a rootRow prop propagated through TreeNode → TreeSplit so
semanticSides fires on the right split regardless of root orientation.
2. Clicking the "logs" tab in a minimized terminal/logs zone needed a
double-click when the zone was minimized via the zone menu (right-click →
minimize), not the toggle. restoreTreePane called the opener
($logsOpen.set(true)) and returned early — but when the store was already
true, nanostores don't fire the listener, so setPaneCollapsed/revealTreePane
never ran and the zone stayed minimized. Now restoreTreePane also
un-minimizes directly + calls revealTreePane when the zone is still
minimized after the opener runs.
Reasoning ("Thinking") rendered through MarkdownTextContent with a smooth
typewriter reveal. TextMessagePartProvider mints a fresh part object on every
text change, and useSmooth resets its reveal to empty whenever the part
identity changes — so each reasoning delta restarted the animation from the
first character (h / hell / hello wo ...). Token-streaming reasoners
(R1/Qwen/GLM/Claude thinking) fire a delta per token and flash hard; GPT-5's
coarse reasoning summary updates too rarely to notice, which is why it looked
fine.
Drop smooth on the reasoning path so it plain-appends, exactly like the
assistant answer already does. Removes the now-dead smooth plumbing too.
Generalize the clarify opt-out into an UNBOUNDABLE_TOOLS set so a run
containing image_generate also stays a plain, fully-visible stack instead
of collapsing into the bounded window, where the max-height + gradient mask
clipped the image the same way it clipped clarify forms.
When an agent turn finishes while the user is viewing a different
session, the sidebar now shows a steady green dot on that session —
distinct from the blue pulsing dot of a running turn and the gray dot
of an idle one. Opening the session clears the indicator.
The unread state is ephemeral renderer-side state, matching the
existing $workingSessionIds and $attentionSessionIds pattern: no
persistence, no backend involvement, wiped on gateway-mode switch.
Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: dschnurbusch <dschnurbusch@users.noreply.github.com>
Co-authored-by: Flow Digital Inc. <flow-digital-ny@users.noreply.github.com>
Follow-up hardening on top of #64158 (@DavidMetcalfe):
Backend (the root-cause fix):
- hermes_cli/moa_config.py: add validate_moa_payload() — strict write-time
counterpart to the deliberately tolerant normalize_moa_config(). Flags
half-filled slots, empty reference lists, recursive moa slots, naming the
exact preset/slot.
- hermes_cli/web_server.py: PUT /api/model/moa validates before normalizing
and returns 422 with the specific problems instead of silently swapping the
user's preset for hardcoded defaults (#64156). Also declares
fanout / reference_max_tokens / reasoning_effort on the Pydantic payload so
client round-trips no longer erase hand-set values.
Desktop:
- Replace sanitize-then-send with hold-while-incomplete: the debounced
autosave is deferred (not repaired) while any slot is half-filled, and
flushes once the model pick completes the edit. Mid-edit UI state is never
repainted by a save response (generation guard covers held edits too).
- updateMoaSlot only clears the model when the provider actually changed.
- Explicit preset ops (set default / add / delete) cancel the pending
autosave and invalidate in-flight responses so the two writers can't race.
- Stable row keys (preset+index) so mid-edit rows don't remount; cleared
model shows the 'Model' placeholder instead of vanishing.
Both TS clients' MoaConfigResponse types now declare the round-tripped
fields (fanout, reference_max_tokens, reasoning_effort).
Tests: 12 new backend unit tests (validate_moa_payload contract incl.
validate/normalize agreement), 3 new web_server endpoint tests (422 on
half-filled ref/aggregator, fanout round-trip), 3 new desktop vitest cases
(autosave held while half-filled, flush on completion, same-provider
reselect no-op). E2E validated against a live TestClient with isolated
HERMES_HOME: bug sequence now 422s with config untouched.
Fixes#64156
Three fixes in model-settings.tsx:
1. Filter incomplete slots before autosave: sanitizeMoaRefsForSave() strips
reference slots with empty model before any autosave (the 600ms debounce
was sending half-filled provider-but-no-model slots to the backend, where
_clean_slot rejects them and _normalize_preset falls back to hardcoded
defaults). Presets with zero valid refs keep their empty reference_models
array rather than being silently dropped. The aggregator slot is also
sanitized when its model is empty.
2. Add withActive() to MoA provider dropdowns: the reference and aggregator
provider Selects filtered to authenticated-only, so unauthenticated
current values (e.g. openai-codex) rendered blank. Mirror the existing
pattern from the model Select.
3. Add generation counter to scheduleMoaSave(): stale save responses could
overwrite newer state. Bump a counter on each save and skip setMoa/setError
if a newer save was scheduled in the meantime.
ReviewDirRow never rendered a DiffCount, so collapsed folders gave no
indication of the additions/deletions inside them. The tree builder
already aggregates added/removed onto directory nodes (verified by
tree-data tests) — this just renders them, matching the file rows.
CI runners have no global git identity. The remote repo seed commit
needs inline -c user.email/user.name flags, matching the pattern
already used by ensureGitRepo.
When "new worktree" branches off a remote-tracking ref like origin/main,
`git worktree add -b <branch> <dir> origin/main` auto-sets upstream
tracking (branch → origin/main), producing `branch:origin/main` in branch
listings. The user wants a standalone local branch — like `git checkout
origin/main && git checkout -b branch` — not one silently wired to the
remote. Add `--no-track` when the base is an `origin/` ref. Local branch
bases are unaffected (they never triggered tracking).
Extract shared hermes_cli/input_sanitize.py (bracketed-paste wrapper stripping
and terminal ~[[e artifact suffix collapse) and wire it into prompt.submit
and the Desktop composer so corrupted user text is cleaned before
messages.content is persisted.
resetLayoutTree() claims to "restore everything" but never reopened collapsed
SIDES. A sidebar hidden before a reset survived it, so the next ⌘B toggled from
that stale-hidden state into a SHOW — and the user's hide never persisted across
reload. Reopen every bound side through its store so the toggles stay truthful.
Adds a store-level regression test (real modules, re-import = reload) covering
hide→reload and the reset→hide→reload repro.
Drive the composer's layout off its OWN measured width (the existing
ResizeObserver, so it reacts per-tile, not per-viewport) instead of only
stacking or overflowing:
- >=440px: full inline row, full model label.
- 320-440px: still inline, but the model pill sheds its label for its chevron
icon (~120px back) so the controls stop crowding the placeholder.
- <320px: stacks to the two-row layout, pill stays iconized.
Adds COMPOSER_COMPACT_PILL_PX above the stack breakpoint and a `compactPill`
signal from useComposerMetrics, wired to the model pill's existing compact mode.
Make every session tab speak the same drag language as a sidebar row, and
keep tab focus 1:1 across the sidebar, tiles, and main:
- Drag a session tile's OR the main workspace tab onto a composer to link the
chat (@session chip), or onto a zone/edge to stack/split — tabDrag now
returns whether it took the drag, so the workspace tab defers to the generic
pane move on a fresh draft (nothing to link).
- A lone tool panel (terminal/logs) dragged to its own zone keeps its header,
so it stays draggable/closable instead of stranding a dead, tab-less zone.
- The sidebar highlight follows the FOCUSED session (interacted tile, else the
main selection) rather than only the main one.
- Clicking an already-open session jumps to its tab — an open tile, or the
workspace tab when it's the main session and focus sits on a tile — instead
of a dead no-op reload.
- Secondary (single-chat pop-out) windows boot to the default tree with no
tiles and never persist their stripped-down layout back, so they can't
inherit or clobber the primary window's tabs/splits.
The pointer drag ghost is extracted to a shared lib/drag-ghost and the drop
affordances drop their now-unused labels.
@assistant-ui/react 0.14 makes respondToApproval required on
ToolCallMessagePartProps; the settledClarifyProps helper still lacked it
after the upgrade cherry-picks, so tsc failed on clarify-tool.test.tsx.
a reader of this subclass can't recover from hermes code alone that the metadata.isOptimistic flag drives core's off-branch eviction and export() omission, so a future core change to it would silently break placeholder cleanup. flagged in the upgrade review.
lock the four behaviors the built-in normalizeMathDelimiters/escapeCurrencyDollars introduce over the deleted custom helpers: $$<digit>$$ display math stays intact, double-backslash brackets and [/math]/[/inline] tag pairs rewrite to dollar delimiters, and currency dollars in prose are escaped. the existing preprocessMarkdown suite had no math cases.
the parseIncompleteMarkdown comment implied the reveal frontier is repaired; repair runs on the full accumulated text, so reword it to say that. drop the now-dead "multiple surfaces render the same content" clause from the block-cache comment (the smooth and defer wrappers that caused it were removed), and trim the math-preprocess comment to the load-bearing prose-only constraint.
@assistant-ui/store renamed its index-out-of-bounds throw from tapClientLookup/tapClientResource to useClientLookup in the 0.14 upgrade, so the boundary's /tapClient.../ filter stopped matching and re-threw the transient session-switch and reconnect race to root, blanking the app. broaden the regex to accept the new prefix (keeping the old one for older store versions) and point the test at the real message so it exercises the live path instead of the dead string.
delete the custom rewriteLatexBracketDelimiters and escapeCurrencyDollars
implementations from markdown-preprocess.ts (~40 lines). the built-in
exports from @assistant-ui/react-streamdown 0.3.4 are strict
improvements:
- normalizeMathDelimiters combines rewriteLatexBracketDelimiters (now
handles double backslashes and trims body whitespace) with
rewriteCustomMathTags (handles [/math]...[/math] and
[/inline]...[/inline] tags that some models emit — new capability
HA didn't have before)
- escapeCurrencyDollars excludes $ as a preceding character, so
display math $$5 is no longer incorrectly escaped (bugfix)
the call site in preprocessMarkdown changes from
rewriteLatexBracketDelimiters(escapeCurrencyDollars(part)) to
normalizeMathDelimiters(escapeCurrencyDollars(part)).
verified: tsc 0 errors, eslint clean, all 16 preprocessMarkdown tests
pass (including currency dollar escaping), vitest 0 new failures,
manual verification of currency amounts, LaTeX bracket delimiters,
display math, and dollar signs inside code blocks.
delete lib/remend-tail.ts (108 lines) and lib/remend-tail.test.ts (105
lines). the tailBoundedRemend export from @assistant-ui/react-streamdown
0.3.4 is algorithmically identical — same findRemendWindowStart boundary
scan, same fence/math tracking, same slice-and-repair strategy. the only
differences are improvements: the built-in handles \r (CR) in line
endings for Windows compatibility, and accepts an optional RemendOptions
parameter passed through to remend.
the import in markdown-text.tsx moves from @/lib/remend-tail to
@assistant-ui/react-streamdown. the call site
(preprocessWithTailRepair) is unchanged.
verified: tsc 0 errors, eslint clean, vitest 0 new failures (15
pre-existing, 786 passing — 6 fewer than before because the deleted
remend-tail.test.ts had 6 cases), manual verification of incomplete
markdown repair during streaming.
delete SmoothStreamingText, DeferStreamingText, and useSmoothReveal
(~174 lines) from markdown-text.tsx. the built-in defer and smooth
props on StreamdownTextPrimitive now handle the same work:
- defer: routes streaming text through useDeferredValue so markdown
re-parsing runs at lower priority (typing/scrolling stay responsive)
- smooth: typewriter-style reveal via useSmooth with SmoothOptions
{ drainMs: 500, maxCharsPerFrame: 30, minCommitMs: 33 }, matching
the old useSmoothReveal constants exactly
MarkdownTextContent (reasoning text) gets both defer and smooth.
MarkdownText (assistant text) gets defer only, matching the previous
behavior where text messages had no typewriter effect.
the internal pipeline order changes from smooth → defer → preprocess
to preprocess → smooth → defer (the built-in primitive runs preprocess
first). this is functionally equivalent: the tail-bounded remend repair
runs once on the full text instead of per revealed prefix, and the
smooth reveal operates on already-repaired markdown. end result is
identical.
verified: tsc 0 errors, eslint clean, vitest 0 new failures (15
pre-existing, 792 passing), manual verification of 6 streaming
scenarios (defer, smooth reveal, typing-while-streaming, code blocks,
math, long text performance).
bumps @assistant-ui/react from ^0.12.28 to ^0.14.23 and
@assistant-ui/react-streamdown from ^0.1.11 to ^0.3.4. this crosses
two minor bumps on each package and unlocks the built-in defer, smooth,
and tail-bounded remend primitives for PR 2.
breaking change from core 0.2.x: MessageRepository.appendOptimisticMessage
was removed (assistant-ui#4162). inline the three steps it did (generateId
+ fromThreadMessageLike + addOrUpdateMessage) in
incremental-external-store-runtime.ts, and set metadata.isOptimistic so
the new off-branch eviction logic cleans up the placeholder correctly.
fromThreadMessageLike and generateId graduated to the public API in
0.14.22 (assistant-ui#4414), so they now import from @assistant-ui/react
instead of @assistant-ui/core/internal. ExportedMessageRepository in the
test file moves to the public import for the same reason. the remaining
internal imports (AssistantRuntimeImpl, BaseAssistantRuntimeCore,
ExternalStoreThreadListRuntimeCore, ExternalStoreThreadRuntimeCore,
hasUpcomingMessage) are runtime construction internals with no public
equivalent and stay on @assistant-ui/core/internal.
the @assistant-ui/store npm override is removed: all transitive ranges
now resolve to 0.2.18 without it.
verified: tsc --noEmit passes, vitest shows zero new failures (15
pre-existing, 792 passing, identical to baseline before the upgrade).