mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 23:44:41 +00:00
2c8e8a826810337492cc5a38bb0bbd705b6fb632
86
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2c8e8a8268 |
fix(app): use Command+F for Find on macOS across chat, file, and terminal (#5129)
Control+F moves the caret forward on macOS, so chat Find and file pane Find were stealing a keystroke the text editor owns. Chat Find listens on `document` in the capture phase, so the composer received an already-cancelled Control+F and the caret never moved. Each Find surface carried its own copy of the shortcut predicate and only the terminal's was platform-aware. There is now one predicate, `isFindShortcut` in `@/pane-find`, that takes the platform as a parameter: Command without Control on macOS, Control without Command elsewhere, no Shift or Alt. Chat Find, file pane Find, the Find widget's own query box, terminal Find, and the terminal emulator runtime all call it, and `terminal-find-shortcut.ts` is gone. App surfaces source the platform from `findShortcutPlatform()`, which wraps the app's existing `getShortcutOs()`. The predicate stays free of React Native imports because the terminal emulator runtime is bundled on its own into the terminal WebView; its default platform and `getShortcutOs()`'s browser branch now share one user-agent read in `@/utils/mac-user-agent`. Closes #5097 |
||
|
|
5d70ab2ab8 |
fix(app): restore character-paced streaming replies (#5013)
* fix(app): restore character-paced streaming reveal Restore the v0.8 character reveal model and remove the word-level fade implementation so streamed assistant and reasoning text remains opaque, grapheme-safe, and immediately completes when a turn ends. * fix(android): preserve inverted timeline selection React Native's prebuilt Android library does not consume source patches under node_modules. Register the compiled scroll manager before the core package so inverted child-reveal calculations leave focus and text selection in place while normal and explicit scrolling remain unchanged. |
||
|
|
bb4763b38c |
fix(app): open chat find from the focused composer (#4991)
* fix(app): open chat find from the focused composer Listen during capture so the composer text input cannot swallow the find shortcut before it reaches chat search. * test(app): verify draft retention on first chat find |
||
|
|
6830c46e9a |
fix(app): keep composer layout stable across chat and workspace screens (#4973)
* fix(app): dock the New workspace composer like a chat on phones Only the composer is bounded by the viewport; the setup fields sit above it in a bottom-anchored stack that moves up as the draft grows. The keyboard reservation is retained on every screen, so the composer keeps one height across keyboard transitions. Removes the per-screen reserve policy. * fix(app): share composer docking across mobile hosts Extract chat's retained capacity and platform keyboard motion so chat, workspace drafts, and New workspace share one sizing and dismissal boundary. Keep the native drivers and settled portal reconciliation unchanged. * fix(app): preserve overlays in centered composer docks Honor the optional overlay child in both layouts so changing dock placement does not silently discard part of its content. * test(app): resolve keyboard controller through its web entry The shared keyboard entry point makes stream tests load the provider. Keep its real ESM dependencies inside the Vitest resolver so CommonJS does not bypass the React Native web alias and parse native Flow sources. * fix(app): keep native chat scrolling responsive Place keyboard dismissal behind scrollable content so an idle Android Fabric JS responder cannot intercept native drags. Keep decorative form padding transparent to iOS hit testing and exercise idle native scrolling after submission from every composer host. |
||
|
|
64b1a62ed3 |
Fix voice reply latency and hide internal prompt wrappers (#4927)
* fix(voice): speed up replies and hide prompt wrappers Keep voice prompt cleanup in the client presentation path so provider history and live messages render consistently without rewriting canonical input. Correct native TTS scheduling and threading, and reconnect interruption to speech detection and session cancellation. * test(voice): clarify playback and timeline regression journeys * test(voice): centralize native worker cleanup and repair fixture path * test(app): exercise workspace eviction without reloading the app * test(app): preserve streamed output during workspace eviction Hold the turn until the workspace is evicted instead of dropping all stream events during navigation. Slow CI runners can finish the entire response before navigation ends; waiting for producer completion before release makes that ordering deterministic. * test(app): stabilize subscriptions during workspace eviction Opening another chat replaces the timeline subscription, making buffered frames from earlier generations unroutable. Preopen all chats before holding the stream, finish production before eviction, and assert both early and final response content with no subscription replacement. |
||
|
|
7557992dff |
fix(app): stabilize composer height and isolate typing updates (#4902)
* fix(app): preserve composer capacity when the keyboard closes Releasing the keyboard reservation enlarged an unchanged capped draft. Retain the last editing reservation while still recalculating capacity for viewport changes and subsequent keyboard openings. * perf(app): isolate composer editing and resize updates Draft and cursor subscriptions rerendered the chat and full composer on every native edit. Give editing consumers a live text source, preserve attachment identity on text updates, and read viewport settling state without a render. Read live input text for web resize measurement so removing per-keystroke props does not collapse a draft when its width changes. * fix(app): restore current draft when the composer remounts The last replacement command does not include subsequent edits. Seed remounted editors from the live draft snapshot so switching launch modes restores the saved text without subscribing the composer to every edit. * fix(app): synchronize composer resolution state before paint Avoid an extra passive-effect delay when forwarding forge resolution state from the isolated binding. Exercise live-text width measurement through the actual composer E2E journey instead of a hook-only test component. |
||
|
|
80b4387bc6 |
Find text throughout chat history (#4765)
* feat(app): add pane Find to file source * fix(app): float the pane Find widget and tighten its chrome Find pushed the first lines of the file down, ran 480px wide with 32px ghost buttons, and let the replacement input start left of the query input. Float the CodeMirror top panel out of flow so the widget overlays the top right of the pane, move the disclosure toggle into a gutter that spans both rows so the two fields share a leading rail, put the match count inside the query field, and drop the icon controls to the shared pane-toolbar chrome the file bar above already uses. * fix(app): keep the active match visible and drop the Find button The floating widget covered the active match whenever the match sat under it and the document was too short to scroll clear. CodeMirror's own panel scroll margin cannot reveal a first line, so the file integration now owns placement: the widget renders as an overlay beside the editor, the model measures it against the active match, and flips it to the opposite corner only when the current corner collides. Hysteresis keeps navigation from bouncing the widget between corners. Also removes the standalone Find button shown while Find is closed, per user direction. Cmd/Ctrl+F is the entry point. * fix(app): stop the Find placeholder from hiding Go to line Hiding `.cm-panels` suppressed every CodeMirror panel in the file editor, including the Go to line dialog that Mod-Alt-g opens through the same search keymap. Hide only the inert Find placeholder panel instead, by its own class, and leave the shared panel container alone. * feat(app): find text throughout chat history Discover candidate transcript locations on the daemon, then reuse prompt navigation to load history and verify rendered occurrences on the client. Keep search state inside its own modules without changing timeline or task presentation contracts. * fix(app): show actionable chat search failures Keep transport error details in the search model while showing localized retry guidance. Use named client request options for timeline search. |
||
|
|
6215e08efe |
Smooth streaming replies with a continuous left-to-right fade (#4859)
* feat(app): reveal streaming text as one continuous fade front Streaming assistant text is released by word and every character fades in over the same 150 ms envelope from a start time owned by the message. Start times never decrease in reading order, so a line paints as a single front moving left to right, however the text is chunked. Per-word envelopes with an internal stagger let the next word's first letter overtake the previous word's last letter, which read as holes and flashing words at the real frame rate; 100 ms frame strips hid it. Native renderers receive the fade ranges as a prop on a host view wrapping the root Text, so ranges and text land in the same Fabric mount transaction. The earlier binding sent ranges through a separate async module call keyed by view tag and had to reconcile them against the view's text; on a fast phone the UI thread lags JS by several commits, mounts and calls interleave, and every reconciliation heuristic guessed wrong. Native code no longer infers newness or creates start times. The mock load-test provider now separates cycles with a blank line so a cycle heading is not appended to the previous paragraph. * fix(ci): refresh streaming dependencies and isolate E2E setup * test(app): distinguish fade cleanup from Markdown replacement * test(app): detect restarted fades during Markdown updates * docs(app): keep streaming guidance focused on constraints and QA * test(app): control animation time in Android fade assertions Fixed wall-clock sleeps let emulator scheduling select the sampled animation frame. Inject the animator time source and drive paint at exact timestamps within one UI-thread operation; production keeps the system clock. Verify the mid-fade sample and the exact cleanup boundary without sleeps, polling, or retries. * fix(test): compile the desktop renderer before lifecycle checks The lifecycle fixture treated Metro /status as app readiness. Its first Settings click therefore spent its 90-second UI budget compiling the cold bundle. An isolated cold-cache run restricted to one CPU reproduced the click timeout before the app runtime had loaded. Reuse the browser suite's existing bundle warmup before launching Electron, and preserve Metro output in the lifecycle artifacts. The UI timeout, build-request timeout, lifecycle assertions and product behavior are unchanged. CI did not preserve Metro output, so its precise startup timing is not recoverable from the failed run. The retained log makes subsequent startup failures diagnosable without relying only on an empty renderer text dump. Validation: three complete lifecycle E2E runs under private Xvfb displays; existing Metro readiness tests; app typecheck; targeted lint; root format. * test(file-observer): await burst delivery through native recovery The deletion phase required every event within 10 seconds, but native coalescing recovery uses a 30-second safety audit followed by the existing reconciliation debounce. An idle diagnostic excludes that delayed audit and therefore cannot act as a completion barrier. Await the required delivered events instead, preserving every deletion assertion and the existing overall test watchdog. A real-filesystem test with controlled native notifications reproduces 95 outstanding paths at the old deadline and verifies completion when the safety audit delivers them. No production observer behavior changes. * test(file-observer): verify burst recovery through public events The controlled-clock regression constructed a private native backend, bypassing the interface used by consumers. Consolidate recovery coverage into the public observer integration test instead: drive real writes, deletes and directory renames, then await every required public event, including the 450 surviving files at their renamed paths. No public capability was added: subscribe callbacks already express completion. No production behavior or test timeout changes. * test(app): drive Android fade tests through the native host The instrumentation suite constructed TailFadeInAnimator directly, advanced its private clock, and counted FadeInSpan instances. It now mounts the WordFadeHost that React mounts, feeds it the ranges prop the bridge sends, and asserts rendered pixels: hidden before a start time, a monotone front inside and across words, settled frames equal to the baseline, and no frame callbacks left after settling. Span counts and internal timing state are no longer asserted. Choreographer cannot be advanced deterministically from a test and the fade epoch is the message's wall-clock start time, so the host takes a WordFadeFrameClock (time plus frame callbacks) as its smallest public seam; production keeps the Choreographer-backed default. Fixture text is sized in pixels so every sample stays on one line at any density; at 420 dpi the 32sp fixture wrapped and the column helper summed glyphs from the first line. |
||
|
|
0aca3b605a |
fix(plugins): transform whole timeline items before grouping and Markdown splitting (#4675)
Send every original tool call and each whole accumulated assistant message through plugin transformers before display grouping or Markdown splitting. Preserve streaming callbacks, original payloads, native Markdown block identities, and chat outline reading position. Reject legacy fragment caches and retain failing-first plugin-interface and browser regressions. Fixes #4673. Fixes #4789. Consolidates #4820. Co-authored-by: Matt Cowger <matt@cowger.us> Co-authored-by: Jegor Kitskerkin <jegor.kitskerkin@gmail.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
0d7760e68b |
fix(agents): keep rejected plans collapsed in conversation order (#4756)
Keep plan proposals at their original timeline position through approval, rejection, and cancellation. Make plan cards collapsible while retaining readable history after follow-up prompts and daemon restart. |
||
|
|
6f2195904b |
fix(subscriptions): isolate observers and silence unrequested feeds (#4596)
Give each observation a server-issued ID owned by its physical connection. Preserve snapshot-only reads and combined list-and-subscribe, source-bound replies, lifecycle cleanup and legacy wire compatibility. |
||
|
|
75eb9d44a3 |
feat(agents): surface provider notifications in the timeline (#3411)
* feat(pi): surface extension notify in the agent timeline Pi extension ctx.ui.notify() messages were dropped or buffered away and never appeared anywhere in Paseo. Emit them as notification timeline items so they show in the agent chat feed, with info/warning/error levels mapped to activity log styling. * fix id * feat(agents): unify timeline notifications Represent Pi and OpenCode notices as one first-class timeline item instead of provider-specific synthetic tool calls. Gate the new wire variant so older strict clients keep working during version drift. * test(omp): update notice expectations --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
bb676ef086 |
Smooth mobile keyboard transitions (#4275)
* perf(app): smooth mobile keyboard transitions Animating dock padding forced Fabric and Yoga to relayout the chat tree on every keyboard frame, producing visible stepping and React list churn. Keep the surface full-height and move it as one translated unit, with scroll range and portal measurements updated only after keyboard motion settles.\n\niOS keeps its native keyboard animation driver behind the same component boundary because its Reanimated signal reports the endpoint at animation start. * fix(app): isolate native keyboard implementation |
||
|
|
e34aea206e |
feat(plugins): add live timeline rows and client slash commands
Plugins can transform live timeline source items at render time, append durable plugin rows from daemon handlers, and register client-only slash commands in the composer. Related to #3940 and #3952. |
||
|
|
8dd4da6ee0 |
Keep mobile streaming and panel motion stable under JS stalls (#4190)
* fix(app): activate mobile panels after motion settles Gesture previews were publishing React presentation state and activating retained content before the native motion finished. Keep transient motion on the UI thread and publish the active panel only from the accepted animation completion. * perf(app): re-render only changed history rows Every coalesced stream tick re-rendered all mounted history rows because the inverted FlatList recreates each cell with a new index and ref. Keep layout item identity for unchanged rows and memoize the row on it, and resolve tool-call groups through a stable event so the render callbacks stop churning. * perf(app): count replica row bytes once Budget accounting measured every stored row of a touched host on each persist through the Buffer polyfill. Cache the UTF-8 length per immutable row and count without materialising bytes. * fix(app): settle mobile panels on position Activity now follows the UI-thread position reaching the canonical anchor instead of an animation completion callback. A successful gesture keeps its finishing motion and the matching command adopts it; settlement and command acceptance may arrive in either order. * fix(app): keep settled panel props across JS stalls Turn off Reanimated's FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS. Its settled-props collector drops entries older than two seconds before React receives them, so a JS stall across a settle left the overlay display and backdrop opacity reverted on the next commit while the store held the new panel. * fix(app): reconcile hidden keyboard end on iOS Only a hidden end event resets the keyboard shift. A late visible end no longer resurrects a keyboard that already dismissed. * fix(app): focus the replacement input through autoFocus after clearing Clearing swaps the native input for a fresh instance. Focusing the doomed instance asked Android for the keyboard and then tore the focused view down, which cancelled the show; a JS focus command sent alongside the remount reached a view that was not attached yet and the IME ignored it. Focus and blur requested while the replacement is pending now decide the replacement's autoFocus, which native applies once the view is attached. * fix(app): revise history rows in every viewport The history row memo boundary bails out until its item identity changes, but only the native viewport cloned rows flagged by historyRowRevision. On web and desktop a history host whose tool-call group kept updating from the live head rendered stale. Move the revision into useRevisedHistoryRows and run both viewports' history through it. Regression test renders the web strategy with a memoized row and asserts only the revised host re-renders. |
||
|
|
ba54575ab7 |
Show completion times for hidden-prompt turns (#4170)
System-injected prompts have canonical turns but no visible user row, so completed timing was discarded despite an exact end timestamp. Preserve completion metadata with an explicitly unknown duration so the footer can show the truthful timestamp. |
||
|
|
7ae5133ed2 |
feat(plugins): let plugins transform and render timeline items (#3940)
* feat(plugins): add timeline item contributions Keep canonical daemon history unchanged while plugins project and render custom timeline items in clients. Live matches refresh authoritative projected history before replacement so lifecycle deltas stay collapsed. * fix(nix): update npm dependency hash The plugin SDK lockfile change invalidated the fixed-output npm dependency derivation. Use the hash calculated by the macOS Nix build for the current lockfile. |
||
|
|
e22ad117fd |
Stream assistant text at a steady rate instead of in arrival lumps (#3612)
* Stream assistant text at a steady rate instead of in arrival lumps Assistant text arrived in lumps and was painted that way. The daemon coalesces one message per 60ms window, and that window carries however many characters the model produced in it, which swings by an order of magnitude within a turn. Painting each delta as it landed made the size of those lumps visible. Three changes: - The agent stream coalescer is now leading + trailing. The first delta after an idle window flushes synchronously instead of waiting a full window, matching the invariant TerminalOutputCoalescer already documents. The window stays at 60ms; shrinking it would raise message rate on a loop that already contends with terminal frames and relay encryption, without making the stream smoother. - The client reducer queue commits on a frame instead of an arbitrary 48ms timer, with the timer kept as a ceiling so a hidden tab still advances. - Assistant and reasoning text are revealed at a rate derived from the backlog rather than from arrival timing. Arrival changes the target, not the rate. The store keeps the full text and only the rendered slice is paced, so copy, selection, the chat outline, and scroll geometry all stay consistent with what is on screen. The leading flush means a burst's first chunk lands as its own timeline row. The projection already merges contiguous same-turn rows and clients read the projected timeline, so history is unchanged; the server tests that assert on raw rows were updated for the extra row. Measured on Expo web against a local dev daemon. Mock bursty-stream provider, 6s samples, paint-on-arrival vs paced: frames that advanced the text 55 -> 244, chars-per-frame CV 2.66 -> 1.44, gap p50 100ms -> 17ms. Real Claude Haiku agent over 52s: 84% of frames advance the text, CV 1.29, gap p50 and p95 both 17ms (one frame at 60Hz). Adds a bursty-stream mock model with seeded burst sizes, a gated browser perf spec, and docs/agent-stream-performance.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Address review: safe reveal boundaries and a testable seam Two findings from the PR review. Unicode boundaries. Slicing the revealed text at an arbitrary index could strand a lone surrogate, which paints as a replacement glyph, or split an emoji ZWJ sequence so a family briefly rendered as its members. Both were visible for a frame or two mid-stream — the exact flicker the paced reveal exists to remove. The rendered cut is now pulled back off surrogate pairs, combining marks, variation selectors, skin tone modifiers, and ZWJ joins. The reveal counter stays monotonic and only the painted boundary moves, so a long cluster cannot stall the reveal. Test placement. The hook test used JSDOM, @testing-library renderHook, and monkey-patched requestAnimationFrame, all of which docs/testing.md rules out; it prescribes fixing the missing seam instead. The reveal policy is now a pure state machine in text-reveal.ts (begin/retarget/advance/ complete/visible) covered by plain unit tests with no environment, and the hook is reduced to the frame-clock binding. Rendered behavior is left to the browser spec, which no longer carries frame sampling and statistics in its body — those moved to e2e/support/helpers/stream-smoothness.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Measure total painted text, not the tail message The smoothness sampler read the length of the last assistant-message element. A real turn emits many assistant messages, so that element keeps changing identity and its length is not monotonic — handovers read as resets, and a stream that was plainly moving on screen reported almost no growth at all. Sampling the total across every assistant message fixes it. This invalidated the numbers previously recorded for the real-agent run, so the doc table is replaced with a fresh A/B taken with the corrected sampler against a Claude Haiku agent: frames that advanced the text 6% -> 87%, chars-per-frame CV 4.11 -> 1.86, gap p50 317ms -> 17ms, p95 383ms -> 17ms. The paced column also reported zero Unicode defects across a stream containing emoji, which exercises the reveal boundary clamp end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(agent-stream): harden paced text reveal Use standards-compliant grapheme segmentation when available and preserve full arrivals on older runtimes. Cap reveal commits at 60Hz and measure the real browser rendering path so high-refresh displays stay smooth without rendering at hardware frame rate. * Hold back trailing fragments split by arrival boundaries The reveal boundary is not the only place a grapheme gets cut. The daemon's coalescing window ends a delta wherever the window closes, so a caught-up reveal can be handed half a cluster and paint it. Measured on a mixed-script Haiku stream: five frames in ~350 ending in a single regional indicator, i.e. half a flag on screen for a frame. While the turn is streaming, a trailing fragment that cannot stand alone — a dangling high surrogate, a trailing joiner, an odd regional indicator — now waits for the rest of its cluster. Once the turn ends everything received is painted, because nothing more is coming. Complements the grapheme-boundary clamp, which handles the reveal cut; this handles the arrival cut. The same stream now measures one such frame in ~400 rather than five. The remainder is a flush boundary that coincides with the end of an assistant message, putting the two halves in different messages, which the client cannot rejoin — that one belongs in the coalescer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
e19d3914ae |
fix(rewind): replace long timelines without replay (#3642)
* fix(rewind): refresh canonical timeline after rewind A rewind replaces the daemon timeline and epoch, but replaying the rebuilt history made long conversations visibly reload from the beginning. Capable clients now refresh the existing bounded tail path after a replacement signal while legacy clients retain replay compatibility. * test(rewind): cover coherent timeline replacement Hold rewind completion in the browser regression so it can verify the existing pending state and detect any reconstructed timeline rows before the normal tail refresh is allowed to finish. |
||
|
|
c914bcb44b | feat(app): make Explorer a first-class pane host (#3826) | ||
|
|
65fbe38dd3 |
Restore compact and stable chat timelines (#3716)
Restore compact message and footer spacing, reserve independent composer-pill clearance for transcript content and floating controls, and keep visible web tool-call expansion from shifting the reading position. |
||
|
|
19a7d26349 |
feat(app): make the side panel user-directed (#3605)
* feat(app): let the file panel close its tree rail The file panel's tree rail was permanent on desktop — the changes panel had a folder-tree toggle in its toolbar, the file toolbar had nothing, so the rail was there whether you wanted it or not. The changes panel's private DesktopTreeToggle becomes a shared TreeRailToggle so both toolbars render the same affordance; that also drops the treeToggleStyle prop threaded through ChangesToolbarProps, which recomputed exactly what the component already built internally. The i18n keys move out of workspace.git.diff into workspace.tree, since a shared component reaching into the git namespace would rot. The two rails keep independent flags — the file rail in panel-store, the changes rail in useChangesPreferences — so closing one leaves the other alone. FileTreeToggle wires itself to the store rather than taking a callback: the bar renders several components below FilePanel, where the rail actually lives. Both rails also get narrower (320 -> 260 default, 200 -> 180 minimum). The persist version bump resets treeRailWidth on purpose: nearly every existing install carries the old 320 default, so without the reset the narrower rail would only ever reach new users. Anyone who dragged their rail to a deliberate custom width loses it once. Note that MIN_CONTENT_WIDTH still clamps the rail to `containerWidth - 240`, so in a narrow pane the new default never applies — that floor, not the default, is what sets the width there. * feat(app): make the side panel an empty workspace surface The fixed, hideable side panel keeps supporting context beside an agent without seeding content. Pane-local choices stay local, while implicit supporting opens follow a persisted routing preference. * test(app): exercise side panel policy through its capability boundary The browser rail journey already covers the real tree-toggle behavior. Side-panel policy tests now pass their platform capability through the public boundary instead of replacing a production module. * test(app): expect an empty side panel after upgrade * test(app): expect an empty first split pane * test(app): account for the empty side panel in split retention * feat(app): make workspace panel tabs independent Give explicit New actions fresh tab instances, keep panel state opaque to tab management, preserve File navigation context, and stabilize uncached File transitions. * fix(app): preserve file conflicts during preview refresh Keep the current preview mounted for same-target reads and conversions so the editor retains its conflict observation state. Clear retained previews only when the file target changes. |
||
|
|
2a0a0ff656 |
Keep mobile workspace switching responsive with long chats (#3610)
* perf(app): bound native workspace mount work Native workspace switches retained inactive React trees and mounted large chat histories in aggressive list batches, making Hermes and Fabric work scale with prior usage. Retain only the active native workspace, reveal a shared turn-aligned history window locally before daemon pagination, preserve desktop retention semantics, and pace native list mounting. * test(app): cover bounded history journeys Exercise local history reveal and hidden outline jumps through the production browser viewport while preserving held-page pagination and scroll anchoring. * fix(app): reveal fetched outline targets A bounded merged jump could load its target before the mounted history window, leaving the pending jump unable to scroll. Reveal fetched hidden targets through the existing history-window capability, then scroll once they mount. * test(app): follow anchored history reveals History-start helpers could stop at a transient edge before local window expansion restored the content anchor. Retry the real wheel or keyboard action after geometry settles until the timeline remains at history start. * test(app): wait for history anchor release |
||
|
|
345638dc7f |
fix(app): retire the docked explorer sidebar
Upgrading to 0.5.0-beta.1 left the old docked explorer sidebar showing next to the new explorer pane, with no way to dismiss it. The sidebar rendered off the persisted `desktop.fileExplorerOpen` flag. #3287 rerouted the explorer toggle to the workspace-layout explorer pane, so nothing on a wide layout ever wrote that flag false again, and no migration cleared it. Assistant directory links and review attachments could still set it. On Windows and Linux the sidebar's close button was suppressed because the OS window controls own the top-right corner, so there was no affordance at all. The explorer is now one surface picked from two facts — is the layout compact, and can the platform render split panes: compact -> slide-over overlay (panel store) non-compact + splits -> dedicated right pane (layout store) non-compact, no splits -> ordinary tabs in the focused pane `workspace-tabs/explorer-surface.ts` owns that choice and every entry point routes through it, so the toggle, Ctrl+E, Ctrl+., directory links, review attachments and side file opens can no longer disagree about where the explorer lives. Pane splits stay gated on `isWeb`: they depend on dnd-kit and DOM-backed accessibility helpers. Native tablets are non-compact without splits, so they get explorer tabs rather than a second pane — deliberately trading side-by-side agent and Changes for a surface that only renders what the single-column tab row can actually reach. Ctrl+. is handled by the workspace screen rather than the app shell. The pathname alone cannot identify the active workspace: desktop cold-starts at "/" and restores the remembered workspace from route params, so a key derived from the pathname is null exactly when a workspace is mounted, and the explorer reads as permanently closed. `desktop.fileExplorerOpen` and `explorerWidth` are dropped by a migration to panel-state v15 but stay declared in the persisted schema. That schema is a strict object and the validated storage deletes the whole entry when parsing fails, so removing the keys outright would wipe every upgrading user's sidebar width, expanded paths and diff-collapse state. |
||
|
|
1cc6712e51 |
fix(app): make composer track clearance deterministic
Layout measurement excluded pill margin on some renderers, collapsing the intended gap above visible tracks. Keep the two-pill rail on one line and derive its complete tail clearance from shared geometry so chat switching and layout callbacks cannot change the result. |
||
|
|
5ec26f0c09 |
feat(app): align interface typography to base size
Treat the appearance setting as the rendered base size so the type ramp scales predictably. Native starts one pixel larger while web and desktop preserve the authored 14px base, with legacy values migrated on load. |
||
|
|
df3e646fbe |
fix(app): float composer track pills consistently
Keep agent and task pills as a transparent transcript overlay while measuring their rendered height for scroll-tail clearance. Share geometry and spacing with the import-session pill so borders, type, corners, and composer separation match across platforms. |
||
|
|
f3202ac419 |
fix(app): merge hidden-prompt turns visually (#3484)
System-injected prompts never enter the Paseo timeline, but their provider runs keep distinct canonical turn IDs. Keep those IDs for lifecycle correctness while grouping adjacent output without a visible prompt into one response. |
||
|
|
ab274d635e |
Make the workspace right pane a first-class tab host (#3287)
* feat(app): add persistent workspace side panes Keep the common review and file-browsing workflow beside the active agent without tearing down pane contents. Workspace tabs now share one compact chip language, assistant file links route into the retained right pane, and diff/file trees share stable geometry. * fix(app): localize pull request empty state * test(app): update explorer pane coverage * fix(app): preserve explorer pane workflows * test(app): make explorer tab selection idempotent * fix(app): persist workspace review pins * fix(app): preserve sidebar scrim drag hook * fix(app): restore companion pane interactions * test(app): follow selected sidebar surface |
||
|
|
42245d139a |
Add active-turn steering (#3394)
* feat(agents): add active-turn steering Let users choose whether an immediate prompt interrupts or joins the active turn. The daemon owns exact-turn admission, provider delivery, safe fallback, and canonical timeline identity while queueing remains client-side. * fix(agents): stabilize steering history and replay QA * fix(agents): preserve steering timeline order * fix(agents): reconcile resumed provider history * fix(agents): serialize steering with cancellation * test(agents): prove Claude multi-tool steering * test(agents): force explicit history hydration * fix(agents): flush output before steer admission |
||
|
|
d99258cdd0 |
fix(app): keep web scrollbars from shifting content
Use an overlay scrollbar for desktop chat timelines so scrollbar gutters cannot move centered content. Share its behavior with existing workspace scrollbars and derive resting and hover colors from semantic foreground tokens. |
||
|
|
d4435bd016 |
perf(app): keep warm workspace switching responsive (#3447)
* perf(app): reduce warm workspace switch work Retained workspaces avoided mounts but pane activity still rerendered the full composer, message input, and agent-control presentation. Keep the activity contract intact while moving focus-sensitive behavior behind small context consumers, and add a Playwright benchmark that measures rapid retained workspace switching on one browser clock. * perf(app): accelerate retained workspace switching Avoid repeated retained-stream layout reconciliation and unrelated workspace structure recomputation. Use the lightweight route transition while the host is already focused, while preserving targeted root POP_TO for returns from app-wide routes where it prevents duplicate hidden decks. * perf(app): defer composer focus during workspace switches * perf(app): retain recently used workspaces longer Keep up to ten workspace screens warm so normal multi-workspace navigation does not repeatedly rebuild recently used timelines. Inactive screens expire after ten minutes, while the active screen remains protected and capacity eviction stays least-recently-used. * fix(app): reconcile retained timeline geometry * fix(app): refresh retained reading position |
||
|
|
db6520bf36 |
Show live task progress while agents work (#3227)
* feat(agents): surface live task progress Keep current task state above the composer and turn task snapshots into semantic timeline activity. Normalize provider task protocols while preserving genuine plan flows. * fix(agents): admit tasks through stream sequencing * test(agents): expect normalized task status |
||
|
|
7253228dc5 |
feat(app): render mermaid diagrams in agent chat with pan/zoom (#2306)
* feat(app): render mermaid diagrams in agent chat with pan/zoom Fenced mermaid blocks in assistant messages now render as diagrams instead of code blocks. Web: mermaid renders inline via dynamic import (code-split, loads on first diagram). Drag to pan, ctrl/cmd+wheel or trackpad pinch to zoom about the cursor, double-click to reset; plain wheel still scrolls the chat. Rendered SVGs are cached across virtualization remounts. iOS/Android: diagrams render in a WebView running a self-contained generated bundle (build:mermaid-webview, mirrors the native terminal's xterm pattern — no CDN, mermaid ships in the app). The inline preview is touch-shielded and height-driven by a bridge message; tapping opens a fullscreen viewer where the platform WebView owns pinch-zoom. Streaming source that doesn't parse yet falls back to the highlighted code block and keeps the last good render; renders are serialized through one queue with stale-task skipping so token-by-token updates cost at most one in-flight layout. Security: mermaid can fetch external resources during layout (image shapes, CSS url()) before output sanitization runs — see mermaid-js/mermaid#7645 — so resource-bearing source is rejected up front (containsUnsafeMermaidSource) on every platform, htmlLabels stay off, securityLevel is strict, and the mermaid secure list is extended so directives can't override app theming or themeCSS. The lockfile diff includes npm 11.17 normalization churn beyond the mermaid closure; a no-op npm install produces the same rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): serialize native mermaid renders and pin viewer to last good source Serialize render messages inside the mermaid webview entry so a newer streamed update can't re-initialize global mermaid state (e.g. flip the theme) while a prior layout is still running; superseded queued renders are skipped. Track the last successfully rendered source on the RN side and open the fullscreen viewer with that, so tapping mid-stream while the latest chunk is still invalid can't show a blank viewer. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): pair native mermaid renders with their source via request id A "rendered" bridge event was credited to the newest sent source, but an older render can complete while a newer (possibly invalid) chunk's send is in flight, mis-crediting the older render's success to the newer source. Render messages now carry a request id that the entry echoes on rendered/renderError; the RN side resolves the source by id, so the fullscreen viewer always opens the exact source that produced the visible render. Addresses PR review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): harden mermaid guard and native preview per adversarial review - Guard bypass (blocker): mermaid accepts quoted and unicode-escaped object keys, so quoted "img": evaded the resource denylist while still parsing into a real img property. The guard now decodes u/x escapes and strips quoting characters before matching, and tests both raw and normalized source. - structuredClone polyfill banner in the webview bundle: the app's iOS floor is 15.1 but structuredClone exists only from 15.4; esbuild targets don't polyfill runtime APIs. - Preview height sizes the inner content view so fence padding/borders no longer clip the reported SVG height, and measurement happens at the final content width (measuring overlay keeps the fence insets). - The invisible measuring overlay is hit-test transparent, so an invalid diagram's fallback code block keeps its copy button and gestures. - renderError now prunes all superseded source-map entries, matching the rendered path, so a long broken stream can't accumulate sources. - The entry's render chain can no longer be poisoned: the whole task body is inside try/catch and the chain appends a trailing catch. - "Mermaid diagram" accessibility label is translated in all locales (message.mermaidDiagram) on web and native. - esbuild pinned in packages/app devDependencies so the generated webview artifact doesn't depend on hoisting layout; </script is escaped in the generated JS. Addresses adversarial review round 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(app): mermaid source toggle and visible zoom controls Web: a hover control cluster on the diagram (zoom in/out anchored to the viewport center, reset view, view source) mirroring the code block's hover-copy idiom; always visible on compact form factors where hover doesn't exist. View source swaps to the highlighted code block (with its own copy button) and a view-diagram button swaps back. Native: the fullscreen viewer gains a source toggle beside the close button, swapping the webview for the highlighted source in a scrollable view. All control labels are translated in every locale under message.mermaid.*; the flat mermaidDiagram key from the previous commit is folded into the same group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): align mermaid control icons with the app's icon vocabulary Network is the settings Connections icon — reusing it for "view diagram" gave one glyph two meanings; use Workflow instead. RotateCw is the app's established reset/reload glyph (RotateCcw had no precedent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): use Scan for mermaid reset-view control RotateCw is the app's reload glyph (19 uses) — a reset-zoom button wearing it invites a reload misread. Scan (corner-bracket fit-to-frame) is unused and carries the intended fit/reset semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): remove raw NUL byte from mermaid cache key separator A literal 0x00 in the template string made git classify the file as binary, rendering the PR diff for it unreviewable. Use the escaped sequence instead — identical runtime key, plain-text source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): align mermaid source-view toggle with the copy button The source view wrapped the code block while the fence margins stayed inside it, so the floating view-diagram toggle hung in the margin whitespace above the visible box — misaligned with the code block's copy button and outside its hover region. Hoist the margins onto the wrapper (both buttons now share the box edge) and render the toggle in the copy button's plain style so the pair reads as one control row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): close mermaid review findings * chore(app): finish Mermaid PR rebase cleanup * refactor(app): isolate diagram rendering and stabilize streaming * fix(app): keep Mermaid previews stable while streaming * fix(nix): update npm dependency hash * fix(app): preserve streamed rows through completion --------- Co-authored-by: Damon Meledones <dmeledon@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
3a6d6db9a5 |
fix(app): anchor image growth during timeline reload
Intrinsic image dimensions can arrive after the timeline has painted, outside the React segment-change path. Let the browser preserve the visible content anchor during that resize instead of exposing a frame before the deferred bottom correction. |
||
|
|
2298e9efdf |
Fix composer toolbar collapse flicker on tab switch (#2937)
Retained tabs stay mounted under display:none while inactive, which reports a zero-width onLayout. ControlledAgentControls fed that straight into its density resolver, so a hidden tab's toolbar collapsed to its tightest density and painted one frame collapsed before re-expanding when the tab became visible again. Extract the zero-width guard that useContainerWidthBelow already had into a shared readMeasuredWidth helper, and use it in the composer toolbar's own layout handler so a display:none measurement can never drive its density. Also bump the chat outline rail's MIN_PANEL_WIDTH breakpoint (818 -> 918) so the rail disappears earlier under a narrower panel, and widen the chat-outline e2e spec's viewport so its panel keeps healthy margin over the new threshold. |
||
|
|
211489c658 |
refactor(app): unify status colours on one token family
Three systems were reporting the same states: a full and a muted status tier in the theme, plus raw palette steps in StatusBadge and getStatusDotColor. A PR icon was muted in the sidebar and full-strength in the PR panel, and the Resolved badge used a theme-independent green so it painted a dark block in light mode. The muted tier is deleted and every status surface now reads the one token per signal. Dots keep a band of their own. A dot is a few points of solid colour with no shape to read and no label attached, and the running one pulses, so at the status band's lightness it sat below the check icons and host badge on its own row — backwards, since the dot is the row's state. Chroma caps at 90% of gamut rather than 100% because at the edge green reaches C=0.215 while blue manages 0.116 and the set stops reading as one family; dark lightness caps at 0.72 because red desaturates to pink above it. Host badges needed a foreground variant of the identity table. That table is tuned for a white letter on the colour as a fill; used as text it cannot clear 4.5:1 on both themes at once, since light needs luminance <= 0.183 and dark needs >= 0.212. The new set is generated on the status family's exact lightness and chroma fraction, so a host name and a CI check in the same meta row carry equal weight instead of the brighter one winning the row. Also: PR avatars had their own eight-hex palette and hash duplicating identity-colors; workspace rows under a project group now indent to the same rail as status-grouped rows; group headers are muted; and the pulsing dot is faster and fades further. |
||
|
|
f25e68b8cc |
Preserve chat scroll position across workspace switches (#2838)
* fix(app): preserve chat scroll across workspace switches Hidden retained panels can emit geometry-driven scroll events while collapsed, which previously looked like user intent and disabled output following. Keep the retained DOM mounted, suspend its inactive scroll work, and only detach from the bottom after explicit input. * fix(app): classify chat scroll intent at input A held pointer is not evidence that a geometry-driven scroll was user initiated. Detach output following directly from upward wheel, key, touch, or mouse-drag input, and keep ordinary scroll events responsible only for metrics and bottom reattachment. * fix(app): keep editing keys out of scroll intent Question inputs render inside the chat viewport, so their caret-navigation keys bubble through the same keyboard boundary. Treat keys from editable descendants as editing intent while preserving viewport-level keyboard scrolling. * fix(app): exclude zoom gestures from scroll intent Ctrl-wheel and trackpad pinch events can carry a negative wheel delta without scrolling chat content. Keep those browser zoom gestures from disabling output following. * fix(app): require pointer-driven chat movement Mouse movement alone is not viewport scroll intent. Correlate actual upward scroll with short-lived content-drag evidence or a scrollbar-gutter gesture, while excluding embedded editors and retaining geometry-only following. * fix(app): recognize overlay chat scrollbars * fix(app): scope scroll intent to chat viewport * fix(app): rearm chat history at scroll start * fix(app): track chat autoscroll intent * fix(app): separate pointer scroll gestures |
||
|
|
adf33930b3 |
Preserve assistant selection formatting when copying (#2808)
* fix(app): preserve formatting when copying assistant selections Browser selection serialized React Native Web layout nodes instead of the assistant message semantics. Recover semantic markup from renderer-owned metadata and reuse the rich clipboard generator used by Copy turn. * fix(app): preserve assistant copy semantics Carry source-only Markdown metadata through the selection copy boundary, unwrap generated code links, and cover ordered starts, hard breaks, and fence languages in Playwright. Refresh the Nix dependency hash for the added clipboard dependency. * fix(app): preserve partial selection context Wrap selected fragments with their shared semantic ancestor chain and enable GFM table and strikethrough conversion. Cover partial nested selections and GFM output through the browser clipboard. * fix(app): preserve copied list and autolink text Adjust partial ordered-list starts for omitted siblings, strip parser-generated anchors before Markdown reconstruction, and refresh the Nix dependency hash computed by CI. * fix(app): normalize partial selection context Normalize cloned ordered lists across block boundaries, degrade incomplete table fragments to selected text, and preserve table alignment provenance through Markdown reconstruction. * fix(app): escape copied table delimiters Rendered table cells no longer retain source escape characters. Escape literal pipes during DOM-to-Markdown conversion so copied tables preserve their column structure. * fix(app): preserve authored link semantics The rendered link boundary conflated parser-generated links with authored autolinks and discarded authored titles. Carry title semantics into the anchor and unwrap only links with explicit generated provenance. * fix(app): preserve nested copy structure Blank-line splitting separated loose nested lists before Markdown rendering, leaving no nesting for clipboard serialization to preserve. Derive protected separators from the Markdown block structure and keep copy metadata independent from monospace surface styling. * fix(app): preserve selected file links Assistant messages and rich clipboard HTML must apply the same link validation policy. A shared parser keeps file links semantic in both MIME representations while retaining markdown-it's unsafe-scheme rejection. * fix(app): preserve partial table content A selected body row wider than its retained header cannot be represented as GFM and is truncated when rich HTML is regenerated. Treat only those over-wide fragments, plus headerless fragments, as incomplete while preserving valid short rows. |
||
|
|
ccb668b3b4 |
Improve sidebar workspace readability and host identity (#2790)
* fix(app): align turn footer actions Keep running-turn actions in the same position as completed-turn actions and use the shared small icon size for both copy and fork controls. * feat(app): improve sidebar workspace identity Make dense sidebar workspace rows easier to scan and let each device control how connected hosts are identified. * fix(app): keep host badges readable Use a compact display menu and keep identity color on the host icon so translated controls fit and badge text retains standard contrast. * fix(app): announce selected sidebar badge mode * fix(app): retain icon-only host names for screen readers * fix(app): name hoisted workspace rows * fix(app): retain host identity in workspace labels * fix(app): retain workspace status in row labels * fix(app): defer host appearance until identity resolves * fix(app): serialize host appearance writes * fix(app): serialize host registry writes * fix(app): bound sidebar host badge labels * test(app): match workspace status labels * fix(app): consolidate sidebar identity behavior * test(app): assert status grouping behavior * fix(app): keep sidebar row actions accessible * fix(app): stabilize optimistic message layout |
||
|
|
553ac53643 |
feat(app): add chat outline navigation (#2792)
* feat(app): add chat outline navigation Long agent sessions need prompt navigation without coupling viewport movement to disposable server windows. Keep fetched timeline pages in client state so repeated jumps stay local, and settle prompt jumps against virtualized row measurement changes. * fix(app): preserve chat outline timeline coverage * fix(app): reconcile outline window submissions * fix(app): harden outline timeline navigation |
||
|
|
048b82f2a7 |
Preserve interactive ACP permission choices (#2762)
* fix(acp): preserve permission choices ACP question bridges encode answers as repeated permission options. Preserve each option through the UI and return the exact selected ID, while keeping auto-accept from choosing on the user's behalf. * fix(acp): validate selected permission behavior Reject inconsistent action IDs instead of allowing the action ID to override the response behavior. Move the misplaced browser spec so the current base CI contract can run. * fix(acp): preserve invalid permission requests Validate selected ACP actions before consuming the pending request so inconsistent client responses can be corrected and retried. * test(acp): verify real permission questions * fix(acp): reject empty permission action ids * test(e2e): scope Kimi config to ACP spec |
||
|
|
47c18a9213 |
Fork an agent session mid-run from the in-flight turn footer (#2638)
* feat(app): fork an agent session mid-run from the tab menu
Forking already existed, but only on the footer of a *completed*
assistant turn. While an agent is running, the in-flight turn renders
RunningTurnFooter with no fork menu, so exploring another path meant
scrolling back to hunt for an older turn's footer — or waiting for the
run to finish.
Add two entries to the agent tab context menu ("Fork chat in a new tab",
"Fork chat in a new workspace"), reachable regardless of run state. They
omit the fork boundary, and selectForkContextRows projects the whole
timeline when no boundary is given, so a mid-run fork carries everything
up to now — including the partially streamed in-flight turn.
Fork logic moves out of agent-stream/view.tsx into a shared useForkAgent
hook so the turn footer and the tab menu share one code path; the
per-turn behavior is unchanged (it still pins a boundary to its turn).
No server, protocol, or capability changes — this reuses the existing
agent.fork_context RPC and agentForkContext host feature. The daemon
handler is already a pure in-memory timeline read, so it was safe
mid-turn before this change.
* fix(app): resolve missing agent records before failing a fork
handleForkAgent read the agent record from the session store and gave up
if it was absent from both agents and agentDetails. A persisted agent tab
can outlive its record — restored on startup before hydration — so both
fork entries failed with a generic toast even though the agent exists on
the daemon and is perfectly forkable.
Fall back to client.fetchAgent and store the result before failing,
mirroring the resolution path agent-panel.tsx already uses. Only report
forkFailed when the agent genuinely cannot be resolved; a disconnected
host now says so instead.
The resolution logic lands in resolveForkAgentSource with injected
dependencies, since workspace-screen.tsx has no handler-level test
harness and a sibling testable module is the pattern in this directory.
storeFetchedAgentDetail moves out of panels/agent-panel.tsx into
utils/agent-detail-store.ts so the workspace screen doesn't import a
panel module — and into its own file rather than agent-directory-sync.ts,
which would close an import cycle through legacy-daemon-workspaces.
* feat(app): fork from the in-flight turn instead of the tab menu
Move the mid-run fork affordance to where the fork button already lives:
the assistant turn footer, beside the progress loader.
RunningTurnFooter previously showed only a spinner and elapsed timer, so
the fork button vanished for the whole duration of a run. It now renders
AssistantForkMenu alongside them, reachable the moment a run starts.
The in-flight fork passes no boundary, so selectForkContextRows projects
the whole timeline and the fork captures the response being streamed. A
fork button sitting next to a live response that silently dropped that
response would be worse than no button. Completed turns still pin their
own boundary, enforced by a separate handler type that keeps boundary
required.
layout.ts is untouched — the affordance is added to the running footer
component rather than by lifting the footer suppression, so no copy or
duration affordance leaks onto an incomplete turn.
Reverts the workspace tab context menu approach, along with the agent
resolution helper and store-writer extraction that existed only to serve
it. The footer path already holds the agent record.
* test(app): cover the in-flight turn fork with a Playwright spec
Extends the existing assistant fork menu spec rather than adding a
sibling file, so the completed-turn and in-flight cases sit side by side.
The new test pins the behavior that defines the feature: the in-flight
fork omits the boundary fields entirely, while the completed-turn fork
sends one. Both assertions read the actual agent.fork_context frames off
the WebSocket, using the read-only page.on("websocket") idiom from
helpers/timeline-delivery.ts so the observer cannot perturb the flow it
measures. Note the request omits the fields rather than nulling them —
daemon-client spreads them conditionally — so absence is what's asserted
there, and null only on the response.
Also asserts the fork trigger is scoped inside turn-working-indicator,
that no copy or duration affordance leaks onto the incomplete turn, that
the attachment carries the streamed text, and that the source agent keeps
streaming afterwards.
* test(app): cover the in-flight fork call site, gate, and teardown
Three behaviors this PR introduced had no coverage below the Playwright
spec. A copy-paste of resolveAssistantTurnForkBoundary into
handleForkInFlightTurn would have left every unit test green.
view-fork-in-flight.test.tsx mounts the real AgentStreamView with the
render strategy stubbed to the live-auxiliary slot, so handleForkInFlightTurn,
useStableEvent and the readOnly expression all stay real. It asserts the
fork request has no boundary KEY — matching the wire contract, where the
field is absent rather than null — for both targets, and that the read-only
gate withholds the trigger, with positive controls either side so an empty
tree cannot pass for a working gate.
turn-footer.test.tsx covers the race this PR created: the fork menu now
mounts only while a run is in flight, so a fork can be pending when the
subtree is torn down. The behavior is benign — React 18 makes the orphaned
setState a no-op — so the test asserts the observable outcome and leaves a
tripwire for unhandled rejections, since AssistantForkMenu's finally has no
catch.
Each test was verified by mutation: adding boundary: undefined, dropping the
readOnly gate, and keeping the running subtree alive each fail the matching
test.
Scoped to what this PR changed. The relocated hook's error paths, draft
setup construction, and attachment metadata predate it and stay untested
here.
* test(app): replace mocked fork coverage with Playwright
---------
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
|
||
|
|
8021d2b6c9 |
Keep agent timelines stable from submission through resume (#2718)
* Reapply: Stop completed turns from appearing stuck (#2484)
* Reapply: Keep older chat history and image previews stable (#2490)
* Lock timeline layout stability with user-driven specs
Two reds reproduce the reported jank:
- new agent, first prompt: the row paints at top=100 then jumps to 144 through
authoritative hydration, because the draft tab and the agent panel each render
their own AgentStreamView for the same logical row.
- long loaded timeline, submit: scrollHeight collapses 10793 -> 9111 mid-turn,
because splitOrderedTail reclassifies measured rows into the virtualized segment
where the virtualizer falls back to estimateStreamItemHeight.
Also restores centered timeline rows: the pagination anchor wrapper introduced in
#2490 was a plain block div, which broke alignSelf: center.
* Measure the turn footer blink from when the footer appears
Bounding the window at the prompt row conflated two things: the footer arriving a
frame after the row, and the footer leaving the layout and coming back. Bounded from
the footer's first frame, the blink still reproduces, so the footer-ownership race is
real rather than a sampling artifact.
* Rename the shadowed prompt row binding in the frame recorder
* fix(desktop): allow concurrent worktree dev instances
Desktop terminals can inherit another instance's profile path and debugging flags. Scope the profile to the current dev root and select an available debugging port so instances do not share global runtime state.
* fix(desktop): expose development providers in dev builds
Desktop-managed daemons inherited no Paseo runtime mode, so dev builds constructed the production provider registry. Mark unpackaged internal launches as development while continuing to force packaged launches to production.
* fix(timeline): stabilize optimistic turn layout
Keep initial history loading dormant until user intent and preserve active-turn layout across delayed transport updates. Add painted-frame coverage for optimistic timeline transitions.
* fix(timeline): preserve optimistic prompts through hydration
Canonical replacement relied on a transport submission record that can settle before a provider echoes the user row. Keep presentation continuity owned by the unreconciled row while destructive history replacement remains governed by active transactions.
* fix(timeline): make submitted prompts canonical
Submission settlement previously depended on status updates that could arrive after the optimistic row. Persist every accepted prompt in the canonical timeline and keep the footer active until RPC and canonical acknowledgement converge.
* fix(timeline): stop older history pages repeating entries
Backward pagination counted source sequence rows, so page boundaries inside a merged projected item could return that item repeatedly. Count projected entries and advance from their display anchors instead, with server and browser regressions covering the contract.
* fix(timeline): resume agents from the latest history
Use a bounded tail probe on resume so long background runs appear atomically instead of replaying every missed page. Preserve unchanged views, reconcile local/live rows, and keep skipped history reachable through backward pagination.
* fix(timeline): reconcile turn state after missed updates
Turn completion events can be missed while an agent timeline is unsubscribed, leaving footer activity stale even after the directory reports idle. Reconcile the single-owned turn lifecycle from authoritative resume snapshots while preserving newer live evidence.
* test(timeline): cover running state across refresh
Protect the hydration boundary where directory state must restore consistent tab, composer, and footer activity. Expose the running indicator semantically so the regression checks user-visible state instead of spinner implementation details.
* fix(timeline): keep history loading geometry stable
Reserve a fixed history-start gutter regardless of pagination state so showing or hiding the loader cannot shift timeline content.
* fix(timeline): keep turn activity coherent
Derive footer, tab, and composer activity from one lifecycle and submission projection. Idle directory transitions now request an authoritative resume so missed terminal events converge without treating directory delivery order as turn authority.
* fix(tests): align checks with runtime boundaries
* fix(desktop): stop dev app surviving its terminal
Electron was detached from the desktop runner, allowing it to outlive a stopped workspace terminal with broken output pipes and retain the single-instance lock. Keep Electron owned by the runner and preserve explicit process-group cleanup for detached child trees.
* fix(timeline): preserve activity through turn handoff
Correlate lifecycle events and snapshots with daemon-owned turn identity, and keep optimistic submissions active until their canonical row arrives. This prevents stale updates and RPC timing from exposing a false idle state.
* refactor(timeline): remove submission timing state
* fix(timeline): restore prompt identity on resume
* fix(timeline): exclude attachments from canonical prompt text
Structured provider prompts can include hidden history attachments before the user's text. Persist only plain prompt blocks so authoritative user messages match what the composer displayed while provider input remains unchanged.
* refactor(agent): remove unused pending-run message identity
* fix(desktop): let Chromium allocate the debugging port
* refactor(agent): remove impossible finalized-turn tracking
* fix(timeline): load history after keyboard scrolling
Treat observed upward displacement as user intent so keyboard and scrollbar navigation can wake dormant pagination.
* test(server): preserve accepted turn stream order
The manager-owned accepted turn boundary intentionally precedes coalesced output. Assert that lifecycle event explicitly while retaining the timeline-before-terminal ordering contract.
* test(app): scope sidebar fixtures by identity
Shared E2E daemon state may contain unrelated projects. Keep stale-route coverage tied to its named missing project and drag only the two rows seeded by the reorder spec.
* fix(timeline): gate prompt revisions by client capability
* fix(timeline): finalize canonical prompt delivery
Treat provider identity as a single stored-row transition so legacy clients receive one deliverable canonical prompt. Release identity-pending rows before later foreground output or the current terminal while preserving stale-turn isolation.
* fix(server): preserve legacy prompt pagination
* fix(timeline): keep assistant block identity stable
Assign canonical block IDs during live promotion so terminal finalization cannot remount image-bearing rows.
* fix(timeline): apply covered revisions during resume append
Run the exact canonical revision pass before overlapping resume pages append newer rows, while leaving cursor advancement to the incremental path.
* fix(server): preserve queued agent transitions
Project each queued live snapshot before awaiting stored metadata so later lifecycle state cannot overwrite an earlier transition.
* fix(server): snapshot queued agent updates
Capture each live agent payload before it enters the per-agent queue so shared nested state cannot change while an earlier storage read is pending.
* docs(app): date agent turn compatibility cleanup
* refactor(timeline): mint submitted prompt identity at acceptance
Canonical prompt rows now keep one immutable wire identity while provider identity remains internal for rewind translation. This removes the unreleased same-sequence revision and legacy projection contract.
* refactor(app): remove prompt revision reconciliation
Submitted prompt rows are immutable on the wire, so resume and live reducers no longer need to apply already-covered same-sequence updates.
* test(app): cover submitted prompt rewind roundtrip
* docs(timeline): describe immutable submitted prompt identity
Provider echoes now enrich daemon-internal identity only; wire rows keep their acceptance-time identity and rewind performs the provider translation.
* test(server): validate mock rewind targets
Reject unknown provider message IDs so app rewind coverage exercises wire-to-provider identity translation.
* fix(server): validate rewind before interrupting
* fix(server): omit rewind identity for daemon prompts
* test(app): guard rewind eligibility and rejection
* fix(server): preserve prompt identity across hydration
* refactor(timeline): remove stale revision remnants
* Revert "fix(server): preserve prompt identity across hydration"
This reverts commit
|
||
|
|
acb8506d1f |
Revert the timeline optimistic and pagination rework (#2596)
* Add npm release track invariant and beta dist-tag procedure * Revert "Keep older chat history and image previews stable (#2490)" The timeline optimistic/pagination rework needs more work than main releases can wait for. Reverted together with #2484; both are reapplied on integration/timeline-optimistic-rework. * Revert "Stop completed turns from appearing stuck (#2484)" See the #2490 revert. #2490 built on this submission-authority change, so the two revert and reapply as a pair. |
||
|
|
504b687f89 |
Keep older chat history and image previews stable (#2490)
* fix(app): keep timeline history and previews stable Treat persisted timeline replicas as display-only so authoritative pagination always comes from the daemon. Require renewed user intent between older-history pages unless the viewport genuinely remains at the history edge. Model assistant image acquisition as loading, loaded, or failed so recreating a preview URL cannot be rendered as an error. * fix(app): keep file image previews current * fix(app): stabilize history pagination edges * fix(app): recover transient timeline loads * fix(app): retain assistant image previews * Stabilize history pagination lifecycle * Stabilize history settlement and image previews * Retry file images after reconnect * Keep active previews and pagination requests alive * Replace image hook tests with typed ports * Integrate timeline hydration with submission authority * Harden pre-hydration timeline reconciliation * Make rewind E2E use real scroll intent * Preserve live timeline state through hydration * Keep mounted image attachments retained * Protect preview persistence and lifecycle hydration * Preserve idless live assistant continuations * Preserve timeline rows across delayed hydration * test(app): cover real assistant image files * Preserve assistant tool ordering during hydration |
||
|
|
e241e02afb |
feat(app): dismiss the chat keyboard on a fast upward flick (#2417)
* feat(app): dismiss the chat keyboard on a fast upward flick Scrolling the history to read earlier messages left the keyboard up, so the visible transcript stayed cramped and the keyboard had to be closed by hand first — two steps for what should be one. React Native's keyboardDismissMode cannot express the wanted behaviour: "on-drag" fires on the first pixel and kills the keyboard on any peek-scroll, and "interactive" is broken on inverted lists. So the gesture is measured here: samples are taken from the scroll events and, at release, the speed over the drag's final stretch decides. Measuring at release rather than averaging the whole drag is what keeps a fast but controlled read-scroll from counting as a flick, since such a gesture decelerates before the finger lifts. Timestamps and offsets come from the events, never from a clock: with a busy JS thread the callbacks arrive in a burst long after the gesture, and wall-clock spacing then reads a calm scroll as a flick. The release offset comes from the end-drag event for the same reason — the last onScroll can be stale by the time a short flick lands. Android needs both the blur and the dismiss. Dismissing alone leaves the input focused and the keyboard inset applied, so the layout stays shifted with an empty gap where the keyboard was; blurring alone releases focus but leaves the IME on screen. Verified on an Android device with a Release build: a slow drag and a 0.6 dp/ms scroll keep the keyboard, a 2.9 dp/ms flick dismisses it and the composer settles back with no leftover gap. iOS was exercised by hand on device only, without automated coverage of the gesture itself. * refactor(app): isolate keyboard flick dismissal * fix(app): isolate keyboard shift context --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
fa1198c2be |
Stop completed turns from appearing stuck (#2484)
* fix(app): stop completed turns from appearing stuck Turn activity was inferred from a user-message row flag, so a stale duplicate row could keep the working footer active after the turn ended. Track submission lifecycle separately and route every send path through the shared submission flow. * fix(app): guard pending message identity * test(app): measure submission layout independent of scroll * fix(app): keep submitted messages consistent through reconnects Track each in-flight send until its own RPC establishes acceptance, and keep canonical timeline placement authoritative. Restore legacy cached rewind IDs during cache deserialization. * fix(app): close canonical submission races * test(app): cover canonical submission races in browser * fix(app): keep replacement submissions authoritative Keep current pending rows out of legacy cache migration and leave ambiguous terminal lifecycle events to the daemon snapshot. This prevents fabricated rewind identities and stale completion events from marking replacement turns idle. * fix(app): keep submitted messages stable across sync Give submission transport and canonical timeline ingestion separate authority. Preserve every unresolved local send across replacement, reconcile provider identity once, and bridge RPC acceptance to authoritative running state without deriving lifecycle from timeline rows. * fix(app): settle submissions in either acknowledgement order Complete submission transactions when RPC and provider acknowledgement arrive in either order. Cache only transaction-owned local rows as transient data, preserve canonical ID-less prompts, and invalidate ambiguous legacy display caches instead of inventing provider identity. * fix(app): keep agent visible during history handoff Running and terminal updates could clear create continuity before the initial authoritative timeline arrived, leaving a streaming agent behind the loading screen. End the handoff only when authoritative history is applied. * fix(app): settle attachment-only submissions Canonical providers can acknowledge image-only prompts with empty text. Reconcile those events by client identity without rendering a blank canonical row. * fix(agent): settle out-of-band message submissions Accepted commands that do not allocate a foreground turn previously had no canonical user acknowledgement. Record the command before its handler runs so submission state and reconnect history converge through the normal timeline producer. * fix(app): settle out-of-band submissions compatibly * fix(app): preserve canonical prompt order * test(app): keep workspace status check timing-independent The workspace-status scenario asserted footer settlement before initial agent creation had necessarily completed. The dedicated draft-handoff coverage owns that lifecycle contract. |
||
|
|
b97d6d13f3 |
Load complete chat history when reaching the top (#2481)
* fix(chat): load older timeline history consistently Pagination depended on scroll events, so short or compacted initial pages could never request older history. Reevaluate edge visibility as layout and history change, and standardize loading indicators on the canonical app spinner. * fix(chat): keep older history loading reliably Use authoritative timeline cursor progress so merged rows cannot stall pagination. Wait for bottom anchoring before evaluating the history edge, and re-arm retries on a fresh upward gesture without automatic failure loops. |
||
|
|
72c7d3fe3e |
fix(app): keep streamed chat position stable on Android
Ignore synthetic momentum-end events emitted after native anchor corrections. Only active user momentum may settle sticky-bottom intent. |