0841ccdd570ef64039e32dde9acea87f7b50c8f4
33
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
63b4a5b996 |
feat(ui): enable TimelineDialog with full-text search across all message roles in one session (#1104)
* feat: register open_timeline_dialog shortcut (mod+t) * feat: enhance TimelineDialog with full-text search across all roles * feat: add open_timeline_dialog shortcut labels to all locales * feat: wire TimelineDialog into ChatContainer * fix: add setTimelineDialogOpen to hook dependency array * fix: tighten timeline dialog interactions --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
03c9065c90 |
fix: preserve per-session scroll position on session switch (#1083)
* fix: restore scroll position when switching chat sessions When switching between chat sessions, scroll position now restores to where the user left off instead of always jumping to the bottom. - Save pixel-level scrollPosition (scrollTop/scrollHeight/clientHeight) in viewport store on every scroll event - Add restoreSavedScrollPosition to timeline controller for ratio-based restoration (handles content size changes between visits) - Suppress intermediate scroll events during session transition with an explicit flag, cleared deterministically after restore completes - Cancel in-flight animations/follow-loops on session switch - Preserve scrollPosition when session-ui-store rebuilds SessionMemoryState * fix: keep streaming sessions pinned on restore --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
9424cff02c |
fix: reconnect SSE immediately on OS wake-from-sleep (#1066)
* fix: reconnect SSE immediately on OS wake-from-sleep When the desktop app resumes from OS sleep, TCP connections are dead but timers were paused during sleep so the heartbeat watchdog doesn't fire until ~30s after wake. Add Electron powerMonitor.resume → renderer notification → event-pipeline immediate abort, cutting reconnection delay from ~30s to ~0ms. Changes: - electron/main.mjs: import powerMonitor, emit openchamber:system-resume to all renderer windows on OS resume - ui/sync/event-pipeline.ts: listen for openchamber:system-resume, set attemptAbortReason and abort the active SSE/WS attempt to trigger immediate reconnection with retryDelayMs=0 and lastEventId preservation * fix: reconnect SSE immediately on OS wake-from-sleep When the desktop app resumes from OS sleep, TCP connections are dead but timers were paused during sleep so the heartbeat watchdog doesn't fire until ~30s after wake. Add Electron powerMonitor.resume → renderer notification → event-pipeline immediate abort, cutting reconnection delay from ~30s to ~0ms. Changes: - electron/main.mjs: import powerMonitor, emit openchamber:system-resume to all renderer windows on OS resume - ui/sync/event-pipeline.ts: listen for openchamber:system-resume via globalThis.window, set attemptAbortReason and abort the active SSE/WS attempt to trigger immediate reconnection with retryDelayMs=0 and lastEventId preservation - Test: event-pipeline-resume.test.js verifies abort → reconnect flow |
||
|
|
5c7f5aa4f7 |
fix(sync): resync the active session after reconnect transitions (#1011)
* fix(sync): resync the active session after reconnect transitions Include the viewed session in reconnect recovery and trigger a targeted resync on transport switches so the active chat catches up after missed live events. Constraint: Keep the fix in the sync layer instead of adding ChatContainer-only recovery Rejected: Widen reconnect heuristics for every cached session | broader recovery scope than needed Confidence: high Scope-risk: narrow * Avoid no-op reconnect resync store writes --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
17dd526731 |
fix: eliminate parent-child session desync across reconnect and navigation (#985)
* fix(pipeline): distinguish transport switch from real disconnect WS_FALLBACK errors (e.g. ready timeout → SSE fallback) are transport switches, not disconnections. No events are lost because lastEventId is preserved across the switch. Previously, every WS timeout triggered onDisconnect → onReconnect with a full resyncDirectoryAfterReconnect, which: - Missed idle parent sessions in candidate selection (root cause 1) - Could overwrite in-flight SSE state with stale fetch data (root cause 4) - Caused isConnected to flash false→true Now: WS_FALLBACK fires onTransportSwitch (sets isConnected only). Real disconnections (heartbeat timeout, network error) still fire the full onDisconnect → onReconnect → resync cycle. * fix(sync): relationship-aware reconnect with merge-not-replace Two changes to resyncDirectoryAfterReconnect: 1. Candidate selection now also includes parent sessions of any child sessions in the directory. Previously, if a child completed during the disconnect gap (busy→idle), neither child nor parent was selected because both appeared idle. The parent's task tool part would remain permanently stale. 2. Parent resync merges parts instead of replacing. Previously, the resync deleted parts for messages not in the fetch snapshot, which could erase parts delivered by SSE events that arrived between the fetch and the setState. Now only parts for messages in the snapshot are overwritten; everything else is preserved. * fix(sync): demand-load child session messages on access Bootstrap only loads session metadata — messages are populated exclusively by SSE events. When a user navigates to an old session that spawned subagents, child session messages were never in the store. Add useEnsureSessionMessages hook that detects this gap (session exists in state.session but state.message[sessionID] is absent) and triggers a background API fetch to load messages and parts. ToolPart already calls useSessionMessageRecords(taskSessionId) which returns empty when not loaded. Now it also calls useEnsureSessionMessages to populate the store on first access. * fix(sync): unmount-safe parent resync when child session goes idle When a child session transitions to idle (completes), the sync layer now schedules a targeted parts repair for the parent session's task tool part. Previously this only happened when the ToolPart component was mounted and had observed the child being active (taskChildSeenActive). This covers: - User navigated away while child was running - App restarted with active subagent sessions - SSE reconnect where child completed during disconnect Uses the existing repairSessionParts mechanism with its 5s cooldown to avoid redundant fetches. * fix: type-check fixes for sync-layer parent resync Fix TypeScript errors in Fix 5 implementation: - Convert currentSessionId from null to undefined for resolveFallbackTaskSessionId - Add default empty string for dir parameter in getScopedSdkClient - Use explicit sessionID parameter for scopedClient.session.messages All type-checks now pass. * fix(sync): address PR review feedback on deduplication - Use enqueuePartsRepair for session.idle parent resync instead of direct repairSessionParts call. enqueuePartsRepair already has a 5s cooldown to prevent redundant parallel API calls when multiple child sessions go idle concurrently. - Move useEnsureSessionMessages loading guard from component-scoped React.useRef to a module-level Set keyed by directory:sessionID. Prevents parallel fetches when multiple ToolPart instances mount for the same child session. * fix(sync): add missing semicolon on useEnsureSessionMessages call Address Greptile P2 review comment on ToolPart.tsx:1931. |
||
|
|
81591430a1 |
fix(chat): restore desktop editor file-open in PendingChangesBar (#981)
* fix(chat): restore desktop editor file-open in PendingChangesBar (#979) Commit d1553ba removed the runtime?.editor branch from handleOpenFile, breaking file-click in VS Code and Electron desktop runtimes. Restore the 3-branch logic: editor.openDiff(patch) → editor.openFile() → openContextDiff() web fallback. Display paths remain relative. * fix(chat): route non-git changed files to file view in desktop DiffView requires a git repository to display diffs. In desktop (Electron/Tauri), runtime.editor is unavailable, so non-git files fell through to openContextDiff which shows "Not a git repository". Route non-git files to openContextFile instead, which works without git. Git-tracked files continue to use openContextDiff. * feat(chat): per-turn changes dropdown for non-git; self-sufficient git sync in bar - PendingChangesBar seeds git store + listens to onGitRefreshHint; works without RightSidebarTabs (fixes VS Code where bar never rendered). - Bar is git-only now; non-git shows per-turn dropdown at end of completed assistant turns. - Dropdown uses base-ui Popover for collision-aware position; icon-only collapse with tooltips at narrow container widths. - Extract shared helpers (changedFiles.ts), popover list (ChangedFilesList.tsx), styles (changedFilesPopover.ts). --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
247dc0481e | fix: allow git checkout with uncommitted files (#945) | ||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
6d5afe55db |
fix: harden SSE compression exclusion and add Caddy reverse proxy docs (#939)
The compression middleware filter runs before route handlers, so the
res.getHeader('Content-Type') check in shouldSkipCompression is always
undefined at decision time. SSE exclusion relied entirely on the Accept
header, which non-standard clients (curl, fetch) may omit.
Add deterministic path-based exclusion for all known SSE routes so
compression is skipped regardless of client behavior. Also add a Caddy
reverse proxy example and a CDN double-compression warning to docs.
|
||
|
|
304b14b4b1 |
feat: add response compression middleware to reduce bandwidth (#928) (#935)
* feat: add response compression middleware for HTTP responses Add compression middleware to Express server with SSE route exclusion and 1KB threshold. Reduces bandwidth for non-streaming API responses (history, sessions, files, static assets) by 60-80%. Closes #928 * fix: harden proxy compression and proxy docs --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
79a9f93ae2 | feat(files): auto-refresh file tree on external changes (#919) | ||
|
|
fccf4bad32 |
feat: session worktree isolation (#913)
* feat: add session-worktree contract types and canonicalizeWorktreeState API - Add SessionWorktreeAttachment type and worktree metadata fields (worktreeRoot, worktreeStatus, headState, worktreeSource) to session/worktree types - Add GitAPI.validateWorktreeDirectory() and canonicalizeWorktreeState() methods with full HTTP delegation chain (gitApiHttp → routes.js → service.js) - Add canonicalizeWorktreeState() implementation that resolves worktreeRoot, headState (branch/detached/unborn), attentionReason (merge/rebase/etc), and worktreeStatus (ready/missing/invalid/not-a-repo) for a given directory - Add validateWorktreeDirectory() to check whether a cwd is inside a worktreeRoot - Add session-worktree-contract.ts: pure functions for resolving session worktree state, formatting badges, and building repair actions - Add session-worktree-store.ts: authoritative Zustand store for session-to-worktree attachments, replacing session-ui-store as the source of truth for worktree binding - Add unit tests for contract functions and store operations * feat: canonicalize worktree metadata producers - worktreeManager.listProjectWorktrees: derive headState (branch/detached/unborn) from worktree list entry instead of relying on external state, and populate all Phase 1 canonical fields (worktreeRoot, worktreeStatus, worktreeSource) for each discovered worktree entry - worktreeManager.createWorktree: include all Phase 1 canonical fields (worktreeRoot, worktreeStatus, headState, worktreeSource) in returned metadata - useDetectedWorktreeRoot: populate fallback canonical fields so that sessions without store-based metadata still have worktreeRoot/worktreeStatus/ headState/worktreeSource when resolved through the fallback path * feat: route sessions through authoritative worktree attachments - session-ui-store: import session-worktree-store as the authoritative source for session↔worktree attachment state - setWorktreeMetadata: mirror all writes to session-worktree-store so that session-worktree-store.attachments is always the authoritative record; local worktreeMetadata map is kept for backward-compatible reads - Add session-ui-store.test.js with unit tests covering: valid cwd routing, degraded fallback, created-for-session attachments, legacy upgrade recovery, missing/not-a-repo status handling * feat: clarify session worktree targets - session-worktree-contract: extend buildSessionTargetOptions to accept pendingBootstrapDirectory and mark pending worktrees with pending=true; extend SessionTargetOption to include optional pending flag - ChatInput: replace manual worktree branch options construction with buildSessionTargetOptions; add ⏳ prefix for pending bootstrap worktrees - Add test for pending bootstrap worktree distinction * feat: show worktree-backed session state - Header: read worktree attachment from authoritative session-worktree-store and render needs-attention/degraded/missing badge with alert icon next to current session info when session has degraded/missing/invalid state - GitView: show 'Worktree features are unavailable' message when session has missing worktree status and open-without-worktree-features repair action * feat: enforce safe mutations for attached worktrees - session-worktree-contract: add getMutationBlockingReasons helper that returns blocking reasons (missing/invalid/attention state) for high-risk mutations - GitView: gate handleCheckoutBranch, handleCreateBranch, and handleRenameBranch with getMutationBlockingReasons; block with explicit toast message when worktree is missing, invalid, or has an in-progress git operation - session-worktree-contract.test: add 7 tests covering mutation blocking for missing/invalid/attention states (merge/rebase/cherry-pick) * feat: implement session worktree isolation This adds a shared session↔worktree contract that makes session switching worktree-backed. Sessions attached to different worktrees keep stable branch context without shared-directory auto-checkout. Commits: - feat: add session-worktree contract types and canonicalizeWorktreeState API - feat: canonicalize worktree metadata producers - feat: route sessions through authoritative worktree attachments - feat: clarify session worktree targets - feat: show worktree-backed session state - feat: enforce safe mutations for attached worktrees * feat: make authoritative attachment first-priority source for session directory resolution Phase A: resolveSessionDirectory, getDirectoryForSession, hooks read authoritative attachment before falling back to worktreeMetadata. Phase B: createSession canonicalizes and writes attachment on creation; setCurrentSession recovers legacy/missing attachments via async canonicalization. * feat: make authoritative attachment the primary branch source in Header/GitView Phase C: Header branch label and GitView project root now read from authoritative SessionWorktreeAttachment first, falling back to live git and legacy sources only when attachment is absent, degraded, or legacy. Adds getAttachmentBranchLabel() helper with 7 tests. * feat: add runtime parity for validateWorktreeDirectory and canonicalizeWorktreeState Phase D: Web runtime API, VS Code bridge, and VS Code gitService now expose validateWorktreeDirectory and canonicalizeWorktreeState, matching the server-side implementations. All three runtimes (web, desktop, VS Code) can now delegate worktree canonicalization without HTTP fallback. * feat: add dirty-tree blocking to mutation safety gates getMutationBlockingReasons now accepts an optional gitStatus param and blocks branch mutations when the tree has uncommitted changes. GitView passes live status to all three blocking call sites. 5 new tests covering dirty, clean, null, combined, and no-file-count cases. * refactor: revert branch label to live-git-first, remove getAttachmentBranchLabel Live git is the correct source for branch labels in all scenarios: dedicated worktree sessions have identical live/attachment branches, and shared-directory sessions must show the real current branch. Attachment remains authoritative for worktreeRoot, cwd, degraded/ missing/repair status, and mutation blocking. * chore: remove session worktree isolation plan doc * refactor: simplify session worktree isolation implementation --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
ea5c19e934 |
fix(sync): deduplicate overlapping delta after coalesced part.updated (#916)
* fix: hide archived section and empty folders when no sessions remain - Only push archived group in useSessionGrouping when there are archived sessions, preventing an empty archived section from rendering - Hide empty folders in archived bucket via shouldKeepFolder check in SessionGroupSection (folders with no sessions and no content in children are filtered out) - Always filter folders through shouldKeepFolder, not just during search * perf: memoize archived folder filtering * fix(sync): deduplicate overlapping delta after coalesced part.updated When message.part.updated coalesces in the event pipeline and a message.part.delta for the same part arrives in the same flush window, the reducer appends the delta verbatim to the already-complete field value, producing duplicated text in tool output and assistant messages. Add targeted overlap reconciliation: only when a part.updated replaces an existing part with overlapping string content, mark the next delta for that field as dedupe-eligible. Normal streaming deltas remain untouched (pure append). Covers four cases: - Full overlap: delta already present -> no-op - Partial overlap: only non-overlapping suffix appended - No overlap: unchanged append behavior - Legitimate repeated output (ha + ha -> haha): preserved --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
9b169aaacf |
feat: deliver polished desktop first-launch experience with smart recovery (#850)
* feat: implement desktop boot outcome architecture
- Add structured DesktopBootOutcome with target/status fields
- Implement boot outcome computation and validation
- Add desktop hosts configuration management (Tauri + TypeScript)
- Add desktop hosts probing with timeout and retry logic
- Support local/remote host classification and health checks
This provides the foundational infrastructure for desktop onboarding
flow to determine whether to show local setup, remote connection,
or recovery screens based on OpenCode availability and remote host
reachability.
* feat: add desktop onboarding UI components
Add comprehensive onboarding flow for desktop app:
- ChooserScreen: First-launch local/remote selection
- LocalSetupScreen: CLI installation guidance and manual detection
- RecoveryScreen: Recovery mode with routing to local/remote
- RemoteConnectionForm: Remote host connection with validation
- DesktopConnectionRecovery: Recovery variants and routing logic
- ConnectionSettingsPage: Manage remote connections
Components handle:
- Local vs remote choice persistence
- Recovery scenarios (unreachable, wrong-service, missing)
- Manual CLI detection (replaced auto-polling)
- Back navigation and state preservation
* feat: integrate desktop onboarding with app shell
- Update App.tsx to handle onboarding routing and recovery
- Add onboarding mode switching (first-launch/local-setup/recovery)
- Integrate desktop hosts in SettingsView
- Update DesktopHostSwitcher with recovery routing
- Add desktop shell utilities for onboarding detection
- Update web manifest for desktop app metadata
Completes the desktop onboarding feature integration,
allowing users to choose local or remote OpenCode on
first launch and recover from connection failures.
* fix: hide back button in remote connection form for first-launch chooser
In first-launch chooser mode, the back button is redundant since users
can simply click the "Local Install" tab. The back button is still shown
in recovery mode where there's no tab interface.
Changes:
- Add showBackButton prop to RemoteConnectionForm (default: true)
- Set showBackButton={false} in ChooserScreen remote tab
- Keep showBackButton={true} in RecoveryScreen for navigation
* refactor: remove Connection Settings page and simplify recovery UI
Remove the Connection Settings page as it was redundant:
- Local server is single-instance (no need to "choose")
- Remote servers are one-time setup (first-launch chooser)
- SSH Instances remain for multi-instance management
Changes:
- Remove ConnectionSettingsPage component and directory
- Remove 'connection' from Settings metadata
- Remove "Open Settings" button from recovery screens
- Remove desktopBootBypassToSettings state and logic
- Update recovery config to use 'local' icon instead of 'settings'
- Update tests to reflect removed showOpenSettings field
This simplifies the UX by focusing on:
- First-launch chooser for initial local/remote decision
- Remote Instances (SSH) for managing multiple remote machines
- No persistent "server management" needed for typical desktop usage
* fix: remove unused enableCliPolling prop and clean up TypeScript errors
Remove the obsolete enableCliPolling prop that was used for auto-
polling CLI detection. We replaced this with manual "Check and Continue"
button in a previous commit, so this prop is no longer needed.
Changes:
- Remove enableCliPolling from OnboardingScreen props and usage
- Remove enableCliPolling from App.tsx calls
- Remove unused 'connection' case from getSettingsNavIcon()
- Remove unused RiGlobalLine import
This resolves all TypeScript compilation errors reported by Copilot.
* fix: remove unused onChooseLocal prop and CLI_MISSING_ERROR_REGEX
These were left over from the refactoring:
- onChooseLocal in RecoveryScreen was defined but never used
- CLI_MISSING_ERROR_REGEX in App.tsx was leftover from removed enableCliPolling code
* fix: remove unused variables and fix React Hook dependency warnings
Remove unused memoized components and variables that were causing
lint errors in packages/ui:
- MainLayout.tsx: Remove unused MemoHeader, MemoChatView, MemoPlanView,
MemoGitView, MemoDiffView, MemoTerminalView, MemoFilesView,
MemoRightSidebarTabs, DesktopLeftSidebar, and DesktopRightPanel
- useGitHubPrStatusStore.ts: Remove unused prVisualPriority function
- useChatScrollManager.ts: Add missing markProgrammaticScroll dependency
to React.useEffect hook
These fixes resolve the CI lint failures in PR 850.
* chore: remove local claude settings from repo
* refactor(desktop): drop vibrancy code from onboarding PR
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
|
||
|
|
bb1d522838 |
fix(task): prevent subagent silent failures in session resolution and polling lifecycle (#903)
* fix(task): prevent subagent silent failures in session resolution and polling lifecycle
Two failure points fixed:
1. Fallback session resolution window too narrow (3s):
- resolveFallbackTaskSessionId now accepts hasRetried boolean
- First attempt uses 3s window (avoids binding wrong sessions)
- Subsequent attempts widen to 8s (handles late-appearing child sessions)
- Uses useState + useEffect instead of side effects in Zustand selector
2. Polling stops before child results are captured:
- When child session goes idle before parent sees it active, polling
would stop without fetching results
- Added final-fetch-before-stop: a one-shot delayed fetch that runs
after the settle grace period, ensuring child results are captured
- Uses taskFinalFetchDoneRef to guarantee exactly one final fetch
- Preserves existing happy path (active child → normal settle timer)
* fix(task): serialize final fetch after polling stops
Move the subagent final-fetch into a dedicated effect that runs only after
polling has stopped, avoiding races between polling writes and final-fetch
writes to the child sync store.
Also retry safely on final-fetch failure by reopening polling instead of
marking the final fetch as done before the request succeeds.
* feat(task): distinguish child session errors from normal idle
When a subagent terminates with an error, abort, timeout, or failure,
the parent session could not tell it apart from a normal completion.
Changes:
- event-reducer.ts: session.error now stores { type: 'error' } instead of
{ type: 'idle' }, so consumers can distinguish failed from completed sessions
- useSessionActivity.ts: add 'error' phase to SessionActivityPhase and
isError flag to SessionActivityResult; error phase is non-active (like idle)
but distinguishable via isError
- ToolPart.tsx: pass childSessionError to TaskToolSummary and show
'Subagent session ended with an error.' instead of the generic
'No subagent session id on task metadata.' when the child errored
Other consumers of session_status that only check 'busy'/'idle' are
unaffected — 'error' falls through to existing idle-like behavior.
* Revert "feat(task): distinguish child session errors from normal idle"
This reverts commit b3cc749bde16ed8fc55e4f304dcc455484335fba.
* fix(task): delay fallback retry window widening
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
|
||
|
|
1656c3bb93 |
perf(sync): optimize multi-session event pipeline with per-directory queues and delta coalescing (#908)
* fix: hide archived section and empty folders when no sessions remain - Only push archived group in useSessionGrouping when there are archived sessions, preventing an empty archived section from rendering - Hide empty folders in archived bucket via shouldKeepFolder check in SessionGroupSection (folders with no sessions and no content in children are filtered out) - Always filter folders through shouldKeepFolder, not just during search * perf: memoize archived folder filtering * perf(sync): per-directory event queues to eliminate cross-session HoL blocking The SSE event pipeline previously used a single global queue and a single flush timer shared across all directories. Under concurrent multi-session workloads, a busy directory's delta storm would block other directories' status and state events from reaching the UI until the next flush tick, producing the "multi-session latency" symptom users report. Split the queue into one DirectoryQueue per directory, each with its own coalesce map, stale-delta set, and flush timer. Directories flush independently so a busy directory can no longer starve a quiet one. Coalesce keys are now scoped to a single directory's queue, so the directory prefix is removed from the key strings. Cross-directory behavior only; same-directory multi-session behavior is unchanged (React 18 auto-batching still collapses a single directory's flush into one render). * perf(sync): coalesce consecutive message.part.delta events per flush window Within a 16ms flush window, consecutive delta events for the same (messageID, partID, field) tuple are string-concatenated into a single accumulated delta rather than being queued individually. This directly addresses same-project multi-session workloads — most notably parent sessions with subagent tasks (child sessions share the same directory queue). Both parties stream deltas concurrently, which previously multiplied raw event count proportionally to the number of active sessions. Coalescing can reduce queue depth by 10-100x during active streaming. Safety: verified against event-reducer.ts — the delta handler is a pure string append (existingValue + props.delta) with no per-event side effects (no time.updated, no notifications, no diff calculations). The merged result is semantically identical to applying each delta separately. The staleDeltas skip mechanism is unaffected: accumulated delta payloads retain their type and identifiers, so message.part.updated supersession still works correctly. * test(sync): cover per-directory queues and delta coalescing Extend event-pipeline.test.js with behavioural coverage for both optimizations landed in 98d013a and 258acf0: P1 (per-directory queues) - Delivers events from two directories without loss - Keeps distinct sessionIDs in the same directory as independent coalesce slots (session.status is not overwritten across sessions) - Collapses repeated session.status for the same session down to latest Option C (delta coalescing) - Accumulates consecutive deltas for the same (messageID, partID, field) into a single dispatched event with concatenated content - Does not merge deltas across different fields on the same part - Does not merge deltas across different parts on the same message - Does not merge deltas across different directories (per-dir queues) - Skips accumulated deltas when message.part.updated is coalesced onto an earlier update, proving staleDeltas still works with C - Leaves non-delta coalescing (session.status replace semantics) intact All 13 tests pass under bun:test. Also adds event-pipeline.bench.js, a runnable synthetic benchmark that reports delta reduction and byte integrity across 8 workload scenarios from "single session, 500 tokens" up to "10 projects × 5 sessions × 1000 tokens". Run with: bun packages/ui/src/sync/__tests__/event-pipeline.bench.js Current numbers on this machine: 99.5% - 99.9% delta event reduction with full byte-level integrity (concatenated delta bytes always equal the input total). * fix(sync): remove staleDeltas — it silently drops delta events --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
5584537da5 |
fix: question tool content disappears after refresh (#879) (#909)
* fix: show question content in ToolPart instead of 'Awaiting response...' after refresh Previously, when the question tool was pending/running or completed without parseable output, the ToolPart fell through to a generic 'Awaiting response...' message. After a page refresh or app restart, this made questions appear empty even though the tool state still contained the question input data. Now the ToolPart reads question text, headers, and options from the tool state's input field, ensuring question content persists across refreshes regardless of QuestionCard store availability. Fixes #879 * fix: restore QuestionCard after refresh and pause working status during active questions Two fixes for question tool UX: 1. ChatContainer: sessionIsWorking now returns false when there are active questions (same as it already did for permissions). This prevents the status row from showing 'Asking question...' and instead shows the QuestionCard. 2. sync-context: resyncDirectoryAfterReconnect now re-fetches pending questions via listPendingQuestions(). Previously only sessions and messages were re-fetched on SSE reconnect, so questions asked during disconnection were lost, causing QuestionCard to disappear after page refresh. Refs #879 * fix: hide assistant working status while questions are pending The assistant status hook only special-cased pending permissions, so question tools still surfaced 'Asking question...' after refresh even when the UI was already waiting on a QuestionCard response. Treat pending questions like other blocking requests by clearing the working indicator until the user answers. Refs #879 * fix: merge question/permission stores instead of full replace on bootstrap and reconnect The root cause of QuestionCard disappearing after refresh was a race condition between SSE events and HTTP bootstrap. Bootstrap and reconnect both did full replacement of state.question, wiping SSE-delivered data that arrived between the HTTP call initiation and response arrival. Changes: - bootstrap.ts: question and permission stores now use merge semantics. Only sessions present in the API response are overwritten. Sessions absent from the response are left untouched (they may hold SSE data). - sync-context.tsx: reconnect question resync uses the same merge pattern. No longer clears question entries for sessions not in the API response. - bootstrap.ts: sdk.question.list() now passes directory parameter to scope the query correctly. This ensures SSE-delivered question data survives the bootstrap window, while still allowing the API response to be authoritative for sessions it covers. Refs #879 * fix: prune stale pending requests after reconnect --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
291d00de8b |
fix: remove shell-specific \&\& operators for non-POSIX shell compat (#870) (#888)
Replace shell `\&\&` and `||` operators in execCommand calls with TypeScript control flow. This fixes re-integrate commits failing when the user's default shell is Nushell, which does not support `\&\&`. Changes: - createTempWorktree: split `mkdir \&\& mktemp` into two calls - ensureLocalBranch: use isOk() instead of `echo ok || echo missing` - isCherryPickInProgress: use isOk() instead of `echo yes || echo no` Closes #870 |
||
|
|
6425161bef |
fix(sidebar): auto-expand parent node when navigating to subagent session (#893)
When a user navigates to a subagent (child) session, the parent session node in the left sidebar was not automatically expanded, making the child session invisible in the tree. This change adds a useEffect that detects when the current session has a parentID and automatically adds that parentID to expandedParents, ensuring the subagent session is visible in the sidebar hierarchy. |
||
|
|
964c209ff6 |
fix(sync): remove stale-delta skip and add parts-gap recovery (#889)
The pipeline's stale-delta mechanism incorrectly marked all message.part.delta events as stale when a message.part.updated coalesced, regardless of queue position. This caused valid streaming deltas to be silently dropped, resulting in blank or incomplete assistant messages. Additionally, when part events were dropped by the reducer (missing parts array or partID not found), there was no recovery path — the state stayed permanently out of sync until the next SSE reconnect or manual refresh. Also discovered: message.updated that successfully writes an assistant message but has empty parts would render a blank bubble, with no repair triggered since repair only ran on reducer return false. Changes: - Remove staleDeltas Set and deltaKey from event-pipeline.ts - Coalesce still replaces same-key events, but deltas are never skipped - Add enqueuePartsRepair + repairSessionParts to sync-context.tsx (5s cooldown, deduped, async SDK re-fetch) - Trigger repair on reducer return false for part events - Trigger repair on message.updated return true with empty parts - Add sync debug.ts with gated diagnostic logging - Add pipeline coalescing tests |
||
|
|
700138c9b5 |
fix(git): restore changes panel visibility and sidebar sync (#886)
* fix: restore git changes panel visibility and sidebar sync
Two independent fixes:
1. ChangesSection virtualizer returned empty rows due to useMemo caching
getVirtualItems() with a stale stable reference. First render produced
an empty array, and useMemo never recomputed because rowVirtualizer
reference never changed. Removed the useMemo to call getVirtualItems()
directly on every render. Also added a ResizeObserver to force
remeasurement on visibility transitions (defensive).
2. RightSidebarTabs now keeps git status fresh while the sidebar is open
via useRightSidebarGitSync hook (10s polling with ensureStatus).
Replaces the GitPollingProvider removed in commit
|
||
|
|
b91a72c74b |
fix(chat): replace hover bridge with padding to unblock desktop interactions (#885)
* fix(chat): replace hover bridge with padding to unblock desktop interactions PR #826 removed pointer-events-none from the hover bridge div to fix short user message revert button hover, but the transparent 44px full-width bridge blocked all interactions (clicks, text selection) with messages below it on Tauri desktop where the rendering difference made the overlay consume pointer events across the entire message area. Replacing the bridge div approach with pb-12 padding on the message bubble container (max-w-[85%]) when sticky inline hover is active. This extends the hover hit area downward within the bubble's own padding, maintaining continuous hover path from bubble to action buttons without any overlay that could block sibling elements. The padding approach works because: - The 48px bottom padding extends max-w-[85%]'s hover area to cover the gap between bubble bottom and the absolute-positioned buttons - Hovering the padding counts as hovering group/user-shell (parent) - No absolute-positioned invisible layer means nothing blocks the next message's interactive content * fix(chat): reduce hover padding from pb-12 to pb-5 for tighter spacing |
||
|
|
5ea2fed0d3 |
fix: hide empty archived section and folders (#872)
* fix: hide archived section and empty folders when no sessions remain - Only push archived group in useSessionGrouping when there are archived sessions, preventing an empty archived section from rendering - Hide empty folders in archived bucket via shouldKeepFolder check in SessionGroupSection (folders with no sessions and no content in children are filtered out) - Always filter folders through shouldKeepFolder, not just during search * perf: memoize archived folder filtering |
||
|
|
4fd1d68eb9 | fix(chat): allow revert button hover on short user messages (#826) | ||
|
|
4cb918f6cb |
fix: implement loading timeout, SSE reconnect, and message retry (#857)
* fix: implement loading timeout, SSE reconnect, and message retry - Loading timeout: 30s timeout with retry/cancel buttons to prevent infinite loading - SSE reconnect: Auto-reconnect up to 3 times with exponential backoff (1s, 2s, 4s) - Message retry: Ensure critical messages reach webview with 5s timeout and 3 retries This fix prevents the chat from getting stuck in a permanent loading state and improves reliability of SSE connections and webview communication. Fixes #851 * fix: harden vscode bridge retry flow --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
e63450ae2c |
fix: resync session state after SSE reconnect to prevent stuck subagent UI (#817)
* fix: resync session state after SSE reconnect to prevent stuck subagent UI When a subagent completes while the page is in the background (common on mobile PWA and desktop webview), the final SSE events are lost. The UI then stays stuck on 'Waiting for subagent activity...' because: - part.state.status never transitions to 'completed' - session_status is never updated to 'idle' - activeLatched remains true indefinitely Fix: - Add onReconnect callback to event pipeline, fired after SSE reconnect - Add pageshow listener for bfcache restores (mobile PWA back-forward) - On reconnect, re-fetch session list for directories with active sessions - Pass explicit directory to useSessionActivity in ToolPart for subagents to ensure the correct child store is queried Closes #810 * fix(chat): resolve pending subagent task binding before metadata arrives * fix: restore subagent activity and tool visibility after reconnect - Resyncs session status and child session data after SSE reconnect - Ensures child task tool messages are read from the correct directory - Prevents stale assistant fallback from keeping sessions stuck as active --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
aa071556bf |
fix(worktree): fix worktree detection and state reset when switching (#779)
* fix(worktree): reset IntegrateCommitsSection state when switching worktrees
Three fixes for the re-integrate commits panel getting stuck:
1. Add `key={worktreeMetadata.path}` to IntegrateCommitsSection so React
fully remounts it when switching to a different worktree, clearing any
stale `ui` state (conflict, loading, ready) from the previous session.
2. Add `cancelled` flag to the conflict-restore effect so that an async
callback started for session A cannot overwrite session B's state after
the user switches sessions. Without this guard the stale callback could
restore the old session's conflict state on top of the new session's
computed-ready state.
3. Fix off-by-one in continueIntegrate: `moved` was returning
`remaining.length` (N-1, after shifting currentCommit out) instead of
`state.remainingCommits.length` (N), undercounting the commit that was
moved by `cherry-pick --continue`.
* fix(worktree): add git-based fallback detection when store metadata is missing
Root cause: the existing worktreeMetadata resolution relies entirely on
cached store state (worktreeMap + availableWorktrees). When the store
lookup fails—due to hydrateSessionWorktreeMetadata deleting entries on
API failure, availableWorktrees being stale, or worktrees created
externally via CLI—the "Re-integrate commits" section permanently shows
"Available in worktree mode." with no way to recover.
Fix: add useDetectedWorktreeMetadata hook that performs a lightweight
git probe (`git rev-parse --absolute-git-dir --abbrev-ref HEAD`) when
the store-based lookup returns undefined. If the current directory is a
secondary git worktree, a minimal WorktreeMetadata is synthesised with
the correct projectDirectory and branch, allowing IntegrateCommitsSection
and other worktree features to function regardless of store state.
The store-based lookup remains the primary fast path; the git probe only
runs as a fallback and caches its result per directory.
* fix(worktree): fix detection command and pass current branch from git status
Two bugs in the fallback worktree detection hook:
1. `git rev-parse --absolute-git-dir --abbrev-ref HEAD` combines two
independent rev-parse options whose combined output is unreliable –
the two-line assumption (`lines.length < 2`) caused silent null
returns, meaning the fallback never actually set worktreeMetadata.
Now uses only `git rev-parse --absolute-git-dir` (single-line,
deterministic output) for worktree detection.
2. The hook was called before `useGitStatus`, so no branch was
available. Move the call to after `const status = useGitStatus(...)`
and pass `status?.current` as `currentBranch`, eliminating the need
for a second git command and keeping the branch in sync with the
already-polled git status.
Also removes `detected` from the useEffect deps array – it was an
unnecessary dep that triggered a re-run on every detected state change.
* fix(worktree): use worktree toplevel path and reset stale metadata immediately
Two bugs in useDetectedWorktreeMetadata:
1. path was set from currentDirectory (the active sub-folder) instead of the
worktree root. git rev-parse --show-toplevel now provides the actual
worktree toplevel, so operations like `git worktree remove` receive a valid
root path regardless of which sub-directory is open.
2. When currentDirectory changed with no storeMetadata, the hook kept
returning the prior detected value until the async git probe finished.
Calling setDetected(undefined) before launching the async task eliminates
the stale-metadata window.
|
||
|
|
58d7713581 |
fix(server): strip compression headers in generic OpenCode proxy (#795)
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
fd31a8cc2d |
fix(desktop): auto-cleanup stale server processes on startup (#711)
When updating OpenChamber, stale openchamber-server processes from previous versions can prevent the new version from starting. The app shows a loading screen indefinitely with no error message. This change adds a kill_stale_sidecar_processes() function that terminates any existing openchamber-server processes before spawning a new one, ensuring a clean startup every time. - macOS/Linux: uses pkill -x for exact process name match - Windows: uses taskkill /F /IM Fixes startup issues after version updates. |
||
|
|
bf61ccedc7 |
fix: external links in desktop app - context menu and open behavior (#716)
* fix: allow native context menu on links in chat messages The desktop app was blocking the context menu on all elements except specific allowlisted ones (terminal, input, textarea, etc.). This prevented users from right-clicking on HTTP links in chat messages to access the 'Open Link' option. Added 'a' (anchor) tag to the allowlist to restore native context menu functionality for links. Fixes #708 * fix: use tauri.shell.open for external links in desktop app - Add global window.open override to init_script that routes HTTP/HTTPS URLs through tauri.shell.open() instead of window.open() - Add openExternalUrl utility that prefers tauri.shell.open with window.open fallback - Add openExternalUrl to MarkdownRenderer for link safety.onLinkCheck - Replace window.open with openExternalUrl in ProvidersPage for OAuth URLs Fixes #708 * fix: unify external link opening across desktop and UI Added a shared URL opener that only allows http/https links. Replaced duplicated Tauri/window link-open logic in key UI sections. Removed fragile desktop window.open override and markdown external-link modal behavior. --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |