c8a08588d9cf1881a53de3fa5644b5eda22b84bb
252
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
673951cb3a | fix(ui): restore composer focus after CodeMirror migration | ||
|
|
8ff6525906 | fix(ui): capture double escape abort in composer | ||
|
|
e2fa7dbad2 |
feat(ui): context panel 2.0 - surface rail, changes-first git view, live PR surface (#2418)
* feat(ui): add context surface registry and rail switcher * feat(ui): move git and project notes into context surfaces, embed editor file tree * feat(ui): replace right sidebar with context surfaces, per-surface panel widths * refactor(ui): retire legacy main-tab overlays and right-sidebar state * feat(ui): rail polish, right-docked file tree, terminal surface * feat(ui): move terminal into context surface, per-surface tab closing, editor empty state * feat(ui): tune default rail order and activity dot * fix(ui): keep context panel controls anchored during width animations * feat(ui): lazy-follow context panel resize with window-level drag tracking * feat(ui): panel dividers, right-dock tree icon, muted outline folder icons * feat(ui): restructure git view into changes-first surface with standalone PR surface - Remove commit/update/pr tabs; git view is always changes + commit - Promote pull request to its own rail surface with shared repo context - Move update-branch and re-integrate flows into separate dialogs - Add PR status chip and repo actions menu to the git header row - Seed new PR-status entries from resolved sibling remotes to avoid a false "checking status" state when the PR is already known - History/graph dialog refresh button, fingerprint global identity icon, muted outline folder icons follow-ups * feat(ui): progressive-disclosure PR surface with live checks and pinned chat context - Segment the PR surface into Overview / Checks / Comments pill tabs with live badges; merge controls move to the status row - Live checks segment: progress bar, per-run rows with workflow names, elapsed timers, expandable failures, auto-refresh while pending - PR comments and failed checks pin as chat-context drafts (like terminal selections) instead of sending an immediate message; works on new-session drafts too - Shared prContext cache client+server, ETag conditional requests in the octokit wrapper (304s bypass rate limits), extended checks aggregate (inProgress/queued/startedAt) - Resolve gh-CLI auth login for merge-permission checks - Full-width description editor with matched control heights * fix(ui): single source of truth for PR checks and status readers - Derive the checks aggregate from the visible run list and sync it into the PR-status store so bar, badges, header, and git-view chip agree - Route PR body hydration through the shared context cache - Git-view PR chip reads the freshest entry across remote keys * fix(github): freshness stamps prevent stale cache responses from regressing PR state - pr/status and pulls/context responses carry a server-side fetchedAt that survives cache serves - The status store rejects responses older than the held snapshot (only clearing the loading flag), and the checks sync adopts the context's stamp so stale status polls cannot flip fresher derived checks - Regression test for the stale-response guard * perf(github): repo-level pull-list cache collapses per-branch PR resolution - One pulls.list per repo per state per 45s answers every branch (10 worktrees = 1 call, not 10 query fans); in-flight fetches coalesce - A complete repo list makes a no-PR miss authoritative, skipping the per-owner head queries AND the Search API fallback (the 30/min killer) - force refresh bypasses the repo list cache; PR create/merge/ready invalidate it * perf(github): back off Search API misses per repo+branch A branch without a PR re-searched on every poll; with >100 closed PRs the list miss is never authoritative, so the search fallback still ran and burned the 30/min search quota. Remember misses for 10 minutes; PR creation clears remembered misses for the repo. * fix(github): dedupe re-run check runs to the latest per (app, name) listForRef returns the superseded completed run alongside its re-run; GitHub's UI shows only the latest per name. Mirror that in both pr/status and pulls/context so counts and run lists match github.com. * fix(ui): address review findings on registry test, surface docs, and PR-context keys - Rail-order test asserts against the registry itself (was stale after the 'pr' surface landed and failed) - surfaces DOCUMENTATION.md describes actual behavior: has-content surfaces hide until content exists; only multi-instance/terminal panes are keep-alive, singleton surfaces remount and restore from stores - PR-context cache keys are runtime-scoped JSON tuples; invalidation compares the directory exactly instead of by string prefix (+ test) * fix(ui): wrap long unbreakable tokens in check-run details Annotation messages with long SHAs/URLs overflowed the panel; break-words on annotation title/message/rawDetails and output summary/text, and the expanded run body clips instead of widening the panel. * fix(ui): busy state for context-attach buttons and honest attach labels - 'Attach failed checks' / 'Attach all to chat' show a spinner and disable while the context request runs (previously nothing happened for seconds) - Action labels/tooltips reworded from send-to-agent to attach-to-chat semantics across all locales * fix(i18n): Ukrainian attach wording uses 'прикріпити' with proper cases * fix(ui): runtime-scope PR-view remote caches, correct surfaces doc on preview - Remote/remote-url caches in PullRequestView are keyed by runtime + directory so a backend switch never serves another runtime's remotes - surfaces DOCUMENTATION.md: preview is not keep-alive; preview tabs remount on switch like singleton surfaces * fix(ui): rail active color, clearer collapse icon, remove dead bottom-terminal dock Design-review feedback on the context panel: - Context rail: icons enlarged 16px -> 18px; the active surface is now highlighted with the primary color only (no background, no scale animation), replacing the previous scale-up effect that read as a resize rather than a selected state. - Files tree: the icon-only 'collapse all folders' toolbar button now uses collapse-vertical instead of contract-up-down, which was easily mistaken for a close button. The labelled 'Collapse all' dropdown item in the session sidebar keeps its icon since text removes the ambiguity. - Terminal: removed the leftover bottom-dock expand/close buttons that rendered in the context-panel terminal but controlled a dock that no longer exists (nothing toggles it anymore), so the expand button appeared to do nothing and duplicated the panel-header fullscreen control. Cleaned up the entire inert layer with it: four useUIStore fields (isBottomTerminalOpen/Expanded, bottomTerminalHeight, hasManuallyResizedBottomTerminal), five actions, their persistence, the MainLayout resize listener that only served the dock height, the dock-driven refit effect in TerminalView, and the terminalView.bottomDock.* keys across all 10 locale dictionaries. Validated: ui type-check and lint clean; messages parity test (2 pass) and useUIStore contextPanel test (13 pass) green; icon sprite regenerated via icons:generate. * refactor: use PR visual state for git header icon Derives the pull request icon color from a single visual state Covers merged, closed, draft, blocked, and open PR states Removes conditional class handling from the git header icon |
||
|
|
8aaef3dec1 |
fix(desktop): dedupe Linux tray D-Bus calls to stop main-thread freeze (#2459)
The tray controller called setToolTip and setContextMenu unconditionally on every snapshot push. On Linux both are synchronous D-Bus calls into plasmashell's StatusNotifierItem host. useTraySync debounced pushes at 120ms (up to ~8/sec during token streaming), so each push made 2 blocking D-Bus round-trips even when only dockBadgeCount changed or a token streamed into an already-listed session — i.e., tooltip and menu content didn't change. The native block has no JS-level log, is intermittent (depends on plasmashell load), and eventually freezes the Electron main thread until crash. setTitle and setImage were already deduped; setToolTip and setContextMenu were the gap. Mirror the existing lastX pattern: - tray.mjs: dedupe setToolTip (exact string compare via lastTooltip) and setContextMenu (via lastMenuKey, a lightweight signature of menu-affecting fields: sessions/approvals/usage). menuKey is a string concat, cheaper than buildMenu itself, so skipping buildMenu when the key matches also saves work. Cache-after-call ordering matches lastTitle/lastTooltip so a throw forces a retry rather than skipping one. destroy() resets both new vars. - useTraySync.ts: FLUSH_DEBOUNCE_MS 120 -> 500. Tray doesn't need sub-second updates; approvals are rare discrete events that flush through the debounce and the main app UI stays instant via SSE/stores. The first change is the real fix; the second is a complement. Either alone helps; together they eliminate the freeze under streaming. |
||
|
|
16df9412fd |
fix: update keyboard shortcut abort logic
Use current session activity to determine when abort shortcuts are available Prevent abort priming when there is no active session |
||
|
|
18b58bdb6b |
feat(desktop): Linux AppImage releases and desktop feature parity (#2398)
* feat(electron): add Linux AppImage releases * ci: cache Linux OpenCode CLI artifacts * fix(ci): await Linux release inventory check * fix(electron): add frameless window controls on Linux desktop Linux AppImages were created without native WM decorations and without in-app controls, leaving users unable to close the window with a mouse. Treat Linux like Windows: frameless BrowserWindow plus the existing WindowsWindowControls header buttons and app-menu entry. macOS keeps hidden title bar with traffic lights unchanged. Shared usesFramelessElectronChrome() helper drives main window, mini chat, header insets, and titlebar controls. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Linux desktop feature parity: Open in, background start, tray, multi-window (#2392) * feat(electron): Linux parity for Open in, background start, and tray Enable Linux desktop feature parity with macOS/Windows: open projects in the default file manager and discovered apps, XDG autostart with --background launches, system tray (including minimize-to-tray), and tray sync from the renderer. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(electron): allow packaged UI protocol navigations on Linux Prevent openchamber-ui:// navigations from being handed to shell.openExternal, which fails on Linux and blocked desktop UI flows. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(ui): surface Linux tray settings in settings search Include isLinux in settings search runtime context so minimize-to-tray is discoverable on Linux desktop, matching Windows search behavior. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(electron): stop Linux AppImage Waiting-for-OpenCode flicker Sync updated boot-outcome init scripts to all BrowserWindows after desktop_hosts_set, and prefer state.initScript on dom-ready so chooser reloads inject local/ok instead of a stale not-configured outcome. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(desktop): restore Linux AppImage updater feed and error UX (#2396) Treat missing latest-linux*.yml (404) as no update available instead of a hard failure, and stop swallowing updater capability/download errors in the desktop bridge so About/sidebar can show actionable messages. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * docs: credit Linux AppImage contributors in changelog Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: jibanez-staticduo <staticduo@gmail.com> Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> |
||
|
|
89f7c37d60 |
fix: make session ordering follow activity lifecycle
Session lists now promote a conversation when it starts working and again when it settles, instead of reacting to every streaming timestamp update. This keeps ordering responsive without bringing back the sidebar churn removed by the recent performance work. Apply the same user-visible order across Recent, project and worktree groups, session switchers, mobile navigation, widgets, the command palette, and the desktop tray. Preserve pinned priority, freeze timestamp fallback ordering, and keep child-session activity scoped to siblings under the same parent so it never moves the root conversation. Seed reconnect snapshots without synthetic jumps, clear ephemeral ranks on deletion and runtime changes, and cover lifecycle transitions, mixed root/child trees, metadata-only updates, and project-group ordering with regression tests. |
||
|
|
1c7cdf68e3 |
feat: add a macOS menu bar toggle for desktop
Adds a General settings switch to show or hide the macOS menu bar item. Persists the new desktopMacMenuBarEnabled setting and passes tray state into Electron. Disables tray sync and native tray setup when the menu bar is turned off. |
||
|
|
ab97e00ff2 | fix(ui): show active assistant model in status | ||
|
|
85400459e9 |
perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing cache, synchronization, and persistence correctness across runtimes, projects, directories, and worktrees. - prioritize selected and visible sessions during bootstrap and defer non-critical enrichment work - reduce redundant message loading, event processing, store publication, and hidden sidebar work - prevent stale session and message requests from overwriting newer authoritative state - preserve existing data when authoritative fetches fail instead of treating failures as successful empty responses - scope session materialization, messages, drafts, queues, todos, pins, permissions, folders, tabs, Git state, and pull request data by runtime and directory identity - harden runtime switching, reconnect, cleanup, mutation reconciliation, and persisted-state ordering - preserve live subagent Task linkage when metadata arrives after an older message request or while streaming parts are suspended - coalesce overlapping tail refreshes without losing newer refresh demand - improve cold-session loading by moving deferrable work out of the critical bootstrap path - isolate URL authentication, mobile credentials, native secrets, and other runtime-owned state across endpoint changes - bound long-lived caches and remove avoidable allocations from event and rendering hot paths - limit virtualization to archive collections where it improves rendering without disrupting active sidebar layout - stabilize session folders, pin ordering, expanded state, and persisted sidebar behavior - open skill files through the same secure editor and outside-workspace grant flow used by file navigation, including worktree sessions - expand regression coverage for stale completions, runtime collisions, reconnect behavior, persistence races, authoritative empty results, and subagent refresh ordering - document the updated synchronization, cache ownership, performance, and runtime-isolation invariants |
||
|
|
4be6854ba5 |
feat: show model in working indicator and fix streaming bottom jitter
- Working indicator shows provider icon and model name with the live status (e.g. 'Fable 5 is reading file'), localized via chat.statusRow.modelStatus - Pin auto-follow to the exact fractional scroll maximum and re-pin on every passive follow instead of skipping within tolerance - Round message text line-height to whole pixels so streamed content grows on the pixel grid; kills the 1px vertical jitter of bottom-anchored rows during streaming |
||
|
|
d4a8c4d2e1 |
feat(terminal): refactor runtime and add mobile workspace (#2280)
Replace the legacy terminal flow with a shared authenticated WebSocket runtime used across web, desktop, relay, and mobile surfaces. - introduce the v3 terminal protocol with scoped attachments, snapshots, ordered output, bounded replay history, reconnects, and explicit lifecycle - harden PTY creation, restart, resize, close, force-kill, idle cleanup, shell selection, login mode, environment sanitization, and appearance sync - add runtime-aware terminal APIs with relay authentication and Electron parity - add a fullscreen mobile terminal workspace with touch scrolling, long-press selection, safe-area controls, quick keys, and Ctrl/Alt input - add terminal selection attachments, preview detection, project actions, shell settings, and localized UI - harden Ghostty rendering, resize recovery, Unicode handling, block characters, line height, and stale-row behavior - remove the obsolete terminal SSE path and update reverse-proxy guidance - expand terminal runtime, transport, input, selection, and store coverage - avoid duplicate web builds when preparing mobile assets in root CI builds |
||
|
|
1fb448d827 |
feat(chat): desktop prompt navigator rail (#2054)
* feat(chat): add desktop prompt navigator rail Add a ChatGPT-style right-center prompt marker rail for web/desktop chat with hover/keyboard preview panel, load-more for partial history (panel only), Chat setting, and mod+alt+p shortcut. Disabled in VS Code across rail, shortcut, settings, help, and search surfaces. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(ui): read promptNavigatorEnabled from getState in shortcut handler Match the file convention used by other shortcut handlers so the toggle does not rely on a hook-level selector closure. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(ui): drop use-no-memo and default prompt navigator off Remove the project-unprecedented React Compiler opt-out, and ship the prompt navigator as opt-in to match other recent chat UI toggles. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> |
||
|
|
502c96630e |
feat(desktop): Linux AppImage polish — window controls, updater UX, docs (#2144)
* feat(electron): add Linux AppImage releases * ci: cache Linux OpenCode CLI artifacts * fix(ci): await Linux release inventory check * fix(electron): add frameless window controls on Linux desktop Linux AppImages were created without native WM decorations and without in-app controls, leaving users unable to close the window with a mouse. Treat Linux like Windows: frameless BrowserWindow plus the existing WindowsWindowControls header buttons and app-menu entry. macOS keeps hidden title bar with traffic lights unchanged. Shared usesFramelessElectronChrome() helper drives main window, mini chat, header insets, and titlebar controls. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * feat(desktop): add configurable window controls position by OS Add desktopWindowControlsPosition setting (auto/left/right) with OS-aware defaults: Linux left, Windows right. Wire frameless chrome controls in Header, TitlebarLeftControls, and MiniChatLayout, plus a Sessions settings control for Windows and Linux desktop shells. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(desktop): address Linux AppImage release review findings Propagate updater capability errors to the UI, treat missing latest-linux.yml feeds as no-update, stop installed-apps IPC spam on Linux, document FUSE/AppImage limits, add CHANGELOG entry, migrate remaining btriapitsyn URLs, and run Electron Linux unit tests on PRs. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: jibanez-staticduo <staticduo@gmail.com> Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> |
||
|
|
7888dd65b3 | fix(chat): enable in-place subtask navigation in embedded session-chat iframe (#2138) | ||
|
|
c9ac8676e7 |
feat: file-backed goal objectives + compaction accounting fixes
Compaction fixes (observed in a real long run): - the summary message's zeroed tokens froze the goal counter at its pre-compaction value; segments now close with the previously displayed total as a continuity floor - audits and continuations after a summary tail now take execution params (provider/model/agent/variant) from the newest non-summary assistant turn instead of inheriting agent 'compaction' and the summarize model File-backed objectives: - the objective text lives in <data-dir>/goals/<sessionId>.md, keyed by session id (one goal per session, a new goal overwrites the file); metadata carries only an objectiveFile flag so session.updated fanout stays light, and never a path — ids are pattern-validated before any filesystem access - limit raised to 5000 chars, no snapshot field: the UI fetches content via PUT/GET/DELETE /api/goals/objective/:sessionId (behind the blanket /api auth gate), writes the file before stamping metadata, and falls back to an inline objective when the write fails - the loop reads the file fresh on every tick, so objectives are live-editable mid-goal; a missing file falls back to the inline text - scheduled goal tasks write the objective file server-side; VS Code degrades to the audit note (route unavailable there by design) |
||
|
|
bb45164ae8 |
feat: session goals - server-driven goal loop with independent small-model audit (#2148)
Arm the target button in the composer and the next prompt becomes a goal: the server keeps the session working toward it (idle tick -> small-model audit -> continuation) until the objective is verifiably complete, blocked, or out of budget — even with the UI closed. Server (packages/web/server/lib/session-goal): - event-driven loop on the global SSE hub; goal state lives in session.metadata.openchamber.goal (merge-safe patches, stale-write guard by goal id), so it survives restarts and syncs to every client for free - the small-model audit (objective + last assistant turn only, language pinned to the objective) is the sole termination authority; blocked needs 3 consecutive verdicts, audit outages tolerate one unaudited continuation then stop the goal as resumable-blocked - hard stops: optional token budget, auto-continuation cap (Resume grants a fresh allowance), turn errors; user abort pauses the goal instead of blocking it, and resuming over an aborted tail nudges immediately - token accounting as a snapshot of the latest turn (input + cache.read + output), goal-relative via a creation baseline and segmented across compactions; a compaction summary skips the audit and continues - continuations reuse the session's own provider/model/agent/variant UI: - three-mode target button (arm / disarm / manage dialog), informational goal strip with inline pause/resume and an Evaluating indicator, sidebar state glyph, objective length counter (2000-char server clamp), read-only completed goals - goal entry points: composer (sessions and drafts), start-new-session- from-answer dialog, plan implement dialog (plan content becomes the objective), scheduled tasks (Run as goal + budget) - Settings -> Chat -> Goal: feature toggle + default token budget with three-layer parity (web server, client persistence, VS Code bridge); VS Code renders goal state but hides the entry points (the loop runs in the web server only) Notifications: per-turn "ready" notifications are suppressed while a goal is active; settling sends one final notification (desktop, web-push, APNs generic titles with the session name as body) honoring the completion toggle. Error/question/permission notifications are untouched. Docs: user guide (session-goals) in all 9 locales + sidebar entry, scheduled-tasks cross-reference, server module DOCUMENTATION.md. |
||
|
|
82c039117a | fix(queue): back off failed queued auto-sends | ||
|
|
002c7a70ad |
fix(ui): dispatch queued messages when session is already idle (#1978)
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com> |
||
|
|
4296efb64d |
feat(electron): add Windows startup and system tray support (#2112)
Add Windows launch-at-login with background startup support and extend the native tray integration to Windows. Add a Windows-only setting to minimize or close the main window to the system tray, persist it through desktop settings, and expose it in Settings search and all locale dictionaries. Keep tray state synchronized with live sessions on both macOS and Windows, while preserving the existing macOS behavior. |
||
|
|
42e470cefa |
fix: keep browser tab session navigation stable
Keep the browser pane loaded URL separate from the in-frame current URL so SPA navigation updates the address bar and history without remounting the iframe or resetting the Electron webview src. Preserve parsed ?session= route params during initial URL normalization and pass a directory hint when applying deep links, preventing embedded OpenChamber sessions from collapsing back to / while bootstrap catches up. |
||
|
|
40dfff4a9a | fix: handle ambiguous prompt transport failures | ||
|
|
182fc89b8d | feat: split session recap and suggestion settings | ||
|
|
1824b51155 | fix: scope session assist to session directory | ||
|
|
28f0736d69 |
feat: small-model utility calls on existing OpenCode providers (#2049)
Adds a server-side "small model" capability: direct, cheap LLM calls that reuse the user's existing OpenCode provider logins — the mechanism OpenCode uses internally for titles and summaries but does not expose through the SDK or plugins. Zero new dependencies; plain fetch with per-provider wire formats, credentials never leave the server. Core (packages/web/server/lib/small-model): - Resolution mirrors OpenCode's session scoping: explicit settings override → small_model from the OpenCode config → family scan within the session's provider → the session's own model. The global provider scan only serves callers without a session context, and background callers forbid it entirely (restrictToPreferredProvider), so conversation content never reaches a provider the user didn't pick — explicit choices excepted. - Per-provider auth replicating OpenCode's plugin loaders: GitHub Copilot (device token as bearer, no exchange), ChatGPT plan via the codex Responses API (single-flight OAuth refresh written back to auth.json), Anthropic messages, Google generateContent, generic OpenAI-compatible. - OpenCode's free models (opencode/big-pickle, *-free) are never called directly; unauthenticated providers are skipped by design. - Prompt clamping to the model's catalog context limit; thinking disabled where a wire switch exists (Z.AI/GLM, MiniMax-M3, Gemini Flash); robust content parsing with a clear error when a thinking model spends its whol budget on reasoning. - Settings → Sessions gains a Small Model group: use-default checkbox plus an override picker limited to authenticated providers, persisted with web/desktop/VS Code sanitization parity. Consumers: - Session assist: a server-side watcher on the global SSE hub generates a short recap and one suggested follow-up after a session idles quietly fo a minute, stored on session metadata (openchamber.assist). Freshness is keyed to the last assistant message id, so new activity invalidates the payload everywhere with no extra writes. The chat shows the recap under the last message after five quiet minutes and the suggestion as a dismissible chip above the composer (tap fills the input, never sends). Gated by a new Chat setting (default on) that is a hard generation switch. Language is anchored to the conversation itself, with a script-mismatch guard against model/backend language hallucination. - TTS: a third input mode, summarized — long replies are condensed to spoken prose before playback on any TTS engine. - Git: commit-message and PR generation moved off the active chat session onto the small model fed with real diffs and the commit list (bodies included), with a session-transport fallback for free-model-only setups. - Notes: Add to notes distills long selections into 1-3 dense sentences preserving exact identifiers, with verbatim fallback on failure. Fixes along the way: - The global event watcher now starts unconditionally; it was gated behind the desktop-notify env, leaving the server-side event hub dead in packaged apps. - OpenCode re-emits message.updated for old user messages after idle; the watcher no longer mistakes those for new activity. - Session metadata merges from a fresh read right before the PATCH, so writes made during the generation window (suggestion dismissals, review links) are preserved; the assist runtime stops during graceful shutdown. |
||
|
|
2c220f3c52 |
perf(mobile): constant-viewport chat scroller across keyboard transitions
Transforms on the chat scroller (content or container) forced WebKit to rebuild its composited scrolling layers — a multi-second stall on long chats. The scroller now gets no transforms at all: it keeps a constant client height by extending below its shrunken region (by keyboard minus the safe inset the shell gives up, so the settle snap is geometry-neutral) and converting the keyboard strip into its own bottom padding, driven by --oc-kb-scroll-inset from the very start of the rise. Nothing resizes for the virtualizer, every reachable row stays mounted, and open/close is a single cheap scrollTop write: the re-pin happens as the keyboard starts rising, and the hide clamp lands behind the still-visible keyboard. The composer and draft title keep sliding with the keyboard, now via inline transforms set by the choreography — WebKit does not reliably start transitions when a transform changes through a CSS custom property, which had parked the composer until the keyboard finished. |
||
|
|
fb839b66a9 |
feat(mobile): collapsed pill composer and mobile input redesign
Mobile composer redesign: when the keyboard is closed the input collapses into a narrow pill (sessions, attach, placeholder, mic) with a round new-session button that fades away on the draft screen. Model and agent selectors move into a row above the textarea; the draft project/branch pickers and the attachment menu become searchable bottom sheets reusing MobileOverlayPanel; a drag handle (also available while dictating) swipes the composer into and out of a fullscreen mode. Keyboard-lifecycle hardening: composer controls (agent cycle, dictation and its overlay controls) no longer steal focus and dismiss the keyboard; overlays reopen the keyboard on close via a debounced restore chain that survives menu-to-picker handoffs and skips the native file picker; open overlays and dictation keep the composer expanded. Dictation starts directly from the pill and its overlay content fades in after the shape settles. The keyboard slide compensates the pill-to-full height change in one motion, and the mobile highlight mirror is disabled so the caret always matches the text layout. |
||
|
|
531039b690 |
feat(mobile): transform-based keyboard choreography on iOS
Stop animating the app shell height when the keyboard opens: per-frame reflow plus the scroll-follow chase caused visible micro-jitter on the composer and pinned chat. The shell layout now snaps exactly once per open/close at an invisible choreography point, while the composer and pinned chat content slide via compositor-only transforms in sync with the keyboard. Dismissal starts from the textarea focusout (no bridge latency), runs a shorter 0.2s leg, and the WebKit form accessory bar is disabled. Composer keeps a 12px gap above the open keyboard. |
||
|
|
de1b85ac56 |
feat(voice): first-class voice input and local TTS across web, desktop, and mobile (#2018)
Complete rebuild of voice input on a server-authoritative streaming architecture, replacing the legacy Web Speech / whole-blob / WASM engines and the dead voice-agent layer (~4k lines removed). Speech-to-text (dictation): - Client streams 16 kHz mono PCM16 chunks over /api/dictation/ws with seq/ack ordering; buffered audio is retained and replayed on reconnect - Server transcribes and streams live partial transcripts back; segments auto-commit every ~15s with silence suppression and adaptive finalization timeouts - Local provider (default, zero config): sherpa-onnx models in a forked worker process — auto-download with progress, staged extraction with verification, corrupt-model auto-recovery, idle shutdown after 5 min - Model catalog with settings picker (accuracy/speed ratings, sizes, download/delete): Parakeet TDT v2 (English) and v3 (25 European languages, auto-detected), Whisper base and tiny (multilingual, light) - OpenAI-compatible provider for any Whisper endpoint - Composer overlay with live transcript, volume meter, timer, and cancel / insert / insert-and-send actions; failed transcriptions keep their audio for retry or accepting the partial text as-is - Configurable keyboard shortcut (default mod+alt+v) toggles dictation; Enter confirms and Escape cancels while recording - Overlay is pixel-aligned with the composer (measured footer height, matching paddings/typography/gaps) — no layout shift when toggling Text-to-speech: - Local Kokoro provider (English, 11 voices) synthesized in the same worker via /api/dictation/tts/speak, managed by the shared model pipeline; sentence-pipelined playback keeps time-to-first-audio at ~1 sentence regardless of message length, and stop cancels in-flight synthesis - Sanitizer keeps inline-code content (strips backticks only), reads interword slashes aloud, and removes only absolute file paths Settings: - Voice page unified: a single read-aloud toggle owns all playback options (the confusing "Enable Voice Mode" is gone); a new "Enable voice input" toggle (default on, persisted to settings.json) hides the composer mic entirely when disabled Mobile and transport: - iOS/Android microphone permissions added (dictation was previously impossible on mobile) - Fixed Android WebSocket upgrades: the Capacitor WebView origin (https://localhost) was missing from the packaged-client allowlist, 403-ing every WS connection — root cause of the old mobile SSE lock, which is now removed for all transports Security and conventions: - All HTTP routes sit behind the global /api auth gate; the WS upgrade explicitly validates the UI session and origin, with oc_url_token narrowly allowlisted and covered by tests; the dictation socket mints a fresh URL token before connecting - Routes register before the generic OpenCode proxy; the client goes through runtimeFetch/getRuntimeUrlResolver, and runtime switches reset the dictation socket - VS Code deliberately reports dictation as unavailable (no server process in that runtime) CI: workflow Node bumped 20 -> 22 to match the repo engines and fix better-sqlite3 installs broken by node-gyp@latest on Node 20. New dependency: sherpa-onnx-node (prebuilt N-API; macOS/Linux x64+arm64, Windows x64 — Windows-on-ARM falls back to the OpenAI-compatible provider) |
||
|
|
61a4a23add |
feat: native iOS & Android mobile apps (Capacitor) (#1954)
* feat(mobile): add Capacitor native shell * docs: add serve-sim workflow guidance * docs(mobile): add implementation handoff * chore(mobile): clean up generated defaults * feat(mobile): add connection onboarding * feat(mobile): manage saved instances * feat(mobile): refine connection management UI * chore(mobile): upgrade Capacitor 8 * fix(mobile): reliable saved-instance auth with secure token storage - store client tokens in the OS secure store (iOS Keychain / Android Keystore) per instance URL via direct native plugin calls; keep only token-less metadata in localStorage. Bound every secure call so a stalled bridge can't hang unlock. - bypass the secure-storage JS wrapper's lazy platform load (which stalled in the webview) by calling internalSetItem/internalGetItem/internalRemoveItem directly. - harden the shared connect/unlock controller (health + session + progressive password) and drop the heavy pre-connect hydration that stalled no-token hosts. - await token persistence before switching runtime endpoints (no fire-and-forget). - sync native iOS/Android projects + Keyboard/StatusBar config for Capacitor 8. * fix(mobile): keep UI stable across connection churn (no transport hardcoding) The "reload every ~10s" was a UX bug, not a transport one: - MobileSurfaceShell received a fresh inline onClose each parent render, so any re-render (e.g. an SSE/WS event) re-ran the focus effect and refocused the first element — stealing focus from the active input and collapsing the keyboard mid-edit. onClose now lives in a ref so the focus/keydown effect depends only on `open`. Fixes all sheets (Instances/Files/Changes/Settings). - Gate the mobile shell on connectionPhase, not the live isConnected flag, so a transient reconnect keeps MobileShell mounted instead of flashing the loader. - Instances form: populate fields imperatively on edit/cancel/save instead of via an effect keyed on the derived connection, so list churn can't wipe input. Transport stays on `auto` (WS-first with SSE fallback) — no hardcoded override, so WS-only Quick Tunnels and SSE-capable proxies both keep working. * feat(mobile): add native QR pairing-code scanner Wire the connection onboarding + Instances scan buttons to a real native scanner via @capacitor-mlkit/barcode-scanning, which registers as the BarcodeScanner plugin the existing mobileQrScan helper already resolves at runtime. Add NSCameraUsageDescription and bump the iOS deployment target to 15.5 (GoogleMLKit 8 requirement). * fix(cli): repair connect-url host resolution Define the missing isWildcardBindHost helper that connect-url called but was never declared, which crashed any link generation that reached host resolution. Also treat a full http(s) --host value as a public server URL so '--host https://example.com' produces a correct link instead of 'http://https://example.com:port'. * fix(mobile): make input follow the keyboard across all surfaces Switch the native Capacitor Keyboard plugin to resize: 'none' and drive the layout from an --oc-keyboard-inset CSS variable set on keyboardWillShow, which fires at the start of the iOS keyboard animation. A transition tuned to the native keyboard curve/duration (0.25s, cubic-bezier(0.38, 0.7, 0.125, 1)) makes the layout rise together with the keyboard instead of snapping into place after the built-in 'native' resize finished (~1.5s lag). The inset is consumed by every surface that can hold a focused input: - chat shell shrinks its height; - portal sheets/overlays raise their bottom edge; - the full-screen connect/login view caps its height so it actually scrolls (and is now generally scrollable for long saved-connection lists). * feat(mobile): rounder chat composer + native bottom safe area Round the mobile chat composer corners a touch more (1rem), and reserve a small app-level bottom safe area for the native shell via the --oc-app-bottom-safe token so controls clear the phone's rounded hardware corners. The reservation folds into the keyboard inset (no gap above the keyboard), and the composer's own bottom padding tightens while the keyboard is open. * fix(mobile): remove iOS 26 dark status-bar band; polish composer The dark band behind the status bar in system Dark Mode was iOS 26's automatic scroll edge effect (Liquid Glass) dimming the WebView's top edge beneath the status bar — appearance-coloured, so it tracked the system theme regardless of the in-app theme. Hide it via UIScrollView.topEdgeEffect/bottomEdgeEffect on the WebView's scroll view (iOS 26+), and make the WebView non-opaque so the themed web background shows under the overlaid status bar. Also: re-assert the status-bar overlay on resume, paint the document canvas with the theme background in the native shell, round the composer corners to 1.5rem, and enlarge the app-level bottom safe area so controls clear the rounded corners. * feat(mobile): logo splash until first paint is final (no FOUT / layout shift) Cold start flashed the fallback font and then reflowed once the real font and persisted appearance prefs landed, and text jumped a frame after mount because the mobile typography classes were applied from a hook effect. Fix it on three fronts: - apply device classes (device-mobile / mobile-pointer) synchronously in renderMobileApp before the first React paint, so mobile --text-* sizes are in effect from the start; - hold a logo splash (useFontsReady) until the UI web font has loaded; - gate that splash on appBootReady too, resolved once async appearance/typography preferences are applied, plus a double rAF so styles commit before reveal. All under a 2.5s safety timeout so a slow/offline CDN can't block startup. * feat(mobile): native local notifications; APNs implemented but frozen The native app now delivers agent ready/error/question/permission events as iOS (and Android) Local Notifications: a native notifications API backed by @capacitor/local-notifications replaces the Web Notifications API (which doesn't display in a WKWebView), driven by the notification SSE stream now subscribed in the mobile app. Tapping a notification opens its session. Also fix the settings toggle, which treated the Capacitor app as a browser and gated 'Enable Notifications' on the absent Web Notification permission, leaving it un-toggleable. Remote APNs push is implemented end-to-end (dependency-free HTTP/2 + ES256 JWT server runtime, token routes, client registration, iOS native config) but kept dormant: config-gated so it never fires, client registration not wired, and the aps-environment entitlement / background mode removed so the app builds with no Apple push setup. It will be reused once OpenChamber ships its own encrypted relay so users don't each configure APNs. See notifications/APNS.md. WKWebView can't use web push (unlike an installed PWA), so true background-when-suspended delivery on native requires APNs via that relay. * feat(mobile): APNs relay-mode background push Deliver native iOS background push through the central relay: the server posts device tokens + generic, model-based text to api.openchamber.dev/v1/push/send (default), which holds the single APNs key and signs+sends; dead tokens (410) are dropped from the per-session store. Direct APNs (HTTP/2 + ES256 JWT) stays as a fallback when OPENCHAMBER_PUSH_RELAY_DISABLED=true. The mobile push payload is generic only (model + scenario) so no session content crosses the relay. Re-enable the client token registration (useNativePushRegistration) and the aps-environment entitlement (alert pushes need no background mode). Wired into the same fanout as web push; focus-suppressed and only when tokens exist. * fix(mobile): APNs-only native notifications, generic templates, no foreground Make APNs the single notification channel for the native app and fix delivery: - Remove local notifications entirely (the @capacitor/local-notifications plugin and the SSE-driven path). A WKWebView can't tell foreground from background (document.hasFocus() is unreliable), so local notifications leaked while the app was open; the in-app dispatch is no-op'd on native. - Stop gating APNs on UI visibility — a backgrounded WebView can't report 'hidden' before iOS suspends it, which dropped background push. Instead always send and let iOS suppress the foreground banner (PushNotifications presentationOptions: []). - Fix a ReferenceError (out-of-scope 'variables') that crashed maybeSendPushForTrigger before any push was sent. - Mobile push text is generic: a scenario title ('Agent response is ready' / 'needs your input' / 'needs permission' / 'hit an error') + the session name, no model or message content. - Hide the focus toggle, templates, and test button in mobile notification settings. * feat(push): sign relay requests + bind tokens per server Each OpenChamber server now auto-generates an ECDSA P-256 keypair (persisted in settings, like the VAPID keys) and uses it to: - bind every newly-seen device token to the server on the relay (POST /v1/push/register-token, signed), and - sign every push send (publicKeyJwk + ts + signature over ts.sortedTokens.title). The relay derives serverId = SHA-256(publicKey), verifies the signature + timestamp, and only delivers to tokens bound to that server. Result: a leaked device token alone can no longer be used to push to a device — the sender also needs the server's private key. Stays zero-config (the keypair generates on first use). Drops the soft PUSH_RELAY_TOKEN bearer. * docs(push): describe relay data-confidentiality model Document that the push payload is not application-encrypted (TLS-in-transit only), what the relay and Apple can see (generic scenario title + session name, plus token/sessionId), that the signature is authentication rather than encryption, and what an end-to-end encrypted payload would require. * fix: invalid skill description * feat(push): app-icon badge for native notifications Send an absolute aps.badge with each native push = the count of distinct collapse-ids (tag) pushed since the app was last foregrounded, mirroring the lock-screen banner stack. Cleared server-side on user engagement (session view, message-sent, visibility beacon) and on-device via sceneDidBecomeActive. * feat(mobile): auto-connect last instance on launch + notification deep-links Cold launch silently reconnects to the most-recent saved instance (when reachable and a token is saved), holding the splash instead of flashing the connect screen; falls back to the connect screen when there's no saved instance, it's unreachable, or it needs a re-login. Notification-tap deep-links are now captured unconditionally (even before connect / on cold launch) and applied once the app is ready, so a tap opens the target session instead of being lost on the login screen. * fix(mobile): resolve theme background before first paint on cold launch The mobile shell entry (mobile.html) had no pre-paint theme step, so a cold launch flashed the WebView's default light canvas, then the baked design-system default (.dark { --background: #151313 }) via body.bg-background, before React's theme system injected the real theme vars. Add a blocking script that resolves dark/light from the persisted theme + system preference and sets --background (plus color-scheme and the element background) inline on the root, so the very first paint matches the resolved theme. Falls back to the default flexoki backgrounds when no theme has been persisted yet. * feat(mobile): openchamber:// deep-link foundation + arm64 simulator build Add a typed deep-link vocabulary (deepLinks.ts: parse/build + DeepLinkIntent) and a single native navigation layer (deepLinkNavigation.ts) that handles both the openchamber:// URL scheme (App.appUrlOpen — widgets, Live Activities, external links) and notification taps, normalising each into an intent. Session and new-session resolve against the store; shell surfaces (sessions/settings/ views/changes) register handlers. Cold-launch intents stash until the app is ready. Replaces the push-only useNativePushDeepLink and keeps backwards compatibility with bare sessionId payloads. Register the openchamber:// scheme in Info.plist. Dev tooling: with-mobile-env now honours xcode-select (-p) instead of hardcoding Xcode.app, so an Xcode beta is used. build:ios:simulator runs a new ios-sim-build script that temporarily drops the MLKit barcode-scanning pod (no arm64-simulator slice) so the app builds an arm64 binary installable on Apple Silicon simulators, then restores the Podfile + Pods for device builds. QR scanning already degrades cleanly when the native plugin is absent. * feat(mobile): iOS home/lock/Control Center widgets + push-driven refresh Add a Widget Extension (OpenChamberWidget) and a Notification Service Extension (OpenChamberNotificationService), wired into the Xcode project, sharing an App Group with the app. Widgets: - Overview (medium): recent sessions with read/unread dots + four quick actions (new, status, instances, settings). - Sessions (large): session list with per-session project label, attention count and a new-session button in the header. - Quick Actions (small): New chat pill + status/instances. - Lock Screen (accessoryCircular x2): brand logo to new session, attention counter. - Control Center control: brand logo (custom SF Symbol) to new session. Data: the app writes a session-overview snapshot (attention count + recent sessions with project labels) to the App Group on scene activate/resign; the NSE refreshes it from each push (aps.badge + sessionId) so widgets update even when the app is closed (needs aps mutable-content, added to the server + relay). Deep links: add openchamber://status (session status panel) and reuse view/instances; all widget taps route through the existing deep-link channel. * feat(mobile): large Sessions widget lists 6 sessions with project labels * feat(mobile): edge-swipe to switch sessions with directional slide+fade * fix(mobile): keep widgets in sync via reload-on-change + periodic refresh Widgets sharing the app's WidgetKit reload budget refreshed unevenly, leaving the large Sessions widget stale (no unread dot / attention count) while medium updated. Drop the per-call updatedAt from the snapshot, only write + reloadAllTimelines when the session overview actually changed (so we don't burn the budget on every scene activate/resign), and give each widget a periodic timeline refresh so a missed reload self-corrects. * feat(mobile): Android support — chrome fixes, SSE lock, icon, QR scan Cosmetics: - Status bar: on Android inset the WebView below the bar (overlay:false) and paint it with the resolved theme background + correct content Style, since Android doesn't feed env(safe-area-inset-top) to CSS. - Keyboard: skip the manual --oc-keyboard-inset on Android (the window resizes natively, so applying it double-counted and floated the composer); declare windowSoftInputMode=adjustResize and disable the shell height transition on Android so the header no longer bounces on keyboard open. Transport: lock Capacitor apps to SSE — native WebSocket streaming is unreliable on Android (events only arrive once a run finishes). Forced in sync-context and the other options are disabled in the Chat settings UI. Push: gate APNs registration to iOS only; on Android @capacitor/push-notifications register() needs Firebase/FCM (not configured) and crashes at launch. QR pairing: declare CAMERA permission + the ML Kit barcode_ui dependency, and install/await the Google barcode scanner module (with a post-install retry) before scanning so the first scan works without a manual retry. Icon: Android adaptive launcher icon generated from the cube logo (full-bleed white background, no edge artifact on One UI). Source assets under mobile/assets. Tooling: adb-based android-device.mjs + android:* scripts for device deploy. * feat(notifications): presence-aware push routing (don't spam the phone) Only push to a device when the notification would otherwise be missed there. A notification is suppressed on devices where the user is already present. - Tag every client's visibility beacon and web-push subscription with a platform ('ios' | 'android' | 'vscode' | 'desktop' | 'web') via getClientPlatform(). - Server tracks visibility per client (keyed by oc_ui_session) with the platform, and exposes isAnyInteractiveClientVisible() = any visible non-mobile client. - Native push (APNs) and mobile PWA web-push are now suppressed when an interactive (desktop/web/vscode) client is visible — it already shows the in-app notification. Gated on the desktop's visibility (reliable), never the phone's own (a backgrounded WKWebView can't report "hidden"). - Desktop/web web-push keeps the any-visible gate (a visible client absorbs it). - Skipping APNs also skips the badge increment so it doesn't drift. Fixes the case where every session on a shared instance pushed to the phone even while the user was actively working on desktop. * feat(mobile): Android FCM push notifications Enable native background push on Android via Firebase Cloud Messaging, in parallel with the existing iOS APNs path. - Add google-services.json + declare POST_NOTIFICATIONS (Android 13+). The Google Services Gradle plugin is applied when the file is present, so register() returns an FCM token instead of crashing. - Un-gate native push registration to iOS OR Android, and tag the registered token with its platform ('ios' | 'android') so the relay routes it to APNs vs FCM. - Server stores the platform per device token and binds it to the relay (platform included in the signed register message). - Notification small icon: monochrome cube silhouette with a mark on the top face, set as the FCM default_notification_icon so the status-bar icon reads as the logo. Relay-side FCM sending ships in openchamber-website. * docs(mobile): refresh HANDOFF with current state, dev/deploy process, and CI gap * chore(mobile): iOS store-review prerequisites (privacy manifest, encryption flag) - Add the app's PrivacyInfo.xcprivacy (no tracking; required-reason UserDefaults for the App Group snapshot shared with the widget + notification service extension) and wire it into the App target's resources — Apple requires an app-level privacy manifest. - Set ITSAppUsesNonExemptEncryption=false to skip the per-build export-compliance prompt. - HANDOFF: add a store-review-readiness checklist (in-repo vs release-time console/infra items). Verified: plist lint, xcodebuild parse, and an iOS simulator build with PrivacyInfo.xcprivacy bundled into App.app. * refactor(mobile): dedupe capacitor detection + make beacon guard explicit Addresses non-blocking PR review notes: - Consolidate the repeated Capacitor-native check (mobileConnections, deepLinkNavigation, usePushVisibilityBeacon each redefined it) onto the single isCapacitorApp() in lib/platform. - usePushVisibilityBeacon now guards on isWebRuntime() OR isCapacitorApp() instead of relying on isWebRuntime() being true for Capacitor, so the beacon can't silently stop if that changes. |
||
|
|
1505274f94 |
perf(stores): defer safeStorage writes off the interaction path (#1941)
* perf(stores): defer safeStorage writes off the interaction path Session switches funnel every persisted store slice through safeStorage.setItem, and doing those large JSON.stringify writes synchronously blocked the main thread for over a second. Add a write-behind buffer that: - Defers each setItem/removeItem to a later task via setTimeout(0) so the click-to-paint path is not blocked. - Coalesces repeated writes to the same key into a single backing flush. - Serves pending values from memory so read-after-write stays consistent within the deferral window. - Flushes synchronously on pagehide/beforeunload/visibilitychange/freeze so deferred state survives tab close, reload, and the mobile freeze lifecycle. Adds a test covering write deferral, coalescing, and pending read serving. * fix(stores): defer persisted JSON serialization * fix(stores): defer direct safeStorage writes --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
088a70fe5a |
fix(chat): stop Thinking stream from fighting chat scroll
Render a streaming Thinking block inline instead of inside a capped, independently-scrollable max-height box (the cap now applies only to finished thinking, for compact review). The nested scroll box was capturing the wheel and auto-pinning to its own bottom, so the chat could not be scrolled while thinking streamed. With it gone the chat's own auto-follow owns the scroll. Two auto-follow refinements make that solid: - Direction-aware bottom-zone re-engage: scrolling UP into the bottom spacer zone no longer re-arms follow (which the next growth would yank back). Follow resumes only when the user arrives at the bottom by scrolling down, is already following, or is at the true bottom. Kills the dead-zone fight near the bottom. - Animation guard: while a Thinking block COLLAPSE animation runs, transient geometry / trailing async scroll events are treated as our own and never trigger a false release. Genuine user gestures still release instantly. |
||
|
|
9e6d0df942 |
fix(chat): hold bottom on first session open while late async content lands
On the first open of a session, late async data (most visibly a task/subagent tool whose nested rows are fetched from the child session after entry) grew the timeline a beat after the one-shot entry pin, stranding the viewport mid-history. The steady-state idle gate intentionally ignores that growth, so re-pinning could not recover it. Add a short, gesture-cancellable entry-stick window that forces the bottom on every growth until content quiesces (or the user scrolls), covering both the ResizeObserver and the structural notifyContentChange path. |
||
|
|
b2598fa45e |
fix(chat): stop scroll twitch and message skipping with expanded tools
Gate passive auto-follow on active (working/settling) state so idle layout churn from virtualizer re-measurement no longer re-pins the viewport to the bottom. Render default-open tool bodies synchronously on mount so the virtualizer measures the real row height up front instead of growing a frame later and lurching scroll past several messages. |
||
|
|
84d7303346 |
feat(desktop): add macOS dock badge for chats with unseen activity
Show a count of chats (root sessions) with unseen activity on the macOS dock icon. The count is computed in the existing tray snapshot (full cross-project list, not the capped tray view; a subtask's unseen rolls up to its root only when subtask notifications are enabled) and pushed to the main process over the existing desktop_tray_update IPC, which calls app.setBadgeCount (0 clears it). The badge clears as sessions are marked seen on window focus. Add a Dock badge toggle in Appearance settings (default on, persisted, darwin desktop only), localized across all dictionaries, with a matching settings-search entry whose availability mirrors the render guard exactly. |
||
|
|
757c704956 |
fix(chat): rebuild auto-follow on a single instant writer to kill scroll twitch
Replace the RAF easing follow loop + settle burst with an always-on instant-follow model: while pinned, the content ResizeObserver re-pins to the bottom synchronously (scrollTop = scrollHeight, before paint) and is the only writer of scrollTop. A position+TTL auto marker distinguishes our own programmatic writes from genuine user scrolling, so a scroll event that lands at the just-written bottom never trips a false release. This removes the feedback loop where the easing animation, growing content, and the user's own scroll all fought for scrollTop in the same frame -- the infinite twitch when scrolling down during streaming, and the jiggle on send / from the queue. The public hook interface is unchanged; all consumers keep working untouched. |
||
|
|
3c3abd2afe |
fix(chat): keep historical session entry an instant snap to bottom, not a smooth scroll
Entering a historical session sometimes showed a smooth scroll from a mid
position instead of landing instantly at the bottom. The single-writer change
had startFollowLoop stop the settle burst, so a content-measurement
ResizeObserver tick during restore would downgrade the authoritative instant
pin into an easing follow loop that scrolled in from the partially-measured
position.
- startFollowLoop now yields to an active settle burst instead of stopping it.
The asymmetry is intentional: the settle burst is the authoritative instant
pin (session restore / goToBottom 'instant') and must not be preempted by the
easing loop. startSettleBurst still stops the follow loop, so the two never
write scrollTop in the same frame.
- tickFollow snaps deltas larger than a full viewport (discrete jumps: late
history measurement, session entry, a big block in one commit) instead of
easing them; only small streaming-sized deltas ease.
- restoreSnapshot mirrors goToBottom('instant') — instant write + settle burst,
no startFollowLoop.
|
||
|
|
b384deb812 |
fix(chat): stop scroll jiggle and double-scroll on send while pinned
Two scroll owners were writing the chat container's scrollTop concurrently
during pinned content growth and on send, fighting frame-to-frame and
producing the reported flicker/jiggle (after a pause, from the queue, on user
interruptions) plus a visible double scroll on a normal user send.
Enforce a single-writer invariant in useChatAutoFollow:
- The easing follow loop and the instant settle burst now mutually exclude:
starting one stops the other, so they can never write scrollTop in the same
frame. The isFollowingProgrammatically flag (which suppresses the overlay
scrollbar) is owned by whichever loop is active and cleared only when both
are idle, including the settle burst's natural 280ms end.
Stop the redundant re-pin storm in useChatTimelineController:
- While pinned, route goToBottom('instant') only for a prepend (history loaded
above), not on every bottom append / streaming part. Normal growth is owned
by the follow loop (kicked by the content ResizeObserver and chunk handlers).
Remove the double movement on send:
- Add scrollToBottomOnSend: when already following, just (re)kick the follow
loop for a single smooth movement instead of also firing an instant
goToBottom that raced the ResizeObserver-driven loop. When released (scrolled
up), keep the instant jump to the just-sent message.
|
||
|
|
3e3cd82a47 |
feat: inherit model-picker reorder/accordion persistence and shift-delete from otto-ui (#1833)
* feat(model-picker): drag-to-reorder providers and persist accordion state Ports two model/provider picker QoL features from otto-ui: - Persist collapsed state of picker sections (favorites, recent, each provider) via a new persisted zustand store so collapse survives remounts and reloads, shared across every picker surface. - Desktop drag-to-reorder of provider sections (whole header as the mouse activator, 8px threshold so a plain click still toggles collapse), with the order persisted in useUIStore.providerOrder and applied across ModelControls, ModelMultiSelect and ModelSelector. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * feat(sessions): shift-click quick action hard-deletes thread without prompt Ports the shift-quick-delete feature from otto-ui. The sidebar quick action (normally archive) becomes a no-prompt hard delete while Shift is held: the icon switches to a trash bin, the affordance turns destructive, and the click bypasses the confirmation dialog via a new skipConfirm source flag. A shared useShiftKeyHeld hook (single window listener set, useSyncExternalStore) keeps only the small action button re-rendering on Shift state changes, and resets on window blur so the affordance can't get stuck after alt-tab. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> |
||
|
|
1f549e4525 | feat: add automatic review loop (#1840) | ||
|
|
00821700de |
chore: remove dead code (59 unused files + ~125 unused exports) (#1835)
* chore: remove dead/unreferenced files across ui, vscode Remove 59 unused source files (components, hooks, lib utils, stores, barrels, and orphaned vscode github modules) that are not imported by any entry-reachable code. Also drop a stale test mock for the removed execCommands module. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * refactor: remove unused exported symbols (types, functions, consts, hooks) Remove exported symbols whose identifier is referenced nowhere in the repository (verified via repo-wide search), across ui types/contracts, lib utilities, sync layer, stores, and components. Also drop the few imports/private helpers orphaned by these removals. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * refactor: remove more unused exports (desktop, shortcuts, worktree, vscode) Continue removing repo-wide unreferenced exported functions, consts and types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and vscode gitService, with cascading orphaned helpers/imports cleaned up. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * chore: add dead-code cleanup tooling * refactor: checkpoint dead-code cleanup * refactor: remove dead-code suppressions --------- Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
9a2012c94e |
fix(chat-input): dismiss open question prompt when sending a message (#1740)
Sending a message while a question prompt was open left the prompt lingering, blocked the send, or collided with the still-blocked agent turn. Two root causes: useSessionActivity treated pending permissions as idle but not pending questions, so the send button became Stop during a question and Enter queued/collided instead of sending. handleSubmit also never dismissed the open question, stranding the session in a half-answered state. The send path now dismisses open questions for the session subtree (optimistic local clear so the card vanishes instantly, plus a formal question.reject) and queues the message. The queued-message auto-send hook then delivers it as the next turn once the rejected turn winds down and the session returns to idle. Queueing avoids aborting the turn, which surfaced an unwanted "running turn was stopped" notice. Regression tests cover the no-op, subtree dismissal (root + subagent child), and QuestionNotFoundError paths. |
||
|
|
f2d4a7833d |
fix(chat): keep session switch pinned to bottom without backward jump
Release auto-follow based on position (the user has left the near-bottom zone) instead of scroll-delta direction. The old `currentTop < previousTop` check treated the tiny scrollTop clamp the browser applies when the composer grows — which keeps you at the bottom — as a user scroll-up and released follow, so content finishing loading then drifted the view backward. Also always return to the bottom on session switch, dropping the saved-ratio restore: it had a low success rate and, by landing 'released' partway up, produced the same visible backward jump as content finished loading. overflow-anchor is already disabled on the chat scroll container, so no delta-threshold workaround is needed; this is a net simplification. |
||
|
|
1abb0dc103 |
fix(chat): set scroll position before paint on session-switch replay (#1730)
Replace useEffect with useLayoutEffect in the pendingInitialRestoreRef replay so restoreSnapshot runs synchronously after DOM commit, before the browser paints. Prevents visible flash of content at the wrong scroll position when the scroll container mounts after session hydration. Adapted from openchamber/openchamber#1553 (Fix 2). The virtualVersion counter (Fix 1) is not applicable: virtua (post #1651) does not use useVirtualizer's useState-based instance pattern that motivated it. Validation: - bun --cwd packages/ui type-check - no new errors in useChatAutoFollow.ts - bun --cwd packages/ui lint - passed Co-authored-by: herjarsa <herjarsa@users.noreply.github.com> |
||
|
|
0fef61e25f |
fix: sync context panel iframe themes
Keeps embedded sessions aligned with parent theme Prevents iframe reloads on theme changes Supports theme hotkeys from focused iframes |
||
|
|
9199798a14 |
perf(right-sidebar): gate live effects, memoize lookups, always-mount tabs (#1674)
* perf(right-sidebar): gate live effects, memoize lookups, always-mount tabs
Performance fixes for the right sidebar (git/files/context tabs).
== Correctness / leak fixes (P0)
* RightSidebar: drop dead useEffect that re-nulled refs the resize
handler already nulled; collapse the redundant width/minWidth/maxWidth
triple into width + the existing --oc-right-sidebar-width variable.
* useUIStore: clamp setRightSidebarWidth to [MIN, MAX]; simplify
setRightSidebarOpen (22 lines -> 12).
* RightSidebarTabs: useRightSidebarGitSync now takes the right tab and
main tab and only polls when the right git tab is the visible consumer
and the browser is online + visible. Replaces a global poll that
fired for the lifetime of the open sidebar.
* GitView: commit-files fetch refactored to cancelled + Promise.all
(was a per-hash loop that could setState after unmount); getRemoteUrl
and refreshRemotes gated on cancelled/mountedRef; new module-scoped
mountedRef guards setIsSettingIdentity from firing after unmount.
* GitView + useGitmojiList: extract gitmoji fetch/cache into a hook
with module-level inflight promise + subscribers Set; stale-while-
revalidate from localStorage; ensureLoaded() for call-site-initiated
hydration; cancelled flag on setIsLoading to avoid the React
setState-after-unmount race.
* ProjectNotesTodoPanel: 400 ms notes debounce now cancels on blur
(was double-saving); persistProjectData chained per project through
a module-level Map<projectId, Promise> so a fast todo toggle racing
the debounced save no longer hits the server in parallel; resize
auto-adjust guards against same-value pings.
== Render fanout (P1)
* RightSidebarTabs: all three tab content components are now always
mounted with the hidden attribute. State and cache survive tab
switches. When activeMainTab === 'git' (or 'context') the matching
right tab is filtered out of the tab strip and a redirect effect
snaps any persisted-but-now-hidden right tab to 'files'. onSelect is
now a type-guarded handler instead of `as RightTab`.
* GitView: 13 separate useGitStore action selectors collapsed into one
useShallow block (one re-evaluation per store change instead of 13).
* GitView: new isGitViewActive flag (true when this instance is the
visible consumer) gates the 7 live effects — load identities, fetch
remote URL, refresh remotes, ensureAll, sessionEvents.onGitRefreshHint,
worktree bootstrap poll, default-identity auto-apply. Hidden
GitView instances no longer run these.
* GitView: gitViewSnapshots module-level Map is now backed by an
LRU wrapper (cap 20) so per-directory draft snapshots cannot leak
across hundreds of project switches. Removed the dead `unique.set`
dedup in changeEntries — GitStatus.files is already unique by path.
* SidebarFilesTree: statusByPath Map<path, FileStatus> and
badgeByDir Map<dirPath, { modified, added }> are precomputed once
per gitStatus change. Tree render is O(1) per node instead of O(N)
per node via the previous per-row find/scan. badgeByDir walks each
file's path segments and increments counters for every ancestor
dir, so total cost is O(N + total_dirs_in_files) per gitStatus
change.
* SidebarFilesTree: FileRow wrapped in React.memo with a custom
comparator. Context-menu open state moved INTO FileRow as local
state — opening a menu in one row no longer re-renders siblings.
* SidebarFilesTree: loadDirectory accepts an isCancelled predicate;
the batch-load effect for expandedPaths passes a stable predicate
so per-dir fetches stop touching state once the effect tears down.
* SidebarFilesTree: module-level fileTreeCacheByRoot Map (LRU,
cap 8 roots) hydrates childrenByDir / loadErrorsByDir /
loadedDirsRef on mount or root change. Mirror effects write state
back to the cache. Survives close-and-reopen of the right sidebar;
populated entries are dropped on unmount only when they had no
data.
== Result
Net diff: 6 files modified, 1 new (useGitmojiList.ts), 682 insertions,
283 deletions. Existing test suite baseline preserved (537 pass / 58
fail / 1 error) — no new regressions. The 58 pre-existing failures are
in unrelated chat/streaming tests and were verified via git stash on
the same branch.
Architecture assumptions, verified by manual review:
- P1.1's redirect effect snaps rightSidebarTab to 'files' whenever
activeMainTab === 'git', so the right and main GitView instances
are mutually exclusive — isGitViewActive cannot be true for both.
- The 7 gated effects plus the useRightSidebar GitSync poll cover all
cases where git state should advance: visible consumer fetches; the
poll keeps the store warm when only the right git tab is visible.
- The aborted loadDirectory predicate is sufficient because
inFlightDirsRef and loadedDirsRef dedup at the call site before
any network IO is initiated.
* fix(sidebar): always clean up inFlightDirsRef regardless of cancellation
* refactor(sidebar): deduplicate RIGHT_SIDEBAR_MIN/MAX_WIDTH constants, export from useUIStore
* docs: split right sidebar perf plan into standalone file, clean up merged master status from chat plan
* fix(git): gate GitView effects by instance visibility
---------
Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
|
||
|
|
e372c8d8cb |
perf: instant startup via cache hydration + decoupled readiness (#1650)
* perf(startup): hydrate providers/agents from cache (stale-while-revalidate)
Persist last-known provider/agent snapshots instead of stripping them, so the
model/agent pickers paint instantly on cold start. Freshness is preserved by the
background refresh in initializeApp() and activateDirectory() (which overwrite on
success) and by the existing provider/agent config-change subscriptions, so the
prior stale-provider regression stays fixed without blanking the UI during fetch.
* perf(startup): cache directory session list for instant sidebar
Persist a capped slice of each directory's session list and seed the child store
from it on creation, so the sidebar paints chats immediately on cold start.
Bootstrap phase-3 loadSessions overwrites with the fresh list; its empty-list
race guard preserves the seeded sessions during OpenCode warmup.
* perf(startup): hold API requests through OpenCode warmup instead of 503
The readiness gate returned 503 the instant OpenCode wasn't ready, pushing the
client into an exponential-backoff retry loop (500ms -> 1s -> ...) that wasted
seconds of cold-start time and could fail bootstrap outright. Now hold the
request and poll readiness up to a bounded window so the first call succeeds as
soon as OpenCode is up (typically sub-second); still 503 fast past the window so
a genuinely-down server doesn't hang. Adds coverage for both paths.
* perf(startup): surface cached providers/agents in pickers (optimistic readiness)
The model/agent pickers gated purely on isInitialized, so they showed
"Loading…" for the entire init round-trip even when provider/agent data was
already hydrated from cache — making the persisted-cache work invisible. Treat
the pickers as ready as soon as cached providers are present (stale-while-
revalidate), so they paint last-known models/agents instantly and refresh in the
background. First-ever launch (no cache) still shows Loading until init.
* perf(startup): don't abort directory bootstrap on transient phase-1 failure
A failed initial path.get OR session.status aborted the whole directory
bootstrap, stranding it in loading and skipping phase 2/3 (session load).
session.status is live data the event pipeline keeps current, and path.get is
tolerable once a project is resolved from global state. Now only a total
failure (or path.get failing with no resolved project) aborts, so the sidebar
and chat keep advancing and loading sessions through warmup hiccups.
* perf(startup): don't bootstrap directories from archived sidebar rows
Each sidebar session row called useDirectoryStore(dir), which defaulted to
bootstrap:true and triggered a full directory bootstrap. Archived sessions point
at dozens of (often deleted) worktrees, so on startup this fired a session-list
fetch + 6x2s empty-retry storm per dead directory (the logs the user saw). The
store ref there is only read on-demand via getState() in export handlers, never
subscribed, so archived rows don't need it bootstrapped. Add a { bootstrap }
option to useDirectoryStore and skip bootstrap for archived rows; active rows
still bootstrap so live cross-directory session/status keeps aggregating.
* perf(startup): stop empty-session bootstrap retry storm on web/desktop
The post-bootstrap retry re-ran the full directory bootstrap 6x2s whenever the
session list came back empty, on the theory that empty meant OpenCode wasn't
ready. But loadSessions already retries transient failures twice over
(listGlobalSessionPages throws on 5xx and retries internally), so on web/desktop
an empty result is authoritative — the directory genuinely has no sessions (e.g.
deleted worktrees referenced only by archived sessions). That produced the
dozens of '[bootstrap] sessions empty ... 6 attempts; giving up' log storms.
Gate the retry to VS Code, where the bridge can return an empty 200 during
warmup that the inner retries can't catch.
* perf(startup): scope provider/agent config to project (worktrees inherit)
Providers/agents/defaults are project-level, but were keyed per directory, so a
worktree fetched and cached its own snapshot — duplicating the parent project's
load (the trace showed initializeApp loading the worktree and activateDirectory
loading the project concurrently, ~8s of redundant background work).
- resolveConfigDirectory() maps a worktree to its owning project; loadProviders
/loadAgents/activateDirectory now key by it, so a worktree reuses one shared
project snapshot. activateDirectory resolves up-front so activeDirectoryKey and
the snapshot key always match (picker stays consistent); the OpenCode working
directory is unaffected.
- Add a 30s runtime freshness guard so the stale-while-revalidate background
refresh skips re-fetching config that was just loaded (initializeApp then
activateDirectory for the same project), and to avoid churn on rapid project
switches. Config-change invalidation clears the snapshot, which bypasses the
guard, so freshness never masks a needed refresh.
* fix(sidebar): default archived sessions to hidden to avoid startup flash
useSessionDisplayStore defaulted showArchivedSessions to true, so on startup
archived sessions rendered by default and then vanished once the persisted
preference rehydrated to hidden — a visible flash. Default to hidden so the
pre-hydration state is the quiet one; users who opted into showing archived keep
their persisted true (default change doesn't override persisted state).
* perf(startup): persist worktree->project mapping to kill cold double-load
The worktree->project map (availableWorktreesByProject) is populated by async git
discovery, so it isn't ready when initializeApp runs — a worktree's first config
load couldn't resolve to its project and duplicated the project's provider/agent
load, saturating OpenCode during cold start (the source of the slow first
createSession/send the user observed). Cache resolved worktree->project mappings
to localStorage so resolveConfigDirectory resolves synchronously at init on
subsequent launches; the project is loaded once and activateDirectory hits the
freshness guard. worktree->project is immutable so a cached entry is safe; live
resolution still populates/corrects the cache.
* perf(startup): persist worktree map for instant sidebar + first-launch keying
Worktree discovery is async (git), so availableWorktreesByProject was empty at
startup: the sidebar worktree list appeared late, and useConfigStore couldn't
resolve a worktree to its project on the first launch (causing the cold
worktree+project double-load). Persist the discovered worktree map to
localStorage and seed it synchronously on store init (stale-while-revalidate:
discovery refreshes in the background via the existing setState, which now
write-through persists). The sidebar paints worktrees instantly and
resolveConfigDirectory resolves the project from the very first launch.
* perf(startup): coalesce concurrent duplicate OpenCode reads in runtimeFetch
On cold start the sync bootstrap and the config store independently fire the same
idempotent reads (providers, config, path, agents, project) concurrently with no
shared dedup, saturating the single OpenCode process and delaying work queued
behind it (e.g. createSession). Coalesce genuinely-concurrent identical GETs to
those read endpoints at the transport layer so OpenCode does the work once; each
caller receives an independent response clone. Tightly scoped: GET only,
allowlisted read paths, never event streams, never a signal-bearing request (so
one caller's abort can't cancel the shared fetch). Entries clear on settle, so it
only shares overlapping in-flight requests — never a stale response.
* perf(startup): cache git branches so the draft branch selector paints instantly
The branch selector above the composer was the slowest-loading element: it's
gated behind a cold 'git branch' fetch (useGitStore, not persisted). Cache the
per-directory branch list to localStorage and seed the store on init (with
isGitRepo:true so the selector's gate passes), and write the cache on every
successful fetchBranches. The ChatInput draft-branch effect now refreshes on
staleness (>30s) rather than mere absence, so seeded branches show immediately
and still refresh in the background without a spinner — no stale-forever
regression. Only the branch list is cached; status/log/diff are untouched.
|
||
|
|
928b7ff1d6 | revert: restore stable chat history scrolling | ||
|
|
db3558f906 |
fix: prevent blank chat viewport when switching sessions (#1553)
When switching to a session with long context (especially in Electron desktop when changing servers), the chat viewport could render blank until the user scrolled. Two interacting issues caused this: 1. historyVirtualRows memo never recomputed after the first render because historyVirtualizer (from useVirtualizer's useState) is a stable reference. Frozen range meant items rendered at the top while paddingBottom filled the visible viewport after scrolling. 2. pendingInitialRestoreRef replay ran in useEffect (after paint), showing a frame at scrollTop:0 with the stale virtualizer range. Fixed by: adding a virtualVersion counter driven by useVirtualizer's onChange to bust the memo; switching the replay to useLayoutEffect so scroll position is set before the browser paints. |
||
|
|
106b31a407 | Harden remote API security boundaries |