synabun 2026.4.25-beta.1 → 2026.4.25-beta.2
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 +14 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +73 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +32 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +10 -2
- package/neural-interface/public/shared/cli-status.js +157 -0
- package/neural-interface/public/shared/ocp/ocp-panel.js +82 -0
- package/neural-interface/public/shared/ocp/ocp-render.js +2 -1
- package/neural-interface/public/shared/ocp/ocp-styles.js +33 -0
- package/neural-interface/public/shared/ui-claude-panel.js +123 -11
- package/neural-interface/server.js +11 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -43,6 +43,20 @@ 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
|
+
|
|
46
60
|
## 2026-04-24
|
|
47
61
|
|
|
48
62
|
### Added — "Continue planning" in OpenCode, Codex, and Claude plan UIs
|
|
@@ -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,74 @@ function startupEmptyText() {
|
|
|
390
393
|
return '';
|
|
391
394
|
}
|
|
392
395
|
|
|
396
|
+
// ── CLI installation status (banner + send-block) ──
|
|
397
|
+
function renderCliMissingBanner(emptyEl) {
|
|
398
|
+
if (!emptyEl) return;
|
|
399
|
+
const label = getCliLabel('codex');
|
|
400
|
+
const cmd = getCliInstallCommand('codex');
|
|
401
|
+
const url = getCliDocUrl('codex');
|
|
402
|
+
const statusEl = emptyEl.querySelector('.cxp-empty-status');
|
|
403
|
+
if (statusEl) statusEl.style.display = 'none';
|
|
404
|
+
let banner = emptyEl.querySelector('.cxp-cli-missing');
|
|
405
|
+
if (!banner) {
|
|
406
|
+
banner = document.createElement('div');
|
|
407
|
+
banner.className = 'cxp-cli-missing';
|
|
408
|
+
banner.innerHTML = `
|
|
409
|
+
<div class="cxp-cli-missing-title">${label} CLI not installed</div>
|
|
410
|
+
<div class="cxp-cli-missing-body">Run: <code>${cmd}</code></div>
|
|
411
|
+
<div class="cxp-cli-missing-actions">
|
|
412
|
+
<a class="cxp-cli-missing-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
|
|
413
|
+
<button class="cxp-cli-missing-recheck" type="button">Re-check</button>
|
|
414
|
+
</div>
|
|
415
|
+
`;
|
|
416
|
+
emptyEl.appendChild(banner);
|
|
417
|
+
const btn = banner.querySelector('.cxp-cli-missing-recheck');
|
|
418
|
+
btn?.addEventListener('click', async () => {
|
|
419
|
+
btn.disabled = true;
|
|
420
|
+
btn.textContent = 'Checking…';
|
|
421
|
+
try { await recheckCliStatus('codex'); }
|
|
422
|
+
finally {
|
|
423
|
+
if (btn.isConnected) {
|
|
424
|
+
btn.disabled = false;
|
|
425
|
+
btn.textContent = 'Re-check';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function refreshCliBanners() {
|
|
433
|
+
if (!_panel) return;
|
|
434
|
+
const empties = _panel.querySelectorAll('.cxp-empty');
|
|
435
|
+
if (_cliInstalled === null) {
|
|
436
|
+
_panel.classList.add('cxp-cli-blocked');
|
|
437
|
+
for (const el of empties) renderCliMissingBanner(el);
|
|
438
|
+
} else {
|
|
439
|
+
_panel.classList.remove('cxp-cli-blocked');
|
|
440
|
+
for (const el of empties) {
|
|
441
|
+
const banner = el.querySelector('.cxp-cli-missing');
|
|
442
|
+
if (banner) banner.remove();
|
|
443
|
+
const statusEl = el.querySelector('.cxp-empty-status');
|
|
444
|
+
if (statusEl) statusEl.style.display = '';
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Re-evaluate send button state with new CLI flag
|
|
448
|
+
try { syncInputEnabled(); } catch {}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function isCdxCliInstalled() {
|
|
452
|
+
return _cliInstalled !== null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function ensureCliSubscription() {
|
|
456
|
+
if (_cliUnsub) return;
|
|
457
|
+
_cliUnsub = subscribeCliStatus('codex', (info) => {
|
|
458
|
+
_cliInstalled = info?.installed || null;
|
|
459
|
+
refreshCliBanners();
|
|
460
|
+
});
|
|
461
|
+
document.addEventListener('cdx-empty-rebuilt', () => refreshCliBanners());
|
|
462
|
+
}
|
|
463
|
+
|
|
393
464
|
// ═══════════════════════════════════════════
|
|
394
465
|
// setVisible — Show / Hide Panel
|
|
395
466
|
// ═══════════════════════════════════════════
|
|
@@ -426,6 +497,7 @@ function setVisible(nextVisible) {
|
|
|
426
497
|
loadCurrentProfile();
|
|
427
498
|
loadRecallProfile();
|
|
428
499
|
panelEl('#cxp-input')?.focus();
|
|
500
|
+
refreshCliBanners();
|
|
429
501
|
} else {
|
|
430
502
|
document.removeEventListener('keydown', _cdxDocEscHandler, { capture: true });
|
|
431
503
|
}
|
|
@@ -971,6 +1043,7 @@ function ensurePanel() {
|
|
|
971
1043
|
autosizeInput();
|
|
972
1044
|
restoreTabs();
|
|
973
1045
|
syncInputEnabled();
|
|
1046
|
+
ensureCliSubscription();
|
|
974
1047
|
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'codex' } }));
|
|
975
1048
|
// Close Codex when a Claude/OpenCode pill is clicked (mutual exclusion)
|
|
976
1049
|
const tray = document.getElementById('term-minimized-tray');
|
|
@@ -984,6 +984,38 @@ 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-missing {
|
|
988
|
+
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
989
|
+
max-width: 320px; padding: 14px 16px; margin-top: 4px;
|
|
990
|
+
border: 1px solid rgba(255,180,80,0.35); border-radius: 8px;
|
|
991
|
+
background: rgba(255,180,80,0.06);
|
|
992
|
+
}
|
|
993
|
+
.cxp-cli-missing-title {
|
|
994
|
+
font-size: 12px; font-weight: 600; color: rgba(255,200,120,0.9);
|
|
995
|
+
letter-spacing: 0.02em;
|
|
996
|
+
}
|
|
997
|
+
.cxp-cli-missing-body {
|
|
998
|
+
font-size: 11px; color: rgba(255,255,255,0.55); text-align: center; line-height: 1.45;
|
|
999
|
+
}
|
|
1000
|
+
.cxp-cli-missing-body code {
|
|
1001
|
+
display: inline-block; padding: 1px 6px; margin-top: 4px;
|
|
1002
|
+
background: rgba(0,0,0,0.35); border-radius: 4px; font-size: 10.5px;
|
|
1003
|
+
color: rgba(255,255,255,0.75);
|
|
1004
|
+
}
|
|
1005
|
+
.cxp-cli-missing-actions { display: flex; gap: 8px; margin-top: 4px; }
|
|
1006
|
+
.cxp-cli-missing-link, .cxp-cli-missing-recheck {
|
|
1007
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1008
|
+
padding: 5px 10px; font-size: 11px; font-weight: 500;
|
|
1009
|
+
border-radius: 5px; border: 1px solid rgba(255,255,255,0.15);
|
|
1010
|
+
background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85);
|
|
1011
|
+
text-decoration: none; cursor: pointer; user-select: none;
|
|
1012
|
+
}
|
|
1013
|
+
.cxp-cli-missing-link:hover, .cxp-cli-missing-recheck:hover {
|
|
1014
|
+
background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3);
|
|
1015
|
+
}
|
|
1016
|
+
.cxp-cli-missing-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
1017
|
+
.codex-panel.cxp-cli-blocked .cxp-send { opacity: 0.4 !important; cursor: not-allowed !important; }
|
|
1018
|
+
.codex-panel.cxp-cli-blocked .cxp-input { opacity: 0.7; }
|
|
987
1019
|
.cxp-system {
|
|
988
1020
|
align-self: center;
|
|
989
1021
|
max-width: 92%;
|
|
@@ -2041,6 +2041,7 @@ function startupEmptyText() {
|
|
|
2041
2041
|
export function clearTranscript(emptyText = freshThreadText()) {
|
|
2042
2042
|
if (!_messagesEl) return;
|
|
2043
2043
|
_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>`;
|
|
2044
|
+
document.dispatchEvent(new CustomEvent('cdx-empty-rebuilt'));
|
|
2044
2045
|
_items.clear();
|
|
2045
2046
|
_requestCards.clear();
|
|
2046
2047
|
_blockingServerRequests = new Map();
|
|
@@ -3271,11 +3272,12 @@ export function syncInputEnabled() {
|
|
|
3271
3272
|
const hasPaths = !!(tab?.pendingPaths?.length);
|
|
3272
3273
|
const blockedByRequest = hasBlockingServerRequests();
|
|
3273
3274
|
if (input) input.disabled = !_connected || !_bootstrapped || _startingThread || blockedByRequest;
|
|
3275
|
+
const cliBlocked = document.getElementById('codex-panel')?.classList.contains('cxp-cli-blocked');
|
|
3274
3276
|
if (send) {
|
|
3275
3277
|
const isStopMode = !!(tab?.running || _running);
|
|
3276
3278
|
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));
|
|
3279
|
+
send.title = cliBlocked ? 'Codex CLI not installed' : (isStopMode ? 'Stop' : 'Send');
|
|
3280
|
+
send.disabled = cliBlocked || (isStopMode ? false : (!_connected || !_bootstrapped || _running || _startingThread || blockedByRequest || (!hasText && !hasImages && !hasPaths)));
|
|
3279
3281
|
}
|
|
3280
3282
|
if (fresh) {
|
|
3281
3283
|
pruneDiscardableTabs({ keepTab: tab, persist: true });
|
|
@@ -4528,6 +4530,12 @@ export function sendPrompt() {
|
|
|
4528
4530
|
const { input, tab, prompt, images, paths } = payload;
|
|
4529
4531
|
if (!input || !_connected || !_bootstrapped) return;
|
|
4530
4532
|
if (!prompt && !images.length && !paths.length) return;
|
|
4533
|
+
// Block when CLI binary missing — banner already informs user.
|
|
4534
|
+
const panel = document.getElementById('codex-panel');
|
|
4535
|
+
if (panel?.classList.contains('cxp-cli-blocked')) {
|
|
4536
|
+
appendSystem('Codex CLI not installed. Install it first, then click Re-check.', 'error');
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4531
4539
|
handleLocalSlashCommand(payload).then((handled) => {
|
|
4532
4540
|
if (handled) return;
|
|
4533
4541
|
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) {
|
|
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;
|
|
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,69 @@ function _ocpDocEscHandler(event) {
|
|
|
1503
1520
|
abortMessage();
|
|
1504
1521
|
}
|
|
1505
1522
|
|
|
1523
|
+
// ── CLI installation status (banner + send-block) ──
|
|
1524
|
+
function renderCliMissingBanner(emptyEl) {
|
|
1525
|
+
if (!emptyEl) return;
|
|
1526
|
+
const label = getCliLabel('opencode');
|
|
1527
|
+
const cmd = getCliInstallCommand('opencode');
|
|
1528
|
+
const url = getCliDocUrl('opencode');
|
|
1529
|
+
const hintEl = emptyEl.querySelector('.ocp-empty-hint');
|
|
1530
|
+
if (hintEl) hintEl.style.display = 'none';
|
|
1531
|
+
let banner = emptyEl.querySelector('.ocp-cli-missing');
|
|
1532
|
+
if (!banner) {
|
|
1533
|
+
banner = document.createElement('div');
|
|
1534
|
+
banner.className = 'ocp-cli-missing';
|
|
1535
|
+
banner.innerHTML = `
|
|
1536
|
+
<div class="ocp-cli-missing-title">${label} CLI not installed</div>
|
|
1537
|
+
<div class="ocp-cli-missing-body">Run: <code>${cmd}</code></div>
|
|
1538
|
+
<div class="ocp-cli-missing-actions">
|
|
1539
|
+
<a class="ocp-cli-missing-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
|
|
1540
|
+
<button class="ocp-cli-missing-recheck" type="button">Re-check</button>
|
|
1541
|
+
</div>
|
|
1542
|
+
`;
|
|
1543
|
+
emptyEl.appendChild(banner);
|
|
1544
|
+
const btn = banner.querySelector('.ocp-cli-missing-recheck');
|
|
1545
|
+
btn?.addEventListener('click', async () => {
|
|
1546
|
+
btn.disabled = true;
|
|
1547
|
+
btn.textContent = 'Checking…';
|
|
1548
|
+
try { await recheckCliStatus('opencode'); }
|
|
1549
|
+
finally {
|
|
1550
|
+
if (btn.isConnected) {
|
|
1551
|
+
btn.disabled = false;
|
|
1552
|
+
btn.textContent = 'Re-check';
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function refreshCliBanners() {
|
|
1560
|
+
if (!_panel) return;
|
|
1561
|
+
const empties = _panel.querySelectorAll('.ocp-empty');
|
|
1562
|
+
if (_cliInstalled === null) {
|
|
1563
|
+
_panel.classList.add('ocp-cli-blocked');
|
|
1564
|
+
for (const el of empties) renderCliMissingBanner(el);
|
|
1565
|
+
} else {
|
|
1566
|
+
_panel.classList.remove('ocp-cli-blocked');
|
|
1567
|
+
for (const el of empties) {
|
|
1568
|
+
const banner = el.querySelector('.ocp-cli-missing');
|
|
1569
|
+
if (banner) banner.remove();
|
|
1570
|
+
const hintEl = el.querySelector('.ocp-empty-hint');
|
|
1571
|
+
if (hintEl) hintEl.style.display = '';
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
syncSendButton();
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function ensureCliSubscription() {
|
|
1578
|
+
if (_cliUnsub) return;
|
|
1579
|
+
_cliUnsub = subscribeCliStatus('opencode', (info) => {
|
|
1580
|
+
_cliInstalled = info?.installed || null;
|
|
1581
|
+
refreshCliBanners();
|
|
1582
|
+
});
|
|
1583
|
+
document.addEventListener('ocp-empty-rebuilt', () => refreshCliBanners());
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1506
1586
|
function setVisible(nextVisible) {
|
|
1507
1587
|
if (!_panel) return;
|
|
1508
1588
|
if (!nextVisible) {
|
|
@@ -1528,6 +1608,7 @@ function setVisible(nextVisible) {
|
|
|
1528
1608
|
if (_curProject) loadBranches(_curProject);
|
|
1529
1609
|
loadCurrentProfile();
|
|
1530
1610
|
loadRecallProfile();
|
|
1611
|
+
refreshCliBanners();
|
|
1531
1612
|
} else {
|
|
1532
1613
|
document.removeEventListener('keydown', _ocpDocEscHandler, { capture: true });
|
|
1533
1614
|
}
|
|
@@ -1651,6 +1732,7 @@ function ensurePanel() {
|
|
|
1651
1732
|
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'opencode' } }));
|
|
1652
1733
|
|
|
1653
1734
|
syncSendButton();
|
|
1735
|
+
ensureCliSubscription();
|
|
1654
1736
|
|
|
1655
1737
|
// Connect WebSocket
|
|
1656
1738
|
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,39 @@ export function injectStyles() {
|
|
|
1521
1521
|
stroke: currentColor; fill: none;
|
|
1522
1522
|
stroke-width: 1; opacity: 0.3;
|
|
1523
1523
|
}
|
|
1524
|
+
.ocp-cli-missing {
|
|
1525
|
+
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
1526
|
+
max-width: 320px; padding: 14px 16px; margin-top: 4px;
|
|
1527
|
+
border: 1px solid rgba(255,180,80,0.35); border-radius: 8px;
|
|
1528
|
+
background: rgba(255,180,80,0.06);
|
|
1529
|
+
color: rgba(255,255,255,0.6);
|
|
1530
|
+
}
|
|
1531
|
+
.ocp-cli-missing-title {
|
|
1532
|
+
font-size: 12px; font-weight: 600; color: rgba(255,200,120,0.9);
|
|
1533
|
+
letter-spacing: 0.02em;
|
|
1534
|
+
}
|
|
1535
|
+
.ocp-cli-missing-body {
|
|
1536
|
+
font-size: 11px; color: rgba(255,255,255,0.55); text-align: center; line-height: 1.45;
|
|
1537
|
+
}
|
|
1538
|
+
.ocp-cli-missing-body code {
|
|
1539
|
+
display: inline-block; padding: 1px 6px; margin-top: 4px;
|
|
1540
|
+
background: rgba(0,0,0,0.35); border-radius: 4px; font-size: 10.5px;
|
|
1541
|
+
color: rgba(255,255,255,0.75);
|
|
1542
|
+
}
|
|
1543
|
+
.ocp-cli-missing-actions { display: flex; gap: 8px; margin-top: 4px; }
|
|
1544
|
+
.ocp-cli-missing-link, .ocp-cli-missing-recheck {
|
|
1545
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1546
|
+
padding: 5px 10px; font-size: 11px; font-weight: 500;
|
|
1547
|
+
border-radius: 5px; border: 1px solid rgba(255,255,255,0.15);
|
|
1548
|
+
background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85);
|
|
1549
|
+
text-decoration: none; cursor: pointer; user-select: none;
|
|
1550
|
+
font-family: inherit;
|
|
1551
|
+
}
|
|
1552
|
+
.ocp-cli-missing-link:hover, .ocp-cli-missing-recheck:hover {
|
|
1553
|
+
background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3);
|
|
1554
|
+
}
|
|
1555
|
+
.ocp-cli-missing-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
1556
|
+
.ocp-panel.ocp-cli-blocked .ocp-input { opacity: 0.7; }
|
|
1524
1557
|
|
|
1525
1558
|
/* ── Thinking indicator ── */
|
|
1526
1559
|
.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,41 @@ 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 (replaces empty-state hint when claude binary missing) ── */
|
|
436
|
+
.cp-cli-missing {
|
|
437
|
+
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
438
|
+
max-width: 320px; padding: 14px 16px; margin-top: 4px;
|
|
439
|
+
border: 1px solid rgba(255,180,80,0.35); border-radius: 8px;
|
|
440
|
+
background: rgba(255,180,80,0.06); pointer-events: auto;
|
|
441
|
+
}
|
|
442
|
+
.cp-cli-missing-title {
|
|
443
|
+
font-size: 12px; font-weight: 600; color: rgba(255,200,120,0.9);
|
|
444
|
+
letter-spacing: 0.02em;
|
|
445
|
+
}
|
|
446
|
+
.cp-cli-missing-body {
|
|
447
|
+
font-size: 11px; color: rgba(255,255,255,0.55); text-align: center; line-height: 1.45;
|
|
448
|
+
}
|
|
449
|
+
.cp-cli-missing-body code {
|
|
450
|
+
display: inline-block; padding: 1px 6px; margin-top: 4px;
|
|
451
|
+
background: rgba(0,0,0,0.35); border-radius: 4px; font-size: 10.5px;
|
|
452
|
+
color: rgba(255,255,255,0.75);
|
|
453
|
+
}
|
|
454
|
+
.cp-cli-missing-actions { display: flex; gap: 8px; margin-top: 4px; }
|
|
455
|
+
.cp-cli-missing-link, .cp-cli-missing-recheck {
|
|
456
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
457
|
+
padding: 5px 10px; font-size: 11px; font-weight: 500;
|
|
458
|
+
border-radius: 5px; border: 1px solid rgba(255,255,255,0.15);
|
|
459
|
+
background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85);
|
|
460
|
+
text-decoration: none; cursor: pointer; user-select: none;
|
|
461
|
+
}
|
|
462
|
+
.cp-cli-missing-link:hover, .cp-cli-missing-recheck:hover {
|
|
463
|
+
background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3);
|
|
464
|
+
}
|
|
465
|
+
.cp-cli-missing-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
466
|
+
/* Block send button visually + functionally when CLI missing */
|
|
467
|
+
.claude-panel.cp-cli-blocked .cp-send { opacity: 0.4 !important; cursor: not-allowed !important; }
|
|
468
|
+
.claude-panel.cp-cli-blocked .cp-input { opacity: 0.7; }
|
|
469
|
+
|
|
430
470
|
/* ── Browser embed (live screencast inside panel) ── */
|
|
431
471
|
.cp-browser-embed {
|
|
432
472
|
display: none;
|
|
@@ -2649,14 +2689,14 @@ document.addEventListener('click', (e) => {
|
|
|
2649
2689
|
});
|
|
2650
2690
|
|
|
2651
2691
|
// ── Tab lifecycle ──
|
|
2652
|
-
function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
|
|
2692
|
+
function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
|
|
2653
2693
|
if (_tabs.length >= MAX_TABS) return null;
|
|
2654
2694
|
// Capture current dropdown state for the new tab's project/model/effort
|
|
2655
2695
|
const _$proj = _panel?.querySelector('#cp-project');
|
|
2656
2696
|
const _$mod = _panel?.querySelector('#cp-model');
|
|
2657
2697
|
const _$think = _panel?.querySelector('#cp-think-toggle');
|
|
2658
2698
|
const tab = {
|
|
2659
|
-
id: id || crypto.randomUUID(),
|
|
2699
|
+
id: id || crypto.randomUUID(),
|
|
2660
2700
|
sessionId,
|
|
2661
2701
|
label,
|
|
2662
2702
|
project: ddGetValue(_$proj) || '', // per-tab project isolation
|
|
@@ -2712,6 +2752,7 @@ function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id
|
|
|
2712
2752
|
msgDiv.innerHTML = `<div class="cp-empty"><div class="cp-empty-logo">${CLAUDE_ICON}</div><span class="cp-empty-name">Claude</span><span class="cp-empty-hint">Send a message to start</span></div>`;
|
|
2713
2753
|
container.appendChild(msgDiv);
|
|
2714
2754
|
tab.messagesEl = msgDiv;
|
|
2755
|
+
if (_cliInstalled === null) _renderCliMissingBanner(msgDiv.querySelector('.cp-empty'));
|
|
2715
2756
|
// Connect WebSocket
|
|
2716
2757
|
connectTab(tab);
|
|
2717
2758
|
// Create pill (rendered in shared tray, not inside panel)
|
|
@@ -2975,7 +3016,7 @@ function restoreTabs() {
|
|
|
2975
3016
|
const data = JSON.parse(raw);
|
|
2976
3017
|
if (data.tabs?.length) {
|
|
2977
3018
|
for (const saved of data.tabs) {
|
|
2978
|
-
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
3019
|
+
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
2979
3020
|
if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
|
|
2980
3021
|
if (t && saved.running) t._wasRunning = true; // trigger reattach on WS open
|
|
2981
3022
|
if (t && saved.project) t.project = saved.project;
|
|
@@ -2998,7 +3039,7 @@ function restoreTabs() {
|
|
|
2998
3039
|
const data = JSON.parse(legacyRaw);
|
|
2999
3040
|
if (data.tabs?.length) {
|
|
3000
3041
|
for (const saved of data.tabs) {
|
|
3001
|
-
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
3042
|
+
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
3002
3043
|
if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
|
|
3003
3044
|
if (t && saved.running) t._wasRunning = true;
|
|
3004
3045
|
if (t && saved.project) t.project = saved.project;
|
|
@@ -3535,6 +3576,7 @@ async function selectSession(sid, label) {
|
|
|
3535
3576
|
const btn = _panel?.querySelector('.cp-session-label');
|
|
3536
3577
|
if (btn) btn.textContent = tab.label;
|
|
3537
3578
|
tab.messagesEl.innerHTML = `<div class="cp-empty"><div class="cp-empty-logo">${CLAUDE_ICON}</div><span class="cp-empty-name">Claude</span><span class="cp-empty-hint">Send a message to start</span></div>`;
|
|
3579
|
+
if (_cliInstalled === null) _renderCliMissingBanner(tab.messagesEl.querySelector('.cp-empty'));
|
|
3538
3580
|
if (sid) {
|
|
3539
3581
|
loadSessionHistory(sid, tab.messagesEl);
|
|
3540
3582
|
// Restore session cost from server
|
|
@@ -6544,6 +6586,10 @@ function runSlashCommand(tab, raw) {
|
|
|
6544
6586
|
}
|
|
6545
6587
|
|
|
6546
6588
|
function send({ shift = false } = {}) {
|
|
6589
|
+
if (_cliInstalled === null) {
|
|
6590
|
+
notify({ type: NOTIF_TYPE.WARN, title: 'Claude CLI not installed', body: 'Install the Claude Code CLI before sending a message.' });
|
|
6591
|
+
return;
|
|
6592
|
+
}
|
|
6547
6593
|
const tab = activeTab();
|
|
6548
6594
|
if (!tab) return;
|
|
6549
6595
|
const $input = _panel?.querySelector('#cp-input');
|
|
@@ -7246,6 +7292,70 @@ function syncReservedWidth() {
|
|
|
7246
7292
|
}
|
|
7247
7293
|
}
|
|
7248
7294
|
|
|
7295
|
+
// ── CLI installation status (banner + send-block) ──
|
|
7296
|
+
function _renderCliMissingBanner(emptyEl) {
|
|
7297
|
+
if (!emptyEl) return;
|
|
7298
|
+
const label = getCliLabel('claude-code');
|
|
7299
|
+
const cmd = getCliInstallCommand('claude-code');
|
|
7300
|
+
const url = getCliDocUrl('claude-code');
|
|
7301
|
+
const hint = emptyEl.querySelector('.cp-empty-hint');
|
|
7302
|
+
if (hint) hint.style.display = 'none';
|
|
7303
|
+
let banner = emptyEl.querySelector('.cp-cli-missing');
|
|
7304
|
+
if (!banner) {
|
|
7305
|
+
banner = document.createElement('div');
|
|
7306
|
+
banner.className = 'cp-cli-missing';
|
|
7307
|
+
banner.innerHTML = `
|
|
7308
|
+
<div class="cp-cli-missing-title">${label} CLI not installed</div>
|
|
7309
|
+
<div class="cp-cli-missing-body">Run: <code>${cmd}</code></div>
|
|
7310
|
+
<div class="cp-cli-missing-actions">
|
|
7311
|
+
<a class="cp-cli-missing-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
|
|
7312
|
+
<button class="cp-cli-missing-recheck" type="button">Re-check</button>
|
|
7313
|
+
</div>
|
|
7314
|
+
`;
|
|
7315
|
+
emptyEl.appendChild(banner);
|
|
7316
|
+
const btn = banner.querySelector('.cp-cli-missing-recheck');
|
|
7317
|
+
btn?.addEventListener('click', async () => {
|
|
7318
|
+
btn.disabled = true;
|
|
7319
|
+
btn.textContent = 'Checking…';
|
|
7320
|
+
try { await recheckCliStatus('claude-code'); }
|
|
7321
|
+
finally {
|
|
7322
|
+
if (btn.isConnected) {
|
|
7323
|
+
btn.disabled = false;
|
|
7324
|
+
btn.textContent = 'Re-check';
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
});
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
|
|
7331
|
+
function _refreshCliBanners() {
|
|
7332
|
+
if (!_panel) return;
|
|
7333
|
+
const empties = _panel.querySelectorAll('.cp-empty');
|
|
7334
|
+
if (_cliInstalled === null) {
|
|
7335
|
+
_panel.classList.add('cp-cli-blocked');
|
|
7336
|
+
for (const el of empties) _renderCliMissingBanner(el);
|
|
7337
|
+
} else {
|
|
7338
|
+
_panel.classList.remove('cp-cli-blocked');
|
|
7339
|
+
for (const el of empties) {
|
|
7340
|
+
const banner = el.querySelector('.cp-cli-missing');
|
|
7341
|
+
if (banner) banner.remove();
|
|
7342
|
+
const hint = el.querySelector('.cp-empty-hint');
|
|
7343
|
+
if (hint) hint.style.display = '';
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
// Re-sync send button state — installed transition should re-enable input drafts
|
|
7347
|
+
const $send = _panel.querySelector('#cp-send');
|
|
7348
|
+
if ($send && _cliInstalled === null) $send.disabled = true;
|
|
7349
|
+
}
|
|
7350
|
+
|
|
7351
|
+
function _ensureCliSubscription() {
|
|
7352
|
+
if (_cliUnsub) return;
|
|
7353
|
+
_cliUnsub = subscribeCliStatus('claude-code', (info) => {
|
|
7354
|
+
_cliInstalled = info?.installed || null;
|
|
7355
|
+
_refreshCliBanners();
|
|
7356
|
+
});
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7249
7359
|
// ── Public API ──
|
|
7250
7360
|
export async function toggleClaudePanel() {
|
|
7251
7361
|
if (!_panel) {
|
|
@@ -7255,13 +7365,14 @@ export async function toggleClaudePanel() {
|
|
|
7255
7365
|
wireEvents();
|
|
7256
7366
|
await loadConfig(); // must complete before restoreTabs — checks server boot ID
|
|
7257
7367
|
loadSkills();
|
|
7258
|
-
restoreTabs();
|
|
7259
|
-
loadMonthlyCost();
|
|
7260
|
-
syncSessionCosts();
|
|
7261
|
-
_wireBrowserSync();
|
|
7262
|
-
_startHeartbeat();
|
|
7263
|
-
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
|
|
7264
|
-
|
|
7368
|
+
restoreTabs();
|
|
7369
|
+
loadMonthlyCost();
|
|
7370
|
+
syncSessionCosts();
|
|
7371
|
+
_wireBrowserSync();
|
|
7372
|
+
_startHeartbeat();
|
|
7373
|
+
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
|
|
7374
|
+
_ensureCliSubscription();
|
|
7375
|
+
}
|
|
7265
7376
|
_visible = !_visible;
|
|
7266
7377
|
if (_visible) {
|
|
7267
7378
|
if (_tabs.length === 0) createTab(null, 'New chat');
|
|
@@ -7269,6 +7380,7 @@ export async function toggleClaudePanel() {
|
|
|
7269
7380
|
_panel.classList.add('open');
|
|
7270
7381
|
syncReservedWidth();
|
|
7271
7382
|
_panel.querySelector('#cp-input')?.focus();
|
|
7383
|
+
_refreshCliBanners();
|
|
7272
7384
|
} else {
|
|
7273
7385
|
const tab = activeTab();
|
|
7274
7386
|
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));
|