synabun 2026.4.21 → 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 +175 -881
- package/README.md +1 -1
- package/hooks/claude-code/prompt-submit.mjs +39 -18
- package/hooks/claude-code/session-start.mjs +10 -3
- package/hooks/claude-code/shared.mjs +39 -2
- package/hooks/claude-code/stop.mjs +33 -6
- package/mcp-server/src/index.ts +9 -1
- package/mcp-server/src/services/neural-interface.ts +42 -14
- package/mcp-server/src/services/profiles.ts +8 -7
- package/mcp-server/src/tools/browser-advanced.ts +12 -10
- package/mcp-server/src/tools/browser-interact.ts +39 -38
- package/mcp-server/src/tools/browser-navigate.ts +12 -4
- package/mcp-server/src/tools/loop.ts +7 -3
- package/mcp-server/src/tools/profile.ts +19 -1
- package/neural-interface/lib/loop-logger.js +103 -0
- package/neural-interface/lib/mcp-installer.js +109 -0
- package/neural-interface/lib/plugin-installer.js +264 -0
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/shared/api.js +66 -17
- package/neural-interface/public/shared/cdx/cdx-icons.js +5 -4
- package/neural-interface/public/shared/cdx/cdx-panel.js +117 -14
- package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
- package/neural-interface/public/shared/cdx/cdx-styles.js +55 -10
- package/neural-interface/public/shared/cdx/cdx-tabs.js +394 -72
- package/neural-interface/public/shared/cli-status.js +157 -0
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/html-shell.js +32 -11
- package/neural-interface/public/shared/ocp/ocp-panel.js +127 -28
- package/neural-interface/public/shared/ocp/ocp-render.js +52 -44
- package/neural-interface/public/shared/ocp/ocp-styles.js +33 -0
- package/neural-interface/public/shared/ocp/ocp-tabs.js +693 -82
- package/neural-interface/public/shared/styles.css +635 -0
- package/neural-interface/public/shared/ui-automation-studio.js +38 -12
- package/neural-interface/public/shared/ui-claude-panel.js +322 -15
- package/neural-interface/public/shared/ui-file-explorer.js +38 -18
- package/neural-interface/public/shared/ui-menubar.js +34 -10
- package/neural-interface/public/shared/ui-navbar.js +8 -6
- package/neural-interface/public/shared/ui-schedules-studio.js +1464 -0
- package/neural-interface/public/shared/ui-settings.js +274 -50
- package/neural-interface/public/shared/ui-sidepanel-layout.js +18 -7
- package/neural-interface/public/shared/ui-sidepanel-tray.js +287 -0
- package/neural-interface/public/shared/ui-sync.js +9 -5
- package/neural-interface/public/shared/ui-terminal.js +76 -27
- package/neural-interface/public/shared/ui-tool-updates.js +49 -15
- package/neural-interface/public/shared/ui-tutorial-steps.js +4 -2
- package/neural-interface/public/shared/ui-tutorial.js +24 -18
- package/neural-interface/public/shared/ui-whiteboard.js +79 -31
- package/neural-interface/public/shared/vterm-buffer.js +9 -8
- package/neural-interface/public/variant/2d/main.js +9 -7
- package/neural-interface/public/variant/3d/main.js +10 -8
- package/neural-interface/server.js +1292 -255
- package/package.json +1 -1
- package/skills/leonardo/SKILL.md +5 -1
- package/skills/leonardo/modules/image-prompter.md +5 -1
- package/skills/leonardo/modules/video-prompter.md +5 -1
- package/skills/synabun/SKILL.md +44 -26
- package/skills/synabun/modules/audit.md +40 -36
- package/skills/synabun/modules/changelog.md +22 -18
- package/skills/synabun/modules/memorize.md +19 -15
- package/skills/synabun/modules/schedule.md +285 -0
- package/CLAUDE.md +0 -217
|
@@ -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
|
+
}
|
|
@@ -67,8 +67,9 @@ export const KEYS = {
|
|
|
67
67
|
ACTIVE_WORKSPACE: 'neural-active-workspace', // sessionStorage
|
|
68
68
|
GRID_SNAP: 'neural-grid-snap',
|
|
69
69
|
CATEGORIES_VISIBLE: 'neural-categories-visible',
|
|
70
|
-
WHITEBOARD: 'neural-whiteboard',
|
|
71
|
-
|
|
70
|
+
WHITEBOARD: 'neural-whiteboard',
|
|
71
|
+
WHITEBOARD_RULESET_ALERT_DISMISSED: 'neural-whiteboard-ruleset-alert-dismissed',
|
|
72
|
+
TUTORIAL_COMPLETED: 'neural-tutorial-completed',
|
|
72
73
|
TUTORIAL_SKIPPED: 'neural-tutorial-skipped',
|
|
73
74
|
TUTORIAL_STEP: 'neural-tutorial-step',
|
|
74
75
|
TUTORIAL_STARTED: 'neural-tutorial-started',
|
|
@@ -294,11 +294,23 @@ export function getSharedHTML() {
|
|
|
294
294
|
<span class="menu-shortcut">A</span>
|
|
295
295
|
</div>
|
|
296
296
|
<div class="menu-sep"></div>
|
|
297
|
-
<div class="menu-item" id="menu-automations-schedules">
|
|
298
|
-
<span class="menu-check"></span>
|
|
299
|
-
<span class="menu-text"><span class="menu-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></span> Schedules</span>
|
|
300
|
-
</div>
|
|
301
|
-
<div class="menu-
|
|
297
|
+
<div class="menu-item" id="menu-automations-schedules">
|
|
298
|
+
<span class="menu-check"></span>
|
|
299
|
+
<span class="menu-text"><span class="menu-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></span> Schedules</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="menu-item" id="menu-automations-schedule-new">
|
|
302
|
+
<span class="menu-check"></span>
|
|
303
|
+
<span class="menu-text">New Cron Schedule</span>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="menu-item" id="menu-automations-quick-timer">
|
|
306
|
+
<span class="menu-check"></span>
|
|
307
|
+
<span class="menu-text">Quick Timer</span>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="menu-item" id="menu-automations-schedule-groups">
|
|
310
|
+
<span class="menu-check"></span>
|
|
311
|
+
<span class="menu-text">Schedule Groups</span>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="menu-sep"></div>
|
|
302
314
|
<div class="menu-item" id="menu-automations-new">
|
|
303
315
|
<span class="menu-check"></span>
|
|
304
316
|
<span class="menu-text">${t('menu.automations.newAutomation')}</span>
|
|
@@ -703,12 +715,21 @@ export function getSharedHTML() {
|
|
|
703
715
|
</div>
|
|
704
716
|
|
|
705
717
|
<!-- Static background (shown in Focus Mode) -->
|
|
706
|
-
<div id="static-bg">
|
|
707
|
-
<div class="focus-breathe"></div>
|
|
708
|
-
<img src="synabun.png?v=2" alt="SynaBun" class="static-bg-logo">
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
718
|
+
<div id="static-bg">
|
|
719
|
+
<div class="focus-breathe"></div>
|
|
720
|
+
<img src="synabun.png?v=2" alt="SynaBun" class="static-bg-logo">
|
|
721
|
+
<div id="wb-ruleset-alert" class="wb-ruleset-alert" aria-live="polite">
|
|
722
|
+
<div class="wb-ruleset-alert-text">
|
|
723
|
+
Add <code>AGENTS.md</code> and <code>CLAUDE.md</code> rulesets so memory usage stays consistent.
|
|
724
|
+
</div>
|
|
725
|
+
<div class="wb-ruleset-alert-actions">
|
|
726
|
+
<button type="button" id="wb-ruleset-settings" class="wb-ruleset-alert-btn">Setup</button>
|
|
727
|
+
<button type="button" id="wb-ruleset-dismiss" class="wb-ruleset-alert-btn wb-ruleset-alert-dismiss">Don't Show This Again</button>
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
|
|
731
|
+
<!-- Whiteboard (Focus Mode canvas) -->
|
|
732
|
+
<div id="wb-root">
|
|
712
733
|
<div id="wb-toolbar" class="glass">
|
|
713
734
|
<button class="wb-tool active" data-tool="select" data-tooltip="Select">
|
|
714
735
|
<svg viewBox="0 0 24 24"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/></svg>
|
|
@@ -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 {
|
|
@@ -21,11 +22,18 @@ import {
|
|
|
21
22
|
setPanelEl, setOnUpdate, setPanelVisible, setOnShow, setOnHide, setOnEditPlan,
|
|
22
23
|
createTab, switchTab, closeTab, renderPills, saveTabs, restoreTabs,
|
|
23
24
|
loadSessions, createSession, renameSession, loadProviders, loadAgents, populateModelDropdown,
|
|
24
|
-
sendMessage, abortMessage,
|
|
25
|
+
sendMessage, abortMessage, handleSSEEvent,
|
|
25
26
|
revertSession, compactSession, shareSession, executeCommand, renderTabMessages,
|
|
26
27
|
getActiveTabMode, setTabMode, resolveContextInputTokens, setActivityVisible,
|
|
27
|
-
toggleActivityExpanded, abortAgent,
|
|
28
|
+
toggleActivityExpanded, abortAgent, ensurePlanFile, showPostPlanUI, applySavedPlan,
|
|
29
|
+
flushAllSessionSnapshots,
|
|
28
30
|
} from './ocp-tabs.js';
|
|
31
|
+
|
|
32
|
+
window.addEventListener('beforeunload', () => { try { flushAllSessionSnapshots(); } catch {} });
|
|
33
|
+
window.addEventListener('pagehide', () => { try { flushAllSessionSnapshots(); } catch {} });
|
|
34
|
+
document.addEventListener('visibilitychange', () => {
|
|
35
|
+
if (document.hidden) { try { flushAllSessionSnapshots(); } catch {} }
|
|
36
|
+
});
|
|
29
37
|
import { isClaudePanelOpen, toggleClaudePanel } from '../ui-claude-panel.js';
|
|
30
38
|
import { isCodexPanelOpen, toggleCodexPanel } from '../ui-codex-panel.js';
|
|
31
39
|
|
|
@@ -57,9 +65,15 @@ let _serverVersion = null;
|
|
|
57
65
|
let _serverManaged = false;
|
|
58
66
|
let _projects = [];
|
|
59
67
|
let _projectsLoaded = false;
|
|
68
|
+
let _cliInstalled = null;
|
|
69
|
+
let _cliUnsub = null;
|
|
60
70
|
|
|
61
71
|
function panelEl(sel) { return _panel?.querySelector(sel) || null; }
|
|
62
72
|
|
|
73
|
+
function activeTabRunning() {
|
|
74
|
+
return !!activeTab()?.running;
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
function sessionTitleFor(sessionOrTab) {
|
|
64
78
|
const sid = sessionOrTab?.id || sessionOrTab?.sessionID || sessionOrTab?.sessionId || '';
|
|
65
79
|
const candidates = [sessionOrTab?.sessionTitle, sessionOrTab?.title, sessionOrTab?.description, sessionOrTab?.slug];
|
|
@@ -496,10 +510,10 @@ function wireEvents() {
|
|
|
496
510
|
}
|
|
497
511
|
if (e.key === 'Escape') {
|
|
498
512
|
hideSlashHints();
|
|
499
|
-
if (
|
|
513
|
+
if (activeTabRunning()) {
|
|
500
514
|
e.preventDefault();
|
|
501
515
|
e.stopPropagation();
|
|
502
|
-
|
|
516
|
+
abortMessage();
|
|
503
517
|
}
|
|
504
518
|
}
|
|
505
519
|
// Navigate slash hints with arrow keys
|
|
@@ -543,17 +557,17 @@ function wireEvents() {
|
|
|
543
557
|
|| _panel.querySelector('#ocp-slash-hints:not([hidden])')
|
|
544
558
|
|| _panel.querySelector('.ocp-rename-input')
|
|
545
559
|
|| _panel.querySelector('#ocp-session-menu.open');
|
|
546
|
-
if (!ownsEsc &&
|
|
560
|
+
if (!ownsEsc && activeTabRunning()) {
|
|
547
561
|
e.preventDefault();
|
|
548
562
|
e.stopPropagation();
|
|
549
|
-
|
|
563
|
+
abortMessage();
|
|
550
564
|
}
|
|
551
565
|
}
|
|
552
566
|
});
|
|
553
567
|
if (sendBtn) {
|
|
554
568
|
sendBtn.addEventListener('click', () => {
|
|
555
|
-
if (
|
|
556
|
-
|
|
569
|
+
if (activeTabRunning()) {
|
|
570
|
+
abortMessage();
|
|
557
571
|
} else {
|
|
558
572
|
handleSend();
|
|
559
573
|
}
|
|
@@ -896,9 +910,17 @@ async function handleSend() {
|
|
|
896
910
|
if (!input) return;
|
|
897
911
|
const tab = activeTab();
|
|
898
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
|
+
}
|
|
899
917
|
let text = input.value.trim();
|
|
900
918
|
const images = Array.isArray(tab.attachedImages) ? [...tab.attachedImages] : [];
|
|
901
919
|
if (!text && !images.length) return;
|
|
920
|
+
if (tab.showPostPlanActions) {
|
|
921
|
+
setStatus('working', 'Choose Continue, Compact, or Edit plan first.');
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
902
924
|
|
|
903
925
|
// Parse and execute slash commands. If executeSlashCommand returns false
|
|
904
926
|
// (e.g. a SynaBun skill that the server will expand), keep `text` in flight
|
|
@@ -991,14 +1013,26 @@ function syncSendButton() {
|
|
|
991
1013
|
const tab = activeTab();
|
|
992
1014
|
if (!sendBtn) return;
|
|
993
1015
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
+
|
|
1026
|
+
if (tab?.running) {
|
|
998
1027
|
sendBtn.disabled = false;
|
|
999
1028
|
sendBtn.classList.add('running');
|
|
1000
1029
|
sendBtn.setAttribute('data-tooltip', 'Stop turn (Esc)');
|
|
1001
1030
|
sendBtn.setAttribute('aria-label', 'Stop turn');
|
|
1031
|
+
} else if (tab?.showPostPlanActions) {
|
|
1032
|
+
sendBtn.classList.remove('running');
|
|
1033
|
+
sendBtn.disabled = true;
|
|
1034
|
+
sendBtn.setAttribute('data-tooltip', 'Choose a plan action first');
|
|
1035
|
+
sendBtn.setAttribute('aria-label', 'Choose a plan action first');
|
|
1002
1036
|
} else {
|
|
1003
1037
|
sendBtn.classList.remove('running');
|
|
1004
1038
|
sendBtn.disabled = (!input?.value.trim() && !(tab?.attachedImages?.length)) || !_serverReady;
|
|
@@ -1478,12 +1512,75 @@ function syncReservedWidth() {
|
|
|
1478
1512
|
function _ocpDocEscHandler(event) {
|
|
1479
1513
|
if (event.key !== 'Escape') return;
|
|
1480
1514
|
if (!_visible) return;
|
|
1481
|
-
if (!
|
|
1515
|
+
if (!activeTabRunning()) return;
|
|
1482
1516
|
const hintsEl = panelEl('#ocp-slash-hints');
|
|
1483
1517
|
if (hintsEl && hintsEl.classList.contains('open')) return; // let input handler dismiss hints first
|
|
1484
1518
|
event.preventDefault();
|
|
1485
1519
|
event.stopImmediatePropagation();
|
|
1486
|
-
|
|
1520
|
+
abortMessage();
|
|
1521
|
+
}
|
|
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());
|
|
1487
1584
|
}
|
|
1488
1585
|
|
|
1489
1586
|
function setVisible(nextVisible) {
|
|
@@ -1511,6 +1608,7 @@ function setVisible(nextVisible) {
|
|
|
1511
1608
|
if (_curProject) loadBranches(_curProject);
|
|
1512
1609
|
loadCurrentProfile();
|
|
1513
1610
|
loadRecallProfile();
|
|
1611
|
+
refreshCliBanners();
|
|
1514
1612
|
} else {
|
|
1515
1613
|
document.removeEventListener('keydown', _ocpDocEscHandler, { capture: true });
|
|
1516
1614
|
}
|
|
@@ -1599,32 +1697,31 @@ function ensurePanel() {
|
|
|
1599
1697
|
if (tab.planFilePath) { openPlan(tab.planFilePath); return; }
|
|
1600
1698
|
if (!tab.planContent) return;
|
|
1601
1699
|
try {
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1605
|
-
body: JSON.stringify({ content: tab.planContent }),
|
|
1606
|
-
});
|
|
1607
|
-
const result = await res.json();
|
|
1608
|
-
if (result.ok && result.path) {
|
|
1609
|
-
tab.planFilePath = result.path;
|
|
1610
|
-
saveTabs();
|
|
1611
|
-
openPlan(result.path);
|
|
1612
|
-
}
|
|
1700
|
+
const planPath = await ensurePlanFile(tab);
|
|
1701
|
+
if (planPath) openPlan(planPath);
|
|
1613
1702
|
} catch (e) {
|
|
1614
1703
|
console.error('[ocp-panel] Edit plan failed:', e);
|
|
1704
|
+
showPostPlanUI(tab);
|
|
1615
1705
|
}
|
|
1616
1706
|
});
|
|
1617
1707
|
|
|
1618
1708
|
// Listen for edited plan content from file explorer
|
|
1619
|
-
on('plan-saved', ({ content, source, tabId }) => {
|
|
1709
|
+
on('plan-saved', ({ filePath, content, source, tabId }) => {
|
|
1620
1710
|
if (source !== 'opencode') return;
|
|
1621
1711
|
const tab = getTabs().find(t => t.id === tabId) || activeTab();
|
|
1622
1712
|
if (tab && content) {
|
|
1623
|
-
tab
|
|
1624
|
-
saveTabs();
|
|
1713
|
+
applySavedPlan(tab, content, filePath);
|
|
1625
1714
|
}
|
|
1626
1715
|
});
|
|
1627
1716
|
|
|
1717
|
+
on('plan-edit-cancelled', ({ source, tabId } = {}) => {
|
|
1718
|
+
if (source !== 'opencode') return;
|
|
1719
|
+
const tab = getTabs().find(t => t.id === tabId) || activeTab();
|
|
1720
|
+
if (!tab) return;
|
|
1721
|
+
tab.showPostPlanActions = true;
|
|
1722
|
+
showPostPlanUI(tab, tab.postPlanHeader || 'PLAN COMPLETE');
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1628
1725
|
wireEvents();
|
|
1629
1726
|
autosizeInput();
|
|
1630
1727
|
|
|
@@ -1632,8 +1729,10 @@ function ensurePanel() {
|
|
|
1632
1729
|
if (!restoreTabs()) {
|
|
1633
1730
|
createTab({ project: storage.getItem(STOR.project) || '' });
|
|
1634
1731
|
}
|
|
1732
|
+
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'opencode' } }));
|
|
1635
1733
|
|
|
1636
1734
|
syncSendButton();
|
|
1735
|
+
ensureCliSubscription();
|
|
1637
1736
|
|
|
1638
1737
|
// Connect WebSocket
|
|
1639
1738
|
connectWs(wsCallbacks());
|