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.
The eslint pass in release:prepare caught what the package-scoped checks
did not: Header's handleOpenContextPlan and servicesTabs lost their last
callers with the removed shortcuts, the settings-synced listeners no
longer need the DesktopSettings import, and the store's openContextPlan
action itself went unused once the plan surface was reachable only through
the digit switcher and the rail.
The agent's browser.open used to force the context panel open and steal
the active surface, which read as panels opening by themselves. Tab
upserts now take a reveal option: the agent's opener passes reveal: false,
so the tab mounts invisibly (panes are kept alive regardless of
visibility, so agent control still works) while the panel and the active
tab stay exactly as the user left them. Manual opens are unchanged.
A trailing configure button on the rail — outside the sortable list and the
digit shortcuts — opens a dialog that toggles each surface. The choice is
stored as the hidden set so newly added surfaces appear for everyone, and
the rail and the mod+alt+digit switcher share the same visibility filter, so
badges and shortcuts always agree. Hidden surfaces keep their data and stay
reachable from the command palette.
mod+alt+arrows step through this window's session-open history (or between
neighbouring tabs when session tabs are on), mod+k r renames the current
session inline, and mod+k a toggles permission auto-accept. Pending
permission cards respond to alt+enter / alt+shift+enter / alt+backspace with
the keys printed on the buttons. The commit message box commits on
mod+enter, alt+arrows step the diff review between changed files, and the
command palette gains search-only commands for rare actions so the initial
list stays short.
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 hover info card (project/branch/PR/time) added noise without pull —
the tab title plus the sidebar already cover it. The tooltip body
component, its per-hover subscriptions, and the native title attribute
are gone; right-click/menu and the status dot are untouched.
A new Session tabs group (web/desktop only) turns the header session
tabs off; disabled, the header renders the exact pre-tabs view — plain
session title with meta row and the always-visible session menu (the
same block VS Code uses). The Alt+W close-tab shortcut no-ops while
tabs are off. Registered in settings search with a matching anchor;
labels translated in all locales.
The close-with-neighbour-activation logic moved into a shared helper
(the strip and the shortcut use the same path). Alt+W is the default
because the browser owns Cmd/Ctrl+W on web; desktop users can rebind it
— the action is registered as customizable, so it appears in the
Settings shortcuts section and the shortcuts help automatically. VS
Code is excluded (it has no session tabs).
Auto-add means the strip only grows, so past 20 tabs the oldest one
leaves the working set (the newly opened tab is always the survivor).
The branch/worktree/PR/project subscriptions that feed the hover
tooltip moved into the tooltip body component, which mounts only while
the tooltip is open — a resting tab now subscribes only to its status
dot's session status and unread count.
Rename from another tab's menu stored only a boolean that the
session-switch reset effect wiped before the menu finished closing, so
the tab activated but rename never began. The pending rename now
carries the target session id and begins exactly when that session
becomes active (whether the menu closes before or after the switch),
without the reset effect cancelling it — and without an in-flight
rename being cancelled by unrelated re-renders. Tab background/text
state changes animate at 75ms so activation reads immediate.
The title fade no longer sits on controls: it lifts while the tab is
hovered or its controls/menu are open (hard clip instead) and is not
applied at all while the active tab renames. The active tab clips its
title instead of showing an ellipsis, matching inactive tabs. Tab
separators drop the extra transparency that made them nearly
invisible.
Keeps enough space for the chat panel when the sessions sidebar stays visible
Bases the expanded layout breakpoint on the sidebar width instead of a fixed value
Right-click now opens the session menu under the cursor (the sidebar's
context-menu pattern; the same header-supplied items back both the
"..." dropdown and the context menu via injected menu primitives) and
still never changes the active tab. Hovering a tab shows the sidebar's
session tooltip — title, last-activity time, project, branch and PR
status — after a delay, suppressed while a menu is open or a drag is in
flight. Each tab carries the sidebar's status dot at its end (accent
while the session runs, info-blue for unread), hidden while the hover
controls overlay it. Tab titles fade out instead of ending in "...",
and while the active tab is renaming its hover controls stay hidden so
only the rename controls show.
The strip now owns a single dropdown per tab, fed by the header with
items bound to that tab's session — rename (activates the tab first),
copy id, share/copy link/unshare, export and move-to-worktree (active
tab only, they need the loaded directory), close other tabs, archive
and delete with the confirm dialog targeting the right session. The
separate inactive-tab menu is gone, and there is no Close item — the
tab's close button covers it, now placed after the menu button.
Right-click opens that menu without activating the tab. The menu's
anchor overlay stays mounted until the close animation finishes, which
removes the popup flashing in the top-left corner on close. The
scroller hides its scrollbar via a dedicated CSS class (the bar was
shifting the header content vertically).
The old flex spacer split the header in half, boxing the strip into the
middle while tabs shrank to slivers; the spacer now renders only for
the VS Code and surface-title layouts, tabs keep a fixed width (w-44)
and the strip scrolls behind the right-side buttons. Every tab gains a
hover-revealed close button next to the menu; the active tab's menu is
hover-revealed too (it was always visible) — the header passes it into
the strip so both overlays behave identically. The active tab's inner
padding matches inactive tabs.
Uniform compact tabs (h-7, 6px radius, 13px medium) that share the strip
width and shrink before scrolling, thin separators between inactive
neighbours that disappear around the active or hovered tab, a
hover-revealed menu button anchored to the tab's end, a soft selection
pill for the active tab, and hidden scrollbars with fade edges. The
active tab is single-line: the project/branch meta row stays only in the
VS Code header, which keeps the plain title.
Web/desktop header replaces the single session title with a strip of
soft pill tabs, one per session the user has opened (sidebar, palette
or deep link — opening anywhere adds a tab once). The active tab is the
familiar title block — rename, meta row and the full session menu —
inside a gently selected pill; a brand-new draft shows as a transient
pill until its session exists. Inactive tabs show the title with a
hover-revealed "..." menu (close tab, close other tabs, copy id) that
nudges the text like sidebar rows, and close by middle-click too.
Tabs drag to reorder, scroll behind the right-side header buttons with
soft fade edges, respect the reserved window-controls inset, and
persist across reloads. Closing the active tab activates its neighbour
(or opens a new draft when it was the last). Tab ids whose session is
not in the loaded list stay stored but hidden, so a partial session
list never destroys the working set. VS Code keeps the plain title;
mobile is untouched.
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.
Phone viewports run the separate MobileApp shell (and a viewport crossing
now reloads into it), so the desktop layout's mobile branch was
unreachable: the drawer machinery, the full-screen secondaryView surface
switch (including the terminal/diagram desktop carve-out nothing could
trigger), the mobile header with its tab bar, the Cmd+number tab
shortcuts, the mobile quota panel, and the surface guard that reset
non-chat tabs. DrawerContext had no consumers left and is deleted.
Header drops from 2630 to 1853 lines; the desktop render is unchanged.
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.
Share the desktop session-card currency formatter (lib/money.ts) and surface
the current session's cost in the extension chat-header context usage tooltip.
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 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
Busy context-panel session chats could render only the working-status row
when the iframe booted inactive or lost its visibility handshake, because
message reads shared the composer/background-work gate. Keep message
subscriptions enabled in the mounted session-chat panel so materialized
history remains visible (#2903, #2892).
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
The preview panel worked by proxying a dev server through OpenChamber's own
origin and rewriting the HTML that came back. Anything the rewriter did not
anticipate broke, and pages that refuse to be embedded never loaded at all.
This deletes the proxy (-1604 lines and its tests) and merges the preview and
browser panels into one surface backed by a real Chromium view.
What the panel is now
- A `<webview>` in its own session partition: logins and cookies persist, hot
reload works because nothing is rewritten, DevTools are one click away.
- Annotation: pick one element, drag a region, or draw freehand, write a note,
and it reaches chat with a screenshot of the visible page with the marks on it.
- Toolbar: hard reload, page zoom, device sizes, a light/dark switch that
applies to the page rather than the app, and cookie/cache clearing scoped to
the panel alone.
- Several pages at once, each tab showing the page's own favicon, and an address
bar that suggests pages already visited in this project.
- Dev servers are listed from what is actually listening on the machine, checked
against what a project announced, so a server is offered no matter how it was
started. One that is still starting is waited for instead of failing.
Remote dev servers
The desktop app binds a local port and pipes raw bytes to the OpenChamber host
over the existing authenticated connection, so the page keeps its own origin at
the root of its own host. The reachable set is exactly what discovery reports
and is re-checked per connection, so an authenticated client cannot dial
arbitrary local services on the host. Links and redirects to another loopback
port stay on the machine that served the page. A tunnel that cannot be opened is
reported; it is never replaced by the plain loopback URL, which would answer
from the user's own machine under a remote address.
Agent control
Browser actions are a separate `openchamber_web` tool: open, snapshot, click,
type, scroll, inspect computed styles, resize between mobile/tablet/desktop, and
capture a screenshot into `.openchamber/screenshots/` in the project. The
existing `openchamber` tool keeps sessions, worktrees and scheduled tasks. Each
has its own setting in the new Settings -> General -> OpenChamber Tools section,
and the plugin is not injected at all when both are off.
Capability belongs to the connected client, not to configuration: a client
declares on its event stream that it can drive a page, which only a Chromium
host does. Exactly one client performs each request — it claims the request
before acting, and the first claim wins — because deciding by whose result
arrives first would be too late for a click that already happened. No client
listening is answered immediately with an explanation rather than a timeout.
Runtime boundaries
Web tabs get a plain iframe that can display a page but not inspect one. The
VS Code extension no longer offers the surface at all, since nothing that makes
the panel worth having works there. Mobile is unaffected.
Native boundary
Camera, microphone, location and device-picker requests from panel pages are
denied — Electron grants them by default when no handler is set, and the panel
loads whatever address the user types. Page capture, appearance emulation and
storage clearing verify that their target belongs to the panel's own session
instead of trusting a web-contents id from the renderer.
Persisted state
Stored `preview` tabs migrate to `browser` (v13 -> v14). Context panel tab
limits are now per surface, so filling one surface no longer evicts another's
tabs. Address history is stored per project and per runtime.
Documentation
`preview.mdx` and `desktop-browser.mdx` rewritten across all locales, the agent
tool settings path corrected, new `DOCUMENTATION.md` for the browser-control
broker and the dev tunnel, and the `ui-api-decoupling` skill updated where it
still described the deleted proxy.
Pass initial subagent prompting settings into embedded chat URLs and views
Prevent inactive embedded chats from stealing focus on load
Treat review sessions as independent conversations when resolving prompt read-only state
* 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>
* fix(ui): mount only the active session chat iframe
* repro(ui): mount-all-persisted session-chat iframes (#2815)
Adds a regression-style reproduction for issue #2815: ContextPanel
renders one full-application iframe for every chat tab (inactive tabs
are only hidden via the Tailwind 'hidden' class, never unmounted), so a
reload restores all persisted session-chat tabs from the ui-store and
mounts N embedded OpenChamber apps in one browser tab.
The test reads the real ContextPanel.tsx render block, drives the real
useUIStore with the issue's persisted scenario (11 tabs, 8 read-only
session-chat tabs), and models the render block with the real
buildEmbeddedSessionChatURL helper, showing 8 live src iframes (7
hidden but loaded).
* test(ui): adapt issue 2815 reproduction for active chat
* fix(ui): unmount session chat when panel closes
---------
Co-authored-by: ChangeHow <23733347+ChangeHow@users.noreply.github.com>
Drops the macOS vibrancy toggle, IPC, and related settings copy
Updates dialogs, popovers, tooltips, and dropdowns to use shared glass styles
Adds sticky header fade behavior to model picker and sidebar lists