Commit Graph
899 Commits
Author SHA1 Message Date
Bohdan Triapitsyn be65a9bd1f fix(electron): preserve changelog on download + reliable restart-to-update
- useUpdateStore: keep sidecar-sourced body when merging fresh desktopInfo
  (electron-updater returns 'See release notes at ...' which clobbered it)
- main.mjs: defer quitAndInstall/relaunch via setImmediate so IPC reply
  flushes first; wire update-downloaded and error events; log restart path
2026-04-22 00:57:58 +03:00
Bohdan Triapitsyn 2a0aef89d5 release v1.9.7 2026-04-22 00:36:25 +03:00
Bohdan Triapitsyn 5ec7f46105 fix: surface disconnect reason, switch health probe to /global/health (#978)
- event-pipeline: onDisconnect now carries a reason tag (ws_closed:code=N, ws_error_frame, ws_closed_before_ready, sse_error)
- useConfigStore: store lastDisconnectReason, clear on reconnect
- send guards: embed reason in Connection lost toast so we can tell which path tripped
- lifecycle: isOpenCodeProcessHealthy hits /global/health (healthy flag) with 5s timeout instead of /session with 2s, avoids false restarts under stream load
2026-04-22 00:33:21 +03:00
vhqtvn 8e86891d8e feat(pwa): add install orientation setting (#900) 2026-04-21 23:17:34 +03:00
Artёm 125094d371 fix: сохранил pinned sessions после reload (#943) 2026-04-21 23:13:54 +03:00
Artёm 2eb71d5fdd fix: сделал loopback origin валидным для push VAPID (#944) 2026-04-21 23:08:04 +03:00
jwcrystal 247dc0481e fix: allow git checkout with uncommitted files (#945) 2026-04-21 23:03:27 +03:00
ricautomation 45f8d6c2be fix: automatically close opencode process when exiting openchamber (closes #927) (#947) 2026-04-21 23:02:01 +03:00
ricautomation 3509a5f98b fix: add test notification button (closes #930) (#948)
Adds a "Send test notification" button to the notification settings page to allow users to immediately verify their configuration. The button uses the existing notifyAgentCompletion API.
2026-04-21 22:52:07 +03:00
cyanandBohdan Triapitsyn a8719f0a8e Match Opencode Config Resolution Behavior (#949)
* fix(config): match opencode config resolution behavior

* fix(config): use primary opencode config file

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-04-21 22:47:42 +03:00
jwcrystalandBohdan Triapitsyn d4a4f43a83 feat: show file change summary bar (#950)
* feat: add FileChangeSummary component for multi-file diff preview in ToolPart

Provides an aggregated diff card for apply_patch and multi-edit tools,
showing per-file stats with click-to-expand diff view.

* feat: add PendingChangesBar above chat input with collapse/expand and file opening

- Collapsed/expanded toggle with aggregate +N -N stats (green/red)
- Relative path display, chat-column alignment with ChatInput
- Click file to open in diff viewer (web/desktop) or editor (VS Code)
- Support edit/multiedit/apply_patch/write tool metadata extraction
- Add PendingChangesBar to main chat view (ChatContainer)

* feat: dual-mode ChangedFilesBar — Git diff state vs latest AI turn

Git mode: reads git status from useGitStore (status.files + diffStats),
auto-clears on commit/restore. Shows 'N files changed in workspace'.

Non-Git mode: latest assistant turn only (no accumulation), clears on new
user message or manual dismiss. Shows 'AI updated N files in the last reply'.

Both modes: dismiss button with signature-based tracking.
Add pendingChangesBarDismissed state to session-ui-store, cleared on sendMessage.

* fix: address code review issues in ChangedFilesBar

- Gate non-git mode on streaming state to prevent flicker during AI turns
- Return null when isGitRepo is unknown (loading state)
- Add group/row class for reject button visibility in FileChangeSummary
- Use per-session Map for dismiss tracking to prevent cross-session leaks
- Include additions/deletions in dismiss signature for re-edit detection
- Extract shared parsePatchStats/parseCount to fileChangeHelpers

* chore: revert local .opencode/package-lock.json changes from PR

* fix: per-part fallback guard and git-only reject button

- Fix extractChangedFiles to use per-part files.length snapshot instead
  of global guard, preventing file entries from being skipped when
  earlier parts already contributed files
- Hide reject button in FileChangeSummary when not in a git repo,
  preventing silent revert failures

* refactor: remove FileChangeSummary — dead code redundant with ToolPart

FileChangeSummary duplicated diff rendering that ToolPart already
provides (PatchDiff, per-file stats, DiffViewToggle). The only unique
feature was a git revert button, which conflicts with the design
principle of not having accept/reject on file change previews.

Moved parsePatchStats/parseCount back into PendingChangesBar (sole
consumer) and deleted the shared helper module.

* chore: remove stray Tester.txt

* fix: deduplicate Fallback 4 'Diff' placeholder via seen set

* chore: update non-Git mode copy to neutral 'changed in the last reply'

* fix(chat): unify changes row with tasks

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-04-21 22:34:06 +03:00
Dave Otero d73edc672e Improve MCP settings auth flow, remote config support, and diagnostics UX (#953)
* feat: improve MCP settings auth workflow

* fix: complete MCP settings auth flow

* fix: harden MCP settings auth flow

* fix: add MCP settings refresh control

* fix: stabilize MCP authorization and status handling

* fix: clarify MCP advanced remote options toggle

* fix: improve MCP import and diagnostics

* feat: improve MCP settings panel visual hierarchy and UX

* fix: expose MCP auth actions in connected state

* fix: remove MCP import snippet helper text

* fix: address MCP review feedback

* fix: correct MCP page transport layout after rebase
2026-04-21 20:46:51 +03:00
JasonandBohdan Triapitsyn b1a96c7b36 fix: only pre-fetch when branch prefix is a known remote name (#958)
* fix: only pre-fetch when branch prefix is a known remote name

* fix(ui): avoid unused ProjectEntry import

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-04-21 20:46:17 +03:00
youfchandBohdan Triapitsyn bc6cb91954 Chat input: drag-drop files and folders from file tree with @folder autocomplete (#963)
* feat(chat): drag-drop files/folders from file tree into chat input

Features:
- Support drag-and-drop files and folders from file tree into chat input
- Support @folder autocomplete for file/folder mentions in chat input
- Add drag support to file tree nodes for dropping into chat input
- Persist confirmed mentions to localStorage across sessions

Fixes:
- Fix drag state getting stuck with drag enter counter and onDragEnd cleanup
- Fix pendingSearchRef counter leak on query cancellation
- Fix confirmed mentions highlight/delete for folder paths
- Fix clear confirmed mentions on send and autocomplete loading state
- Guard drag-start against empty and root-relative paths
- Reset loading state when pendingSearchRef reaches zero

* fix(chat): address Greptile PR review findings

- P1: move confirmedMentionsRef.clear() out of handleQueueMessage to
  prevent bare-name mentions from being lost when queued messages are
  sent via handleSubmit
- P2: distinguish internal file-tree drag from external file drag in
  overlay text (Drop to insert as mention vs Drop files here to attach)
- P2: add directories to marquee overflow effect dependency array

* fix(chat): prune stale confirmed mentions

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-04-21 20:31:14 +03:00
Les De Ridder 36b177dc81 Single select question radios (#965)
* fix(ui): make radios not jump around upon state change

* fix(ui): use radios for single-select questions instead of checkboxes
2026-04-21 20:28:50 +03:00
jwcrystal 2f5912c287 fix(files): refresh open file content after external changes (#967)
* fix(files): refresh open file content after external edits

Previously, opening a file in the Files view and then editing it externally
(e.g. via CLI or another editor) would show stale content. Even closing and
reopening the file returned cached content — a full page reload was required.

Root causes:
1. The in-memory readFile cache used path-only hits, with no metadata
   validation. External edits were invisible until the cache was evicted.
2. No polling mechanism existed to detect external changes to the open file.

Fix:
- Add mtimeMs to statFile across all runtimes (web, VS Code, desktop).
- Cache layer (RuntimeAPIProvider): validate cache hits against current stat
  metadata (mtimeMs + size). On miss, use stat→read→stat to avoid TOCTOU.
- UI layer (FilesView): poll the open file every 2s; on detected change, set
  loadedFilePath=null to trigger the existing load effect once (no double
  reload). Skip polling when tab is hidden or editor has unsaved changes.
- After save, refresh the stat ref so the next poll doesn't see a spurious
  change from the save itself.

Addresses review feedback from PR #827 (double reload + TOCTOU).

* fix(files): address P2 review findings

- readFreshFile retry now uses stat→read→stat to maintain TOCTOU
  protection during the retry path (not just the initial read).
- Replace isDirty in polling effect deps with isDirtyRef to avoid
  unnecessary interval teardown/restart on every edit/save cycle.
2026-04-21 18:04:24 +03:00
Gyumin Lee 5f3b57b5ed fix: preserve --host flag across update and restart (#972) 2026-04-21 18:02:44 +03:00
jwcrystal 68fd1d01b4 fix: invalidate worktree list cache after create and remove (#973)
* fix: invalidate worktree list cache after create and remove

After creating or removing a worktree, the list cache was not cleared,
so the UI would show stale data for up to 30 seconds.

* fix: normalize cache key in removeProjectWorktree

Greptile review caught that project.path was used raw while the cache
stores normalizePath(project.path), so the delete could silently miss
on paths with trailing slashes or backslashes.
2026-04-21 18:00:48 +03:00
Bohdan Triapitsyn 630d9e3a82 fix: keep notifications alive so clicks still work on macOS
GC was collecting the JS Notification object after ~1 min, silently
killing click handlers. Hold a ref in a Set, release on click/close/
failed. Also order app.focus({steal}) before restore/show so the app
comes forward when minimized to Dock or Cmd+H'd.
2026-04-20 23:46:24 +03:00
Bohdan Triapitsyn 08258908c2 feat: bulk select sessions in sidebar
Select toggle in header enters selection mode. Click toggles, shift-click
ranges, parent auto-includes subtasks, Cmd/Ctrl+A selects visible, Esc
exits, Cmd/Ctrl+Backspace deletes. Bulk bar: move to folder (existing or
new inline), archive/delete via existing confirm + "Never ask" pref.
Cross-scope forbidden — new scope clears previous.

Also: web sidebar header folds toggle into action row (no separate chrome
row, same position on toggle); sidebar min width bumped 250→280 so icons
fit; minimal-mode hover reveal padding trimmed.
2026-04-20 23:32:32 +03:00
Bohdan Triapitsyn daa485c5d9 refactor: drop ellipsis dots on active tool labels, use opacity only
Ellipsis dots after tool names were visually noisy. Replace with a
subtle opacity dip (100% -> 70% with 200ms transition) while the tool
is active.
2026-04-20 22:28:50 +03:00
Bohdan Triapitsyn 34a8cb3f3b fix: don't silently swallow bootstrap SDK errors
sdk.path.get() / sdk.project.list() / sdk.app.agents() etc. return
{ data, error, response } without throwing on non-2xx. The x.data! /
x.data ?? [] pattern turned HTTP 5xx warmup responses into empty state,
and retry() never saw an error to retry.

Wrap everything through unwrap(): preserve the HTTP status on the
thrown Error so retry()'s transient detection (5xx) fires. Fixes the
class of bugs where a slow opencode sidecar leaves agents/commands/
sessions looking empty after startup.
2026-04-20 22:28:41 +03:00
Bohdan Triapitsyn 70fd6aaacc fix: stop settings.json from being wiped on launch
Electron main, ssh-manager, and the embedded web server all write the
same settings.json. readJsonFile/readJsonRoot silently coerced any read
failure (including mid-write parse errors) to {}, and writes were plain
fs.writeFile. A partial read during a concurrent write let the reader's
next read-modify-write overwrite the whole file with only the field it
just set — wiping projects, desktopDefaultHostId, and more. Next launch
showed the welcome chooser because defaultHostId was gone, and the
sidebar was empty because projects were gone.

- Switch all writers to atomic tmp+rename so readers never see partial
  JSON.
- Add mutateSettingsRoot() in Electron main to serialize read-modify-
  write pairs across its own call sites (hosts config, window state,
  desktop port, ssh instances, vibrancy).
- Keep read-on-error returning {} to avoid crashing startup callers,
  but log loudly now so we can catch it if it ever happens again.
- useProjectsStore: don't clobber a populated cache with empty incoming
  settings. If settings ever do come back empty, the sidebar stays
  intact until a real, non-empty sync lands.
2026-04-20 22:28:32 +03:00
Bohdan Triapitsyn f1b84e3291 fix: always signal connected state on first successful connect
markConnected skipped onReconnect when hasConnected was false and there
was no prior disconnect. Consumer state (isConnected) starts at false and
stayed false — the send button threw "Connection lost" until the HTTP
health check in initializeApp happened to race a setState through.

Now onReconnect fires on every successful connect; hasConnected is no
longer needed and gets removed.
2026-04-20 22:28:18 +03:00
Bohdan Triapitsyn 895edaf557 fix: preserve sessions when bootstrap races with event stream
Two fixes for intermittent "sessions missing at app launch":

- loadSessions: if session.list comes back empty but the store already
  has sessions populated via WS events, don't clobber. OpenCode can
  answer HTTP with [] while the WS snapshot is still arriving.
- loadSessions: wrap SDK errors preserving HTTP status so retry()'s
  transient detection (5xx) actually fires. String(errorObject) was
  erasing the status and retry gave up after one attempt.
- retry: broaden transient 5xx window from just 502/503 to any 5xx.
  OpenCode can return 500/504 while warming up (session store reading
  from disk), and those deserve the same retry treatment.
- Log bootstrap empty-session retries for future diagnosis.
2026-04-20 21:42:16 +03:00
Bohdan Triapitsyn c2eeadd9a3 fix: unstick isConnected when first connect follows a disconnect
markConnected skipped onReconnect on the very first successful connect
(hasConnected=false branch), but an earlier failed attempt could have
already fired onDisconnect -> isConnected=false. Consumer state stayed
stuck at false forever. Fire onReconnect in that case too.

Repro: launch app while opencode sidecar is still warming up so the
first WS attempt fails. Second attempt succeeds but isConnected stays
false; user sees "Connection lost" toast on every send.
2026-04-20 21:42:04 +03:00
Bohdan Triapitsyn a8a336b0a0 perf: halve stream flush cadence from 60hz to 30hz
FLUSH_FRAME_MS 16 -> 33. Delta events keep coalescing, just into larger
windows. AssistantTextPart already throttles text at 100ms, so the extra
granularity produced no visible benefit — just extra store.setState calls
and subscriber notifications per second.
2026-04-20 20:56:24 +03:00
Bohdan Triapitsyn 9f81c8fa46 perf: trim observer work and defer Prism during streaming
- Drop characterData from scroll-follow and file-link MutationObservers;
  ResizeObserver already catches content growth.
- extractMermaidBlocks: bail out when markdown has no 'mermaid' at all.
- MarkdownCodeBlock: skip Prism highlighting while code is actively
  changing, render plain pre until settled for 300ms.
2026-04-20 20:52:57 +03:00
Bohdan Triapitsyn ad7790b3ec perf: event-driven scroll follow, kill 60hz RAF during streaming
Persist mode was rescheduling RAF every frame forever, forcing layout
reads (scrollHeight) on each tick. Swap to ResizeObserver+MutationObserver
that trigger short RAF bursts only when content actually changes.

-10pp renderer CPU under active streaming.
2026-04-20 19:15:43 +03:00
Bohdan Triapitsyn ec38468e93 refactor: refresh chat indicators, settings pages, and shared UI primitives 2026-04-20 18:48:33 +03:00
Bohdan Triapitsyn 1a2738a2e2 fix: sidebar dropdown works properly for sub-sessions 2026-04-20 18:03:34 +03:00
Bohdan Triapitsyn 0f753fa62e fix: restore attach PR dialog checkbox behavior
Fix include-diff checkbox so clicks reliably toggle state
Return checkbox to normal size in the attach PR dialog
Keep label and checkbox interaction consistent
2026-04-20 16:48:50 +03:00
Bohdan Triapitsyn 75c6277fa5 chore: update opencode sdk version 2026-04-20 16:02:54 +03:00
Bohdan Triapitsyn 285c3bcaae Migrate desktop shell from Tauri to Electron (#964)
* feat(electron): scaffold Electron desktop package

Main + preload + ssh manager, packaging scripts, icons, root build/lint/type-check wiring.

* feat(ui): add Electron runtime detection and desktopNative facade

isElectronShell via window.__OPENCHAMBER_ELECTRON__, isDesktopShell now covers both. desktopNative wraps window/title/theme calls so UI avoids direct Tauri imports. revealDesktopPath added.

* refactor(ui): route window/title/theme/export through desktopNative

SessionSidebar, MultiRunLauncher, useWindowTitle, ThemeSystemContext, exportSession drop direct @tauri-apps imports.

* refactor(ui): treat all desktop shells uniformly

device.ts switches Tauri-only checks to isDesktopShell. Header OpenInApp button uses actionDirectory so it falls back to the active project path.

* fix(ui): menu Copy clipboard fallback and softer sidebar tint

useMenuActions falls back to Clipboard API for the native Copy action when the page doesn't intercept. cssGenerator lowers sidebar strong/soft alpha so the tinted surface reads gentler.

* chore(electron): mirror Tauri build/type-check script shape

build script becomes no-op so root 'bun run build' skips packaging. Syntax validation (node --check) moves into type-check. electron:build root script still runs full sidecar+bundle+electron-builder.

* fix(electron): sync app identity, preload path, boot outcome, dev entry

Read version from packages/electron/package.json so 'electron ./main.mjs' dev entry reports the app version instead of Electron's. Bump electron package to 1.9.6 for workspace parity.
Resolve preload via app.getAppPath() in prod (bundle lives in dist-bundle while preload.mjs ships at app root).
Compute and inject __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__ in main + preload so the loading gate dismisses (mirrors Tauri Rust injection).
Dev entry uses ./main.mjs to bypass the stale dist-bundle so source edits apply.

* refactor(open-in-app): split directory and file flows

Header button now opens the project/worktree directory only — drop activeFilePath prop and its Header prop passthrough. FilesView editor dropdown opens the active file only via new openDesktopFileInApp.

Electron main.mjs mirrors Tauri's open-chain logic: buildOpenProjectSpecs (finder/terminal direct, vscode-like via CLI -n, JetBrains via open -na --args) and buildOpenFileSpecs (finder -R reveal, terminal opens parent dir, editors via CLI or open -a). runSpecChain falls through specs until one exits 0.

* fix(files-view): keep floating toolbar mounted while its dropdowns are open

Portalled Base UI menu popups render outside floatingToolbarRef. The document mousedown listener and onMouseLeave collapsed the toolbar as soon as the popup appeared, unmounting the DropdownMenu root and swallowing clicks on its items. Track open dropdowns via onOpenChange and skip the collapse while count > 0; also ignore mousedowns that land inside a dropdown-menu-content/item.

* feat(electron): add quit confirmation with risk poller

Mirrors Tauri's macOS-only behavior: poll /api/openchamber/scheduled-tasks/status and /api/openchamber/tunnel/status every 5s. If active tunnel or running/enabled scheduled tasks are detected, Cmd+Q / dock Quit / menu Quit shows a native warning dialog listing reasons; otherwise quit proceeds silently.

performConfirmedQuit persists window state, kills sidecar, shuts down SSH, and fires a 1500ms unref'd safety timeout that calls app.exit(0) if the normal quit sequence stalls.

* feat(notifications): fix payload parsing, restore-on-click, session deep-link

Normalize input so both sidecar stdout path (flat) and UI IPC path ({ payload: {...} }) work; previous destructuring missed requireHidden (camelCase) and the payload wrapper so notifications showed with empty body.

Click handler restores the window if minimized, shows it if hidden, and focuses. When the notification payload carries sessionId, emit openchamber:open-session which the App listener routes to setCurrentSession — matches the PWA service-worker deep-link behavior. macOS notifications now also use sound 'Glass' for parity with Tauri.

* chore(electron): bump to Electron 41 + latest updater/context-menu

electron ^38.2.0 -> ^41.2.1
electron-updater ^6.6.2 -> ^6.8.3
electron-context-menu ^4.0.4 -> ^4.1.2

Dev boot verified: main process starts, preload exposes globals, API server + quit risk poller + autoUpdater all initialize without errors.

* fix: keep todo row alignment stable when expanding text

Keep checkbox and action buttons vertically centered in collapsed todo rows
Prevent first todo line from shifting when expanding to multiple lines

* fix: make commit highlights visible and input behavior reliable

Switch commit message field to native textarea for predictable auto-resize
Fix AI highlights append flow so inserted text is applied consistently
Make chat scroll-to-bottom control fully circular

* style: increase chat bubble corner radius consistency

Use larger radius for user chat message bubbles
Match chat input container radius to user message styling

* feat(electron): adopt OpenCode playbook improvements

mac: hardenedRuntime + entitlements.mac.plist + notarize + dmg.sign for Apple notarization parity.
single-instance lock + openchamber:// protocol with session/project/host routing (host switch done fully in main via activateMainWindow).
setAppUserModelId for Win toast identity; proxy-bypass-list switch; chdir(homedir) for Finder-launch cwd safety.
shell env probe (\$SHELL -il -> -l) merged into sidecar spawn; PATH deduped.
electron-log with 5MB rotation + 7-day cleanup; autoUpdater.logger wired; startup info log.
webContents zoom locked to 1 (zoom-changed + did-finish-load).
UI: openchamber:open-project -> useDirectoryStore.setDirectory.

* fix(electron): make bootOutcome mutable across re-navigation + project deep-link

host deep-link used to land on chooser because contextBridge exposed bootOutcome as read-only; initScript re-assignment became a silent no-op. drop preload's contextBridge for bootOutcome, inject it via main-world initScript, and move injection from did-finish-load to dom-ready so it lands before React mounts.

project deep-link updated currentDirectory only; activeProjectId stayed stale so the sidebar didn't highlight the new project. switch to projectsStore.setActiveProject (or addProject for new paths) which updates both.

add log.info around deep-link dispatch + host switch for diagnostics.

* fix(electron): desktop_hosts_set IPC args + persist initialHostChoiceCompleted + re-eval bootOutcome

UI calls invoke('desktop_hosts_set', { input: {...} }) but main was reading args.config — every onboarding 'i've completed installation' / host-dialog save wrote nothing, so desktopDefaultHostId stayed null and the chooser screen looped forever.

also:
- writeDesktopHostsConfig now persists desktopInitialHostChoiceCompleted so the tauri-compat flag survives writes.
- readDesktopHostsConfig returns initialHostChoiceCompleted so the UI-side config mirror is complete.
- after writing hosts, recompute state.bootOutcome + state.initScript; a subsequent window.location.reload() picks up target=local/status=ok via dom-ready injection without needing a full app restart.
- app.setName('OpenChamber') early (pre log.initialize) so electron-log logs land in ~/Library/Logs/OpenChamber/ instead of the package-derived '@openchamber/electron' path.

* chore(electron): rename appId to dev.openchamber.desktop

ai.opencode.* is the OpenCode team's reverse-DNS namespace; OpenChamber should not squat there. now that we're on Electron, drop the tauri-era inherited identifier and claim our own under openchamber.dev.

user-facing productName stays "OpenChamber". tauri identifier left as-is — legacy shell on the way out.

* feat(ci): add electron build+notarize+publish jobs to release workflow

three new jobs in release.yml, running in parallel with tauri:

- build-desktop-electron-macos: matrix(arm64, x86_64) on macos-26; installs Developer ID via keychain, runs build:sidecar + bundle:main + electron-builder --mac --arch <> --publish=never (with APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID env mapped from existing secrets). verifies hardened runtime, stapled notary ticket, required entitlements. uploads DMG/ZIP/blockmaps to the release and emits per-arch latest-mac.yml as a GH artifact.

- combine-electron-manifests: downloads latest-yml-*-apple-darwin artifacts, runs the existing finalize-latest-yml.mjs to merge per-arch files entries into a single latest-mac.yml, uploads combined yml to the release.

- finalize-release: now also waits on the two new jobs before flipping the draft release to published.

also: explicit artifactName in electron-builder config so arm64 and x64 dmg/zip never collide.

electron-updater in main.mjs (setFeedURL btriapitsyn/openchamber) fetches this latest-mac.yml on desktop_check_for_updates; downloadUpdate / quitAndInstall wire through our existing IPC handlers unchanged.

* docs: future-agent brief for tauri -> electron auto-update cutover

self-contained plan for the one-shot migration release that carries existing tauri installs into the electron shell via tauri's updater. written so a fresh agent with no branch context can execute it.

covers: the trick (repackage signed electron .app as a tauri tarball, minisign with existing TAURI_SIGNING_PRIVATE_KEY), workflow surgery on release.yml, rollback plan, validation steps against a real tauri install, and edge cases (CFBundleIdentifier change, notification perms re-prompt, deep-link re-registration).

* docs: soften framing of cutover playbook (no user-shaming)

* chore: mark electron as primary desktop shell; tune dmg installer window

AGENTS.md: explicit note that new desktop work lands in packages/electron/, packages/desktop/ (tauri) is maintenance-only until the cutover described in docs/TAURI_TO_ELECTRON_CUTOVER.md. updated runtime/entry-points/build-commands sections accordingly.

electron/package.json build.dmg: cleaner title ("OpenChamber 1.9.6" without -arch suffix), 660x400 window matching the tauri layout users are used to, icon size 128, explicit app/Applications positions.

* refactor(web): drop bun-specific runtime deps from server

- 11 test files migrated bun:test -> vitest; API (describe/it/expect) is drop-in; all 73 tests pass under vitest run.
- bun:sqlite -> better-sqlite3 in git/service.js::syncSandboxesToOpenCodeDb. api shift is db.query().get()/run() -> db.prepare().get()/run().
- add "test": "vitest run" script in packages/web.

no production code used Bun.* APIs; server is Express-on-Node already. this commit removes the remaining bun-runtime shape so the server module can be imported and booted inside an electron main process.

* feat(electron): boot web server in-process, drop sidecar subprocess

the electron main process now imports @openchamber/web/server/index.js as a workspace dependency and calls startWebUiServer({...}) directly. the returned handle exposes getPort() / stop() and the notification emitter takes an onDesktopNotification callback, so we no longer spawn a bun-compiled sidecar binary and no longer parse stdout for the one-line notify protocol.

- packages/electron/package.json: +@openchamber/web (workspace:*); extraResources drops 'sidecar'; build:sidecar script renamed to build:web-assets (kept the vite build step, dropped the bun compile step).
- packages/electron/main.mjs: remove spawn/kill-stale-sidecar/sidecar path resolver/stdout-prefix parser; rewrite spawnLocalServer to probe a free port (stored | DEFAULT_DESKTOP_PORT | OS-assigned) then import server and await startWebUiServer; killSidecar calls handle.stop({ exitProcess: false }); hoist user shell env (PATH, etc.) onto process.env once so opencode / git / rg children still inherit the expected runtime environment.
- packages/web/server/lib/notifications/emitter-runtime.js: accept an onDesktopNotification callback (late-bindable via setOnDesktopNotification). when set, notifications are dispatched through the callback instead of process.stdout; tauri path still uses stdout when no callback is bound.
- packages/web/server/index.js: main() wires options.onDesktopNotification to notificationEmitterRuntime.setOnDesktopNotification.
- release.yml + AGENTS.md updated for the new script name + runtime shape.

payoff: -300ms cold start on mac, single process in activity monitor, no stdio IPC, no bun binary in the packaged app. tauri sidecar path is untouched.

* build(electron): rebuild native deps explicitly, bump electron-builder

the previous build failed because electron-builder 24.13.3 tried to run \`bun rebuild\` on native deps (better-sqlite3, node-pty) and bun has no rebuild subcommand; it also couldn't find prebuild-install because bun hoists under node_modules/.bun/<pkg>@<ver>/ and never populates node_modules/.bin for transitive deps.

fix:
- bump electron-builder devDep to ^26, whose packageManager detection understands bun workspace layouts.
- add @electron/rebuild devDep + scripts/rebuild-native.mjs. the script rebuilds better-sqlite3 / node-pty / bun-pty against the installed electron version before electron-builder is invoked.
- set build.npmRebuild=false so electron-builder no longer attempts its own broken PM-based rebuild.
- package script: build:web-assets -> bundle:main -> rebuild:native -> electron-builder.

verified: CSC_IDENTITY_AUTO_DISCOVERY=false bun run electron:build produces signed-ad-hoc dmg/zip/blockmap/latest-mac.yml; artifacts land under packages/electron/dist as expected. cold-start from Applications should work (native bindings now match electron 41 node ABI).

* fix(electron): externalize web server + native deps from main bundle

the ESM bundle was statically inlining @openchamber/web transitively, which pulled in bun-pty/src/terminal.ts with its top-level \`import { dlopen } from "bun:ffi"\`. node's ESM loader parses every static import when the bundle loads, so the bun:ffi scheme crashed the packaged app at startup with ERR_UNSUPPORTED_ESM_URL_SCHEME — the runtime guard (if (globalThis.Bun) { await import('bun-pty') }) never got a chance to skip it.

fix: bundle-main.mjs marks @openchamber/web (+ its bun-pty / node-pty / better-sqlite3 transitives) as external. the dynamic \`await import('@openchamber/web/server/index.js')\` in main.mjs stays a runtime resolution; the conditional bun-pty import stays dynamic; native modules load from node_modules via the standard resolver.

* perf(web): classify UI-only deps as devDependencies, shrink packaged app

packages/web is a hybrid package: server code in server/, react UI source in src/, compiled UI output in dist/. the server serves dist/ as static files — it never imports react/radix/codemirror/etc. at runtime. but electron-builder, npm install, and similar tools treat everything under "dependencies" as shipping surface, so all of react + @radix-ui/* + @codemirror/* + @fontsource/* + @simplewebauthn/browser + cmdk + ghostty-web + ... were landing in app.asar even though the same code is already baked into dist/ chunks.

move ~24 UI-only packages to devDependencies. vite + its plugins still install them in dev (bun install fetches devDependencies in workspaces), so \`bun run build\` is unchanged. consumers doing \`npm install @openchamber/web\` no longer pull ~150MB of unused browser-side modules.

measured on aarch64 darwin build:
- app.asar: 281MB -> 44MB (-237MB, -84%)
- .dmg: 320MB -> 132MB (-59%)
- .zip: 305MB -> 129MB (-58%)

verified type-check, ui build, 73 vitest tests, packaged launch.

* chore(electron): center dmg installer icons, use cream brand background

dmg-builder 26 ignored our previous dmg.contents positions against its template background (they stayed at template coords, producing misalignment with the drawn arrow). switch to a solid backgroundColor (#FFFCF0, the splash light tone) so the template image is dropped entirely and our coordinates are authoritative. window tuned to 540x340, iconSize 100, iconTextSize 13.

dmgbuild treats contents coordinates as icon *centers* (not top-left), so with iconSize=100 in a 540 window, x=180 and x=360 place left and right clusters with equal 130px gaps on both sides of the window. y=140 vertically centres the icon+label pair.

* fix(electron): eliminate main-thread freezes in in-process server

Three blocking paths were running sync work on the Electron main event
loop, causing multi-second UI freezes under the new in-process server:

- package-manager.detectPackageManagerDetails fired spawnSync(pnpm/npm/
  yarn/bun bin -g) with 10s timeouts. In desktop runtime PM detection is
  pointless (app is .app bundle, updates via electron-updater) — short-
  circuit when OPENCHAMBER_RUNTIME=desktop. This was the ~5s freeze.
- buildInstalledApps iterated 22 OPEN_IN_APPS × spawnSync(mdfind, sips).
  Converted to execFile promises so child waits yield to the loop.
- orphan-project-file recovery re-scanned disk on every settings read
  (3+/s from fs/list/etc). Cache the outcome per process lifetime.

Also: resolveProjectDirectory prefers settings.lastDirectory over
activeProjectId so file-open from sidebar/chat doesn't 400 with
"Path is outside of active workspace" after the user navigates.

Plus dropdown typeahead fixes in DesktopHostSwitcher/BranchSelector:
stopPropagation on input keys so cmdk doesn't swallow typing.

* feat(electron): restore desktop LAN access for in-process server

spawnLocalServer now reads settings.desktopLanAccessEnabled and binds
on 0.0.0.0 when enabled, so phones/tablets on the same Wi-Fi can open
the app via http://<lan-ip>:<port>. Adds desktop_get_lan_address IPC
(UDP-connect route lookup with networkInterfaces fallback) for the
settings UI to show the reachable URL.

UI and settings plumbing already existed from the sidecar build; only
the Electron main-process wiring was missing.

* chore: added electron package to version bump script

* fix(electron): address PR review — harden IPC surface + polish

P1 security:
- Gate openchamber:invoke and openchamber:dialog:open by webContents
  origin. Only local (loopback / dev file://) senders can call desktop_*.
  Blocks remote hosts loaded via DesktopHostSwitcher from reading local
  files, opening apps, relaunching, etc.
- desktop_read_file now refuses paths outside $HOME / tmpdir and denies
  .ssh/.aws/.gnupg/.config/gh/credentials + .env/.pem/.key by name
  (defense-in-depth behind the origin gate).

P2:
- webPreferences.sandbox:false: add comment explaining preload needs Node
  (contextBridge+ipcRenderer) and why flipping to true would break IPC.
- desktop_set_vibrancy: comment the intentional no-op (no Electron
  equivalent for the Tauri NSVisualEffectView path), drop requiresRestart.
- desktopNative.ts: replace isTauriShell() guards with isDesktopShell()
  so the semantics match (previous check worked only because Electron
  preload exposes a __TAURI__ shim).
- AGENTS.md: correct entry description — server runs in-process, not as
  a sidecar subprocess.

* fix(electron): stop leaking desktop shell APIs to remote renderer pages

Preload was exposing __TAURI__ and __OPENCHAMBER_ELECTRON__ unconditionally,
so after DesktopHostSwitcher navigated the window to a remote OpenChamber
instance the remote UI saw isDesktopShell() === true and tried to invoke
desktop_* IPC. The main-process origin gate then threw "IPC not available
for this origin", surfacing as a user-visible error on the onboarding
screen of the remote.

Preload re-runs on cross-origin navigation; compute current origin up
front and only expose the shell globals + the openchamber:emit listener
when the document is loopback / state.localOrigin / file://. Remote
pages now look like a plain web runtime — no IPC path to reject.

* fix(electron): restore remote UI shell integration via per-command gate

Previous commit stripped __TAURI__ / __OPENCHAMBER_ELECTRON__ from remote
pages wholesale, which broke DesktopHostSwitcher for anyone switched to
a remote instance: no hosts list, "Unknown" probe status, open-in-new-
window dead. Also lost window chrome affordances that the remote UI
needs to render correctly inside the Electron shell.

Switch from an origin-level gate to a per-command allowlist:

- preload.mjs exposes __TAURI__ and __OPENCHAMBER_ELECTRON__ on every
  page (shell identity + IPC channel). __OPENCHAMBER_LOCAL_ORIGIN__ and
  __OPENCHAMBER_MACOS_MAJOR__ also go everywhere since HostSwitcher and
  window chrome depend on them and neither grants capability.
  __OPENCHAMBER_HOME__ stays local-only (leaks the OS username and is
  misleading if consumed as a workspace hint on a remote page).

- main.mjs ipcMain.handle accepts a curated COMMANDS_SAFE_FOR_REMOTE set
  (hosts_get, host_probe, new_window, new_window_at_url, set_window_*,
  is_window_fullscreen, start_window_drag, get_app_version,
  get_lan_address). Filesystem, shell.openPath, installed-apps scans,
  app relaunch, auto-update, hosts_set, dialog:open, read_file stay
  local-only — remote UI doesn't need them and can't weaponize them.

* ci(release): rebuild native modules against Electron ABI before packaging

Electron job skipped rebuild:native so bun install's Node-ABI builds of
better-sqlite3/node-pty/bun-pty shipped into the asar — packaged app
would crash on require. Local bun run package runs the step via
scripts/rebuild-native.mjs (npmRebuild is disabled in package.json);
mirror it in CI and pass ELECTRON_BUILDER_ARCH so the x64 matrix
cross-builds from the arm64 runner.

Tauri job untouched — both builds continue to produce side-by-side
release artifacts (latest.json for Tauri, latest-mac.yml for Electron)
so each shell's updater finds its own manifest.

* ci(release): split Electron arm64/x64 onto native macOS runners

Both Electron matrix entries were running on macos-26 (arm64) and
cross-building x64 from there. Works for Rust/Tauri; brittle for
native Node modules — better-sqlite3, node-pty, bun-pty (with its
rust-pty crate) each have their own cross-target quirks.

Pin arm64 → macos-14 and x64 → macos-13 so node-gyp and
@electron/rebuild build against the host arch. ELECTRON_BUILDER_ARCH
now just mirrors the runner for clarity.

* Revert "ci(release): split Electron arm64/x64 onto native macOS runners"

This reverts commit f217880e49609cf1418818af0f837b333dbb6f42.

* ci(test-build): add Electron DMG job to arm64 dispatch workflow

Parallel job to the existing Tauri DMG builder, same runner + Apple
cert path. Mirrors the release workflow steps (build:web-assets,
bundle:main, rebuild:native, electron-builder) so maintainers can
smoke-test a signed+notarized Electron DMG before merging.

* ci: use electron-builder v26 boolean arch flags

v26 dropped --arch <name> in favour of per-arch booleans (--arm64,
--x64, etc.). Test build was failing at dispatch time; release job
had the same bug latent. Switch both to the supported form.

* fix(electron): route external links to the system browser

<a href> clicks and window.open calls with non-local URLs were loading
inside the Electron BrowserWindow (or spawning a second Electron window
as a makeshift browser). Add an origin-aware navigation guard to each
window: loopback / state.localOrigin / configured desktop hosts keep
their existing in-window behaviour (HostSwitcher, in-window probes);
everything else hands off to shell.openExternal so http/https links
open in the user's default browser.
2026-04-20 15:41:15 +03:00
Bohdan Triapitsyn 4506c18f53 UI refresh: Base UI migration + flat tinted button language + mobile polish (#960)
* deps: bump @opencode-ai/sdk to 1.4.6 and add @base-ui/react

* build: align vite configs with Base UI and packages update

* feat(ui): migrate primitives to Base UI

Replace Radix-based wrappers (button, checkbox, collapsible, command,
dialog, dropdown-menu, input, radio, scroll-area, select, separator,
sonner, switch, textarea, toggle, tooltip) with @base-ui/react
equivalents and a refreshed visual language: flexoki theme tokens,
typography utilities, and a redesigned CommandPalette. Add new
fancy-button variant set and a local slot helper for asChild composition.

* refactor(ui consumers): adopt Base UI primitives across the app

Update chat, layout, git views, settings, multirun, session and agent
management screens to use the new Base UI-backed primitives (anchor
width token rename, dropdown/select/dialog usage adjustments, new
component APIs).

* fix(ScrollableOverlay): forward `disabled` to inner element

Add `disabled` to the type so it propagates via rest spread to the
underlying element (e.g. textarea in simple mode). Resolves a type
error reported after the main merge.

* refactor(dropdown consumers): migrate preventDefault sites to closeOnClick

Base UI's Menu.Item does not check `event.defaultPrevented` before
closing the menu, so the Radix-era `onSelect={(e) => e.preventDefault()}`
pattern no longer keeps the menu open. Replace those sites with the
Base UI `closeOnClick={false}` prop (VSCodeLayout rate-limit info
rows; AgentGroupDetail worktree actions).

* docs(AGENTS): note Base UI as primary source for UI primitives

* fix(ContextPanel): center expand/close buttons in tab header

Header (h-8) had the action row pinned with items-end + pb-1.5, which
placed the h-7 buttons ~2px above the header top and made them look
cropped. Switch to items-center so they sit on the same baseline as
the tabs.

* style(tabs): drop transitions on default variant, soften active pill

Default (underline) variant now switches instantly — no transform/width
transition on the underline indicator, no color transition on tab
buttons. Active pill bg is color-mixed down to 55% of
--interactive-selection so light-theme selection doesn't scream.

* style(button): pill-style elevation and introduce chip variant

Solid variants (default/destructive/neutral/primary/basic) now use the
active-pill shadow stack — hairline navy outer ring + stacked tight/soft
drops — instead of the pure-black inset/border approach. Outline
variant (and FancyButton basic) gets the same elevation without the
white top highlight (no fill to catch light).

Add a new chip variant: flat `border border-border/60` button without
elevation, intended for one-of-N toggle groups where a raised
button would read as the wrong affordance.

* style(header): apply pill-style shadow to OpenIn and Project Actions

Both split-button wrappers previously used a 1px CSS border for
definition. Replace that with the pill elevation stack so the header
buttons match the new Button variants and feel less hard-edged.

* refactor(toggles): migrate one-of-N segmented groups to chip variant

Color Mode, When-sessions-expire, voice/STT providers, auth method,
agent mode, MCP transport, tunnel type and terminal Ctrl/Cmd modifier
toggles all read as flat chip selectors rather than raised buttons.
Switch them from variant="outline" to the new chip variant so the
raised pill-style elevation doesn't fight the selected-state tint.

* fix(elevation): theme-adaptive pill-style shadow for dark mode

The navy (rgba(14,18,27,*)) outer ring was invisible on dark surfaces,
erasing the button/pill edge. Add dark: variants that use a subtle
white outer ring (rgba(255,255,255,0.10)) and darker drop shadows
(rgba(0,0,0,0.30/0.20)) so the elevation reads on both themes.

Applies to Button solid/outline variants, FancyButton equivalents,
the OpenIn and Project Actions header split-buttons, and the active
pill indicator in SortableTabsStrip.

* style(button): flat tinted variants with squircle corner-shape

Replace elevated pill-shadow design with a flat tinted language: pale tinted
fill + hairline tinted border + saturated tinted text for default/destructive.
Add CSS corner-shape: squircle with @supports gate so Chrome 136+ gets the
softened superellipse corners at 50px, Safari falls back to a slightly larger
round radius.

* style(tabs): replace active pill shadow with tinted border

* style(header): flat border and squircle corners for OpenIn / Actions

Drop pill-style shadow stacks in favor of a plain hairline border and apply
the same squircle corner-shape treatment as Button so header controls match
the new visual language.

* style(checkbox): drop filled background for tinted checkmark

Checked / indeterminate states now keep the transparent box, show a
primary-base check/minus glyph, and use a softened primary-tinted inset
border (50% mix) so the control reads as selected without a solid swatch.

* style(layout): widen sidebar-to-chat corner radius to 10px

* style(button): retune tinted variants and unify outline fill

- Bump primary fill toward noticeable but still soft (light 10/16/22,
  dark 16/22/30) and weaken both primary and destructive borders
  (12% light / 20% dark) so the tone reads without shouting.
- Further calm destructive across fill and border so revert-style
  actions match the muted reference.
- Align the outline variant fill with --surface-elevated so git-view
  generate/commit buttons share the header action button surface.

* style(tabs): align pill variant with new button language

Active pill now uses --surface-elevated fill and border-border/60 to
match the outline button. Track and tab/pill geometry adopt the same
squircle treatment as Button: 9-10px fallback with 50px radius under
@supports(corner-shape:squircle). Also tighten track padding (inline
2px, block 2px) and grow active tab height so the indicator fills
the track instead of floating inside it.

* refactor(theme): hardcode radius scale, drop per-theme override

Radius values are now fixed in the design system (flexoki-dark's scale
becomes the app default). Themes can no longer override --radius-* — the
config.radius block is removed from every theme JSON, dropped from the
Theme type, and no longer emitted by the CSS generator.

* chore(ui): lint/type cleanup across Base UI wrappers

* style(tabs): dial pill track background down to near-invisible

* refactor(chip): aria-pressed drives the selected tinted state

The chip variant now carries the tinted primary palette (same as the
default filled button) under aria-pressed, so consumers toggle selection
via a single aria attribute instead of repeating the border/text/bg
override classes. All existing chip consumers migrate to aria-pressed
and drop their hand-rolled selected className.

* chore(defaults): humanize variant label in thinking selector

* fix(diff): render all lines in single-file mode

Single-file mode wrapped PierreDiffViewer in its own layout=fill which
creates a nested virtual-root; combined with the outer CSS containment
on the wrapper, Pierre's virtualizer measured a clamped viewport and
stopped rendering lines past ~150. Mirror the stacked-mode approach:
hoist a ScrollableOverlay as the sole virtual root/content and render
PierreDiffViewer inline, so the shared virtualizer sees the real scroll
viewport and paints every line.

Also drops an unused cn import in SessionRetentionSettings.

* feat(files): collapse floating editor controls behind icon button

The floating file editor toolbar now starts as a single pill icon button
and expands into the full control row on hover or click. Clicking outside
or moving the pointer away collapses it back, keeping the editor surface
unobstructed while the file is being read/edited. Fullscreen overlay
keeps its always-visible toolbar.

* fix(chat): align mobile session status bar radius with chat input

* fix(mobile): keep radio/checkbox/switch at their native size

Mobile CSS forced min 36x36px on every <button>, which swallowed our
Radio and Checkbox primitives (also buttons with role=radio/checkbox)
and ballooned them on touch devices. Exclude role=radio/checkbox/switch
from the touch-target rule and pin the Radio/Checkbox box size with
arbitrary px so padding-scale overrides cannot stretch them either.

* chore(mobile): drop unused cornerRadius prop plumbing

The status bar now hardcodes var(--radius-lg) on both views, so the
cornerRadius prop and the useUIStore selector that sourced it in
ChatInput are dead weight.
2026-04-19 15:52:54 +03:00
Bohdan Triapitsyn 3e52be95c5 feat: add configurable planning magic prompts 2026-04-19 01:49:50 +03:00
Bohdan Triapitsyn 46102ffbf3 feat: add ability to upload external plans and remove them from sidebar 2026-04-19 00:52:14 +03:00
Bohdan Triapitsyn f04d732e15 fix(chat): resolve parent project for worktree-session messages
Save-as-plan and Add-to-notes actions were disabled on messages whose session
ran in a worktree, because session.directory alone does not reflect the
worktree attachment — the authoritative cwd lives in session-worktree-store.

Use useEffectiveDirectory (attachment cwd > worktree metadata > session.directory)
and resolve the parent project via the worktree map the sidebar already builds,
extracted into lib/projectResolution so session-ui-store and chat actions share
one resolver. No extra git calls.
2026-04-18 16:05:55 +03:00
Bohdan Triapitsyn df13a54f37 fix(vscode): hide Save-as-plan and Add-to-notes actions in VS Code runtime
Both flows write into ~/.config/openchamber which isn't reachable from the
VS Code webview, so the buttons were dead weight there.
2026-04-18 13:50:09 +03:00
Bohdan Triapitsyn 01b4f11e56 fix(agent-selector): read agents from active-directory config store
AgentSelector used useAgentsStore which is only warm after a chat session
loads, so the todo send dialog opened from the project context panel showed
an empty agent list. Prefer useConfigStore.agents (the same source ModelControls
uses in chat) with useAgentsStore as fallback, and load both on mount when
neither is populated.

Also set the primary-mode filter on the scheduled task editor agent selector
to keep its options consistent with the other flows.
2026-04-18 13:47:24 +03:00
Bohdan Triapitsyn 8d43678fea feat(notes): add selected chat text to project notes with AI-distilled insight
Extend the text selection popover with an Add to notes action that runs the
selection through the shared text summarizer in note mode and appends the
distilled insight to the active project's notes.
2026-04-18 13:47:16 +03:00
Bohdan Triapitsyn a7bc2a228c feat(todos): send todo to new session or worktree with model/agent/variant picker
Add a dedicated TodoSendDialog that lets the user pick provider/model,
reasoning variant and agent before opening a new session (or a fresh
worktree branch) pre-seeded with the todo text and a hidden instructions
prompt (plan.todo).
2026-04-18 13:47:11 +03:00
Bohdan Triapitsyn 799a202e99 feat(plans): save assistant messages as editable project plans with improve/implement flows
Add Save-as-plan action on assistant messages that writes a markdown plan
into the project plans directory and suggests a title from the text. Plan
view opens a specific saved plan via targetPath, supports inline edits, and
can kick off improve or implement flows in a new or worktree session.

Ship the matching hidden magic prompts (plan.todo/improve/implement) so the
instructions applied by these flows are visible and editable in the Magic
Prompts page.

Remove the plan editor font override, CodeMirror now uses the app font stack.
2026-04-18 13:47:05 +03:00
Bohdan Triapitsyn d203ba2ae0 feat(sidebar): project notes/todos live in right sidebar context tab
Replace the header sticky-note popover/mobile overlay with a dedicated Context
tab in the right sidebar. Context is cycled by the right-sidebar shortcut
alongside git and files, and persisted across reloads.
2026-04-18 13:46:57 +03:00
Bohdan Triapitsyn a9c51f115e refactor(text): extract shared summarization service with tts/note/notification modes
Move server-side summarization out of lib/tts/summarization.js into a shared
lib/text module exposing tts, note and notification modes. TTS and notification
runtimes now delegate to the shared service, note mode is used to distill a
selected excerpt into a short project note. Expose the new endpoint via
/api/text/summarize and route it through the common request middleware.

Client voice/summarize.ts accepts a mode option and points at the new
endpoint with an absolute URL that respects the desktop server origin.
VS Code webview swaps the stubbed tts summarize path for /api/text/summarize
and hardens URL parsing with window.location.href as the base.
2026-04-18 13:46:51 +03:00
Bohdan Triapitsyn 4901cf60b8 fix(projects): deterministic project identity + safe per-project persistence
Derive project.id from project.path (path_<base64url(path)>), shared helper on
both server (lib/projects/project-id.js) and client (lib/projectId.ts). Replace
random UUIDs so icons, notes, todos, actions, setup-worktree, plans and
scheduledTasks share one id across restarts and reinstalls.

Fix read-then-overwrite clobber in project-config.js: scheduled-task writes now
merge with the existing project json instead of replacing it, preserving
client-written fields that live in the same file.

On settings load migrate legacy UUID ids to canonical path ids, moving config
json, storage dir contents and icon files, and remap activeProjectId. Scan for
orphan non-path_* project configs and merge them into the canonical project
when a \$ROOT_PROJECT_PATH/<file> reference resolves on disk, logging any that
can't be matched.

Client openchamberConfig.writeOpenChamberConfig re-asserts server-owned keys
(version, scheduledTasks) on write to defeat the symmetric race. Stores and
persistence derive ids from path consistently.
2026-04-18 13:46:38 +03:00
Bohdan Triapitsyn 5710198864 release v1.9.6 2026-04-17 19:06:27 +03:00
Bohdan Triapitsyn 2766327a68 fix: tighten disconnect recovery guards 2026-04-17 19:00:44 +03:00
jwcrystal f5535dcaf1 fix: recover from sleep/wake disconnection with connection state tracking and immediate health check (#940)
When the computer sleeps and wakes, the SSE/WS event stream drops
silently. Messages appeared sent (optimistic insert) but never reached
the OpenCode server, and the user had no indication the system was
disconnected.

Three fixes:

1. Connection state tracking: add onDisconnect callback to the event
   pipeline. Stream failures set isConnected=false in useConfigStore;
   successful reconnect sets isConnected=true.

2. Send guard: optimisticSend, respondToPermission, and
   respondToQuestion now check isConnected before making API calls,
   throwing a clear error that surfaces as a toast to the user.
   The /compact command also checks connection with error feedback.

3. Faster server recovery: add triggerHealthCheck() to the server
   lifecycle and wire it into the WS event stream runtime. When the
   upstream OpenCode connection fails, the server immediately checks
   health and restarts if needed, instead of waiting up to 15s for
   the periodic health check.
2026-04-17 18:48:39 +03:00
Bohdan Triapitsyn b3f2732dff fix(vscode): make session markdown export save/reveal work 2026-04-17 18:46:13 +03:00