Marks active embedded chat sessions as seen only while focused
Shows real session titles for context panel chat tabs
Names review sessions after the implementation session
Show a count of chats (root sessions) with unseen activity on the macOS
dock icon. The count is computed in the existing tray snapshot (full
cross-project list, not the capped tray view; a subtask's unseen rolls up
to its root only when subtask notifications are enabled) and pushed to the
main process over the existing desktop_tray_update IPC, which calls
app.setBadgeCount (0 clears it). The badge clears as sessions are marked
seen on window focus.
Add a Dock badge toggle in Appearance settings (default on, persisted,
darwin desktop only), localized across all dictionaries, with a matching
settings-search entry whose availability mirrors the render guard exactly.
* feat(shortcuts): make 'Open model selector' shortcut customizable
Lets users remap the model selector shortcut (e.g. to Ctrl+M) via
Settings > OpenChamber > Shortcuts, matching OpenCode's quick
model-switch keybinding workflow.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* fix(agents): surface manual-restart needed when on external OpenCode
Agent prompt/permission/settings edits are written to disk, but an
external OpenCode server (skip-start or auto-detected on the default
port) is not owned by OpenChamber and is only health-probed on config
change, so it keeps serving its startup-cached config until restarted.
The API previously claimed a successful reload, so the UI silently
reverted the edit to the stale/default value on refresh.
Now refreshOpenCodeAfterConfigChange reports whether a real reload
happened; agent routes return requiresManualRestart for external mode;
and the agents UI keeps the saved values and warns the user to restart
their OpenCode server instead of showing a false success. Managed mode
behavior is unchanged (process is restarted and reload is live).
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* feat(model-picker): drag-to-reorder providers and persist accordion state
Ports two model/provider picker QoL features from otto-ui:
- Persist collapsed state of picker sections (favorites, recent, each
provider) via a new persisted zustand store so collapse survives
remounts and reloads, shared across every picker surface.
- Desktop drag-to-reorder of provider sections (whole header as the
mouse activator, 8px threshold so a plain click still toggles
collapse), with the order persisted in useUIStore.providerOrder and
applied across ModelControls, ModelMultiSelect and ModelSelector.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* feat(sessions): shift-click quick action hard-deletes thread without prompt
Ports the shift-quick-delete feature from otto-ui. The sidebar quick
action (normally archive) becomes a no-prompt hard delete while Shift is
held: the icon switches to a trash bin, the affordance turns destructive,
and the click bypasses the confirmation dialog via a new skipConfirm
source flag. A shared useShiftKeyHeld hook (single window listener set,
useSyncExternalStore) keeps only the small action button re-rendering on
Shift state changes, and resets on window blur so the affordance can't
get stuck after alt-tab.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Uses the linked target session's last model choice for follow-up review transfers
Prevents reviewer thinking settings from leaking into implementer follow-ups
* feat(agents): expose thinking variant configuration in agent settings
Fix#1425: add variant field to agent config UI so users can configure
thinking/reasoning depth per agent without editing opencode.json.
Changes:
- Added variant to AgentConfig and AgentDraft types in useAgentsStore
- Pass variant in createAgent and updateAgent API calls
- Support null for temperature, top_p, and variant to clear overrides
- Added variant input field in AgentsPage 'Model & Parameters' section
- Added variant to settings search registry
- Added i18n strings for variant field in all 8 non-English locales
The variant field maps to provider-specific parameters (e.g. Anthropic
high/max variant, OpenAI reasoning effort). Users can enter any string
value; the SDK passes it through to the model provider.
Clearing temperature/topP/variant now sends null to the server instead
of omitting the field, which properly removes the override in
opencode.json.
* fix(sync): preserve tool state.time in materialization merge
* chore: trigger re-review
* fix(agents): use thinking variant selector in settings
* fix(agents): preserve thinking variant values
---------
Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
Makes Commit & Sync fetch and pull before push when needed
Prevents stale git status from showing already up to date
Adds regression coverage for git status cache invalidation
* fix(settings): persist per-model visibility and sibling selector state
The server-side settings sanitizer only allowlisted favoriteModels and
recentModels, so hiddenModels, collapsedModelProviders, recentAgents, and
recentEfforts were stripped on every write to settings.json — per-model
visibility and collapsed-provider state silently reset on every container
redeploy or settings reload.
Add the four missing fields to sanitizeSettingsUpdate:
- hiddenModels: sanitizeModelRefs(..., 1024) — same shape as favoriteModels;
1024 covers dense multi-provider setups while bounding persistence/memory.
- collapsedModelProviders: normalizeStringArray with Array.isArray gate
(matches usageDropdownProviders).
- recentAgents: normalizeStringArray (Array<string> per ui-store).
- recentEfforts: new sanitizeRecentEfforts validating Record<string, string[]>
(shape confirmed in ui-store + addRecentEffort action); trims/dedupes keys
and variants, caps at 128 keys x 5 variants/key (5 matches client slice).
No ui-store version bump or migration: zustand's default merge spreads
persisted state over defaults, so missing fields fall back to [] / {} until
the next toggle. favoriteModels and recentModels are untouched.
Tests: 8 new cases in settings-helpers.test.js using the real
sanitizeModelRefs / normalizeStringArray — round-trips, empty-[] parity with
favoriteModels, garbage rejection, and a full-payload regression test.
* fix: sync model selector settings
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
* fix: handle non-ISO-8859-1 characters in fetch headers and Content-Disposition
Browser Headers API rejects characters above U+00FF. The x-opencode-directory header carries raw filesystem paths, which breaks when paths contain Chinese/CJK characters. Also fixes Content-Disposition for non-ASCII filenames per RFC 5987.
* refactor: export header sanitization helpers, deduplicate, add tests
Export isLatin1Safe and sanitizeHeadersForBrowser from runtime-fetch.ts so VS Code webview can import them instead of duplicating the logic. Add tests: isLatin1Safe boundary checks, sanitizeHeadersForBrowser encoding/deduplication, runtimeFetch round-trip encode/decode, and Content-Disposition RFC 5987 output for both ASCII and non-ASCII filenames.
* fix: mark encoded directory headers
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
The runtime URL refactor in #1228 switched gitApiHttp's buildUrl from
absolute window-origin URLs to the relative URLs returned by the default
runtime URL resolver, but gitApiHttp.test.ts kept asserting the old
absolute URLs. The two index-mutation tests have failed ever since
(no CI step runs the test suite, so it went unnoticed). Update the
expectations to the relative URLs the helper now produces.
Co-authored-by: Ibrahim Khan <ibrakhxn@amazon.com>
The VS Code webview was misdetected as a mobile device when the panel
was narrow on touch-capable machines, because device detection only
exempted the Electron shell. That added the `mobile-pointer` class,
letting mobile.css override the typography vars with `!important`, which
beats the inline styles from applyTypography/applyPadding — so font-size
and padding settings had no effect.
Treat the VS Code runtime like the desktop shell, as Electron already is.
The oc_url_token has a ~50s effective lifetime and was only fetched once at
preview mount, so HTML/image/PDF previews cycled to 'authentication required'
when it expired and nothing forced a re-render with a fresh token.
Add a consumer-gated proactive refresh in runtime-auth: while at least one
url-token consumer is active, a single scheduler mints a fresh token just
before the skew window and swaps it in atomically (the previous token stays
valid until the new one lands — no empty-token window for other consumers).
acquire/release manage the consumer count; subscribe fires only on a real
token replacement.
FilesView consumes this via a shared useAssetAuthRefresh hook (replacing three
near-duplicate effects) and remounts the iframe/img only when the token
actually changes, not on a blind interval.
Removes startup blocking on OpenCode config defaults
Preserves manual and directory-specific model selections
Adds regression coverage for config races
Adds a 'Cron' schedule type option to the scheduled task editor, allowing
users to create and edit cron-based schedules through the UI.
- Add cron expression input with inline validation (cron-parser)
- Show next 4 upcoming run times as a preview
- Provide clickable example chips (every 5min, hourly, Monday 9am, etc.)
- Preserve cron expressions when editing existing cron tasks
- Add cron.ts utility module for validation and next-run computation
- Add i18n keys across all 8 locale files
Closes#1586
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Bring the CodeMirror file editor up to the same rich highlighting as the Shiki
file view, so toggling edit <-> view is visually consistent. lezer collapses far
more tokens than TextMate (import/from/const are all "keyword"), so a theme
remap can't reach parity — instead, project real Shiki tokens onto decorations.
- Worker: add highlightTokens — tokenize with an arbitrary registered TextMate
theme and return per-line styled runs with offsets. The theme object ships to
the worker once per name; later calls send only the name.
- New shikiHighlight CodeMirror extension: a StateField of mark decorations
built from worker tokens. Re-tokenizes on a short idle (off the keystroke
path) and maps decorations through edits so colors persist while typing.
- flexokiTheme: add { syntaxColors: false } to keep only the editor UI theme,
so the lezer highlighter doesn't compete with the Shiki decorations.
- FilesView: enable Shiki highlighting (same language resolver as the file view
→ identical language) and drop lezer token colors when it's active. lezer
language stays on for indentation/folding/brackets.
The Prism syntaxTheme prop is no longer read after code highlighting moved to
the Shiki worker. Remove the now-dead prop threading and its source.
- Drop syntaxTheme from interfaces, destructures, prop passes, and React.memo
comparators across ChatMessage, MessageBody, ProgressiveGroup, ToolPart,
TurnActivity, ToolOutputDialog, and ChatInput.
- Drop the unused _syntaxTheme param from renderWebSearchOutput.
- Remove the dead generateSyntaxTheme usages (ChatMessage memo, PlanView
unassigned memo) and delete the now-unimported syntaxThemeGenerator module.
* fix: pass effective workspace directory in Files API requests
The web Files API used useDirectoryStore.currentDirectory as the
workspace root, but the FilesView's effective directory comes from
useEffectiveDirectory() which can differ (e.g. worktree sessions).
When they diverged the server rejected file reads with 'Path is
outside of active workspace'.
Add directory override to FileReadOptions so callers can pass the
effective directory per-call. The FilesView now passes its root
(from useEffectiveDirectory) through readFile, statFile, image/PDF
URLs, and the desktop image fallback. The server receives the
correct workspace root via x-opencode-directory header or directory
query parameter.
Fixes#1456
* fix: cover files workspace directory regressions
* fix: sync directory store on draft session and forward cache options
The content cache wrapper in RuntimeAPIProvider was dropping the
options parameter (including the per-call directory override) when
making internal statFile and readFreshFile calls during cache
validation and misses. This caused the underlying web API to fall
back to getDirectory() which reads useDirectoryStore.currentDirectory.
Additionally, openNewSessionDraft, setNewSessionDraftTarget, and
overrideNewSessionDraftTarget updated the draft's directory without
ever syncing useDirectoryStore. Since the web API's getDirectory()
reads from that store, it returned the stale previous-project
directory during draft sessions, causing 'Path is outside of active
workspace' errors when opening files.
Forward options through all internal calls in the content cache
wrapper, and sync useDirectoryStore via setDirectory() whenever the
draft session directory changes.
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
* perf(startup): hydrate providers/agents from cache (stale-while-revalidate)
Persist last-known provider/agent snapshots instead of stripping them, so the
model/agent pickers paint instantly on cold start. Freshness is preserved by the
background refresh in initializeApp() and activateDirectory() (which overwrite on
success) and by the existing provider/agent config-change subscriptions, so the
prior stale-provider regression stays fixed without blanking the UI during fetch.
* perf(startup): cache directory session list for instant sidebar
Persist a capped slice of each directory's session list and seed the child store
from it on creation, so the sidebar paints chats immediately on cold start.
Bootstrap phase-3 loadSessions overwrites with the fresh list; its empty-list
race guard preserves the seeded sessions during OpenCode warmup.
* perf(startup): hold API requests through OpenCode warmup instead of 503
The readiness gate returned 503 the instant OpenCode wasn't ready, pushing the
client into an exponential-backoff retry loop (500ms -> 1s -> ...) that wasted
seconds of cold-start time and could fail bootstrap outright. Now hold the
request and poll readiness up to a bounded window so the first call succeeds as
soon as OpenCode is up (typically sub-second); still 503 fast past the window so
a genuinely-down server doesn't hang. Adds coverage for both paths.
* perf(startup): surface cached providers/agents in pickers (optimistic readiness)
The model/agent pickers gated purely on isInitialized, so they showed
"Loading…" for the entire init round-trip even when provider/agent data was
already hydrated from cache — making the persisted-cache work invisible. Treat
the pickers as ready as soon as cached providers are present (stale-while-
revalidate), so they paint last-known models/agents instantly and refresh in the
background. First-ever launch (no cache) still shows Loading until init.
* perf(startup): don't abort directory bootstrap on transient phase-1 failure
A failed initial path.get OR session.status aborted the whole directory
bootstrap, stranding it in loading and skipping phase 2/3 (session load).
session.status is live data the event pipeline keeps current, and path.get is
tolerable once a project is resolved from global state. Now only a total
failure (or path.get failing with no resolved project) aborts, so the sidebar
and chat keep advancing and loading sessions through warmup hiccups.
* perf(startup): don't bootstrap directories from archived sidebar rows
Each sidebar session row called useDirectoryStore(dir), which defaulted to
bootstrap:true and triggered a full directory bootstrap. Archived sessions point
at dozens of (often deleted) worktrees, so on startup this fired a session-list
fetch + 6x2s empty-retry storm per dead directory (the logs the user saw). The
store ref there is only read on-demand via getState() in export handlers, never
subscribed, so archived rows don't need it bootstrapped. Add a { bootstrap }
option to useDirectoryStore and skip bootstrap for archived rows; active rows
still bootstrap so live cross-directory session/status keeps aggregating.
* perf(startup): stop empty-session bootstrap retry storm on web/desktop
The post-bootstrap retry re-ran the full directory bootstrap 6x2s whenever the
session list came back empty, on the theory that empty meant OpenCode wasn't
ready. But loadSessions already retries transient failures twice over
(listGlobalSessionPages throws on 5xx and retries internally), so on web/desktop
an empty result is authoritative — the directory genuinely has no sessions (e.g.
deleted worktrees referenced only by archived sessions). That produced the
dozens of '[bootstrap] sessions empty ... 6 attempts; giving up' log storms.
Gate the retry to VS Code, where the bridge can return an empty 200 during
warmup that the inner retries can't catch.
* perf(startup): scope provider/agent config to project (worktrees inherit)
Providers/agents/defaults are project-level, but were keyed per directory, so a
worktree fetched and cached its own snapshot — duplicating the parent project's
load (the trace showed initializeApp loading the worktree and activateDirectory
loading the project concurrently, ~8s of redundant background work).
- resolveConfigDirectory() maps a worktree to its owning project; loadProviders
/loadAgents/activateDirectory now key by it, so a worktree reuses one shared
project snapshot. activateDirectory resolves up-front so activeDirectoryKey and
the snapshot key always match (picker stays consistent); the OpenCode working
directory is unaffected.
- Add a 30s runtime freshness guard so the stale-while-revalidate background
refresh skips re-fetching config that was just loaded (initializeApp then
activateDirectory for the same project), and to avoid churn on rapid project
switches. Config-change invalidation clears the snapshot, which bypasses the
guard, so freshness never masks a needed refresh.
* fix(sidebar): default archived sessions to hidden to avoid startup flash
useSessionDisplayStore defaulted showArchivedSessions to true, so on startup
archived sessions rendered by default and then vanished once the persisted
preference rehydrated to hidden — a visible flash. Default to hidden so the
pre-hydration state is the quiet one; users who opted into showing archived keep
their persisted true (default change doesn't override persisted state).
* perf(startup): persist worktree->project mapping to kill cold double-load
The worktree->project map (availableWorktreesByProject) is populated by async git
discovery, so it isn't ready when initializeApp runs — a worktree's first config
load couldn't resolve to its project and duplicated the project's provider/agent
load, saturating OpenCode during cold start (the source of the slow first
createSession/send the user observed). Cache resolved worktree->project mappings
to localStorage so resolveConfigDirectory resolves synchronously at init on
subsequent launches; the project is loaded once and activateDirectory hits the
freshness guard. worktree->project is immutable so a cached entry is safe; live
resolution still populates/corrects the cache.
* perf(startup): persist worktree map for instant sidebar + first-launch keying
Worktree discovery is async (git), so availableWorktreesByProject was empty at
startup: the sidebar worktree list appeared late, and useConfigStore couldn't
resolve a worktree to its project on the first launch (causing the cold
worktree+project double-load). Persist the discovered worktree map to
localStorage and seed it synchronously on store init (stale-while-revalidate:
discovery refreshes in the background via the existing setState, which now
write-through persists). The sidebar paints worktrees instantly and
resolveConfigDirectory resolves the project from the very first launch.
* perf(startup): coalesce concurrent duplicate OpenCode reads in runtimeFetch
On cold start the sync bootstrap and the config store independently fire the same
idempotent reads (providers, config, path, agents, project) concurrently with no
shared dedup, saturating the single OpenCode process and delaying work queued
behind it (e.g. createSession). Coalesce genuinely-concurrent identical GETs to
those read endpoints at the transport layer so OpenCode does the work once; each
caller receives an independent response clone. Tightly scoped: GET only,
allowlisted read paths, never event streams, never a signal-bearing request (so
one caller's abort can't cancel the shared fetch). Entries clear on settle, so it
only shares overlapping in-flight requests — never a stale response.
* perf(startup): cache git branches so the draft branch selector paints instantly
The branch selector above the composer was the slowest-loading element: it's
gated behind a cold 'git branch' fetch (useGitStore, not persisted). Cache the
per-directory branch list to localStorage and seed the store on init (with
isGitRepo:true so the selector's gate passes), and write the cache on every
successful fetchBranches. The ChatInput draft-branch effect now refreshes on
staleness (>30s) rather than mere absence, so seeded branches show immediately
and still refresh in the background without a spinner — no stale-forever
regression. Only the branch list is cached; status/log/diff are untouched.
Load startup config under the owning project's directory key (resolving from a
worktree directory when needed) so the auto-opened draft, which activates the
project, finds a ready providers/agents snapshot instead of triggering a second
load. Also dedupe app.agents: listAgents now takes the directory directly and
shares an in-flight request, so the config store and agents store no longer
issue duplicate agent fetches at startup.
The global event-stream WebSocket opened before a valid oc_url_token was
minted, so the upgrade failed auth ("no valid credentials available") in
packaged builds with a UI password. The resulting reconnect storm churned
the sync store and made session status flicker busy<->idle. Await the URL
auth token before connecting (a WS upgrade can't send a bearer header like
SSE does) and drop a rejected token on pre-ready close so the next attempt
re-mints a fresh one.
Also harden /session/status reconciliation: the watchdog poll is now
monotonic (only confirms/raises active status, never blindly lowers a
busy/retry session to idle on a transient or misscoped snapshot). Idle is
applied only by the authoritative reconnect/escalation resync, which trusts
the live server snapshot as the source of truth. Add a Help -> Toggle
Developer Tools menu item so production builds can open the console.
Add per-hunk staging, unstaging, and discarding to the Changes diff
view, so a single change region inside a file can be acted on in
isolation instead of forcing whole-file stage/revert. The change is
wired end-to-end across the web server, the shared UI runtime API
contract, and the VS Code extension, with Electron inheriting the web
path unchanged (it boots the server in-process).
Server
------
- New `applyHunk(directory, filePath, { patch, action })` in
packages/web/server/lib/git/service.js. It resolves the repository
context and validates the file path with the same helpers used by
stageFiles/unstageFiles (resolveGitFileContext +
validateRepositoryFilePaths), then writes the single-hunk patch to a
temporary file in the OS temp dir (never inside the repo, so it
cannot show up as an untracked file) and runs `git apply` with flags
chosen per action:
stage -> git apply --cached (working tree -> index)
unstage -> git apply --cached --reverse (index -> working tree)
discard -> git apply --reverse (revert in working tree)
A `git apply --check` runs first with the same flags, so a stale
hunk that no longer applies fails with a clear "Hunk no longer
applies - refresh and try again" message instead of leaving a
partial mutation. The patch's target path is parsed and must match
the requested file (with /dev/null tolerated for new/deleted files),
preventing a patch from silently targeting a different path. The
whole operation runs inside withGitIndexMutationQueue to avoid
racing with concurrent stage/unstage. The temp file is removed in a
finally block.
- New `POST /api/git/apply-hunk` route in routes.js, registered
alongside stage/unstage. Validates directory, path, non-empty patch,
and action before delegating.
- DOCUMENTATION.md updated with the new service entry.
Patch extraction
----------------
- packages/ui/src/lib/diff/patchFileDiff.ts gains
splitPatchIntoHunks(patch) and extractHunkPatch(patch, hunkIndex).
They keep the original file header (diff --git / index / --- / +++)
and emit exactly one @@ hunk per standalone patch, which is what
`git apply` expects. Each emitted patch is guaranteed to end with a
trailing newline (without it git apply reports "corrupt patch").
Runtime API contract
--------------------
- GitAPI (packages/ui/src/lib/api/types.ts) gains optional
stageGitHunk / unstageGitHunk / revertGitHunk, matching the
stageGitFiles? / unstageGitFiles? precedent so runtimes that do not
support it degrade gracefully.
- gitApi.ts delegates to the registered runtime git API, falling back
to gitApiHttp, exactly like the existing whole-file helpers.
- gitApiHttp.ts posts to /api/git/apply-hunk.
- Web runtime composes the three methods in packages/web/src/api/git.ts.
VS Code parity
--------------
- packages/vscode/src/gitService.ts adds applyGitHunk(), implemented
natively with the existing execGit helper + a temp patch file +
`git apply` (--cached / --cached --reverse / --reverse), mirroring
the server's --check-first safety and temp-file cleanup.
- bridge-git-runtime.ts handles the new api:git/apply-hunk bridge
message; webview/api/git.ts sends it. VS Code users get identical
stage/unstage/discard-hunk behavior.
UI
--
- New DiffHunkActions component renders a compact per-hunk strip
above each expanded file diff in the Changes view. Each hunk chip
shows its +additions / -deletions counts and offers:
working scope -> Stage + Discard
staged scope -> Unstage
Clicking extracts that hunk's standalone patch via
extractHunkPatch(patch, hunkIndex) and calls the runtime git API.
Because the chip index comes directly from fileDiff.hunks[] and the
patch is sliced in the same order, the hunk the user sees is always
the hunk that gets applied. While any action is in flight all buttons
disable to prevent conflicting concurrent mutations; the per-hunk
spinner reflects in-flight state.
- DiffView wires DiffHunkActions into InlineDiffViewer (text diffs
only; binary/image and full-file-content modes are excluded since
they have no patch). MultiFileDiffEntry passes directory/staged
through and handles onHunkApplied by bumping the diff reload nonce
(so the file's diff re-fetches and the affected hunk disappears)
and refreshing git status (so file counts and the staged/changed
scope update). Hunk actions are therefore available wherever the
default patch-context diff is shown.
i18n
----
- 10 new keys (diffView.hunk.*) added to all 9 locales (en, es, fr,
ko, pl, pt-BR, uk, zh-CN, zh-TW), including stage/unstage/discard
labels, tooltips with the hunk index, a stale-hunk error message,
and an unsupported-runtime fallback.
Tests
-----
- packages/ui/src/lib/diff/patchFileDiff.test.ts covers
splitHunks/extractHunkPatch: multi-hunk split, header preservation,
single-hunk and empty patches, out-of-range indices.
- service.test.js adds an applyHunk suite that builds real temp repos
with two separate hunks and verifies: staging one hunk leaves the
other unstaged, discarding reverts only the targeted hunk in the
working tree, unstaging removes only one hunk from the index, and a
retargeted patch (different file path) is rejected. Also covers
invalid-action / missing-hunk-header validation.
- packages/web/src/api/git.test.ts mock completed with the new methods
(and previously-missing exports that prevented the test from
loading) and asserts the three hunk methods are exposed.
- routes.test.js continues to pass under bun.
CHANGELOG updated under [Unreleased].
Packaged desktop showed no sessions in 1.12.4. Root cause: the sanitized
session-list proxy path added in #1538 forwarded the renderer's
"authorization" header (the OpenChamber UI client token) to the managed
OpenCode upstream alongside the managed "Authorization" credential.
OpenCode does not recognize UI client tokens, so every session-list
request answered 401 — only in the packaged app, because only its
renderer (openchamber-ui:// origin) attaches a bearer token; dev web and
dev Electron run same-origin without one. The legacy http-proxy path
overwrote the header correctly, which is why everything except session
lists kept working.
Proxy fix:
- proxy-headers: filter the client "authorization" header out of
forwarded request headers; the OpenCode upstream must only ever see
its own managed credentials. Covered by tests.
Desktop cwd:
- electron: launch the managed OpenCode CLI from the user home instead
of app userData, matching upstream desktop behavior. userData-as-cwd
made OpenCode treat the app-data folder as a separate empty workspace.
Home directory poisoning loop:
- directoryPersistence: stop replaying localStorage homeDirectory
through synchronizeHomeDirectory on boot/auth resync. The persisted
value is only a boot-time cache; replaying it re-wrote stale values
(e.g. a project path) into desktop settings on every start, overriding
the authoritative /api/fs/home resolution.
- persistence: never overwrite an injected window.__OPENCHAMBER_HOME__
with a persisted value.
- useDirectoryStore: host switches happen in place (no reload), so
re-resolve home from the new runtime's /api/fs/home on endpoint
change instead of keeping the previous host's value.
- opencode client: only short-circuit to the injected desktop home when
the active runtime is local; remote runtimes ask /api/fs/home.
Settings hygiene:
- persistSettings: log field names only — change payloads can carry
credentials (UI password, client tokens, tunnel tokens) that must not
reach the log file; drop step-by-step log chatter.
- validateProjectEntries: only stat project paths when the incoming
update actually touches the projects list, not on every settings save.
- remove the write-only approvedDirectories setting everywhere and add
a migration that strips the stale key from persisted settings.
Tests:
- usePluginsStore.test: register an own runtime-fetch module mock so the
suite is independent of process-global mock.module leakage from other
files, and restore globalThis.fetch after the suite.
- persistence.test: clean up the window global created for the suite.
* feat(ui): add cache hit rate to context sidebar with verified formula
Add a Cache Hit row to the last-assistant-message token breakdown in
the context sidebar. The percentage is computed by the new
computeCacheHitRate utility:
cache.read / (input + cache.read + cache.write) x 100
The formula was verified against the SDK source
(packages/opencode/src/session/session.ts:getUsage), which reports
input as the non-cached portion only
(totalInputTokens - cacheReadInputTokens - cacheWriteInputTokens).
Also export sumTokenBreakdown from tokenUtils for reuse, and fix the
event-reducer test type errors (setDelta/getText helpers, toBeCloseTo
replacement).
Closes: #
* fix: wire i18n key for Cache Hit label, fix formatNumber type, drop unintended event-reducer changes
* chore: remove unused cacheHitRate and cacheHitRateTooltip i18n keys from en.ts
* fix: correct cache hit token display
* fix: add French cache hit label
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>