- ChatContainer swaps the timeline on a deferred copy of the selection so the
active row, URL, and tab commit first. (This part had been overwritten by a
concurrent edit before the previous commit and is restored here.)
- A session whose messages are not in memory keeps the previous conversation
on screen for up to 400ms while they load, instead of flashing a skeleton
between two conversations.
- The reveal fade runs only for a session the user waited for and is 100ms;
a session that was ready at the click appears in the same frame.
- The sidebar prefetches the two rows on either side of the open session
150ms after it settles, two at a time, so neighbouring switches are warm.
Switching sessions ran as one synchronous commit: sidebar highlight, URL,
a full timeline remount with markdown re-parse, and around nine requests,
so nothing changed on screen for 150-250ms after the click.
- ChatContainer swaps the timeline on a deferred copy of the selection, so
the active row, URL, and tab commit first and the timeline renders behind
them; selection policy keeps reading the live store value.
- The message fetch starts before the selection is published.
- Sidebar rows stop re-rendering on a project switch: directory-scoped sync
hooks read the runtime context and a subscribable current-directory source
instead of the directory-bearing context; the grouping builder reads git
branches through a ref and section caches key the branches they use;
descendant ids are keyed by content. Rows per switch went from 73 to 8.
- Markdown skips the async re-render when the settled cached blocks are
already painted, and mounts synchronously once its lazy module is loaded;
the module is preloaded at boot.
- A timeline reveal gate holds a freshly opened session at opacity 0 while
any provisional markdown paint catches up (250ms cap), then fades the whole
timeline in once, so text, tools, and recap appear together.
- Switch fan-out trimmed: knowledge summary deduped, MCP status refreshed only
when stale, non-repo directories cached by the git repo check, OpenChamber
defaults cached briefly, agent memory reused for the same project, goal
text cached, PWA manifest rebuilt after the switch settles.
- Header tabs snap into the active state and keep the title at the same
height in both states.
- Prefetch on row press; composer focus moved off the commit.
`bun run profile:switch` records ack/content latency, longest task, and
requests per switch, cold and warm, and compares runs against a baseline.
Measured warm switch: ack 228ms to about 40-60ms, content 228ms to about
100-120ms.
* fix(sidebar): search the managed chats along with project sessions
The sidebar builds search data per group, and every group renders
`filteredNodes ?? []` while a query is active. Only project groups were ever
given that data, so the managed chats — which render outside any project
section — collapsed to an empty list as soon as anything was typed, however
well a title matched. The header count ignored them for the same reason.
Pass the chats group to the sections hook as a standalone group so it takes
the same search pass and joins the match count. Building it before the hook
runs keeps search-data ownership in one place instead of registering entries
from the rendering component.
No CHANGELOG entry: it is left out deliberately to keep this branch free of
conflicts with other open PRs, and is to be written at release time.
fixes#3200
* fix(sidebar): report the real search match count to the header
The header rendered whatever `SessionSidebar` passed it, and that was a
literal `0` — so "0 matches" was shown no matter how many sessions matched.
The count exists: the sections hook already computes it, but only the session
list can see it, and the header renders above the list.
Report it upwards from the list instead of recomputing it in the header, so
the number and the visible rows cannot disagree, and reset it to 0 when the
list unmounts.
fixes#3200
* fix(sidebar): keep chat-only search results on screen
The chats render inside the scroller's top content, and the branch that
handles "no project section matched" replaced the whole list with the empty
state. A query matching only a chat therefore showed "No matching sessions"
while the header — now reporting the real count — said one match, and the
chat itself stayed hidden. That is the exact reproduction in the issue; the
earlier manual check missed it because the query used also matched a project
session.
Tell the scroller when its top content holds results, and keep it on screen
in that branch. The decision moves to `sessionProjectRender` next to the
other render-selection helpers, where it can be tested without mounting the
Vite-only component graph.
fixes#3200
Post-merge follow-ups for #2740#2735#2734#2690#2676#2738#2684#2689#2733#2739#2462#2687#2736#2618#2697, plus three regressions
found while reviewing them:
- ctrl/cmd+digit while typing no longer switches session tabs (#2503 was
still open in practice: the guard only covered the mod+alt surface binding)
- Shiki template-call sanitizer now covers every bundled grammar, including
the js/ts aliases and embedding grammars; timed-out highlight requests are
memoized and no longer cancel unrelated in-flight requests
- settings flush on suspend uses keepalive and also fires on Capacitor
appStateChange; keeps the selected model persisted across mode switches
- remote-only branches fetch before checkout; range helpers fail clearly
- git status invalidation now fires for runtime adapters too
- settings number inputs and select triggers size in ch so they scale with
the interface font
- recent-activity timestamps tick from one list-level ticker
- Markdown preview find goes through the shared find_in_file keybind with
containment, no longer counts its own bar, and debounces observer runs
- #2676 reverted; #2524 fixed by fading the sticky header's own background
instead of overlaying the content below it
- sticky group headers in the model picker and sidebar render again
(oc-sticky-fade-scroller class restored after 9b9d7069c)
- project switcher names are left-aligned again (wrapper lost in 26dbc2f30)
- tool card quick-open icon is always visible and opens the same line as the
expanded card's button
- tautological tests replaced or removed; new oxlint findings fixed
The Recent list's metadata slot only opened for activity duration, a
goal glyph, or a branch marker, so plain rows fell back to hover-only
or touch-only timestamps. Extend the slot guard to also open for
renderContext === 'recent' and render the compact timestamp inline,
mirroring the existing touch-only branch.
Closes#2560
Two findings from the openchamber-bot review at 478f1e9c:
1. Pending selections silently swallowed the Open-in-Finder pick
(prev #4). handleOpenInFinder flows into the batch-first branch of
finalizeSelection, so with checkboxes ticked the OS pick was
ignored and the selections were added instead. Clear selectedPaths
before finalizeSelection so the Finder-sourced target is honored.
2. Within-batch dedup was missing in the VS Code branch (nit from
the previous review). A path repeated within one batch hit
addWorkspaceFolder twice. Mirror the non-VS Code contract with
a seen Set; add a regression test asserting the host is called
once per unique path.
Chat sessions without a project were dropped from the mobile session tree
because no registered project owned their directory. The sheet now partitions
sessions like the desktop sidebar and shows Chats as a collapsible section above
the project tree, with the same rows, swipe actions and paging; search results
label them "Chats" instead of the raw directory name.
Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98
Two non-blockers from the openchamber-bot review at b2ab4af15:
1. VS Code batch add reported a misleading failure. addProjects
returned [] unconditionally for the VS Code runtime because
addWorkspaceFolder is reached only by addProject. Iterate addProject
per path so valid selections succeed and the host is called once
per selection. addProjects is now async (returns Promise<ProjectEntry[]>);
the call site in DirectoryExplorerDialog awaits it; existing
tests in useProjectsStore.test.ts updated to await.
2. Turkish locale (tr.ts) was missing the two new keys that other
11 dictionaries received: actions.addSelected and browse.selectForAdd.
Add both with real Turkish translations: "Seçilenleri ekle" and
"Eklemek için seç".
Two blockers from the openchamber-bot review:
1. Space swallowed in the path input. The handler at
handleKeyDown was calling preventDefault on every Space,
turning paths with spaces into no-op strokes and toggling the
highlighted row instead. Gate the toggle on hasTrailingPathSeparator
(query) so Space is a literal character when the user is typing
a path or filter and only acts as a selection toggle when they
have navigated into a directory.
2. Batch path was unreachable when the filter had no exact match.
With checkboxes ticked and a typed filter that has no exact match,
shouldCreateTarget evaluated true and the primary action (Add
selected) called createDirectory for the typed text instead of
adding the selections. Move the batch branch above
shouldCreateSelection so explicit selections always win over the
single-target create path. Drop the trailing else-if (now
unreachable) which also lost { asProject: true }.
The variable was referenced at line 484 (the single-target create branch)
but never declared, so every non-clone add path (Add button, Cmd+Enter,
Open in Finder, mobile add) threw a ReferenceError that surfaced as a
'Failed to select directory' toast. Add the missing local declaration
matching the condition the original review intended: !isCloneMode &&
shouldCreateTarget && the target path equals the user-typed path.
The checkbox button calls togglePathSelection() on click but does not stop
event propagation, so the click bubbles to the parent row's onClick which
calls executeRow() -> browseToEntry(). With a mouse, every checkbox click
would (a) toggle the selection, (b) navigate into the directory, and (c)
the navigation effect would clear selectedPaths. The primary interaction
of the multi-select feature was unusable.
The existing handleQuickAdd helper avoids this exact bug by calling
event.stopPropagation() inside its onClick handler. Apply the same
pattern to the new togglePathSelection onClick.
Refs openchamber-bot review on #2877.
Add a multi-select mode to the "Add project directory" dialog: each
directory row gets a select toggle (checkbox icon, Space toggles the
highlighted row), and the primary action becomes "Add selected" and
registers every selected directory in one store update. Selections apply
to the currently browsed directory and reset on navigation, dialog open,
and clone-mode entry. Clone mode keeps its single-target flow.
Add addProjects() to useProjectsStore: validates, normalizes, and dedups
paths (already-added or duplicated), creates entries in a single state
update and single persist, activates the first newly added project, and
discovers icons for each entry. Mirrors addProject semantics for the
single entry.
Refs OPE-142
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
Saved Project knowledge plans opened as an empty editor whenever the
viewer could not resolve the owning project from the current directory:
managed chats (openchamber:chats is not a registered project), worktrees
outside the repo path, and plan tabs restored after a reload. Titles
still rendered because the list reads the manifest through the correct
owner.
- Thread the owner explicitly (savedProjectPlan = { projectRef, planId })
from the panel, mobile surfaces, and persisted context tabs; PlanView
no longer guesses the project.
- An unrecognized directory resolves to no owner instead of borrowing
the active project's knowledge.
- Serialize plan writes per document (planSaveQueue) so close/switch
within the autosave debounce no longer drops the last edits, saves
cannot land out of order, and a recovered save clears the error banner.
- Send saved-plan contents inline in Improve/Implement prompts (they
have no file path); disable those actions for managed-chat plans,
which have no project directory to create a session in.
- Drop persisted plan tabs that carry an id without an owner rather than
reopening them against a guessed project.
Single chords stay for everyday actions; open/go actions move to two-step
mod+k sequences; held mod+digit switches header session tabs and held
mod+alt+digit switches context panel surfaces. Rare actions leave the
shortcut schema for the command palette, every remaining action ships with
a default binding, and stored overrides from the old layout reset once.
Key matching now follows the physical key on non-Latin layouts and for
Option-modified digits on macOS, including in the recording dialog.
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.
- Markdown DOM cache: key includes a content-length fingerprint so an
edited or reverted part re-materializing under the same id cannot
restore stale DOM, memoization uses scalar identities instead of the
part object (store reducers recreate part objects on unrelated updates,
which re-ran the async render pipeline for identical content), and a
probe with a mismatched locale/directory no longer destroys the entry
it failed to claim.
- Sidebar bootstrap: the layout-level sync owner only knows known
directories, so expanded projects bootstrapped serialized at background
priority. The visible collection now publishes a second, expansion-
aware demand owner, restoring concurrent hydration for expanded
projects and worktree groups.
- Settings: local changes still sitting in the debounce buffer are not
yet tracked as mutations, so a settings GET racing the debounce window
briefly reverted them; reconciled results now reapply the pending
buffer.
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.
activeSurface was permanently 'chat' after the legacy mobile layout
removal, so the whole concept is gone: the store field, surfaceGuard,
setActiveSurface/setSurfaceGuard, the per-runtime surface memory in
prepare/restoreForRuntimeSwitch, and WorkspaceSurface itself. All ~30
setActiveSurface('chat') call sites were no-ops and are deleted;
always-true 'is the chat active' checks in keyboard shortcuts, Header
and ChatContainer are unconditional now. FilesView's dirty-file guard
kept its file-switch and close protection but drops the surface-switch
branch nothing could trigger. TerminalView visibility comes only from
its callers. The router keeps parsing legacy ?tab= links (they open the
matching context-panel surface) via its own RouteTab type and no longer
serializes a tab or diff file into URLs — desktop URLs never carried
them anyway.
MainTab/activeMainTab/setActiveMainTab/setMainTabGuard were deprecated
mirrors of the surface names — every call site now uses
activeSurface/setActiveSurface/setSurfaceGuard directly and the aliases
are gone, including the persisted mirror field.
The 'diagram' surface had no way to open it (navigateToDiagram had no
callers except a .drawio attachment click that navigated to a surface
nothing rendered); the surface, DiagramView, and its store plumbing are
removed, and a .drawio attachment now opens in the file panel.
?tab= deep links map to the matching context-panel surface instead of
setting a main-area surface nothing renders, and a persisted non-chat
surface can no longer rehydrate into a blank main area.
Sidebar session/folder/group search and the Todos, Memory, Plans and
Notes filters required the whole query as one literal substring; they
now match tokens in any order and ignore punctuation via
matchesRankQuery, keeping their own list order and tree structure.