synabun 2026.4.25-beta.1 → 2026.4.25-beta.3
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 +35 -0
- package/mcp-server/package.json +8 -2
- package/neural-interface/package.json +5 -1
- package/neural-interface/public/shared/cdx/cdx-panel.js +81 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +64 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +13 -2
- package/neural-interface/public/shared/cli-status.js +157 -0
- package/neural-interface/public/shared/ocp/ocp-panel.js +93 -0
- package/neural-interface/public/shared/ocp/ocp-render.js +2 -1
- package/neural-interface/public/shared/ocp/ocp-styles.js +63 -0
- package/neural-interface/public/shared/ui-claude-panel.js +164 -11
- package/neural-interface/server.js +11 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -43,6 +43,41 @@ Raw internal changelogs are archived in [changelog/](changelog/).
|
|
|
43
43
|
### Changed — Claude Restored Tab Selection
|
|
44
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
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
|
+
|
|
46
81
|
## 2026-04-24
|
|
47
82
|
|
|
48
83
|
### Added — "Continue planning" in OpenCode, Codex, and Claude plan UIs
|
package/mcp-server/package.json
CHANGED
|
@@ -24,15 +24,21 @@
|
|
|
24
24
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
25
25
|
"chalk": "^5.6.2",
|
|
26
26
|
"express": "^4.21.0",
|
|
27
|
-
"uuid": "^
|
|
27
|
+
"uuid": "^14.0.0",
|
|
28
28
|
"zod": "^3.24.2"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/express": "^5.0.0",
|
|
32
32
|
"@types/node": "^22.12.0",
|
|
33
|
-
"@types/uuid": "^10.0.0",
|
|
34
33
|
"tsx": "^4.19.0",
|
|
35
34
|
"typescript": "^5.9.3",
|
|
36
35
|
"vitest": "^4.1.0"
|
|
36
|
+
},
|
|
37
|
+
"overrides": {
|
|
38
|
+
"protobufjs": "^7.5.5",
|
|
39
|
+
"hono": "^4.12.14",
|
|
40
|
+
"@hono/node-server": "^1.19.13",
|
|
41
|
+
"postcss": "^8.5.10",
|
|
42
|
+
"vite": "^8.0.10"
|
|
37
43
|
}
|
|
38
44
|
}
|
|
@@ -21,8 +21,12 @@
|
|
|
21
21
|
"express": "^4.21.2",
|
|
22
22
|
"node-html-markdown": "^2.0.0",
|
|
23
23
|
"node-pty": "^1.1.0",
|
|
24
|
-
"playwright": "1.
|
|
24
|
+
"playwright": "^1.59.1",
|
|
25
25
|
"undici": "^8.1.0",
|
|
26
26
|
"ws": "^8.19.0"
|
|
27
|
+
},
|
|
28
|
+
"overrides": {
|
|
29
|
+
"lodash": "^4.18.1",
|
|
30
|
+
"protobufjs": "^7.5.5"
|
|
27
31
|
}
|
|
28
32
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { storage } from '../storage.js';
|
|
5
5
|
import { state, emit, on } from '../state.js';
|
|
6
6
|
import { reserveRightPanelLayout, clearRightPanelLayout } from '../ui-sidepanel-layout.js';
|
|
7
|
+
import { subscribeCliStatus, recheckCliStatus, getCliDocUrl, getCliInstallCommand, getCliLabel } from '../cli-status.js';
|
|
7
8
|
import { isClaudePanelOpen, toggleClaudePanel } from '../ui-claude-panel.js';
|
|
8
9
|
import { injectStyles } from './cdx-styles.js';
|
|
9
10
|
import { appendAssistantMarkdownMessage, flushCardBody, renderPostPlanActions } from './cdx-render.js';
|
|
@@ -46,6 +47,8 @@ document.addEventListener('visibilitychange', () => {
|
|
|
46
47
|
let _panel = null;
|
|
47
48
|
let _visible = false;
|
|
48
49
|
let _resizeBound = false;
|
|
50
|
+
let _cliInstalled = null;
|
|
51
|
+
let _cliUnsub = null;
|
|
49
52
|
|
|
50
53
|
// ═══════════════════════════════════════════
|
|
51
54
|
// Bound State (mirrors active tab — managed by cdx-tabs)
|
|
@@ -390,6 +393,82 @@ function startupEmptyText() {
|
|
|
390
393
|
return '';
|
|
391
394
|
}
|
|
392
395
|
|
|
396
|
+
// ── CLI installation status (banner + send-block) ──
|
|
397
|
+
let _cliInstallFailureForced = false;
|
|
398
|
+
|
|
399
|
+
function ensureCliBannerEl() {
|
|
400
|
+
if (!_panel) return null;
|
|
401
|
+
const container = panelEl('#cxp-messages-container');
|
|
402
|
+
if (!container) return null;
|
|
403
|
+
let banner = container.querySelector(':scope > .cxp-cli-banner');
|
|
404
|
+
if (banner) return banner;
|
|
405
|
+
const label = getCliLabel('codex');
|
|
406
|
+
const cmd = getCliInstallCommand('codex');
|
|
407
|
+
const url = getCliDocUrl('codex');
|
|
408
|
+
banner = document.createElement('div');
|
|
409
|
+
banner.className = 'cxp-cli-banner';
|
|
410
|
+
banner.innerHTML = `
|
|
411
|
+
<div class="cxp-cli-banner-icon">!</div>
|
|
412
|
+
<div class="cxp-cli-banner-text">
|
|
413
|
+
<div class="cxp-cli-banner-title">${label} CLI not installed</div>
|
|
414
|
+
<div class="cxp-cli-banner-body">Run <code>${cmd}</code> or follow the install guide.</div>
|
|
415
|
+
</div>
|
|
416
|
+
<div class="cxp-cli-banner-actions">
|
|
417
|
+
<a class="cxp-cli-banner-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
|
|
418
|
+
<button class="cxp-cli-banner-recheck" type="button">Re-check</button>
|
|
419
|
+
</div>
|
|
420
|
+
`;
|
|
421
|
+
container.insertBefore(banner, container.firstChild);
|
|
422
|
+
const btn = banner.querySelector('.cxp-cli-banner-recheck');
|
|
423
|
+
btn?.addEventListener('click', async () => {
|
|
424
|
+
btn.disabled = true;
|
|
425
|
+
btn.textContent = 'Checking…';
|
|
426
|
+
try { await recheckCliStatus('codex'); }
|
|
427
|
+
finally {
|
|
428
|
+
if (btn.isConnected) {
|
|
429
|
+
btn.disabled = false;
|
|
430
|
+
btn.textContent = 'Re-check';
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
return banner;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function removeCliBannerEl() {
|
|
438
|
+
if (!_panel) return;
|
|
439
|
+
const container = panelEl('#cxp-messages-container');
|
|
440
|
+
const banner = container?.querySelector(':scope > .cxp-cli-banner');
|
|
441
|
+
banner?.remove();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function refreshCliBanner() {
|
|
445
|
+
if (!_panel) return;
|
|
446
|
+
const missing = _cliInstalled === null || _cliInstallFailureForced;
|
|
447
|
+
_panel.classList.toggle('cxp-cli-blocked', missing);
|
|
448
|
+
if (missing) ensureCliBannerEl();
|
|
449
|
+
else removeCliBannerEl();
|
|
450
|
+
try { syncInputEnabled(); } catch {}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function isCdxCliInstalled() {
|
|
454
|
+
return _cliInstalled !== null && !_cliInstallFailureForced;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function flagCdxCliInstallFailure() {
|
|
458
|
+
_cliInstallFailureForced = true;
|
|
459
|
+
refreshCliBanner();
|
|
460
|
+
recheckCliStatus('codex').catch(() => {});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function ensureCliSubscription() {
|
|
464
|
+
if (_cliUnsub) return;
|
|
465
|
+
_cliUnsub = subscribeCliStatus('codex', (info) => {
|
|
466
|
+
_cliInstalled = info?.installed || null;
|
|
467
|
+
if (_cliInstalled) _cliInstallFailureForced = false;
|
|
468
|
+
refreshCliBanner();
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
393
472
|
// ═══════════════════════════════════════════
|
|
394
473
|
// setVisible — Show / Hide Panel
|
|
395
474
|
// ═══════════════════════════════════════════
|
|
@@ -426,6 +505,7 @@ function setVisible(nextVisible) {
|
|
|
426
505
|
loadCurrentProfile();
|
|
427
506
|
loadRecallProfile();
|
|
428
507
|
panelEl('#cxp-input')?.focus();
|
|
508
|
+
refreshCliBanner();
|
|
429
509
|
} else {
|
|
430
510
|
document.removeEventListener('keydown', _cdxDocEscHandler, { capture: true });
|
|
431
511
|
}
|
|
@@ -971,6 +1051,7 @@ function ensurePanel() {
|
|
|
971
1051
|
autosizeInput();
|
|
972
1052
|
restoreTabs();
|
|
973
1053
|
syncInputEnabled();
|
|
1054
|
+
ensureCliSubscription();
|
|
974
1055
|
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'codex' } }));
|
|
975
1056
|
// Close Codex when a Claude/OpenCode pill is clicked (mutual exclusion)
|
|
976
1057
|
const tray = document.getElementById('term-minimized-tray');
|
|
@@ -984,6 +984,70 @@ export function injectStyles() {
|
|
|
984
984
|
.cxp-empty-logo svg { width: 32px; height: 32px; opacity: 0.25; }
|
|
985
985
|
.cxp-empty-name { font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.35); letter-spacing: 0.02em; }
|
|
986
986
|
.cxp-empty-status { font-size: 11px; color: rgba(255,255,255,0.28); line-height: 1.5; min-height: 1em; }
|
|
987
|
+
.cxp-cli-banner {
|
|
988
|
+
flex-shrink: 0;
|
|
989
|
+
display: grid;
|
|
990
|
+
grid-template-columns: auto 1fr auto;
|
|
991
|
+
gap: 10px;
|
|
992
|
+
align-items: center;
|
|
993
|
+
margin: 8px 12px 0;
|
|
994
|
+
padding: 10px 12px;
|
|
995
|
+
border: 1px solid rgba(255,180,80,0.45);
|
|
996
|
+
border-radius: 8px;
|
|
997
|
+
background: rgba(255,180,80,0.08);
|
|
998
|
+
color: rgba(255,235,200,0.92);
|
|
999
|
+
font-size: 11px;
|
|
1000
|
+
line-height: 1.4;
|
|
1001
|
+
position: relative;
|
|
1002
|
+
z-index: 5;
|
|
1003
|
+
}
|
|
1004
|
+
.cxp-cli-banner-icon {
|
|
1005
|
+
width: 22px; height: 22px;
|
|
1006
|
+
display: flex; align-items: center; justify-content: center;
|
|
1007
|
+
border-radius: 50%;
|
|
1008
|
+
background: rgba(255,180,80,0.25);
|
|
1009
|
+
color: rgba(255,210,140,1);
|
|
1010
|
+
font-weight: 700;
|
|
1011
|
+
}
|
|
1012
|
+
.cxp-cli-banner-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
1013
|
+
.cxp-cli-banner-title {
|
|
1014
|
+
font-size: 12px; font-weight: 600;
|
|
1015
|
+
color: rgba(255,210,140,0.95);
|
|
1016
|
+
}
|
|
1017
|
+
.cxp-cli-banner-body {
|
|
1018
|
+
font-size: 11px;
|
|
1019
|
+
color: rgba(255,255,255,0.7);
|
|
1020
|
+
word-break: break-word;
|
|
1021
|
+
}
|
|
1022
|
+
.cxp-cli-banner-body code {
|
|
1023
|
+
padding: 1px 5px;
|
|
1024
|
+
background: rgba(0,0,0,0.35);
|
|
1025
|
+
border-radius: 3px;
|
|
1026
|
+
font-size: 10.5px;
|
|
1027
|
+
color: rgba(255,255,255,0.85);
|
|
1028
|
+
}
|
|
1029
|
+
.cxp-cli-banner-actions { display: flex; gap: 6px; }
|
|
1030
|
+
.cxp-cli-banner-link, .cxp-cli-banner-recheck {
|
|
1031
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1032
|
+
padding: 4px 9px;
|
|
1033
|
+
font-size: 11px;
|
|
1034
|
+
font-weight: 500;
|
|
1035
|
+
border-radius: 4px;
|
|
1036
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
1037
|
+
background: rgba(255,255,255,0.05);
|
|
1038
|
+
color: rgba(255,255,255,0.9);
|
|
1039
|
+
text-decoration: none;
|
|
1040
|
+
cursor: pointer;
|
|
1041
|
+
user-select: none;
|
|
1042
|
+
font-family: inherit;
|
|
1043
|
+
}
|
|
1044
|
+
.cxp-cli-banner-link:hover, .cxp-cli-banner-recheck:hover {
|
|
1045
|
+
background: rgba(255,255,255,0.12);
|
|
1046
|
+
border-color: rgba(255,255,255,0.4);
|
|
1047
|
+
}
|
|
1048
|
+
.cxp-cli-banner-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
1049
|
+
.codex-panel.cxp-cli-blocked .cxp-send { opacity: 0.4 !important; cursor: not-allowed !important; }
|
|
1050
|
+
.codex-panel.cxp-cli-blocked .cxp-input { opacity: 0.7; }
|
|
987
1051
|
.cxp-system {
|
|
988
1052
|
align-self: center;
|
|
989
1053
|
max-width: 92%;
|
|
@@ -552,6 +552,9 @@ function applySocketError(msg) {
|
|
|
552
552
|
if (_compacting) setCompactingUI(false);
|
|
553
553
|
appendSystem(text, 'error');
|
|
554
554
|
setStatus(text, 'error');
|
|
555
|
+
if (/Codex CLI not found|ENOENT|app-server exited before responding|app-server spawn/i.test(text)) {
|
|
556
|
+
import('./cdx-panel.js').then((m) => m.flagCdxCliInstallFailure?.()).catch(() => {});
|
|
557
|
+
}
|
|
555
558
|
if (msg.requestId) {
|
|
556
559
|
if (_sessionListRequest?.requestId === msg.requestId) {
|
|
557
560
|
_sessionListRequest.reject?.(new Error(text));
|
|
@@ -2041,6 +2044,7 @@ function startupEmptyText() {
|
|
|
2041
2044
|
export function clearTranscript(emptyText = freshThreadText()) {
|
|
2042
2045
|
if (!_messagesEl) return;
|
|
2043
2046
|
_messagesEl.innerHTML = `<div class="cxp-empty"><div class="cxp-empty-logo">${OPENAI_ICON}</div><span class="cxp-empty-name">Codex</span><span class="cxp-empty-status"></span></div>`;
|
|
2047
|
+
document.dispatchEvent(new CustomEvent('cdx-empty-rebuilt'));
|
|
2044
2048
|
_items.clear();
|
|
2045
2049
|
_requestCards.clear();
|
|
2046
2050
|
_blockingServerRequests = new Map();
|
|
@@ -3271,11 +3275,12 @@ export function syncInputEnabled() {
|
|
|
3271
3275
|
const hasPaths = !!(tab?.pendingPaths?.length);
|
|
3272
3276
|
const blockedByRequest = hasBlockingServerRequests();
|
|
3273
3277
|
if (input) input.disabled = !_connected || !_bootstrapped || _startingThread || blockedByRequest;
|
|
3278
|
+
const cliBlocked = document.getElementById('codex-panel')?.classList.contains('cxp-cli-blocked');
|
|
3274
3279
|
if (send) {
|
|
3275
3280
|
const isStopMode = !!(tab?.running || _running);
|
|
3276
3281
|
send.classList.toggle('cxp-send-stopping', !!isStopMode);
|
|
3277
|
-
send.title = isStopMode ? 'Stop' : 'Send';
|
|
3278
|
-
send.disabled = isStopMode ? false : (!_connected || !_bootstrapped || _running || _startingThread || blockedByRequest || (!hasText && !hasImages && !hasPaths));
|
|
3282
|
+
send.title = cliBlocked ? 'Codex CLI not installed' : (isStopMode ? 'Stop' : 'Send');
|
|
3283
|
+
send.disabled = cliBlocked || (isStopMode ? false : (!_connected || !_bootstrapped || _running || _startingThread || blockedByRequest || (!hasText && !hasImages && !hasPaths)));
|
|
3279
3284
|
}
|
|
3280
3285
|
if (fresh) {
|
|
3281
3286
|
pruneDiscardableTabs({ keepTab: tab, persist: true });
|
|
@@ -4528,6 +4533,12 @@ export function sendPrompt() {
|
|
|
4528
4533
|
const { input, tab, prompt, images, paths } = payload;
|
|
4529
4534
|
if (!input || !_connected || !_bootstrapped) return;
|
|
4530
4535
|
if (!prompt && !images.length && !paths.length) return;
|
|
4536
|
+
// Block when CLI binary missing — banner already informs user.
|
|
4537
|
+
const panel = document.getElementById('codex-panel');
|
|
4538
|
+
if (panel?.classList.contains('cxp-cli-blocked')) {
|
|
4539
|
+
appendSystem('Codex CLI not installed. Install it first, then click Re-check.', 'error');
|
|
4540
|
+
return;
|
|
4541
|
+
}
|
|
4531
4542
|
handleLocalSlashCommand(payload).then((handled) => {
|
|
4532
4543
|
if (handled) return;
|
|
4533
4544
|
if (_running && tab) {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — CLI Status (shared)
|
|
3
|
+
// Detects whether Claude / Codex / OpenCode CLIs are installed by
|
|
4
|
+
// querying /api/system/tool-versions. Caches the response, fans out
|
|
5
|
+
// updates to subscribers, and auto-polls (every 30s) while at least
|
|
6
|
+
// one subscribed tool reports installed=null.
|
|
7
|
+
// ═══════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
const TOOL_KEYS = ['claude-code', 'codex', 'opencode', 'gemini'];
|
|
10
|
+
|
|
11
|
+
const CLI_DOC_URLS = {
|
|
12
|
+
'claude-code': 'https://docs.claude.com/en/docs/claude-code/setup',
|
|
13
|
+
'codex': 'https://developers.openai.com/codex/cli/',
|
|
14
|
+
'opencode': 'https://opencode.ai/docs/',
|
|
15
|
+
'gemini': 'https://github.com/google-gemini/gemini-cli',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const CLI_INSTALL_CMDS = {
|
|
19
|
+
'claude-code': 'npm install -g @anthropic-ai/claude-code',
|
|
20
|
+
'codex': 'npm install -g @openai/codex',
|
|
21
|
+
'opencode': 'npm install -g opencode-ai',
|
|
22
|
+
'gemini': 'npm install -g @google/gemini-cli',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const CLI_LABELS = {
|
|
26
|
+
'claude-code': 'Claude Code',
|
|
27
|
+
'codex': 'Codex',
|
|
28
|
+
'opencode': 'OpenCode',
|
|
29
|
+
'gemini': 'Gemini CLI',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const POLL_INTERVAL_MS = 30000;
|
|
33
|
+
const CACHE_TTL_MS = 10000;
|
|
34
|
+
|
|
35
|
+
let _cache = { fetchedAt: 0, tools: null };
|
|
36
|
+
let _inflight = null;
|
|
37
|
+
const _subscribers = new Map();
|
|
38
|
+
let _pollTimer = null;
|
|
39
|
+
|
|
40
|
+
export function getCliDocUrl(toolKey) {
|
|
41
|
+
return CLI_DOC_URLS[toolKey] || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getCliInstallCommand(toolKey) {
|
|
45
|
+
return CLI_INSTALL_CMDS[toolKey] || '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getCliLabel(toolKey) {
|
|
49
|
+
return CLI_LABELS[toolKey] || toolKey;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function _fetchToolVersions(force) {
|
|
53
|
+
if (!force && _cache.tools && (Date.now() - _cache.fetchedAt) < CACHE_TTL_MS) {
|
|
54
|
+
return _cache.tools;
|
|
55
|
+
}
|
|
56
|
+
if (_inflight) return _inflight;
|
|
57
|
+
const url = force ? '/api/system/tool-versions?force=1' : '/api/system/tool-versions';
|
|
58
|
+
_inflight = (async () => {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(url);
|
|
61
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
_cache = { fetchedAt: Date.now(), tools: data?.tools || {} };
|
|
64
|
+
return _cache.tools;
|
|
65
|
+
} finally {
|
|
66
|
+
_inflight = null;
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
return _inflight;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _emitChange(prevTools, nextTools) {
|
|
73
|
+
for (const key of TOOL_KEYS) {
|
|
74
|
+
const prev = prevTools?.[key]?.installed ?? null;
|
|
75
|
+
const next = nextTools?.[key]?.installed ?? null;
|
|
76
|
+
const subs = _subscribers.get(key);
|
|
77
|
+
if (!subs || !subs.size) continue;
|
|
78
|
+
if (prev !== next || prevTools == null) {
|
|
79
|
+
const info = nextTools?.[key] || { installed: null };
|
|
80
|
+
for (const cb of subs) {
|
|
81
|
+
try { cb(info); } catch (e) { console.error('[cli-status] subscriber error:', e); }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _hasMissingSubscribed() {
|
|
88
|
+
for (const key of _subscribers.keys()) {
|
|
89
|
+
const subs = _subscribers.get(key);
|
|
90
|
+
if (!subs?.size) continue;
|
|
91
|
+
const installed = _cache.tools?.[key]?.installed ?? null;
|
|
92
|
+
if (!installed) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _maybeStartPoller() {
|
|
98
|
+
if (_pollTimer) return;
|
|
99
|
+
if (!_hasMissingSubscribed()) return;
|
|
100
|
+
_pollTimer = setInterval(async () => {
|
|
101
|
+
if (!_hasMissingSubscribed()) {
|
|
102
|
+
_stopPoller();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const prev = _cache.tools;
|
|
106
|
+
try {
|
|
107
|
+
const next = await _fetchToolVersions(true);
|
|
108
|
+
_emitChange(prev, next);
|
|
109
|
+
if (!_hasMissingSubscribed()) _stopPoller();
|
|
110
|
+
} catch { /* ignore — retry next tick */ }
|
|
111
|
+
}, POLL_INTERVAL_MS);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _stopPoller() {
|
|
115
|
+
if (_pollTimer) {
|
|
116
|
+
clearInterval(_pollTimer);
|
|
117
|
+
_pollTimer = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function getCliStatus(toolKey, { force = false } = {}) {
|
|
122
|
+
const tools = await _fetchToolVersions(force);
|
|
123
|
+
return tools?.[toolKey] || { installed: null, latest: null };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function subscribeCliStatus(toolKey, callback) {
|
|
127
|
+
if (!TOOL_KEYS.includes(toolKey)) return () => {};
|
|
128
|
+
let set = _subscribers.get(toolKey);
|
|
129
|
+
if (!set) { set = new Set(); _subscribers.set(toolKey, set); }
|
|
130
|
+
set.add(callback);
|
|
131
|
+
|
|
132
|
+
const cached = _cache.tools?.[toolKey];
|
|
133
|
+
if (cached) {
|
|
134
|
+
try { callback(cached); } catch (e) { console.error('[cli-status] subscriber error:', e); }
|
|
135
|
+
_maybeStartPoller();
|
|
136
|
+
} else {
|
|
137
|
+
_fetchToolVersions(false).then((tools) => {
|
|
138
|
+
try { callback(tools?.[toolKey] || { installed: null }); } catch {}
|
|
139
|
+
_maybeStartPoller();
|
|
140
|
+
}).catch(() => {});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return () => {
|
|
144
|
+
const s = _subscribers.get(toolKey);
|
|
145
|
+
if (!s) return;
|
|
146
|
+
s.delete(callback);
|
|
147
|
+
if (!s.size) _subscribers.delete(toolKey);
|
|
148
|
+
if (!_hasMissingSubscribed()) _stopPoller();
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function recheckCliStatus(toolKey) {
|
|
153
|
+
const prev = _cache.tools;
|
|
154
|
+
const next = await _fetchToolVersions(true);
|
|
155
|
+
_emitChange(prev, next);
|
|
156
|
+
return next?.[toolKey] || { installed: null };
|
|
157
|
+
}
|
|
@@ -8,6 +8,7 @@ import { state, emit, on } from '../state.js';
|
|
|
8
8
|
import { storage } from '../storage.js';
|
|
9
9
|
import { fetchProjects } from '../api.js';
|
|
10
10
|
import { reserveRightPanelLayout, clearRightPanelLayout } from '../ui-sidepanel-layout.js';
|
|
11
|
+
import { subscribeCliStatus, recheckCliStatus, getCliDocUrl, getCliInstallCommand, getCliLabel } from '../cli-status.js';
|
|
11
12
|
|
|
12
13
|
import { injectStyles } from './ocp-styles.js';
|
|
13
14
|
import {
|
|
@@ -64,6 +65,8 @@ let _serverVersion = null;
|
|
|
64
65
|
let _serverManaged = false;
|
|
65
66
|
let _projects = [];
|
|
66
67
|
let _projectsLoaded = false;
|
|
68
|
+
let _cliInstalled = null;
|
|
69
|
+
let _cliUnsub = null;
|
|
67
70
|
|
|
68
71
|
function panelEl(sel) { return _panel?.querySelector(sel) || null; }
|
|
69
72
|
|
|
@@ -907,6 +910,10 @@ async function handleSend() {
|
|
|
907
910
|
if (!input) return;
|
|
908
911
|
const tab = activeTab();
|
|
909
912
|
if (!tab) return;
|
|
913
|
+
if (_cliInstalled === null || _cliInstallFailureForced) {
|
|
914
|
+
setStatus('offline', 'OpenCode CLI not installed. Install it first, then click Re-check.');
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
910
917
|
let text = input.value.trim();
|
|
911
918
|
const images = Array.isArray(tab.attachedImages) ? [...tab.attachedImages] : [];
|
|
912
919
|
if (!text && !images.length) return;
|
|
@@ -1006,6 +1013,16 @@ function syncSendButton() {
|
|
|
1006
1013
|
const tab = activeTab();
|
|
1007
1014
|
if (!sendBtn) return;
|
|
1008
1015
|
|
|
1016
|
+
const cliBlocked = _cliInstalled === null || _cliInstallFailureForced;
|
|
1017
|
+
|
|
1018
|
+
if (cliBlocked) {
|
|
1019
|
+
sendBtn.classList.remove('running');
|
|
1020
|
+
sendBtn.disabled = true;
|
|
1021
|
+
sendBtn.setAttribute('data-tooltip', 'OpenCode CLI not installed');
|
|
1022
|
+
sendBtn.setAttribute('aria-label', 'OpenCode CLI not installed');
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1009
1026
|
if (tab?.running) {
|
|
1010
1027
|
sendBtn.disabled = false;
|
|
1011
1028
|
sendBtn.classList.add('running');
|
|
@@ -1503,6 +1520,77 @@ function _ocpDocEscHandler(event) {
|
|
|
1503
1520
|
abortMessage();
|
|
1504
1521
|
}
|
|
1505
1522
|
|
|
1523
|
+
// ── CLI installation status (banner + send-block) ──
|
|
1524
|
+
let _cliInstallFailureForced = false;
|
|
1525
|
+
|
|
1526
|
+
function ensureCliBannerEl() {
|
|
1527
|
+
if (!_panel) return null;
|
|
1528
|
+
const container = _panel.querySelector('.ocp-messages-container');
|
|
1529
|
+
if (!container) return null;
|
|
1530
|
+
let banner = container.querySelector(':scope > .ocp-cli-banner');
|
|
1531
|
+
if (banner) return banner;
|
|
1532
|
+
const label = getCliLabel('opencode');
|
|
1533
|
+
const cmd = getCliInstallCommand('opencode');
|
|
1534
|
+
const url = getCliDocUrl('opencode');
|
|
1535
|
+
banner = document.createElement('div');
|
|
1536
|
+
banner.className = 'ocp-cli-banner';
|
|
1537
|
+
banner.innerHTML = `
|
|
1538
|
+
<div class="ocp-cli-banner-icon">!</div>
|
|
1539
|
+
<div class="ocp-cli-banner-text">
|
|
1540
|
+
<div class="ocp-cli-banner-title">${label} CLI not installed</div>
|
|
1541
|
+
<div class="ocp-cli-banner-body">Run <code>${cmd}</code> or follow the install guide.</div>
|
|
1542
|
+
</div>
|
|
1543
|
+
<div class="ocp-cli-banner-actions">
|
|
1544
|
+
<a class="ocp-cli-banner-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
|
|
1545
|
+
<button class="ocp-cli-banner-recheck" type="button">Re-check</button>
|
|
1546
|
+
</div>
|
|
1547
|
+
`;
|
|
1548
|
+
container.insertBefore(banner, container.firstChild);
|
|
1549
|
+
const btn = banner.querySelector('.ocp-cli-banner-recheck');
|
|
1550
|
+
btn?.addEventListener('click', async () => {
|
|
1551
|
+
btn.disabled = true;
|
|
1552
|
+
btn.textContent = 'Checking…';
|
|
1553
|
+
try { await recheckCliStatus('opencode'); }
|
|
1554
|
+
finally {
|
|
1555
|
+
if (btn.isConnected) {
|
|
1556
|
+
btn.disabled = false;
|
|
1557
|
+
btn.textContent = 'Re-check';
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
return banner;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function removeCliBannerEl() {
|
|
1565
|
+
if (!_panel) return;
|
|
1566
|
+
const banner = _panel.querySelector('.ocp-messages-container > .ocp-cli-banner');
|
|
1567
|
+
banner?.remove();
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function refreshCliBanner() {
|
|
1571
|
+
if (!_panel) return;
|
|
1572
|
+
const missing = _cliInstalled === null || _cliInstallFailureForced;
|
|
1573
|
+
_panel.classList.toggle('ocp-cli-blocked', missing);
|
|
1574
|
+
if (missing) ensureCliBannerEl();
|
|
1575
|
+
else removeCliBannerEl();
|
|
1576
|
+
syncSendButton();
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
export function flagOcpCliInstallFailure() {
|
|
1580
|
+
_cliInstallFailureForced = true;
|
|
1581
|
+
refreshCliBanner();
|
|
1582
|
+
recheckCliStatus('opencode').catch(() => {});
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function ensureCliSubscription() {
|
|
1586
|
+
if (_cliUnsub) return;
|
|
1587
|
+
_cliUnsub = subscribeCliStatus('opencode', (info) => {
|
|
1588
|
+
_cliInstalled = info?.installed || null;
|
|
1589
|
+
if (_cliInstalled) _cliInstallFailureForced = false;
|
|
1590
|
+
refreshCliBanner();
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1506
1594
|
function setVisible(nextVisible) {
|
|
1507
1595
|
if (!_panel) return;
|
|
1508
1596
|
if (!nextVisible) {
|
|
@@ -1528,6 +1616,7 @@ function setVisible(nextVisible) {
|
|
|
1528
1616
|
if (_curProject) loadBranches(_curProject);
|
|
1529
1617
|
loadCurrentProfile();
|
|
1530
1618
|
loadRecallProfile();
|
|
1619
|
+
refreshCliBanner();
|
|
1531
1620
|
} else {
|
|
1532
1621
|
document.removeEventListener('keydown', _ocpDocEscHandler, { capture: true });
|
|
1533
1622
|
}
|
|
@@ -1583,6 +1672,9 @@ function wsCallbacks() {
|
|
|
1583
1672
|
},
|
|
1584
1673
|
onError(msg) {
|
|
1585
1674
|
console.error('[ocp] Error:', msg.message);
|
|
1675
|
+
if (typeof msg.message === 'string' && /opencode.*(not.*found|ENOENT|spawn)|opencode CLI/i.test(msg.message)) {
|
|
1676
|
+
flagOcpCliInstallFailure();
|
|
1677
|
+
}
|
|
1586
1678
|
},
|
|
1587
1679
|
};
|
|
1588
1680
|
}
|
|
@@ -1651,6 +1743,7 @@ function ensurePanel() {
|
|
|
1651
1743
|
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'opencode' } }));
|
|
1652
1744
|
|
|
1653
1745
|
syncSendButton();
|
|
1746
|
+
ensureCliSubscription();
|
|
1654
1747
|
|
|
1655
1748
|
// Connect WebSocket
|
|
1656
1749
|
connectWs(wsCallbacks());
|
|
@@ -985,9 +985,10 @@ export function renderEmptyState(container) {
|
|
|
985
985
|
<div class="ocp-empty">
|
|
986
986
|
<svg viewBox="0 0 240 300"><path fill-rule="evenodd" d="M0 0h240v300H0V0zm30 30v240h180V30H30z" fill="currentColor"/><rect x="30" y="150" width="180" height="120" opacity=".45" fill="currentColor"/></svg>
|
|
987
987
|
<span>OpenCode</span>
|
|
988
|
-
<span style="font-size:10px;opacity:0.5">Send a message to start</span>
|
|
988
|
+
<span class="ocp-empty-hint" style="font-size:10px;opacity:0.5">Send a message to start</span>
|
|
989
989
|
</div>
|
|
990
990
|
`;
|
|
991
|
+
document.dispatchEvent(new CustomEvent('ocp-empty-rebuilt'));
|
|
991
992
|
}
|
|
992
993
|
|
|
993
994
|
// ── Render full history from messages:list response ──
|
|
@@ -1521,6 +1521,69 @@ export function injectStyles() {
|
|
|
1521
1521
|
stroke: currentColor; fill: none;
|
|
1522
1522
|
stroke-width: 1; opacity: 0.3;
|
|
1523
1523
|
}
|
|
1524
|
+
.ocp-cli-banner {
|
|
1525
|
+
flex-shrink: 0;
|
|
1526
|
+
display: grid;
|
|
1527
|
+
grid-template-columns: auto 1fr auto;
|
|
1528
|
+
gap: 10px;
|
|
1529
|
+
align-items: center;
|
|
1530
|
+
margin: 8px 12px 0;
|
|
1531
|
+
padding: 10px 12px;
|
|
1532
|
+
border: 1px solid rgba(255,180,80,0.45);
|
|
1533
|
+
border-radius: 8px;
|
|
1534
|
+
background: rgba(255,180,80,0.08);
|
|
1535
|
+
color: rgba(255,235,200,0.92);
|
|
1536
|
+
font-size: 11px;
|
|
1537
|
+
line-height: 1.4;
|
|
1538
|
+
position: relative;
|
|
1539
|
+
z-index: 5;
|
|
1540
|
+
}
|
|
1541
|
+
.ocp-cli-banner-icon {
|
|
1542
|
+
width: 22px; height: 22px;
|
|
1543
|
+
display: flex; align-items: center; justify-content: center;
|
|
1544
|
+
border-radius: 50%;
|
|
1545
|
+
background: rgba(255,180,80,0.25);
|
|
1546
|
+
color: rgba(255,210,140,1);
|
|
1547
|
+
font-weight: 700;
|
|
1548
|
+
}
|
|
1549
|
+
.ocp-cli-banner-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
1550
|
+
.ocp-cli-banner-title {
|
|
1551
|
+
font-size: 12px; font-weight: 600;
|
|
1552
|
+
color: rgba(255,210,140,0.95);
|
|
1553
|
+
}
|
|
1554
|
+
.ocp-cli-banner-body {
|
|
1555
|
+
font-size: 11px;
|
|
1556
|
+
color: rgba(255,255,255,0.7);
|
|
1557
|
+
word-break: break-word;
|
|
1558
|
+
}
|
|
1559
|
+
.ocp-cli-banner-body code {
|
|
1560
|
+
padding: 1px 5px;
|
|
1561
|
+
background: rgba(0,0,0,0.35);
|
|
1562
|
+
border-radius: 3px;
|
|
1563
|
+
font-size: 10.5px;
|
|
1564
|
+
color: rgba(255,255,255,0.85);
|
|
1565
|
+
}
|
|
1566
|
+
.ocp-cli-banner-actions { display: flex; gap: 6px; }
|
|
1567
|
+
.ocp-cli-banner-link, .ocp-cli-banner-recheck {
|
|
1568
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1569
|
+
padding: 4px 9px;
|
|
1570
|
+
font-size: 11px;
|
|
1571
|
+
font-weight: 500;
|
|
1572
|
+
border-radius: 4px;
|
|
1573
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
1574
|
+
background: rgba(255,255,255,0.05);
|
|
1575
|
+
color: rgba(255,255,255,0.9);
|
|
1576
|
+
text-decoration: none;
|
|
1577
|
+
cursor: pointer;
|
|
1578
|
+
user-select: none;
|
|
1579
|
+
font-family: inherit;
|
|
1580
|
+
}
|
|
1581
|
+
.ocp-cli-banner-link:hover, .ocp-cli-banner-recheck:hover {
|
|
1582
|
+
background: rgba(255,255,255,0.12);
|
|
1583
|
+
border-color: rgba(255,255,255,0.4);
|
|
1584
|
+
}
|
|
1585
|
+
.ocp-cli-banner-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
1586
|
+
.ocp-panel.ocp-cli-blocked .ocp-input { opacity: 0.7; }
|
|
1524
1587
|
|
|
1525
1588
|
/* ── Thinking indicator ── */
|
|
1526
1589
|
.ocp-thinking {
|
|
@@ -9,6 +9,7 @@ import { fetchClaudeSessions, fetchBrowserSessions, searchSessions } from './api
|
|
|
9
9
|
import { createFrameRenderer } from './utils.js';
|
|
10
10
|
import { notify, NOTIF_TYPE } from './ui-notifications.js';
|
|
11
11
|
import { reserveRightPanelLayout, clearRightPanelLayout } from './ui-sidepanel-layout.js';
|
|
12
|
+
import { subscribeCliStatus, recheckCliStatus, getCliDocUrl, getCliInstallCommand, getCliLabel } from './cli-status.js';
|
|
12
13
|
|
|
13
14
|
const CLAUDE_ICON = '<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"/></svg>';
|
|
14
15
|
|
|
@@ -120,6 +121,10 @@ function activeTab() { return _tabs[_activeTabIdx] || null; }
|
|
|
120
121
|
let _browserEmbed = null; // { sessionId, ws, canvas, ctx, urlBar, container }
|
|
121
122
|
let _browserEmbedVisible = false;
|
|
122
123
|
|
|
124
|
+
// ── CLI-installed status (claude binary detection) ──
|
|
125
|
+
let _cliInstalled = null; // null = unknown / missing, version string = installed
|
|
126
|
+
let _cliUnsub = null;
|
|
127
|
+
|
|
123
128
|
// Marked.js (lazy loaded)
|
|
124
129
|
let _marked = null;
|
|
125
130
|
(async () => {
|
|
@@ -427,6 +432,73 @@ function injectStyles() {
|
|
|
427
432
|
.cp-messages:has(.msg) .cp-empty,
|
|
428
433
|
.cp-messages:has(.msg-status) .cp-empty { display: none; }
|
|
429
434
|
|
|
435
|
+
/* ── CLI-not-installed banner (sticky top of messages container) ── */
|
|
436
|
+
.cp-cli-banner {
|
|
437
|
+
flex-shrink: 0;
|
|
438
|
+
display: grid;
|
|
439
|
+
grid-template-columns: auto 1fr auto;
|
|
440
|
+
gap: 10px;
|
|
441
|
+
align-items: center;
|
|
442
|
+
margin: 8px 12px 0;
|
|
443
|
+
padding: 10px 12px;
|
|
444
|
+
border: 1px solid rgba(255,180,80,0.45);
|
|
445
|
+
border-radius: 8px;
|
|
446
|
+
background: rgba(255,180,80,0.08);
|
|
447
|
+
color: rgba(255,235,200,0.92);
|
|
448
|
+
font-size: 11px;
|
|
449
|
+
line-height: 1.4;
|
|
450
|
+
pointer-events: auto;
|
|
451
|
+
position: relative;
|
|
452
|
+
z-index: 5;
|
|
453
|
+
}
|
|
454
|
+
.cp-cli-banner-icon {
|
|
455
|
+
width: 22px; height: 22px;
|
|
456
|
+
display: flex; align-items: center; justify-content: center;
|
|
457
|
+
border-radius: 50%;
|
|
458
|
+
background: rgba(255,180,80,0.25);
|
|
459
|
+
color: rgba(255,210,140,1);
|
|
460
|
+
font-weight: 700;
|
|
461
|
+
}
|
|
462
|
+
.cp-cli-banner-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
463
|
+
.cp-cli-banner-title {
|
|
464
|
+
font-size: 12px; font-weight: 600;
|
|
465
|
+
color: rgba(255,210,140,0.95);
|
|
466
|
+
}
|
|
467
|
+
.cp-cli-banner-body {
|
|
468
|
+
font-size: 11px;
|
|
469
|
+
color: rgba(255,255,255,0.7);
|
|
470
|
+
word-break: break-word;
|
|
471
|
+
}
|
|
472
|
+
.cp-cli-banner-body code {
|
|
473
|
+
padding: 1px 5px;
|
|
474
|
+
background: rgba(0,0,0,0.35);
|
|
475
|
+
border-radius: 3px;
|
|
476
|
+
font-size: 10.5px;
|
|
477
|
+
color: rgba(255,255,255,0.85);
|
|
478
|
+
}
|
|
479
|
+
.cp-cli-banner-actions { display: flex; gap: 6px; }
|
|
480
|
+
.cp-cli-banner-link, .cp-cli-banner-recheck {
|
|
481
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
482
|
+
padding: 4px 9px;
|
|
483
|
+
font-size: 11px;
|
|
484
|
+
font-weight: 500;
|
|
485
|
+
border-radius: 4px;
|
|
486
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
487
|
+
background: rgba(255,255,255,0.05);
|
|
488
|
+
color: rgba(255,255,255,0.9);
|
|
489
|
+
text-decoration: none;
|
|
490
|
+
cursor: pointer;
|
|
491
|
+
user-select: none;
|
|
492
|
+
font-family: inherit;
|
|
493
|
+
}
|
|
494
|
+
.cp-cli-banner-link:hover, .cp-cli-banner-recheck:hover {
|
|
495
|
+
background: rgba(255,255,255,0.12);
|
|
496
|
+
border-color: rgba(255,255,255,0.4);
|
|
497
|
+
}
|
|
498
|
+
.cp-cli-banner-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
499
|
+
.claude-panel.cp-cli-blocked .cp-send { opacity: 0.4 !important; cursor: not-allowed !important; }
|
|
500
|
+
.claude-panel.cp-cli-blocked .cp-input { opacity: 0.7; }
|
|
501
|
+
|
|
430
502
|
/* ── Browser embed (live screencast inside panel) ── */
|
|
431
503
|
.cp-browser-embed {
|
|
432
504
|
display: none;
|
|
@@ -2649,14 +2721,14 @@ document.addEventListener('click', (e) => {
|
|
|
2649
2721
|
});
|
|
2650
2722
|
|
|
2651
2723
|
// ── Tab lifecycle ──
|
|
2652
|
-
function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
|
|
2724
|
+
function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
|
|
2653
2725
|
if (_tabs.length >= MAX_TABS) return null;
|
|
2654
2726
|
// Capture current dropdown state for the new tab's project/model/effort
|
|
2655
2727
|
const _$proj = _panel?.querySelector('#cp-project');
|
|
2656
2728
|
const _$mod = _panel?.querySelector('#cp-model');
|
|
2657
2729
|
const _$think = _panel?.querySelector('#cp-think-toggle');
|
|
2658
2730
|
const tab = {
|
|
2659
|
-
id: id || crypto.randomUUID(),
|
|
2731
|
+
id: id || crypto.randomUUID(),
|
|
2660
2732
|
sessionId,
|
|
2661
2733
|
label,
|
|
2662
2734
|
project: ddGetValue(_$proj) || '', // per-tab project isolation
|
|
@@ -2975,7 +3047,7 @@ function restoreTabs() {
|
|
|
2975
3047
|
const data = JSON.parse(raw);
|
|
2976
3048
|
if (data.tabs?.length) {
|
|
2977
3049
|
for (const saved of data.tabs) {
|
|
2978
|
-
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
3050
|
+
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
2979
3051
|
if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
|
|
2980
3052
|
if (t && saved.running) t._wasRunning = true; // trigger reattach on WS open
|
|
2981
3053
|
if (t && saved.project) t.project = saved.project;
|
|
@@ -2998,7 +3070,7 @@ function restoreTabs() {
|
|
|
2998
3070
|
const data = JSON.parse(legacyRaw);
|
|
2999
3071
|
if (data.tabs?.length) {
|
|
3000
3072
|
for (const saved of data.tabs) {
|
|
3001
|
-
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
3073
|
+
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
3002
3074
|
if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
|
|
3003
3075
|
if (t && saved.running) t._wasRunning = true;
|
|
3004
3076
|
if (t && saved.project) t.project = saved.project;
|
|
@@ -4329,6 +4401,9 @@ function _processTabMsg(tab, msg) {
|
|
|
4329
4401
|
if (tab.compacting) { tab.compacting = false; if (tab === activeTab()) _setCompactingUI(false); }
|
|
4330
4402
|
appendError(tab, msg.message);
|
|
4331
4403
|
notify('panel', NOTIF_TYPE.ERROR, tab.label || 'Claude Code', { tabId: tab.id });
|
|
4404
|
+
if (typeof msg.message === 'string' && /Claude CLI not found|ENOENT/i.test(msg.message)) {
|
|
4405
|
+
flagClaudeCliInstallFailure();
|
|
4406
|
+
}
|
|
4332
4407
|
break;
|
|
4333
4408
|
}
|
|
4334
4409
|
}
|
|
@@ -6544,6 +6619,10 @@ function runSlashCommand(tab, raw) {
|
|
|
6544
6619
|
}
|
|
6545
6620
|
|
|
6546
6621
|
function send({ shift = false } = {}) {
|
|
6622
|
+
if (_cliInstalled === null || _cliInstallFailureForced) {
|
|
6623
|
+
notify({ type: NOTIF_TYPE.WARN, title: 'Claude CLI not installed', body: 'Install the Claude Code CLI before sending a message.' });
|
|
6624
|
+
return;
|
|
6625
|
+
}
|
|
6547
6626
|
const tab = activeTab();
|
|
6548
6627
|
if (!tab) return;
|
|
6549
6628
|
const $input = _panel?.querySelector('#cp-input');
|
|
@@ -7246,6 +7325,78 @@ function syncReservedWidth() {
|
|
|
7246
7325
|
}
|
|
7247
7326
|
}
|
|
7248
7327
|
|
|
7328
|
+
// ── CLI installation status (banner + send-block) ──
|
|
7329
|
+
let _cliInstallFailureForced = false;
|
|
7330
|
+
|
|
7331
|
+
function _ensureCliBannerEl() {
|
|
7332
|
+
if (!_panel) return null;
|
|
7333
|
+
const container = _panel.querySelector('#cp-messages-container');
|
|
7334
|
+
if (!container) return null;
|
|
7335
|
+
let banner = container.querySelector(':scope > .cp-cli-banner');
|
|
7336
|
+
if (banner) return banner;
|
|
7337
|
+
const label = getCliLabel('claude-code');
|
|
7338
|
+
const cmd = getCliInstallCommand('claude-code');
|
|
7339
|
+
const url = getCliDocUrl('claude-code');
|
|
7340
|
+
banner = document.createElement('div');
|
|
7341
|
+
banner.className = 'cp-cli-banner';
|
|
7342
|
+
banner.innerHTML = `
|
|
7343
|
+
<div class="cp-cli-banner-icon">!</div>
|
|
7344
|
+
<div class="cp-cli-banner-text">
|
|
7345
|
+
<div class="cp-cli-banner-title">${label} CLI not installed</div>
|
|
7346
|
+
<div class="cp-cli-banner-body">Run <code>${cmd}</code> or follow the install guide.</div>
|
|
7347
|
+
</div>
|
|
7348
|
+
<div class="cp-cli-banner-actions">
|
|
7349
|
+
<a class="cp-cli-banner-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
|
|
7350
|
+
<button class="cp-cli-banner-recheck" type="button">Re-check</button>
|
|
7351
|
+
</div>
|
|
7352
|
+
`;
|
|
7353
|
+
container.insertBefore(banner, container.firstChild);
|
|
7354
|
+
const btn = banner.querySelector('.cp-cli-banner-recheck');
|
|
7355
|
+
btn?.addEventListener('click', async () => {
|
|
7356
|
+
btn.disabled = true;
|
|
7357
|
+
btn.textContent = 'Checking…';
|
|
7358
|
+
try { await recheckCliStatus('claude-code'); }
|
|
7359
|
+
finally {
|
|
7360
|
+
if (btn.isConnected) {
|
|
7361
|
+
btn.disabled = false;
|
|
7362
|
+
btn.textContent = 'Re-check';
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
});
|
|
7366
|
+
return banner;
|
|
7367
|
+
}
|
|
7368
|
+
|
|
7369
|
+
function _removeCliBannerEl() {
|
|
7370
|
+
if (!_panel) return;
|
|
7371
|
+
const banner = _panel.querySelector('#cp-messages-container > .cp-cli-banner');
|
|
7372
|
+
banner?.remove();
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
function _refreshCliBanners() {
|
|
7376
|
+
if (!_panel) return;
|
|
7377
|
+
const missing = _cliInstalled === null || _cliInstallFailureForced;
|
|
7378
|
+
_panel.classList.toggle('cp-cli-blocked', missing);
|
|
7379
|
+
if (missing) _ensureCliBannerEl();
|
|
7380
|
+
else _removeCliBannerEl();
|
|
7381
|
+
const $send = _panel.querySelector('#cp-send');
|
|
7382
|
+
if ($send && missing) $send.disabled = true;
|
|
7383
|
+
}
|
|
7384
|
+
|
|
7385
|
+
export function flagClaudeCliInstallFailure() {
|
|
7386
|
+
_cliInstallFailureForced = true;
|
|
7387
|
+
_refreshCliBanners();
|
|
7388
|
+
recheckCliStatus('claude-code').catch(() => {});
|
|
7389
|
+
}
|
|
7390
|
+
|
|
7391
|
+
function _ensureCliSubscription() {
|
|
7392
|
+
if (_cliUnsub) return;
|
|
7393
|
+
_cliUnsub = subscribeCliStatus('claude-code', (info) => {
|
|
7394
|
+
_cliInstalled = info?.installed || null;
|
|
7395
|
+
if (_cliInstalled) _cliInstallFailureForced = false;
|
|
7396
|
+
_refreshCliBanners();
|
|
7397
|
+
});
|
|
7398
|
+
}
|
|
7399
|
+
|
|
7249
7400
|
// ── Public API ──
|
|
7250
7401
|
export async function toggleClaudePanel() {
|
|
7251
7402
|
if (!_panel) {
|
|
@@ -7255,13 +7406,14 @@ export async function toggleClaudePanel() {
|
|
|
7255
7406
|
wireEvents();
|
|
7256
7407
|
await loadConfig(); // must complete before restoreTabs — checks server boot ID
|
|
7257
7408
|
loadSkills();
|
|
7258
|
-
restoreTabs();
|
|
7259
|
-
loadMonthlyCost();
|
|
7260
|
-
syncSessionCosts();
|
|
7261
|
-
_wireBrowserSync();
|
|
7262
|
-
_startHeartbeat();
|
|
7263
|
-
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
|
|
7264
|
-
|
|
7409
|
+
restoreTabs();
|
|
7410
|
+
loadMonthlyCost();
|
|
7411
|
+
syncSessionCosts();
|
|
7412
|
+
_wireBrowserSync();
|
|
7413
|
+
_startHeartbeat();
|
|
7414
|
+
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
|
|
7415
|
+
_ensureCliSubscription();
|
|
7416
|
+
}
|
|
7265
7417
|
_visible = !_visible;
|
|
7266
7418
|
if (_visible) {
|
|
7267
7419
|
if (_tabs.length === 0) createTab(null, 'New chat');
|
|
@@ -7269,6 +7421,7 @@ export async function toggleClaudePanel() {
|
|
|
7269
7421
|
_panel.classList.add('open');
|
|
7270
7422
|
syncReservedWidth();
|
|
7271
7423
|
_panel.querySelector('#cp-input')?.focus();
|
|
7424
|
+
_refreshCliBanners();
|
|
7272
7425
|
} else {
|
|
7273
7426
|
const tab = activeTab();
|
|
7274
7427
|
const $input = _panel?.querySelector('#cp-input');
|
|
@@ -10454,7 +10454,8 @@ app.get('/api/system/backup', async (req, res) => {
|
|
|
10454
10454
|
'loop-templates.json', 'loop-schedules.json', 'loop-folders.json', 'auto-backup-config.json',
|
|
10455
10455
|
'cost-tracking.json', 'skin-config.json', 'image-favorites.json',
|
|
10456
10456
|
'browser-config.json', 'browser-storage.json', 'last-session.json',
|
|
10457
|
-
'invite-sessions.json'
|
|
10457
|
+
'invite-sessions.json', 'stored-plans.json', 'synabun-plugins.json',
|
|
10458
|
+
'mcp-registry.json', 'active-profile.json']) {
|
|
10458
10459
|
addFile(`data/${f}`, resolve(dataDir, f));
|
|
10459
10460
|
}
|
|
10460
10461
|
|
|
@@ -10478,6 +10479,9 @@ app.get('/api/system/backup', async (req, res) => {
|
|
|
10478
10479
|
// Screenshots, whiteboard captures, staged images
|
|
10479
10480
|
addDirRecursive('data/images', resolve(dataDir, 'images'));
|
|
10480
10481
|
|
|
10482
|
+
// Plan markdown history (data/plans/YYYY-MM-DD/*.md)
|
|
10483
|
+
addDirRecursive('data/plans', resolve(dataDir, 'plans'));
|
|
10484
|
+
|
|
10481
10485
|
// 3. mcp-server/data/
|
|
10482
10486
|
if (existsSync(CATEGORIES_DATA_DIR)) {
|
|
10483
10487
|
for (const f of readdirSync(CATEGORIES_DATA_DIR)) {
|
|
@@ -10613,7 +10617,7 @@ app.post('/api/system/restore',
|
|
|
10613
10617
|
// data/ files
|
|
10614
10618
|
const dataDir = resolve(DATA_HOME, 'data');
|
|
10615
10619
|
mkdirSync(dataDir, { recursive: true });
|
|
10616
|
-
for (const subdir of ['pending-remember', 'pending-compact', 'loop']) {
|
|
10620
|
+
for (const subdir of ['pending-remember', 'pending-compact', 'loop', 'plans']) {
|
|
10617
10621
|
mkdirSync(resolve(dataDir, subdir), { recursive: true });
|
|
10618
10622
|
}
|
|
10619
10623
|
|
|
@@ -10837,7 +10841,8 @@ function buildBackupZipToFile(destPath) {
|
|
|
10837
10841
|
'loop-templates.json', 'loop-schedules.json', 'loop-folders.json', 'auto-backup-config.json',
|
|
10838
10842
|
'cost-tracking.json', 'skin-config.json', 'image-favorites.json',
|
|
10839
10843
|
'browser-config.json', 'browser-storage.json', 'last-session.json',
|
|
10840
|
-
'invite-sessions.json'
|
|
10844
|
+
'invite-sessions.json', 'stored-plans.json', 'synabun-plugins.json',
|
|
10845
|
+
'mcp-registry.json', 'active-profile.json']) {
|
|
10841
10846
|
addFile(`data/${f}`, resolve(dataDir, f));
|
|
10842
10847
|
}
|
|
10843
10848
|
|
|
@@ -10859,6 +10864,9 @@ function buildBackupZipToFile(destPath) {
|
|
|
10859
10864
|
// Screenshots, whiteboard captures, staged images
|
|
10860
10865
|
addDirRecursive('data/images', resolve(dataDir, 'images'));
|
|
10861
10866
|
|
|
10867
|
+
// Plan markdown history (data/plans/YYYY-MM-DD/*.md)
|
|
10868
|
+
addDirRecursive('data/plans', resolve(dataDir, 'plans'));
|
|
10869
|
+
|
|
10862
10870
|
if (existsSync(CATEGORIES_DATA_DIR)) {
|
|
10863
10871
|
for (const f of readdirSync(CATEGORIES_DATA_DIR)) {
|
|
10864
10872
|
if (f.endsWith('.json')) addFile(`mcp-data/${f}`, resolve(CATEGORIES_DATA_DIR, f));
|