Commit Graph
366 Commits
Author SHA1 Message Date
Bohdan Triapitsyn c2136ce838 perf(chat): hold the previous timeline while a session loads; fade only when waited
- 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.
2026-08-29 17:22:31 +03:00
Bohdan Triapitsyn edfc9779cf perf(chat): make session switching feel instant
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.
2026-08-29 17:01:15 +03:00
𝖎𝖚𝖑𝖎𝖎𝖆 3f993fffec fix(sidebar): search the managed chats along with project sessions (#3212)
* 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
2026-08-29 02:36:05 +03:00
Bohdan Triapitsyn 48bcac1758 fix: harden and de-slop the merged sidebar/chat/settings batch
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
2026-08-29 01:06:43 +03:00
Bohdan Triapitsyn 5c2e3a6955 feat(directory-explorer): show success toast after batch project add 2026-08-29 00:50:22 +03:00
Bohdan Triapitsyn 7052d94c2a Merge remote-tracking branch 'origin/main' into port-2877
# Conflicts:
#	CHANGELOG.md
2026-08-29 00:48:50 +03:00
Bohdan Triapitsyn 49ca62a8a2 fix(sidebar): keep permission badge and hover actions from overlapping (#2738)
fix(sidebar): keep permission badge and hover actions from overlapping
2026-08-28 23:46:43 +03:00
Bohdan Triapitsyn 1b58b54f29 fix(sidebar): show compact timestamp in recent activity rows
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
2026-08-28 23:45:30 +03:00
Bohdan Triapitsyn e0869efeb0 Merge remote-tracking branch 'origin/main' into port-2738
# Conflicts:
#	packages/ui/src/components/session/sidebar/DOCUMENTATION.md
#	packages/ui/src/components/session/sidebar/sessions/SessionNodeItem.tsx
#	packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.test.ts
2026-08-28 23:45:25 +03:00
Bohdan Triapitsyn 52fc1fb25d merge main 2026-08-28 23:44:00 +03:00
herjarsa 23219d4049 fix(directory-explorer): clear selections on Finder pick; dedup VS Code batch
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.
2026-08-28 19:15:39 +02:00
Bohdan Triapitsyn 505f9b9e8c feat(mobile): list managed Chats in the sessions sheet
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
2026-08-28 20:08:58 +03:00
herjarsa 478f1e9c03 fix(directory-explorer): make batch add work in VS Code; add Turkish locale keys
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ç".
2026-08-28 18:59:47 +02:00
herjarsa b2ab4af157 fix(directory-explorer): gate Space toggle on browse position; reorder batch path
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 }.
2026-08-28 17:50:34 +02:00
herjarsa cd02678ee7 fix(directory-explorer): declare shouldCreateSelection in finalizeSelection
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.
2026-08-28 16:41:36 +02:00
herjarsa 223849261a fix(ui): stop click propagation on the multi-select checkbox
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.
2026-08-28 16:41:36 +02:00
herjarsa b56e60462e fix(ui): resolve merge conflict in DirectoryExplorerDialog batch add 2026-08-28 16:41:36 +02:00
Serhii Dziupin 04c37d32a5 feat(projects): support adding multiple projects at once in the directory picker
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
2026-08-28 16:41:36 +02:00
Bohdan Triapitsyn 4f53db17e6 fix(worktrees): protect user changes across ambiguous move failures
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
2026-08-28 12:03:55 +03:00
mattv8 bcaa70216f fix(sessions): restore worktree menu type wiring 2026-08-27 18:43:37 -06:00
mattv8 13ef639479 fix(sessions): keep chat worktree actions hidden 2026-08-27 18:43:37 -06:00
mattv8 a35ff1032c fix(sessions): avoid staged-change rollback conflicts 2026-08-27 18:43:37 -06:00
mattv8 1a9ff65380 docs(sessions): document dirty-source move choice 2026-08-27 18:43:37 -06:00
mattv8 26d6d72551 fix(sessions): preflight every worktree move 2026-08-27 18:43:37 -06:00
mattv8 8fc08853b3 feat(sessions): confirm dirty-source worktree moves 2026-08-27 18:43:37 -06:00
mattv8 52bea6d1f3 fix(sessions): harden worktree move recovery 2026-08-27 18:43:37 -06:00
mattv8 415f443a79 fix(sessions): keep worktree menu hook order stable 2026-08-27 18:43:37 -06:00
mattv8 9babd19d9b feat(sessions): move sessions to existing worktrees 2026-08-27 18:42:57 -06:00
Bohdan Triapitsyn 56e1eb5416 Merge main 2026-08-28 01:25:37 +03:00
Bohdan Triapitsyn 3d397b4ad4 Merge pull request #1752 from bashrusakh/fix/1736-branch-filter
fix(worktree): hide non-matching branches during search
2026-08-28 01:24:36 +03:00
Bohdan Triapitsyn 5da564df81 Merge pull request #2639 from weixiang1862/fix/add-project-browse-added-directory
fix(ui): allow entering already-added directories in the add-project picker
2026-08-27 23:14:59 +03:00
Bohdan Triapitsyn 43c4cc625f fix(ui): open saved plans against their owning project
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.
2026-08-27 20:18:12 +03:00
Bohdan Triapitsyn 03f4b5e3e0 fix(ui): scope agent memory to project context owner 2026-08-27 20:18:12 +03:00
Alexandre Reyes Martins 8ea94a119f fix: restore baseline validation (#3142) 2026-08-26 17:15:49 +03:00
Bohdan Triapitsyn 7977842e1e feat(ui): redesign the default shortcut layout around a mod+k leader
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.
2026-08-26 14:52:31 +03:00
Bohdan Triapitsyn 94f6b5fd38 feat(ui): migrate all shortcut surfaces to the centralized registry 2026-08-26 10:59:04 +03:00
Bohdan Triapitsyn 428854fd78 refactor(sync): replace the session-status setState monkeypatch with an owned API
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.
2026-08-26 03:13:21 +03:00
Bohdan Triapitsyn 448ecc12b8 fix(ui): close review findings from the switch-and-scroll integration
- 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.
2026-08-26 01:03:47 +03:00
Bohdan Triapitsyn ac880e8e62 Merge main (anchored-turn chat scrolling) into perf/switch-and-scroll
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.
2026-08-26 00:59:45 +03:00
c_w_xiaohei ceee7f350e fix(ui): preserve upstream sidebar and selection behavior 2026-08-26 02:36:07 +08:00
c_w_xiaohei 1d694932aa fix(ui): complete session rebase integration 2026-08-26 02:31:19 +08:00
c_w_xiaohei fa1503a038 perf(ui): narrow session switch fanout
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.
2026-08-26 00:44:14 +08:00
c_w_xiaohei 701173e399 perf(ui): index and batch session updates
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.
2026-08-26 00:42:37 +08:00
c_w_xiaohei 532bdab5e8 fix(ui): preserve upstream behavior after performance rebase 2026-08-26 00:42:36 +08:00
c_w_xiaohei 9b9d7069c7 perf(ui): isolate scroll shadow effects 2026-08-26 00:42:36 +08:00
c_w_xiaohei ed37d8e249 perf(ui): isolate sidebar session selection 2026-08-26 00:42:36 +08:00
c_w_xiaohei 26dbc2f309 perf(ui): streamline session sidebar state 2026-08-26 00:42:36 +08:00
Bohdan Triapitsyn c82f188fc8 refactor(surface): remove the main-area surface concept entirely
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.
2026-08-24 16:36:41 +03:00
Bohdan Triapitsyn 4da7604d7d refactor(surface): drop the deprecated main-tab aliases and dead diagram surface
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.
2026-08-24 16:21:05 +03:00
Bohdan Triapitsyn da309a2a8d refactor(search): session sidebar and project-context filters use the shared matcher
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.
2026-08-24 15:04:14 +03:00