- Remove dead precache manifest from SW (no fetch handler to consume it)
- Set empty globPatterns in vite-plugin-pwa config
- Fix bottom chin bar padding: use scaled safe-area token instead of raw inset
- Add 'New Session' PWA shortcut (client + server manifest)
- Add PWA meta tags to mobile.html (manifest, Apple icons, theme-color)
- Fix static site.webmanifest: add id/scope, correct description
- Remove duplicate apple-mobile-web-app-title from index.html
- Move SW registration to module scope (earlier, no window.load delay)
- Update manifest route tests for new shortcut order
createSession() only sent { directory, title } to the OpenCode server,
dropping the agent field. The create() function read payload.agent but
never forwarded it, so every session was created with the OpenCode
default (build) and the agent was only applied later at prompt time.
Now forwards agent/model/variant in the session create POST body so the
session is created with the requested agent from the start.
routes.js reads resp.page?.hasMore and resp.page?.nextUrl — the old
HTTP client returned page as an object from parsePageInfo. The CLI
pivot accidentally returned page as a number, making hasMore always
undefined in routes.js. Restore the object shape in both gitea and
gitlab clients. Update tests to assert the object shape.
Fix critical bugs in the CLI transport layer that were masked by
idealized unit test mocks. All fixes verified against live binaries.
Server (gitea/client.js):
- C1: Remove --paginate flag (tea rejects it with exit 1)
- C2: Pass auth via -H 'Authorization: token' header instead of
GITEA_SERVER_TOKEN env (tea ignores that env var)
- C3: Parse --include output from stderr (tea writes headers to
stderr, not stdout)
- H2: Parse Link header for hasMore/pagination
Server (gitlab/client.js):
- C4: Remove /api/v4 prefix (glab adds it automatically; double
prefix caused every call to 404)
- H2: Parse Link header for hasMore/pagination
Tests (both client.test.js):
- H1: Rewrite mocks to match real CLI behavior: headers on stderr
for tea, no --paginate, auth via -H header, Link header parsing
- Add C3, C1, C4 specific regression tests
UI (GiteaSettings, GitLabSettings):
- U4: Replace return null loading state with animated skeleton
(prevents blank flash)
- U2: Surface actual error message in connect failure toast
- U5: Add CLI transport hint below connect form
- U6 (GitLab): Add transport hint for unconnected state
UI (GiteaIssuePickerDialog, GitLabIssuePickerDialog):
- U3: Add retry button when error state is displayed
Live smoke tests passed:
- tea api -H 'Authorization: token WRONG' /user → 401
- tea api /repos/Vibing/openchamber/issues?state=open&limit=2 → 200
- glab api user with GITLAB_TOKEN=dummy → 401 (not 404)
The root package.json pins 12 @codemirror packages through overrides
but declared 10 of them as direct dependencies with caret ranges. npm
refuses that combination with EOVERRIDE while bun accepts it, so every
npm and npx invocation inside the repository failed instantly while
workspace builds kept passing.
That includes npx-based MCP servers spawned for sessions rooted in this
checkout, because OpenCode starts stdio servers with the instance
directory as their working directory, so those servers died on spawn
and surfaced as MCP error -32000.
The 10 dependency specs now match the overrides exactly. Resolutions do
not change; the overrides already forced these exact versions. Verified
npx -y mcp-mermaid now exits 0 from the repo root and from
packages/web, and an OpenCode instance rooted at this checkout connects
every previously failing npx server (exa, zai-mcp-server, mcp-mermaid,
godot, svelte).
* fix(sessions): keep a shared chat directory until its last session is deleted
Deleting a root chat session removed its managed scratch directory even
when forks, side threads, or subagents still lived in it; OpenCode then
failed every prompt in those sessions with FileSystem.realPath NotFound.
The directory is now removed only once no other known session resolves
to it. The deleted subtree does not count, because the server cascade-
deletes it, and an unloaded global cache keeps the directory instead of
guessing.
Closes#3312.
* fix(sessions): relocate a session whose worktree directory disappeared
A worktree removed outside OpenChamber, by the agent or by hand, left its
sessions pointed at a path that no longer exists: every terminal create
and restart failed with "Invalid working directory" and the tab stayed
stuck, while Git, Files, and prompts kept targeting the dead path.
The terminal server now names that one rejection (TERMINAL_CWD_MISSING)
instead of substituting a directory of its own. The shared UI reuses the
archived-restore fallback for live sessions: a server-confirmed missing
directory moves the session and its stranded subtree to the project's
primary directory through the control-plane move, clears the worktree
hint, re-selects the session, and tells the user where it went. It runs
from a terminal failure and on activation of any session whose directory
is neither a project root nor a managed chat directory; available,
unknown, and failed probes leave everything untouched.
Closes#3338.
* fix(scripts): make oc-dev load again after the changelog cleanup
The changelog cleanup referenced fs.existsSync in a module that imports
existsSync by name and never binds fs, so every oc-dev invocation failed
with "fs is not defined" before reaching its action.
* fix(sessions): probe directory availability on disk, not through OpenCode path resolution
OpenCode's /path never checks that a directory exists: it echoes the
requested path and resolves its project through Git discovery that
swallows errors, so a deleted worktree came back as a valid location and
every missing-directory fallback (draft recovery, archived restore,
session relocation) stayed inert on a real server. The probe now asks
OpenChamber's own /api/fs/list, which stats the path and reports
not-found and not-directory explicitly; anything else stays unknown.
* fix(sidebar): keep a worktree whose directory is gone visible as missing
git keeps a worktree registered after its directory is deleted outside
git and marks it prunable; the list parser ignored that line, so a
deleted worktree looked alive, and nothing in the app asked for a new
listing anyway. The server now reports prunable, the UI keeps such a
worktree in the topology with worktreeStatus missing and a warning icon
on its sidebar group, and relocating a session out of a confirmed-
missing directory raises an in-app topology signal the sidebar
rediscovers on. Dropping the worktree instead would hide every session
that lived there, and a hidden session can never be opened or relocated.
No idle polling is added.
* fix(sessions): never relocate a session to the filesystem root
OpenCode files a directory outside any Git repository under its global
project, whose worktree is the filesystem root. A managed chat whose
directory vanished would otherwise be moved to /. The relocation now
refuses a root destination, and the activation probe recognizes chat
directories through the home-based check as well, so it does not depend
on the chats root having been resolved yet.
* test(sessions): mirror the relocation action in the issue-2039 session-actions mock
session-ui-store now imports relocateSessionFromMissingDirectory, and the
mocked module in this test listed every other action but not that one, so
the file failed on import.
Input history (#3035) shipped with "All projects" as the default scope and
only recorded prompts sent after the upgrade, so ArrowUp showed other
sessions' prompts and, once switched to "Current session", nothing at all.
Default to the current session and merge the visible transcript's prompts
with the persisted bucket. Existing sessions recall as they did before
#3035, while new prompts keep their attachments and stay recallable after
a revert hides them from the transcript.
Cleanup across #1855, #2297, #3072, #3178, #3035 and #3135: drop the
duplicate poll guards in the file content poller, the zod schema the
VS Code package cannot depend on, a copied file-URL helper and stray
whitespace; move the Enter-to-send strings into the settings namespace;
document OPENCHAMBER_CHATS_DIR, resolve the chats root once on the server
and warm it alongside the other bootstrap calls.
* 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>
* feat(ui): add composer enter-to-send toggle and native hardware-keyboard detection
Replaces the settings-page "Enter sends with a keyboard attached" checkbox with
an EnterKeyToggle in the composer footer: plain Enter submits / Shift+Enter
inserts a newline when enabled, Shift+Enter submits / Enter inserts a newline
when disabled. Ctrl/Cmd+Enter always submits as the soft-keyboard fallback.
Persisted as enterToSend.
Adds the Android HardwareKeyboardPlugin: scans input devices for an alphabetic
physical keyboard (ignoring phantom key/sensor devices), re-answers on config
changes/foreground, and confirms attachment from real hardware key events.
MainActivity surfaces key events to it before the WebView consumes them. The
composer and draft layout start keyboard-aware instead of inferring one focus
late; ComposerEditor preserves Enter modifiers through CodeMirror's deferred
re-dispatch so the toggle can tell Shift/Ctrl+Enter from plain Enter.
Removes the settings search entry and i18n keys for the old checkbox.
* refactor(ui): keep enter-to-send branch focused
* fix(ui): preserve enter-toggle taps on touch
* fix(ui): preserve enter key defaults and move setting
* fix(ui): keep enter setting lint-clean
* fix(i18n): preserve current Turkish message parity
* fix(settings): persist enter-to-send preference
* fix(ui): clarify enter-to-send setting
* fix(ui): apply enter preference on desktop
* fix(ui): match enter setting focus mode default
* test(ui): cover enter key policy matrix
* fix(ui): harden deferred enter handling
* fix(chat): preserve untouched Enter policy and validate settings
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
* fix(files): guard file polling races
- Ignore sub-millisecond mtime jitter on a same-size file so an unchanged
open file no longer loops through reload and flickers.
- Swap externally changed text content into the open editor in place
instead of clearing the loaded path and showing the load spinner.
- Read content only after metadata changed, confirm it with a second
read, and skip the swap when the file is unchanged, the buffer is
dirty, or a newer local write landed.
- Keep the stat baseline unchanged when a poll cannot observe content so
a failed read is retried rather than treated as unchanged.
- Fall back to a full reload for images, PDFs, binaries, and files above
the content-poll byte limit, and when a poll returns binary content.
- Serialize polls and dispose the poller on unmount, file switch, and
directory change.
- Add a `fresh` file read option that bypasses the content cache and the
HTTP cache.
* fix(files): invalidate stale polls after diagram saves
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Replace raw fetch transport with CLI subprocess calls:
- Gitea: spawn 'tea api --include' with GITEA_SERVER_TOKEN env var
- GitLab: spawn 'glab api --include' with GITLAB_TOKEN env var
Binary paths env-overridable (TEA_BIN / GLAB_BIN).
8s request timeout via AbortSignal on spawned process.
ETag cache and rate-limit cooldown dropped (tradeoff documented).
Pagination via --paginate for list endpoints.
Tests mock child_process.spawn instead of globalThis.fetch.
The update dialog (server and desktop) now reads changelog/index.json and
renders title, intro and groups; the release workflow builds the GitHub
Release body and name from changelog/<version>.md; oc-dev, the issue-intake
agent, AGENTS.md and the changelog skill no longer point at the file.
CHANGELOG.md stays as a legacy copy for installs up to 1.22.1, which fetch
it for update notes. The generator refreshes it while it exists and never
recreates it, so deleting it after 2026-09-19 retires it for good.
Generated outputs hold released versions only, so editing unreleased.md
never makes them stale: agents write that file and nothing else, and
oc-dev create-release does the generation.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
Every release file now carries a title: two to six plain words naming the
change a user would remember it for. The generator refuses a release without
one, promote carries the title from unreleased.md, and the unreleased
template opens with a title line. The changelog-authoring skill and the
format README describe how to write it.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
The grouped notes put the OpenCode Go header change under Improvements, so
the site's headline fallback (first bold bullet) picked Message queue. The
explicit title keeps the release's most important change on the index.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
Release notes now carry New, Improvements, Fixes, and Misc headings.
Inside the update dialog they render as small uppercase labels over
their lists instead of full-size document headings.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
The https://gitea@giteassh.buzzbee.dev:Vibing/... URL parsed the colon as a
port. Use the SSH form gitea@giteassh.buzzbee.dev:Vibing/openchamber.git
which the runner's deploy key authenticates.
rm -rf $GITHUB_WORKSPACE deleted the runner's own working directory,
breaking git clone (fatal: Unable to read current working directory).
Clone into $GITHUB_WORKSPACE/repo instead.
Releases 1.13.1 through 1.22.1 are regrouped by what each change is
(New, Improvements, Fixes, Misc) and reworded in the voice the
changelog-authoring skill asks for: one or two plain sentences per
bullet, the symptom or the capability named, no mechanism. Packed
bullets were split, every fact and every contributor credit kept.
Older releases keep the mechanical grouping.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
Release notes now live in changelog/<version>.md (front matter with
version and date, then ## App and ## VS Code sections grouped into
New, Improvements, Fixes, Misc) plus changelog/unreleased.md for what
has not shipped. `bun run changelog:build` renders CHANGELOG.md,
packages/vscode/CHANGELOG.md, and changelog/index.json from them;
`changelog:check` fails when the outputs are behind and runs in CI and
in release:prepare. `oc-dev create-release` promotes unreleased.md to
the versioned file dated today and rebuilds.
The existing history was split mechanically: every bullet kept, sorted
into groups by keyword, five hand-typed headers with one-digit days
normalised to YYYY-MM-DD (the update dialog matched none of them). The
generated files keep today's release headers, which the update dialog,
the release workflow, and the website match by regex.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
The workflow assumed $GITHUB_WORKSPACE was already a git repo, but the
host-mode runner doesn't auto-checkout. Added an explicit clone step using
the runner's deploy key (gitea@giteassh.buzzbee.dev).
The fork only needs its own deploy pipeline (.gitea/workflows/deploy-custom.yml).
All 17 upstream workflows (bot-help, pr-review, oc-review, mobile-ci, release,
stale, vscode-extension, etc.) are noise on this fork — they target upstream's
bot accounts, macOS/mobile release machinery, and comment triggers that don't
apply here. They were also failing with 'Resolve pull request context' errors
because they're designed for upstream's environment, not our fork.
Release sections now carry New, Improvements, Fixes, and Misc groups,
and the bullets are written for a user who never opened the code: one
or two plain sentences naming what they see. The changelog-authoring
skill makes that shape the primary rule, with a worked example, a
five-second reader test, and checks per bullet.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
The splash and the UI bundle both started in English on a fresh install
even when VS Code ran in a supported language, because only OpenChamber's
own saved locale was consulted. The HTML now exposes VS Code's display
language; the splash and detectInitialLocale use it until the user picks
a locale, which still wins.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
While a turn runs, the collapsed pill showed a plain Send button for a
draft that the expanded composer shows as Queue with a rotated icon, and
the pill routed it through the primary action. The pill now queues, with
the same label and icon as the expanded composer.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
The sidebar indicator kept the terminal listing loop going for as long as
the sidebar was visible, in every client, whether or not anything was
running. It now lists once on mount, to pick up runs another client
started, and keeps the loop only while a project action is known to be
running anywhere; an idle sidebar costs no polling.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
The runtime stage copied node_modules from the deps stage, but patch-package
runs in the builder stage, so a patched server-side dependency reached the
image unpatched; only the bundled dist carried the fix. Both node_modules
copies now come from builder.
Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH