Repository navigation
fix(transcript): automatically continue restored reading ranges - #1850
Merged
vastsa merged 2 commits intoOct 24, 2026
Merged
Conversation
Restored reading ranges should preserve their bookmark without leaving readers behind a manual history paging control. Continue bounded pages near the viewport boundary and rejoin live output only after the gap closes. Keep explicit search paging isolated, preserve the viewport and model input, and cover continuation, retries, and hidden-pane cleanup.
vastsa
added a commit
that referenced
this pull request
Oct 25, 2026
…#1796) * fix(asktool): keep card drafts across session switches The AskTool card kept its question index and draft answers in component state, so switching sessions unmounted the card and returning to the same pending ask reset every answer. Store the draft by requestId instead and let the card read and write that entry, dropping it when the ask is resolved, dismissed, or removed. Amends the asktool card spec's request-transition rule and adds E2E-ASKTOOL-draft-survives-session-switch. * fix(persistence): drop empty assistant rows regardless of status The transcript guard only dropped empty messages whose status was error or aborted. An empty complete assistant message therefore reached the outbox on every provider retry, growing the request body and feeding the EMPTY loop. Drop empty assistant rows whatever their status, while keeping messages that carry an error payload, a tool row, or a steering reservation. Adds empty-assistant-persistence tests for each of those cases. * fix(chat): open worktree references from the branch copy An isolated session's files live in its worktree, but chat references can still carry a path from the parent checkout: project instructions, MCP tools, or a reference captured before isolation. The resolver rejected that path as outside its roots, and the renderer then handed the file view a relative path that it resolved against the parent checkout, so the click either failed or opened the main-branch copy. Rebase an absolute parent-checkout reference onto the same relative path in the worktree when that file exists, and keep the worktree's absolute path in the renderer so the file view opens the branch copy. Paths that exist only in the parent still report outside-allowed-roots. fixes #1747 * fix(transcript): let an unpinned bookmark own the pane reveal Follow mode can still read pinned when a send or retry hid the pane. On the next reveal the scroll hook then bottomed the transcript before the reading-position hook could restore the recorded bookmark, so a reader who had scrolled up returned to the newest turn. Route the reveal through one shared ownership rule: an unpinned bookmark or a pending restore keeps the edge, a genuinely pinned pane bottoms, and an unpinned pane without a bookmark reapplies its retained offset. Both hooks now agree on which one acts. fixes #1771 * fix(agent-runtime): stop waiting for tools host-core cannot cancel The abort listener returned immediately unless the tool was Bash or GenerateImages, so Stop was a no-op for every other tool, and tools.execute was awaited with no deadline. One report measured a Grep running 21m39s after six Stop clicks. The listener now applies to every tool. Bash and GenerateImages keep sending tools.abort and get a bounded 10s grace window for the partial output the host collected before the kill; every other tool abandons the wait the moment the signal fires and returns TOOL_ABORTED without a tools.abort RPC (the host cannot act on it, and the 60s tombstone it would leave could be inherited by a later Bash reusing the call id). Both paths race the execute call against the abort, so losing the race abandons the wait rather than the host call: the orphaned call settles on its own and its result is discarded. Fixes #1767 * fix(agent-runtime): release in-flight tools from the stop itself A stop can reach the runtime without reaching the tool it was meant to cancel. The executor then keeps awaiting `tools.execute`, the turn never settles, and `abort()` — which awaits that settlement — never returns. The renderer awaits the abort RPC before it clears "processing", so the session stayed pinned even though the ledger had already recorded the turn as aborted: three turns in one field report were `aborted` while the tool's process was still alive minutes later. `abort()` now drives the in-flight tools directly instead of relying only on the signal each one was handed: it walks the active abort handlers and runs the same path a signal abort would. Cancellable tools still send `tools.abort` and get the grace window; every other tool abandons the wait at once. Refs #1767 * fix(composer): search past a truncated file index for @ mentions The file index keeps only the shallowest 8000 entries, so in a large workspace — a Unity project, where every asset has a .meta sibling — a script several folders down matches no query at all, even though the truncation note invites the reader to refine it (#1781). While the snapshot is truncated, a non-empty query is now also searched over the workspace's full listing through the read-only fs/search channel, after a short typing pause, and the best rows merge with the index's own matches. The listing is the git list the index already builds, or a deeper ignore-set walk when the index came from the walk. A complete index never searches, and a remote session keeps its snapshot because the remote file protocol has no search operation. fixes #1781 * fix(work-panel): keep the active tab's close button inside the strip The close button is a sibling of the tab button inside the tab wrapper, but scroll-into-view targeted only the button. With `inline: "nearest"` a button that is already visible scrolls zero pixels, so the sibling close button stayed clipped outside the strip's overflow edge — the tab looked like it had no close control at all (issue #1762). Scroll the wrapper instead (falling back to the button), and reserve the close button's width at the inline end via `scroll-padding-inline` so sub-pixel rounding cannot reintroduce the clip. * fix(mcp): fail loudly when a server has no DCR endpoint #1782: when discovery returns no registration endpoint, the login flow fell back to a literal client id: } else { clientId = "pi-desktop"; } No authorization server has ever registered that id, so the app opened `<authorize>?client_id=pi-desktop` and the user landed on a bare 404 — with no explanation and no way forward. It is not a GitHub-specific failure: any OAuth MCP server without Dynamic Client Registration is affected. Throw before opening anything, naming what is missing and the two real ways out. A settings field for a client id does not exist yet, so the message does not promise one. The test drives the real startLogin through a mocked discovery that omits registration_endpoint, and asserts both the message and that openExternal was never called. Verified against the old code: the test does not merely fail, it hangs until the 300s timeout, because the previous flow waited for a callback that could never arrive — the same experience the report describes. Verification: mcp-oauth.test.mjs 20/20, mcp-oauth-discovery.test.mjs pass, tsc --noEmit clean. * fix(settings): keep the last successful market list when a query fails A failed catalog query used to drop the list back to built-in picks, discarding what the user was just looking at (issue #1692 follow-up). Both markets now keep the last successful result for the same query on screen — under the failure with a timestamped note, and during the retry that follows — until a retry succeeds or the query changes. The snapshot never answers a different query, an empty successful answer is not a failure to cover, and Load more refreshes it. * fix(e2e): escalate to SIGKILL when the certificate probe hangs #1751's Electron E2E job ran 30m18s and was cancelled by the job budget with no failing step to read. The cause is this runner's only timeout: const timer = setTimeout(() => child.kill(), 60_000); SIGTERM is a request. An Electron child still initialising its renderer can ignore it, and the 'close' event this script awaits then never arrives, so the step holds the job until GitHub cancels it. The runner's own 60s budget is never enforced. Escalate: SIGTERM at 60s, SIGKILL at 65s, and report which one fired. A probe that cannot start now fails this step in about a minute with its output attached, instead of burning the job budget. Verified: normal path still passes ("provider client certificate E2E passed"); with a child that ignores SIGTERM the escalation force-kills it at ~800ms in an isolated harness, so the timeout path terminates rather than hanging. * perf(sessions): cache file-derived session summaries by file identity native.session.list() re-read, re-hashed and re-parsed every session file on each call: nativePiSnapshot() does readFileSync + SHA-256 + per-line JSON.parse, and list() then rebuilt SessionManager + branch + buildSessionContext per file. Cost grew linearly with session count and file size, all of it synchronous on the host main thread. Cache only the facts that come from one file's bytes (header identity, cwd, title, message count, timestamps, model, thinking level, structural flags), keyed by (dev, ino, size, mtimeMs). Any append, rotation or replacement changes the key and re-parses; projections for files a scan no longer sees are dropped. Nothing live is cached. Ownership, project trust, provider availability and cwd existence are recomputed on every call, and every write path still hashes the full bytes through nativePiSnapshot(), so external rewrite detection and lease validation are unchanged. The list path also stops needing a full snapshot to prove ownership: a missing .pi-desktop.lock already means the lease is acquirable, so the snapshot is only materialized when a lock file exists. Measured on a real ~/.pi/agent/sessions tree (28 sessions, one file at 21 MB): list() 826/807/933 ms before, 802 ms cold and 21/16 ms warm after. The cold round still parses every file, as before. * fix(runtime): size the output cap from the folded request transcript Long sessions accumulate one system-transcript snapshot per instruction change. Unless a model declares mid-conversation system support, every adapter folds them into a single leading message, but the output-cap estimate charged each superseded snapshot again. The clamp collapsed to 1 and the provider floor (16) went on the wire while the request was far from the window, so reasoning models were cut off and the turn ended as EMPTY_MODEL_RESPONSE without compaction ever triggering. The parent and subagent stream paths now fold the transcript the way the adapter will before clamping, and hand that same transcript to pi-ai so its own streamSimple clamp agrees. Context calibration and the compaction budget keep their existing basis. * feat(transcript): add display modes and thinking shimmer Add a persisted ranscriptDisplayMode setting with flat, command-first, and fully-folded layouts. Fully-folded keeps the v0.20.0 command grouping, command-first keeps run rows standalone, and flat removes nested activity groups while preserving one outer process. Make Thinking rows default expanded in flat and command-first, collapse to a whole-run TextShimmer indicator, and use the same shimmer for the hidden reasoning live indicator. Keep fully-folded's existing Thinking defaults. Deprecate keepCommandsExpanded; it remains readable for compatibility. Cover mode segmentation, settings normalization and sync, and renderer wiring; update transcript and settings specs and ADRs. * test(transcript): align turn-process probe with Thinking defaults Open the completed process and Thinking disclosure before asserting the retained reasoning text now that a collapsed Thinking row shows only its label. Also assert flat mode keeps one outer process without nested activity groups or standalone run wrappers. * fix(agent-runtime): skip expired OS trust anchors when merging the CA set The sidecar merges the operating system's trust store into Node's default CA set at startup. Those stores are a cache and keep entries that already expired: Windows still lists Let's Encrypt's `ISRG Root X2` and `R3` with a 2025-09-15 expiry long after the fact. Merging one of those makes OpenSSL accept an expired certificate as the anchor for any chain that names it as issuer. A chain like leaf -> YE2 -> Root YE -> ISRG Root X2 -> ISRG Root X1 stopped at the expired `ISRG Root X2` instead of walking up to the valid `ISRG Root X1`, so the handshake failed with `CERT_HAS_EXPIRED` even though the provider's own chain was valid. The failure is reported as a TLS category error and is correctly not retried, so the provider is unusable until the expired entry leaves the store. Every provider behind such a chain failed, which is why the report looked provider-specific (issue #1393). Reproduced on Windows 11 with Electron 43.6.0: `tls.getCACertificates("system")` returned 146 certificates, 17 of them expired, and the same host handshakes fine with the bundled roots alone. An anchor that has already expired can verify nothing, so it is never merged. The bundled and `NODE_EXTRA_CA_CERTS` sets are untouched, and chain, expiration and hostname validation stay enabled - this narrows the merge, it never disables verification. - `system-ca.ts`: filter the system store by `validTo` before merging. - `system-ca.test.ts`: assert no expired system anchor reaches the defaults. - `e2e-provider-certificates.mjs`: the trust probe now measures the usable system roots, because the previous "every system certificate is present" expectation cannot hold once expired entries are excluded. * test(transcript): mock TextShimmer in session reference probe The shared transcript module now imports the Thinking shimmer. Keep the session-reference renderer probe's dependency table complete so it can continue exercising message references without loading the app store. * fix(e2e): test certificate probe kill escalation Keep the normal Electron path unchanged while asserting that a child ignoring SIGTERM receives SIGKILL after the grace period. * fix(mcp): localize missing DCR recovery Fail before opening OAuth with an unregistered client, and send a stable error code so settings can show translated recovery guidance. * fix(transcript): fall back to the bookmarked offset when its row is unmounted Stacked on #1783, which fixes who owns the reveal edge but not this path. A reveal mounts only a trailing slice of history (TRANSCRIPT_INITIAL_MOUNT 15, then TRANSCRIPT_WINDOW_MIN 60, lib/transcript-window.ts:23-28), so an older bookmark routinely names a row that is not in the DOM. restoreTranscriptReading Position reported that as false, and the alignment loop returns on false without arming the deadline that would clear pendingRef (use-transcript-reading- position.ts:99). While a restore is pending, useTranscriptScroll discards every scroll event (:384-387), so the reader cannot travel up to mount the row either: no restore, no scrolling, permanently. The bookmark already recorded the absolute offset it was taken at (TranscriptReadingPosition.scrollTop, captured at :59/:68), but the restore path never read it. Use it as the fallback: an approximate place is strictly better than a pane that can neither scroll nor restore. A bookmark with no usable offset (NaN, negative, infinite) still reports failure, and a pane with no layout box is still refused outright. Tests: transcript-reading-position-fallback.test.mjs (5) covers the mounted-row path, the unmounted-row fallback, a zero-height row, unusable offsets, and the no-layout refusal. Related suites: 42/42. typecheck clean after a workspace build. * fix(host-runtime): expect the platform scratch separator in the launch-resolver test join(dataDir, ...) yields native separators and the assertion hardcoded the POSIX spelling, so the suite failed on every Windows checkout while Linux CI stayed green. Mirror the production expression instead. * fix(agent-runtime): tolerate the Windows symlink privilege in the sessionDir tests The #1732 tests create real file symlinks, which EPERMs on an unprivileged Windows checkout and crashes the fixtures (storage-directory-migration already degrades the same way). Catch EPERM and keep the remaining assertions, which hold without the alias; privileged runs and CI keep the full coverage. * feat(composer): toggle background task output from the whole row The background-tasks bar only opened a task's log through its "Output" button, so the row itself — the state dot, the command and the meta text — looked clickable but did nothing, and the click target was a few pixels wide. The row lead is now the toggle: mouse click, Enter and Space all flip the log, with aria-expanded on the interactive region. Stop and dismiss keep their own buttons outside that region, and a source test pins the split so a later refactor cannot regress the click target. * docs(spec): restore the missing rationale cell on D669 The D669 decision row landed with three cells in a four-column table in both languages, so `pnpm --filter @pi-desktop/docs test` fails on `origin/main` with a page-structure error. Fill in the rationale the decision actually records: the shallowest-8000 entry index matched nothing for a script several folders down, which is what issue #1781 reported. The table parses again. * fix(e2e): bound the client certificate baseline fetch Electron's direct fetch rejects before reaching the TLS fixture on macOS, but can stall at the same client-certificate negotiation on Linux. Bound the baseline probe and accept its abort as the platform-specific outcome, while still requiring that no HTTP request reached the fixture. This keeps the certificate discovery, proxy, redirect, and cancellation assertions running instead of letting the child process time out. * fix(e2e): bound the client certificate baseline probe Electron's direct fetch can remain pending during client-certificate negotiation on Linux, and its AbortSignal does not reliably interrupt that platform request. Race the baseline probe against a short deadline; keep the process alive for the remaining discovery checks, then exit Electron after the fixture has been verified. A successful fetch still fails the test, and the fixture must receive no HTTP request before the certificate error or platform timeout. * fix(chat): parse positioned and ambiguous file references safely Fixes #1722 Tested: pnpm lint && node --test apps/desktop/test/chat-links.test.mjs apps/desktop/test/chat-ref-resolve.test.mjs && pnpm --filter @pi-desktop/agent-runtime exec vitest run src/runtime.test.ts && node scripts/e2e-file-ref-line-scroll.mjs && pnpm docs:check passed (exit code 0) * fix(transcript): keep activity details visible after turns settle Fixes #1806 Tested: pnpm test:e2e:transcript && node --test apps/desktop/test/thinking-ui.test.mjs apps/desktop/test/transcript-display-modes-ui.test.mjs && pnpm typecheck passed (exit code 0) * test(transcript): align search test with group defaults * fix(window): restrict recovery pinning to macOS Bounds recovery could leave Windows and Linux windows always-on-top because only the macOS watchdog releases the temporary recovery pin. Keep pin acquisition on the platform that owns its cleanup. Regression coverage protects normal focus and hidden or minimized state while preserving the existing macOS recovery behavior. * fix(transcript): keep running marker inline The shimmer styles were inserted after the first half of the running-label selector, so the active label inherited inline-grid and the pulse marker became an auto-placed grid item. Separate the marker and shimmer selectors again so the marker stays in the label's inline flow and does not disturb truncation. * fix(transcript): center running marker The live dot was generated content inside the running label. A selector collision with the shimmer rule turned that label into a grid and pushed the dot onto a second row; even with the selectors separated, the inline pseudo-element sat below the row centre and could be clipped by the label's ellipsis box. Render the marker as a real flex sibling so the header's existing align-items centres it, and keep the label's text truncation untouched. * chore(models.dev): refresh catalog before beta.5 Refresh the bundled provider catalog so the 0.21.0-beta.5 test build packages the current models.dev snapshot. * test(provider): align DeepSeek window with catalog The refreshed models.dev snapshot publishes a 1,048,576-token window for deepseek-v4-flash; update the endpoint metadata assertion so the release verification pins the shipped catalog. * chore(release): v0.21.0-beta.6 * feat(transcript): flatten edit rows and restore group hierarchy A review-backed Edit or Write now renders as a single tool-level review row: the file header is the only disclosure header, with its diff body directly beneath it. This removes the duplicate generic tool row while keeping rollback and adding a copy action for the hunks. The command-first and fully-folded group bodies restore the released v0.20.1 12px child indent and left hover/focus collapse rail, so non-command tools keep their two-level hierarchy without adding a third disclosure. * fix(settings): size segmented controls to their content Keep translated display-mode labels on one line instead of squeezing them into the fixed field column. Allow settings controls and whole options to wrap when the available pane width is limited. * feat(chat): rework the top-level turn fold header The turn header now reads as a hairline row: status, recorded duration and tool count stay in one left-aligned run, with the disclosure caret trailing the metadata instead of sitting at the far edge of the transcript. Hovering only strengthens the text colour, and the issue count is no longer repeated at this level because the status already names a failure. Nested activity-group headers and their hover fill stay unchanged so the transcript hierarchy below the turn fold is not flattened. * fix(chat): keep the hairline header on the transcript edge The top-level row no longer bleeds into the gutter, so its fill starts at the prose edge while nested group and run fills keep their 4px hover bleed. The shared 5px gap keeps the turn title on the same text line as the nested titles, and the transcript E2E probes the new content edge and full-width hairline. * fix(e2e): measure the turn hairline against its process band The probe compared the header with the padded fixture root, so a correct full-width row could never match. Measure the header against the turn process that owns it instead. * feat(chat): show per-turn token usage capsule Completed assistant turns now show the model, average generation speed, and total tokens on one quiet row. Hovering or focusing the capsule reveals cache-read, input, and output counts without opening the composer inspector, while the composer remains the last-request occupancy authority. The capsule aggregates every parent assistant message in the visual turn, so its total answers what the round consumed rather than what the newest request occupied. Subagent spend stays out of the per-message usage contract. * fix(agent-runtime): preserve executions when providers reuse tool IDs Repeated provider IDs were mistaken for replayed history, dropping real results and freezing the outgoing request. Assign execution IDs before Pi journals each response and retain completed pairs at the request boundary. Keep identity reservations across compaction and model fallback. Fixes #1815 * fix(live-voice): repair Gemini work setup and report protocol failures Gemini rejects the additionalProperties keyword in its setup Schema. Use its supported schema subset while keeping OpenAI and Host validation strict. Reject failed setup immediately and retain the first socket failure's diagnostics in the provider log without exposing them over IPC. Fixes #1812 * fix(desktop): keep toasts clear of the work panel Anchor the global toast stack to the live main-content bounds so docked and resized work panels remain usable while notifications are visible. Preserve viewport fallback placement outside the chat shell and cover resize and horizontal-shift geometry in the question-notification journey. Refs #1820 * test(e2e): control toast focus transitions Drive the focus and blur lifecycle through explicit DOM events so the countdown coverage does not depend on operating-system window activation. Keep native keyboard input coverage in Electron. * fix(transcript): wait for history before applying fallback A bookmarked anchor can be outside the mounted window while its bounded history lookup is pending. Start viewport alignment only after the lookup succeeds so the fallback deadline cannot cancel it. Cover delayed lookup and document the approximate offset behavior. * fix(persistence): retain one trusted empty completion row Drop ordinary empty assistant rows to stop provider retries growing the transcript, while preserving the one-shot silent reply allowed by a Host-verified completion notice. Spend or revoke that exception on the first completed response, accepted steering, and turn settlement. fixes #1766 * fix(mcp-market): retain source badges in stale results The retained list is rendered after the current remote entries are cleared, so source badges must follow the displayed snapshot. Keep the existing provenance visible until a retry replaces it. * test(work-panel): cover tab close reachability after resize Make the geometry fixture shrink the actual scrollport and reset it between stages so resize and hover are independently exercised. Update the bilingual E2E scenario to document both paths. * fix(agent-runtime): cancel pending tools safely Stop immediately only when the active work is read-only or has a host cancellation path. Cancel pending approvals and remote reads, while waiting for started mutations and extension calls to settle before reporting the turn stopped. * fix(test): stub the turn usage capsule in the session reference fixture The session reference UI test transpiles the real shared transcript module and asserts every dependency is stubbed. Importing the new capsule broke that fixture graph, so the fixture now provides a capsule stub and the breakdown helper Mock. * feat(chat): show project breadcrumb and inline title editing The conversation top bar only named the task, so the project a task belongs to was hidden in a tooltip. Put the session's project beside the title as a quiet crumb and let the title be renamed in place, matching the rest of the app's direct-manipulation surfaces. The crumb resolves from the active session rather than the globally selected workspace, so a task opened from another project stays labeled correctly. Remote and scheduled transcripts keep a plain title because their titles are host-owned, and the inline editor keeps the existing title limit, IME behavior, and error path. Update the component spec and its source-contract test to match. * docs(spec): sync the Chinese topbar description The English component spec now describes the project breadcrumb, the inline title editor, and the expanded state table. Mirror those changes in the Chinese page so the locale pair keeps the same table and fence structure and the docs checks stay green. * feat(chat): keep the workspace crumb on an empty chat An empty chat has no session yet, so the crumb previously disappeared even though the app still has an active project. Fall back to the workspace name only in that state; temporary sessions keep the plain title because they belong to no project. Extend the topbar rendering test and both spec pages. * fix(ci): realign stale store and adapter contracts Recent main changes added AskTool drafts and persistence bookkeeping without updating every fixture, moved the empty-message guard into session-transcript, and changed the AppShell section attributes and host-core cancellation shape. The stale assertions failed all PR checks, including unrelated branches. The Gemini Live close handler also assumed a reason buffer always exists; a close event without one now keeps the adapter's teardown path instead of throwing. * chore(models.dev): refresh catalog before beta.7 Refresh the packaged provider catalog to the current models.dev snapshot so the prerelease ships the same model metadata the release workflow packages. * chore(release): v0.21.0-beta.7 * feat(shared): add inline visualization contracts The desktop shell, host-core, and agent runtime need one definition of where visualization fragments live and how a reply refers to them, so the reference format and URL scheme cannot drift between processes. This adds the `pi-genui:` URL helpers, the session directory and file name rules, the `::visualize{...}` reference parser (outside code blocks, tolerant of Windows paths), the guest message types, and the VISUALIZATION_HTML_ONLY error code. refs #1825 * feat(host-core): add a session visualization root Visualizations need a host-owned place the model can write without a permission prompt that is still clearly separated from the project and from disposable scratch files. Local sessions get `<data_dir>/visualizations/<sessionId>/` as a second contained root next to scratch (amends D114). Write/Edit there accept only direct `.html` fragments, an Edit move out of the contained roots still asks, and the root wins over a workspace that contains the data directory. The directory is removed with its session, only orphans are swept at startup, and forks copy the fragments they reference so cards survive deleting the parent. Remote and contract sessions get no root. refs #1825 * feat(desktop): serve visualizations through a sandboxed pi-genui scheme The renderer must be able to frame a model-written fragment without granting it network, host, or same-origin access, and without the fragment reaching outside the session's visualization directory. Main registers `pi-genui:` (standard and secure only, no fetch or CORS), resolves `pi-genui://session/<id>/<file>` against the real session directory with O_NOFOLLOW and a 2 MB cap, rejects full HTML documents, and reports failures as fixed error codes. Responses wrap the fragment in a shell with a strict CSP and Permissions-Policy, a small host runtime for size, theme, view, wheel, and Escape messages, and a styling kit adapted from ZCode's Apache-2.0 visualize plugin. Bundled D3 and Floating UI are served from `pi-genui://assets`, pinned CDN URLs are rewritten to them, and every other remote asset is blocked. The main window CSP gains only `frame-src pi-genui:` and permission requests from the scheme are denied. refs #1825 * feat(agent-runtime): teach agents to write inline visualizations A model only produces visualizations if it knows where to write them, what the sandbox allows, and how to reference the result. Local sessions advertise the visualization directory in the system prompt next to scratch, and a built-in `pi-desktop/visualize` skill (condensed from ZCode's Apache-2.0 skill) describes the fragment format, theme tokens, kit classes, and the reference line. Remote workspaces get neither the directory nor the skill, including in the `/` menu and send-time command expansion, and disabling the skill also withholds the directory. refs #1825 * feat(chat): render visualization references as inline cards Completed assistant messages should show a referenced visualization as an interactive card instead of a raw `::visualize{...}` line. The transcript splits reference lines out before Markdown rendering and hides an unfinished one while streaming. Each card frames the fragment from its owning session in a `sandbox="allow-scripts"` iframe, follows the app theme through postMessage, sizes itself from the guest, forwards wheel scrolling to the transcript, and can expand into a focus-trapped dialog. At most eight frames stay live; cards release their slot off-screen and reload on return. Missing, invalid, remote, or oversized fragments show a localized unavailable card. refs #1825 * docs: record inline generative UI visualizations Captures the decision and its trade-offs so later changes to the sandbox, storage root, or fork behavior can be reviewed against them. Adds ADR gen-ui-inline-visualization and D671, the session visualization directory in the tools-and-permissions and data-storage specs, the VISUALIZATION_HTML_ONLY error code, the E2E-GENUI-inline-visualization-card scenario, an unreleased note, and the matching zh-CN mirrors. closes #1825 * fix(agent-runtime): emit extension context before every request Trusted extensions never saw the first provider request of a run: the context event was emitted from prepareNextTurn, which pi-agent-core only calls after a completed turn. A reply without tool calls was never shown to context handlers at all, unlike pi-coding-agent, which fires the event before every LLM call. Emit context from transformContext instead, so each request is shaped once and the replacement stays a per-request view instead of becoming the run's working context. A runtime test covers the first request, the follow-up after a tool result, and the persisted transcript. fixes #1838 * fix(gen-ui): harden contained roots and native fragment serving Visualization paths must not grant implicit access through redirected roots or let startup cleanup remove unrelated data. Validate owned roots at authorization and execution, preserve Windows canonical spelling, and keep fork references independent of a relocated data prefix. Serve fragments through pinned native directory capabilities instead of raceable path snapshots. Enforce response-level sandboxing and one frame quota, preserving inline state while expanded. Add regression and real Host/Electron coverage without changing renderer or plugin permissions. refs #1826 * fix(test): allow standalone host cold-start handshake The native Host can spend about thirty seconds initializing without an Electron bridge, so the smoke client's thirty-second handshake races its own cold start. Match the shared harness's forty-five-second setup budget while retaining thirty-second deadlines for ordinary requests. Cover both deadlines with a controlled clock and real NDJSON streams so candidate smoke validation does not fail before exercising task code. * fix(composer): drop stale failure restores from superseded submits restoreDraftForKey guards only an empty slot, so a late accepted=false from an older submit repainted that message, attachments included, once a newer submit's clear had emptied the composer again (#1809). clearDraftForKey already refuses to clear a draft it does not own; give the restore the same staleness guard with a submit epoch, and let only the newest rejection restore. * fix(composer): track the submit epoch without a renderer-owned ref remote-unified-chat drives the submit hook outside a renderer, where useRef throws, so the #1809 guard moves into a module-scope Map keyed by draft slot. Per-slot keys also keep two composers (home/docked, plugin panes) from invalidating each other's restores, and the epoch survives re-renders the way a fresh closure could not. * fix(composer): release stale submit restore ownership Only the newest unresolved submission may restore a rejected draft. Unique submission tokens prevent older requests from regaining ownership after settlement, and completed entries are released so draft slots do not accumulate in a module-level map. A controlled async regression test protects the user-visible draft behavior. * fix(chat): open turn usage popover upward The capsule sits at the end of a completed turn, where the next answer or the composer follows immediately. Opening the breakdown upward keeps that content visible, so the capsule now uses the placement helper's above-first default and the unused below-preference option is removed. * fix(gen-ui): establish Windows directory share pins Attribute-only directory handles do not participate in Windows sharing accounting, leaving ancestors replaceable during fragment reads. Request list/read access so the existing no-write/no-delete sharing policy pins the entire canonical chain for the duration of each read. Exercise the actual native reparse operation and preexisting mutation handles, with an unpinned victim control. Correct cmd junction fixture quoting and DOS spelling so these tests reach the filesystem boundary. * fix(browser): keep session pages available in the background Decouple browser execution from side-panel presentation so switching conversations, collapsing the panel, or reloading the renderer no longer interrupts a session's page. Preserve trusted session routing and sandbox boundaries while retaining a hidden native backing host for Chromium. Cover background input, capture, renderer recovery, and resource cleanup with regression and native Electron tests. * fix(gen-ui): bind Windows fragment reads to native directories Windows permits attribute-only handles to change reparse points despite read sharing pins, as the real FSCTL control demonstrated. Open canonical ancestors and every owned child relative to retained native directory handles so no absolute child path can be redirected during serving. Keep the successful attribute-junction attack as a regression control at session, base, and data boundaries. Preserve bounded same-handle reads, reparse validation, normal ACLs, and the existing private RPC contract. * fix: preserve explicit model reasoning effort overrides Let a saved model binding opt into reasoning_effort for compatible gateways without removing the automatic DeepSeek protection. Preserve the choice through Host persistence, runtime projection, copying and configuration sync. Add the tri-state settings control, request-body and persistence regressions, and real renderer save/reopen coverage across locales and window sizes. Fixes #1821 * feat(chat): make inline visualizations an opt-in setting Inline visualization cards were on for every local session, so a model could write fragments and the transcript would frame them without the user ever asking for the feature. The new `AppSettings.inlineVisualizations` switch is absent and false by default, which keeps the transcript unchanged for anyone who does not opt in. One helper decides what the process treats as disabled, so the session launch, the `/` menu, and the on-demand `Skill` tool cannot disagree. While the switch is off the prompt advertises no visualization directory, the `visualize` skill is withheld from the catalog, and the renderer still strips reference lines — a historical reply must not show its absolute path as text — but produces no card, so nothing is read from disk or framed. * feat(models): infer custom endpoint auth from optional API keys Let custom endpoints work without a key instead of requiring a separate no-auth switch. Preserve stored keys on blank edits and stage explicit key removal until Save, including anonymous model discovery. Cover credential transitions and localized setup paths while retaining the existing host storage and runtime no-auth contracts. * feat(chat): scope background tasks to their session and share one dock with todo (#1844) * feat(chat): scope background tasks to their session and share one dock with todo Background commands belonged to the host, not to the session that started them: the list was global, the tab was global, and deleting a session left its servers running. Todo and the task bar were also two stacked surfaces, so a running task competed with the plan and ask rows for the same vertical space. Tasks now carry a per-session concurrency limit of 10, refusals name that limit, a process-wide ceiling stays only as a host safety net, and a spawn is rejected when its session is already gone. Deleting a session signals its running tasks through the existing per-session cleanup path; archiving does not, because archiving is reversible. Both are covered by host tests. The renderer keeps the wire contract session-agnostic and filters locally, so switching sessions changes only what is displayed. Todo and tasks share one container whose header carries the progress summary, the live count, the tab list and the disclosure: collapsing hides only the panels, so tabs stay reachable, and a newly started task raises a badge on its tab instead of stealing the tab. Removal is a view action gated on a terminal state in the state layer, so a running or still-settling task can never be dropped from the list while its process lives. * docs(chat): record the task-dock plan behind the shared container The layout was settled by building it and clicking through it, so the reasoning belongs with the change rather than in a chat log. The interactive prototypes live outside the repository and are linked from the pull request. The plan records the decisions that shaped the result and the ones rejected along the way: why the per-session limit is 10 while a process-wide ceiling stays behind it, why the wire contract keeps returning every task and the renderer filters, why deletion signals stop while archiving does not, and why the header became a row of siblings instead of one button. * docs(chat): point the plan at the uploaded prototype The prototypes are an uploaded artifact, not repository content, so the plan must not describe a docs/demos tree that does not exist. It now links the single chosen page and records which alternatives it beat and why: an inline tab list in the header won because it is the only form that keeps the tabs reachable while collapsed without introducing a second header state. * fix(chat): expand the dock when a collapsed tab is clicked The container advertised a behaviour it did not implement: clicking a tab while collapsed only swapped the remembered tab, so the panel stayed hidden and the click looked like it did nothing (D15). A tab click now goes through selectTab, which writes the tab and the expansion together, making the user's intent one operation instead of two that could disagree. The badge rule also decided visibility with its own copy of the tab logic and passed a hard rue for the todo side, so it could disagree with the panel actually on screen. Both paths now call the same rule, and the hook takes the todo side from the container that already reads it. dock-state.ts had no direct tests, which is how the two defects survived. It now has a suite covering the collapse-tab rule, the fallback for an emptied side, and the badge/visibility agreement. --------- Co-authored-by: v_wyxgwang <v_wyxgwang@tencent.com> * chore(release): v0.21.0-beta.8 (#1854) * docs(changelog): note session-scoped tasks and the shared dock The 0.21.0 catalog is the in-app whats-new source for the beta that ships these two user-visible changes, so testers can read them without a network fetch. English stays the source of truth and the other eight catalogs carry the same two entries in the same position. * chore(release): v0.21.0-beta.8 * fix(models): place saved-key clear icon beside the input Keep credential controls on one row instead of spending another row on key removal. Retain the localized tooltip, accessible name, and existing staged credential-removal behavior. Exercise the rendered icon geometry, target size, focus restoration, and localized setup journeys. * fix(transcript): continue restored reading ranges automatically (#1850) Restored reading ranges should preserve their bookmark without leaving readers behind a manual history paging control. Continue bounded pages near the viewport boundary and rejoin live output only after the gap closes. Keep explicit search paging isolated, preserve the viewport and model input, and cover continuation, retries, and hidden-pane cleanup. Co-authored-by: Lan <48862574+vastsa@users.noreply.github.com> * fix(provider): send reasoning_effort to official DeepSeek endpoints (#1849) * fix(provider): send reasoning_effort to official DeepSeek endpoints deepseekRequestCompat() forced supportsReasoningEffort off for every DeepSeek-family Completions row so relays would stop rejecting the field (#1701 / #1664). Official api.deepseek.com rows document reasoning_effort next to thinking, so they only ever sent the thinking toggle and low, high, and max all produced the same request — the selected level was silently ignored (#1821). Keep the suppression for non-official endpoints, where the field was reported rejected, and leave official rows to pi-ai's URL detection. The per-model supportsReasoningEffort selection added for #1846 still wins in both directions, so a user can omit the field on an official row too. Probed api.deepseek.com live with deepseek-flash and deepseek-v4-pro: every level the runtime can send (low, medium, high, xhigh, max) returns 200 and the request body carries the selected value; off sends only thinking: {type: "disabled"}. Recorded as D672 amending D389. refs #1821 * chore(ci): rerun checks after a runner-side Electron E2E hang The previous run failed only in the trusted-extensions Electron E2E at scripts/e2e-provider-client-certificate.mjs: the mTLS fixture probe hung and was killed by SIGKILL after 65s. The same job on main prints the same "Electron 43 net.fetch failed before HTTP" baseline line and passes, so the failure is runner-side rather than caused by this branch. Rerun is not available to this account (workflow_dispatch and run rerun both require repository admin rights), so this empty commit retriggers the workflow without changing any content. * fix(provider): match official DeepSeek hosts precisely Substring matching can classify a relay as an official DeepSeek endpoint and disable the relay compatibility guard. Match parsed hostnames so only deepseek.com and its subdomains use the official endpoint behavior. --------- Co-authored-by: Lan <48862574+vastsa@users.noreply.github.com> Co-authored-by: vastsa <hhxk666@gmail.com> * fix(chat): 切回缓存会话时保留阅读页,修复误显示【加载后续消息】(#1851) (#1852) * fix(chat): keep the returned reading page across session revalidation Switching back to a retained session committed its cached page and then replaced it with a bounded history read around the bookmark, because the "bookmark already loaded" short-circuit was gated on selectingSessionId not matching the target session. During a warm return that condition is always false, so the 60-row window replaced the 100-row page and painted the load-later affordance even though no newer row was missing, while live turns arriving afterwards were not merged. Publish the retained page as the reading range when it already holds the bookmark, which keeps the anchor and the reader's offset across the revalidation swap. Fall back to the bounded read only when the anchor is outside the loaded page, as ADR 0137 intends. fixes #1851 * fix(chat): preserve active transcript navigation Automatic reading position restoration must not replace an explicit search or navigation that already owns the transcript view. Check focused or loading state before accepting a bookmark already present in the current page. --------- Co-authored-by: vastsa <hhxk666@gmail.com> * fix(transcript): make automatic folding deterministic A pointer press or a nested disclosure toggle recorded every enclosing level's current state as a permanent choice, so automatic folding stopped applying for the rest of the pane's life and a reveal could pin a level closed. Activity groups also folded as soon as they stopped owning the live segment, hiding work mid-run and folding away reasoning the reader had just asked to see. Disclosure ownership is split in two: an explicit header or rail choice stays local to its level, while focus or a selection holds a level and its ancestors open only until the reader leaves. Group defaults now follow the turn, and visible reasoning keeps its group open. The activity issue counter becomes one inline chip instead of an unstyled span that wrapped its icon and count in narrow bands. * fix(composer): keep the task dock collapse across session switches The Todo/background-task container stored its collapse inside the per-session entry, so every session the user switched to started from its own default and reopened a container they had just closed. Move the collapse out of the per-session state: it is one container-wide condition that survives session switches, while the tab choice and the badge stay per session. The container now also starts collapsed, so the composer keeps its room until the user opens it, and the state is no longer pruned together with dead sessions. * chore(release): v0.21.0 Promote the 0.21 preview line to a stable desktop release. Align all workspace version surfaces, refresh the bundled model catalog, and complete the shipped release notes in every supported locale. * fix(agent-runtime): do not treat connection abort wording as user Stop (#1865) A bare abort match classified socket and stream failures such as "software caused connection abort" and "stream aborted" as TURN_ABORTED. Those turns skipped provider retry and rendered as a user Stop. Message text is a cancellation only for the fixed runtime and AbortError phrases. Connection-abort wording stays a retriable NETWORK_ERROR, and stream-abort wording stays a retriable STREAM_FAILED. Co-authored-by: xpeng5278-web <330743750+xpeng5278-web@users.noreply.github.com> * fix(file-manager): theme editor search controls (#1861) CodeMirror kept light control defaults while the dark file view inherited white text, making find and replace content invisible. Rebuild the vendored view with explicit theme surfaces, ship a bounded hotfix version, and cover the light/dark user path in Electron. Co-authored-by: vastsa <hhxk666@gmail.com> --------- Co-authored-by: liubai <liubai0106@hotmail.com> Co-authored-by: zszz3 <91608029+zszz3@users.noreply.github.com> Co-authored-by: blackEvil217 <1278844978@qq.com> Co-authored-by: muzimu217 <muzimu217@users.noreply.github.com> Co-authored-by: CJLY-0915 <CJLY-0915@users.noreply.github.com> Co-authored-by: spacker-343 <67576022+spacker-343@users.noreply.github.com> Co-authored-by: v_wyxgwang <v_wyxgwang@tencent.com> Co-authored-by: luoshang <luoshang@local> Co-authored-by: Lan <48862574+vastsa@users.noreply.github.com> Co-authored-by: vastsa <hhxk666@gmail.com> Co-authored-by: liuzixiang <zixiang326@163.com> Co-authored-by: xieqiying <xiexiexienila@outlook.com> Co-authored-by: Xuese <3275996223@qq.com> Co-authored-by: TioitWang <104554654+Tioit-Wang@users.noreply.github.com> Co-authored-by: Seraph-x <zzs012310@163.com> Co-authored-by: Tony <13052086+AuhNijGnahz@users.noreply.github.com> Co-authored-by: new-Beginner <100430809+new-Beginner@users.noreply.github.com> Co-authored-by: xpeng5278-web <xpeng5278@gmail.com> Co-authored-by: xpeng5278-web <330743750+xpeng5278-web@users.noreply.github.com> Co-authored-by: Tony <zhangjinhua@fastmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow up on my reading-position restoration change in #1665 without reverting bookmark restoration.
Restoring an evicted session loads a bounded range around its bookmark. That range was treated as a permanently frozen historical snapshot, while automatic paging only handled older messages. This left ordinary restored reading behind a Load later messages button and prevented it from naturally rejoining live output.
Verification
Task candidate:
a3306d456e36d6f0bd8d2d0eb48e1488bbbd6f2cBase main:
cdad68e61e504adbfd1af10492a338daec09bdfaEnvironment: Windows; isolated Electron fixture, no running user instance or real model provider.
Passed on the final candidate:
pnpm --filter @pi-desktop/desktop build(chunk-size warnings only)pnpm --filter @pi-desktop/desktop typecheckpnpm lintnode --test apps/desktop/test/transcript-*.test.mjs apps/desktop/test/session-search*.test.mjs— 163 passed, 0 failednode scripts/e2e-transcript-reading-position.mjs— scrolling, real SessionPane restoration/continuation, and continuation lifecycle probes all passedgit diff --checkandpnpm check:pr-baseFull-suite limitation
A broader Windows desktop test run during implementation was not green: 4,281 passed, 64 failed, 8 skipped. Failures included path/permission, SSH/askpass/host fixtures, and longer user-path tests. Two updater path failures were independently reproduced against unchanged main; the remaining failures have not all been baseline-verified and are not claimed to be pre-existing. The focused transcript/search suite is green, but upstream integration CI is still required before merge.