A lost or delayed turn-ending `session.idle` left the busy spinner up until
the watchdog poll caught it (5-10s). An assistant `message.updated` carrying
`time.completed` now schedules one status check for that session, and
`streaming.ts` stops treating a completed trailing message as streaming.
The check is deferred by 750ms and re-reads the session status when the timer
fires, so the overwhelmingly common case — the turn's own `session.idle`
arriving right behind the completed message — settles on its own and costs
zero extra requests; only a session the store still believes busy spends a
fetch. The poll shares the watchdog's in-flight directory guard, so the
deferred check and the periodic poll cannot overlap on one directory.
Status authority is unchanged: the monotonic pass never lowers status, and an
authoritative resync runs only when the snapshot disagrees.
Post-merge hardening of the session-to-worktree move (#2998), driven by
review findings on the follow-up pass:
- an ambiguous transport failure (relay abort, timeout) on the
change-carrying move no longer force-deletes the fresh worktree that
may hold the user's only copy of their changes; both intent kinds
surface honest guidance and refresh both directories
- assertSdkSuccess re-tags ambiguous transport errors when wrapping SDK
failures, so ambiguity classification survives the wrapper on every
path, matching the prompt-send precedent
- session liveness checks scan all child stores plus the global status
index, and report unknown (not idle) when no store covers the session
— an evicted background directory can no longer make a busy session
look movable
- incomplete-rollback errors carry the changes-may-be-in-destination
guidance instead of swallowing it
- move-message assembly shared across the three call sites; tests now
exercise the real ambiguity classifier (extracted to
send-failure-classification.ts) instead of a hand-mirrored mock
- i18n fallout from the merge train: Turkish gains the 21 worktree-move
keys, all 12 locales get the hedged ambiguous-failure toast; owning
DOCUMENTATION.md files record the new contracts
Follow-ups promised on merge, plus review findings on the batch itself:
- chat: task-tool output now respects the 512KiB render cap; quick-open
icon is visible at rest on coarse pointers and reachable by keyboard
(row keydown no longer swallows inner-button Enter/Space); composer
inline-code decoration drops the metric-shifting padding; a btw fork
send carries only the boundary instruction, never the promotion notice
- sync: cascade revert/unrevert aborts busy descendants, busy state is
read from every child store at the moment of use; rule 9 documents
redo clearing all descendant revert markers
- electron: renderer recovery keeps memory-eviction (a valid
render-process-gone reason) and both windows share one
attachRendererRecovery helper
- vscode: process registry is a thin re-export of the web module
(provider-env-aliases precedent) with ordered register/unregister
writes and an awaited close
- server/cli: managed-process registry takes injectable deps (fixes the
unreaped-orphans ReferenceError), corrupt settings errors name the
file, getWorktrees test restores console.warn
- tests: module-mock harnesses removed (AgentsSidebar, SettingsView
mobile focus — behaviors stay live but uncovered, accepted trade),
QuestionMarkdown asserts rendered DOM
- i18n: German gains the debug-panel request keys, Japanese/German drop
removed worktree keys, Ukrainian unit spacing fixed
- changelog: Copilot AI Credits entries (main + VS Code)
## Problem
Review on #3151: with isCurrent reduced to store liveness, a completed
run's context stays current while a forced same-directory rerun starts,
so a late deferred response could commit over the newer run's state.
## Fix
Track a per-directory run sequence. isCurrent captures the sequence at
run start and stays true across settle (deferred recovery pulls still
commit — the original bug) but flips false the moment a newer run for
the same directory begins. Entries are cleared on directory disposal.
## Validation
- New test: after a forced rerun the first context is retired and the
second is current. 20 pass in child-store.test.ts.
- bun test src/sync/: 540 pass / 15 fail — same 15 pre-existing on main.
- type-check and oxlint clean on authored lines.
## Problem
Pending permission requests (and questions, MCP/LSP/VCS status) vanish
permanently after a page reload. The deferred recovery phase in
bootstrapDirectory — the code that re-pulls permission.list after load —
never executed, so the UI had no way to re-render a card for a request
the opencode server still holds. Fixes#3150.
## Root cause
The deferred phase is scheduled via setTimeout(0) guarded by isStale(),
which maps to the pump's isCurrent(). isCurrent required the run token
to still be present in runningBootstraps, but the pump deletes that
token in .finally() as soon as onBootstrap settles — always before the
setTimeout macrotask fires. The guard was therefore deterministically
stale and the phase was dead code (introduced by 85400459).
## Fix
Make isCurrent a store-liveness check — disposed, generation, and store
identity — instead of run-token ownership. The pump never replaces a
running entry for the same directory (queueBootstrap defers via
rerunRequested), so during the run itself this is equivalent. This
mirrors the existing isCurrent contract in session-message-loader.ts.
## Validation
- New regression test in child-store.test.ts fails on main and passes
with the fix (isCurrent stays true across the post-settle macrotask,
flips false after teardown).
- bun test src/sync/: 539 pass / 15 fail — the same 15 fail on pristine
main; the only delta is the new passing test.
- bun run type-check (packages/ui): pass.
- bunx oxlint on both changed files: no findings on authored lines.
The global session-status store patched its own setState to derive
active membership for callers replacing statusById — a boundary that
silently trusted any caller passing both fields to keep them consistent.
replaceGlobalSessionStatusById is now the one sanctioned way to swap the
map from outside the event reducers; the runtime-switch reset and the
tests that replaced the map directly go through it, and the patch is
gone. Follow-up to #3126 review.
Main replaced the chat timeline scroll engine while this branch was in
flight, which obsoletes two of its subareas and reshapes a third:
- Chat timeline: main's LegendList-based MessageList/ChatContainer win;
the activation-overscan staircase targeted the removed tanstack path
(LegendList provides adaptive rendering natively) and is dropped along
with its test.
- Scroll shadows: main's hook-based masks stay (the virtualized list owns
its scroll element — there is no wrapper to hand the styling to); the
viewport-wrapper ScrollShadow rewrite, its index.css replacement, its
test, and the call-site viewportClassName adaptations are reverted to
main. The chat OverlayScrollbar keeps this branch's disableHorizontal.
- OverlayScrollbar: the direct-DOM rewrite lands, but binding now follows
the live container node instead of binding once per ref object — the
chat scroller remounts on every session switch, and a bind-once
contract left the scrollbar attached to a dead element.
- Markdown renderer: the detached-DOM cache and warm-block fast path
merge with main's block-commit reveal (enter cascade), streaming code
highlighting, and gutter reservation; the per-block reconcile keeps
both the decoration-refresh path and the reveal cascade.
Keep sidebar rows independent from the active sync directory by moving export history loading behind an explicit-directory command.
Make active-project selection preserve project topology and remove activeProjectId from mounted group render props. Reuse a per-session-array ID index so subscription snapshots do not repeatedly scan session lists.
Batch global session, status, ordering, and activity timing mutations at the existing directory event boundary so large subagent bursts publish each owner once.
Maintain active session roots, children, and directory buckets in the global store, reuse them in Sidebar projections, and avoid rebuilding live aggregates and structural data for unrelated renders while preserving authoritative ordering reconciliation.
The streaming tail only overlaid the actively streaming message with live
parts from the sync store. When a turn moved to its next step message, the
previous message fell back to its lagging base record, briefly dropping its
completed tool parts — remounting them and replaying their reveal
animation once the record caught up.
Overlay live parts for every message of the streaming tail via a new
useSessionPartsForMessages hook, guarded so an empty live array never
erases parts the record does have. Also key generate-effect glyphs by
index so appended text does not replay earlier characters, and keep tool
row reveal wrappers mounted unconditionally.
Every user-attached context item (diff/file/plan comments, terminal
selections, browser annotations, PR comments and failed checks, linked
issues/PRs, and new chat-quote comments from the selection menu) is now
sent as its own synthetic text part carrying an openchamberContext
metadata payload. The model-facing text keeps the previous wording; the
timeline reads the metadata back and renders each item as a context card
instead of raw prompt text. Legacy messages still render via the old
text sniffing.
The selection menu gains a Comment option with an inline multiline
input, the quoted fragment stays highlighted while commenting, and on
mobile the input overlays the composer pill by rendering inside the
composer form. Add to chat is renamed Add to input; the menu is
restyled and the mobile Copy tile removed. Terminal drafts move their
terminal id out of the language field (persisted-draft migration v3),
and the dead preview-console source is deleted.
* feat(chat): /btw command — side questions in a temporary forked session
/btw <question> forks the current session (full context inheritance) and
opens a compact peek panel docked above the composer. The composer itself
becomes the btw input while the panel is open: sends route to the fork,
the placeholder and a mode chip reflect the target, and the stop button
aborts the fork's turn. Closing the panel (or the chip's ✕) destroys the
fork, leaving the main conversation untouched.
The panel shows only the fork's own tail (messages at/after the fork
creation time) and live permission/question cards scoped to the fork.
- chat/btw/BtwPanel: peek sheet (desktop + mobile), fork-tail view,
auto-close on disappearance, Esc to close
- lib/btw: startBtwSession (fork + rename + routed send), closeBtwPanel
(close = destroy), filterBtwTailMessages
- ChatInput: btw-mode send routing via SendMessageOptions.sessionId,
btw-aware activity (stop/abort), placeholder + mode chip
- useSessionActivity: exported for per-session activity reads
- i18n: btw keys across all 11 locales
* fix(chat): keep btw sends isolated
* refactor(chat): rework /btw into a metadata-scoped peek panel
- Link the active btw fork through the parent session's metadata
(openchamber.btwSessionID) so the panel exists only in the session that
invoked /btw, follows parent navigation, and survives reloads; the fork
carries a kind:'btw' marker with its originalSessionID.
- Replace the wall-clock history boundary with the id of the newest cloned
message (server-generated ascending ids), stored in fork metadata.
- Derive panel identity in useBtwPanelState; useBtwStore shrinks to
transient per-parent UI state (collapsed/creating/destroying).
- Panel UX: dropdown-style glass surface, chat ScrollShadow, single
title+chevron collapse toggle, muted header controls, promote action
(keep as a full session and navigate to it), Esc collapses instead of
destroying, reserved Working indicator row, streaming auto-follow via
ResizeObserver keyed on content readiness.
- Add a 'peek' chat surface mode that suppresses per-message controls and
turn footers inside the panel; user bubbles keep a small gap below.
- Hide btw forks from the sidebar, session switcher, and command palette
until promoted; mark the fork before inserting it into local stores.
- Delete/archive lifecycle: removing the fork unlinks the parent; removing
the parent also removes its temporary fork.
- patchSessionMetadata now mirrors updated sessions into live stores.
- Localize new strings across all 12 dictionaries; add unit tests for
metadata helpers, the btw flow, and the UI store.
* fix(chat): clamp the btw panel below the app header when the keyboard is open
Reuse useMobileAutocompleteMaxHeight (the composer autocomplete precedent)
on the panel's scroll body, reserving the panel header and bottom spacer
height, so the sheet adapts to the visual viewport instead of riding under
the app header on mobile.
* fix(lint): drop unused destructured bindings in sessionBtwMetadata
CI eslint has no underscore ignore pattern; strip metadata keys with typed
copies and delete instead of discard-destructuring.
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Neither failure came from the change that surfaced them; both suites were
asserting behavior the product had already moved.
issue-2039 mocks `session-actions`, and session creation's authoritative
steps live there: the created session becomes current under the directory
the server confirmed, and that directory enters sync's routing index. The
mock did neither, so every assertion about current session and directory
routing read an empty store. It now performs both, which is what makes
those assertions test the store's real resolution order again.
issue-2903 pinned `<ChatEmptyState />` inside ChatContainer's empty-and-idle
branch. The draft transition animation moved that surface to the draft, and
the branch now returns a neutral placeholder. What the test is actually for
— the idle branch not rendering a status row of its own — is kept.
A project could pin the model new chats start on, but not the level to
run it at: the default cascade dropped any variant as soon as a project
model won, and only ever considered the global one — which belongs to
the global model.
Projects now carry `defaultVariant` alongside `defaultModel`, stored and
sanitized only next to that model, and the cascade passes it through.
Both controls sit in one "Defaults for new chats" group laid out like the
Sessions defaults, and the level appears only for models that offer them.
The server echo of an optimistic user message carries different part
ids, so the reducer dropped the optimistic part and appended the server
one at the end, and MessageBody keyed user parts by part id. The key
change remounted the text subtree (blank frame, markdown re-parse,
truncation state reset) and the append reordered text against file
parts.
Replace the optimistic part in place and key user parts positionally.
Render managed Chats through the shared folder-aware session group and keep folder ownership at the Chats root, including new drafts and older per-directory scopes.
Hide worktree actions for Chats, make worktree shortcuts inert on Chat drafts, and let new-session shortcuts inherit an active session directory while explicit Chats controls still create managed Chat drafts.
Create projectless chat sessions under a managed, date-scoped Chats directory and clean abandoned or deleted session folders.
Add Chats to sidebar state, startup cache, shared context, and Electron Mini Chat while keeping VS Code project-only. Resolve managed chat directories to one server-side memory owner and document the runtime contracts.
* fix: reconcile busy sessions after managed OpenCode restart
Forced health-check restarts previously rebound the event stream without
settling in-flight turns, so sessions stayed busy with no terminal state.
Interrupt those sessions, classify health failures, and retain bounded
process diagnostics for post-restart diagnosis.
Fixes#2943
Co-authored-by: serkraser <serkraser@gmail.com>
* fix: surface interrupted chats after OpenCode restart
Complete unfinished assistant turns as aborted once the session is
authoritatively idle, and show a persistent toast so users can continue
instead of remaining silently stranded.
Fixes#2943
Co-authored-by: serkraser <serkraser@gmail.com>
* fix: redact Basic auth credentials in restart diagnostics
The key/value sanitizer stopped at whitespace, so Authorization: Basic
credentials survived in stderr tails and health snapshots. Redact the
scheme token before that rule runs.
Co-authored-by: serkraser <serkraser@gmail.com>
The panel stored notes, todos and plans inside one shared JSON file that
six unrelated domains also wrote to, synchronised itself through window
CustomEvents, and could only read plans. It is now Project knowledge:
server-owned storage with explicit routes, a store with rollback, a
section sidebar, plans that open and edit in place, and search across
all of it.
Notes and plans the user pins travel with every message sent in that
project. Pinning is project state, not an attachment to one message, so
it holds until unpinned and the work status panel names what is riding
along and can detach it.
Agent memory is added alongside, in two scopes: what is true about the
user, and what is true about this codebase. The split is not cosmetic —
a wrong project fact costs one project and is noticed, while a wrong
global fact quietly shapes every session everywhere and the user has no
code to check it against. It stays separate from notes so an agent
mistake cannot land in what the user wrote. Sessions receive an index of
titles only; bodies are read on demand, because an index carrying full
text grows until it crowds out the conversation.
Deciding what a session must be told, and whether it has been told, now
lives on the server. The client owned it before, which meant sessions
started without a UI — scheduled tasks, sessions the agent dispatches —
received nothing at all, and a tab's record of what it had sent outlived
the conversation: after compaction the agent no longer held the block
while the tab went on believing it did. What was delivered is recorded
in the session's own metadata, and compaction restores it through the
runtime that already restores pinned messages, in the same turn.
Agent memory ships dark behind OPENCHAMBER_MEMORY_ENABLE: unset, there
is no tool, no routes, no session index, no settings row and no panel
tab. Absent rather than switched off, so nothing invites turning on a
feature that has not been announced. Pinned notes and plans are
unaffected and ship as normal.
The SDK client fetch wrapper now applies a 30s timeout to non-streaming
reads. Without it, a socket that neither resolves nor rejects keeps the
directory bootstrap concurrency slot busy forever and the UI stays on
"loading sessions". Long-lived streams (POST prompts, the /event SSE)
are explicitly excluded so they are not cut off mid-flight.
The normalized "request timed out" error is added to the retry
allowlist alongside undici's "terminated" (the exact failure observed
in #2470 when undici tears down a half-open upstream connection); both
are transient while the managed OpenCode process restarts. Caller-
initiated aborts keep their original error shape so a user-cancelled
request is not retried.
Tests cover: GET timeout fires after the bound, POST is not timed out,
/event SSE is not timed out, caller abort wins, AbortError is not
retried, and the SDK normalized error is retried 3x.
The token breakdown of an assistant message accumulates across every API
round-trip inside the turn: each tool call re-reads the whole cached
prompt, so input/cache.read add up to several times the context window.
Every context-usage surface summed those fields, which is why the meter
could read 330% of a 1M window whose real fill was 232,872 tokens
(23.3%), and why reopening an older session jumps the readout (#2562).
The server reports the final round-trip's window as tokens.total
(optional in the message schema; opencode 1.18.18 returns it, verified
against its live /session/:id/message API). Prefer it everywhere the
window fill is displayed and fall back to summing only when the server
did not send it: contextTokensFromBreakdown in tokenUtils now owns that
rule, and the context store extractor, sync store getter, work status
panel, context sidebar, VS Code layout, mini chat, and mobile metadata
all use it instead of their own inline sums.
Fixes#2562
Create no longer aborts when recoverStaleDraftDirectory rewrites the
implicit new-chat draft to the active project during the create probe.
Also rank the Unreleased Chat bullet below the changelog highlights.
Co-authored-by: serkraser <serkraser@gmail.com>
When a regular new-chat draft inherits a deleted lastDirectory, update
the visible draft target to the active project immediately. lastDirectory
still stays unchanged until session creation succeeds.
Co-authored-by: serkraser <serkraser@gmail.com>
New chats inherited a persisted lastDirectory even after that worktree
was removed, so the first message saved but the prompt never started.
Validate the implicit draft directory, fall back to the active project
only when OpenCode confirms the path is missing, and leave explicit
worktree targets and unknown probes unchanged.
Co-authored-by: serkraser <serkraser@gmail.com>
* chore: remove verified dead declarations
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: narrow unused internal exports
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: remove newly exposed dead helpers
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: remove unused deep-link serializer
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: drop two tests that assert on copies of the code
mainLayoutMobileSidebarMount read MainLayout.tsx and SessionSidebar.tsx as
strings and asserted on source substrings down to exact indentation, so it
failed on formatting rather than behaviour. useProjectSessionSelection.test
reimplemented the hook's visitNodes logic inside the test file and asserted
against that copy, so it could not observe the hook at all.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: repair sync suites that had rotted while unrunnable
No runner executed packages/ui, so these drifted from the source unnoticed:
two imported helpers that are no longer exported, one directory-store stub
predated the session field routeMessage reads, and the WebSocket fake missed
the mandatory url-token mint plus the close event the socket wrapper reads.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: stop the web suite failing on timeouts and a hand-copied mock
The Git suites drive a real git binary, so the 5s default made a valid suite
fail differently per run. The gitApiHttp mock listed ~70 export names by hand
and fell behind the source; it now derives every stub from the real module,
which the added shared-UI aliases make resolvable.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: run every suite from one command and in CI
packages/ui (232 files) and packages/vscode (22) had no test script at all, CI
ran neither, and 9 vscode files could never run because Node cannot resolve
their extensionless TypeScript imports. Three electron files sat outside every
script list, one of them importing vitest, which that package does not depend
on. A runner gives each file its own process, since these suites keep
module-level singletons and fail by load order when sharing one.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* chore: delete a superseded repro harness and a completed plan
The issue-2638 harness needed lsof, overrode process.platform and spawned real
servers, and nothing referenced it; event-stream/rebind.test.js now covers the
same hub-pinned-to-the-old-port behaviour. The pairing v2 plan described relay
and the pairing UI as out of scope, both of which shipped.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* docs: point at the theme tools and record the github barrel invariant
convert-vscode-theme and harmonize-theme were referenced nowhere, so the
theme-authoring reference now names them. The github barrel is loaded through
await import('./index.js') and destructured per route, which no static report
can see; documenting that is what stops the next cleanup from deleting it.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* test: repair merge drift in bridge and route-registry mocks
upstream/main gained upsertProviderConfig on bridge-system-runtime and a
PATCH scheduled-task route after this branch forked. Their test doubles
were never updated to match:
- bridge-system-runtime.test.js: add upsertProviderConfig to the
opencodeConfig mock so the import resolves.
- sse-routes.test.js: add app.patch to the route registry stub.
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Snapshot the new-session draft (and keep the existing-session target captured) at
submit time, then use that snapshot for draft materialization and routing instead
of re-reading live selection state after async preparation.
Fixes#2222Fixes#2315