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
Adds a work-status panel beside the transcript. Context fill, model and
cost, todos, running subagents and the permission requests blocking
them, branch and working-tree state, MCP servers, pinned messages and
context sources were scattered across the header, the composer and the
context panel — a blocked subagent was reported nowhere at all. The
panel reads them from live channels rather than persisted history, and
becomes an overlay where the chat is too narrow to seat a column.
It is on by default, including for existing installs. Because it now
carries these readouts, the desktop header and composer drop the ones it
duplicates: todo and changed-files chips, usage and MCP tabs. VS Code
and mobile keep theirs — neither hosts the panel.
Fixes MCP authorization, which was broken from the panel, invalidated by
a directory switch through a redirect URI that encoded the working
directory, and left the desktop app in the background because browsers
will not follow a custom-protocol link without a user gesture. The
settings page no longer asks the user to understand the MCP spec before
adding a server: one field takes the command or the link, with the kind
inferred and a visible override, and client-registration fields appear
only when a server actually asks for its own credentials.
Also: skills load from the panel instead of only when the composer's
slash autocomplete opens; the header button names the current instance
rather than falling through to the word "Instance" for relay hosts.
Three new optional UI settings keys, all migrated. No change to stored
MCP server configuration.
When a document (PPTX, DOCX, XLSX, ODP, ODT, ODS) is attached,
extracted slide images were left orphaned after removing the
parent text entry. This adds a sourceDocumentId field to
AttachedFile that links all entries from the same document
extraction, and cascades removal of all entries in the group
when any one is removed.
Fixes#2426
Co-authored-by: chiamsun <chiamsun@users.noreply.github.com>
When a managed OpenCode process dies mid-turn (crash, health-check
restart), the persisted turn never settles: the trailing assistant
message has no time.completed and its tool parts stay pending/running
forever — the server never finalizes them (anomalyco/opencode#19023).
The existing settle-triggered tail refresh refetches the same stale
records, so the UI kept running tool timers and working styling
indefinitely (#2577).
Now, when a session is authoritatively settled (session.idle/
session.error event, or an authoritative status snapshot lowering a
previously busy session) and the trailing assistant message is still
unfinished with active tool parts and no pending question/permission,
the orphaned parts are finalized locally as error/"Interrupted" with
an end time — the same shape OpenCode itself writes for cancelled
tools. The mark is gated on an explicit idle status (absent status is
"unknown", never judged), never applies while busy (including
question/permission waits), and a later terminal event or refresh
supersedes it while a stale running refresh cannot regress it (the
reducer and materializer already preserve final statuses).
Fixes#2577
Adds a per-session pending-question badge to sidebar rows, driven by the
live directory-store question state through a dedicated per-session
subscription channel so unrelated streaming never re-renders rows.
Collapsed parent rows roll up pending questions of hidden descendants
from their owning directory stores without bootstrapping them. Question
state is cloned on session delete/archive so badges clear when sessions
disappear. Adds the questionChangeCallbacks sync performance counter,
i18n keys for all locales, and unit tests for the subscription channel
and scope selection.
Fixes#2634
Answering a question tool (or a permission prompt) could leave the session
permanently stuck on "asking question": resolveDirectoryForBlockingRequest
returned the containing child-store key, which only proves containment.
For a worktree session (or any session whose record is grouped under a
parent project store), the reply was addressed to the parent directory's
OpenCode instance, where the pending request does not exist - the server
answered QuestionNotFoundError, the local request was removed, and the
trailing question-tool part stayed running with no recovery until Stop.
Resolve the directory from the request's own session record (server-
confirmed ownership: session.directory, then project.worktree) before
falling back to the containing store key. When a reply/reject comes back
not-found, also enqueue the settled-running-tool tail materialization so
the tool part converges to the server's actual state instead of leaving
the UI stuck.
Refs OPE-236
The suite mocks `session-actions` by listing its exports one by one, and had
fallen behind `unarchiveSession`/`unarchiveSessions`. `session-ui-store` imports
both, so the file threw on import and ran zero tests — the draft auto-accept and
canonical-worktree-directory guarantees it covers were unprotected, and the
report looked almost like silence rather than a failure.