synabun 2026.4.25-beta.3 → 2026.4.26-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,199 +2,128 @@
2
2
 
3
3
  Raw internal changelogs are archived in [changelog/](changelog/).
4
4
 
5
- ## 2026-04-25
6
-
7
- ### Added — Dedicated Schedules Studio
8
- - **Standalone schedule window** — Added `neural-interface/public/shared/ui-schedules-studio.js` and initialized it from both `neural-interface/public/variant/2d/main.js` and `neural-interface/public/variant/3d/main.js`. The new window opens from `automations:open-schedules` and keeps schedule management out of the Automation Studio detail pane.
9
- - **Tabbed schedule workspace** — Built Cron, Timers, Groups, and Activity tabs so recurring schedules, quick timers, group management, and run history have separate focused surfaces instead of sharing the automation editor area.
10
- - **Schedule editor flow** — Added a dedicated editor with cron presets, timezone, day selection themes, enabled state, group assignment, and launch overrides for profile/model/effort/MCP/browser settings.
11
- - **Timer controls** — Added quick timer creation, run-now actions, active timer countdowns, saved schedule timers, pause/resume, trigger, edit, and delete controls inside the schedules window.
12
-
13
- ### Added — Schedule Groups API and Persistence
14
- - **Group-aware schedule store** — Added `loadScheduleStore()`, `saveScheduleStore()`, `loadScheduleGroups()`, `saveScheduleGroups()`, `validateScheduleGroupId()`, and `applyScheduleLaunchOverrides()` in `neural-interface/server.js`. Existing `loop-schedules.json` data remains compatible while the store can now persist `{ schedules, groups }`.
15
- - **Schedule group endpoints** — Added `GET /api/schedule-groups`, `POST /api/schedule-groups`, `PUT /api/schedule-groups/:id`, `DELETE /api/schedule-groups/:id`, and `POST /api/schedule-groups/reorder` for group CRUD and ordering.
16
- - **Schedule assignment metadata** — Extended schedule create/update and launch payloads to carry `groupId`, `profile`, `model`, `effort`, `mcpProfile`, and `usesBrowser`, allowing schedules in a group to keep their launch configuration with the schedule itself.
17
- - **Client sync helpers** — Added schedule group API helpers in `neural-interface/public/shared/api.js` and broadcast handling for `schedule-group:created`, `schedule-group:updated`, `schedule-group:deleted`, and `schedule-group:reordered` in `neural-interface/public/shared/ui-sync.js`.
18
-
19
- ### Changed — Automation Studio Schedule Routing
20
- - **Schedules removed from the Automation Studio content flow** — `initAutomationStudio()` no longer handles `automations:open-schedules` by switching `_view` to `schedules`, and Automation Studio no longer initializes schedule WebSocket rendering. The header Schedules button now emits the shared schedules event for the standalone window.
21
- - **Automations submenu expanded** — Added Schedules, New Cron Schedule, Quick Timer, and Schedule Groups menu items in `neural-interface/public/shared/html-shell.js`, with handlers in `neural-interface/public/shared/ui-menubar.js` that open the dedicated schedules window directly to the requested flow.
22
- - **Schedules window styling** — Added the `.schedules-studio-panel` glass shell and `.sched-*` responsive layout styles in `neural-interface/public/shared/styles.css` so the standalone window has its own grouped sidebar, tab rail, schedule lists, dialogs, and mobile layout.
23
-
24
- ### Changed — Live Countdown on Schedule Cards
25
- - **Live ticking countdown** — Replaced static "Next: 4h" text on Cron-tab schedule cards with a 1-second-tick live countdown. `formatNextRun()` in `neural-interface/public/shared/ui-schedules-studio.js` rewritten as adaptive 2-unit format (`2d 3h`, `4h 23m`, `15m 42s`, `45s`, `Overdue`) that drops zero second-unit. Card markup wraps the value in `<span class="sched-countdown" data-countdown="<iso>">` so `updateTimerCountdowns()` can refresh text without re-rendering each tick. `POLL_INTERVAL` lowered from `5000` to `1000`; tick lifecycle stays bound to `startPolling()` / `stopPolling()` so it only runs while the panel is open.
26
- - **Tabular-nums styling** — Added `.sched-countdown` rule in `neural-interface/public/shared/styles.css` with `font-variant-numeric: tabular-nums` to prevent layout jitter as digits change each second.
27
-
28
- ### Added — Edit Prompt Shortcut on Schedule Cards
29
- - **Edit prompt icon button** — Each cron schedule card in `neural-interface/public/shared/ui-schedules-studio.js` now has an "Edit prompt" icon button (document-with-lines `script` icon) between Run Now and Edit Schedule. Click closes the Schedules panel and opens Automation Studio with the schedule's linked template selected directly in detail/editor view.
30
- - New `script` icon added to the `ICONS` map
31
- - New button uses `data-action="schedule-edit-prompt"` with `data-template-id="${s.templateId}"`
32
- - Existing pencil tooltip clarified from "Edit" to "Edit schedule" to differentiate
33
- - New `case 'schedule-edit-prompt'` handler reads `el.dataset.templateId`, calls `closePanel()`, then emits `automations:open` with `{templateId}` payload
34
- - **`automations:open` event accepts `{templateId}` payload** — `initAutomationStudio()` listener in `neural-interface/public/shared/ui-automation-studio.js` now passes `opts` to `openPanel(opts = {})`. When payload contains `templateId`: if panel already open, finds template in `_templates` and calls `switchToDetail(tpl)`; on fresh open, after `loadData()` runs, looks up template and routes to detail view instead of `renderView()`. Falls back to default welcome view when no payload or template not found.
35
-
36
- ### Added — Sidepanel Tray Placeholder Pills
37
- - **Persisted pills before panel bootstrap** — Added `neural-interface/public/shared/ui-sidepanel-tray.js` to render minimized placeholder pills for saved Claude, Codex, and OpenCode tabs before those provider panels have been lazy-loaded. The tray reads window-scoped tab storage, falls back to legacy single-session keys, filters out blank default sessions, and opens the matching provider via `claude-panel:show`, `codex-panel:show`, or `opencode-panel:show` with the selected `tabId`.
38
- - **Provider handoff events** — Wired `initSidepanelTrayPlaceholders()` from `ui-navbar.js`, then dispatch `sidepanel-tray:provider-loaded` from `toggleClaudePanel()`, Codex `ensurePanel()`, and OpenCode `ensurePanel()` so placeholders disappear once real provider-owned pills are restored.
39
-
40
- ### Fixed — Right-Side Toolbar Reservation During Sidepanel Switches
41
- - **Toolbar could overlap an open sidepanel** — `ui-sidepanel-layout.js` previously tracked one `_activeOwner`, so closing or switching one sidepanel could clear `--right-panel-width` and `--right-panel-gap` while another right panel was still active. Replaced that with a per-owner reservation map, applying the widest active reservation and returning `true` from `clearRightPanelLayout()` only when no reservations remain, preserving toolbar and adjacent editor spacing during multi-provider toggles.
42
-
43
- ### Changed — Claude Restored Tab Selection
44
- - **Restored Claude tabs keep stable IDs** — Updated `createTab()` in `ui-claude-panel.js` to accept a restored `id`, and changed `restoreTabs()` to pass saved tab IDs through. Placeholder click-through can now target the restored Claude tab instead of losing selection because the panel generated a fresh `crypto.randomUUID()` during bootstrap.
45
-
46
- ### Changed — Backup System Coverage Expanded
47
- - **Backup file lists extended** — Added `stored-plans.json`, `synabun-plugins.json`, `mcp-registry.json`, and `active-profile.json` to both manual `GET /api/system/backup` (`neural-interface/server.js:10457`) and auto-backup `buildBackupZipToFile()` (`neural-interface/server.js:10844`) file arrays. These cover the post-plan hook plan→memory tracker, plugin install registry, third-party MCP server registry + profile defs, and active MCP profile selection — all previously lost on restore.
48
- - **Plan markdown history backed up recursively** — Added `addDirRecursive('data/plans', ...)` to both manual and auto backup paths (`neural-interface/server.js:10483`, `:10868`) so the date-sliced plan tree (`data/plans/YYYY-MM-DD/*.md`) rides along.
49
- - **Restore creates `plans` subdir** — Added `'plans'` to the `mkdirSync` subdir list in `POST /api/system/restore` (`neural-interface/server.js:10617`). Existing recursive `data/` write loop already extracts plan files via `dirname(target)` mkdir.
50
- - **Backward compatible with old backups** — Manifest version check still accepts `version 1 || 2`. Old backups restore exactly as before — missing new files skipped silently, no schema migration required.
51
-
52
- ### Added — CLI-Not-Installed Indicator in Sidepanels
53
- - **Empty-state banner across Claude, Codex, OpenCode panels** — Detects when the corresponding CLI binary is missing and renders a warning banner inside the panel's empty state (`.cp-empty` / `.cxp-empty` / `.ocp-empty`) with the install command, an "Install guide" link to the official docs, and a "Re-check" button. Banner is removed automatically once the CLI is detected.
54
- - **Shared status module `cli-status.js`** — New `neural-interface/public/shared/cli-status.js` wraps the existing `/api/system/tool-versions` endpoint with a subscriber + auto-poll layer. Exposes `subscribeCliStatus(toolKey, cb)`, `recheckCliStatus(toolKey)`, `getCliDocUrl(toolKey)`, `getCliInstallCommand(toolKey)`, `getCliLabel(toolKey)`. Polls every 30s while at least one subscribed tool reports `installed === null`; stops once detected or unsubscribed. 10s in-memory cache + single in-flight fetch dedup.
55
- - **Send-button gating** — Each panel's send/input flow is now blocked while the CLI is missing. `ui-claude-panel.js` `send()` returns early with a notification. `cdx/cdx-tabs.js` `syncInputEnabled` reads the `.cxp-cli-blocked` panel class to force `send.disabled = true`, and `sendPrompt()` returns early with a system message. `ocp/ocp-panel.js` `syncSendButton` short-circuits to disabled with the "OpenCode CLI not installed" tooltip; `handleSend` returns early via `setStatus('offline', …)`.
56
- - **Banner re-render hooks** — `cdx/cdx-tabs.js` `clearTranscript` now dispatches a `cdx-empty-rebuilt` CustomEvent and `ocp/ocp-render.js` `renderEmptyState` dispatches `ocp-empty-rebuilt`, so the banner re-injects itself after the empty state is rebuilt during session resets.
57
- - **Official install URLs** — `claude-code` → `https://docs.claude.com/en/docs/claude-code/setup`, `codex` → `https://developers.openai.com/codex/cli/`, `opencode` → `https://opencode.ai/docs/`. (`gemini` constants defined for future use; no Gemini sidepanel currently exists.)
58
- - **Banner styles** — Added `.cp-cli-missing*`, `.cxp-cli-missing*`, `.ocp-cli-missing*` rule sets to `ui-claude-panel.js` style block, `cdx/cdx-styles.js`, and `ocp/ocp-styles.js`. Includes `.cp-cli-blocked` / `.cxp-cli-blocked` / `.ocp-cli-blocked` panel classes for the dimmed input state.
59
-
60
- ### Fixed — CLI-Not-Installed Banner Hidden After Errors / Broken-Binary Cases (Sidepanels)
61
- - **Banner relocated to top of messages container** — Was rendered inside `.cxp-empty` / `.cp-empty` / `.ocp-empty`. Once any system message appeared (e.g. `Codex session closed` + `Codex app-server exited before responding to request 1`), the empty-state collapsed and took the banner with it. Banner is now an independent `:first-child` of `#cxp-messages-container`, `#cp-messages-container`, and `.ocp-messages-container` via `container.insertBefore(banner, container.firstChild)`. Stays visible across transcript states. New `ensureCliBannerEl()` / `removeCliBannerEl()` / `refreshCliBanner()` helpers replace the old empty-state-scoped `renderCliMissingBanner` / `refreshCliBanners` in `neural-interface/public/shared/cdx/cdx-panel.js`, `neural-interface/public/shared/ui-claude-panel.js`, and `neural-interface/public/shared/ocp/ocp-panel.js`.
62
- - **Spawn-failure pattern detection** — Banner only triggered when `/api/system/tool-versions` reported `installed: null`. A broken-but-present binary (e.g. `codex 0.125.0` spawning then exiting immediately) reports a real version, so the banner never showed. Added a per-panel `_cliInstallFailureForced` flag flipped on by WS error patterns:
63
- - **Codex** — `applySocketError` in `cdx/cdx-tabs.js` matches `/Codex CLI not found|ENOENT|app-server exited before responding|app-server spawn/i` → dynamic-imports `cdx-panel.js` and calls exported `flagCdxCliInstallFailure()`.
64
- - **Claude** — `case 'error'` in WS handler matches `/Claude CLI not found|ENOENT/i` → `flagClaudeCliInstallFailure()`.
65
- - **OpenCode** — `onError` callback matches `/opencode.*(not.*found|ENOENT|spawn)|opencode CLI/i` → `flagOcpCliInstallFailure()`.
66
- - All three flags clear automatically when the `cli-status` subscriber callback later receives an installed value.
67
- - **Send-button gate now reads forced flag** — Claude `send()`, Codex `sendPrompt()` (via `.cxp-cli-blocked` panel class read in `syncInputEnabled`), and OpenCode `syncSendButton` / `handleSend` all check `_cliInstalled === null || _cliInstallFailureForced` so a broken-binary install also blocks send.
68
- - **Banner restyled as sticky alert** — New `.cxp-cli-banner*`, `.cp-cli-banner*`, `.ocp-cli-banner*` rule sets in `cdx/cdx-styles.js`, `ui-claude-panel.js` style block, and `ocp/ocp-styles.js`. Three-column grid (icon, text, actions), orange/amber theme, `flex-shrink: 0`, sits at top of the scroll region with `z-index: 5` so it stays above transcript content.
69
-
70
- ### Fixed — High Severity npm audit Alerts in Install CLI (Neural Interface)
71
- - **Playwright SSL cert verification CVE** — `neural-interface/package.json` pinned `playwright` at `1.54.2`, vulnerable to GHSA-7mvr-c777-76hp (browser downloader skipped SSL cert authenticity check). Bumped to `^1.59.1` (non-major) — fixed in `<1.55.1`. `setup.js` install flow no longer prints the high severity warning after `Installing MCP Server dependencies`.
72
- - **lodash code injection + protobufjs RCE via transitives** — `lodash@4.17.23` (pulled by `archiver` → `archiver-utils@5.0.2`) had high severity GHSA-r5fr-rjxr-66jc (`_.template` import key code injection) plus moderate prototype pollution. `protobufjs@7.5.4` (pulled by `@huggingface/transformers` → `onnxruntime-web@1.22.0-dev.20250409-89f8206ba4`) had critical GHSA-xq3m-2v4x-88gg (arbitrary code execution). Added `overrides` block in `neural-interface/package.json` pinning `lodash: ^4.18.1` and `protobufjs: ^7.5.5` — both within the parents' implicit ranges, no breaking change.
73
-
74
- ### Fixed — Moderate/High npm audit Alerts in Install CLI (MCP Server)
75
- - **uuid buffer bounds check CVE** — `mcp-server/package.json` had `uuid@^11.1.0`, vulnerable to GHSA-w5hq-g745-h8pq (missing buffer bounds check in `v3`/`v5`/`v6` when `buf` provided), fixed in `<14.0.0`. Bumped to `^14.0.0` (semver major) — `v4` named export unchanged, `src/tools/remember.ts` and `src/tui.ts` compile cleanly with no source modifications.
76
- - **hono / @hono/node-server / postcss / vite / protobufjs transitives** — multiple moderate-to-high CVEs in vitest's transitive `vite@8.0.0-8.0.4` (path traversal, `server.fs.deny` bypass, websocket arbitrary file read), `hono@<4.12.14` (cookie validation, path traversal in `toSSG()`, ipRestriction IPv4-mapped IPv6), `@hono/node-server@<1.19.13` (serveStatic repeated-slash bypass), `postcss@<8.5.10` (XSS via unescaped `</style>`), and `protobufjs@<7.5.5`. Added `overrides` block in `mcp-server/package.json` pinning `protobufjs: ^7.5.5`, `hono: ^4.12.14`, `@hono/node-server: ^1.19.13`, `postcss: ^8.5.10`, `vite: ^8.0.10` — patches only, no breaking semver bumps to direct deps.
77
-
78
- ### Changed — Removed Deprecated `@types/uuid` devDependency
79
- - **uuid v14 ships own type defs** — removed `@types/uuid: ^10.0.0` from `mcp-server/package.json` devDependencies. npm warned the stub package as deprecated (`This is a stub types definition. uuid provides its own type definitions, so you do not need this installed`). `tsc` build verified — no missing-types errors.
80
-
81
- ## 2026-04-24
82
-
83
- ### Added — "Continue planning" in OpenCode, Codex, and Claude plan UIs
84
- - **OpenCode** — Added `data-action="continue-planning"` button to the post-plan card in `ocp-render.js` and wired `onContinuePlanning` in `showPostPlanUI()` (`ocp-tabs.js`). Handler clears `showPostPlanActions` while keeping `mode='plan'`, so the user can keep prompting to refine the plan without exiting plan mode.
85
- - **Codex** — Added `Continue planning` button to the post-plan card in `cdx-render.js`. Handler removes the card, then dispatches a new prompt with `forcePlanModePrefix: true` and the current plan text as context, allowing iterative plan refinement.
86
- - **Claude** — Added `Continue planning` action entry in `ui-claude-panel.js`. Handler clears `showPostPlanActions` and saves tabs, keeping `tab.planMode = true` so the next user message continues in plan mode.
87
- - **OpenCode guard message** — Updated the "choose before sending" error to include "Continue planning" alongside the existing three options.
88
-
89
- ### Fixed — OpenCode Sidepanel Post-Plan Card Resurrection
90
- - **`setTabMode()` leaves `showPostPlanActions` flag true when leaving plan mode** — Mode button clicks, Tab-key mode cycling, and other `setTabMode()` callers bypassed `onContinue`'s flag clear, so `tab.showPostPlanActions` could persist into build/chat. Combined with the unguarded restore in `renderTabMessages()` at `ocp-tabs.js:2258`, any subsequent tab switch / session switch / revert / WS reconnect re-rendered the "PLAN COMPLETE" card in the message area even though the user had already moved on to implementation. Fixed in `neural-interface/public/shared/ocp/ocp-tabs.js` with two defenses:
91
- - `setTabMode()` now clears `tab.showPostPlanActions` and calls `removePostPlanCards(container)` on any `plan → non-plan` transition, symmetric with the existing `_exitPlanDetected` reset on entry.
92
- - `renderTabMessages()` restore guard tightened to `tab.mode === 'plan' && tab.showPostPlanActions && tab.planContent`, so a leaked flag cannot resurrect the card outside plan mode.
93
-
94
- ### Fixed — Loop Launch Silent Hang (Automation Studio Browser Loops)
95
- - **Orphan Chrome holding `SingletonLock` froze `launchPersistentContext`** — When a previous loop-owned Chrome in `data/browser-profiles/<hash>/` stayed alive past its session, the OS-level singleton lock remained held by the live process. Existing stale-file cleanup at `neural-interface/server.js:20080` only removed unlocked lock files — useless when a running Chrome owned the lock. Subsequent `chromium.launchPersistentContext()` calls blocked indefinitely, so `POST /api/loop/launch` never returned. UI waited with no CLI window, no toast, no error surfaced. Fix in `neural-interface/server.js:20062` (`launchPersistentProfile`) adds a pre-launch orphan-kill pass: Unix uses `pgrep -f "user-data-dir=<root>"` + `SIGKILL`, Windows uses PowerShell `Get-CimInstance Win32_Process` filtered by `CommandLine -like "*user-data-dir=$root*"` (path passed via `SYNABUN_KILL_ROOT` env var to avoid backslash quoting) + `taskkill /F /PID`.
96
- - **`launchPersistentContext` had no timeout** — Added `timeout: 45000` to `launchOpts` at `neural-interface/server.js:19938` as a platform-agnostic safety net. If any edge case still leaves the singleton locked, Playwright errors after 45s instead of hanging silently, letting the UI surface `Failed to launch loop`.
97
-
98
- ### Added — Diagnostic Alert Fallbacks for Silent Launch Failures
99
- - **`attachDetached` catch in `neural-interface/public/shared/ui-terminal.js:5132`** — `showTermToast` appends its toast to `#term-container`; when the terminal panel is hidden (`offsetParent === null`), the toast renders invisibly and the failure gets swallowed. Added a post-toast container-visibility check that triggers a browser `alert()` with the error message plus a DevTools pointer when the container is hidden, surfacing attach failures that otherwise disappear.
100
- - **`confirmLaunch` catch in `neural-interface/public/shared/ui-automation-studio.js:1491`** — Added an `alert()` alongside the existing `showToast` so errors thrown before the `terminal:attach-floating` emit (network failures, missing state, module-load errors) reach the user even when the toast host is offscreen.
101
-
102
- ### Fixed — Codex MCP Approval Gating
103
- - **MCP approval prompts bypassed sidepanel Auto mode** — `handleCodexSkinWebSocket()` in `neural-interface/server.js` previously auto-accepted `mcpServer/elicitation/request` events for any SynaBun tool listed in `~/.claude/settings.json`, so tools like `mcp__SynaBun__remember` could run without a Codex approval card even when the tab Auto toggle was off. Added per-turn `activeTurnAutoAccept` tracking and gated the silent MCP accept path on both the current tab's Auto state and the permitted-tool list.
104
- - **Auto state now travels with each Codex query** — `dispatchPrompt()` in `neural-interface/public/shared/cdx/cdx-tabs.js` includes `autoAccept: !!tab?.autoAccept` in the WebSocket `query` payload, and `server.js` passes it into `startTurn()` so approval behavior follows the visible tab state.
105
- - **Per-turn auto-accept state is cleared after the turn** — `server.js` resets `activeTurnAutoAccept` on `turn/completed`, `thread/closed`, Codex child close, and `turn/start` failure to prevent a prior Auto-enabled turn from leaking into later tool approvals.
106
-
107
- ### Fixed — Codex Sidepanel Edit Plan Cancel Handoff
108
- - **Post-plan card stayed disabled after closing the editor** — `renderPostPlanActions()` in `neural-interface/public/shared/cdx/cdx-render.js` intentionally marks the post-plan card busy when `Edit plan` opens the file editor, but the Codex `plan-edit-cancelled` path in `neural-interface/public/shared/cdx/cdx-panel.js` tried to recover through unregistered `_host.renderPostPlanActions` callbacks. The Codex `plan-saved` and `plan-edit-cancelled` listeners now import and call `appendAssistantMarkdownMessage()` / `renderPostPlanActions()` directly from `cdx-render.js`, so closing without saving re-renders a fresh clickable card and saving reopens the `PLAN UPDATED` handoff correctly.
109
-
110
- ### Fixed — Codex Loop Silent Failure (Sentinel Ignored Exit Code)
111
- - **`attachExecLoopDriver` in `neural-interface/server.js:17230` advanced 162 ghost iterations after codex broke** — Iteration sentinel was built as `<codex exec ...>; echo SYNABUN_ITER_DONE_<N>`, so the echo fired regardless of `codex` exit status. After codex started failing fast at iter 4 (~4s vs ~7m for real runs), driver kept seeing the sentinel and looped 162 fake iters in 33 minutes until the PTY died with exit code 1. Fixed by:
112
- - Sentinel now `<cmd>; rc=$?; echo SYNABUN_ITER_DONE_<N> rc=$rc` — captures exit code in the echo.
113
- - Detection regex `new RegExp(\`${EXEC_SENTINEL_PREFIX}${session._execCurrentIter} rc=(-?\\d+)\`)` extracts `rc`.
114
- - Per-driver `consecutiveNonZero` and `consecutiveFastFails` counters with `FAST_FAIL_THRESHOLD_MS=30000` and `FAIL_LIMIT=3`. On 3-streak (non-zero exit OR sub-30s duration), driver marks loop `active:false`, sets `stoppedReason: 'fast-fail-rc'` or `'fast-fail-duration'`, records `lastExitCode`, kills PTY, and clears the interval.
115
- - Iteration start timestamp tracked via `session._execIterStartedAt = Date.now()` at iter-begin.
116
-
117
- ### Fixed — PTY Exit Left Loop File Active (Zombie Loops)
118
- - **`createTerminalSession` PTY exit handler in `neural-interface/server.js:16718` did not update loop state** — When a loop-owned PTY died, `terminalSessions.delete()` ran and the driver interval cleared, but the on-disk loop file still showed `active:true`. UI/scheduler saw a phantom running loop forever. Fixed by adding a post-`pty:exit` block that calls `findLoopFileForTerminal(sessionId)`, marks the file `active:false`, sets `completedAt`, `stoppedReason: 'pty-exit-<code>'`, and records `lastExitCode`. Logs `pty:exit | marked loop inactive` for forensics.
119
-
120
- ### Changed — Loop Tab Isolation Hardening
121
- - **Dropped `_switchSessionTab` calls during loop launch** — `neural-interface/server.js:8351` (manual launch) and `:8751` (scheduled launch) previously called `_switchSessionTab(claimedSession, ..., browserTabId)` after `createSessionTab`, flipping the shared browser session's `activeTabId`. With concurrent loops sharing one Chromium, this caused active-tab churn and risked any code path falling back to `session.activeTabId`. Each loop already pins its own tab via the `SYNABUN_BROWSER_TAB` env var, so the switch was unnecessary. Removed both calls; replaced with a comment explaining the constraint.
122
- - **`SYNABUN_BROWSER_TAB` env now wins over caller-supplied `tabId`** — `mcp-server/src/services/neural-interface.ts:88` (`resolveSession`) reordered the resolution: pinned env value takes precedence over the explicit `tabId` argument. If the caller passes a different tabId, it's logged as `[MCP] tabId override ignored` and the pin is honored. Prevents an agent or model from accidentally leaking into another loop's tab by passing the wrong tabId. Rebuilt via `npm run build` in `mcp-server/`.
123
-
124
- ## 2026-04-23
125
-
126
- ### Added — Claude Code Plugin Install from MCP UI
127
- - **CLI-delegated plugin installer** — Rewrote `neural-interface/lib/plugin-installer.js` to call `claude plugin marketplace add <url>` + `claude plugin install <plugin>@<marketplace>` instead of hand-writing `~/.claude/plugins/*.json`. Claude Code now owns its own marketplace + cache state; installs survive CC restarts. Legacy `synabun-symlink` source type removed.
128
- - **Auto-repair on server startup** — `server.js` `app.listen` callback invokes `repairSynabunPlugins()`. Scans `~/.claude/plugins/known_marketplaces.json` for legacy `source: synabun-symlink` entries, purges dangling symlinks and stale `installed_plugins.json` rows, reinstalls via CLI from the remembered repo path.
129
- - **`POST /api/plugins/repair`** — Manual trigger for the same repair flow.
130
- - **`data/synabun-plugins.json` tracker** — New JSON file SynaBun owns, recording which plugins we installed (CC remains source of truth for install state). `setTrackerPath()` export pins the path to `DATA_HOME/data/synabun-plugins.json` on boot.
131
- - **GitHub URL preferred over local clone** — `/api/mcp/install/github` claude-plugin branch passes `githubUrl` into the installer; CC re-clones fresh into its own cache so the temp clone at `data/mcp/<name>/repo` is only used to read the manifest.
132
-
133
- ### Added — Plugin Slash Commands in Sidepanel Autocomplete
134
- - **`GET /api/claude-code/plugin-commands`** — New endpoint in `server.js`. Walks `~/.claude/plugins/cache/*/*/*/commands/*.{toml,md}`, parses `description` from TOML (`description = "..."`) or markdown frontmatter, returns `[{ name, description, source: 'plugin', plugin, marketplace }]`. No caching — fresh scan per request, so newly installed plugins appear without restart.
135
- - **Autocomplete merge in `ui-claude-panel.js`** — `loadSkills()` adds a fourth merge step after SynaBun skills + CLI built-ins + extras. Plugin commands show in the `/` hint dropdown with `(plugin: <name>)` suffix.
136
-
137
- ### Fixed — Broken Legacy Caveman Install
138
- - **CC marketplace loader errored with `Marketplace configuration file is corrupted`** — Caused by the old installer writing `"source": "synabun-symlink"` to `known_marketplaces.json`; CC prunes unknown source types and wiped the `cache/caveman/` symlink so `installPath` in `installed_plugins.json` pointed at a missing directory. Hooks never fired. Cleanup + CLI reinstall restored caveman — `claude plugin list` now shows `caveman@caveman ✔ enabled` with a valid `cache/caveman/caveman/<sha>` dir.
139
-
140
- ### Fixed — Sidepanel Slash-Hints Silent Bail
141
- - **`showSlashHints()` returned early when `_skillsCache` null** — First `/` keystroke could fire before the async `loadSkills()` fetch resolved, leaving no dropdown. Made `showSlashHints` async; awaits `loadSkills()` when cache empty so the dropdown is guaranteed on first keystroke.
142
-
143
- ### Fixed — Sidepanel Slash-Hints Clipped by Input Wrapper
144
- - **`.cp-input-wrap` had `overflow: hidden`** — Parent wrapper clipped the upward-expanding `.cp-slash-hints` dropdown (positioned `bottom: calc(100% + 4px)`). Removed the overflow rule — the wrapper's conic-gradient border already clips itself via `-webkit-mask-composite: xor`, so the overflow was redundant. Dropdown now expands unclipped and scrolls internally.
145
-
146
- ### Changed — Sidepanel Slash-Hints Dropdown Sizing
147
- - **`.cp-slash-hints` — `max-height: 200px → 320px`** — Fits more rows before scroll kicks in.
148
- - **Added `overscroll-behavior: contain`** — Mouse-wheel scroll inside the dropdown no longer bubbles to the message list beneath.
149
-
150
- ### Fixed — GitHub MCP Installer Discarded Servers on Install Failure
151
- - **`/api/mcp/install/github` soft-fails local dep errors** — In `neural-interface/server.js`, the install endpoint returned HTTP 500 and aborted the whole flow when `pip install -r requirements.txt` or `npm install` errored, even though the clone + detection already succeeded. Reproducible with `AminForou/mcp-gsc`: launch command was `uvx mcp-search-console` (self-fetching) but `pip` missing on PATH made the endpoint bail before registry write. Now install failures attach an `installWarning` field to the 200 response — the server still registers, the user sees a `⚠` line in the GitHub install form, and deps can be fixed manually.
152
- - **Self-fetcher runtimes skip local install** — Added `commandSelfFetches(detected)` in `neural-interface/lib/mcp-installer.js` that matches `uvx` / `uv run` / `npx` / `pipx` / `bunx` / `pnpm dlx` / `yarn dlx`. When true, `installDependencies` is never called — the server is registered with `install.skipped = true, reason: "command '<cmd>' self-fetches at runtime"` and the UI shows a `⚙ Skipped local deps` hint.
153
-
154
- ### Added — Cross-OS Preflight for Self-Fetching MCP Runtimes
155
- - **`checkCommandOnPath(cmd)` in `neural-interface/lib/mcp-installer.js`** — OS-agnostic PATH probe. Uses `where <cmd>` on Windows (which resolves `.exe` via PATHEXT), `sh -c 'command -v <cmd>'` on Unix. 5s timeout guard. Returns a boolean.
156
- - **`checkSelfFetcherAvailable(detected)` preflight** — Called by the install endpoint when `commandSelfFetches` is true. Returns `{ available, command, installHint }`. Missing binary attaches an OS-specific install command to `installWarning` (e.g. *"'uvx' not found on PATH. Install: brew install uv. Server registered anyway — will fail at launch until installed."*).
157
- - **`SELF_FETCHER_INSTALL_HINTS` map** — Per-OS install commands for `uvx`/`uv`/`pipx`/`npx`/`bunx`/`bun`/`pnpm`/`yarn`. Mac uses `brew`, Linux uses official curl installers, Windows uses `winget` or PowerShell `irm`. `installHintFor(cmd)` picks the row based on `process.platform`. Ensures fresh SynaBun installs on Windows/Mac/Linux get actionable guidance instead of a silent launch failure.
158
-
159
- ### Changed — Codex Sidepanel Reasoning and Response Controls
160
- - **Expanded Codex reasoning effort support** — Updated `neural-interface/public/shared/cdx/cdx-icons.js`, `cdx-panel.js`, `cdx-styles.js`, and `cdx-tabs.js` so the Codex footer effort toggle now supports the full current set of sidepanel overrides: `off`, `minimal`, `low`, `medium`, `high`, and `xhigh`. The toolbar indicator now renders five effort dots, includes visual states for `minimal` and `xhigh`, and `/effort` validates input while treating `default` and `none` as no per-turn override.
161
- - **Exposed response-control config keys** — Added `model_reasoning_summary` and `model_verbosity` to `CODEX_CONFIG_KEYS` in `neural-interface/server.js`, then surfaced both in the Codex settings form and `/status` output in `neural-interface/public/shared/cdx/cdx-tabs.js`. Settings now support reasoning summary choices `auto`, `concise`, `detailed`, and `none`, plus response verbosity choices `low`, `medium`, and `high`.
162
-
163
- ### Fixed — Codex Sidepanel Session Cross-Talk
164
- - **Per-tab WebSocket identity** — `sendSocket()` in `neural-interface/public/shared/cdx/cdx-tabs.js` now stamps every Codex sidepanel WebSocket message with the bound tab's `sessionId`, so bootstrap, query, reattach, model, interrupt, and compact traffic is routed to the owning tab instead of only the shared `windowId`.
165
- - **Strict orphan reattach keys** — `_codexOrphanKey()` in `neural-interface/server.js` now requires both `windowId` and `sessionId`, and the reattach handler returns `reattach_result: false` when the tab id is missing. The close handler refuses to create window-wide orphans and kills sessionless processes, preventing a newly opened tab from replaying another tab's buffered tool-call events.
166
- - **Reconnect state cleanup** — `onClose()` sets `pendingReattach` only for abnormal Codex socket closes and `onReattachResult()` clears it, preserving correct orphan recovery for the original tab without leaking state to other sessions.
167
-
168
- ### Fixed — Codex Sidepanel Active-Tab Stop Scope
169
- - **Stop button aborted every running Codex tab** — The shared composer stop state in `syncInputEnabled()` was keyed off `_tabs.some((t) => t?.running)`, and `cdx-panel.js` wired click/Escape to `interruptAllTabs()`, so stopping one visible tab sent `interrupt` and delayed `force_kill` to every running tab WebSocket. The handlers now call `interruptTurn()` only when `activeTab()?.running`, the stop icon is scoped to the active tab, and `interruptTab()` sends session-scoped stop messages for the bound tab.
170
- - **Stale force-kill retries could outlive successful interrupts** — `onInterruptAck()` now clears each tab's `_abortRetry`, preventing the fallback timer from killing a Codex app-server after the server already acknowledged an interrupt.
171
- - **Server accepted unscoped stop messages** — `validateCodexControlMessage()` in `server.js` rejects `interrupt`/`force_kill` without a matching `sessionId` or with a mismatched `threadId`, while `forceKillCodexChild()` logs pid/session/thread/reason before terminating the app-server.
172
-
173
- ### Fixed — Codex Plan Mode Handoff
174
- - **Post-plan actions no longer get skipped** — `dispatchPrompt()` in `neural-interface/public/shared/cdx/cdx-tabs.js` now marks sent plan turns with `planMode`, `planTurnActive`, and `lastPlanTurnId`, so `handleNotify('turn/completed')` captures the completed plan even if the toolbar `planMode` flag has already changed by the time the turn finishes.
175
- - **Approval gate persists across tab state** — Added `planApprovalPending` / `planTurnActive` persistence through `snapshotBoundState()`, `restoreBoundState()`, `commitBoundState()`, `bindTabState()`, `createTab()`, and `saveTabs()`, and blocks `sendPrompt()` / `advanceQueue()` until the user chooses Continue, Compact, or Edit.
176
- - **Continue/Edit paths clear or restore the handoff correctly** — `renderPostPlanActions()` in `cdx-render.js` clears pending plan state before dispatching the non-plan implementation prompt and restores it if send fails, while `cdx-panel.js` marks edited plan saves as pending approval again.
177
- - **Plan turns run read-only server-side** — `server.js` passes the client `planMode` flag into `startTurn()`, detects explicit `[PLAN MODE]` prompts with `isCodexPlanModePrompt()`, and forces `buildCodexSandboxPolicy(..., 'read-only')` for plan turns.
178
- - **Prompt text now enforces the sidepanel handoff** — `CODEX_PLAN_MODE_PREFIXES` in `cdx-icons.js` and `server.js` now tells Codex to stop after producing the plan and wait for the sidepanel’s Edit, Compact, or Continue choice.
179
-
180
- ### Fixed — CLI/TUI Rendering Corruption (Terminal)
181
- - **Stable xterm renderer path** — `loadXterm()` in `neural-interface/public/shared/ui-terminal.js` now intentionally skips `@xterm/addon-webgl` and `@xterm/addon-canvas`, leaving CLI/TUI windows on xterm's DOM renderer. This avoids WebGL glyph atlas/context corruption seen after multiple long-lived floating CLI sessions.
182
- - **Serialized terminal writes** — Added `_createTerminalWriter()` with 64KB chunking and `term.write()` callback backpressure, then routed fresh session, reconnect, and WebSocket reconnect output through it. This prevents large PTY bursts from piling up unbounded parser/render work during long Codex/Claude/Gemini sessions.
183
- - **Snapshot-based reconnects** — `createTerminalSession()` in `neural-interface/server.js` now maintains a capped `VTermBuffer` per PTY, updates it on output and resize, and `/ws/terminal/:id` sends a synthesized `snapshot` payload instead of replaying an arbitrary raw ANSI tail. The snapshot includes a plain-text companion so `_sendOnceReady()` can still detect ready prompts without waiting for its fallback timer.
184
- - **Virtual terminal snapshot support** — `neural-interface/public/shared/vterm-buffer.js` now exposes `useAltBuffer` and accepts a per-instance scrollback cap, letting live PTY sessions keep only the rows needed for reconnect recovery while preserving the larger default for existing capture use cases.
185
-
186
- ## 2026-04-22
187
-
188
- ### Added — Smart "Install from GitHub" (MCP + Claude Code Plugins Auto-Route)
189
- - **Repo classifier** — New `classifyRepo(repoPath)` in `neural-interface/lib/mcp-installer.js` runs BEFORE `detectServer()` and returns `{ kind: 'mcp' | 'claude-plugin' | 'unknown' }`. Prefers MCP when both markers exist, emits `alsoClaudePlugin` hint for mixed repos, scans for codex/gemini/cursor/windsurf adjacency signals.
190
- - **Claude Code plugin installer** — New `neural-interface/lib/plugin-installer.js` with `installClaudePlugin()`, `uninstallClaudePlugin()`, `listSynabunPlugins()`. Symlinks the cloned repo into `~/.claude/plugins/marketplaces/<name>` and `~/.claude/plugins/cache/<marketplace>/<plugin>/unknown`, appends SynaBun-tagged entries to `known_marketplaces.json` + `installed_plugins.json` (dedupe by `scope:'user' + installPath`), captures `gitCommitSha` via `git rev-parse HEAD`.
191
- - **`POST /api/mcp/install/github` branches on classification** — After clone, `kind: 'claude-plugin'` skips MCP detection/deps install and calls `installClaudePlugin`; `kind: 'unknown'` returns 422 with hints list; MCP path unchanged. New endpoints `GET /api/plugins/list` and `DELETE /api/plugins/:marketplace?pluginName=&purgeFiles=1`.
192
- - **Settings UI rename + plugin list** — "+ Add Server" button renamed to "+ Install from GitHub" in `neural-interface/public/shared/ui-settings.js`. New "Claude Code Plugins" subsection with per-row Uninstall. Install handler swaps the form into "plugin mode" (hides Type/Command/Args/env/platforms/Save) when response is `kind: 'claude-plugin'` since the plugin is already activated server-side.
193
-
194
- ### Added — Installed MCPs Sub-Tab (Cross-CLI Unified View)
195
- - **`GET /api/mcp/installed-all` endpoint** — Aggregates every externally-installed MCP across SynaBun registry (non-builtin), `~/.claude.json`, `~/.gemini/settings.json`, `~/.codex/config.toml` (regex `^\[mcp_servers\.([^\].]+)\]$` excludes nested `.tools.` sections), and OpenCode's `mcp.*` config. Merges by server name; `registeredWith` is a sorted array of CLI keys.
196
- - **Sub-tab navigation inside Settings → MCP** — New `.stg-mcp-page-tabs` at top of the page swaps between **Profiles** (existing matrix + always-on + per-profile External Servers toggles) and **Installed** (new cross-CLI list + Claude Code Plugins + Install form). SynaBun's built-in tool groups are excluded from the Installed view.
197
- - **Per-CLI badges** — Each row in the Installed list shows compact `SB/CC/OC/CX/GE` badges indicating which CLIs have the server registered. Styles added for `.stg-mcp-page-tabs`, `.stg-mcp-page-tab.active`, `.stg-mx-ext-cli-badge`.
198
-
199
- ### Added — Cross-CLI Uninstall Button on Installed-Tab Rows
200
- - **One-click uninstall across all registered CLIs** — Each row in the Installed sub-tab carries a `×` button with `data-registered` containing its precise CLI list. Click handler confirms using the badge labels (e.g., "Remove foo from CC, OC, CX?"), then: if `registeredWith` includes `synabun` → `DELETE /api/mcp/registry/servers/:name`; for every remaining CLI key → `DELETE /api/mcp/sync` with `{ name, platforms: cliList }`. Refreshes both Installed list and the Profiles-tab External Servers list afterward. Avoids pointless 404 churn by only hitting CLIs that actually have the server.
5
+ ## New Features
6
+
7
+ - **Dedicated Schedules studio** — Standalone window with Cron, Timers, Groups, and Activity tabs. Pulls schedule management out of the Automation Studio detail pane and adds cron presets, timezone, day selection, group assignment, and per-schedule launch overrides (profile, model, effort, MCP profile, browser).
8
+ - **Schedule groups** — Group recurring schedules with full CRUD endpoints and ordering. Group membership and launch metadata persist with each schedule.
9
+ - **Claude Code plugin install from MCP UI** — Settings > MCP > "+ Install from GitHub" auto-detects whether a repo is an MCP server or a Claude Code plugin and installs via the `claude plugin marketplace add` + `claude plugin install` flow. Plugin slash commands surface in the sidepanel `/` autocomplete with a `(plugin: <name>)` suffix.
10
+ - **Installed MCPs cross-CLI sub-tab** — Settings > MCP now has a unified "Installed" view aggregating servers across SynaBun, Claude Code, Gemini, Codex, and OpenCode with per-CLI badges (SB/CC/OC/CX/GE) and one-click cross-CLI uninstall.
11
+ - **CLI-not-installed banner across sidepanels** — Claude, Codex, and OpenCode panels detect missing or broken CLI binaries (including spawn-failure patterns like `ENOENT` or `app-server exited`) and render a sticky orange banner with the install command, docs link, and Re-check button. Send button is gated until the CLI is detected.
12
+ - **Workspace trust flow for Windows** — Amber **Trust workspace** prompt in both the file explorer and onboarding wizard when Windows flags a repo with "dubious ownership."
13
+ - **One-click CLI install toggles** — Onboarding Step 5 now exposes live install toggles for Claude Code, Gemini CLI, Codex CLI, and OpenCode. Cursor/Windsurf live in a collapsible JSON section.
14
+ - **OpenCode panel greeting** — Fully isolated greeting config for OpenCode, separate from Claude and Codex state.
15
+ - **Multi-CLI Resume with provider picker** — Resume dropdown switches between Claude, Codex, Gemini, and OpenCode sessions via a pill bar.
16
+ - **Hybrid session content search** — SQLite FTS5 full-body search combined with semantic embeddings (Claude). Sidepanel session menus surface match-kind pills ("strong match," "related") with highlighted snippets.
17
+ - **GitHub MCP installer** — Settings > MCP > "From GitHub" clones a repo, auto-detects MCP config, installs dependencies, and pre-fills the form. Includes central env vault, auto profile registration, and sensitive-key masking.
18
+ - **Multi-runtime skills** — `/synabun` and `/leonardo` install independently to Claude, Codex, and OpenCode via per-target chips in Settings.
19
+
20
+ ## Schedules
21
+
22
+ - **Live ticking countdown on schedule cards** — Replaced static "Next: 4h" with a 1-second-tick adaptive countdown (`2d 3h`, `4h 23m`, `15m 42s`, `Overdue`). Tabular-nums styling prevents layout jitter.
23
+ - **Edit prompt shortcut on schedule cards** — New script icon between Run Now and Edit Schedule jumps directly to the linked automation template in detail view.
24
+ - **Quick timer controls** — Quick timer creation, run-now actions, active timer countdowns, saved schedule timers, plus pause/resume, trigger, edit, and delete from inside the schedules window.
25
+ - **Automations submenu expanded** — Added Schedules, New Cron Schedule, Quick Timer, and Schedule Groups menu items that open the dedicated schedules window directly to the requested flow.
26
+
27
+ ## Front End / Sidepanel UI
28
+
29
+ - **Sidepanel tray placeholder pills** — Saved Claude / Codex / OpenCode tabs render as minimized pills before the provider panel has been lazy-loaded; clicking opens the matching provider with the selected tab preserved.
30
+ - **Codex effort and response controls expanded** — Codex footer effort toggle now supports `off`, `minimal`, `low`, `medium`, `high`, and `xhigh`. Settings expose `model_reasoning_summary` (auto/concise/detailed/none) and `model_verbosity` (low/medium/high).
31
+ - **Slash-hints dropdown polish** — Larger 320 px max-height, no longer clipped by the input wrapper, and `overscroll-behavior: contain` so wheel scroll inside the dropdown doesn't bubble to the message list. First `/` keystroke now reliably shows the dropdown even before the skills cache loads.
32
+ - **Unified context gauge** — Single-fill bar across Claude, Codex, and OpenCode with threshold-based color (gray → amber >60% → red >80%). OpenCode gauge now uses model-specific context windows with full token breakdown.
33
+ - **OpenCode agents drawer redesign** — Gradient backdrop, color-coded subagent type chips, shimmering init placeholder, per-agent ✕ abort button, and amber stop button with pulse. Expandable inline step view with live in-place elapsed timer.
34
+ - **OpenCode tool activity dock** — Floating left-edge dock lists every tool call; click a row to scroll to the card. `Ctrl/Cmd+Shift+A` toggles.
35
+ - **OpenCode permission cards** — Now show the actual tool (bash/edit/write/read/webfetch), command or path, matched patterns, and expandable diff instead of a generic "Permission request" chip.
36
+ - **Claude sidepanel view mode picker** — Normal / Transcript / Focus now visible on the project bar instead of only via `Ctrl+O`.
37
+ - **TODO widget → left-edge slide-out drawer (Claude)** — Tag docks on the edge when todos exist; click to expand. Per-tab persistence; `Ctrl+T` toggles.
38
+ - **New session modal rebuilt** — Centered overlay on "+" with Project, Branch, and Name fields. Branch checkout defers to Save. Applies to Claude, Codex, and OpenCode.
39
+ - **Codex sidepanel feature parity** — Model selector, effort toggle, plan mode, auto-accept, image attachments, file-path chips, hold-to-talk voice, slash hints, FIFO prompt queue with drag-reorder, copy buttons, thread fork/archive, settings overlay, stall detection, auto-compact. Fully isolated from the Claude panel.
40
+ - **Session menu overhaul (Claude + Codex)** — Sticky search, "Hide empty" / "Archived" filters, per-item archive/unarchive, Claude branch dropdown, and infinite scroll.
41
+ - **Multi-question cards** — All questions from one `AskUserQuestion` call render in a single card with multi-select, "Other" custom-answer inputs, and `Submit (x/y)` progress (OpenCode + Codex + Claude).
42
+ - **Syntax highlighting, file-path linkification, language labels, hover-only copy buttons** — All across OpenCode code blocks.
43
+ - **Throttled streaming + live thinking** — 32ms render throttle during streaming; thinking blocks update in real time.
44
+ - **MCP Settings toggle matrix** — Replaced drag-and-drop pill system with a clickable permission grid: tool groups as rows, profiles as columns.
45
+ - **Universal MCP add form** — Smart-paste textarea auto-detects 5 config formats; multi-platform sync registers a server to every selected CLI in one action.
46
+ - **Capability badges & filters (OpenCode)** — 9 colored pills (Reason, Tools, Vision, Audio, Video, PDF, ImgGen, TTS, Files) per model, with filter pills in chat dropdown, Models, and Providers settings.
47
+ - **AskUserQuestion polish** — Consistent card spacing, always-available custom-answer input, multi-select square checkboxes, and "Select all that apply" hint text.
48
+
49
+ ## Terminal & Rendering
50
+
51
+ - **Stable xterm DOM renderer for long CLI sessions** — Skips `addon-webgl` / `addon-canvas` to avoid WebGL atlas/context corruption seen across multiple long-lived floating CLI sessions. Output writes are serialized into 64 KB chunks with backpressure.
52
+ - **Snapshot-based reconnects** — Per-PTY virtual-terminal buffer (with alt-buffer support) replaces arbitrary raw-ANSI tail replay on reconnect, so reconnects render correctly without replaying mid-frame escape sequences.
53
+ - **Single xterm renderer** — Collapsed the legacy HTML terminal renderer (~2,400 lines removed). All CLI profiles route through xterm + WebGL with Canvas fallback.
54
+ - **Long-session rendering glitches fixed** — Periodic texture-atlas pruning, coalesced resize fits, 64 KB output-burst cap, ordered WebGL dispose on close, Windows ConPTY hint, and post-open fit+refresh for fresh and resumed sessions.
55
+ - **Terminal tab close/detach reliability** — Enlarged hit targets and suppressed redundant re-renders so the × and detach icons land during CLI streaming.
56
+ - **Banner no longer doubles on reconnect** — PTY buffer replay now resets the xterm before paint. Ink splash screens render once.
57
+ - **Boot flicker eliminated** — PTY launches at the real viewport size instead of 120×30, removing the SIGWINCH redraw and stray zsh `%`.
58
+ - **Floating terminal Escape forwarding** — Raw `\x1b` now reaches OpenCode when the floating window is focused, restoring cancel/close behavior.
59
+
60
+ ## Automation Studio
61
+
62
+ - **Loop launch no longer hangs on orphan Chrome** — Pre-launch sweep kills stale `user-data-dir` Chromiums (Unix `pgrep -f` + `SIGKILL`, Windows PowerShell + `taskkill`) before relaunching. A 45 s `launchPersistentContext` timeout acts as a final safety net.
63
+ - **Loop tab isolation hardened** — `SYNABUN_BROWSER_TAB` env now wins over caller-supplied `tabId` so an agent or model can't accidentally leak into another loop's tab. Removed the active-tab churn from concurrent loop launches.
64
+ - **Codex loop fast-fail detection** — Iteration sentinel now captures exit code and per-iter duration. Three consecutive non-zero exits or sub-30 s iterations stop the loop with `fast-fail-rc` / `fast-fail-duration` instead of running ghost iterations.
65
+ - **Zombie loop file fix** — PTY exit handler now marks the on-disk loop file `active: false` with `stoppedReason: 'pty-exit-<code>'`, ending phantom-running loop UI state.
66
+ - **Diagnostic alerts when toast host is offscreen** — Loop attach and confirm-launch failures fall back to a browser `alert()` when the terminal panel is hidden, so the error reaches the user instead of disappearing into an invisible toast.
67
+ - **User-created folders in sidebar** — Collapsible folders with name, color, and icon. Drag-and-drop to move templates, right-click for rename/recolor/delete. Included in backup/restore and export/import.
68
+ - **Per-template launch defaults** — Each template remembers CLI, model, thinking effort, and MCP profile. Launch modal pre-selects the preset.
69
+ - **OpenCode CLI as fourth option** — Alongside Claude, Codex, Gemini with a curated model list.
70
+ - **Runtime card cleanup** — CLI, Model, MCP Profile moved from chips to native selects in a 2-column "Runtime" layout. Labels no longer truncate.
71
+ - **Per-loop stop** — Stop button targets a specific loop instead of killing every running automation.
72
+
73
+ ## CLI Integrations & MCP
74
+
75
+ - **Auto-repair of legacy plugin installs on startup** — Purges old `synabun-symlink` marketplace entries and reinstalls via the Claude Code CLI so installs survive CC restarts. `POST /api/plugins/repair` triggers the same flow manually.
76
+ - **Self-fetching runtime preflight** — `uvx`, `pipx`, `npx`, `bunx`, `pnpm dlx`, `yarn dlx`, etc. no longer trigger local dep installs. Missing binaries surface OS-specific install hints (`brew install uv`, winget, etc.) instead of failing silently at launch.
77
+ - **GitHub MCP installer no longer discards servers on dep failure** — Local `pip install` / `npm install` errors are now soft warnings; the server still registers and the user sees a `⚠` line in the install form.
78
+ - **Codex MCP approval respects per-tab Auto state** — The silent MCP accept path is now gated on the visible tab's Auto toggle and travels with each query, preventing background accept of `mcp__SynaBun__remember` and similar tools when Auto is off.
79
+ - **Unified update commands** — OpenCode, Claude Code, Codex, and Gemini CLI all update via `npm install -g <package>@latest`, plus OpenCode's built-in `opencode upgrade` fallback.
80
+ - **Codex Settings wired to SDK** — Config read/write via correct RPCs; model, reasoning effort, approval policy, sandbox mode, and web search read from a cached effective config. Layer badges (`user` / `project` / `default`) per field.
81
+ - **Codex MCP env fix** — Added `DOTENV_PATH`, `SYNABUN_DATA_HOME`, `MEMORY_DATA_DIR` so the server finds its data directory.
82
+ - **OpenCode OAuth flow repaired** — Authorize body now sends `{ method: idx }`, auth buttons only render when methods actually exist, save/failure toasts added.
83
+ - **Native skills cross-runtime** — `/synabun` and `/leonardo` no longer hard-depend on `AskUserQuestion` or Claude-only tool names.
84
+ - **MCP server add on Windows** — `claude mcp add` command fixed: removed `--` separator, properly quotes `-e "KEY=VALUE"` pairs across PowerShell shims.
85
+
86
+ ## Memory / Plan / Session
87
+
88
+ - **"Continue planning" action across OpenCode, Codex, and Claude** — Post-plan card now offers Continue planning alongside Continue / Edit / Compact, allowing iterative plan refinement without exiting plan mode.
89
+ - **Codex plan mode handoff hardened** — Post-plan actions persist across tab state, plan turns run read-only server-side, and the prompt prefix instructs Codex to stop after producing the plan and wait for the sidepanel choice.
90
+ - **Plan files reorganized** — Plans now live in `data/plans/YYYY-MM-DD/slug.md` with descriptive kebab-case names. Legacy flat plans auto-migrate. Root `PLAN.md` is cleaned up on ExitPlanMode.
91
+ - **Recall output always shows full metadata** — Tags, related files, and source always appear (with `none` fallback instead of being omitted).
92
+ - **Session cache & cross-provider discovery** — Codex and OpenCode session discovery endpoints added; Claude paginates server-side, Codex/OpenCode virtual-paginate from SQLite.
93
+ - **Claude sidepanel cross-talk fix** — Orphan registry now keys by `windowId:sessionId` so multi-tab disconnects can't hand the wrong process to a reconnecting tab.
94
+
95
+ ## Backup & Restore
96
+
97
+ - **Backup coverage expanded** — Auto and manual backups now include `stored-plans.json`, `synabun-plugins.json`, `mcp-registry.json`, `active-profile.json`, and the recursive `data/plans/YYYY-MM-DD/*.md` plan tree. Backward compatible with old backups (`version 1 || 2`).
98
+
99
+ ## Automation & Browser
100
+
101
+ - **Browser session ownership model** — `_loopOwned` / `_agentOwned` flags with registry cross-checks prevent loops and sidepanel Claude from fighting over the same tab. Pinned sessions auto-recover if destroyed.
102
+ - **Browser tooling upgrades** — New `browser_cheatsheet` tool replaces bloated per-platform descriptions. `browser_snapshot` adds `interactive` / `landmarks` modes and viewport filtering. `browser_navigate` / `click` / `scroll` can return a snapshot in one call. Failing selectors auto-heal via a `textHint` param.
103
+
104
+ ## Stability & Fixes
105
+
106
+ - **High and moderate npm audit CVEs patched** — Bumped `playwright` to ^1.59.1 (SSL cert verification CVE) and `uuid` to ^14 (buffer bounds), and added `overrides` for `lodash`, `protobufjs`, `hono`, `@hono/node-server`, `postcss`, and `vite` across both Neural Interface and MCP server packages. `npm audit` now clean.
107
+ - **Codex sidepanel cross-talk fix** — Per-tab WebSocket identity (every message stamped with `sessionId`), strict orphan reattach keys (`windowId + sessionId`), and reconnect-state cleanup prevent buffered tool-call events from one tab replaying into another.
108
+ - **Codex sidepanel stop scope** — Stop button now aborts only the active tab instead of every running Codex tab; server validates `sessionId` / `threadId` on `interrupt` / `force_kill` and refuses unscoped stop messages.
109
+ - **OpenCode post-plan card no longer resurrects** — `setTabMode()` clears the post-plan flag on `plan → non-plan` transitions and the renderer guards on `tab.mode === 'plan'`, so a leaked flag can't re-render the PLAN COMPLETE card after the user moved to implementation.
110
+ - **Codex edit-plan cancel handoff** — Closing the file editor without saving now re-renders a fresh clickable post-plan card; saving reopens the PLAN UPDATED handoff correctly.
111
+ - **Right-side toolbar reservation tracks all owners** — Per-owner reservation map prevents one closing sidepanel from clearing layout while another right panel is still active.
112
+ - **Restored Claude tabs keep stable IDs** — Sidepanel tray placeholder click-through targets the restored tab instead of losing selection because the panel generated a fresh UUID during bootstrap.
113
+ - **Stop button + ESC reliability** — Abort works from anywhere in the Claude, Codex, and OpenCode panels, cancels in-flight fetches server-side via AbortController, and covers every running tab.
114
+ - **Orphan grace extended to 30 min** — Claude and Codex processes no longer die during brief WebSocket drops (sleep, network, tab throttling). Added server-side ping/pong keepalive.
115
+ - **1M context window activation** — The `[1m]` model-ID suffix is now applied correctly so Opus/Sonnet 1M variants actually use the 1M beta window.
116
+ - **Plan mode flow fixes** — Answers to `AskUserQuestion` in plan mode are now re-injected per-question instead of collapsed into one comma-joined string. Plan-approval card fires at detection, not after implementation streams. Process is killed on ExitPlanMode so buffered tool calls can't execute before the user approves.
117
+ - **Blank assistant row pivot fix** — Claude no longer leaves an empty bubble when an assistant message contains only tool calls or whitespace-only text.
118
+ - **"DONE" toast no longer fires mid-turn (OpenCode)** — Completion notifications wait for the real `session.idle` event.
119
+ - **Git UI on large-status repos** — `git status` no longer throws on repos with tens of thousands of changes; branch button and git popover render even when parsing fails.
120
+ - **Notifications permission status** — Updates live when the user unblocks browser notifications, with a manual Re-check button.
121
+ - **CLI notification spam suppressed** — Repeated OS notifications during long AskUserQuestion / permission prompts dedupe until the session returns to idle.
122
+ - **Concurrent agent/loop browser sessions** — Ownership flags set immediately on creation; grace timer uses in-memory ownership checks instead of racy file reads.
123
+ - **Neural Interface crash on Playwright dialog race** — `No dialog is showing` ProtocolError now ignored alongside frame-detached errors.
124
+
125
+ ## Windows Support
126
+
127
+ - **Git UI reliability** — Resolved silent failures from PATH shims, old Git versions, and cmd.exe quoting. Works across Scoop, winget, and older Git for Windows, with a startup probe that surfaces problems.
128
+ - **CLI auto-detect after reinstall** — Detection commands now run with an augmented PATH including `~/.local/bin`, `/usr/local/bin`, `~/.npm-global/bin`.
129
+ - **Native dropdowns in dark mode** — Added `color-scheme: dark` so Windows Chrome/Edge stop rendering native `<select>` popups with white backgrounds.
@@ -24315,3 +24315,73 @@
24315
24315
  .ig-lb-prev { left: 16px; }
