Commit Graph
905 Commits
Author SHA1 Message Date
Bohdan Triapitsyn 34c8887fc1 feat(chat): tighten markdown typography and unify text selection colour
Chat prose read looser than the rest of the app: 15px body with a single
line-height for everything, headings barely larger than the text, list
bullets drawn as dashes with a 2em gutter, no vertical rhythm for lists or
quotes, and inline code that took a syntax colour from the theme.

Body text is 14px with a whole-pixel line height (22.75px rounded to 23px
so baselines stay on the pixel grid). Every block carries the same 0.65rem
margin on both sides, so any two neighbours are one spacing apart and the
outer blocks add nothing. Headings step 20/18/16/14px at weight 600 with
more air above than below. Lists use native markers that cycle by depth
with a 1.25rem gutter and 0.25rem between items; task-list checkboxes
replace the marker. Blockquotes get their left rule back. Headings, list
markers and inline code render in the text colour; inline code is a muted
chip one step smaller than the prose. The inlineCode theme tokens are
removed from the built-in themes, the generator and the VS Code adapter;
custom themes that still carry them are simply ignored.

The remaining semantic sizes shrink by the same step (code 12px, labels and
meta 13px, settings title 17px), in VS Code proportionally.

Text selection in chat used the browser default in dark themes while the
comment overlay painted the theme's row-selection token, so the colour
changed the moment Comment was clicked; that token is also nearly the page
background in several light themes and opaque in monokai, where the overlay
hid the text. Native selection, file-preview selection and the overlay now
share one translucent accent tint (primary at 30%), which stays visible over
every built-in theme's background.

