Commit graph

1321 commits

Author SHA1 Message Date
Pascal André
269cff64ee
fix(server): prevent Linux workspace launch identity timeouts (#625)
## 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
2026-07-26 20:22:49 +01:00
Pascal André
70c9548f93
fix(permissions): ignore stale permission updates (#621)
## 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
2026-07-26 17:57:48 +01:00
Pascal André
9d47f73ea7
fix(files): search large workspaces beyond candidate cap (#623)
## 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
2026-07-26 17:51:56 +01:00
Pascal André
b1bb8a723a
fix(ui): avoid mobile selection action overlap (#622)
## 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
2026-07-26 17:51:30 +01:00
Pascal André
b81044ffa2
fix(auth): submit the displayed default username (#620)
## 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
2026-07-26 17:50:45 +01:00
Pascal André
30563a22e2
feat(ui): add right panel customization (#615)
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
2026-07-26 17:50:28 +01:00
Pascal André
25af76c243
feat(settings): reorganize settings and add chat block visibility (#614)
## 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
2026-07-26 17:50:08 +01:00
Pascal André
d74a8a39f1
fix(ui): label new local instance action (#613)
## 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
2026-07-20 16:13:42 +01:00
Pascal André
4c50829da0
fix(ui): defer session virtualization for closed left panel (#612)
## 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
2026-07-19 17:19:32 +01:00
Pascal André
24a26807e0
feat(restore): persist Yolo and session expansion state (#611)
## 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.
2026-07-19 17:16:56 +01:00
Pascal André
47ae6626dd
fix(ui): polish provider usage status panel ( and Yolo panel ) (#610)
## 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
2026-07-19 17:16:24 +01:00
Pascal André
8f6aa44b15
perf(ui): virtualize nested session list (#586)
## 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
2026-07-17 22:17:35 +01:00
Pascal André
b766b76a69
fix(tauri): add native About and Help menus (#596)
## 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
2026-07-17 22:17:26 +01:00
Pascal André
0bab9e3438
fix(restore): simplify persistence and harden cross-platform cleanup (#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
2026-07-17 22:17:15 +01:00
Pascal André
e586ea39f4
feat(windows): update CodeNomad from the in-app notification (#597)
## 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)
2026-07-17 10:51:14 +01:00
Pascal André
7584ccfe29
feat(ui): make open workspace action explicit (#600)
## 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
2026-07-17 10:50:32 +01:00
Pascal André
18564d4094
fix(ui): temporarily raise project session list ceiling (#604)
## 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
https://github.com/NeuralNomadsAI/CodeNomad/pull/565#issuecomment-4990950140.

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)
2026-07-17 10:49:51 +01:00
Pascal André
e169c0aaa1
feat(settings): add OpenCode update action (#585)
## 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"
/>
2026-07-17 10:48:58 +01:00
Pascal André
f6cf6c5352
fix(ui): surface session loading failures (#582)
## 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`
2026-07-17 10:48:37 +01:00
Shantur Rathore
40cee071a3
feat(ui): add line wrap toggle to markdown code blocks (#599)
## 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.
2026-07-15 10:34:43 +01:00
Pascal André
709557d12d
feat(usage): show provider quota in session status (#584)
## 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"
/>
2026-07-15 10:25:00 +01:00
Pascal André
e815eba872
fix(ui): refresh worktrees from OpenCode ready events (#583)
## 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
https://github.com/NeuralNomadsAI/CodeNomad/pull/516#issuecomment-4658638464.

## Validation

- `bun test --conditions browser packages/ui/src` (`126` passing)
- `npm run typecheck --workspace @codenomad/ui`
- `git diff --check`
2026-07-15 10:24:00 +01:00
Pascal André
f31d8c17f6
fix(ui): persist environment variable removal (#581)
## 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
2026-07-15 10:22:42 +01:00
Pascal André
1efe5e91d1
feat(desktop): persist and restore UI state across restarts (#578)
## 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.
2026-07-15 10:22:23 +01:00
Pascal André
b1d6421b0d
feat(ui): focus messages after prompt submission (#577)
## 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.
2026-07-15 10:21:58 +01:00
Yao Jianxuan
07e8ce6810
feat(ui): support bracket math delimiters (#588)
## 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.
2026-07-14 14:00:26 +02:00
heunghingwan
f900af1b4d
fix(ui): cancel pending microphone acquisition on early release (#550) (#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
2026-07-13 20:12:24 +02:00
heunghingwan
483fac2e07
feat(speech): separate STT/TTS provider configuration (#579)
## 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)
2026-07-13 13:27:50 +02:00
bluelovers
f34e807108
feat: detect realpath when open workspace (#486)
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>
2026-07-12 23:42:16 +02:00
Shantur Rathore
c8d4a5099a
build: publish deb and portable tar.gz Linux artifacts (#492)
## 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>
2026-07-12 19:29:33 +02:00
VooDisss
56d1e00f2f
PR: Extend delete overlays and clean companion parts during tool deletion (#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>
2026-07-12 18:40:28 +02:00
Joe Huss
34706228bb
feat(ui): implement recursive nested session expansion (#478)
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>
2026-07-12 01:18:31 +02:00
heunghingwan
ca06bd99d7
feat(yolo): move permission auto-accept (Yolo) to the server (#561)
## 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>
2026-07-08 21:13:44 +02:00
Pascal André
bce975d940
fix(ui): stabilize strict follow autoscroll (#576)
## 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
2026-07-08 18:46:55 +02:00
Dark
275770b51b
fix(ui): remove scope=project from session list requests (#572)
## Problem

PR #565 (commit e29c3a08) 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>
2026-07-08 17:56:12 +02:00
Dark
e8623d3132
fix(ui): SSE connection status indicators for mobile network drops (#549)
## 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>
2026-06-27 20:39:13 +02:00
Shantur Rathore
31414521f6 Min server 0.18.0 2026-06-21 20:09:49 +01:00
Shantur Rathore
353d8d2eae
fix(ui): keep message-part tool output auto-scroll synced (#567)
## Summary
- Pass MessagePart render notifications into ToolCall as
onContentRendered.
- Ensures streaming bash output rendered through the generic MessagePart
path notifies the outer VirtualFollowList when content grows.

## Validation
- npm run typecheck --workspace @codenomad/ui
2026-06-21 18:09:03 +01:00
Shantur Rathore
e29c3a085c
fix(ui): scope project session list requests (#565)
## Summary
- update @opencode-ai/sdk to 1.17.8
- load session lists through client.session.list with scope=project
- use a single high-limit project request because this endpoint does not
support cursor pagination

## Validation
- npm run typecheck --workspace packages/ui

## Notes
- Existing untracked .opencode/package-lock.json was left untouched.
2026-06-19 17:42:12 +01:00
Shantur Rathore
fdc0c92641 Bump version to 0.18.0 2026-06-19 13:55:00 +01:00
Shantur Rathore
cf661c6b46
Improve compact mobile UI controls (#564)
## Summary
- Refine narrow session header controls, including overflow menu
placement and centered command/context controls.
- Autosize the narrow prompt input while preserving desktop/hybrid
behavior and avoiding mobile keyboard refocus flicker.
- Improve tab bar behavior at small widths by preventing action button
shrink and disabling drag-reorder on touch-only devices.
- Includes earlier branch changes for message scroll control collapse
and square-corner styling documentation.

## Validation
- npm run typecheck --workspace @codenomad/ui
2026-06-19 13:52:26 +01:00
Shantur Rathore
c5056c37c2
feat(ui): Add tool level expansion settings. (#563)
## Summary
- add a central tool-call registry for renderer lookup and configurable
expansion defaults
- add Appearance presets for transcript detail: Minimal, Balanced,
Detailed, and Everything
- allow per-item customization for thinking, known tool calls, and Other
tools with responsive settings layout
- persist toolCallExpansionDefaults while keeping legacy expansion
preferences synchronized for command compatibility

## Validation
- npm run typecheck --workspace @codenomad/ui
2026-06-19 10:52:26 +01:00
Shantur Rathore
1c9eddd698
WIP: Compact mode message UI redesign (#557)
## Summary
This branch implements a compact-mode redesign for the message
stream, focused on making dense conversations easier to scan while
keeping important actions available.

### Message Stream Redesign
- Compacts user, assistant, reasoning, step, compaction, and tool-call
surfaces into tighter header/body layouts.
- Moves message time next to the speaker / agent-model identity instead
of using a separate right-side timing cluster.
- Restores the pre-branch user message background so user prompts remain
visually distinct.
- Aligns right-edge action positioning across text messages, reasoning
blocks, tool calls, compaction rows, and usage rows.

### Action Model
- Consolidates action controls across message parts.
- Keeps only copy and speak visible inline on wide layouts.
- Moves selection, delete, delete-up-to, fork, revert, tool input
visibility, and task-session navigation into overflow menus.
- On narrow layouts, hides inline actions and exposes all available
actions through overflow.
- Replaces native selection checkboxes in message surfaces with
overflow-menu selection actions using lucide square/check-square icons.

### Tool Calls and Bash Output
- Merges tool-call header chrome into the tool call itself instead of
wrapping it with an extra external header.
- Moves the tool status icon next to the tool title instead of pinning
it to the far right.
- Refines input/output spacing for expanded tool calls.
- Keeps streaming bash output pinned and improves final ANSI output
rendering/restoration.

### Thinking / Reasoning Blocks
- Compacts thinking blocks into a header-first treatment.
- Uses extracted reasoning titles or duration fallback labels for
compact scanning.
- Keeps reasoning copy/speak available while moving
secondary/destructive actions to overflow.

### Styling and i18n
- Updates message and tool-call CSS for compact spacing, right-edge
alignment, action button consistency, and surface cleanup.
- Adds localized strings for new reasoning labels/actions across
supported message locales.

## Validation
- npm run typecheck --workspace @codenomad/ui
- git diff --check
2026-06-18 21:37:25 +01:00
Pascal André
01ced5a289
fix: stabilize hold-mode assistant autoscroll (#533)
## Summary
- Keep Hold mode latched until the user manually reaches the true
bottom.
- Restrict Hold targets to user-readable Assistant answer text.
- Exclude tool/status/output and reflection/reasoning blocks from Hold
target eligibility.

## Validation
- node --test packages/ui/src/components/virtual-follow-behavior.test.ts
- npm run typecheck --workspace @codenomad/ui
- npm run build --workspace @codenomad/ui
2026-06-18 21:18:32 +01:00
Shantur Rathore
8bd7bc3d76
fix(ui): handle session created SSE events (#562)
## Summary
- Route `session.created` SSE events through the existing session update
handler.
- Prepend newly-created root sessions to the session list pagination ids
so they appear in the sidebar immediately.

Fixes #560

## Validation
- `npm run typecheck --workspace packages/ui`
2026-06-18 21:08:36 +01:00
Shantur Rathore
8b5ffa1905
fix(ui): keep streaming bash output pinned (#556)
## Summary
- notify the existing virtual follow-list path after streaming bash ANSI
output updates
- restore the inner bash output scroll immediately after direct pre
updates
- apply the same coalesced notification pattern to final ANSI output

## Validation
- npm run typecheck --workspace @codenomad/ui
2026-06-16 15:56:41 +01:00
Aayurt Shrestha
9c8baf86ca
Bugfix/tab close bug (#552)
#545 Bug fix

https://github.com/NeuralNomadsAI/CodeNomad/issues/545#issuecomment-4689389224

---------

Co-authored-by: Shantur Rathore <i@shantur.com>
2026-06-15 09:40:39 +01:00
Pascal André
5c91c41181
fix: wire Winget automation into release pipeline (#551)
## Summary
- wire Winget submission into the stable release pipeline instead of
relying on a separate `release.published` workflow
- keep the existing release asset resolution and Komac-backed submission
flow
- document the new trigger model and manual fallback path

## Validation
- `git diff --check origin/dev...HEAD`
- `node --check scripts/winget/resolve-release-asset.cjs`
- `node scripts/winget/resolve-release-asset.cjs --help`
- live release metadata and asset-resolution dry-run against stable
`v0.17.0`

## Notes
- this fixes the case where a release created by GitHub Actions with the
default `GITHUB_TOKEN` does not fan out into a second workflow run
- assumes the existing Winget repo secret/variables remain configured
- refs #462
2026-06-13 17:51:24 +01:00
Dark
14dfd3e371
fix(ui): throttle SSE part deltas and prevent stale delta text duplication (#536)
## Problem

Streaming responses send many `message.part.delta` events per second.
Each event triggers a Solid store mutation + re-render, causing:

1. **Mobile freezes** — KaTeX/highlight.js re-renders on every delta
block the main thread
2. **Text duplication** — When `message.part.updated` replaces a part
with the server's complete state, previously enqueued deltas would flush
AFTER the replacement, concatenating stale text onto the fresh part

## Solution

### 1. Delta throttling (50ms buffer)
Accumulate deltas in a buffer and flush every 50ms instead of updating
on every event. This batches Solid mutations and cuts main-thread
blocking during streaming.

### 2. Stale delta cleanup
When a full `message.part.updated` arrives, clear any pending deltas for
that part before applying the update. The update already contains the
complete server state, so accumulated deltas are stale.

## Changes

- `packages/ui/src/stores/session-events.ts`: Added `enqueueDelta`,
`flushDeltas`, `clearPendingDeltasForPart`; updated
`handleMessagePartDelta` and `handleMessageUpdate`

## Verification

- Build passes: `npm run build:ui`
- Tested on mobile — eliminates visible freezes during long streaming
responses
- No duplicate text observed at end of assistant messages after part
updates

---------
2026-06-13 16:42:17 +02:00
Dark
17547a15d8
docs: add CONTRIBUTING.md guide for new contributors (#484)
## Summary
Adds a CONTRIBUTING.md file to help new contributors get started.

## Contents
- Quick start setup
- Finding issues to work on (label guide)
- Branch naming conventions
- Development workflow (fork, branch, commit, PR)
- Project structure overview with key file paths
- Styling conventions and file size guidelines
- i18n workflow for adding translations
- Code principles (KISS, DRY, single responsibility)

This gives new contributors everything they need in one place to start
contributing effectively.

---------
2026-06-13 14:01:04 +02:00