- use-keybinds: bail out of the global keydown dispatcher while an IME
composition is active. Windows Chinese IMEs use Ctrl+, as their
punctuation toggle, which also matched nav.settings and navigated away
mid-word — destroying the unsent composer draft (#41079).
- use-composer-draft.test: regression-pin the unmount-stash/remount-restore
contract so route navigation (Settings) can never again drop an unsent
draft with the React tree.
- use-composer-actions.test: the bounded-preview pipeline keys attachments
to the durable path and resolves thumbnails asynchronously; the dropped-
screenshot test now asserts the durable-path contract instead of the
retired full-res previewUrl field.
Addresses review feedback on #68744: downscaling inside
attachmentPreviewDataUrl made previewUrl the 2048px PNG, but current main
(d8cb73b4ab) feeds that field to ImageLightbox and useImageDownload, so
large attachments would open and download at reduced resolution.
- attachmentPreviewDataUrl returns the full-resolution data URL again
- ComposerAttachment gains thumbnailUrl?: string (store/composer.ts)
- attachImagePath stores previewUrl (full-res) + thumbnailUrl (downscaled)
- Attachment pill renders thumbnailUrl ?? previewUrl; lightbox/download
keep the full-res previewUrl
- optimisticAttachmentRef prefers thumbnailUrl for the in-flight bubble
display ref (same main-thread decode freeze at send time)
- Attachment-level regression test in use-composer-actions.test.ts:
full-res previewUrl preserved while thumbnailUrl is a separate
downscaled value (4000x3000 -> 2048x1536)
- 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.
The salvaged branch predates the electron test project's node:test -> vitest
migration (test:desktop:platforms is now `vitest run --project electron`).
Import `test` from vitest so the suites are collected; assertions stay on
node:assert/strict per the existing electron test convention.
Addresses both review findings on the remote-gateway download PR:
1. Unbounded buffering (finding #1). fetchBuffer / fetchBufferViaOauthSession
accumulated the entire response (then copied it again via Buffer.concat)
before saveGatewayFile even opened the save dialog, so a large gateway file
could exhaust the native process. Both auth paths now stream: once response
headers arrive the connect timeout is cleared, the filename is derived, the
save dialog is shown, and the body is piped to the chosen destination with
backpressure. A read/write error tears down the stream and unlinks the
partial file. The byte-moving, data-URL decoding, and filename/path helpers
are extracted into gateway-file-download.ts so they're unit-testable without
Electron.
2. No fallback for older gateways (finding #2). saveGatewayFile required the new
/api/fs/download route. Desktop and the remote gateway update independently,
so a gateway predating this PR 404s. Added a 404-only compatibility fallback
to the existing capped /api/fs/read-data-url route (bounded, so it only
serves smaller files — enough to keep older backends working).
Tests: gateway-file-download.test.ts covers streaming, backpressure,
error-cleanup (unlink on write/response error), data-URL decoding, filename
derivation (incl. traversal reduction), and 404 detection;
gateway-file-download-transport.test.ts asserts both transports stream (no
whole-body Buffer.concat) and that the 404 fallback is wired. Both registered
in the desktop platform test list. Server-side /api/fs/download tests
(streaming + sensitive-file reject) already pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read the durable display transcript when creating a branch instead of copying the compacted model projection. Hydrate the Desktop branch boundary from persisted history, avoid stale whole-chat counts, and seed the new tile from the backend snapshot. Add regression coverage for compacted histories, visible-message counts, selected prefixes, and hydration races.
(cherry picked from commit c3d2d759ae104395adde215b2e293ccf8e895684)
Residual hunk from PR #84832's picker-rebind commit; the functional
surfaces.tsx change landed on main via #86250.
(cherry picked from commit 990936d6f6e752ddb474787c1f7fdc3ad4f4dc60, docstring hunk only)
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.
Chinese/Japanese/Korean IMEs emit keydown events during composition that
carry preedit keystrokes and the commit keypress (Enter/Space/Shift for
candidate selection). Treating them as combos fires unrelated keybinds —
e.g. typing 你 with a CJK IME could dispatch session.new and silently
open a new session.
Guard comboFromEvent():
- Bail out entirely while composing (event.isComposing or key === 'Process')
- Ignore keydowns whose event.key is a bare modifier name but whose code
is a regular key — legacy IMEs that synthesize keystrokes (Q9 2002 sends
key="Control" with code="KeyW") would otherwise canonicalize into
phantom combos like mod+w that close the active tab.
Tested with Q9 (九方) legacy IME on Windows.
A missed compositionend (focus jump, input-source switch, programmatic DOM
swap mid-preedit) left composingRef stuck true, and the stuck flag silently
swallowed every Enter in handleEditorKeyDown and every Send-button submit via
the form onSubmit guard — no error, no RPC, until the composer remounted. For
CJK IME users (where even ASCII typing runs through composition) this read as
"Enter has no effect; messages cannot be sent", degrading composer instance
by composer instance.
Recover in two places, both grounded in invariants Chromium guarantees:
- keydown: every keydown during a genuine composition carries
isComposing=true, so when the native flag says we're not composing, clear
the stale ref before the guard reads it.
- blur: a composition never survives focus loss, so clear the flag
unconditionally — this is what unblocks the Send button path, which has no
native composition flag to consult.
The genuine-IME protection (#37483 class) is untouched: Enter with
isComposing=true is still swallowed.
Fixes#44135
macOS Chinese IME (and some 3rd-party Windows IMEs) emit Enter with
keyCode 229 (legacy VK_PROCESSKEY) after compositionend, while
isComposing is already false. The existing guard only checked
isComposing and the composingRef, so this Enter slipped through and
submitted the message before the committed text was fully in the DOM.
Add an explicit keyCode 229 check in handleEditorKeyDown. keyCode is
deprecated, but it is the only reliable signal for this IME commit
Enter on Chromium-based browsers. Includes a dom-repro test that
simulates the macOS IME sequence.
Fixes: "中文混英文按 Enter 直接上屏"
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
A session deleted in the sidebar disappeared optimistically but flashed
back when any list fetch raced the in-flight DELETE RPC — the backend
page still carried the doomed row until the transaction committed
(#50928, reproduced with 'Load more' and auto-refresh). The optimistic
tombstone ($removedSessionIds) was only honored by the recents slice of
refreshSessions; the messaging slice, the per-platform pager, and
refreshMessagingSessions ingested backend pages unfiltered.
Extract the tombstone filter into dropTombstoned() and apply it at every
session-list ingestion point. Tombstones only exist while a delete or
archive is in flight (they self-clear on confirmation and are removed
immediately on failure), so non-destructive refresh paths are untouched.
Fixes#50928
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.
Config settings auto-save on a 550ms debounce with no undo. The
'Enabled Toolsets' list is rendered by the generic ConfigField with no
destructive-change guard, so a stray select-all + Backspace (or any edit
that empties the list) is persisted the moment Settings closes —
silently disabling memory, terminal, web search, delegation, and most
tools. Recovery required CLI intervention.
Guard the one destructive transition: when the enabled-toolsets list
goes from non-empty to empty, window.confirm() before applying it (the
same pattern env-var removal already uses in toolset-config-panel.tsx).
Every other edit passes through untouched.
The decision is a pure helper (clearsEnabledToolsets) so it is unit
tested directly rather than through a full settings render. New i18n key
toolsetsWipeConfirm added to en + zh; partial locales inherit the
English string via defineLocale fallback.
Fixes#73319
With 25+ sessions the recents list virtualizes into its own nested
scroller inside the sidebar's scroll container. Both carried
overscroll-contain, so once the inner scroller hit a scroll boundary the
wheel gesture was consumed instead of chaining to the outer sidebar
scroller — read as a mid-list wheel dead-zone while scrollbar drag kept
working. Drop the containment on the inner scroller only; the outer
sidebar scroller keeps overscroll-contain so the gesture still never
escapes the sidebar.
Fixes#84964
Non-repo explicit projects (plain folders) get a main-checkout lane whose
label is the folder basename, not a branch. Clicking "+" (new session) on
such a lane calls switchBranchInRepo -> switchBranch, which sanitizes the
basename to "" and throws "Branch name is required.", aborting the
session creation. Short-circuit switchBranch for roots that are not git
work trees so callers proceed with a plain session.
Fixes#83028