Commit Graph

2387 Commits

Author SHA1 Message Date
Harry Yep 102d2d5676 refactor: replace custom math delimiter helpers with built-in normalizeMathDelimiters and escapeCurrencyDollars
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.
2026-07-15 01:42:09 -04:00
Harry Yep 4e10a38c9e refactor: replace custom lib/remend-tail.ts with built-in tailBoundedRemend
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.
2026-07-15 01:42:09 -04:00
Harry Yep 6c95740d9e feat: replace custom streaming wrappers with built-in defer and smooth props
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).
2026-07-15 01:42:09 -04:00
Harry Yep ddd6ad43f0 chore: upgrade @assistant-ui/react 0.12 to 0.14 and react-streamdown 0.1 to 0.3
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).
2026-07-15 01:41:03 -04:00
Brooklyn Nicholson 798e602a8e feat(desktop): tool panels collapse to a persistent rail, ✕ removes them
Terminal and logs now follow the IntelliJ/VS-Code tool-window model: their
toggle (⌃`, ⌘K) COLLAPSES the zone to a rail with the tab still showing instead
of hiding it outright, so "toggle" and "the tab bar" stop fighting. Restore
routes through the pane's store opener (rail click / chevron) so the shortcut and
titlebar toggle stay truthful; the tab's ✕ dismisses the panel (comes back via
its toggle), while a session tile's ✕ still closes the session. New store
primitives (setPaneCollapsed / restoreTreePane / collapseTreePane + a
collapse-pane registry) via a bindPaneCollapse in the controller.
2026-07-14 21:06:40 -04:00
Brooklyn Nicholson 601c1f16cb fix(desktop): keep clarify prompts out of tool overflow
Treat clarify as a hard boundary for bounded tool runs so interactive forms remain fully visible and usable.
2026-07-14 21:01:59 -04:00
Brooklyn Nicholson ec926ce89e fix(desktop): full-reset the thread runtime on a disjoint transcript swap
The incremental external-store runtime reconciles message repositories in place
(addOrUpdateMessage + prune-non-incoming). On a session switch the incoming
transcript shares no ids with the current one, and grafting the new chain onto
the old tree before pruning can strand a stale head/branch — the thread keeps
showing the previous session. When nothing carries over there's nothing to
preserve, so clear the tree first (leaves→root) then rebuild clean. Belt-and-
suspenders alongside the $messages-carryover fix.
2026-07-14 17:20:35 -04:00
Brooklyn Nicholson bccc827dbf refactor(desktop): trim backdrop store to match tool-view style 2026-07-14 16:59:57 -04:00
Brooklyn Nicholson 1813d3046c feat(desktop): add a chat backdrop on/off toggle
The faint statue backdrop behind the transcript was only switchable via
the DEV-only leva panel. Add a persisted Appearance toggle (default on)
so users can hide it; the Backdrop simply skips rendering when off.
2026-07-14 16:50:03 -04:00
Brooklyn Nicholson 7dc21f08a1 fix(desktop): clear the transcript on every cold resume so sessions can't share one
resumeSession hand-rolls $messages (it paints before a runtime id is bound), and
only cleared the old transcript on the cold path at entry. But a warm-cache hit
can bail down to the full resume — an empty-transcript drop, or the cache being
purged during the profile-swap await — without ever clearing, so the previous
session's array leaked into the next one. Symptom: switching sessions kept
showing the same messages (deterministic once tiling pre-warms the cache on
boot). Clear $messages at the single point every cold/bail path converges, so
carryover is structurally impossible; the warm fast-path still repaints in place.
2026-07-14 16:37:54 -04:00
Gille 2d0f2185cf fix(desktop): clear stale compaction status across session switches (#64127)
* fix(desktop): clear stale compaction status

Clear the compaction phase when a turn resumes with model or tool activity, and key response timers by session and turn so switching chats preserves elapsed time.\n\nSupersedes #48115 by porting its resumed-content approach to the current split stream hook and covering tool-first resumptions.\n\nCo-authored-by: liuhao1024 <sunsky.lau@gmail.com>

* fix(desktop): resume after thinking activity

* fix(desktop): clear turn timer on stop
2026-07-14 08:48:48 -04:00
Erosika c7e09f2571 fix(desktop): restore curated declared schema for the provider panel
The desktop provider panel previously rendered the curated declarations
from hermes_cli/memory_providers.py: five hindsight fields, and no panel
at all for undeclared providers like honcho (OAuth connect only). The
dashboard provider-switching rework re-pointed the shared config route
at raw plugin schemas, so the desktop began dumping every internal field
(35 for hindsight) and grew a bespoke honcho panel.

Serve both surfaces from the same route: ?surface=declared returns the
curated schema (empty for undeclared providers) with the original
config-file + env-store write semantics; the dashboard keeps the raw
plugin schema unchanged. The desktop client opts into declared.
2026-07-13 21:01:55 -07:00
Brooklyn Nicholson 5f37c9e85c refactor(desktop): tighten reasoning part typing, drop dead useRef
Self-review nits on the Thinking-widget fix:
- type ReasoningTextPart as ReasoningMessagePartComponent and read the
  typed useMessagePartReasoning() directly, dropping the ad-hoc cast
  (the hook already returns text/status).
- remove useRef, now unused after deleting useSmoothReveal.
- trim the autopsy comments; the PR body carries the narrative.
2026-07-13 20:38:34 -04:00
brooklyn! 19641fab72 fix(desktop): render reasoning text in the Thinking widget (#63999) 2026-07-13 20:38:26 -04:00
Brooklyn Nicholson 8bd4a419de fix(desktop): render reasoning text in the Thinking widget
The Thinking disclosure rendered blank for every reasoning-emitting model
(Fable, DeepSeek, GPT-5.5, ...). Two causes:

1. ReasoningTextPart read a `text` prop that assistant-ui never populates —
   reasoning parts arrive via context, same as text parts — so it always got
   an empty string. Read the text via useMessagePartReasoning() instead,
   mirroring how MarkdownText uses useMessagePartText().

2. The reasoning-only SmoothStreamingText / useSmoothReveal layer stalled at
   revealed="": the reasoning part stays isRunning for the whole message while
   the answer streams and thrashes re-renders, so the char-reveal never
   advanced past 0. Render reasoning through the same DeferStreamingText →
   surface path the assistant answer uses, and drop the dead smoothing code.
2026-07-13 17:57:54 -04:00
Brooklyn Nicholson d14bf23fa3 chore(desktop): build config — keep tsc emit out of src, gitignore artifacts 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson aff205dcc2 chore(desktop): i18n strings for tabs, zones, and session menus 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 369d0eeeff refactor(desktop): retire desktop-controller for the contribution shell; views as contributions 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 10fbade64b feat(desktop): electron — openDir IPC + ⌘W menu bridge (tabs, not windows) 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 7f74b324c3 feat(desktop): store + lib — layout/preview/session atoms, escape-layers, keybind helpers 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 0f922002eb feat(desktop): contribution controller, surfaces, and wiring 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 0f398f8e9c feat(desktop): focused-session-aware titlebar + statusbar 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 2afbe77763 feat(desktop): session hooks — open-in-tile, per-session actions, resilient resume 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 860a3f67bb feat(desktop): chat view — drop overlays, composer scoping, tile integration 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson eae1d7d147 feat(desktop): ⌘W close-tab, ⌘⇧T reopen, ⌘T new tab, ⌘1-9 + ⌃Tab tab switching 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson ac4f596ca2 feat(desktop): pointer session drag/drop + row/tab menus with close others/right/all 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson e6fea77d14 feat(desktop): multi-session tiles — per-profile state, tile pane, pane mirror 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson f1379bd6c2 feat(desktop): routes, nav, and command palette as contributions 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson aae35c5ee4 feat(desktop): shared UI — per-session prompt overlays, gateway overlays, tab primitives 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 63a9bde77b feat(desktop): layout-tree renderer — splits, zones, pointer drag-session, tab strip 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson c388daa665 feat(desktop): layout-tree model + store + workspace geometry 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson 7ed7170960 feat(desktop): plugin manager, runtime loader, and plugins settings 2026-07-13 17:57:53 -04:00
Brooklyn Nicholson aefb36299a feat(desktop): contribution registry — namespaced areas, keybinds, palette 2026-07-13 17:57:53 -04:00
Brooklyn Nicholson 99ff67eb03 feat(desktop): plugin SDK surface — rest door, socket, react-query, UI kit 2026-07-13 17:57:53 -04:00
Brooklyn Nicholson 39230d1738 refactor(desktop): funnel zoom apply+notify so restore can't desync
alelpoan's fix (emit hermes:zoom:changed after restore) is correct, but the
bug's root is duplication: setAndPersistZoomLevel and restorePersistedZoomLevel
each independently did setZoomLevel + send, and restore forgot the send.

Collapse both (and the lifecycle re-assert) into a single applyZoomLevel()
helper in zoom.ts that always applies-then-notifies — the regression can't
recur by omitting a send. Replace the source-grep test (which broke on main:
the sibling source-assertion pet test it copied was refactored to a behavioral
one, dropping the fs/path imports it relied on) with behavioral coverage of the
funnel, matching zoom.ts's "unit-testable without booting a BrowserWindow"
convention.

Co-authored-by: alelpoan <alelpoan@proton.me>
2026-07-13 17:39:03 -04:00
alelpoan 9f7a3cb1f6 test(desktop): cover restorePersistedZoomLevel renderer notification 2026-07-13 17:32:17 -04:00
alelpoan a1a4c8ce1f fix(desktop): sync UI Scale setting after zoom restore on window load 2026-07-13 17:32:17 -04:00
ethernet 5d691374c3 fix(desktop): recognize little-endian Mach-O magic in native binary classifier
classifyNativeBinary only checked big-endian Mach-O/Fat magic bytes
(feedfacf, feedface, cafebabe). Real Darwin .node files from node-pty
prebuilds are stored little-endian on disk (cffaedfe = MH_CIGAM_64),
so every Darwin prebuild classified as null, and validateStagedBinaries
threw a platform mismatch on macOS — breaking npm run check for every
macOS contributor. CI didn't catch it because runners are Linux (ELF
path was correct) and tests only planted big-endian fake headers.

Add recognition for all six Mach-O/Fat byte orderings:
- MH_CIGAM (cefaedfe) — LE 32-bit
- MH_CIGAM_64 (cffaedfe) — LE 64-bit [the one real prebuilds use]
- FAT_CIGAM (bebafeca) — LE universal

Update makeFakeNode to write LE CIGAM_64 bytes for the darwin fixture
(matching real on-disk format) and add regression tests for all new
magic forms.
2026-07-13 17:22:17 -04:00
ethernet 7a44a8fdec fix(desktop): prevent staging wrong-platform node-pty binary for cross targets
stageNodePty received electron-builder's { platform, arch } but unconditionally
copied host build/Release, staging a host binary (e.g. macOS Mach-O) for a
foreign target (e.g. linux-arm64). The fallback rebuild also didn't pass the
target arch to electron-rebuild.

Now:
- build/Release is only staged when target platform+arch match the host
- cross-platform targets with no matching prebuild fail closed
- same-platform different-arch rebuild passes --arch to electron-rebuild
- post-staging validation reads .node magic bytes (ELF/Mach-O/PE) and rejects
  any binary whose platform doesn't match the target

Adds stageNodePtyInto() (testable core) and classifyNativeBinary() (pure),
plus 11 regression tests covering the cross-target scenarios.
2026-07-13 17:22:17 -04:00
ethernet 47d56b802a test(desktop): run scripts/ tests in vitest 2026-07-13 17:22:17 -04:00
ethernet a6857faf48 test(desktop): fix remaining act() warnings in gateway-connecting-overlay
Move setGatewayState + rerender calls inside act() blocks and make the
synchronous soft-switch test async so all state updates are wrapped.
Eliminates the last 4 act() warnings (44 → 0).
2026-07-13 17:22:17 -04:00
ethernet b034fa7026 test(desktop): convert zoom source-regex test to behavior test
Extracted zoomWiringForWindowKind() + ZOOM_WINDOW_CONFIG into zoom.ts so
the pet-overlay-
opts-out / chat-windows-keep-zoom contract is tested via the pure
config,
not by reading source. Callers in main.ts now use
zoomWiringForWindowKind()
instead of inline { zoom: false } / default { zoom: true }.
2026-07-13 17:22:17 -04:00
ethernet 727025a2f3 test(desktop): replace windows-child-process.test.ts regex with real tests 2026-07-13 17:22:17 -04:00
ethernet c5794c505e change(lint): don't ignore config files in eslint conf 2026-07-13 17:22:17 -04:00
ethernet 955c5b73c5 test(desktop): move node tests to vitest as well 2026-07-13 17:22:17 -04:00
ethernet c008f41bb9 fix(desktop): stage-native-deps falls back to electron-rebuild when no native binary exists
When neither a prebuild nor a compiled build/Release/*.node is found for
the target platform-arch, stage-native-deps.mjs now runs
electron-rebuild -f -w node-pty to compile one from source before
re-copying build/Release into the staged dist.

This makes the staging script self-sufficient — it always produces a
working native binary dir regardless of whether npm ci --ignore-scripts
skipped postinstall or whether node-pty publishes prebuilds for the
target (e.g. linux-x64 has no prebuild).
2026-07-13 17:22:17 -04:00
ethernet ef61436967 test(desktop): fix React act() warnings across all desktop test files
Add vitest.setup.ts with IS_REACT_ACT_ENVIRONMENT=true + auto-cleanup,
and wrap render()/fireEvent() calls in act() across 8 test files:

- provider-config-panel.test.tsx: wrap renderPanel + fireEvent in act
- providers-settings.test.tsx: wrap renderProvidersSettings + fireEvent
in act
- use-prompt-actions/index.test.tsx: add actRender helper, wrap all 38
render
  calls, wrap Harness handle methods (submitText/cancelRun/steerPrompt/
  restoreToMessage) in act at the onReady callback level
- attachments.test.tsx: make renderWithI18n async + wrap in act
- skills/index.test.tsx: make renderSkills async + wrap fireEvent in act
- messaging/index.test.tsx: make renderMessaging async + wrap fireEvent
in act
- gateway-connecting-overlay.test.tsx: wrap all render/rerender in act
- preview-pane.test.tsx: wrap render calls in act, make tests async

Reduces act warnings from 44 to 4 (remaining are fake-timer + pre-render
store mutation edge cases). All 146 test files / 1180 tests still pass.
2026-07-13 17:22:17 -04:00
ethernet 999d63b517 test(desktop): fix a handful of broken tests 2026-07-13 17:22:17 -04:00
ethernet 56bec611e5 cleanup(desktop): note that ts imports don't need extension in one comment 2026-07-13 17:22:17 -04:00
ethernet 2762555038 cleanup(desktop): remove 'use strict' in ts & mjs
it's implied by ts and mjs files already
2026-07-13 17:22:17 -04:00