Testing: ui type-check and lint; all theme JSON re-parsed; screenshots of
prose, lists and the selection/comment states in headless Chrome; contrast
of the new selection tint computed against every theme's background and
text.
2026-09-07 17:58:02 +03:00
Bohdan Triapitsyn 85c4320825 Settings storage with scopes, and project setup that can live in the repository (#3413)
* refactor(settings): settings registry and intent-gated writes

Problem: every setting lived in a flat document with ten hand-maintained
key lists that had drifted (three keys the server silently dropped, five
it kept that nothing read), and three code paths wrote to the server
without a person changing anything: the theme persist effect on mount,
bootstrap seeding of server-missing keys, and the auto-save echoing
values just adopted from the server.

Approach: one registry (packages/ui/src/lib/settings/registry.ts) names
every key with its scope (instance / profile / device), a boundary parser
and its store binding; DesktopSettings, the sanitizer, the mirror, the
apply step and the auto-save derive from it. A generated JSON snapshot
carries the key list to the server and the VS Code bridge. Writes carry
intent: the theme context writes only from its user-facing setters, a
missing server key leaves the local store alone instead of resetting it,
updateDesktopSettings drops values the server already holds, and the
auto-savers treat values applied from the server as a new baseline.

Testing: bun test packages/ui (registry + persistence suites cover zero
writes on load, dedup, toggle-back cancellation, failed-save retry, and
snapshot freshness); tsc for every workspace.

* refactor(ui): read and write settings through the shared path only

Problem: fourteen pages and stores fetched /api/config/settings on their
own and re-parsed the raw document by hand, so the registry could not
guard them and two of them treated a failed load as an empty list.

Approach: loadDesktopSettings() and updateDesktopSettings() (which now
resolves { ok }) replace every direct call; SkillsCatalogPage and
AddCatalogDialog refuse to write the catalog list until it is known.

Testing: bun test packages/ui (403 files), eslint on the changed files.

* refactor(server): validate settings writes against the registry snapshot

Problem: the server whitelist was the only guard on PUT /api/config/settings
and had drifted from the client; dead keys were still persisted.

Approach: settings-helpers.js drops any key the generated registry
snapshot does not list as persistable and strips secret keys from
responses; the dead keys (markdownDisplayMode, toolCallExpansion,
typographySizes, expandedEditorToolbar, gitProviderId/gitModelId) are
gone; the profile keys that were client-only now round-trip. A drift
test requires a valid sample for every persistable registry key.

Testing: vitest run in packages/web (182 files), including the packed
tarball import.

* refactor(vscode): gate bridge settings writes by the registry

Problem: the extension host wrote any key the webview sent straight into
settings.json, and commit-message generation read the dead
gitProviderId/gitModelId pair instead of the small-model setting.

Approach: filterPersistableSettingsChanges applies the registry snapshot
before the file write; chooseBridgeGitGenerationModel honours
smallModelUseDefault/smallModelOverride ahead of the zen fallback.

Testing: bun test packages/vscode (37 files), tsc, build:extension.

* feat(settings): split the user's profile into preferences.json

Problem: one flat settings.json held instance facts, the user's
preferences and device state together, so device state travelled between
installs and the profile had no document of its own to sync from.

Approach: the server keeps one merged document for clients but routes
each key by registry scope on disk (settings-files.js): profile keys go to
preferences.json as { value, updatedAt } entries stamped when the value
changes, everything else stays in settings.json, device keys are dropped
from writes. A missing preferences.json is seeded once from settings.json,
which is left intact; an unreadable one is a failure that pauses profile
writes and never gets overwritten. Server modules that read a profile key
off the disk use the merged sync read. Electron main reads the theme mode
from both files and now owns the splash colours, handed over the
window-theme IPC instead of the settings document. Clients stop sending
device keys, seed them once from a pre-split document, and persist
inputBarOffset locally. The PWA manifest keys are instance facts.

Testing: vitest in packages/web (seed, split write, timestamp retention,
unreadable file), bun test in packages/ui and packages/electron, tsc for
every workspace.

* feat(vscode): write the profile to preferences.json from the extension host

Problem: the extension host writes the shared settings files directly and
had to follow the server's split, and its file writes reported success on
failure.

Approach: settings-files.ts mirrors the server's format and split rules
(seed once, unreadable preferences.json is a failure); persistSettings
routes profile keys to preferences.json and the rest to settings.json,
and the atomic writers now throw so a failed save reaches the webview.
Clearing a key now actually removes it from the owning file.

Testing: bun test packages/vscode (38 files), tsc, build:extension.

* feat(settings): store the per-surface profile fields by surface kind

Problem: theme, chat-layout switches and typography sizes are one value
for every client of an instance, so the phone and the desktop cannot
disagree without a hard-coded runtime branch.

Approach: every settings request carries the client's surface kind in the
x-openchamber-surface header (web, desktop, vscode, mobile — the phone app
and the hosted mobile shell are one kind). For the registry's perSurface
keys the store writes a changed value under fields[key].surfaces[kind] in
preferences.json and never touches the base from a surface; reads resolve
the kind's own value, then the base, then nothing. Writes without a
surface (migrations, the seed) set the base. The VS Code host is always
vscode; Electron main resolves desktop for the native window theme. The
Settings UI is unchanged.

Testing: vitest in packages/web (surface write/read, no base copy, unknown
surface falls back to base), bun test in packages/vscode and packages/ui,
tsc for every workspace, build:extension.

* fix(settings): keep a legacy copy of the profile in settings.json

The first write after the split rewrote settings.json with the instance
part only, and that write happens on startup (relay reconcile). A build
from before the split reads only settings.json, so rolling back would
have lost every preference: theme, default model, all of it.

Every write now stores the profile's base values in settings.json next
to the instance part (`legacySettingsDocumentOf`), on the server and in
the VS Code extension host alike. Current builds ignore the copy because
preferences.json wins in the merged read. When preferences.json is
unreadable the copy already on disk is kept rather than dropped.

Testing: settings-runtime tests updated for the copy; full web suite
(182 files), VS Code tests and extension build, tsc clean. Verified live
on a scratch OPENCHAMBER_DATA_DIR: all 136 keys survive startup, theme
changes land per surface, plain keys land in the base.

* feat(settings): make the UI password and tunnel preset tokens write-only

GET /api/config/settings returned desktopUiPassword and the managed
remote tunnel preset tokens to every authenticated client, including
paired phones and the VS Code webview that never need them.

Both keys are now `secret` in the registry: accepted on write, withheld
from reads. The server answers with a hasDesktopUiPassword flag; the
desktop network page shows "Password set" and sends a value only when
the user types a new one or presses "Remove password" (an empty string
clears it and turns LAN access off). The tunnel page already learned
token presence from the status endpoint. The VS Code bridge strips
secret keys from what it hands the webview while still merging them
from disk on write.

Testing: registry, i18n parity, server settings, VS Code gate tests and
tsc; workspace type-check. Verified against a scratch server: GET
carries the flag and no password, PUT with '' clears, PUT with a value
sets. The desktop-only page itself awaits the owner's run.

* fix(settings): send the surface kind as a query parameter, not a header

The packaged desktop shell (openchamber-ui://app) and the phone app are
cross-origin to the OpenChamber server, so the x-openchamber-surface
header turned every settings request into a CORS preflight the server
did not allow. Settings looked reset and every save reported "Save
failed" without reaching persistSettings. An older remote instance would
refuse the header the same way even with the allow-list fixed.

The client now sends ?surface=<kind>, which keeps the request
CORS-simple on every server version; the server reads the query
parameter and still honours the header. The header is also in the CORS
allow-list for completeness.

Testing: workspace type-check, persistence and registry tests, server
opencode tests. On a scratch server: PUT with ?surface=vscode lands
under surfaces.vscode, GET without or with an unknown surface serves the
base, the header fallback resolves. Confirmed in the owner's rebuilt
desktop and on the phone.

* refactor(settings): drop the show-password toggle from the desktop network page

With the password write-only, the field only ever holds a value the user
is typing right now; the reveal toggle and its strings are gone from
every locale.

* refactor(projects): serve project setup through the server, drop the legacy migration

The shared UI read and wrote ~/.config/openchamber/projects/<id>.json
itself: it resolved the home directory, composed the path, and used the
Files API, which only desktop and VS Code have natively and which cannot
see a remote instance's file at all. It also still carried the months-old
migration from <repo>/.openchamber/openchamber.json, which deleted files in
the folder the upcoming shared project config will use.

The client-owned keys (worktree setup commands, project actions, draft
starters) now live behind GET/PUT /api/projects/:projectId/config.
project-setup.js sanitizes and builds the view; the project-config runtime
merges a patch under the same cross-process lock the scheduled-task writers
hold, so unknown and server-owned keys survive. A wrongly shaped key is a
400, not a silent drop. openchamberConfig.ts keeps its exported functions
and is now an HTTP client. The VS Code webview handles the route locally
and bridges to the extension host, which owns the file with a TS mirror of
the sanitizers.

Testing: server tests for sanitizers, round trip, lock, and invalid patch;
client tests against a mocked route; VS Code sanitizer and bridge tests;
workspace type-check, both VS Code builds, UI isolated suite (409 files),
server projects and project-context suites. Live GET/PUT against a
running server with the owner's real project config.

* feat(projects): read the team's shared config and merge it with the personal one

A project can now carry <repo>/.openchamber/project.json (version 1:
setupWorktree, setupWorktreeWait, projectActions, draftStarters,
plansDir). The server finds the checkout from the path-derived project
id, parses the file, and answers GET /api/projects/:id/config with one
merged view: what runs at the top level, plus shared and personal blocks
so a page can edit the personal file without copying a teammate's entry
into it.

Merge rules: shared setup commands run first (a personal
setupWorktreeMode of "replace" uses the personal list only); the
personal wait flag wins when set; actions union by id with a personal
action replacing the shared one and personal hiddenSharedActionIds
dropping shared ones; starters union by type:name; the primary action is
personal only. A shared file that exists but cannot be parsed, or that
names a plansDir outside the repo, is reported as invalid with a reason
and never treated as "no shared setup". Nothing writes the repo file yet.

Client: getProjectSetup exposes the view; the existing helpers return
effective values, while the Projects page sections and the draft
starters hook edit the personal block only. Shared entries show a quiet
"shared" mark in the actions dropdown and read-only lists above the
editable ones on the Projects page; shared starter chips have no remove
handle. The VS Code extension host mirrors the parser and merge.

Testing: server tests for the parser, plansDir guard, merge table, id
round trip, and a runtime test against a temp checkout; client tests
against a mocked route; VS Code sanitizer, merge, and bridge tests; the
section test covers the shared row; locale parity; workspace type-check;
UI isolated suite (409 files). Live: GET against a temp repo with a
shared file and with a broken one.

* feat(projects): ask before the team's shared commands run, once per set of commands

Shared setup commands and shared actions come from a file a git pull can
change, and they run on the machine of whoever pulls. The first time one
would run, a dialog now shows exactly what would run and asks: "Trust and
run" or "Not this time". A "trust" answer is recorded in the personal
config against a SHA-256 of the executable parts (setup commands and each
action's id, command, and runIn; renames and icons do not count), so a
pull that changes a command brings the prompt back. Nothing asks when the
shared file has nothing that executes.

Worktree creation (session creator, new-worktree dialog, session store,
multi-run launcher, agent-manager empty state) resolves its commands
through the prompt; "not this time" runs only the user's own commands.
The actions dropdown asks before a shared action runs. The Projects page
shows "Trusted on this instance" with a "Reset trust" button next to the
shared actions. The dialog is mounted beside the app-link confirmation on
every shell. The VS Code extension host mirrors the hash and the record.

Testing: server tests for hash stability, ordering, and the trusted flag,
plus a runtime test that changes the shared file and sees trust drop;
client tests for the confirmation store (ask, trust, skip, replace mode,
newer request, failed record, reset); VS Code mirror tests; the actions
button, new-worktree dialog, and issue-2039 tests updated for the trust
path; locale parity; workspace type-check; UI isolated suite (410 files).

* feat(projects): share and unshare setup with the team from the Projects page

The repo file <repo>/.openchamber/project.json is now written by the app,
and only when the user shares something: nothing appears in a repository
until then. PUT /api/projects/:id/config/shared replaces the keys it
names over the current file, writes it pretty-printed with version first
and only the keys that carry something, removes the file (and an empty
.openchamber folder) when nothing is left, refuses a missing checkout or
a plansDir outside the repo, and records trust for the writer, who has
seen what they shared.

On the Projects page, actions and setup commands get "Share with team"
and "Make personal"; shared actions can be hidden for this user; a
checkbox switches to "Use only my setup commands". Project starter chips
get share and make-personal hover buttons. A new "Shared config" block
shows the file's path and status, the shared plans folder, and the trust
status with "Reset trust". A share is a repo write followed by a personal
write; a failure after the first leaves the item visible once, as
personal. The VS Code extension host mirrors the writer.

Testing: server tests for the patch, serialization, emptiness, the write
and removal round trip, the writer's trust record, and the refusals;
client test for the shared route; VS Code bridge test for write and
removal; locale parity; workspace type-check; UI isolated suite (410
files). Live on a scratch server: share, invalid plansDir (400), unshare
to removal of file and folder.

* feat(projects): list, edit, and move plans in the team's shared plans folder

When the shared config names a plansDir, every markdown file in that
folder is a plan on the Plans tab: listed after the user's own plans,
marked shared, addressed as shared:<file>, read and edited in place
(the raw document is written verbatim, so a plan another tool wrote
keeps its shape), and deletable. Share moves one of the user's plans
into the folder; make personal moves it back under a new id; a name
collision gets a numeric suffix. Sharing is refused, with a hint in the
panel, until a shared plans folder is set in Project settings. This
answers the request to read plans from an existing folder such as
docs/plans.

Server: the project-context runtime takes resolveSharedPlansDir from the
project-config runtime; readContext reports sharedPlansDir; POST
.../plans/:id/share and /unshare. Client: movePlan in the context store,
a shared badge and a share / make-personal button per plan row. Session
attachments reference plan ids, so an attached plan that moves has to be
attached again.

Testing: runtime tests for listing, foreign markdown titles, id
traversal, in-place update and delete, share and unshare with a
collision, and the refusal without a folder; HTTP route tests; store and
locale parity tests; workspace type-check; full web suite (183 files);
UI isolated suite (410 files). Live on a scratch server against a temp
repo: list, share, read, unshare.

* fix(server): make OPENCHAMBER_DATA_DIR move every folder, not just the flat files

The variable is documented as the OpenChamber data directory, but only
settings, preferences, auth, and push files followed it; projects,
themes, speech models, and the chats default stayed under
~/.config/openchamber. A second instance started with a custom
directory therefore read and wrote the default instance's project
configs.

Every folder now hangs off the one root. An instance that already used
a custom directory gets projects, themes, and speech-models copied in
once at startup; copied, not moved, so a second instance beside the
default one cannot strip it, and nothing is merged into a folder that
already exists. Existing managed chats are not copied, as with
OPENCHAMBER_CHATS_DIR.

Testing: migration tests for copy-once, no-merge, and same-root no-op;
full web suite; a scratch server with an empty data dir copied the real
project configs and kept its writes in the copy.

* fix(projects): keep a plan's id when it moves into or out of the repository folder

A plan moved into the repository plans folder used to be listed under a
new shared:<file> id, so a session that had attached it lost the
attachment. The manifest entry now stays with a `shared` flag that says
which folder holds the file; the id survives both directions. Only a
plan that never had an entry (one written by another tool) gets an id
when it is brought in. A personal file and a repository file may share
a name because they live in different folders.

Testing: runtime tests for share and unshare with a stable id, reading
and editing the moved plan, the suffix on a name collision, and the
adoption of a foreign file.

* feat(projects): default repository plans folder, "move to repository" wording, tooltips

Plans now have a repository folder without any setup: .openchamber/plans
by default. A custom plansDir replaces the default outright (only that
folder is read and written; moving files between the two is the user's
job), and the field's placeholder and hint say so. The move buttons on
plans are therefore always available.

The word "share" is gone from the UI: it read like publishing, while
the action stores an item in the repository so everyone who pulls it
gets it. Labels are "Move to repository" / "Move to my settings", the
badge is "In repo", the block is "Repository config", and every button
on the Projects page carries a tooltip that says what happens (the
"Move to repository" button explains that edits save first while the
form is dirty). The trust status with "reset trust" moved from the
repository block into the Worktree section next to the commands it
guards; the plan row's badge sits beside the title.

Testing: locale parity, section test, workspace type-check, UI isolated
suite (410 files), full web suite.

* fix(projects): leave the icon key out of the repository file when an action has none

Actions without an icon were written as "icon": null into
.openchamber/project.json. The key is now omitted; readers already fall
back to the play icon. Server and VS Code serializers, tests updated.

* docs: describe the repository config file and how items move into it

A new page in every locale: what stays personal and what can move into
the repository, the .openchamber/project.json format with an example
and every key explained (setup commands, actions with the supported icon
names, starters, plansDir), the merge rules, the trust prompt, and plans
in the repository. Linked from the sidebar and from Project Actions.
Translations written by hand.
2026-09-07 17:50:55 +03:00
Bohdan Triapitsyn 39fa8c1917 feat(terminal): replace ghostty-web with an in-repo libghostty-vt adapter
The terminal ran on the ghostty-web npm package plus a hand-written patch,
and every rendering bug (recycled rows, duplicated reflow fragments, prompt
artifacts) had to be worked around from outside. The emulator now is the
official libghostty-vt C ABI compiled to WebAssembly, driven by a browser
adapter ported from T3 Code (MIT, notice in LICENSE-T3CODE) and owned in
packages/ui/src/lib/ghostty. The artifact is reproducible with
scripts/build-libghostty-wasm.sh, including a workaround for Zig 0.15.2 on
macOS 27 SDKs.

On top of the port: one WASM instance per page with every tab kept mounted
and hidden tabs paused; history replayed at the PTY size it was drawn for;
shells spawned only after the first fitted grid so zsh never prints the
PROMPT_SP marker; box drawing, block elements and Powerline arrows drawn
procedurally to the exact cell so TUI borders and block logos have no gaps
between rows; a software-rasterized canvas so Gecko renders every tab's text
with the same smoothing; the symbols-only Nerd Font bundled instead of a CDN
fetch; touch selection and scrolling driven through the surface API; a copy
button in the tab strip for touch hosts; localized aria labels.

Testing: bun tests run the real WASM (reflow, palette, replay isolation,
recycled rows, box glyph geometry); viewport and view tests use a surface
double; verified in Chromium and Zen (windowed and headless) for crisp text,
new tabs, panel reopen, resize and box glyph rendering; package type-check,
oxlint/eslint on new files, web build.
2026-09-07 12:18:22 +03:00
Bohdan Triapitsyn 3132d1361a fix(sessions): keep missing-worktree relocation manual
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.
2026-09-07 02:10:06 +03:00
Bohdan Triapitsyn c2f36fb5e7 fix(terminal): replay snapshot history at the PTY size it was drawn for
Opening the terminal panel sometimes showed stray fragments on the prompt
row: zsh's end-of-line mark and pieces of the prompt path. The shell had
laid its output out for one PTY width, but the client replayed that
history into an emulator of another width (an early size estimate, a
remount, or a renderer rebuild after fonts loaded). ghostty-web's reflow
then left fragments the shell's SIGWINCH redraw never clears.

The server now reports the PTY cols/rows in every snapshot, the transport
carries them through projections and accepted resizes, and the viewport
replays a sized snapshot chunk at that size before returning to the
fitted size. The container-based size estimate only seeds newly spawned
shells and is no longer sent to a running PTY.

Tests cover the sized replay, the store chunk size, the transport
projection, and the server snapshot; verified in a production build by
reloading with the panel open and switching tabs at a changed width.
2026-09-06 22:57:43 +03:00
alvins82 d8215ef5b3 feat(work-status): add opt-in turn statistics (#3177)
Add optional completed-turn statistics without changing the existing panel layout. Separate final text delivery speed from whole-turn throughput, preserve scope and opt-in settings, and explain each metric with localized delayed tooltips.

Validated focused telemetry, lifecycle, sync and persistence tests, all-workspace type-check and lint, web builds, the 12-locale narrow layout, and full GitHub CI.
2026-09-06 22:56:27 +03:00
Howon Lee 5e7c147785 feat: add Charm Hyper quota provider (#3368) 2026-09-05 23:16:13 +03:00
𝖎𝖚𝖑𝖎𝖎𝖆 759af5a77d fix(sessions): recover sessions whose directory disappeared (#3365)
* 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.
2026-09-05 21:26:21 +03:00
Bohdan Triapitsyn f46fb718c5 fix(chat): recall the current session's prompts by default; tidy the six merged PRs
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.
2026-09-05 20:16:14 +03:00
Steffen MächtelandBohdan Triapitsyn 3df97908fe feat(chats): relocate managed chat worktrees via OPENCHAMBER_CHATS_DIR (#3135)
* 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>
2026-09-05 19:24:52 +03:00
Matt VisnovskyandBohdan Triapitsyn 1d6b15bc04 feat(chat): persist more composer input history with global or session scope; recallable with up/down arrow keys (#3035)
* feat(chat): persist input history

* feat(settings): configure input history scope

* fix(web): keep input history validation packaged

* fix(chat): preserve input history across tabs

* fix(settings): restore prompt history limit

* fix(settings): keep history deletion warning visible

* fix(i18n): restore Turkish Git empty state translations

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-09-05 19:19:03 +03:00
Muhammad ZaimandBohdan Triapitsyn 2bdd9af90a feat(ui): add composer enter-to-send toggle (#3178)
* 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>
2026-09-05 19:07:28 +03:00
Ibrahim KhanandBohdan Triapitsyn 7ea24e3c50 fix(files): stop file viewer reload loop from sub-ms mtime jitter (#1489) (#2297)
* 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>
2026-09-05 18:31:35 +03:00
Andrea VandBohdan Triapitsyn a005215458 feat(chat): remember JSON output view mode (#3072)
* feat(chat): remember JSON output view mode

* perf(chat): skip JSON preference reads for text output

* fix(chat): persist JSON view in UI settings

* fix(chat): round-trip JSON view preference

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-09-05 18:27:38 +03:00
Bohdan Triapitsyn c3a83b3181 fix(vscode): default to the VS Code display language before a saved locale
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
2026-09-05 15:08:15 +03:00
Bohdan Triapitsyn 03e14b61cd fix(terminal): key project action state by one canonical directory
The terminal store keyed its directories by trimming trailing slashes,
while the new sidebar activity indicator and the whole-server session
grouping keyed the same folder with the project-action normalizer, which
also rewrites backslashes. On Windows that split one project into two
namespaces: the sidebar reconciled server sessions under "C:/repo" while
the terminal panel and the project actions button worked under "C:\repo",
so the running-action indicator never lit up, adopted tabs were duplicated
and a stop recorded in one namespace did not guard reconciliation in the
other. The project actions button also read the store map directly with a
path normalized somewhere else, missing its own directory entry.

All terminal directory keys now come from normalizeTerminalDirectory in
lib/pathNormalization, and store reads go through getDirectoryState.
2026-09-05 14:48:25 +03:00
Bohdan Triapitsyn e24d2b2cbd fix(ui): guard the remaining comment inputs against IME composition
The IME fix covered the diff/file comment input and the browser annotation
overlay, but two other places where a user writes a comment still acted on
the Enter that confirms an IME candidate: the chat quote comment in the text
selection menu, and the in-place comment editor on a composer context chip.
Confirming a candidate there attached or committed the half-typed reading and
closed the input. Escape, which abandons a candidate, had the same problem.

Both handlers now return early on a composing keystroke, and a single test
asserts the guard across every comment input so the next one added does not
quietly skip it.
2026-09-05 14:48:17 +03:00
Bohdan Triapitsyn 4f1f9e6650 feat(sidebar): show running project actions by directory 2026-09-05 14:12:07 +03:00
Felipe GenéandBohdan Triapitsyn a12b9be443 feat(chat): Comments and review in VS Code, like the OpenChamber desktop app (#1724)
* feat(chat): render code comments as cards instead of fenced text

* fix(vscode): route Add Comment to the active session editor panel

* fix(chat): persist queued inline comments and tighten file-chip path matching

* feat(vscode): comment on code from the editor

* fix(chat): keep attached context in the message and broadcast comment removal

* fix(vscode): hold every pending comment and gate both entry points on the workspace

* fix(vscode): let only the owning surface decide its comment threads

* fix(vscode): drop a comment removed while its delivery was still in flight

* test(vscode): cover the in-flight comment removal guard

* test(vscode): cover comment removal reaching every chat surface

* fix(vscode): give up on a comment the chat never confirmed holding

* fix(vscode): retract a comment everywhere before reporting it discarded

* fix(chat): preserve queued comment cards

* fix: preserve inline comment context across send paths

* fix(chat): preserve command routing with context

* fix(chat): keep unavailable actions on normal send path

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-09-05 12:28:17 +03:00
Bohdan Triapitsyn d37ce34a2e fix(terminal): reconcile project action executions across clients (#3362) 2026-09-05 12:05:39 +03:00
Matt Visnovsky 4e0eed717d fix: Project action terminal lifecycle (#3287)
* fix(terminal): make command sessions own action lifecycle

* fix(ui): reconcile project action terminal state

* feat(ui): show running project actions in terminal tabs

* feat(ui): run project actions from linked worktrees

* fix(ui): guard project action reconciliation

* fix(ui): scope project action preview fallback

* fix(ui): default project actions to worktrees

* fix(ui): reveal project action terminals

* fix(ui): retain terminal output after snapshot replay

* fix(ui): restore running action terminals on revisit
2026-09-05 12:04:36 +03:00
Bohdan Triapitsyn 7c9fdd1ee5 fix: complete goal resume and comment input follow-ups (#3361) 2026-09-05 10:53:34 +03:00
ChangeHow f3f844463b fix(ui): preserve IME composition in comment inputs (#3228)
Thanks for extending the existing IME handling to comment inputs and documenting the browser-event checks. We will also protect the annotation Escape handlers from cancelling composition.
2026-09-05 10:49:05 +03:00
Bohdan Triapitsyn 260bfe666b fix: complete config path and review follow-ups (#3348) 2026-09-05 02:19:59 +03:00
Leonid 2d4e920fbd refactor(client): remove dead legacy file/command wrappers (#3290)
Thanks for removing the unused wrappers and keeping the live file and command APIs intact.
2026-09-05 02:14:07 +03:00
Bohdan Triapitsyn d6f0f2f23c feat(settings): retire the third-party plugin integrations
Settings → Integrations offered install cards for the Claude Code and
Cursor provider plugins. They are gone: the section, its plugin catalog,
its own i18n module and tests, the settings search entries, the page
keywords, and the two sprite icons only it used. The page now holds the
built-in GitHub and Linear cards, so it is hidden in VS Code where neither
applies; its title and description live in the settings dictionaries.

Docs follow: the Integrations page in every locale now documents GitHub
(pointing at its own page) and Linear in full, the GitHub page names
Settings → Integrations as the place to connect and covers linking an
issue or PR to a message, and the Providers pages no longer promise Claude
or Cursor subscriptions.

Claude-Session: https://claude.ai/code/session_01HB9wdLQoZX2vfyDjwv6Rso
2026-09-04 23:34:15 +03:00
Bohdan Triapitsyn 34bf631157 feat(queue): queue a message with everything the composer had attached
Queueing captured only the text and files. Context chips (inline comments,
terminal selections, browser annotations, PR comments and checks, quotes,
linked issue/PR/Linear references, pending synthetic parts) stayed in the
composer and only left with the next manual send, so a queued message the
server delivered went out without them and the chips rode an unrelated
message later.

A queued message now carries what the composer would have sent: the text
with its agent mention stripped and file mentions resolved into
attachments, the attached context as structured parts, and the skill
instruction derived from the text. The server delivers those parts in the
composer's order, the VS Code auto-send does the same, and editing a queued
message puts the chips and linked references back. A failed queue restores
the composer completely. Snapshots and broadcasts omit the captured
context like attachment payloads; a take returns it.

Claude-Session: https://claude.ai/code/session_01HB9wdLQoZX2vfyDjwv6Rso
2026-09-04 22:56:27 +03:00
Bohdan Triapitsyn caac24ac3d Merge pull request #3320 from openchamber/fix/ui-thinking-effort-draft-project-rename
Various session UI fixes: drafts, rename, worktree creation, thinking effort
2026-09-04 20:00:42 +03:00
Bohdan Triapitsyn f81bc26fe5 docs: clarify uncommitted changes warning text 2026-09-04 19:54:16 +03:00
Bohdan Triapitsyn b4a38061bc fix(ui): route answer worktrees from source session 2026-09-04 19:46:21 +03:00
Bohdan Triapitsyn f160f3aac4 Merge remote-tracking branch 'origin/main' into fix/ui-thinking-effort-draft-project-rename 2026-09-04 18:58:08 +03:00
Iuliia Ivashko cb3bc3bd1b merge: bring bulk session archiving and background worktree removal into the thinking-effort branch 2026-09-04 16:54:28 +03:00
Iuliia Ivashko 94c90a16b6 fix(ui): reconcile git state after worktree changes 2026-09-04 16:41:39 +03:00
Bohdan Triapitsyn 07fa83cc72 feat(queue): deliver queued messages from the server
Messages queued while a session is busy used to live in the browser tab and
were sent by that tab once the session went idle, so closing the tab (or
losing the connection) stranded them. The web server now owns the queue:
it persists to <data-dir>/message-queue.json, watches session.status on the
global event hub, re-verifies idleness against OpenCode before sending, and
delivers the head of the queue via prompt_async (or /command for slash
commands) with the model, agent, variant, attachments, and agent mention
captured at queue time. Failed sends stay queued and retry with backoff; a
user abort holds delivery briefly; every change is broadcast so all clients
see one queue.

The shared UI store becomes a projection of the server queue outside VS
Code (hydrate on connect, apply broadcasts, optimistic mutations settled on
the server's copy, one-time upload of locally queued messages from older
builds). Edit / send-now take the full message back from the server. A
UI-driven auto-review run asks the server to hold that session's queue.
VS Code keeps its local queue and foreground auto-send.

Claude-Session: https://claude.ai/code/session_01HB9wdLQoZX2vfyDjwv6Rso
2026-09-04 14:08:08 +03:00
Iuliia Ivashko acdf0c2e1f fix(ui): hide transient dirty state while a worktree bootstraps
A freshly created worktree transiently looks dirty until its setup
commands and initial git reset finish. The work status panel showed
those files as changes on the branch and the draft's worktree dropdown
flashed its dirty warning, and both then froze on that state because
nothing refetched after bootstrap. Make the bootstrap state (the
existing authority on unfinished creation) subscribable, suppress the
dirty probe and the changed-files row while it is pending, and force one
status fetch when it settles so the lifted gate shows the reset tree.
2026-09-04 02:35:56 +03:00
𝖎𝖚𝖑𝖎𝖎𝖆 0d8709a72e fix(worktrees): remove worktrees in the background (#3319)
* refactor(worktrees): fetch source once during creation

* fix(worktrees): remove worktrees in background

* fix(worktrees): show background removal progress

* fix(worktrees): name the worktree in removal toasts
2026-09-03 15:17:28 +03:00
James Tatum 5995802fe3 feat(worktrees): fetch remote source branch before worktree creation (#3296)
* feat(worktrees): fetch remote source branch before worktree creation

New worktrees based on a local branch that is behind its upstream now
fetch first and branch from the remote-tracking ref, so they are not
born stale. A global setting (on by default) in Settings > Behavior
controls this, and fetch failures toast a warning and fall back to
local state instead of blocking creation.

* fix(worktrees): wire fetch-source toggle to store and honor failed runtime fetches

The Behavior toggle only persisted the setting; the consumer reads the
config store at creation time, so a just-toggled-off setting kept
fetching until the next hydration. Update the store optimistically on
toggle and on page load, and roll it back when the save fails.

The VS Code runtime bridge resolves git fetches with { success: false }
instead of throwing, which the consumer read as success and silently
based the worktree on the stale remote ref. Treat any non-success
result as a failed fetch: warn and fall back to local state, matching
the web/desktop/mobile path.

* fix(worktrees): stop new remote-based worktrees from tracking the base branch

Creating a worktree with a remote start ref made git auto-track the
base branch (branch.autoSetupMerge), so with the new remote fetch every
behind-root worktree was born with upstream origin/<base> and plain
git push refused under push.default=simple.

The new branch's own upstream does not exist until its first push, and
the bootstrap deliberately refuses to write tracking config for refs
that were never fetched, so --set-upstream-to cannot re-point it.
Suppress the auto-track with --no-track on new-mode creation from a
remote ref: the branch ships with no upstream, matching the behavior
before the remote fetch until the first push sets it. Explicit
upstream keys now also win over the remote start ref inference,
aligning the create path with the validate path and the VS Code
runtime.

* fix(worktrees): keep the pre-create remote ref refresh soft

The client fetch and the server's pre-create fetchRemoteBranchRef both
refresh the same branch, and the second fetch throws on failure — so a
connection dropped between the two turned the promised soft fallback
into a rejected creation even though the remote-tracking ref was
already available locally.

The refresh is now best-effort when the ref exists locally (creation
proceeds from it) and still mandatory when the ref was never fetched,
preserving the materialization behavior for remote-only branches.
Applied to both the web server and the VS Code runtime.

* chore: ignore the .openchamber app runtime state directory
2026-09-03 14:03:32 +03:00
Bohdan Triapitsyn 2a0672e3a6 fix(ui): preserve VS Code themes during settings broadcasts 2026-09-03 12:41:59 +03:00
Bohdan Triapitsyn 85bf0a99de fix: identify OpenCode Go requests by session 2026-09-03 11:49:35 +03:00
Bohdan Triapitsyn 76128b615f feat: add Fixel font support
Adds Fixel Text as a new UI font option
Allows font sources to load from direct URLs
Keeps existing fontsource-based loading working
2026-09-03 11:49:35 +03:00
Bohdan Triapitsyn 0107abb32d fix: order concurrent instance probes and honour the probe budget
Startup warm-up, opening the switcher and the refresh button can all probe at
once, and a relay host working through tunnel retries takes an order of
magnitude longer than a loopback one — so a slow older run landed last and
replaced a fresh "ok" with its own stale "unreachable". Each host now records
which run owns its status; a status from the switch flow outranks any probe
still running for it.

Each relay attempt is also capped by what is left of the 15s budget rather than
the full per-request timeout, so an attempt started just under the deadline can
no longer run the whole 8s past it.
2026-09-03 11:49:35 +03:00
Bohdan Triapitsyn 17fd46d4e5 fix: keep instance statuses between switcher opens
The switcher held reachability in component state and replaced the whole map at
the end of a probe run. It ran once per open before the config had loaded — with
Local as the only host — so that pass wiped every other instance's status and
each open started on "Checking", including for the instance the app was
connected to and actively talking to.

Statuses move to their own module: startup warms them so the switcher opens on
real values, a re-probe replaces each value in place as it lands rather than
blanking them first, and stale entries are dropped against the loaded config
instead of a partial host list. The connected instance never reads "Checking" —
the live connection already answers what the probe would ask.
2026-09-03 11:49:34 +03:00
Bohdan Triapitsyn 8f1e0c8b5f fix: retry the relay instance probe before reporting it unreachable
The tunnel rejects everything waiting on its channel the moment one connect
attempt fails, even though it has already scheduled the next one with backoff.
That is right for app traffic, which retries for itself, but it made the
one-shot probe report a durable red "Unreachable" for a host that answers when
the user presses refresh a second later — a cold start is exactly when that
first attempt loses.

The probe now spans the tunnel's own reconnects within a 15s budget, and ends
immediately on a terminal tunnel state (auth failed, duplicate client, limit),
which waiting cannot resolve.
2026-09-03 11:49:34 +03:00
Bohdan Triapitsyn a87f068256 fix: support dev server previews over relay 2026-09-03 11:49:34 +03:00
Bohdan Triapitsyn da6ab52a65 feat: add exe.dev usage tracking 2026-09-03 11:49:34 +03:00
𝖎𝖚𝖑𝖎𝖎𝖆 e885afbe89 Improve branch switch safety and recent branch status (#3302)
* feat(ui): block branch switches on dirty trees

* feat(ui): show unpushed commits in git branch selector

* feat(ui): show recent branches in git selector

* fix(ui): persist recent branch status

* feat(ui): add mobile branch picker

* fix(ui): guard mobile branch checkout

* fix(i18n): restore Turkish git empty state labels

* feat(ui): flag dirty draft directories on the branch selector

Replaces the draft dirty-directory banner with an indicator on the branch
selector: a warning icon plus a hover tooltip that opens by itself for five
seconds when the dirty state first appears, then stays hover-only. The copy
states the situation and the options (commit or worktree) without prescribing
either.

* feat(ui): optional push in the dirty branch switch dialog

Commit-and-switch gains an opt-in "Push after commit" checkbox. When the
push fails the commit stands but the switch is cancelled with an explicit
toast, so the user is never moved off a branch without knowing its push did
not happen. Without the checkbox the toast states the commit is local only.

* fix(i18n): align dirty-directory copy across locales

* fix(a11y): name the unpushed-commit badge in the branch picker

The badge showed a bare arrow and number with no accessible name or tooltip.
Both the desktop recents list and the mobile picker now carry a localized
"N commits not pushed" title and aria-label.

* fix(mobile): push before switching dirty branches

Honor the dirty-switch dialog's push option on the mobile Changes surface.
A failed push leaves the new commit on its source branch, refreshes state, and
cancels checkout. Mobile branch selection now also shows the existing dirty
switch notice.
2026-09-03 01:42:50 +03:00
Pablo 97b89cbe85 fix(i18n): restore tr locale key parity for gitView.empty discovery keys
The parity test fails on main: tr.ts is missing discoverFailed,
discoveringRepositories, retryDiscovery and selectRepositoryPlaceholder,
so 'all locales stay in key parity with english' is red on every run.
Carried in this PR to turn the checks green; drop it if you'd rather
land it separately.
2026-08-31 21:56:04 +02:00
Pablo a42fb91daf fix(ui): stop settings save echoes from clobbering theme preferences
Every updateDesktopSettings PUT replays the server's full settings
document as an openchamber:settings-synced event, and the theme listener
adopted it unconditionally - so switching sessions across directories
(any lastDirectory/activeProjectId write) could flip the theme to
whatever the server document held at that moment. A bootstrap GET with
missing theme fields made it worse: materializeAuthoritativeUiSettings
invented useSystemTheme + openchamber defaults and the persist effect
wrote them back to the server and the scoped localStorage entry.

Theme is now adopted only from bootstrap-grade syncs (the renamed
bootstrap flag, formerly adoptWorkspace), missing fields mean 'not set'
and keep the current preference, and the materializer no longer invents
theme defaults. Cross-window same-instance theme sync still rides the
scoped-key storage event; runtime endpoint switches still adopt the new
instance's server theme.

Closes the 'theme flashes to OpenChamber when switching sessions' report;
same family as the pre-#2897 'color mode forced to light' symptom.
2026-08-31 21:18:43 +02:00
Bohdan Triapitsyn b97f2d05bb fix(chat): restore a message's attached context on revert and fork
Review comments, quotes, terminal selections and annotations were consumed
at send and never put back, so reverting pulled the message into the
composer without the context it was sent with.

Claude-Session: https://claude.ai/code/session_01TwLFeTfBnWdvbg9XyezZQx
2026-08-30 18:32:45 +03:00
Bohdan Triapitsyn 65054a5f4a feat(ui): gate the pull-request surface on GitHub, move the account into it, and GitHub sign-in into Integrations
The pull-request rail icon now appears only while GitHub is connected
(OAuth or gh CLI), like Linear; Linear sits after the walkthrough in the
default rail order. The GitHub account avatar and switcher leave the
header for the pull-request panel, where the walkthrough, refresh, and
account controls share one row and one height, and the account stays
visible on the panel's empty state. A manual refresh keeps its spinner on
screen long enough to read as work done.

GitHub sign-in moves from Settings → Git to Settings → Integrations →
Built-in integrations as a card before Linear; search and the connect
buttons follow it.
2026-08-30 14:17:12 +03:00