Remove automatic moves on session activation, terminal failures, and archive restoration while preserving manual moves and worktree deletion.
Replace directory listing probes with a stat-only endpoint using Node built-ins, including an isolated module-load regression test for packaged desktop.
Validation: focused session, worktree, filesystem, localization, and bridge tests; workspace type-check and lint; web and VS Code builds. Desktop startup and behavior verified by the maintainer.
* feat(chats): relocate managed chat worktrees via OPENCHAMBER_CHATS_DIR
Projectless-chat worktrees were hard-pinned to
<home>/.config/openchamber/chats: the UI joined the path client-side,
workspace checks allowed only the config root, and identification matched
the literal path segment. When the OpenCode server runs as a separate
user (UID-separated setups), that root is unreachable — every chat
session answered HTTP 500 (EACCES on the session directory).
The server now owns the chats root. OPENCHAMBER_CHATS_DIR relocates it
(default unchanged: <config root>/chats); /api/fs/home answers
{ home, chatsRoot }; fs workspace checks accept the managed chats root
next to the config root; the client resolves the root from the server
(per-runtime cached, warmed at bootstrap so sync classification sees it)
and falls back to the home join for older servers.
Refs #3130
* chore: trim added comments to local precedent
* fix: forward managedChatsRoot through feature-routes-runtime to registerFsRoutes
* fix(chats): await the root warm-up and keep the legacy chats root owned
Review feedback on #3135:
- bootstrapGlobal now awaits warmChatsRootDirectory, so synchronous
session classification never sees an empty root cache (relocated
sessions were grouped as project sessions when the session list
outran /api/fs/home).
- managedProjectRoots keeps the legacy <config root>/chats entry next to
OPENCHAMBER_CHATS_DIR, so memory ownership of existing chats survives
relocation.
* fix(chats): distinguish chats-root fetch failure from older servers
* fix(sync): rehydrate managed chat sessions after the chats root warms
* fix(fs): pass managed roots through the symlink and git-dirs path checks after the main merge
* docs: drop changelog edits; changelog is the maintainer's release-time work
* fix(chats): keep legacy chat directories deletable while the root is relocated
* fix(chats): resolve roots before cleanup and initial session loads
* test(chats): type runtime spies against actual SDK contracts
---------
Signed-off-by: Steffen Mächtel <info@steffen-maechtel.de>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Resolve all 10 upstream v1.21.1 merge conflicts into custom, keeping
custom's GitLab/Gitea forge customizations layered on upstream while
lifting upstream improvements. Restored tr.ts i18n key parity with the
custom en.ts dictionary (471 custom forge keys added, en fallback) so the
upstream-introduced locale stays in parity.
Also stage bun.lock version alignment (1.21.0 -> 1.21.1) that matches the
staged package.json bump.
Verification: ui + web type-check pass; ui/web tests pass except
pre-existing failures on the custom baseline (forge.test.ts, session-actions,
issue-1637-2270, routes.test.js fs-stat directory scope).
Drop the canonical-containment 403 guard and the extra realpath(base) the
read routes (stat/read/raw/serve) had gained. Every workspace resolution
returns insideWorkspace: true and outside-file grants use
base = dirname(canonicalPath), so the guard could never fire; the flag had
no remaining reader and is gone with it. The read routes are back to the
single realpath(resolved.resolved) they had before.
Move the lexical-base fallback out of the inline header parsing in
routes.js. x-opencode-directory decoding belongs to
project-directory-runtime, so resolveProjectDirectory now also returns
requestedDirectory, the pre-realpath candidate that validated.
resolveWorkspacePathFromContext retries against it when the canonical base
rejects a path, which keeps files under a symlinked project root
addressable without a second copy of the header/query parsing.
Main already returns entry paths under the requested (lexical) directory,
fixed separately. Re-applying the original LIST hunk introduced two
regressions: shadowing of outer 'let requestedPath' inside the try block,
and the gitignore filter comparing lexical entry paths against
'ignoredPaths' built from the canonical realpath.
This commit drops the LIST hunk and the two list tests that accompanied
it. The read-family fixes (stat/read/raw/serve) stay — those were the
actual symlink resolve-before-containment fix and are not affected by
the LIST regressions.
Refs btriapitsyn on #2872 (2026-08-27).
* test: add regression test for stat directory scoping (#3019)
* fix: scope file-reference stat probes to the session directory (#3019)
The /api/fs/stat probe sent by the markdown renderer carried no
directory hint, so the server resolved the workspace from
settings.lastDirectory. When the browsed directory differs from the
session directory the stat returns 400 and the renderer treats it as
file-does-not-exist, silently disabling file-reference links.
Send effectiveDirectory as x-opencode-directory on the probe and
qualify the stat cache key by directory so a rejection under one
directory cannot leak into another. The probe logic moves from
MarkdownRendererImpl into fileReferenceStat with unit coverage.
Read-family fs routes (stat/read/raw/serve) rejected files whose canonical
(realpath) target escaped the project root, so a symlinked folder inside the
workspace (e.g. ~/test_folder -> /shared/test_folder) listed fine but every
file open failed with "Failed to open file".
Resolve symlinks before the containment check: paths that are lexically
inside the active workspace stay readable even when their realpath target
lives outside it, while direct paths outside the workspace (including
traversal and canonical-path requests) remain rejected and write/exec keep
the strict canonical boundary. The directory listing now returns entry
paths under the requested (user-visible) directory so the file tree hands
back addressable paths instead of canonical ones.
Refs OPE-235
Graceful probe: an optional stat of a config/backup path outside the active
workspace now reports exists:false instead of 400, so boot-time probes don't
surface console errors for a check that was never mandatory.
When the project root is not itself a git repository, discover nested
repositories (depth- and visit-capped readdir walk via a new
/api/fs/git-dirs route), auto-select the first one, and show a repository
picker next to the branch dropdown to switch. Selections persist per
runtime and root; discovery failure is a distinct marker with a retry
action, never an empty success.
Closesopenchamber/openchamber#2627
Listing a directory through a workspace symlink was returning realpath
entry paths. The file tree then rejected nested expand toggles because
those paths fall outside the workspace root.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.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>
* fix: make upstream sync actions target the selected remote
Ensure fetch and pull actually honor upstream selection so fork maintenance works from the Git sidebar, and surface upstream branch status alongside the primary origin-tracking indicators.
* feat: add Windows Electron desktop foundation
* fix(electron): stabilize Windows desktop packaging
* fix(electron): stabilize Windows desktop chrome
Use native Windows titlebar behavior with an Alt-accessible hidden menu, and harden Windows dev command launching so the desktop app follows platform conventions.
* fix(electron): stabilize Windows dev startup
* fix(electron): clarify desktop artifact names
* fix(electron): harden Windows desktop release and launch
* fix(electron): address Windows release review
* fix(electron): point updater and release links to org repo
* Fix Windows settings persistence fallback
* Fix Windows Electron dev startup
* Add Windows Electron window controls
* Fix Windows Electron install and opencode launch
* fix: resolve git status for repositories without upstream
Fixes repository detection stuck on Checking repository
Handles git status when no upstream is configured
Adds regression coverage for git status loading
* Add Windows app menu button
* fix: preserve file editor line endings
* ci: add desktop release smoke workflow
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
* perf(server): cache deterministic git rev-parse reads in fs exec route
A fresh client (e.g. immediately after a page reload) has an empty git
store and re-resolves every project's root from scratch, firing identical
`git rev-parse --absolute-git-dir` / `--git-common-dir` lookups against
`/api/fs/exec`. Each spawns a git subprocess server-side, so re-opening a
workspace recomputes everything.
Add a small TTL cache for an allowlist of deterministic, side-effect-free
git plumbing path queries, keyed by `(resolvedCwd, command)`:
- Only `git rev-parse` path lookups (absolute-git-dir, git-common-dir,
show-toplevel) are cacheable; any other command — including any non-git
command — always executes and is never stored.
- Only successful results are cached (failures may be transient).
- TTL is configurable via OPENCHAMBER_GIT_READ_CACHE_TTL_MS (default 30s,
0 disables). The git directory layout is effectively static while the
app runs, so a short TTL safely absorbs the post-reload burst.
- Expired entries are pruned alongside exec jobs.
Complements the client-side root-resolution cache: that one collapses the
in-session N² cascade, this one absorbs the cold-start burst on reload.
Adds tests covering cache hit, per-cwd keying, non-allowlisted commands,
failed-result bypass and the disable switch.
* fix(server): bound git-read cache with count + byte limits; test TTL expiry
Per the project caching policy (AGENTS.md: cap in-memory caches with both
count and byte limits), the git-read cache was unbounded between prunes.
Add dual-constraint LRU eviction (500 entries / 1MB, oldest-first) with
recency refresh on cache hits. Add tests for TTL expiry (fake timers) and
count-cap eviction.
* fix(server): dedupe in-flight git read cache hits