Commit Graph
3096 Commits
Author SHA1 Message Date
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
𝖎𝖚𝖑𝖎𝖎𝖆 9ef235a6b4 feat(sessions): add exact ID search to sidebar and archive (#3428) 2026-09-08 15:42:32 +03:00
Bohdan Triapitsyn 05915e2859 fix: keep task tool spacing consistent
Keep top padding on task tool entries and simplify the Turn stats release note to state its default without an instruction.

Testing: focused ToolPart ESLint, changelog validation, and diff checks passed. No interactive layout verification was run.
2026-09-08 01:40:08 +03:00
Bohdan Triapitsyn 9e2163d839 feat: enable turn stats by default and prepare release notes
Turn stats was hidden unless users opted in. Show it by default, migrate implicit hidden lists, and preserve explicit section choices through reloads and settings sync.

Prepare App and VS Code release notes with Turn stats as the headline and BTW composer changes under improvements.

Testing: 115 focused tests passed; UI type-check passed; UI lint has one existing warning. Changelog validation and diff checks passed. Oxlint findings are limited to existing code in sections.ts; interactive app validation was not run.
2026-09-08 01:38:28 +03:00
Bohdan Triapitsyn 5df08ade90 fix: add divider before final live assistant answer
Shows a separator when the last live assistant answer follows earlier visible content
Keeps render comparisons in sync with the new assistant-text context
Tweaks tool summary spacing and path text styling
2026-09-08 01:16:36 +03:00
Bohdan Triapitsyn 6ba9125635 fix: prevent overlay scrollbar from shifting content
Reserves scrollbar space in overlay overlays when the thumb appears
Keeps text and layout from jumping on hover
Avoids double-reserving space for nested scrollers
2026-09-08 00:45:12 +03:00
Bohdan Triapitsyn a333fa9c34 docs: clarify always-show-scrollbars hint text
Updates the setting hint to explain scrollbars stay visible when the pointer leaves the scrollable area.
Refreshes the wording across all supported locales.
2026-09-08 00:21:46 +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 4e00446adb fix(settings): allow overlay scrollbars in settings 2026-09-07 23:27:42 +03:00
Bohdan Triapitsyn 0753add360 Merge main into bohdan/dev after BTW composer integration 2026-09-07 23:18:17 +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 02581d08c5 feat(chat): turn /btw into an isolated composer (#3398)
* feat(chat): turn /btw into an isolated composer

* fix(chat): preserve direct BTW sends and isolate pending preparation

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-09-07 23:13:07 +03:00
Bohdan Triapitsynandalvins82 7b206b1014 fix(chat): release thinking follow when dragging the scrollbar
Retain the capped reasoning implementation and adopt the scroll-position release behavior from #3394. Cover pause, resume, and observer cleanup on the mounted component.

Co-authored-by: alvins82 <alvins82@gmail.com>
2026-09-07 20:38:00 +03:00
Bohdan Triapitsyn f9d6b5c479 fix(updater): verify native host version and report restart failures
Complete #3227 by keeping browser completion polls on the native updater, checking the requested version, and preserving retry access after a failed restart.
2026-09-07 20:38:00 +03:00
Bohdan Triapitsyn c82aa7879b fix(git): honor standalone credential-helper permission
Complete the createGit option handling introduced in #3383.
2026-09-07 20:37:47 +03:00
Bohdan Triapitsyn cffd903f56 fix(sync): reject obsolete interrupted-turn recovery responses
Follow up #3396 with runtime, SDK, and request ownership guards across hydration and reconnect.
2026-09-07 20:37:47 +03:00
Bohdan Triapitsyn 45bc61f8f9 fix(network): apply connection timeout in server and extension hosts
Complete the runtime entrypoints from #3404 without changing address-family selection.
2026-09-07 20:37:46 +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
ChangeHow 1306b1124c fix(updater): use desktop host updater from web (#3227) 2026-09-07 20:26:59 +03:00
ChangeHow 928bb3eae0 fix(chat): shrink markdown table wrappers to column widths (#3377) 2026-09-07 20:26:43 +03:00
SRnChito 0459c8418b fix(git): pass allowUnsafeCredentialHelper for token identity switching (#3383)
simple-git 3.35/3.36 moved its unsafe-config blocklist into
@simple-git/argv-parser and expanded it to include credential.helper.
setLocalIdentity already opted in for the SSH branch (core.sshCommand)
via createGit({ allowUnsafeSshCommand: true }), but the token branch
(addConfig("credential.helper", "store")) was left without the matching
opt-in, so switching to a token-auth identity throws:

  Configuring credential.helper is not permitted without enabling
  allowUnsafeCredentialHelper

Forward a new allowUnsafeCredentialHelper option through createGit and
enable it in setLocalIdentity alongside the existing SSH opt-in. Cover
the token branch (and the token -> SSH cleanup) with tests mirroring
the existing SSH case.
2026-09-07 20:26:38 +03:00
Pablo Gonzalez cf02d405c6 fix(quota): drop NeuralWatt key-name valueLabel so allowance windows render percent (#3385) 2026-09-07 20:26:33 +03:00
Andrea V 21011cfe0a fix(chat): restore fork prompts in the destination composer (#3387) 2026-09-07 20:26:26 +03:00
alvins82 a059d54b44 fix(ui): reconcile stale active tools after reload (#3396)
* fix: reconcile stale active tools on materialization

* fix: recover stale turns after reload
2026-09-07 20:25:42 +03:00
ouyangjian28 bf4262dbc9 fix(desktop): keep non-ASCII desktop entries out of Open In matching (#3403)
desktopEntryMatchesApp normalized haystack values without dropping empty
ones, so a .desktop entry with no ASCII letters or digits in Name, id,
file name, or Exec (e.g. Name=抖音) normalized to "" and
needle.includes("") matched every requested app — hijacking the
installed-apps list and Open In launch specs. Empty normalized haystack
values are now filtered, matching the existing needles handling.

discovered-apps.json gains a version field (INSTALLED_APPS_CACHE_VERSION
= 2); caches written before the fix are treated as stale and refresh
through the existing TTL-expiry path instead of serving the poisoned
list for the rest of the 24h TTL.
2026-09-07 20:25:30 +03:00
ouyangjian28 c779b765d9 fix(quota): raise Node's 250ms connect-attempt cap for provider fetches (#3404)
Node's happy-eyeballs default (autoSelectFamilyAttemptTimeout=250ms)
aborts every connect attempt to provider endpoints whose TCP handshake
exceeds 250ms, so quota fetches from Node processes fail with
"fetch failed" while the Bun-based opencode CLI path succeeds (#3399).
Raise the per-attempt cap to 5s via a shared applyConnectAttemptTimeout()
helper called from the two Node entrypoints that host the quota module:
the Electron main process and the openchamber CLI. Family autoselection
stays enabled (::1 and IPv6→IPv4 fallback preserved); runtimes without
the setter are a no-op.
2026-09-07 20:25:24 +03:00
Maxim Topciu fc61b10109 feat: add search to the new session project picker (#3408)
* feat: add search to the new session project picker

* fix: punctuate the project picker empty state
2026-09-07 20:25:19 +03:00
Maxime Leduc 0e836e7b75 fix(quota): read OpenRouter usage from /api/v1/key (#3411)
The OpenRouter provider called GET /api/v1/credits, which OpenRouter
documents as requiring a management key. Called with a normal inference
key it returns HTTP 200 and {"total_credits":0,"total_usage":0} instead
of an error, so the provider rendered "$0.00 left - $0.00 spent" for a
funded key and the !response.ok guard could never catch it.

Read GET /api/v1/key instead, which is documented as callable with any
valid API key. A key with a spending limit reports its own usage against
that limit in the window named by limit_reset, and a key without one
reports usage_monthly. Window usage is limit - limit_remaining rather
than usage, because usage is all-time and limit_remaining tracks the
current reset window. limit_remaining is also server-computed and
already honors include_byok_in_limit.

Bring the provider up to the deepseek.js standard while here: a 15s
timeout, 401 and 403 mapped to a session-expired message, parse failures
mapped to an invalid-response message, an explicit no-quota-data result,
and the aliases export that quota/DOCUMENTATION.md requires. Add the
missing openrouter.test.js and keep packages/vscode in sync.

Refs #3060
2026-09-07 20:25:11 +03:00
Bohdan Triapitsyn ab7ee73992 fix(vscode): validate Ollama quota responses consistently
Share the Ollama request and parser between credential validation and quota refresh so unparseable pages cannot produce successful empty usage. Reject redirects and bound requests with a timeout while preserving both plan formats.

Tested with 94 quota tests, VS Code type-check and ESLint, and the extension build. Reviewed dead-code output; existing anti-slop findings remain outside the changed code.
2026-09-07 19:41:40 +03:00
Bohdan Triapitsyn c9bef8bc05 style: increase chat text and heading contrast
Boosted assistant message text opacity for better readability
Styled markdown headings with a slightly stronger foreground color
2026-09-07 19:26:23 +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 ec089b34e8 fix(chat): keep streaming thinking inside its capped scroll box
A streaming Thinking block grew without a height cap and only moved into
the capped, scrollable box once it finished, so a long thought pushed the
whole timeline down while it streamed. The cap was dropped in June because
the box then pinned to its own end on every tick and captured the wheel,
so the chat could not be scrolled while thinking streamed.

The box is capped in every state now and follows its own end only while
streaming and only until the reader wheels or drags upward inside it;
returning to its end re-arms the follow. It is marked as a nested scroller,
so an upward wheel scrolls the box first and reaches the chat once the box
sits at its top. Growth is observed on the content box because markdown
commits asynchronously.

Testing: reasoning block tests extended for the capped, nested box while
streaming; ui type-check and lint; web build. Live reasoning stream not
exercised in a browser.
2026-09-07 17:58:03 +03:00
Bohdan Triapitsyn 13bc0a68e2 fix(chat): give the sorted Activity block the live timeline's row rhythm
The Activity block stacked its tool rows with an extra 0.375rem gap on top
of each row's own padding, while the live timeline stacks the same rows
with nothing between them. Both views now share the 36px row step.

Testing: measured both render modes on one session in headless Chrome;
ui type-check and lint.
2026-09-07 17:58:03 +03:00
Bohdan Triapitsyn 78e3ab9744 fix(chat): keep the dirty-branch tooltip quiet for an auto-opened draft
At cold launch the container opens a new-session draft as a placeholder
until the last session restores. The draft's dirty-directory warning
announced itself by opening its tooltip for five seconds, so on mobile the
tooltip appeared alone over an almost empty screen and vanished with the
draft once the session came back.

The draft state now records that the app opened it automatically, and the
warning does not flash for such a draft. The icon still shows and the
tooltip stays reachable by hover or long press; a draft the user opens
flashes as before.

Testing: ui type-check and lint; session store and composer tests; web
build.
2026-09-07 17:58:03 +03:00
Bohdan Triapitsyn 4915658de1 fix(sync): replace every optimistic part of a just-sent message
The server echoes a sent message part by part. The reducer replaced an
optimistic part with its server echo only while the FIRST part of the
message was still optimistic, so once the text echo had landed the file
echo no longer qualified and was appended instead: an attached image
showed twice until the next page fetch rewrote the parts.

Any remaining optimistic part of the same type is now a replacement
candidate. The scan only runs when a part with a new id arrives, which is
once per part during assistant streaming.

Testing: reducer test extended past the first echo to the file echo; ui
type-check and lint; verified the event order against a live OpenCode
(message, text part, file part within ~70ms of the accepted request).
2026-09-07 17:58:03 +03:00
Bohdan Triapitsyn e3b0088c60 refactor(chat): drop the anchored end space after sending a message
Sending used to park the new user message near the top of the viewport by
reserving blank space below it and holding the viewport there while the
reply streamed in. The reserved space showed up on its own at times and the
hold fought other corrections; with a reliable end-follow and pin in place
the effect no longer pays for its machinery.

The anchoring mode, its arm/claim/position/settle lifecycle, the reserved
end space passed to the list and the anchored-turn geometry are removed.
Sending from the live edge is now an instant return to the end, and the
reply is kept in view by ordinary end-follow; sending from mid-history with
auto-follow off still leaves the viewport untouched.

Testing: scroll module tests trimmed to the remaining geometry; ui
type-check, lint and dead-code report; web build.
2026-09-07 17:58:02 +03:00
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 37f5021715 fix(chat): keep a pinned reader on the end through panel and window resizes
Opening a panel or resizing the window while pinned to the end of an idle
session bounced the timeline and left it above the end with the pin
released. Every row re-wraps on a width change and the list's total
content length lags a frame behind the rows, so anything scrolling to the
end from it (the list's own maintainScrollAtEnd, scrollHeight) landed on a
blank tail or short of the real end; the idle branch then released the pin
instead of recovering.

An idle session no longer hands end-keeping to the list at all: the hook's
same-frame pin owns it, and during a width resize it holds the measured
bottom of the last real row plus the list footer, for streaming readers
too. The footer size was missing from that measurement (the list does not
expose it through getState) and left the viewport short by the tail
spacer; it now arrives through onMetricsChange. Size compensation applies
only to a reader who left the end.

The follow re-arm band is half a viewport instead of 40px: leaving the end
is decided by a real gesture only, so the band now only decides how close
a reader must come back before follow re-arms.

Testing: scroll module tests updated (footer in the real end, half-viewport
band with a 40px floor); ui type-check and lint; measured in headless
Chrome that scrollTop equals the maximum on every frame of a panel toggle,
a height change and a combined resize, where the previous build ended 122px
and 462px above the end.
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 ff75dc9bd5 fix(terminal): unset NODE_CHANNEL_FD for PTY shells on every POSIX host
The PTY runtime exported an empty NODE_CHANNEL_FD to override the daemon's
IPC descriptor, because bun-pty merges the native environ back into the
child and a JS-only delete does not stick. Node CLIs launched from the
shell (opencode, claude) then printed "warn: Failed to parse IPC channel
number ''" on exit.

The Linux-only env -u ARGV0 wrapper now applies on macOS and Linux and
also unsets NODE_CHANNEL_FD, so the variable is gone instead of empty.

Testing: runtime and inherited-env tests updated for the POSIX wrapper;
verified in the running app that exiting opencode no longer prints the
warning.
2026-09-07 12:18:22 +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 eb6f7b0904 fix(terminal): update ghostty-web to a build that clears recycled rows
ghostty-web 0.4.0 hands rows that scroll into view out of recycled WASM
page memory without clearing them, so after a tab or project switch the
new emulator showed the previous terminal's text (upstream #138). The fix
landed only in prereleases, so pin 0.4.0-next.20 and carry the local
block-glyph rendering patch over to the new dist file.

Verified in a production build: creating an emulator after disposing a
full one no longer exposes its rows, and switching between two projects
with live output in each keeps every terminal's content to itself.
2026-09-06 22:57:43 +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
Bohdan Triapitsyn b0282b2720 fix(quota): validate Hyper credentials and clean up balance labels
Reject invalid credentials while preserving valid token fallback, parse balances with existing boundary helpers, and keep credit values free of untranslated unit text. Inject auth and HTTP dependencies for focused tests in both runtimes.

Validated web quota and registry tests (35 passed), VS Code quota tests (70 passed), both package type checks and lint, extension build, and changed-line anti-slop checks. Reviewed dead-code output. Live Hyper validation was not run because no API key is available.
2026-09-05 23:26:35 +03:00
Howon Lee 5e7c147785 feat: add Charm Hyper quota provider (#3368) 2026-09-05 23:16:13 +03:00
Bohdan Triapitsyn 7b42208b8c release v1.22.2 2026-09-05 21:39:04 +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