Commit Graph
403 Commits
Author SHA1 Message Date
Bohdan Triapitsyn cc25e58b55 fix(relay): handle dead connections and preserve usage on refresh failure
Use received frames for peer liveness and reject new requests after terminal relay failures. Keep authoritative usage snapshots separate from refresh errors, bound request lifetime, and guard concurrent refreshes and runtime switches.

Add regression coverage for relay liveness, terminal rejections, quota failures, cancellation and request deadlines. Validated with 51 focused tests, workspace type-check and lint, web build and mobile assets.
2026-09-09 17:41:14 +03:00
Bohdan Triapitsyn ea392a26cc feat(chat): collapse completed live activity with tool summaries
Keep live history compact without changing sorted rendering. Reuse Activity Default, preserve final answers, and summarize tool-result diffs under an animated disclosure.

Validated with the UI test suite, focused disclosure tests, UI type-check and lint, web production build, dead-code scan, and browser and animation fixtures.
2026-09-08 19:16:55 +03:00
0d15cdc838 fix(ui): reveal overlay scrollbar on hover or active scroll (re-port to bindScrollbar) (#3219)
* fix(ui): reveal overlay scrollbar on hover or active scroll (re-port to bindScrollbar)

Re-ports #2825's container-hover reveal onto the rewritten bindScrollbar
architecture that landed on main after the original PR branch was created.

- pointerenter/pointerleave on the scroll container reveal the thumb
  immediately (deliberate intent on every mouse-pointer runtime; inert
  on touch via an event.pointerType === 'mouse' guard, since the Pointer
  Events spec fires pointerenter on touch taps as well)
- hide-timer re-checks pointerOverContainer at fire time, so the thumb does
  not vanish when the pointer moves from container onto the sibling thumb
- suppressVisibility (chat auto-follow) still suppresses hover reveal
- on hover, schedule a re-measure so the horizontal thumb reflects the
  current geometry: if the container has horizontal overflow the thumb is
  revealed, otherwise it stays hidden. This matches the original PR's
  updateMetrics() approach and avoids the regression where onScroll (which
  does not measure) would leave a legitimately overflowing horizontal
  thumb hidden while hovering
- index.css: drop the Settings-specific overlay-scrollbar display:none
  (thumb is never permanently visible anywhere now)
- regression tests: horizontal thumb reveals on hover when overflow exists,
  stays hidden when it does not; touch pointerenter is inert; hand-off
  race (container pointerleave followed by thumb pointerover) keeps the
  thumb visible; pointerleave hides the thumb

* ci(ui): add overlay scrollbar interaction recording workflow

Records the hover-reveal/hide interaction of the overlay scrollbar
(PR #3219 re-port to bindScrollbar) as a webm + GIF via Playwright
recordVideo, converted with ffmpeg palettegen/paletteuse.

- scripts/record-overlay-scrollbar.mjs: drives hover in/out, forces
  overflow on the first .overlay-scrollbar-target so the demo works on
  a clean data dir, exports overlay-scrollbar-hover.{webm,gif}.
- .github/workflows/interaction-recording.yml: mirrors the validated
  screenshots.yml pattern (auth disabled server, Playwright chromium,
  15-min timeout), adds ffmpeg install step.

* docs(ui): add overlay scrollbar interaction recording

Animated GIF captured by the interaction-recording workflow (Playwright
recordVideo + ffmpeg) showing the hover-reveal/hide behavior of the
re-ported overlay scrollbar (bindScrollbar): thumb fades in on pointer
enter, hides again after the hide delay once the pointer leaves.

* feat(ui): adopt ScrollableOverlay in remaining native-scroll panels

Extends the overlay scrollbar (hover-reveal, hide-on-leave) to panels
that still used native overflow-y-auto scrolling:

- Sidebar (left nav): outer flex-1 scroll region
- ContextSidebarTab: full-height tab content
- SessionSwitcherDropdown: session list dropdown (preserves contentRef
  for scrollIntoView / switcher item queries)
- HelpDialog: help content region

All four merge sizing into outerClassName (flex-1 min-h-0 / h-full /
max-h-[60vh]) and keep visual classes on className, with disableHorizontal
where the original hid horizontal overflow. Type-check passes; unit test
failures in OverlayScrollbar.test.tsx and event-pipeline.test.js are
pre-existing (reproduce on pristine HEAD).

* test(ui): flush hide timer before asserting thumb hidden

The hide path always schedules a setTimeout (hideDelayMs: 0 still
schedules a 0ms timer). happy-dom runs real timers, so the test must
let the macrotask fire before asserting dataset.visible — flushing rAF
frames alone is not enough. Fixes the one failing test in
OverlayScrollbar.test.tsx (12/13 -> 13/13).

* fix(ci): make scrollbar interaction recording hover retry across targets

The record script picked the first .overlay-scrollbar-target and moved the
pointer to its center; layout/hydration order varies between CI runs, so
the hover sometimes landed on a target whose thumb cannot reveal (empty
container), failing the run. Now it iterates targets in DOM order until
the vertical thumb actually appears (or fails after exhausting all).

* fix(ci): record workflow + i18n + overlay-chrome hide

- .github/workflows/interaction-recording.yml: drop 'ref: rework' so the
  workflow checks out the PR head SHA on upstream (where 'rework' branch
  does not exist). Replace with persist-credentials: true.
- packages/ui/src/lib/i18n/messages/tr.ts: add 4 missing gitView.empty
  keys (parity fix, only tr.ts was behind en.ts). Translations are
  approximate; the parity test only checks key existence.
- scripts/record-overlay-scrollbar.mjs: on a fresh data dir the web
  build can render onboarding modals (ChooserScreen, AboutDialog,
  ConfigUpdateOverlay) that float above the MainLayout with a blurred
  backdrop. The thumb's isThumbVisible() returns true (DOM-mounted)
  but the captured frame is dominated by the modal, so the user sees
  'dialog in front, blurred background' instead of the scrollbar
  reveal. hideOverlayChrome() injects CSS to hide every plausible
  overlay root and best-effort closes known UI store dialogs.

* fix(ci): drop fork-specific ref in record workflow + add tr locale gitView.empty keys

- .github/workflows/interaction-recording.yml: drop 'ref: rework' so the
  workflow checks out the PR head SHA on upstream (where 'rework' branch
  does not exist). Replace with persist-credentials: true.
- packages/ui/src/lib/i18n/messages/tr.ts: add 4 missing gitView.empty
  keys (parity fix, only tr.ts was behind en.ts). Translations are
  approximate; the parity test only checks key existence.
- scripts/record-overlay-scrollbar.mjs: hide onboarding chrome (modals,
  backdrops, dialogs) that float above the MainLayout when recording
  against a fresh data dir, so the captured GIF shows the actual
  scrollbar reveal instead of a blurred-overlay dialog screen.
- docs/interaction-recordings/overlay-scrollbar-hover.gif: regenerate
  (489 KB) with overlay chrome hidden (same artifact as CI run
  33507731092 which passed).

* ci: noop push to retrigger 'pr checks' on a fresh runner

The 'pr checks' check on this PR's prior head (0509212c3) failed with
'releaseJob is not a function' in packages/web/server/lib/walkthrough/
routes.test.js. This test lives in upstream main and is not touched by
this PR's diff. The same flake is currently hitting PR #3265 and
feat/scheduled-preflight-gate.

Confirmed the two latest upstream main commits (bec7a82568 sidebar
sort, 40e4b6f857 request-security) do not touch walkthrough/, so the
failure is a 20ms timing flake in the test's executor Promise, not a
code regression. This empty commit triggers a new CI run on a
different runner with a different scheduling window.

* feat(settings): adopt ScrollableOverlay in settings shell and dialogs

Several settings surfaces still rendered their scroll containers with
native browser scrollbars (overflow-y-auto / overflow-y-scroll), which
read inconsistently against the overlay scrollbar used everywhere else
in the app once content exceeded the viewport.

Wrap the relevant containers in <ScrollableOverlay>:

  - SettingsView: nav sidebar (mobile), mobile fallback, mobile page
    sidebar, mobile page content, and desktop split view
  - DirectoryExplorerDialog: results list
  - GitHubIntegrationDialog: issues / PRs list
  - GitHubIssuePickerDialog, GitHubPrPickerDialog: lists
  - NewWorktreeDialog: form body

The settings sub-pages (OpenChamberPage, VoiceSettings, PasskeySettings,
etc.) do not carry their own overflow — they inherit the scroll host
from SettingsView, so the shell change is sufficient for them.

type-check, lint, and ui tests (368/369, the one failure is a pre-existing
event-pipeline flake unrelated to this change) all pass.

* ci(record): re-run overlay scrollbar recording on a fresh runner window

* ci(record): give overlay thumb 1500ms to reveal on hover

The 'hover did not reveal the thumb on any target' check has been
flaky across runs since the workflow landed in this branch (about
half the runs fail with the same error). 700ms was tight on cold
GitHub-hosted runners; 1500ms absorbs the cold-start variance
without changing what the GIF captures (the thumb's hide animation
runs after pointerleave, unaffected by the longer pre-leave wait).

* ci(record): debug thumb visibility timing on hover

* ci(record): drop debug logging, keep 1500ms hover wait

Debug logging was used to identify that 200ms is enough on a
healthy runner, but 700ms was not. The flake was the OpenChamber
server being slow to initialize the OpenCode side on cold
runners — the thumb itself renders quickly once the app is up.
Keeping 1500ms absorbs that cold-start variance without slowing
successful runs by more than the GIF's existing post-hover
animation wait (1000ms hideDelayMs).

---------

Co-authored-by: sergiofspedro <sergiofspedro@users.noreply.github.com>
Co-authored-by: openchamber-ops <ops@openchamber.dev>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-09-08 00:11:47 +03:00
Bohdan Triapitsyn 2ec0238e5c feat: add an always-show scrollbars preference
Adds a device-local setting to keep overlay scrollbars visible.
Surfaces the setting in visual settings and settings search.
Updates scrollbar behavior and tests for the new preference.
2026-09-07 23:16:36 +03:00
ChangeHowandBohdan Triapitsyn 5ae1a949c8 fix(ui): improve composer focus, keyboard navigation, and settings (#3376)
* fix(ui): make composer keyboard interactions consistent

* docs(settings): refine description visibility guidance

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-09-07 20:30:19 +03:00
Pablo Gonzalez b238cd7fbc fix: parse ollama cloud cost-based usage windows and credits balance (#3381)
Ollama Cloud's /settings page switched shapes for cost-based plans: parse
the 'Monthly usage $X of $Y used' row as a monthly window with a symmetric
'$X / $Y' money label that reads correctly in both used/remaining display
modes, and surface the Extra usage credits balance as a balance-only
credits_balance window matching the Codex/DeepSeek credits treatment
('Credits Balance' in the UI), omitted when the balance is $0. The VS Code
credential validation accepts the new page shape and the Settings cookie
placeholder shows the two-cookie format. Web and VS Code parsers kept in
sync per quota DOCUMENTATION.md.
2026-09-07 19:25:52 +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 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
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
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 260bfe666b fix: complete config path and review follow-ups (#3348) 2026-09-05 02:19:59 +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
𝖎𝖚𝖑𝖎𝖎𝖆 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 da6ab52a65 feat: add exe.dev usage tracking 2026-09-03 11:49:34 +03:00
Bohdan Triapitsyn bdda3c36bc release v1.22.0 2026-08-30 19:24:09 +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
Bohdan Triapitsyn 391f938334 feat(voice): match local and macOS voices to the language of the text
Text-to-speech picked one voice regardless of what language a reply was in.
A dependency-free language detector (script, marker letters, function words)
now decides the language of the whole message once; with the new
"Match the voice to the language of the text" setting the local provider
switches to a catalog model for that language (Kokoro zh/en and Piper models
for 12 languages, downloaded on first use like the existing model) and macOS
say switches to an installed voice whose locale matches. The local voice
picker lists voices of every installed model, and the settings show which
language models are on disk.

The Ukrainian Piper medium build is a character-level model that sherpa-onnx
turns into noise, so the espeak-based Lada build is used instead.

Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98
2026-08-30 02:24:22 +03:00
Alex Kutas 49f0a9e62f OPE-296: Add linear integration for starting sessions from issues (#3235)
* feat(linear): start sessions from Linear issues
Authorize a Linear workspace on this OpenChamber server, map teams to
projects, attach an issue from chat, start a session or worktree from an
issue, and post started/completed/failed comments that open the session.
Hidden in VS Code.

* feat(linear): connect more than one Linear workspace

Store each OAuth grant on this OpenChamber server and keep one current, so Settings can add and switch workspaces without dropping the others. Project mapping is per workspace. Remove the Linear button next to New Chat; start-from-issue stays on New Worktree.

* feat(linear): add a right-hand issues panel

Browse and filter issues in the rail, open a card to change status or start a session, and collapse search plus most filters to icons on a narrow panel.

* feat(linear): open issues in the rail and filter by Linear status

The rail icon only shows after Linear is connected. Clicking a Linear row on work status opens the panel. Status options match the card, including Done, Canceled, and Duplicate. The Integrations experimental warning sits under Third-party integrations.

* fix(linear): use stable OAuth callback broker

* fix(chat): preview Linear issue attachments

The context switch missed linear-issue, so tsc treated the preview helpers as incomplete.

* fix(ui): restore Linear i18n parity and the #2903 sync harness

Turkish was missing the Linear dictionaries, and the subagent test still wrapped only SyncContext after reads moved to SyncRuntimeContext.

* fix(linear): drop changelog hunks and close review races

Keep changelogs out of this PR, restore CodeMirror ranges, ignore stale Linear list pages, and leave a persisted Linear tab open until auth has actually resolved.

* fix(linear): tint active issue filters and clear them in one click

* fix(markdown): read escaped brackets as text, not display math

`\[...\]` is display math in LaTeX and an escaped bracket pair in
CommonMark. The block tokenizer claimed every `\[`, so prose like
`[title \[Bug\] more](url)` was handed to KaTeX: "Bug" rendered as a
centered formula and the block token split the paragraph, tearing the
link into three pieces. Linear, GitHub and any other source that escapes
brackets the way CommonMark requires hit this.

Display math now has to own its line — `\[` starts one and `\]` ends
one. A formula on its own line still renders; `\[` mid-sentence stays an
escape, which is what CommonMark says it is and what prose almost always
means. Inline `\(...\)` keeps the same ambiguity, but inline math is
legitimately mid-sentence, so there is no position to judge it by.

Covered by regression tests, including the verbatim comment body that
surfaced this.

* feat(linear): make session status comments opt-in and public-only

A status comment lands in a Linear workspace the whole team reads, and
the link it carried pointed at whatever origin started the session —
usually loopback or a LAN address. Everyone but its author got a dead
link, and nobody had agreed to the comments in the first place.

Comments are now off until the user turns them on in Settings ->
Integrations -> Linear, and the check lives on the server: the event hub
posts completed and failure without going through the interface, so a
client-side gate would not hold. When the resolved origin is not
publicly reachable the server posts nothing at all rather than a link
only its author can open; `isPublicSessionOrigin` rejects loopback,
private LAN, carrier-grade NAT, link-local and single-label hosts. The
desktop deep-link origin is gone with it, since no one else can follow
one either.

The comment body also dropped the session title. It repeated the issue
the comment already sits on, and issue titles routinely carry brackets
("[Bug] ...") that broke the markdown link. The body is now one short
link, and `sessionTitle` is gone from the route, client and types.

Also caps the dedupe file at the newest 500 sessions; it grew forever.

* fix(linear): match the pull request panel and clear review findings

Comments in the Linear panel now render as the same avatar timeline the
pull request panel uses, with the shared time-format preference instead
of a raw locale string. Comment authors carry `avatarUrl`, which the
GraphQL selection was not requesting.

Review findings from the same pass:

- `status-runtime.js` hand-rolled `typeof` narrowing and failed the
  vendored anti-slop lint; it now parses through `parse.js` like every
  other file in the module.
- `useLinearAuthStore` turned any failed request into `connected: false`
  with `hasChecked: true`. Since the rail icon, the composer entry and
  the worktree option all gate on `connected === true`, one network blip
  hid Linear for the rest of the session, and Settings only re-checked
  when it had never checked. It now keeps the last known status and
  leaves `hasChecked` false so the next caller retries.
- `LinearIssuesView` (1096 lines) was a static import in `ContextPanel`,
  shipping in the main bundle although its rail icon stays hidden until
  a workspace is connected. It is lazy now, like `GitView`.
- Dropped dead code: the unused port helpers left over from the loopback
  callback, two re-exported default values nothing read, and a redundant
  export in `linkedIssues`.
- Integrations is no longer badged beta.
2026-08-30 02:18:40 +03:00
Bohdan Triapitsyn 48bcac1758 fix: harden and de-slop the merged sidebar/chat/settings batch
Post-merge follow-ups for #2740 #2735 #2734 #2690 #2676 #2738 #2684
#2689 #2733 #2739 #2462 #2687 #2736 #2618 #2697, plus three regressions
found while reviewing them:

- ctrl/cmd+digit while typing no longer switches session tabs (#2503 was
  still open in practice: the guard only covered the mod+alt surface binding)
- Shiki template-call sanitizer now covers every bundled grammar, including
  the js/ts aliases and embedding grammars; timed-out highlight requests are
  memoized and no longer cancel unrelated in-flight requests
- settings flush on suspend uses keepalive and also fires on Capacitor
  appStateChange; keeps the selected model persisted across mode switches
- remote-only branches fetch before checkout; range helpers fail clearly
- git status invalidation now fires for runtime adapters too
- settings number inputs and select triggers size in ch so they scale with
  the interface font
- recent-activity timestamps tick from one list-level ticker
- Markdown preview find goes through the shared find_in_file keybind with
  containment, no longer counts its own bar, and debounces observer runs
- #2676 reverted; #2524 fixed by fading the sticky header's own background
  instead of overlaying the content below it
- sticky group headers in the model picker and sidebar render again
  (oc-sticky-fade-scroller class restored after 9b9d7069c)
- project switcher names are left-aligned again (wrapper lost in 26dbc2f30)
- tool card quick-open icon is always visible and opens the same line as the
  expanded card's button
- tautological tests replaced or removed; new oxlint findings fixed
2026-08-29 01:06:43 +03:00
Bohdan Triapitsyn c788cd2237 fix(providers): keep declared env vars as a credential signal
Providers that read credentials from several environment variables
(Bedrock, Azure, Vertex) never get a single resolved Provider.key from
OpenCode, so dropping the env signal made them show "Credentials
missing", auto-open the auth panel, and hide their models even when
fully configured.

providerHasCredentials takes an envDeclared input again, and both
call sites in ProvidersPage pass whether the provider declares any
non-empty env var name.
2026-08-29 00:50:49 +03:00
Bohdan Triapitsyn e13f27c1a3 Merge remote-tracking branch 'origin/main' into port-2884
# Conflicts:
#	CHANGELOG.md
2026-08-29 00:49:02 +03:00
Bohdan Triapitsyn 63755a8e45 Merge remote-tracking branch 'origin/main' into port-2619
# Conflicts:
#	packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx
2026-08-29 00:26:49 +03:00
Bohdan Triapitsyn 5332d124a6 fix(settings): let fixed-width controls scale with font size and density
Merge main and resolve conflicts in RemoteInstancesPage.tsx (re-applied
the width bumps by hand at their current locations) and
KeyboardShortcutsSettings.tsx (dropped the PR's hunk entirely: main
replaced that fixed-width input with ShortcutRecordingDialog, which has
no equivalent clipping at 200% interface font size).

Closes #2320
2026-08-28 23:45:12 +03:00
Bohdan Triapitsyn 7c1902b9e9 fix(settings): drop the class-name assertion test and redundant truncate from the passkey row 2026-08-28 23:03:16 +03:00
Gautam0507andRepro Agent 8fee25c97b fix(ui): let SettingsFieldRow labels truncate on long content (#3196)
* test: reproduce long passkey label overlay (issue #3181)

* fix(ui): let SettingsFieldRow labels truncate on long content

---------

Co-authored-by: Repro Agent <repro-agent@example.com>
2026-08-28 23:02:50 +03:00
herjarsa 7d20f2897a docs(providers): drop stale disconnect comment; rewrite changelog bullet
Two nits from the openchamber-bot review at 65668f14:

- The disconnect handler carried a stale comment that referenced a
  "removed:false payload gating" feature the rebase removed; the
  helper below it does not do that gating. Drop the comment.
- The Hide button onClick ran setAuthPanelDismissedForId inside the
  setShowAuthPanel updater (idempotent today, impure under StrictMode
  double-invoke). Move both calls outside the updater.

Also rewrite the [Unreleased] changelog bullet to describe the
user-visible change (Connected + models visible for options.apiKey
providers) instead of internal mechanics ("source refetch + optimistic
auth mark"), and broaden the wording from "after saving" to cover
OAuth, custom-provider, and disconnect paths.
2026-08-28 18:22:30 +02:00
herjarsa 65668f1453 fix(providers): wire optionsApiKey through both call sites; restore editable custom exemption
The previous fix added optionsApiKey to providerHasCredentials but
wired it through only one of the two call sites in ProvidersPage.tsx.
The auto-open effect at line 339 still omitted it, so a config-defined
provider whose only credential is options.apiKey would get the auth
panel force-opened on every selection while the summary beside it
said Connected; the dismissal resets on provider switch, so the
panel re-opens each time.

Restore the isEditableCustomProvider exemption that main's
requiresProviderAuth helper carried into both authStatusIncomplete
and shouldShowModelsSection. A keyless local custom provider (LM
Studio / Ollama style) regressed from 'models visible, no banner' to
'Credentials missing' with the models section hidden.

Rewire requiresProviderAuth (its only production consumer was lost
in the rebase) by delegating authStatusIncomplete to it. The helper
already encodes sourcesLoaded && !hasCredentials && !isEditableCustom
Provider, so the call site is one line and the contract matches main.

shouldShowModelsSection now accepts an optional isEditableCustom
Provider flag that lifts the credential gate for editable providers,
matching the exemption the ProvidersPage.test.ts:22 fixture asserts.

Tests: 15/15 pass. Was 14 in the previous commit; added one test
for the editable custom exemption.
2026-08-28 17:47:34 +02:00
herjarsa 75d93f79c0 fix(providers): treat options.apiKey as a credential in credential signal
The previous review flagged that providerHasCredentials misclassifies
working config-defined providers: Provider.key is only set by upstream
when exactly one declared env var resolves or an api-type auth.json
entry exists. Config providers only get options, so provider.<id>.
options.apiKey never reaches key. Result: a provider whose key is
embedded in opencode.json showed 'Credentials missing', lost the
Models section, and forced the auth panel open.

Add optionsApiKey to ProviderCredentialInput and check it in
providerHasCredentials alongside key and authSourceExists. This
matches what main's requiresProviderAuth helper used to do and
honors the OpenChamber docs contract that options.apiKey counts as
a usable login (walkthrough/DOCUMENTATION.md:134).

Wire the new field through ProvidersPage.tsx using a typed
indirection: the SDK Provider type does not yet expose options
publicly, so the read site casts the object to the known shape.
This keeps the call site type-safe without waiting for an SDK
update.
2026-08-28 16:59:52 +02:00
herjarsa addcbfe447 fix(providers): address bot blockers for stale Credentials missing state
- shouldAutoOpenAuthPanel: re-introduce isEditableCustomProvider exemption
  (was dropped when requiresProviderAuth was replaced). Custom providers are
  editable directly in the form and must not be force-opened into the auth
  panel on a stale sources snapshot.
- handleSaveCustomProvider + handleDisconnectProvider: route through
  applyConfigReloadOrRecordDeferred so an externally managed OpenCode that
  throws requiresManualRestart records deferred-restart guidance instead of
  toasting a misleading 'mutation failed' for a write that already persisted.
- handleOAuthConnected: call markAuthWriteSucceeded so the page does not stick
  on a stale 'Credentials missing' summary while the providers refresh lands
  (OAuth previously only updated the deferred-restart payload).
- Sources effect deps: add settingsDirectory back so a directory switch while
  the provider id is unchanged refetches the source snapshot (was swapped for
  providerSourcesRevision in the prior rebase).
- Cleanup: drop dead oauthCodes state and copyTextToClipboard import; restore
  the requiresOpenCodeRestartAfterOAuth import alignment.

13/13 ProvidersPage.test.ts still green.
2026-08-28 16:54:45 +02:00
herjarsa d0e594ab1e docs(changelog): [Unreleased] entry for providers credential refresh (#2884) 2026-08-28 16:54:45 +02:00
herjarsa 06f87c17b0 fix(providers): drop dead handlers from rebased PR 2026-08-28 16:54:45 +02:00
herjarsa 08883ff98f fix(providers): import helpers, declare oauth state, drop conflict marker 2026-08-28 16:54:44 +02:00
Cursor AgentandSerhii Dziupin 82bb814af7 fix(providers): refresh credentials after auth save
Stop treating Provider.env length as credentials, refetch provider
sources after OAuth/API key writes, and respect an explicit auth-panel
Hide so OAuth-only providers do not stick on a stale "Credentials
missing" / empty-models state after a successful login.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-28 16:54:44 +02:00
Bohdan Triapitsyn b8465ae133 fix: harden and de-slop the merged contribution batch
Follow-ups promised on merge, plus review findings on the batch itself:

- chat: task-tool output now respects the 512KiB render cap; quick-open
  icon is visible at rest on coarse pointers and reachable by keyboard
  (row keydown no longer swallows inner-button Enter/Space); composer
  inline-code decoration drops the metric-shifting padding; a btw fork
  send carries only the boundary instruction, never the promotion notice
- sync: cascade revert/unrevert aborts busy descendants, busy state is
  read from every child store at the moment of use; rule 9 documents
  redo clearing all descendant revert markers
- electron: renderer recovery keeps memory-eviction (a valid
  render-process-gone reason) and both windows share one
  attachRendererRecovery helper
- vscode: process registry is a thin re-export of the web module
  (provider-env-aliases precedent) with ordered register/unregister
  writes and an awaited close
- server/cli: managed-process registry takes injectable deps (fixes the
  unreaped-orphans ReferenceError), corrupt settings errors name the
  file, getWorktrees test restores console.warn
- tests: module-mock harnesses removed (AgentsSidebar, SettingsView
  mobile focus — behaviors stay live but uncovered, accepted trade),
  QuestionMarkdown asserts rendered DOM
- i18n: German gains the debug-panel request keys, Japanese/German drop
  removed worktree keys, Ukrainian unit spacing fixed
- changelog: Copilot AI Credits entries (main + VS Code)
2026-08-28 02:08:09 +03:00
Bohdan Triapitsyn f64d8b59c8 Merge pull request #3109 from bashrusakh/fix/2982-mobile-agent-duplicate
fix(agents): open duplicate editor on mobile
2026-08-28 01:23:34 +03:00
Bohdan Triapitsyn 1d423c32d5 Merge pull request #2314 from floze-the-genius/fix/2312-github-settings-disconnect
fix: keep GitHub settings mounted during refresh
2026-08-28 01:23:10 +03:00
Ayoub Achour 61741e58c4 fix(tunnels): update cloudflared download link 2026-08-27 04:10:09 +01:00
𝖎𝖚𝖑𝖎𝖎𝖆 057a4447a3 fix(ui): preserve default in thinking cycle (#3153) 2026-08-26 20:12:01 +03:00
Bohdan Triapitsyn 7977842e1e feat(ui): redesign the default shortcut layout around a mod+k leader
Single chords stay for everyday actions; open/go actions move to two-step
mod+k sequences; held mod+digit switches header session tabs and held
mod+alt+digit switches context panel surfaces. Rare actions leave the
shortcut schema for the command palette, every remaining action ships with
a default binding, and stored overrides from the old layout reset once.
Key matching now follows the physical key on non-Latin layouts and for
Option-modified digits on macOS, including in the recording dialog.
2026-08-26 14:52:31 +03:00
Bohdan Triapitsyn 94f6b5fd38 feat(ui): migrate all shortcut surfaces to the centralized registry 2026-08-26 10:59:04 +03:00
Bohdan Triapitsyn f1c3870909 feat(ui): land the centralized shortcuts core from #2532 with review fixes
The schema/config/bindings/registry/dispatcher module, useKeybind hooks,
recording dialog, reworked shortcuts settings page, help dialog, and the
localized action labels — re-based onto current main rather than merged
(the branch predates 440+ commits including the session-tabs shortcuts).

Review fixes applied on top of the original:
- close_session_tab (alt+w) joins the schema with labels in every locale;
  it shipped on main after the PR's base and would otherwise silently die.
- switch_context_surface's special-case in conflict resolution is now a
  declared prefixStyle config property instead of a magic id string.
- Duplicate handler registration warns in dev builds.
- The risky-browser-shortcut warning inspects every chord and covers
  mod+q/d/h/j/o/u plus mod+shift+w/q.
- The dispatcher remembers which target armed a two-chord prefix so the
  window-level completion handler can distinguish a deliberate sequence
  from typing in an editable field (guard lands with the dispatch hook).
- Schema tests: unique normalized default bindings enforced, and the
  flat-file-era override format proven to keep resolving.
2026-08-26 10:47:44 +03:00
Bohdan Triapitsyn dac31ee026 feat(settings): add streaming auto-follow toggle
New Streaming section on the Chat settings page with a checkbox that
controls whether the viewport follows new content while a response
streams. Default stays on. With it off, the anchored user message still
parks at the top on send, but no glide or end-follow correction runs and
the list's maintain-scroll-at-end stays disabled; the scroll-to-bottom
pill and session open keep scrolling explicitly.

Persisted through desktop settings like the other chat toggles (auto-save
diff, authoritative apply, sanitize), registered in settings search, and
localized in every locale.
2026-08-25 14:53:43 +03:00
Bohdan Triapitsyn b6e14cbf21 refactor(settings): always-docked editor toolbar; reorder Navigation
The 'Always show editor toolbar' preference is gone — the docked
toolbar under the file tabs is now the only mode, and the floating
hover toolbar branch in the files editor (with its open-state and
outside-click machinery) is deleted. The stored preference is dropped
by a store migration and removed from desktop settings persistence,
settings search and every locale.

Navigation section order now reads: file editor keymap, auto-save,
terminal shell + login shell, Terminal Quick Keys, and the Session
tabs group last.
2026-08-25 00:32:09 +03:00
Bohdan Triapitsyn f40a247be9 fix(settings): include the Session tabs toggle in the General page's visible list
The General section renders OpenChamberVisualSettings with an explicit
visibleSettings allowlist, so the new control was searchable but never
rendered.
2026-08-25 00:20:37 +03:00
Bohdan Triapitsyn ce90e7e24f feat(settings): Session tabs toggle in General → Navigation
A new Session tabs group (web/desktop only) turns the header session
tabs off; disabled, the header renders the exact pre-tabs view — plain
session title with meta row and the always-visible session menu (the
same block VS Code uses). The Alt+W close-tab shortcut no-ops while
tabs are off. Registered in settings search with a matching anchor;
labels translated in all locales.
2026-08-25 00:05:45 +03:00
bashrusakh a894a80c0d fix(agents): open duplicate editor on mobile 2026-08-25 07:37:02 +11:00
Bohdan Triapitsyn 841eca5720 feat(surface): switch app shells when the viewport crosses the phone threshold
The mobile-vs-desktop surface is stamped once at boot, so a browser
window narrowed past the phone threshold kept the desktop shell (and
its legacy squeezed layout) until a manual reload. A viewport watcher
now reloads into the other shell once the resize settles — the same
mechanism the old Settings toggle used. Fixed shells (Capacitor,
desktop, VS Code) and ?surface= overrides never switch.

With the new mobile app reachable this way, the old/new mobile layout
preference is gone: phones always get the mobile app.
2026-08-24 16:08:04 +03:00
Bohdan Triapitsyn b8716fe808 refactor(search): unify dropdown filtering on the shared ranked matcher
Branch, project, agent, model, provider, stash, SSH-host, skill-catalog
and archive filters each had their own toLowerCase().includes (or no
ordering at all); the git branch and gitmoji pickers also let cmdk
re-filter and reorder on top of the manual filter, silently dropping
rows. All of them now go through rankByQuery/matchesRankQuery: results
are relevance-ordered, multi-word queries match in any order, matching
ignores punctuation, and cmdk filtering is disabled where the ranked
list is already final. rankBranchesForQuery keeps relevance order
instead of re-sorting matches alphabetically; the model picker now also
matches model ids.
2026-08-24 15:00:52 +03:00