24316
24316
  .ig-lb-next { right: 16px; }
24317
24317
 
24318
+ /* ── Update-check toast (Apps > Check for updates) ── */
24319
+ .update-check-toast {
24320
+ position: fixed;
24321
+ right: 20px;
24322
+ bottom: 80px;
24323
+ z-index: 300050;
24324
+ display: flex;
24325
+ gap: 12px;
24326
+ align-items: flex-start;
24327
+ min-width: 240px;
24328
+ max-width: 360px;
24329
+ padding: 12px 14px;
24330
+ border-radius: 14px;
24331
+ background: rgba(20, 20, 24, 0.85);
24332
+ backdrop-filter: blur(20px) saturate(160%);
24333
+ -webkit-backdrop-filter: blur(20px) saturate(160%);
24334
+ border: 1px solid rgba(255, 255, 255, 0.08);
24335
+ color: var(--t-text);
24336
+ font-size: 12px;
24337
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
24338
+ opacity: 0;
24339
+ transform: translateY(8px);
24340
+ transition: opacity 0.25s ease, transform 0.25s ease, background 0.15s ease;
24341
+ pointer-events: auto;
24342
+ }
24343
+ .update-check-toast.show { opacity: 1; transform: translateY(0); }
24344
+ .update-check-toast--clickable { cursor: pointer; }
24345
+ .update-check-toast--clickable:hover { background: rgba(28, 28, 32, 0.92); }
24346
+ .update-check-toast .utc-icon {
24347
+ width: 24px; height: 24px;
24348
+ border-radius: 50%;
24349
+ display: flex; align-items: center; justify-content: center;
24350
+ font-weight: 700; font-size: 14px;
24351
+ flex-shrink: 0;
24352
+ }
24353
+ .update-check-toast .utc-icon--up { background: rgba(52, 199, 89, 0.18); color: #34c759; }
24354
+ .update-check-toast .utc-icon--ok { background: rgba(52, 199, 89, 0.18); color: #34c759; }
24355
+ .update-check-toast .utc-icon--err { background: rgba(255, 69, 58, 0.18); color: #ff453a; }
24356
+ .update-check-toast .utc-body { flex: 1; min-width: 0; }
24357
+ .update-check-toast .utc-title { font-weight: 600; margin-bottom: 4px; }
24358
+ .update-check-toast .utc-list { list-style: none; margin: 0; padding: 0; }
24359
+ .update-check-toast .utc-list li { padding: 2px 0; color: var(--t-muted); }
24360
+ .update-check-toast .utc-list li .utc-src {
24361
+ display: inline-block;
24362
+ padding: 1px 6px;
24363
+ margin-left: 6px;
24364
+ border-radius: 6px;
24365
+ font-size: 10px;
24366
+ background: rgba(255, 255, 255, 0.08);
24367
+ color: var(--t-text);
24368
+ }
24369
+ .update-check-toast .utc-sub { color: var(--t-muted); }
24370
+ .update-check-toast .utc-hint { color: var(--t-muted); opacity: 0.7; margin-top: 6px; font-size: 11px; }
24371
+
24372
+ .update-channel-pill {
24373
+ display: inline-block;
24374
+ padding: 2px 8px;
24375
+ margin-left: 8px;
24376
+ font-size: 10px;
24377
+ font-weight: 600;
24378
+ letter-spacing: 0.04em;
24379
+ text-transform: uppercase;
24380
+ border-radius: 6px;
24381
+ vertical-align: middle;
24382
+ background: rgba(255, 159, 10, 0.18);
24383
+ color: #ff9f0a;
24384
+ }
24385
+ .update-channel-pill--beta { background: rgba(255, 159, 10, 0.18); color: #ff9f0a; }
24386
+
24387
+
@@ -12,7 +12,8 @@ import { openHelp } from './ui-help.js';
12
12
  import { registerAction } from './ui-keybinds.js';
13
13
  import { isGuest, hasPermission } from './ui-sync.js';
14
14
  import { sendToPanel } from './ui-claude-panel.js';
15
- import { forceCheckToolUpdates } from './ui-tool-updates.js';
15
+ import { forceCheckToolUpdates, showUpdateToast, TOOL_LABELS } from './ui-tool-updates.js';
16
+ import { forceCheckSynabunUpdate, openSynabunUpdateModal } from './ui-update.js';
16
17
 
17
18
  const $ = (id) => document.getElementById(id);
18
19
 
@@ -370,7 +371,34 @@ function wireTerminalMenu() {
370
371
  'menu-terminal-codex': () => emit('terminal:open', { profile: 'codex' }),
371
372
  'menu-terminal-gemini': () => emit('terminal:open', { profile: 'gemini' }),
372
373
  'menu-terminal-opencode': () => emit('terminal:open', { profile: 'opencode' }),
373
- 'menu-check-cli-updates': () => forceCheckToolUpdates(),
374
+ 'menu-check-cli-updates': async () => {
375
+ const [tools, syn] = await Promise.all([
376
+ forceCheckToolUpdates(),
377
+ forceCheckSynabunUpdate(),
378
+ ]);
379
+
380
+ const updates = [];
381
+ for (const [key, info] of Object.entries(tools?.tools || {})) {
382
+ if (info?.updateAvailable) {
383
+ const label = `${TOOL_LABELS[key] || key} v${info.installed} → v${info.latest}`;
384
+ updates.push({ label });
385
+ }
386
+ }
387
+ if (syn?.updateAvailable) {
388
+ const srcLabel = syn.source === 'both' ? 'npm + GitHub' : syn.source === 'github' ? 'GitHub' : 'npm';
389
+ updates.push({ label: `SynaBun v${syn.current} → v${syn.latest}`, source: srcLabel });
390
+ }
391
+
392
+ const errors = [];
393
+ if (!tools) errors.push('CLI versions');
394
+ if (!syn) errors.push('SynaBun version');
395
+
396
+ showUpdateToast({
397
+ updates,
398
+ errors,
399
+ onClick: syn?.updateAvailable ? () => openSynabunUpdateModal() : null,
400
+ });
401
+ },
374
402
  'menu-terminal-shell': () => emit('terminal:open', { profile: 'shell' }),
375
403
  'menu-terminal-browser': () => emit('browser:open'),
376
404
  'menu-terminal-youtube': () => emit('browser:open', { url: 'https://www.youtube.com' }),
@@ -174,7 +174,81 @@ export async function initToolUpdates() {
174
174
  export async function forceCheckToolUpdates() {
175
175
  try {
176
176
  const res = await fetch('/api/system/tool-versions?force=1');
177
- if (!res.ok) return;
178
- applyBadges(await res.json());
179
- } catch { /* silent */ }
177
+ if (!res.ok) return null;
178
+ const data = await res.json();
179
+ applyBadges(data);
180
+ return data;
181
+ } catch { return null; }
182
+ }
183
+
184
+ // Re-export human-readable tool labels so toast/menu code can build summaries
185
+ // without redefining the label map.
186
+ export { TOOL_LABELS };
187
+
188
+ // ── Update-check toast ──
189
+ // Bottom-right ephemeral pill. One of three modes: updates available (with
190
+ // list + optional click handler), all-clear, or check-failed. Auto-dismisses.
191
+
192
+ function _toastEsc(s) {
193
+ const d = document.createElement('div');
194
+ d.textContent = String(s ?? '');
195
+ return d.innerHTML;
196
+ }
197
+
198
+ export function showUpdateToast({ updates, errors, onClick } = {}) {
199
+ document.querySelectorAll('.update-check-toast').forEach(el => el.remove());
200
+
201
+ const toast = document.createElement('div');
202
+ toast.className = 'update-check-toast';
203
+
204
+ const hasUpdates = Array.isArray(updates) && updates.length > 0;
205
+ const hasErrors = Array.isArray(errors) && errors.length > 0;
206
+ const clickable = hasUpdates && typeof onClick === 'function';
207
+ if (clickable) toast.classList.add('update-check-toast--clickable');
208
+
209
+ if (hasUpdates) {
210
+ const items = updates.map(u => {
211
+ const label = _toastEsc(u?.label || '');
212
+ const src = u?.source ? `<span class="utc-src">${_toastEsc(u.source)}</span>` : '';
213
+ return `<li>${label}${src}</li>`;
214
+ }).join('');
215
+ toast.innerHTML = `
216
+ <div class="utc-icon utc-icon--up">↑</div>
217
+ <div class="utc-body">
218
+ <div class="utc-title">Updates available</div>
219
+ <ul class="utc-list">${items}</ul>
220
+ ${clickable ? '<div class="utc-hint">Click to open update guide</div>' : ''}
221
+ </div>`;
222
+ } else if (hasErrors) {
223
+ toast.innerHTML = `
224
+ <div class="utc-icon utc-icon--err">!</div>
225
+ <div class="utc-body">
226
+ <div class="utc-title">Update check failed</div>
227
+ <div class="utc-sub">${_toastEsc(errors.join(' · '))}</div>
228
+ </div>`;
229
+ } else {
230
+ toast.innerHTML = `
231
+ <div class="utc-icon utc-icon--ok">✓</div>
232
+ <div class="utc-body">
233
+ <div class="utc-title">All up to date</div>
234
+ </div>`;
235
+ }
236
+
237
+ if (clickable) {
238
+ toast.addEventListener('click', () => {
239
+ try { onClick(); } catch {}
240
+ toast.classList.remove('show');
241
+ setTimeout(() => toast.remove(), 250);
242
+ });
243
+ }
244
+
245
+ document.body.appendChild(toast);
246
+ requestAnimationFrame(() => toast.classList.add('show'));
247
+
248
+ const ttl = hasUpdates ? 7000 : hasErrors ? 5000 : 3500;
249
+ setTimeout(() => {
250
+ if (!toast.isConnected) return;
251
+ toast.classList.remove('show');
252
+ setTimeout(() => toast.remove(), 250);
253
+ }, ttl);
180
254
  }
@@ -7,27 +7,55 @@
7
7
  const $ = (id) => document.getElementById(id);
8
8
 
9
9
  let _versionData = null;
10
+ let _btnWired = false;
11
+
12
+ function applyTopRightButton(data) {
13
+ const btn = $('topright-update-btn');
14
+ const badge = $('update-badge');
15
+ if (!btn) return;
16
+ const has = !!data?.updateAvailable;
17
+ btn.style.display = has ? '' : 'none';
18
+ if (badge) badge.textContent = has ? '!' : '';
19
+ if (has) {
20
+ const srcLabel = data.source === 'both' ? 'npm + GitHub' : data.source === 'github' ? 'GitHub' : 'npm';
21
+ btn.dataset.tooltip = `Update: v${data.current} → v${data.latest} (${srcLabel})`;
22
+ }
23
+ if (!_btnWired) {
24
+ _btnWired = true;
25
+ btn.addEventListener('click', (e) => {
26
+ e.stopPropagation();
27
+ openUpdateModal();
28
+ });
29
+ }
30
+ }
10
31
 
11
32
  export async function initUpdate() {
12
33
  try {
13
34
  const res = await fetch('/api/system/version');
14
35
  if (!res.ok) return;
15
36
  _versionData = await res.json();
37
+ applyTopRightButton(_versionData);
38
+ } catch { /* silent — no update UI if check fails */ }
39
+ }
16
40
 
17
- if (!_versionData.updateAvailable) return;
18
-
19
- const btn = $('topright-update-btn');
20
- if (!btn) return;
21
-
22
- btn.style.display = '';
23
- const badge = $('update-badge');
24
- if (badge) badge.textContent = '!';
41
+ // Force a fresh server-side check (bypasses 1h cache via ?force=1) and
42
+ // re-applies the top-right button + badge state. Returns the latest
43
+ // _synabunUpdateCache shape so callers can compose toasts.
44
+ export async function forceCheckSynabunUpdate() {
45
+ try {
46
+ const res = await fetch('/api/system/version?force=1');
47
+ if (!res.ok) return null;
48
+ _versionData = await res.json();
49
+ applyTopRightButton(_versionData);
50
+ return _versionData;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
25
55
 
26
- btn.addEventListener('click', (e) => {
27
- e.stopPropagation();
28
- openUpdateModal();
29
- });
30
- } catch { /* silent — no update UI if check fails */ }
56
+ export function openSynabunUpdateModal() {
57
+ if (!_versionData?.updateAvailable) return;
58
+ openUpdateModal();
31
59
  }
32
60
 
33
61
  // ── Modal Wizard ──
@@ -59,16 +87,37 @@ function openUpdateModal() {
59
87
  }
60
88
 
61
89
  function renderStep1() {
90
+ const src = _versionData.source;
91
+ const channel = _versionData.installedChannel || 'stable';
92
+ const channelTag = channel === 'prerelease'
93
+ ? '<span class="update-channel-pill update-channel-pill--beta">beta</span>'
94
+ : '';
95
+
96
+ const srcCopy = src === 'both'
97
+ ? 'A new version of SynaBun is available on npm and GitHub.'
98
+ : src === 'github'
99
+ ? 'A new SynaBun release is tagged on GitHub. The npm publish may still be in progress.'
100
+ : 'A new version of SynaBun has been published to npm.';
101
+
102
+ const npmStable = _versionData.npmLatestStable
103
+ ? `npm latest v${esc(_versionData.npmLatestStable)}`
104
+ : 'npm latest —';
105
+ const npmBeta = _versionData.npmLatestBeta
106
+ ? ` · npm beta v${esc(_versionData.npmLatestBeta)}`
107
+ : '';
108
+ const gitBit = _versionData.gitLatest
109
+ ? ` · GitHub v${esc(_versionData.gitLatest)}`
110
+ : ' · GitHub —';
111
+
62
112
  return `
63
- <h3 class="update-modal-title">Update Available</h3>
113
+ <h3 class="update-modal-title">Update Available ${channelTag}</h3>
64
114
  <div class="update-version-diff">
65
115
  <span class="update-ver update-ver--old">v${esc(_versionData.current)}</span>
66
116
  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
67
117
  <span class="update-ver update-ver--new">v${esc(_versionData.latest)}</span>
68
118
  </div>
69
- <p style="font-size:12px;color:var(--t-muted);margin:12px 0 18px">
70
- A new version of SynaBun is available on npm.
71
- </p>
119
+ <p style="font-size:12px;color:var(--t-muted);margin:12px 0 6px">${srcCopy}</p>
120
+ <p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:0 0 18px">${npmStable}${npmBeta}${gitBit}</p>
72
121
  <div class="tag-delete-modal-actions">
73
122
  <button class="action-btn action-btn--ghost" id="update-cancel">Cancel</button>
74
123
  <button class="action-btn action-btn--primary" id="update-next">Next</button>
@@ -100,14 +149,22 @@ function openUpdateModal() {
100
149
  }
101
150
 
102
151
  function renderStep3() {
152
+ // Channel-aware install command. Beta users staying on beta keep the
153
+ // @beta tag; both targets exist on stable so we surface a single line.
154
+ const channel = _versionData.installedChannel || 'stable';
155
+ const target = _versionData.latest || '';
156
+ const targetLooksBeta = parseSemverPre(target);
157
+ const installTag = (channel === 'prerelease' && targetLooksBeta) ? 'beta' : 'latest';
158
+ const installCmd = `npm install -g synabun@${installTag}`;
159
+
103
160
  return `
104
161
  <h3 class="update-modal-title">Run the Update</h3>
105
162
  <p style="font-size:12px;color:var(--t-muted);margin:4px 0 12px">
106
163
  Run this command in your terminal to update SynaBun:
107
164
  </p>
108
165
  <div class="update-code-block">
109
- <code>npm install -g synabun@latest</code>
110
- <button class="update-copy-btn" data-copy="npm install -g synabun@latest" title="Copy">
166
+ <code>${esc(installCmd)}</code>
167
+ <button class="update-copy-btn" data-copy="${esc(installCmd)}" title="Copy">
111
168
  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
112
169
  </button>
113
170
  </div>
@@ -242,3 +299,12 @@ function esc(str) {
242
299
  d.textContent = str || '';
243
300
  return d.innerHTML;
244
301
  }
302
+
303
+ // Returns the prerelease tag from a semver-ish string ("2026.4.26-beta.3"
304
+ // -> "beta.3"), or null. Used to pick install command target.
305
+ function parseSemverPre(raw) {
306
+ if (raw == null) return null;
307
+ const s = String(raw).trim().replace(/^v\.?/, '');
308
+ const m = s.match(/^\d+\.\d+\.\d+(?:-([0-9A-Za-z.-]+))?/);
309
+ return m && m[1] ? m[1] : null;
310
+ }
@@ -10073,60 +10073,182 @@ app.post('/api/search/memories', async (req, res) => {
10073
10073
  });
10074
10074
 
10075
10075
  // ═══════════════════════════════════════════
10076
- // NPM UPDATE CHECK
10076
+ // SYNABUN UPDATE CHECK (NPM + GITHUB RELEASES)
10077
10077
  // ═══════════════════════════════════════════
10078
+ //
10079
+ // Checks both the npm registry AND the project's GitHub releases. Alerts
10080
+ // when EITHER source is newer than the installed version (catches one-sided
10081
+ // publishes — e.g. release tagged on GitHub before npm publish completes).
10082
+
10083
+ let _synabunUpdateCache = {
10084
+ current: null,
10085
+ installedChannel: 'stable', // 'stable' | 'prerelease'
10086
+ npmLatestStable: null, // dist-tags.latest
10087
+ npmLatestBeta: null, // dist-tags.beta (or null if absent)
10088
+ npmLatest: null, // chosen target for installed channel
10089
+ gitLatest: null, // tag_name from GitHub release matching channel
10090
+ latest: null, // newer of (npmLatest, gitLatest) — legacy field
10091
+ npmUpdateAvailable: false,
10092
+ gitUpdateAvailable: false,
10093
+ updateAvailable: false, // npmUpdateAvailable || gitUpdateAvailable
10094
+ source: null, // 'npm' | 'github' | 'both' | null
10095
+ checkedAt: null,
10096
+ npmError: null,
10097
+ gitError: null,
10098
+ };
10078
10099
 
10079
- let _npmUpdateCache = { current: null, latest: null, updateAvailable: false, checkedAt: null };
10080
-
10081
- async function checkNpmUpdate() {
10100
+ async function checkSynabunUpdate() {
10101
+ let current = _synabunUpdateCache.current;
10082
10102
  try {
10083
- const pkgPath = resolve(PACKAGE_ROOT, 'package.json');
10084
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
10085
- const current = pkg.version;
10086
- _npmUpdateCache.current = current;
10103
+ const pkg = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8'));
10104
+ current = pkg.version;
10105
+ } catch { /* keep prior current if package.json read fails */ }
10087
10106
 
10088
- const controller = new AbortController();
10089
- const timeout = setTimeout(() => controller.abort(), 5000);
10090
- const res = await fetch('https://registry.npmjs.org/synabun/latest', { signal: controller.signal });
10091
- clearTimeout(timeout);
10107
+ const curParsed = parseSemver(current);
10108
+ const installedChannel = curParsed?.pre ? 'prerelease' : 'stable';
10092
10109
 
10093
- if (!res.ok) throw new Error(`npm registry returned ${res.status}`);
10094
- const data = await res.json();
10095
- const latest = data.version;
10110
+ const fetchJsonWithTimeout = async (url, ms = 5000) => {
10111
+ const controller = new AbortController();
10112
+ const timeout = setTimeout(() => controller.abort(), ms);
10113
+ try {
10114
+ const res = await fetch(url, {
10115
+ signal: controller.signal,
10116
+ headers: { 'Accept': 'application/json', 'User-Agent': 'synabun-update-check' },
10117
+ });
10118
+ if (!res.ok) throw new Error(`${url} returned ${res.status}`);
10119
+ return await res.json();
10120
+ } finally {
10121
+ clearTimeout(timeout);
10122
+ }
10123
+ };
10096
10124
 
10097
- const cur = current.split('.').map(Number);
10098
- const lat = latest.split('.').map(Number);
10099
- const updateAvailable = lat[0] > cur[0]
10100
- || (lat[0] === cur[0] && lat[1] > cur[1])
10101
- || (lat[0] === cur[0] && lat[1] === cur[1] && lat[2] > cur[2]);
10125
+ // npm: fetch the FULL registry document so we can read every dist-tag
10126
+ // (latest + beta + any future channel). The /latest shorthand only
10127
+ // surfaces dist-tags.latest, which made beta installs invisible.
10128
+ // github: when installed is a prerelease, /releases/latest is wrong
10129
+ // because it skips prereleases — fetch the full list and pick newest.
10130
+ const npmUrl = 'https://registry.npmjs.org/synabun';
10131
+ const githubUrl = installedChannel === 'prerelease'
10132
+ ? 'https://api.github.com/repos/danilokhury/Synabun/releases?per_page=20'
10133
+ : 'https://api.github.com/repos/danilokhury/Synabun/releases/latest';
10134
+
10135
+ const [npmRes, gitRes] = await Promise.allSettled([
10136
+ fetchJsonWithTimeout(npmUrl),
10137
+ fetchJsonWithTimeout(githubUrl),
10138
+ ]);
10102
10139
 
10103
- _npmUpdateCache = { current, latest, updateAvailable, checkedAt: new Date().toISOString() };
10140
+ let npmLatestStable = null, npmLatestBeta = null;
10141
+ let gitLatest = null;
10142
+ let npmError = null, gitError = null;
10104
10143
 
10105
- if (updateAvailable) {
10106
- console.log(` Updates: v${current} → v${latest} available`);
10144
+ if (npmRes.status === 'fulfilled') {
10145
+ const tags = npmRes.value?.['dist-tags'] || {};
10146
+ npmLatestStable = tags.latest || null;
10147
+ npmLatestBeta = tags.beta || tags.next || tags.rc || null;
10148
+ } else {
10149
+ npmError = npmRes.reason?.message || 'npm fetch failed';
10150
+ }
10151
+
10152
+ if (gitRes.status === 'fulfilled') {
10153
+ if (Array.isArray(gitRes.value)) {
10154
+ // /releases — pick newest non-draft entry whose tag parses as semver.
10155
+ // For stable channel users we'd skip prereleases, but this branch only
10156
+ // runs for prerelease users (per githubUrl above), so include both.
10157
+ let best = null;
10158
+ for (const r of gitRes.value) {
10159
+ if (r?.draft) continue;
10160
+ const tag = r?.tag_name || r?.name || '';
10161
+ const parsed = parseSemver(tag);
10162
+ if (!parsed) continue;
10163
+ if (!best || compareSemver(parsed, best.parsed) > 0) {
10164
+ best = { tag, parsed };
10165
+ }
10166
+ }
10167
+ gitLatest = best?.tag.replace(/^v\.?/, '') || null;
10107
10168
  } else {
10108
- console.log(` Updates: up to date (v${current})`);
10169
+ // /releases/latest — single object
10170
+ const tag = gitRes.value?.tag_name || gitRes.value?.name || '';
10171
+ gitLatest = tag ? tag.replace(/^v\.?/, '') : null;
10109
10172
  }
10110
- } catch (err) {
10111
- if (!_npmUpdateCache.current) {
10112
- try {
10113
- const pkg = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8'));
10114
- _npmUpdateCache.current = pkg.version;
10115
- } catch {}
10173
+ } else {
10174
+ gitError = gitRes.reason?.message || 'github fetch failed';
10175
+ }
10176
+
10177
+ // Channel-aware npm target:
10178
+ // - prerelease user: pick whichever of (stable, beta) is newest. Either
10179
+ // is genuinely "newer than current"; surfacing stable when on beta is
10180
+ // intentional (some users manually flip back to stable).
10181
+ // - stable user: only stable. Beta on a stable install would look like
10182
+ // a downgrade ("2026.4.26 → 2026.4.26-beta.3" via semver = older).
10183
+ let npmLatest = null;
10184
+ if (installedChannel === 'prerelease') {
10185
+ if (npmLatestStable && npmLatestBeta) {
10186
+ npmLatest = compareSemver(npmLatestBeta, npmLatestStable) > 0 ? npmLatestBeta : npmLatestStable;
10187
+ } else {
10188
+ npmLatest = npmLatestStable || npmLatestBeta;
10116
10189
  }
10117
- _npmUpdateCache.checkedAt = new Date().toISOString();
10118
- console.log(` Updates: check failed (${err.message})`);
10190
+ } else {
10191
+ npmLatest = npmLatestStable;
10192
+ }
10193
+
10194
+ const npmUpdateAvailable = !!(npmLatest && semverNewer(current, npmLatest));
10195
+ const gitUpdateAvailable = !!(gitLatest && semverNewer(current, gitLatest));
10196
+ const updateAvailable = npmUpdateAvailable || gitUpdateAvailable;
10197
+
10198
+ // Newer of the two (npm vs github) for legacy "latest" display.
10199
+ let latest = null;
10200
+ if (npmLatest && gitLatest) {
10201
+ latest = compareSemver(npmLatest, gitLatest) >= 0 ? npmLatest : gitLatest;
10202
+ } else {
10203
+ latest = npmLatest || gitLatest;
10204
+ }
10205
+
10206
+ let source = null;
10207
+ if (npmUpdateAvailable && gitUpdateAvailable) source = 'both';
10208
+ else if (npmUpdateAvailable) source = 'npm';
10209
+ else if (gitUpdateAvailable) source = 'github';
10210
+
10211
+ _synabunUpdateCache = {
10212
+ current,
10213
+ installedChannel,
10214
+ npmLatestStable,
10215
+ npmLatestBeta,
10216
+ npmLatest,
10217
+ gitLatest,
10218
+ latest,
10219
+ npmUpdateAvailable,
10220
+ gitUpdateAvailable,
10221
+ updateAvailable,
10222
+ source,
10223
+ checkedAt: new Date().toISOString(),
10224
+ npmError,
10225
+ gitError,
10226
+ };
10227
+
10228
+ if (updateAvailable) {
10229
+ console.log(` Updates: v${current} → v${latest} (${source}; channel: ${installedChannel})`);
10230
+ } else if (npmError && gitError) {
10231
+ console.log(` Updates: check failed (npm: ${npmError}; github: ${gitError})`);
10232
+ } else if (npmError) {
10233
+ console.log(` Updates: up to date (v${current}; npm check failed: ${npmError})`);
10234
+ } else if (gitError) {
10235
+ console.log(` Updates: up to date (v${current}; github check failed: ${gitError})`);
10236
+ } else {
10237
+ console.log(` Updates: up to date (v${current}; channel: ${installedChannel})`);
10119
10238
  }
10120
10239
  }
10121
10240
 
10122
10241
  app.get('/api/system/version', async (req, res) => {
10123
- if (_npmUpdateCache.checkedAt) {
10124
- const age = Date.now() - new Date(_npmUpdateCache.checkedAt).getTime();
10125
- if (age > 6 * 60 * 60 * 1000) {
10126
- checkNpmUpdate().catch(() => {});
10242
+ const force = req.query.force === '1';
10243
+ if (force || !_synabunUpdateCache.checkedAt) {
10244
+ try { await checkSynabunUpdate(); } catch {}
10245
+ } else {
10246
+ const age = Date.now() - new Date(_synabunUpdateCache.checkedAt).getTime();
10247
+ if (age > 60 * 60 * 1000) {
10248
+ checkSynabunUpdate().catch(() => {});
10127
10249
  }
10128
10250
  }
10129
- res.json(_npmUpdateCache);
10251
+ res.json(_synabunUpdateCache);
10130
10252
  });
10131
10253
 
10132
10254
  // ═══════════════════════════════════════════
@@ -10142,17 +10264,80 @@ const TOOL_DEFS = [
10142
10264
 
10143
10265
  let _toolVersionCache = { checkedAt: null, tools: {} };
10144
10266
 
10267
+ // Backwards-compat helper — returns a "MAJOR.MINOR.PATCH" string (no pre).
10268
+ // Kept because legacy callers expect a string for display purposes only.
10269
+ // Comparison logic must use parseSemver/compareSemver below to handle
10270
+ // prerelease/iteration suffixes correctly.
10145
10271
  function parseVersion(raw) {
10146
- const m = raw.match(/(\d+\.\d+\.\d+)/);
10147
- return m ? m[1] : null;
10272
+ const m = String(raw ?? '').match(/(\d+)\.(\d+)\.(\d+)/);
10273
+ return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
10274
+ }
10275
+
10276
+ // Structured semver parse — handles calver bumps and prerelease tags:
10277
+ // "2026.4.26" -> { major:2026, minor:4, patch:26, pre:null }
10278
+ // "2026.4.20-2" -> { major:2026, minor:4, patch:20, pre:"2" }
10279
+ // "2026.4.26-beta.3" -> { major:2026, minor:4, patch:26, pre:"beta.3" }
10280
+ // "v1.2.3-rc.1" -> { major:1, minor:2, patch:3, pre:"rc.1" }
10281
+ function parseSemver(raw) {
10282
+ if (raw == null) return null;
10283
+ const s = String(raw).trim().replace(/^v\.?/, '');
10284
+ const m = s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
10285
+ if (!m) return null;
10286
+ return {
10287
+ major: +m[1],
10288
+ minor: +m[2],
10289
+ patch: +m[3],
10290
+ pre: m[4] || null,
10291
+ };
10292
+ }
10293
+
10294
+ // Semver-spec precedence including prerelease handling:
10295
+ // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0
10296
+ // Returns negative if a<b, positive if a>b, 0 if equal. Accepts strings or
10297
+ // parsed objects.
10298
+ function compareSemver(a, b) {
10299
+ const pa = (a && typeof a === 'object' && 'major' in a) ? a : parseSemver(a);
10300
+ const pb = (b && typeof b === 'object' && 'major' in b) ? b : parseSemver(b);
10301
+ if (!pa && !pb) return 0;
10302
+ if (!pa) return -1;
10303
+ if (!pb) return 1;
10304
+ if (pa.major !== pb.major) return pa.major - pb.major;
10305
+ if (pa.minor !== pb.minor) return pa.minor - pb.minor;
10306
+ if (pa.patch !== pb.patch) return pa.patch - pb.patch;
10307
+ // Equal x.y.z. Prerelease vs none: a release without prerelease is GREATER.
10308
+ if (!pa.pre && !pb.pre) return 0;
10309
+ if (!pa.pre && pb.pre) return 1;
10310
+ if ( pa.pre && !pb.pre) return -1;
10311
+ // Both have prerelease — compare identifiers per semver §11.4.
10312
+ const ia = pa.pre.split('.');
10313
+ const ib = pb.pre.split('.');
10314
+ const len = Math.max(ia.length, ib.length);
10315
+ for (let i = 0; i < len; i++) {
10316
+ if (i >= ia.length) return -1; // shorter set has lower precedence
10317
+ if (i >= ib.length) return 1;
10318
+ const xa = ia[i], xb = ib[i];
10319
+ const na = /^\d+$/.test(xa), nb = /^\d+$/.test(xb);
10320
+ if (na && nb) {
10321
+ const da = +xa, db = +xb;
10322
+ if (da !== db) return da - db;
10323
+ } else if (na && !nb) {
10324
+ return -1; // numeric < alphanumeric
10325
+ } else if (!na && nb) {
10326
+ return 1;
10327
+ } else {
10328
+ if (xa !== xb) return xa < xb ? -1 : 1;
10329
+ }
10330
+ }
10331
+ return 0;
10148
10332
  }
10149
10333
 
10150
10334
  function semverNewer(a, b) {
10151
- if (!a || !b) return false;
10152
- const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
10153
- return pb[0] > pa[0]
10154
- || (pb[0] === pa[0] && pb[1] > pa[1])
10155
- || (pb[0] === pa[0] && pb[1] === pa[1] && pb[2] > pa[2]);
10335
+ // True iff b is strictly newer than a. Falls back to false if either side
10336
+ // can't be parsed — same defensive contract as the prior implementation.
10337
+ if (a == null || b == null) return false;
10338
+ const pa = parseSemver(a), pb = parseSemver(b);
10339
+ if (!pa || !pb) return false;
10340
+ return compareSemver(pa, pb) < 0;
10156
10341
  }
10157
10342
 
10158
10343
  function resolveBinPath(cmd, env) {
@@ -22378,8 +22563,8 @@ const httpServer = app.listen(PORT, async () => {
22378
22563
  }
22379
22564
  } catch { /* ok */ }
22380
22565
 
22381
- // Check for npm updates
22382
- try { await checkNpmUpdate(); } catch {}
22566
+ // Check for SynaBun updates (npm + github releases)
22567
+ try { await checkSynabunUpdate(); } catch {}
22383
22568
 
22384
22569
  // Aggressively prune any stale bundled CLI tools left in our node_modules
22385
22570
  // before the version check runs — defends against `npm install` not auto-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.25-beta.3",
3
+ "version": "2026.4.26-1",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",