fix(sidebar): add project sort modes#9
Conversation
… pool Watching N worktrees fired N PR-status requests at once (startWatching called refresh() directly, bypassing the batch limiter). Each request can take 20s+ under GitHub secondary-rate-limiting, and N of them saturate the browser's ~6 HTTP/1.1 connections per origin, starving the critical path (bootstrap session.status, diffs, sending messages) until they finish — the UI appeared frozen for ~20s on startup. - Gate all PR-status network calls through a global concurrency semaphore (max 2), so free sockets always remain for critical traffic. - Bound resolveGitHubPrStatus with a 12s timeout so a slow request fails fast instead of holding a socket; the client keeps its last-known status. - Reuse already-fetched repo metadata for the default branch instead of a redundant repos.get, reducing serial GitHub calls (less rate-limiting).
A blind probe of the default port 4096 made the desktop hijack a user's separately-running OpenCode (e.g. the OpenCode desktop app): it attached as an external server instead of starting its own. That coupled OpenChamber's lifecycle to the foreign instance and broke initialization against an unexpected server version/config. Attaching to an external OpenCode now requires explicit opt-in via env (OPENCODE_HOST / OPENCODE_PORT / OPENCODE_SKIP_START). Without that, we always start our own managed instance on a freshly-allocated port.
The Electron-side OpenCode killer kills by port (lsof + kill -KILL). getOpenCodeProcessInfo returned openCodePort unconditionally, so for an external/attached OpenCode (e.g. a user's own server on 4096) the only thing stopping the killer from taking it down was the separate `managed` flag — a single weak signal guarding a destructive action. Withhold pid/port unless we actually manage the process, so the killer has no target even if `managed` is ever miscomputed. Managed flow is unchanged.
…ata (openchamber#1813) When clicking a session inside a worktree group, the layout effect in useProjectSessionSelection could override the user's selection with the project's first root session. This happened because projectSections (and thus projectSessionMeta) might not yet include the worktree group on the first render after the click. The fix adds a guard: if currentSessionId is set but not found in the current projectMap (stale data), the effect returns early instead of falling through to auto-selection logic. Fixes openchamber#1804 Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
…penchamber#1829) The stale-event check excluded heartbeats from lastActiveEventAt, so a quiet-but-connected session (only receiving heartbeats) tripped the 20s stale timer and triggered a full resync every ~15s. This re-fetched listPendingQuestions, listPendingPermissions, session.get, and session.messages despite the event stream being healthy. Track all stream activity (including heartbeats) in a global lastStreamActivityAt ref. The stale check now only fires when no events at all arrive for 20s, meaning the stream is genuinely dead. Resyncs still fire correctly on genuine reconnects, transport switches, and status-poll escalation when a real discrepancy is detected. Fixes openchamber#1656
…om sidebar (openchamber#1806) * fix(worktree): include sessions when deleting worktree group from sidebar allGroupSessions was guarded by group.isArchivedBucket, returning [] for active worktree groups. This caused the 'delete worktree' button in the sidebar to send an empty session list — SessionDialogs only removed the git worktree directory and skipped archiving any sessions, leaving them orphaned. Remove the guard so all sessions (including recursive children / subagent sessions) are collected regardless of archived state. * fix(sessions): delete all descendants on hard-delete instead of relying on server cascade The previous code sent only the root session ID and assumed the server would cascade-delete all children. If the cascade failed, children were left orphaned. Delete root + descendants individually; 404 responses from already-cascade-deleted children are treated as success. * fix(sessions): clear worktree metadata when deleting a session Deleted sessions kept their worktree attachment in both session-worktree-store and session-ui-store. Clean it up on successful deletion and on 404 (already deleted). * fix(worktree): search subagent sessions across all directories before delete WorktreeSectionContent and BranchPickerDialog used useSessions(), which is scoped to the current sync directory. Subagent sessions created in other worktrees/project roots were missed and left orphaned. Search across active + archived global sessions when collecting descendants. --------- Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
…ehavior settings (openchamber#1781) * feat: support OpenCode steer delivery / follow-up behavior settings Implements issue openchamber#1766 — steer delivery mode for mid-turn message insertion, replacing the old boolean queue-mode toggle with a tri-state follow-up behavior setting (Steer / Queue / Send immediately). - Plumbing: threaded optional delivery: 'steer' through sendMessage -> routeMessage -> opencodeClient.sendMessage -> promptAsync - Store: messageQueueStore stores followUpBehavior; migration from legacy queueModeEnabled persisted state - Settings: Chat -> Follow-up behavior shows three radio options using existing settings UI patterns - Composer: when session is busy, a floating queue button remains; force-sending a queued message (via chip click) uses delivery: 'steer' during a busy session; Steer button intentionally omitted — steer is available via the two-gesture path (Enter to queue -> chip to steer) - Keyboard: queue mode = Enter queues, Ctrl+Enter sends; otherwise Enter sends, Ctrl+Enter queues - Persistence: DesktopSettings, web settings payload, and server-side sanitizer handle the new key with legacy fallback - i18n: follow-up behavior section and option labels in all 9 locales plus new chat.chatInput.actions.queue label - Search: settings registry updated from chat.queue-mode to chat.follow-up-behavior Validation: type-check passes (no new errors), lint clean. * fix(openchamber#1766): make steer mode actually steer The followUpBehavior === 'steer' branch in handlePrimaryAction and the keyboard handler was a no-op — both fell into the else branch and sent without the delivery: 'steer' flag, so selecting 'Steer (insert into the running turn)' in settings produced identical behavior to 'Send immediately'. - handlePrimaryAction: when steer mode is selected and the session is busy, call handleSubmit({ delivery: 'steer' }) directly - Keyboard handler: in steer mode, Enter steers and Ctrl+Enter sends immediately (consistent with queue mode where Ctrl+Enter bypasses the special handling) Also removes the unused chat.chatInput.actions.queue i18n key from all 9 locales (it was a dead key after the Steer button was removed from the composer). Validation: type-check clean, lint clean. * refactor(openchamber#1766): flatten nested ternary in followUpBehavior resolution Replace nested ternary with explicit if/else chain per project code style (CONTRIBUTING.md). Import FollowUpBehavior type explicitly for the new let declaration. * feat(chat): drop redundant 'immediate' follow-up mode, keep Queue + Steer 'Immediate' was wire-identical to 'Steer' on a busy session: OpenCode only supports delivery 'steer' | 'queue' and defaults to 'steer', so an immediate send (no delivery flag) already steered into the running turn. The three-mode UI therefore exposed two settings that did the same thing. Collapse to two modes — Queue (unchanged: client-side queue with edit/reorder) and Steer. Any persisted/legacy 'immediate' (and legacy queueModeEnabled=false) now maps to 'steer', preserving prior behavior. Removes the immediate option, its keyboard branch, the i18n label across all locales, and narrows the followUpBehavior union to 'steer' | 'queue'. --------- Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
…on fixes, and sync
…choice settings It was rendered in the middle of the checkbox list, where a labeled radio group reads as out of place. Move it into the radio/choice cluster, right after Diff Layout, and drop it from the checkbox section's visibility gate.
The parenthetical explanations wrapped to two lines and added little — the section header already gives context and the two terms are self-explanatory.
…irectory getStatus() screamed 'Failed to get Git status' and rethrew for a directory that no longer exists — a benign case hit when PR-status resolution touches a worktree that was deleted while still being watched. Treat a missing directory like a non-repo: skip the error log (callers already handle/swallow it).
Octokit v22 uses native fetch, which has no built-in timeout, so a stuck GitHub request hung until the PR-status route's 12s overall budget fired — and one slow request could consume the entire budget. Wrap fetch with an 8s AbortSignal.timeout via a shared createOctokit() factory, and route the inline Octokit instantiations through it too.
…ooldown Octokit has no throttling plugin, so under a flood of PR-status calls a primary/secondary rate limit just surfaced as repeated 403s that the cache masked. Add a shared rate-limit gate: PR-status sub-calls note 403/429 responses, and the route short-circuits to cached/stale data during the cooldown instead of issuing more doomed requests. Transient failures (rate limit or the overall timeout) no longer log as hard errors.
resolveGitHubPrStatus walked remotes and candidate repos one network call at a time. Resolve all ranked remotes and fetch all candidate repo metadata with Promise.all instead, preserving rank/priority order and dedup. Cuts wall-clock on multi-remote/fork setups so a resolution is far less likely to hit the overall timeout. The PR-search loop keeps its early-return (parallelizing it would issue more calls, not fewer).
… exists A deleted worktree often still has a session in the sidebar, which keeps polling its PR status — spending a git status call (the source of the noisy 'directory does not exist' errors) plus remote/repo resolution on a gone path. Bail out early when the directory is missing; the route already returns a benign no-repo result, which caches so it stops re-polling.
Omit unset agent fields on create Delete cleared agent fields in VS Code config updates Cover null field removal with a regression test
Only parse full-message generated JSON results Keep markdown prose with JSON examples rendered normally Add regression coverage for embedded JSON examples
Fixes switching and unlocking password-protected remote instances Stores SSH forwarded host client tokens from saved UI passwords Avoids unnecessary auth churn when no runtime headers are configured
… 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.
The native-apps change removed SessionAuthGate from the whole mobile render path, but that path also serves the plain mobile browser: against a --ui-password server it never authenticated and dead-ended on the unreachable-server screen with no way to enter the password. The gate is back for the browser shell; the Capacitor app keeps its own instance-connect flow ungated.
…released changelog The pill's expand focused the textarea from a rAF, outside the user gesture — mobile browsers only show the soft keyboard for synchronous focus, so the composer expanded silently. The expand now flushes the render and focuses in the same gesture, and preventScroll applies only inside the Capacitor shell so browsers keep their native reveal that lifts the field above the keyboard (same for the post-overlay keyboard restore). Also drafts the [Unreleased] changelog entries for everything since v1.13.9.
Drop bullets that fixed bugs introduced within the same unreleased range, fold the load-older button into its feature description, and group the Capacitor-only keyboard/draft/instance polish under the pre-release mobile-apps bullet like the 1.13.9 entry did.
…he composer Starter chips route their text into the submit as an explicit override; the previous flow staged it in the textarea, which doesn't exist while the mobile composer is collapsed into the pill, so the submit read an empty snapshot and silently bailed, leaving the command text sitting in the input.
Command, file/agent, skill, and snippet autocompletes now stop at the top of the chat area (Capacitor) or the visible viewport edge (mobile browsers, which pan the page for the keyboard) and may grow that far, measured live across keyboard settles and viewport changes. On mobile the keyboard-hint footer and description lines are gone, rows center their icons, and list overscroll no longer bounces the page behind. Selecting a command no longer dismisses the keyboard (its rows now block the tap's focus steal like the composer buttons), and the dead dismissKeyboard option is removed.
The mobile browser flashed the unreachable-server screen during the ordinary initial connect; it now keeps the logo splash and only shows the error once the 8s recovery window expires (shared with the native path). All MobileApp splash logos are 120px to hand off seamlessly from the static boot splash instead of visibly shrinking.
…eens Mobile browsers don't shrink the layout for the keyboard, so the fullscreen composer is now pinned to the visual viewport (fixed at its offset and height, tracked as the browser pans) instead of overflowing underneath it; the draft screen's normal composer gets the same pinning anchored to the visible bottom via a rAF tracker, since Safari's own focused-field reveal proved unreliable there after leaving fullscreen. The draft and empty-session roots drop their transform-gpu (a transform would make them the containing block for the pinned form), the app header hides while the browser fullscreen composer is up (the form can't out-stack it from inside the composer wrapper's stacking context), and leaving fullscreen nudges the still-focused field back into view. Draft starter chips now hide while the keyboard is open in browsers too, via an oc-browser-keyboard-open root class driven by composer focus.
…t tokens The client-create gate added in 1.13.9 rejects client tokens without the desktop-local kind, but the kind was only attached when the runtime origin exactly matched the injected local origin — an empty (same-origin) api base, loopback aliases, and the embedded server addressed via a LAN interface (0.0.0.0 binds) all minted untagged tokens, which then hit 403 and surfaced as "Local — Auth required" plus the unreachable-server screen. The renderer now treats same-origin and loopback targets as local, the Electron main additionally matches any of the machine's own interface addresses on the local server's port, and a deduped kind-tagged mint migrates away legacy same-label tokens that predate client kinds. The client-create gate itself is unchanged.
The bundled CLI used to outrank PATH and known install locations, so updating the desktop app silently switched people off their own OpenCode. It is now the last resort: explicit env/settings, PATH, known install locations, and shell discovery all win; the bundle only serves machines with no OpenCode install at all. The env-runtime accepts an injectable homedir so the fallback test stays hermetic on machines with a real ~/.opencode install.
Launching: the VS Code extension spawned .cmd shims with shell:true, which builds an unquoted command line — a path like "C:\Program Files\nodejs\opencode.cmd" broke with "'C:\Program' is not recognized". It now spawns cmd.exe directly with the shim path as its own argv element (the web server's existing cmd-wrapper pattern), and the web server's final launch-spec fallback routes raw .cmd/.bat through the same wrapper instead of handing them to spawn. Configured paths: wrapping quote pairs (Windows "Copy as path" pastes) are now stripped everywhere a binary path enters — the VS Code setting and shared settings.json, env vars on both surfaces, the server's directory-path normalization, and the desktop settings input. Discovery: added the system-wide npm prefix (Program Files\nodejs) and scoop's .exe shim to the Windows candidates, Linuxbrew on Linux, and the where-probe now runs with windowsHide. The macOS desktop-app exclusion also covers the OpenCode Dev/Beta app bundles.
The stop button sent the abort with the UI's active directory, but OpenCode dispatches the request to the per-directory instance — for a session running under a different project, worktree, or a mapped docker path the abort hit an instance that didn't own the prompt, cancelled nothing, and still returned 200. The abort now resolves the session's own directory, like the revert flow's aborts always did.
…ated surface variables
…hamber#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.
Detects the OpenChamber CLI from Bun's global install directory Starts the global instance via the installed CLI path instead of relying on PATH Fails fast if the global CLI was not installed
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Checks the runtime session before restoring a mobile connection Disconnects and resets state when the session is no longer valid Adds tests for reachable, unreachable, and unauthenticated runtimes
Adds a dialog with call booking and survey actions for user feedback Shows a one-time sidebar toast prompting users to share what’s useful or missing Adds localized copy for the new feedback entry points
Keeps the textarea reference available during mobile viewport adjustments Ensures the composer scrolls back into view after keyboard interactions Updates text selection menu dependencies to include the current session
…amber#2000) File references in chat messages can now use the 'path:start-end' form (e.g. 'src/foo.ts:120-145'). The reference becomes clickable in the renderer and, on click, the file opens at the start line. Range selection is intentionally not done at this layer — the 'path:start-end' form is parsed only so the link resolves to the correct path; navigation jumps to the start line, matching the behavior of the existing 'path:line' form. - Extract the file-reference parser to a dedicated module so it can be unit-tested without pulling in the markdown renderer's worker dependencies. - Add a range branch to 'parseFileReference' and update the block-code path regex to recognize the new form. - Switch the colon-form regex to a non-greedy path match so 'path:line:col' is no longer mis-parsed as 'path:line' with the first numeric suffix dropped into the path. - Add a unit test covering the new and existing parser forms.
Uses hunk contents to find the first modified line instead of the hunk start Handles added, removed, and binary-only patches more accurately Adds tests for patch parsing edge cases
Adds a load older button when earlier history is available Preserves scroll position while older messages are loaded Shows date-grouped messages with clearer per-message timestamps
|
Important Review skippedToo many files! This PR contains 420 files, which is 270 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (46)
📒 Files selected for processing (420)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Closing this fork PR; reopening in upstream as requested. |
Summary
Issue
Closes openchamber#1112
Validation
git diff --checkbun run --cwd packages/ui type-check(blocked by missing@types/dom-speech-recognitionand a TSbaseUrldeprecation warning)bun run --cwd packages/ui lint(blocked by missingeslint-plugin-react-hooksin this environment)429 Too Many Requests)Notes