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.
Files changed (61) hide show
  1. package/CHANGELOG.md +175 -881
  2. package/README.md +1 -1
  3. package/hooks/claude-code/prompt-submit.mjs +39 -18
  4. package/hooks/claude-code/session-start.mjs +10 -3
  5. package/hooks/claude-code/shared.mjs +39 -2
  6. package/hooks/claude-code/stop.mjs +33 -6
  7. package/mcp-server/src/index.ts +9 -1
  8. package/mcp-server/src/services/neural-interface.ts +42 -14
  9. package/mcp-server/src/services/profiles.ts +8 -7
  10. package/mcp-server/src/tools/browser-advanced.ts +12 -10
  11. package/mcp-server/src/tools/browser-interact.ts +39 -38
  12. package/mcp-server/src/tools/browser-navigate.ts +12 -4
  13. package/mcp-server/src/tools/loop.ts +7 -3
  14. package/mcp-server/src/tools/profile.ts +19 -1
  15. package/neural-interface/lib/loop-logger.js +103 -0
  16. package/neural-interface/lib/mcp-installer.js +109 -0
  17. package/neural-interface/lib/plugin-installer.js +264 -0
  18. package/neural-interface/package.json +1 -1
  19. package/neural-interface/public/shared/api.js +66 -17
  20. package/neural-interface/public/shared/cdx/cdx-icons.js +5 -4
  21. package/neural-interface/public/shared/cdx/cdx-panel.js +117 -14
  22. package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
  23. package/neural-interface/public/shared/cdx/cdx-styles.js +55 -10
  24. package/neural-interface/public/shared/cdx/cdx-tabs.js +394 -72
  25. package/neural-interface/public/shared/cli-status.js +157 -0
  26. package/neural-interface/public/shared/constants.js +3 -2
  27. package/neural-interface/public/shared/html-shell.js +32 -11
  28. package/neural-interface/public/shared/ocp/ocp-panel.js +127 -28
  29. package/neural-interface/public/shared/ocp/ocp-render.js +52 -44
  30. package/neural-interface/public/shared/ocp/ocp-styles.js +33 -0
  31. package/neural-interface/public/shared/ocp/ocp-tabs.js +693 -82
  32. package/neural-interface/public/shared/styles.css +635 -0
  33. package/neural-interface/public/shared/ui-automation-studio.js +38 -12
  34. package/neural-interface/public/shared/ui-claude-panel.js +322 -15
  35. package/neural-interface/public/shared/ui-file-explorer.js +38 -18
  36. package/neural-interface/public/shared/ui-menubar.js +34 -10
  37. package/neural-interface/public/shared/ui-navbar.js +8 -6
  38. package/neural-interface/public/shared/ui-schedules-studio.js +1464 -0
  39. package/neural-interface/public/shared/ui-settings.js +274 -50
  40. package/neural-interface/public/shared/ui-sidepanel-layout.js +18 -7
  41. package/neural-interface/public/shared/ui-sidepanel-tray.js +287 -0
  42. package/neural-interface/public/shared/ui-sync.js +9 -5
  43. package/neural-interface/public/shared/ui-terminal.js +76 -27
  44. package/neural-interface/public/shared/ui-tool-updates.js +49 -15
  45. package/neural-interface/public/shared/ui-tutorial-steps.js +4 -2
  46. package/neural-interface/public/shared/ui-tutorial.js +24 -18
  47. package/neural-interface/public/shared/ui-whiteboard.js +79 -31
  48. package/neural-interface/public/shared/vterm-buffer.js +9 -8
  49. package/neural-interface/public/variant/2d/main.js +9 -7
  50. package/neural-interface/public/variant/3d/main.js +10 -8
  51. package/neural-interface/server.js +1292 -255
  52. package/package.json +1 -1
  53. package/skills/leonardo/SKILL.md +5 -1
  54. package/skills/leonardo/modules/image-prompter.md +5 -1
  55. package/skills/leonardo/modules/video-prompter.md +5 -1
  56. package/skills/synabun/SKILL.md +44 -26
  57. package/skills/synabun/modules/audit.md +40 -36
  58. package/skills/synabun/modules/changelog.md +22 -18
  59. package/skills/synabun/modules/memorize.md +19 -15
  60. package/skills/synabun/modules/schedule.md +285 -0
  61. 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
- TUTORIAL_COMPLETED: 'neural-tutorial-completed',
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-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-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
- <!-- Whiteboard (Focus Mode canvas) -->
711
- <div id="wb-root">
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, abortAllTabs, anyTabRunning, handleSSEEvent,
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 (anyTabRunning()) {
513
+ if (activeTabRunning()) {
500
514
  e.preventDefault();
501
515
  e.stopPropagation();
502
- abortAllTabs();
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 && anyTabRunning()) {
560
+ if (!ownsEsc && activeTabRunning()) {
547
561
  e.preventDefault();
548
562
  e.stopPropagation();
549
- abortAllTabs();
563
+ abortMessage();
550
564
  }
551
565
  }
552
566
  });
553
567
  if (sendBtn) {
554
568
  sendBtn.addEventListener('click', () => {
555
- if (anyTabRunning()) {
556
- abortAllTabs();
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
- // Stop button shows if any tab in the panel is running, so the user can
995
- // abort the whole panel regardless of which tab is focused.
996
- const panelRunning = anyTabRunning();
997
- if (panelRunning) {
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 (!anyTabRunning()) return;
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
- abortAllTabs();
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 res = await fetch('/api/create-plan', {
1603
- method: 'POST',
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.editedPlanContent = content;
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());