Release v0.19.0 - Persistent workspaces, customizable panels, provider controls, nested sessions and mobile reliability - #656
Open
pascalandr wants to merge 53 commits into
Open
Release v0.19.0 - Persistent workspaces, customizable panels, provider controls, nested sessions and mobile reliability#656pascalandr wants to merge 53 commits into
pascalandr wants to merge 53 commits into
Conversation
) ## Problem Mobile users experience SSE disconnections (wifi drops, Cloudflare idle timeouts) but get no visual feedback. The per-instance status dots remain green even when the SSE transport is down, because instance status updates travel over the broken SSE pipe. ## Solution This PR implements **immediate connection status feedback** by propagating SSE transport state to all instance status indicators: ### Changes 1. **server-events.ts**: Add `onDisconnect()` handler - Mirrors existing `onOpen()` pattern - Fire `disconnectHandlers` in `scheduleReconnect()` when connection drops 2. **sse-manager.ts**: Register SSE lifecycle handlers - `onDisconnect`: set ALL instances to `connecting` (amber dot) - `onOpen`: clear `connecting` status (green dot restored by subsequent events) - Log transitions for debug visibility 3. **AGENTS.md**: Add PR Review Principles - Check regressions first, look for better implementations - Be the PR gatekeeper, ruthless code quality - Test before responding, UI/server version parity ## Verification - ✅ TypeScript compilation clean (pre-existing SDK errors unrelated) - ✅ Vite build successful - ✅ Mobile testing: SSE disconnect → immediate amber dots - ✅ Reconnect → green dots restored ## Edge Cases Handled - **Transient drop**: amber → green on reconnect (no false disconnect modal) - **Instance dies during outage**: amber → green → `disconnected` event → red - **0 instances**: Map empty, loop is no-op - **Rapid reconnect cycles**: idempotent (setting `connecting` on `connecting` is no-op) ## Related - Complements PR #519 (pong retry with timeout) - merged upstream - Addresses mobile UX gap: workspace-level SSE state now visible to all users (not just debug overlay) --------- Co-authored-by: Shantur Rathore <i@shantur.com> Co-authored-by: Pascal André <pascalandr@gmail.com>
## Problem PR #565 (commit e29c3a0) added `scope=project` to session list requests so worktree/project sessions remain visible. With OpenCode 1.17.x, the `/session` endpoint can ignore the `directory` filter when `scope=project` is present and return sessions from unrelated folders that share the same project/global bucket. This can make sessions appear in the wrong project tab. Deleting one of those sessions still deletes it from the shared OpenCode database, so the UI must avoid showing unrelated sessions in the wrong tab. ## Reproduction ```bash PORT=4096 curl -s -u "codenomad:$PASSWORD" \ "http://127.0.0.1:$PORT/session?directory=/home/user/project-a&scope=project" \ | jq 'length' # Can include sessions from other folders too. curl -s -u "codenomad:$PASSWORD" \ "http://127.0.0.1:$PORT/session?directory=/home/user/project-a" \ | jq 'length' # Filters to the requested folder, but may miss project/worktree sessions. Related: anomalyco/opencode#33113 Fix Keep scope: "project" so OpenCode still returns project/worktree sessions, then filter the returned sessions client-side to the current root directory plus known CodeNomad worktree directories. This preserves the worktree-session visibility behavior from #565 while avoiding unrelated cross-directory leakage when OpenCode ignores directory alongside scope=project. Validation Focused session-pagination tests pass. UI typecheck passes. UI build passes. Tauri release build passes. Manual exe tested for validation. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Pascal André <pascalandr@gmail.com>
## Summary - replace message stream follow behavior with a strict following/escaped state model - keep explicit submit bottom pinning deterministic while preserving user scroll-up escape intent - prevent repeated pending question/permission polling from rewriting identical store entries and snapping back to bottom - isolate pending request dedupe logic in a raw-node-testable helper Fixes #574 ## Validation - node --test "packages/ui/src/components/virtual-follow-behavior.test.ts" "packages/ui/src/components/session/session-bottom-pin-intent.test.ts" "packages/ui/src/stores/message-v2/pending-request-dedupe.test.ts" - npm run typecheck --workspace @codenomad/ui - npm run build --workspace @codenomad/ui - git diff --check
## Summary Yolo (permission auto-accept) currently lives entirely in the UI. Each browser keeps its own toggle in `localStorage` and auto-replies to permission requests over a 4-hop path (`OpenCode → server SSE → UI → server proxy → OpenCode`). The server — which already sits on the event stream that carries every `permission.asked` — does none of the work. This PR makes the **server authoritative**: it owns the toggle state, resolves family-root inheritance, and performs the auto-reply in-process via loopback using the same `"once"` semantics the UI used to send. The UI becomes a pure view: it toggles via REST and mirrors state from a `yolo.stateChanged` SSE event. ## Why - **Correctness**: the server already consumes the instance SSE stream (`InstanceEventBridge`); auto-accepting there is the natural choke point instead of bouncing to the UI and back. - **Multi-client**: previously each browser had independent `localStorage` state and never synced. Toggles now broadcast to all connected clients in real time. - **Headless**: Yolo keeps auto-accepting even when no UI is connected (useful for long autonomous runs). - **Latency**: drops from 4 hops to a single in-process loopback call. ## What changed **Server (new, authoritative)** - `permissions/auto-accept-store.ts` — in-memory state keyed by family root. Faithful port of `resolvePermissionAutoAcceptFamilyRoot`: fork/`revert` sessions root at themselves; enabling any member enables the whole family. - `permissions/auto-accept-manager.ts` — subscribes to `instance.event`, builds the session tree from `session.created/updated/deleted` (`properties.info`), intercepts `permission.v2.asked` / `permission.asked`, dedupes in-flight replies, emits `yolo.stateChanged` / `yolo.autoAccepted`, clears per-instance state on `workspace.stopped/error`. - `permissions/opencode-replier.ts` — default replier calling OpenCode directly (`getInstancePort` + auth header), mirroring `background-processes/manager.ts`. - `server/routes/yolo.ts` — `GET/POST /workspaces/:id/yolo/sessions/:sid[/toggle]`, following existing route conventions. - `api-types.ts` / `events/bus.ts` — `YoloStateResponse` + the two new event types registered in `onEvent` so they flow over `/api/events`. **UI (pure view)** - `permission-auto-accept.ts` — removed `localStorage`, persistence, and drain logic; now a runtime (non-persisted) projection. `resolvePermissionAutoAcceptFamilyRoot` is **retained as a display aid** so the badge still lights up for child/sub-sessions of an enabled family (preserving the inheritance UX). - `instances.ts` — toggle calls REST (optimistic, reconciled on success, reverted on failure); subscribes to `yolo.stateChanged`; `ensureYoloStateSynced` backfills the active session's state on first connect (deduped per session, reset on SSE reconnect so it re-syncs after a server restart). - `session-events.ts` — removed the three `drainAutoAcceptPermissionsForInstance` hooks (the server drains now). - `api-client.ts` — `getYoloState` / `toggleYolo`. ## Behavior parity | Aspect | Before | After | |---|---|---| | Reply semantics | `"once"` | `"once"` (unchanged) | | Inheritance | whole family (root + non-fork descendants) | **same** | | Fork isolation | `revert` session is its own root | **same** | | Persistence | UI `localStorage` | none (intentional, see Notes) | | Multi-client sync | ❌ independent per browser | ✅ real-time via SSE | | Works with UI closed | ❌ | ✅ | | Auto-reply path | 4 hops | 1 in-process hop | ## Testing 32 unit tests added (`node:test`), all passing. Server + UI typecheck clean. - **Store (16)**: inheritance (parent/child/sibling), fork isolation, cyclic parent chains, late parent discovery, `revert` re-rooting, per-instance independence, tree maintenance. - **Manager (13)**: real `properties.info` event shapes, v2 vs legacy reply, in-flight dedup + retry-after-resolve, `yolo.stateChanged` emission, `session.deleted` keeps toggle, `workspace.stopped` clears state, `stop()` unsubscribes. - **UI (3)**: retained `resolvePermissionAutoAcceptFamilyRoot` display-projection tests. ## Notes for reviewers - **No persistence is intentional** for this milestone — server restart resets all Yolo state (matches the agreed scope). The UI mirror self-heals via SSE reconnect + `ensureYoloStateSynced`. Persistence can be layered on later (e.g. into `~/.config/codenomad/config.json`) without touching the manager. - **Family-root resolution lives in two places on purpose**: the server resolves it to decide whether to auto-reply; the UI resolves the same pure function to render the badge instantly (synchronous memo). Both are faithful ports; no network round-trip is added for display. - **`properties.info` nesting**: OpenCode wraps session records under `properties.info` for `session.*` events (permission events are flat). The manager handles both and the tests use the real nested shape to guard against regressions. - `getYoloState` / `yolo.autoAccepted` are wired but `yolo.autoAccepted` is not yet consumed by the UI — it's available on the wire for future observability (e.g. an audit log / toast). ## Risk / rollback The change is additive on the server and the UI gracefully degrades to the SSE mirror. If the server lacks the new routes (mixed-version), the UI's optimistic toggle still flips locally and `toggleYolo` failures are logged + reverted, so no hard breakage. --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
This change enables the UI to display nested sessions within nested
sessions in a foldable display, recursively, up to 10 levels deep.
## What Changed
### Core Data Structure (session-state.ts)
- Redefined `SessionThread` type to support true recursive nesting:
- Old: `{ parent: Session, children: Session[], latestUpdated: number }`
- New: `{ session: Session, children: SessionThread[], depth: number,
hasChildren: boolean, latestUpdated: number }`
- Renamed `expandedSessionParents` signal to `expandedSessions` to
reflect that ANY session with children can now be expanded, not just
top-level parents
- Updated `getSessionThreads()` to build a recursive tree structure
using new `buildSessionThreadTree()` and `computeThreadSignature()`
helpers
- Updated `getSessionFamily()` to recursively collect ALL descendants,
not just direct children
- Updated `getVisibleSessionIds()` with `collectVisibleSessionIds()`
helper to recursively collect visible session IDs based on expansion
state
- Maintained backward compatibility aliases for renamed functions:
`isSessionParentExpanded`, `setSessionParentExpanded`, etc.
### Session State Exports (sessions.ts)
- Updated imports and exports to use the new function names:
`ensureSessionExpanded`, `isSessionExpanded`, `setSessionExpanded`,
`toggleSessionExpanded`
### Session Events (session-events.ts)
- Updated `ensureSessionParentExpanded` → `ensureSessionExpanded` in
auto-expand logic for child sessions that start working
### Permission Modal (permission-approval-modal.tsx)
- Updated import and usage of `ensureSessionParentExpanded` →
`ensureSessionExpanded`
### UI Rendering (session-list.tsx)
- Updated `SessionRow` component to accept `session` object directly
instead of `sessionId`, plus `depth` and `isLastChild` props
- Derived `isChild` from `depth > 0` instead of explicit prop
- Added `depthClass()` for CSS depth-based indentation
- Created new `SessionThreadRow` recursive component that:
- Renders the current session via SessionRow
- If expanded and has children, recursively renders children with
increased depth
- Updated `filteredThreads` with `subtreeHasMatch()` and
`filterThreadTree()` helpers for recursive filtering
- Updated `allMatchingSessionIds` with `collectThreadIds()` helper for
recursive ID collection
- Removed child-specific `Bot` icon - all sessions now use `User` icon
- Updated expander visibility to show for ANY session with children,
regardless of depth
### Styling (session-layout.css)
- Added depth-based CSS classes `.session-item-depth-{1-10}` with:
- Progressive indentation: 2.25rem for depth 1, up to 13.5rem for depth
10
- Tree connector styling via `::before` and `::after` pseudo-elements
- Proper vertical line handling for last-child at each depth level
### Tests (session-state.test.ts)
- Added comprehensive test suite (683 lines) covering:
- `getSessionThreads`: empty sessions, single sessions, single-level
children, multi-level nested children, sorting, hasChildren computation
- `getSessionFamily`: recursive descendant collection
- Expansion state: toggle, explicit set, ensure logic
- `getVisibleSessionIds`: visibility based on expansion state at
multiple levels
## User-Facing Behavior
- Nested sessions can now be collapsed/expanded at any depth level
- A chevron expander appears on any session that has children
- Children are indented based on their nesting depth
- Tree lines connect parent-child relationships visually
- Expanding a parent auto-expands ancestors when selecting a deeply
nested child session
## Edge Cases Handled
- Sessions with no children show no expander
- Last child at each depth level has shortened vertical tree line
- Thread sorting by latestUpdated works correctly with nested updates
- Cache invalidation properly tracks thread changes at all depth levels
## Implementation Notes
- Depth is limited to 10 levels to prevent excessive indentation
- The `hasChildren` flag is computed once during tree building for
performance
- The Session type's `parentId` field already supported arbitrary
nesting - only the UI rendering needed to be updated
---------
Co-authored-by: Pascal André <pascalandr@gmail.com>
…tion (#247) # PR: Extend delete overlays and clean companion parts during tool deletion Fixes: #246 ## Purpose This PR addresses incomplete deletions when users bulk-delete specific tool calls from the timeline. Previously, deleting a tool part left behind orphaned reasoning logs (thinking tokens) and generated text parts, and the destructive hover overlay only highlighted the tool cards themselves. This update ensures that deleting tool parts cleans up the entire associated response context (`step-finish`, `reasoning`, and `text` parts) from that message. It also extends the red delete overlay to message headers, reasoning cards, and text parts to accurately reflect the scope of deletion to the user. ## Changes by File ### `packages/ui/src/components/message-block.tsx` - Threaded the `selectedToolPartKeys` prop through `MessageContentItem`, `MessageBlock`, and `ReasoningCard`. - Updated `ReasoningCard`'s `isSelectedForDeletion` logic to check both `selectedMessageIds` (whole message) and `selectedToolPartKeys` (tool-part selection via a `messageId:` prefix match). - Moved the block-level selection highlight out of `isDeleteMessageHovered` to be handled at the part level via `data-delete-part-hover` attributes avoiding double overlays. - Added `data-delete-part-hover` to the `ReasoningCard`'s root `div`. ### `packages/ui/src/components/message-item.tsx` - Added the `selectedToolPartKeys` prop to `MessageItemProps`. - Expanded `isSelectedForDeletion` using the same prefix-match pattern to detect tool-part selections. - The `<header>` element now includes a `delete-hover-scope` class and `data-delete-part-hover` attribute to trigger the overlay at the header level. - Each `.message-part-shell` rendering text content now includes `data-part-type` and `data-delete-part-hover` for per-part overlay highlighting. ### `packages/ui/src/components/message-section.tsx` - Updated the `deleteSelectedMessages` bulk action. It now additionally collects and deletes `step-finish`, `reasoning`, and `text` parts when a message has its tool parts selected for deletion. This ensures no orphaned assistant content remains. - Simplified the deletion guard condition from `selectedToolPartIds.size === 0 || stepFinishPartIds.length === 0` down to `selectedToolPartIds.size === 0`, ensuring reasoning/text parts evaluate correctly even if `step-finish` parts are absent. ### `packages/ui/src/styles/messaging/delete-overlays.css` - Widened the part-level overlay inset from `-2px` to `-4px` for visual consistency with the message-level overlay. - Added a new rule for `.message-reasoning-card[data-delete-part-hover]` using `inset: 0` for flush overlay rendering within reasoning card boundaries. --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
## Summary - Publish one supported installer on Linux: a Tauri .deb package for x64. - Publish one portable fallback: an Electron .tar.gz archive for x64. - Remove the temporary Flatpak path and stop publishing Linux AppImage, RPM, and zip variants. - Remove obsolete Linux assets from reused releases before uploading replacements. - Closes #487, Closes #488 ## Artifact validation - Extract the Electron archive and verify its executable, bundled server resources, app.asar renderer entry, referenced assets, and shared-library closure. - Verify Tauri Debian metadata, bundled resources, desktop entry, shared libraries, and package installation in Ubuntu 24.04. - Synchronize Tauri package versions before every platform build so generated metadata follows release inputs. ## Validation - Parsed the workflow YAML and Electron package JSON. - Verified the local @electron/asar APIs used by CI. - Ran the Tauri version synchronization script. - Ran git diff --check. - Reviewed the final diff against current dev; full Linux packaging and Docker installation are delegated to PR CI. ## Compatibility The Tauri deb is built and installation-tested on Ubuntu 24.04. Older Debian-based distributions are not yet guaranteed. --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
detect realpath when open workspace try avoid path issue in windows - d:\xxx - D:\xxx - symbol link now they are same path when try detect exists workspace --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
## Summary Voice input (speech-to-text / STT) and voice output (text-to-speech / TTS) currently share a single API key, base URL, and provider configuration. Users cannot use different providers for each direction — for example, Groq for transcription (STT) and OpenAI for synthesis (TTS). This PR adds a `separateProviders` toggle to the speech settings. When **off** (default), behavior is unchanged — existing configs work as-is. When **on**, STT and TTS each get their own `apiKey`, `baseUrl`, and `model` fields, allowing independent OpenAI-compatible endpoints per direction. ## Changes ### Server - **`api-types.ts`**: Added `separateProviders`, `sttConfigured`, `ttsConfigured`, `sttBaseUrl`, `ttsBaseUrl` to `SpeechCapabilitiesResponse` - **`speech/service.ts`**: Extended Zod schema with `separateProviders` + nested `stt`/`tts` sub-objects. Split `createProvider()` into direction-aware `createSttProvider()`/`createTtsProvider()` with `resolveSttSettings()`/`resolveTtsSettings()` resolvers that fall back to shared values when per-direction fields are absent - **`settings/public-config.ts`**: Extended `sanitizeServerOwner` to strip per-direction `stt.apiKey`/`tts.apiKey` and set `stt.hasApiKey`/`tts.hasApiKey` booleans (same pattern as the shared `apiKey`) - **`OpenAICompatibleSpeechProvider`**: Completely unchanged — it receives the same flat `NormalizedSpeechSettings` it always has ### UI - **`stores/preferences.tsx`**: Extended `SpeechSettings` type with `separateProviders`, `stt`, `tts` sub-objects. Updated `normalizeSpeechSettings()` and `updateSpeechSettings()` to handle per-direction patches - **`components/settings/speech-settings-card.tsx`**: Added toggle at the top of the card. When enabled, shows two sections (Input/STT and Output/TTS) with their own API Key, Base URL, and Model fields. Playback Mode and TTS Format remain shared. Also includes "Test input" button alongside existing "Test playback" - **`lib/audio-utils.ts`**: Extracted shared `blobToBase64`, `createMediaRecorder`, `stopTracks` utilities (used by both `usePromptVoiceInput` and `useTranscriptionTest`) - **`lib/hooks/use-transcription-test.ts`**: New hook for testing STT transcription from the settings card - **`components/prompt-input/usePromptVoiceInput.ts`**: Uses `sttConfigured` instead of combined `configured` - **`stores/conversation-speech.ts`**, **`lib/hooks/use-speech.ts`**, **`speech-settings-card.tsx`**: TTS consumers use `ttsConfigured` instead of combined `configured` - **`styles/components/settings-screen.css`**: Added `settings-card-section-header`/`settings-card-section-title` styles - **i18n**: Added new keys per locale across all 9 locales (en, es, ja, zh-Hans, fr, de, he, ne, ru) ## Backward Compatibility - `separateProviders` defaults to `false` — existing configs work unchanged - The `configured` field remains in capabilities response as `(sttConfigured || ttsConfigured)` so any consumer not yet updated still works - `NormalizedSpeechSettings` (the provider-facing flat interface) is unchanged ## Testing - 13 unit tests covering direction-aware resolution (shared mode, separate mode, partial config, fallback chains, model overrides) and config sanitization - All tests pass: `npx tsx --test packages/server/src/speech/service.test.ts packages/server/src/settings/public-config.test.ts` - Server typecheck: clean - UI typecheck: clean - UI build: succeeds (no duplicate i18n keys)
#580) ## Problem The voice input button uses push-to-talk: press to start recording, release to stop. On the **second and subsequent recordings**, the button fails to enter the recording state (doesn't turn red) and jumps directly to "transcribing", producing empty or header-only audio (~110 bytes) that the STT provider rejects with HTTP 400. This affects **all platforms** (Linux/Electron, Windows/Chrome), not just Linux as originally reported in #550. ## Root Cause `startRecording()` is async — it awaits `getUserMedia()`. But `beginVoicePress()` in `prompt-input.tsx` fires it with `void` (fire-and-forget): ```typescript void voiceInput.startRecording() // not awaited ``` When the user releases the button before `getUserMedia()` resolves (common on second+ recordings where mic permission is already granted and acquisition is fast), `stopRecording()` finds no active recorder (`mediaRecorder === null`, `state() === "idle"`) and **silently returns**. The pending `getUserMedia()` promise later resolves and starts an **orphan recording** with no matching stop. The user perceives: button doesn't turn red → presses again → the orphan recorder is stopped → jumps to "transcribing". On fast taps, the orphan recording captures zero audio frames, producing a header-only WebM blob (~110 bytes). ### Why it only manifests on second+ recordings On the **first** recording, `getUserMedia()` may show a permission prompt or have slower initial device setup, giving the user time to hold the button. On **subsequent** recordings, the device is already warm and `getUserMedia()` resolves faster, but the user's tap duration remains the same — releasing before acquisition completes. ### What this is NOT - **Not a PipeWire/Linux-specific issue** — reproduced on Windows Chrome (server on Linux, browser on Windows) - **Not a MediaRecorder encoder bug** — tested with Chrome's fake audio device: `start()` vs `start(timeslice)` produce identical results across consecutive recordings - **Not a recorder reuse issue** — the code already creates fresh `MediaStream` + `MediaRecorder` per recording ## Fix Add a `requestGeneration` counter (mirroring the pattern from `use-transcription-test.ts` in PR #579): - `startRecording()` increments the generation before awaiting `getUserMedia()` - After the await resolves, if the generation is stale, the stream is immediately stopped and discarded - `stopRecording()` / `cancelRecording()` increment the generation when called while `state() === "idle"` (pending acquisition), immediately invalidating any in-flight request - All event listeners (`dataavailable`, `stop`) and `finalizeRecording()` check the generation before mutating state - `cleanupMedia()` and `onCleanup()` increment the generation to cover unmount and error paths Additionally, `stopTracks(stream)` is called **before** the transcription network round-trip (instead of after), releasing the audio device sooner. ## Verification - `tsc --noEmit` passes (packages/ui) - Code review: no regressions, no stream leaks, no double-free paths found - Reproduction evidence: confirmed the 110-byte header-only blob signature matches the orphan-recording failure mode Closes #550
## Why LaTeX content commonly uses `\(...\)` and `\[...\]`, but CodeNomad only recognizes `$...$` and `$$...$$` through `marked-katex-extension`. ## What - Add parser-native Marked rules for inline `\(...\)` and display `\[...\]` formulas. - Preserve existing dollar math, code spans and fences, escaped delimiters, and unmatched text. - Add focused regression tests and document the delimiter design.
## Summary Move keyboard focus from the prompt to the active message stream immediately after a desktop prompt submission. This makes conversation navigation keys such as Page Up and Page Down available without requiring a click. Starting to type printable text returns focus to the prompt for a follow-up message. ## Changes - Focus the current session's message stream before waiting for the message, command, or shell request. - Carry an explicit focus intent through first-prompt session creation so the newly mounted session focuses its message stream instead of the detached draft view. - Do not overwrite focus if the user changes controls or opens a modal during first-session activation. - Restore the active prompt when a submission fails. - Make the message stream programmatically focusable without adding it to the normal Tab order. - Keep type-to-focus active on hybrid touch/mouse devices and preserve `!` shell-mode activation from the message stream. ## Unchanged behavior - Touch-only submissions keep their existing focus behavior. - Existing global Escape handling is unchanged. ## Validation - Focused prompt and scrolling tests pass: `24/24`. - UI typecheck passes. - UI production build passes. - `git diff --check` passes. - Independent gatekeeper re-review found no remaining blockers.
## Summary - Restore the primary desktop client's window bounds, zoom, workspace tabs, active sessions, drafts, bounded attachments, scroll positions, and panel layout across launches. - Keep additional Electron and Tauri processes independent: they still start normally but do not read or write the shared native snapshot. - Add translated settings to disable startup restoration or clear the saved state. ## Implementation - Persist a versioned, bounded snapshot atomically in Electron and Tauri, preserving unknown future envelopes until an explicit clear. - Elect one primary process with stale-owner recovery and process identity checks, then protect renderer access with per-document capability tokens. - Flush renderer state before close, quit, reload, and app-owned navigation with bounded failure handling and fresh token rotation. - Reconcile partial or timed-out workspace hydration without losing unsent prompt state or resurrecting explicitly cleared drafts, attachments, sessions, or tabs. - Correlate restore-created workspaces so cancellation and delayed SSE events cannot produce ghost tabs. - Harden workspace launch cancellation and cross-platform process cleanup so failed starts cannot orphan detached POSIX, WSL, or Windows children; incomplete shutdown exits nonzero. The restored shell state remains local to the desktop client. Durable semantic session properties continue to use OpenCode session metadata separately. ## Validation pm run typecheck --workspace @neuralnomads/codenomad - Electron native suite: 43 passed - Tauri suite: 54 passed - Focused UI restore, reconciliation, attachment, authority, and race suites passed - Focused server workspace runtime, manager, process identity, route, and shutdown suites passed - pm run build - pm run build:tauri - git diff --check - 16-pass adversarial review loop completed with final No findings - extensivly optimized, tested and used day by day ## Platform notes - Native menu reloads and app-owned navigation have awaited durability guarantees. Browser-engine crashes or forced process termination remain inherently best effort. - Process cleanup uses identity-guarded platform adapters and fails conservatively when target ownership cannot be proven.
## Summary - Send an explicit JSON Merge Patch deletion when removing an environment variable. - Prevent deleted variables from remaining in config.yaml and being injected into future OpenCode instances. - Maybe closes #575 ## Root Cause The previous removal flow submitted the remaining environment-variable record without the selected key. The settings API recursively applies JSON Merge Patch, where an omitted nested key means no change rather than deletion. ## Validation - npm run typecheck --workspace @codenomad/ui - npm run build --workspace @codenomad/ui - git diff --check
## Problem Worktrees created through OpenCode can leave CodeNomad's cached worktree inventory and workspace mapping stale. The previous approach in #516 polled on `session.idle` and attempted to infer which session created a newly observed worktree, which introduced race conditions and unsafe attribution. ## Fix - route OpenCode's `worktree.ready` event through the existing UI SSE manager; - reload the live Git worktree inventory when that event arrives; - synchronize OpenCode workspace mappings only after the refreshed inventory is available; - avoid idle polling and avoid auto-switching a session because the event identifies the new worktree directory but does not identify an originating session. The server bridge already forwards arbitrary OpenCode events and preserves the global event `directory`, so no server change is required. Supersedes #516 and implements the SSE approach suggested by @shantur #516 (comment). ## Validation - `bun test --conditions browser packages/ui/src` (`126` passing) - `npm run typecheck --workspace @codenomad/ui` - `git diff --check`
## Summary - Add provider quota usage to an expanded-by-default subsection in the session Status panel. - Resolve the active provider and model automatically from the current session. - Display localized quota windows, reset times, values, and color-coded progress bars inside the same bordered rounded container used by neighboring subsections. ## Server implementation - Add GET /api/usage/:providerId with optional modelId filtering. - Add a 60-second in-memory cache and deduplicate concurrent provider requests. - Read credentials only on the server and never return secrets through the API. - Support Claude, Codex, GitHub Copilot, Google/Gemini/Antigravity, Kimi, NanoGPT, OpenRouter, z.ai, Zhipu, MiniMax global/CN, Cursor, Ollama Cloud, Wafer, and OpenCode Go. - Use existing OpenCode and Antigravity access tokens for Google calls; optional environment-provided OAuth client values are required only for token refresh. ## UI behavior - Keep Provider usage open by default while allowing it to collapse like the other Status subsections. - Show loading, unsupported, not configured, unavailable, and no-session states. - Localize all user-visible strings across the nine existing locales. ## Attribution - Include the MIT notice for the OpenChamber usage-provider implementation used as the reference. ## Validation - Server typecheck - UI typecheck - 7 targeted usage tests - Production UI build - Server build and npm package-content verification - Tauri release build without bundling - git diff checks <img width="484" height="444" alt="image" src="https://github.com/user-attachments/assets/69afbaf8-4c9a-4cb3-b316-e5788e416141" />
## Summary - add a wrap toggle beside the copy action in Markdown code block headers - default Markdown code blocks to wrapped lines for both plain and Shiki-highlighted output - preserve per-block wrap choices across async Markdown and syntax-highlight re-renders - add localized labels for the new Markdown code block wrap action ## Validation - npm run typecheck --workspace @codenomad/ui ## Notes - Left unrelated untracked .opencode/package-lock.json out of the commit.
## Summary - preserve and display detailed OpenCode errors when the session list cannot be loaded instead of failing silently - share error extraction and retry UI with per-session message loading while keeping each error scoped correctly - continue instance hydration after list failures and ignore stale overlapping list responses - show loading feedback during retries and localize the new states for every supported locale Closes #543. ## Validation - `bun test --conditions=browser packages/ui/src/stores/session-list-error.test.ts packages/ui/src/stores/session-tree.test.ts` - `npm run typecheck --workspace @codenomad/ui` - `npm run build --workspace @codenomad/ui` - `git diff --check upstream/dev...HEAD`
## Summary
- add an OpenCode update card to the OpenCode settings section
- show the installed version and a versioned update button only when npm
advertises a newer release
- run the upgrade through the generated OpenCode SDK against a ready
instance using the configured binary
- keep running instances uninterrupted; newly started instances use the
installed version
- localize loading, unavailable-instance, success, and failure states
across all supported locales
## Server behavior
- resolve the selected binary with the existing binary probe
- check opencode-ai/latest with a 10-second timeout and a five-minute
cache
- require a ready workspace using that exact binary before enabling the
upgrade
- invoke client.global.upgrade({ target }) rather than assembling an
OpenCode API request manually
- return stable error codes without exposing server details
## Validation
- pm run typecheck in packages/server
- pm run typecheck in packages/ui
- ode --import tsx --test src/opencode-update/service.test.ts
- pm run build in packages/ui
- pm run build in packages/server
- git diff --check
<img width="1034" height="688" alt="image"
src="https://github.com/user-attachments/assets/a71a01c4-d3e2-46f6-a333-a408c058255c"
/>
<img width="1018" height="360" alt="image"
src="https://github.com/user-attachments/assets/983a87a9-6103-48d7-8bcf-e10c5ce6a105"
/>
## Summary - raise PROJECT_SESSION_LIST_LIMIT from 1,000 to 10,000 - keep sessions visible for projects whose subagent activity exceeds the current 1,000-session cap - leave the existing one-shot project-scoped request behavior unchanged ## Context This is an urgent temporary mitigation for the regression described in #565 (comment). PR #565 replaced paginated loading with a single request capped at 1,000 sessions. Projects that exceed that cap can still have all sessions in OpenCode's database, but older sessions disappear from the CodeNomad UI. This PR deliberately does not solve the underlying scalability problem. The follow-up fix should restore proper paginated loading and remove reliance on a fixed high ceiling. ## Validation - npm run typecheck --workspace @codenomad/ui - node --test packages/ui/src/stores/session-pagination.test.ts (5 passed)
## Summary - turn the existing `Open` badge into a prominent action that returns directly to the running workspace - make the rest of every recent-folder row consistently start a new instance - remove the repeated already-open choice dialog and its unused translations ## Why Choosing between the existing workspace and another instance on every activation adds avoidable cognitive load. The two intentions are now represented directly by separate, stable targets: `Open` resumes existing work, while the folder row starts fresh. ## Details - force recent-row activations to request a new instance even during hydration or workspace-creation races - keep Browse, drag/drop, and clone fallback reuse behavior, but switch directly without a dialog - preserve the exact recent-folder alias when returning to an existing instance - keep native Enter/Space behavior on focused controls while preserving list and browse shortcuts - use project-specific accessible names and semantic button/focus tokens ## Validation - `npm run typecheck --workspace @codenomad/ui` - `npm run build --workspace @codenomad/ui` - `bun test --conditions=browser packages/ui/src/stores/session-tree.test.ts packages/ui/src/stores/session-pagination-model.test.ts` - `git diff --check upstream/dev...HEAD` - independent gatekeeper review: PASS
## Summary - launch the official CodeNomad WinGet upgrade from the upgrade-required notification in the local Windows Tauri client - keep non-Windows, Electron and development release actions on their existing release links - wait for WinGet without blocking the async runtime and propagate launch or non-zero exit failures back to the UI - open the official release URL when the native update action fails - preserve the same explicit update action in the live toast and notification history ## Update behavior - run an exact silent upgrade for the NeuralNomadsAI.CodeNomad package from the WinGet source - do not force a reinstall or downgrade - do not attempt to restart CodeNomad after a successful update; the installer owns application shutdown and replacement ## Validation - npm run typecheck in packages/ui - npm run build in packages/ui - cargo test in packages/tauri-app/src-tauri (20 tests)
…602) ## Summary - Follow up #578 by consolidating desktop persistence, restore reconciliation, lifecycle coordination, and regression coverage. - Preserve active drafts and attachments, request-scoped workspace ownership, deletion tombstones, renderer authority, and bounded shutdown behavior. - Fix the reported macOS cleanup failure with targeted BSD process queries and random-token-guarded process-group cleanup, without an unverified PID fallback. ## Platform hardening - Ignore development renderer origins in packaged Electron builds. - Preserve staged Tauri navigation authority and handle confirmed Windows session-end shutdown on the UI thread. - Bound workspace launch preflight, runtime startup, and health readiness. - Retain cleanup ownership after unexpected leaders exit and verify portable POSIX descendants by immutable identity or inherited launch token. - Add real Darwin-only process-group integration tests for macOS CI. ## Scope - 96 files changed. - 6,295 additions and 12,167 deletions, a net reduction of 5,872 lines from the merged implementation. - Consolidated duplicated tests while retaining focused race, durability, cleanup, and platform contracts. ## Validation - pm run typecheck - pm run typecheck --workspace @neuralnomads/codenomad - Electron native suite: 60 passed - Tauri suite: 49 passed - Focused server lifecycle/identity suite: 31 passed, 2 Darwin-only skipped on Windows - Focused UI restore/codec/reconciliation suite: 36 passed - Broader server suite: 59 passed, 3 platform skips - Broader UI suite: 97 passed, 1 skip; 2 Node 25 solid-toast loader failures reproduced on the merged baseline - git diff --check - Final limited gatekeeper: PASS for server/macOS, UI restore, and Electron/Tauri
## Summary - replace the inert Tauri macOS About handler with the native About dialog - add a conventional Help menu with About on Windows and Linux - expose Get Updates through each platform menu and through native About only where the platform supports an actionable link - run the native WinGet updater from the Windows Help menu, with GitHub Releases as the error fallback ## Platform behavior - macOS: CodeNomad > About CodeNomad opens the native dialog; CodeNomad > Get Updates... opens the latest GitHub release - Windows: Help > About CodeNomad opens the native dialog; Help > Get Updates... runs the WinGet updater asynchronously and opens the latest GitHub release only if WinGet cannot start, fails, or cannot be monitored - Linux: Help > About CodeNomad opens the native dialog with an actionable Get updates link; Help > Get Updates... also provides direct access - the displayed application version comes from Tauri package metadata Tauri ignores website metadata in the native macOS About panel and renders it as inert text on Windows, so website metadata is included only on Linux. ## Windows updater integration PR #597 is now merged. The Windows menu action invokes its install_stable_update command without blocking the Tauri menu event loop. macOS and Linux retain the direct Releases URL behavior. ## Validation - cargo test --manifest-path packages/tauri-app/src-tauri/Cargo.toml: 65 passed - cargo check --manifest-path packages/tauri-app/src-tauri/Cargo.toml: passed - focused failed-update fallback regression test - git diff --check: passed Closes #592
## Summary - flatten expanded session trees into a stable pre-order row projection - render only the viewport and buffer with the existing Solid virtua integration - preserve nested connectors, dynamic heights, search expansion, focus, active-session scrolling, and loading sentinel behavior ## Why Large or deeply expanded session trees currently mount every visible row. Status updates then affect a large DOM tree even when most sessions are far outside the sidebar viewport. This PR bounds mounted rows while leaving selection, deletion, filtering, and store semantics unchanged. ## Validation - UI TypeScript typecheck - 47 focused session-tree, pagination, and virtual-scroll tests - 1,000-session projection regression test - production Vite build - git diff whitespace check - final focused regression review - manual test
## Summary - keep the Status tab visible while provider usage refreshes in the background every minute - simplify Yolo controls, and rename the quota subsection to Usage - render the provider name prominently and each quota as one discreet text row plus a progress bar - show Included for subscription-model cost and remove the supplemental Codex credit-balance row - shorten the session-header Yolo badge while preserving its accessible label ## Provider safety - remove the Claude Pro/Max OAuth quota adapter because it's not usable anymore ## Refresh behavior - replace the suspense-aware provider resource with contained signal-based polling - preserve the last successful result during background refresh failures - ignore stale requests after the active session or provider changes ## Validation - 8 targeted server usage tests - server typecheck - UI typecheck - production UI build - git diff checks - focused regression review of polling, failures, and session changes Closes #605
## Summary - Persist server-owned Yolo state through OpenCode session metadata so it survives workspace and CodeNomad restarts. - Restore expanded and collapsed parent/subsession state from the desktop client snapshot. - Serialize CodeNomad metadata mutations so Yolo and worktree metadata cannot overwrite each other. ## Yolo persistence - Stores state under metadata.codenomad.yolo with enabled and rootSessionId fields. - Accepts a persisted marker only when it is owned by that session, preventing copied fork metadata from enabling an unrelated family. - Hydrates the project session tree before processing permission events and queues events while hydration is pending. - Keeps the server authoritative for family inheritance, permission replies, toggles, and cleanup. - Uses get/merge/update writes that preserve worktreeSlug and third-party metadata. - Serializes writes by session across CodeNomad instances and routes worktree slug updates through the same server-owned path. - Reconciles late ancestry and repeated root migrations without re-enabling a family after a concurrent disable. ## Expansion restoration - Captures bounded, deduplicated expanded session IDs per workspace in the local desktop snapshot. - Restores explicit expanded and collapsed state while preserving IDs that are temporarily unavailable during startup reconciliation. - Gives live user changes, deletions, and loaded-session authority precedence over preserved state. - Cleans expansion state when sessions or instances are removed. - Keeps the active child visible when loading legacy snapshots that predate expansion persistence. ## Validation - 39 focused server permission and metadata tests pass. - 66 focused client snapshot and restoration tests pass. - Server and UI TypeScript typechecks pass. - git diff --check passes. ## Limits - Project hydration follows the existing 10,000-session list ceiling. - Snapshot expansion persistence is capped at 256 session IDs. Closes #607 Depends on anomalyco/opencode#23068.
## Summary - do not mount SessionList while the temporary left drawer is floating and closed - keep the session-list error state mutually exclusive with virtualized rows - register focused visibility-policy tests in the PR workflow ## Root cause SUID constructs temporary Drawer children while open is false. This occurs both after restarting with a persisted closed panel and when narrowing the window into mobile mode, which forces the left panel to become unpinned and closed. SessionSidebar then mounted the virtua Virtualizer in a detached staging document. virtua resolves ResizeObserver through ownerDocument.defaultView, which is null for that document. The failure is timing-dependent: if session hydration publishes rows while that closed mobile Drawer is detached, the synchronous render exception escapes through setSessionPage and is caught by fetchSessions as if the successful API request had failed. Opening the panel later therefore reveals an empty list or the misleading Unable to load sessions error. If hydration finishes under a different drawer lifecycle, the bug does not appear. Because the error UI and virtualized rows were both mounted, Retry cleared the error and immediately hit the same poisoned lifecycle again. ## Behavior Session fetching and startup restore continue while the panel is closed. The virtualized DOM is created only after the panel is open or pinned. Genuine list errors dispose the rows; Retry can then mount a clean virtualizer after succeeding. ## Reproduction 1. Narrow the window until CodeNomad enters mobile mode and the left panel can no longer remain pinned. 2. Leave the sessions panel closed while sessions hydrate, or restart in that state. 3. Open the left panel. 4. Before this fix, the list may be empty or show Unable to load sessions with a ResizeObserver null error. ## Validation - 19 focused session visibility, tree, and pagination tests - UI TypeScript typecheck - production Vite build - full Windows Tauri release build - NSIS installer bundle - regression test included in PR CI
## Summary - add an Open new instance hover tooltip to local workspace rows - translate the tooltip across all supported locales - reuse the existing native title pattern without new styling or dependencies ## Validation - npm run typecheck --workspace @codenomad/ui - npm run build --workspace @codenomad/ui - git diff --check Closes #608
## Summary - reorganize Settings into focused General, Chat, Notifications, Speech, Remote Access, OpenCode, Providers, SideCars, Config files, Advanced, and Info sections - add independent expanded, collapsed, and hidden controls for Thinking, Tool output, Diagnostics, Tool inputs, and Token usage - embed the shared Provider manager directly in Settings while retaining the model-selector dialog entry point - move environment, session cleanup, sub-agent idle markers, and Tauri transport controls into Advanced ## Reliability - scope provider loading and authentication operations to the current ready instance - clean up OAuth callbacks and popups when the instance or view changes - preserve failed disconnect state and prevent conflicting provider operations - improve focus management and live status/error announcements ## Validation - npm run typecheck - npm run build:ui - 144 runnable UI tests from the settings review cycle - git diff --check - independent settings and accessibility gatekeepers: SHIP Closes #609
Summary - Add a typed right-panel registry for native tabs and Status sections. - Let users show, hide, and reorder right-panel tabs and Status subpanels, including drag/drop and Up/Down controls. - Persist customization through the existing client layout preference store. Why - Progresses #193 with the smallest reviewable UI customization layer first. - Keeps plugin manifest/lifecycle loading separate from this PR. Validation - npm exec --no -- tsx --test packages/ui/src/components/instance/shell/right-panel/registry.test.ts - npm run typecheck --workspace @codenomad/ui - npm run build --workspace @codenomad/ui - git diff --check
## Summary - Populate the remote login username input with the configured default value instead of displaying it only as a placeholder. - Disable mobile capitalization, autocorrection, and spellchecking for username and password inputs. - Add a route-level regression test for the rendered username value and mobile credential attributes. ## Validation - node --import tsx --test packages/server/src/server/routes/auth.test.ts - npm run typecheck --workspace @neuralnomads/codenomad - node packages/server/scripts/copy-auth-pages.mjs Closes #616
## Summary - place CodeNomad selection actions below the final selected line on touch-only devices - keep desktop and hybrid fine-pointer positioning unchanged - fall back to the message stream's top edge when bottom placement would be clipped ## Validation - 146 UI Node tests - npm run typecheck - npm run build --workspace @codenomad/ui - git diff --check Closes #598
## Summary - count only fuzzy-qualified paths toward the 8000 workspace search candidate cap - scope the bounded cache to the current query and entry type - stop recursive directory-link cycles without hiding legitimate alias paths - expand the @file picker to its parent width and horizontally scroll long paths ## Validation - 6 focused filesystem search/cache tests pass - server typecheck passes - workspace UI/Electron typecheck passes - UI production build passes - gatekeeper round 3: zero findings - full server suite: 241 pass, 3 skip; the unrelated existing Windows git-worktrees fixture fails to parse its mocked branch output Closes #503
## Summary - Preserve the original V2 source when permission.updated changes an already-pending request, so approval continues through the V2 reply API instead of the legacy endpoint. - Keep standalone permission.updated events as valid legacy requests for older supported OpenCode binaries. - Continue suppressing delayed post-reply updates through the existing replied-request tombstone, preventing an approved request from reappearing. ## Reproduced failure The regression test fails on dev before the fix: permission.v2.asked followed by permission.updated changes the request source to legacy, so approval calls the legacy endpoint and never reaches the V2 reply API. ## Compatibility coverage - V2 request plus permission.updated replies through V2 - delayed permission.updated after a successful local reply does not re-enter the queue - standalone legacy permission.updated remains visible and replies through legacy ## Scope This PR intentionally does not add speculative timeout, reconnect, or modal-rendering changes. It only fixes the reproduced source-routing bug and preserves verified legacy behavior. ## Validation - npm run typecheck - npm run build --workspace @codenomad/ui - 144 existing runnable UI tests passed - browser integration tests passed, including all permission lifecycle regressions Closes #566
## Summary - prevent Linux workspace startup from failing when process identity discovery exceeds its one-second command deadline - capture the launched process and its existing process-group members without spawning helper commands for every `/proc` entry - preserve the leader-exit cleanup guarantees introduced by #602 - read immutable Linux process start ticks correctly and tolerate multiline task names ## Root cause Workspace startup captures an immutable process identity before accepting the OpenCode runtime. The Linux implementation scanned every process and launched several `cat`, `cut`, `sed`, `basename`, and `dirname` helpers per entry. On the affected Mint host, that synchronous shell command exceeded its one-second timeout. Identity capture then failed closed and stopped the newly launched OpenCode process; cleanup retried the same expensive probes and produced the repeated `spawnSync sh ETIMEDOUT` errors. The identity parser also used `$20`, which POSIX shells interpret as `$2` followed by `0`, rather than the twentieth positional field. This did not cause the startup timeout but weakened immutable process matching and is corrected here. ## Safety - launch discovery still retains every already-started member of the observed process group - guarded cleanup remains identity- and launch-token-based; no unverified PID fallback is introduced - WSL, macOS, and Windows paths retain their existing platform-specific probes ## Validation - server TypeScript typecheck - focused process identity and runtime suite: 23 passed, 1 Linux-only test skipped on Windows - real WSL `/proc` probe: correct start identity, 2 group members, 4 ms - broader workspace suite: 79 passed, 3 platform skips - `git diff --check` The broader workspace run still has the unrelated existing Windows fixture failure in `git-worktrees.test.ts` (`undefined` instead of `main`). Closes #624
…e client factory (#630) ## Summary PR #561 introduced `createInstanceClient` so server modules would stop hand-assembling the OpenCode loopback URL and use the generated SDK client. Three call sites adopted it; the background-process completion prompt was the remaining holdout — it still hand-built `http://127.0.0.1:{port}/session/{id}/prompt_async` with manual auth/content-type wiring. This PR routes it through the factory + `client.session.promptAsync`, and consolidates the loopback host constant. ## Why - **Consistency** — the last hand-built OpenCode-instance loopback URL (explicitly flagged in #561 as a future adoption target) now uses the same version-correct SDK path as the permission replier, yolo metadata, and the opencode updater. - **Correctness** — the old call sent no directory at all; `notify.directory` was stored but never used (dead data). The factory now scopes the prompt to the session's own directory, which for a worktree/subdirectory session is the correct project context (a normal session coincides with the workspace root, so no change there). ## What changed **Migration** — `background-processes/manager.ts`: `sendCompletionPrompt` → `createInstanceClient(...)` + `client.session.promptAsync({sessionID, parts:[{type:"text", text, synthetic:true}]}, {throwOnError:true})`. Synthetic `<system-message>` text + `synthetic:true` preserved. Side effect: the call now carries the factory's 10s loopback timeout (the old fetch had none; the failure is swallowed + warn-logged, so finalization is unaffected). **Factory** — `workspaces/instance-client.ts`: new optional `directory` override in `InstanceClientOptions` (defaults to workspace root); adopts shared `LOOPBACK_HOST`. Fetch wrapper unchanged from `dev`. **Consolidation** — new `workspaces/loopback.ts` exporting `LOOPBACK_HOST = "127.0.0.1"`, adopted by `instance-client.ts`, `instance-events.ts`, and the workspace manager's health + TCP probes (`manager.ts`). (Other `127.0.0.1` uses — remote-access proxy, sidecars — are different concerns, left alone.) **Tests** - `workspaces/instance-client.test.ts` (8 cases): null-when-no-port, loopback host/port targeting, auth header attach/omit, directory scoping present/absent, explicit directory override, timeout aborts stuck call. - `background-processes/manager.test.ts` (2 cases, first test for this manager): drives the real lifecycle (spawn + exit) against a mocked transport; asserts the `prompt_async` POST URL/body/authorization/`x-opencode-directory` (= session dir, distinct from workspace root), and that a failed prompt is swallowed + warn-logged without aborting finalization. ## Behavior parity | Aspect | Before | After | |---|---|---| | Route / body | `/session/{id}/prompt_async`, `{parts:[{type,text,synthetic}]}` | **same** (via SDK) | | Auth header | manual | via factory | | Directory scoping | none | session's `notify.directory` | | Loopback timeout | none | 10s (factory default) | | Error handling | throw on non-2xx → caught + warn-logged | `throwOnError` → caught + warn-logged (**same**) | ## Testing 10 new tests pass. The background-process integration test is stable 12/12 under `--test-concurrency=4` and is mutation-killed if the `directory` override or `throwOnError` is removed. Full server suite: 255 pass / 1 fail — the single failure is pre-existing (`git-clone.test.ts` Chinese-locale git message, confirmed failing identically on `dev`). Server typecheck clean. ## Risk / rollback Additive refactor; the factory is production-proven by #561's consumers. Rollback = revert the single commit. ## Notes - Per the file-length guideline: `packages/server/src/background-processes/manager.ts` is ~684 lines (above the 500 warn threshold, under the 800 limit); this PR shrank it by ~16 lines net.
Summary - Add a typed right-panel plugin manifest loader. - Let bundled plugin manifests contribute right-panel tabs and Status sections through the existing registry. - Run onLoad/onUnload lifecycle hooks when the RightPanel mounts/unmounts. - Keep the manifest list explicit and empty by default; no arbitrary external code loading in this PR. Stacking - Base branch: feat/right-panel-customization. - Stacked on #615. After #615 merges, retarget this PR to dev. Validation - npm exec --no -- tsx --test packages/ui/src/components/instance/shell/right-panel/plugin-manifest.test.ts packages/ui/src/components/instance/shell/right-panel/registry.test.ts - npm run typecheck --workspace @codenomad/ui - git diff --check - npm run build --workspace @codenomad/ui - final gatekeeper pass: no findings
## Summary - show the file or directory name first in each @ picker result - show the immediate parent and complete root-aligned directory on separate lines - preserve horizontal inspection for long directories and reset it when results change - retain the complete path for assistive technology without changing selection values - run the focused path-splitting regression test in PR CI ## Context PR #623 replaced end truncation with horizontal scrolling. This follow-up keeps that behavior while moving the relevant filename and parent to the start of each result, matching the CLI-style expectation from the report. ## Validation - workspace typecheck passes - UI production build passes - focused path test passes - git diff check passes - Gatekeeper round 3: zero findings Closes #603
## Summary - retry an ephemeral port when an automatic listener receives Windows `EACCES` - keep explicitly configured ports strict - keep non-Windows `EACCES` behavior unchanged ## Context CodeNomad already treats 9898 and 9899 as preferred rather than mandatory ports. The fallback previously handled only `EADDRINUSE`, so a Windows port reservation could terminate Tauri startup before the operating system was asked for an available port. The specific WinNAT attribution in the issue remains unverified, but the observable startup failure is fixed regardless of which Windows reservation produced `EACCES`. ## Validation - focused listener retry test passes - server typecheck passes - server suite: 256 passed, 4 skipped, 1 pre-existing Windows-only `git-worktrees.test.ts` fixture failure - autonomous gatekeeper review will be published as a PR comment after opening Fixes #627
## Summary - add the standard Cmd+Q accelerator to the Tauri macOS Quit menu item - keep the existing custom quit event so shutdown still flushes renderer state and stops the managed CLI - leave Windows Ctrl+W tab closing and Alt+F4 app closing unchanged ## Why The macOS menu used a plain text item with no accelerator. Tauri's predefined native Quit item would restore Cmd+Q, but it bypasses the custom menu event and could skip CodeNomad's coordinated shutdown. A regular menu item with CmdOrCtrl+Q restores the shortcut while preserving cleanup. ## Validation - 84 Tauri tests pass - cargo check --locked passes - ustfmt check passes for main.rs - git diff --check passes - Gatekeeper review: zero findings - macOS runtime unavailable; accelerator and shutdown routing verified against the pinned Tauri/muda behavior Closes #590
## Summary - record normal messages and shell commands in composer history before dispatch - keep failed prompts recoverable with the existing history navigation - avoid duplicate history writes after successful sends ## Validation - pm run typecheck --workspace @codenomad/ui - pm run build --workspace @codenomad/ui - px tsx --test packages/ui/src/components/prompt-input/submitPrompt.test.ts packages/ui/src/stores/app-session-prompt-hydration.test.ts - two independent zero-finding reviews of the final diff Closes #507
## Summary - validate the authenticated OpenCode process configuration before publishing a workspace as ready - stop and remove instances that report invalid configuration - show localized configuration diagnostics, affected paths, validation issues, and typo suggestions in the existing launch-error dialog ## Existing coverage PR #195 already handles binary spawn and early-exit failures. PR #582 surfaces session-list loading failures. This change closes the remaining case where OpenCode reports healthy while configuration-dependent endpoints fail. ## Validation - `bun test ./packages/server/src/workspaces/manager.test.ts` - `node --import tsx --test packages/ui/src/lib/launch-errors.test.ts` - server and UI typechecks - UI build - manual reproduction with OpenCode 1.18.5 - independent Gatekeeper reviews: PASS Closes #508
) ## Problem On mobile, backgrounding CodeNomad while the AI is working suspends JS execution. The SSE connection eventually times out (~45s), and any events that arrived while suspended (a tool call finishing, a message completing) are lost. Returning to the app could leave the UI permanently stuck showing "running" even though the work had actually finished on the server — the only way out was aborting the request. ## Fix **1. `useForegroundRefresh` hook** (`packages/ui/src/lib/hooks/use-foreground-refresh.ts`) Subscribes to the SSE transport's `disconnected -> connected` transition (not `visibilitychange`) and re-fetches session status + force-reloads messages only when a reconnect follows a real disconnect. An earlier attempt triggered on `visibilitychange` after a fixed delay, but that force-reloaded even when the connection had stayed alive the whole time, clearing in-flight "sending" state and making it look like the AI never responded to a message the user had just sent. Also fixes a latent bug in `server-events.ts`: `onPing` could fire for a stale SSE generation after a reconnect, sending a pong tied to the wrong connection. Guarded with the existing `connectGeneration` counter. **2. Two `hydrateMessages` bugs** found while testing the hook against real mobile background/foreground cycles (`packages/ui/src/stores/message-v2/instance-store.ts`): - **Unnecessary full re-render on every reload.** The force-reload path always bumped every message's revision regardless of whether the server returned identical content, invalidating render caches and re-rendering the entire visible session on every reconnect. Several reconnects in a row produced perceptible lag. Fixed by only bumping revision when a message's parts or status actually changed — reload time dropped from ~280-320ms to ~10ms for unchanged content. - **Duplicated message bubble on a race.** If the user sends a message right as a reconnect happens, the optimistic "sending" bubble (client-side temp id) isn't in the REST snapshot yet and was silently dropped from the visible list — without being deleted from the store. When the real SSE echo for it later arrived, the code could no longer find it to swap cleanly, so it created a new record instead, and a subsequent reload could leave both the orphaned bubble and the new one visible at once. Fixed by preserving pending "sending" messages across a reload until they're actually confirmed. ## Validation - `npm run typecheck` (workspace `@codenomad/ui`): clean - `node --test src/stores/message-v2/instance-store.test.ts`: 4/4 pass (2 new tests covering the duplicate-message race) - Full UI test suite run file-by-file: 57/66 pass; the 9 that don't fail identically on a pristine `upstream/dev` checkout with none of this PR's changes applied (pre-existing solid-toast SSR issue in the test environment, unrelated to this change) - `npm run build` (workspace `@codenomad/ui`): succeeds - Manually verified over ~1.7 hours of real mobile background/foreground cycles (14s glances up to two 40+ minute backgrounds) — refresh fires only on genuine reconnects, completes in ~200-450ms, no stuck "running" state and no duplicated messages on return --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
…ion mutation (#644) ## Summary Follow-up to #639. On mobile, validating the reconnect refresh from #639 surfaced a bandwidth regression: the active session's initial message-load effect re-fires on **every** mutation of the reactive sessions map, and one of those re-fires issues a redundant `force:false` fetch that races the authoritative `force:true` reload for the same — often very large — session. ## What happens (validated live on a real mobile device, 2 workspaces open) The active-session effect in `session-view.tsx` reads the whole session object: ```ts const session = () => props.activeSessions.get(props.sessionId) ``` Reading `session()` inside the `createEffect` subscribes it to the entire sessions map. During a foreground/reconnect refresh, each of the ~10 concurrent `loadMessages()` completions calls `setSessions`, re-firing the effect **20-40× per reconnect** (21 firings logged within a single millisecond). Most re-runs no-op via the `isLoading && !force` guard, but the firing that lands in the open window (`isLoading:false` + freshly invalidated flag) issues a real network fetch. On a ~6,578-message active session that produced a duplicate full download racing the authoritative reload: ``` loadMessages HTTP fetch (6578 msgs) force:false (5565ms) ← from this effect loadMessages HTTP fetch (6578 msgs) force:true (7802ms) ← authoritative reload ``` both in flight ~300ms apart, saturating the mobile link (sidebar fetches degraded from 1-4s to 10-11s; bounded retries could exhaust against `FOREGROUND_REFRESH_TIMEOUT_MS`). ## Fix Derive the active session id through a value-diffed `createMemo` and drive the effect from that memo instead of the full session object. The memo only notifies downstream when the id string actually changes, collapsing the storm to a single run per real session change while preserving existing behavior (load on activation and on session switch). This intentionally does **not** touch the authoritative `force:true` reload contract guarded by `session-request-authority.test.ts` — a generic in-flight dedupe was tried and correctly rejected by that test (force:true must always re-fetch), so narrowing the effect's reactive dependency is the right lever. ## Validation - `tsc --noEmit` clean - UI tests: 34/34 (group 1), 18/18 (authoritative group 2, `--conditions=browser --test-force-exit`, incl. `session-request-authority`) - Production UI build succeeds Root cause was captured live via a portable debug overlay (branch `debug/mobile-overlay`). Context: #639 (comment).
## Summary - sanitize raw HTML tokens in assistant Markdown with the existing allowlist - prevent dropped tags such as `<style>` from consuming or altering the rest of a response - add a focused regression for the malformed inline-code sequence reported in #645 ## Validation - `node --conditions=browser --import tsx --test --test-force-exit packages/ui/src/lib/markdown.test.ts` - `npm run typecheck --workspace @codenomad/ui` - `npm run build --workspace @codenomad/ui` - `git diff --check` Closes #645
## Summary - identify remote CodeNomad webviews explicitly as Tauri hosts - grant remote-* windows the minimum native notification permissions - keep dialogs, opener access, menus, and other desktop privileges restricted to the main window ## Cause Remote windows were marked with the remote window context but not with the Tauri runtime host. The UI therefore selected the Web Notification API inside WebView2, where requesting permission had no effect. Those windows also did not match a Tauri capability that allowed notification plugin commands. ## Implementation The initialization script now sets both runtime host and window context before remote UI scripts run. A dedicated capability permits only permission checks, permission requests, and notification delivery for configured HTTP/HTTPS remote origins. ## Validation - cargo test --manifest-path packages/tauri-app/src-tauri/Cargo.toml: 85 passed - npm run typecheck --workspace @codenomad/ui - npm run build:ui - git diff --check - two independent review rounds: zero findings after least-privilege cleanup Fixes #640
## Summary - add project folder, terminal, and explicit editor actions to the command palette and native File menus - replace the Files-row copy button with desktop context actions while preserving internal file navigation and web/remote copy fallback - resolve trusted workspace roots through the authenticated local server and enforce canonical path containment in Electron and Tauri - use safe platform launch policies, including Windows Open/Edit/Open With handling and executable rejection on Unix - localize all new UI actions across supported locales ## Validation - npm run typecheck - npm run test:native --workspace @neuralnomads/codenomad-electron-app (122 passed) - cargo test (86 passed) - npm run build - git diff --check - independent security, platform, and UX reviews: zero findings Closes #570
## Summary - add per-provider model visibility controls to Settings > Providers and the floating provider dialog - persist exact hidden model IDs as CodeNomad presentation preferences - keep large plugin catalogs manageable with search and Show all / Hide all actions ## Behavior - all current and newly reported models remain visible by default - unchecked models are hidden only from model picker lists - active/default models remain usable even when hidden - favorites remain persisted and return when their models are shown again - the same management UI is available from Settings and the floating model-picker dialog This intentionally mirrors OpenChamber/OpenCode Desktop client-side hiding. It does not rewrite OpenCode configuration or change model execution/default semantics. ## Validation - npm run typecheck - node --import tsx --test packages/ui/src/lib/model-visibility.test.ts - npm run build --workspace @codenomad/ui - Gatekeeper reviews: PASS Closes #636
## Summary - size provider card actions to their content on desktop - prevent the global full-width selector button rule from overflowing the Provider Authentication dialog - retain full-width stacked actions in the existing narrow/mobile layout ## Validation - `npm run typecheck --workspace @codenomad/ui` - `npm run build --workspace @codenomad/ui` - `git diff --check` Follow-up to #637. Closes #650.
The Star History chart was broken because it depended on the GitHub stargazer API. Switch it to an alternative provider using the same data source so the community chart renders correctly. No API token required. Co-authored-by: OctoBored <212877535+OctoBored@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Highlights
Desktop continuity and native integration
Sessions and messaging
Providers, speech, and settings
Reliability and platform fixes
Packaging and distribution
.debinstaller for Ubuntu 24.04 and a portable Electron.tar.gzfallback. Obsolete AppImage, RPM, Flatpak, and zip variants are no longer published.Thanks for contributions
Thanks to @shantur, @pascalandr, @JDis03, @heunghingwan, @VooDisss, @detain, @bluelovers, @jollyxenon, and @OctoBored.
GitHub will append the complete merged pull request and contributor list to the generated release notes.
Full Changelog: v0.18.0...v0.19.0