## 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
- 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
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: NeuralNomadsAI/CodeNomad#639
(comment).
## 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>
## 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
## 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
- 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
- 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
- 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
- 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>
## 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
## Summary
- add a release-published workflow that prepares and submits Winget
manifest updates automatically
- poll the GitHub Release API for the stable Windows Tauri asset and
compute its SHA-256 before submission
- document the maintainer secret and repository variables needed for the
Winget automation flow
## Validation
- `node --check "scripts/winget/resolve-release-asset.cjs"`
- `node "scripts/winget/resolve-release-asset.cjs" --help`
- dry-run resolver against the published `v0.16.0` release asset
## Notes
- skips draft and prerelease GitHub releases
- uses the maintainer fork submission flow for `microsoft/winget-pkgs`
- live PR submission still depends on configuring `WINGET_GITHUB_TOKEN`
- Fixes#462
## Problem
The `Comment PR Artifacts` workflow consistently times out before the
`PR Build Validation` run can complete. The build pipeline typically
takes 14–25 minutes (especially the Tauri macOS build), but the comment
workflow only polled for ~12 minutes (30 attempts × 10-second intervals
plus API overhead).
This has been causing the `comment` check to fail on every PR — see PR
#463 where it failed 3 consecutive times.
## Fix
- Increase polling attempts from **30 → 90**
- Increase sleep interval from **10s → 20s**
- Effective maximum wait: ~30 minutes of sleep + API overhead ≈ 45+
minutes total
This gives ample headroom for the full build matrix to complete,
including slower runners like `build-tauri-macos`.
## Why this needs to merge first
The `comment-pr-artifacts.yml` workflow uses `pull_request_target`,
which means it runs **from the base branch (dev)**, not the PR branch.
Changes to this file in PR #463 cannot take effect until this fix lands
on `dev`. Once merged, the comment workflow will stop timing out on PR
#463 and all future PRs.
---
_This PR was created by an AI agent (OpenHands) on behalf of the user to
unblock PR #463._
Co-authored-by: openhands <openhands@all-hands.dev>
## Summary
- revert the Bun standalone desktop packaging path and restore the
server's original `dist/bin.js` bootstrap flow
- add a managed Node runtime for Electron and Tauri that downloads only
the current platform/arch artifact into `~/.config/codenomad`
- update desktop startup and packaging scripts so packaged apps use the
managed runtime consistently, and clean up Electron's expected
navigation-abort log noise
## Testing
- npm run typecheck --workspace @neuralnomads/codenomad-electron-app
- cargo check
- npm run build --workspace @neuralnomads/codenomad
- npm run build:mac --workspace @neuralnomads/codenomad-electron-app
- launch
`packages/electron-app/release/mac-arm64/CodeNomad.app/Contents/MacOS/CodeNomad`
and verify the packaged server reaches ready with the managed Node
runtime
## Summary
- package `packages/server` as a standalone desktop executable so
Electron and Tauri no longer depend on a system-installed Node runtime
in production
- align Electron and Tauri startup logic around launching the packaged
server, resolving binaries from the user shell, and bundling the same
server resources into both desktop apps
- replace the workspace instance proxy path that used
`@fastify/reply-from` with a direct streaming proxy so packaged
standalone builds can talk to spawned `opencode` instances correctly
## Why
Desktop production builds were still depending on a user-provided Node
runtime to launch `packages/server`, which made packaging less
self-contained and created different behavior across machines. While
moving to a standalone server executable, we also found that
Bun-compiled standalone builds could start `opencode` successfully but
failed when proxying requests to those instances through `reply-from`.
The goal of this change is to make desktop production startup
self-contained, keep Electron and Tauri behavior aligned, and restore
correct communication with local `opencode` instances in packaged
builds.
## What Changed
- added a standalone build path for `packages/server` and bundle
`codenomad-server` into desktop resources
- updated Electron production startup to resolve and launch the
standalone server executable
- updated Tauri production startup to resolve and launch the standalone
server executable with matching cwd and shell behavior
- added runtime path helpers so the packaged server can reliably find
its bundled UI, auth templates, config template, and package metadata
- improved bare binary resolution so commands like `opencode` can be
resolved from the user's login shell environment
- upgraded the server stack to newer Fastify-compatible packages needed
for the standalone/runtime work
- replaced the workspace instance proxy implementation with a direct
streaming proxy for requests to spawned `opencode` instances
- updated Electron and Tauri build/prebuild scripts to generate and
package the standalone server, while also repairing missing
platform-specific optional binaries during packaging
## Benefits
- desktop production builds no longer require Node to be installed on
the user's system
- Electron and Tauri now use the same packaged server model in
production, reducing platform drift
- packaged desktop apps can successfully create workspaces, launch
`opencode`, and proxy health/session traffic to those instances
- the server bundle is more self-contained and resilient to different
launch environments
- desktop packaging is more predictable because the required server
executable is built and bundled as part of the app build flow
- Add build scripts for platform-specific builds with zip bundles
- Update CI workflow to use --bundles flag for explicit target selection
- macOS: use app,zip (removed dmg)
- Windows: use nsis,zip
- Linux: use appimage,deb,rpm
Restrict non-dev pull requests to an allowlisted set of actors and skip cross-platform PR builds unless that authorization check passes. Keep dev open for general contributions while guiding other PRs back to the dev branch.
Run the full cross-platform build matrix on pull request creation and updates so build regressions are caught before merge without publishing release artifacts.
Replace dev push builds with nightly schedule that only runs when dev head advances; still runs on manual dispatch. Plumb a ref input through reusable workflows so scheduled runs build the dev commit.
Switch dev workflow to publish the server under @neuralnomads/codenomad-dev with dist-tag latest, avoiding @dev dist-tags. Add workflow input to override package name at publish time.
Publish bleeding-edge builds from dev to GitHub prereleases and npm dist-tag 'dev'. Dev builds poll GitHub prereleases and surface update availability via /api/meta for UI notifications.