synabun 2026.4.21 → 2026.4.25-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +161 -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 +44 -14
  22. package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
  23. package/neural-interface/public/shared/cdx/cdx-styles.js +23 -10
  24. package/neural-interface/public/shared/cdx/cdx-tabs.js +384 -70
  25. package/neural-interface/public/shared/constants.js +3 -2
  26. package/neural-interface/public/shared/html-shell.js +32 -11
  27. package/neural-interface/public/shared/ocp/ocp-panel.js +45 -28
  28. package/neural-interface/public/shared/ocp/ocp-render.js +50 -43
  29. package/neural-interface/public/shared/ocp/ocp-tabs.js +693 -82
  30. package/neural-interface/public/shared/styles.css +635 -0
  31. package/neural-interface/public/shared/ui-automation-studio.js +38 -12
  32. package/neural-interface/public/shared/ui-claude-panel.js +216 -21
  33. package/neural-interface/public/shared/ui-file-explorer.js +38 -18
  34. package/neural-interface/public/shared/ui-menubar.js +34 -10
  35. package/neural-interface/public/shared/ui-navbar.js +8 -6
  36. package/neural-interface/public/shared/ui-schedules-studio.js +1464 -0
  37. package/neural-interface/public/shared/ui-settings.js +274 -50
  38. package/neural-interface/public/shared/ui-sidepanel-layout.js +18 -7
  39. package/neural-interface/public/shared/ui-sidepanel-tray.js +287 -0
  40. package/neural-interface/public/shared/ui-sync.js +9 -5
  41. package/neural-interface/public/shared/ui-terminal.js +76 -27
  42. package/neural-interface/public/shared/ui-tool-updates.js +49 -15
  43. package/neural-interface/public/shared/ui-tutorial-steps.js +4 -2
  44. package/neural-interface/public/shared/ui-tutorial.js +24 -18
  45. package/neural-interface/public/shared/ui-whiteboard.js +79 -31
  46. package/neural-interface/public/shared/vterm-buffer.js +9 -8
  47. package/neural-interface/public/variant/2d/main.js +9 -7
  48. package/neural-interface/public/variant/3d/main.js +10 -8
  49. package/neural-interface/server.js +1281 -252
  50. package/package.json +1 -1
  51. package/skills/leonardo/SKILL.md +5 -1
  52. package/skills/leonardo/modules/image-prompter.md +5 -1
  53. package/skills/leonardo/modules/video-prompter.md +5 -1
  54. package/skills/synabun/SKILL.md +44 -26
  55. package/skills/synabun/modules/audit.md +40 -36
  56. package/skills/synabun/modules/changelog.md +22 -18
  57. package/skills/synabun/modules/memorize.md +19 -15
  58. package/skills/synabun/modules/schedule.md +285 -0
  59. package/CLAUDE.md +0 -217
@@ -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>
@@ -21,11 +21,18 @@ import {
21
21
  setPanelEl, setOnUpdate, setPanelVisible, setOnShow, setOnHide, setOnEditPlan,
22
22
  createTab, switchTab, closeTab, renderPills, saveTabs, restoreTabs,
23
23
  loadSessions, createSession, renameSession, loadProviders, loadAgents, populateModelDropdown,
24
- sendMessage, abortMessage, abortAllTabs, anyTabRunning, handleSSEEvent,
24
+ sendMessage, abortMessage, handleSSEEvent,
25
25
  revertSession, compactSession, shareSession, executeCommand, renderTabMessages,
26
26
  getActiveTabMode, setTabMode, resolveContextInputTokens, setActivityVisible,
27
- toggleActivityExpanded, abortAgent,
27
+ toggleActivityExpanded, abortAgent, ensurePlanFile, showPostPlanUI, applySavedPlan,
28
+ flushAllSessionSnapshots,
28
29
  } from './ocp-tabs.js';
30
+
31
+ window.addEventListener('beforeunload', () => { try { flushAllSessionSnapshots(); } catch {} });
32
+ window.addEventListener('pagehide', () => { try { flushAllSessionSnapshots(); } catch {} });
33
+ document.addEventListener('visibilitychange', () => {
34
+ if (document.hidden) { try { flushAllSessionSnapshots(); } catch {} }
35
+ });
29
36
  import { isClaudePanelOpen, toggleClaudePanel } from '../ui-claude-panel.js';
30
37
  import { isCodexPanelOpen, toggleCodexPanel } from '../ui-codex-panel.js';
31
38
 
@@ -60,6 +67,10 @@ let _projectsLoaded = false;
60
67
 
61
68
  function panelEl(sel) { return _panel?.querySelector(sel) || null; }
62
69
 
70
+ function activeTabRunning() {
71
+ return !!activeTab()?.running;
72
+ }
73
+
63
74
  function sessionTitleFor(sessionOrTab) {
64
75
  const sid = sessionOrTab?.id || sessionOrTab?.sessionID || sessionOrTab?.sessionId || '';
65
76
  const candidates = [sessionOrTab?.sessionTitle, sessionOrTab?.title, sessionOrTab?.description, sessionOrTab?.slug];
@@ -496,10 +507,10 @@ function wireEvents() {
496
507
  }
497
508
  if (e.key === 'Escape') {
498
509
  hideSlashHints();
499
- if (anyTabRunning()) {
510
+ if (activeTabRunning()) {
500
511
  e.preventDefault();
501
512
  e.stopPropagation();
502
- abortAllTabs();
513
+ abortMessage();
503
514
  }
504
515
  }
505
516
  // Navigate slash hints with arrow keys
@@ -543,17 +554,17 @@ function wireEvents() {
543
554
  || _panel.querySelector('#ocp-slash-hints:not([hidden])')
544
555
  || _panel.querySelector('.ocp-rename-input')
545
556
  || _panel.querySelector('#ocp-session-menu.open');
546
- if (!ownsEsc && anyTabRunning()) {
557
+ if (!ownsEsc && activeTabRunning()) {
547
558
  e.preventDefault();
548
559
  e.stopPropagation();
549
- abortAllTabs();
560
+ abortMessage();
550
561
  }
551
562
  }
552
563
  });
553
564
  if (sendBtn) {
554
565
  sendBtn.addEventListener('click', () => {
555
- if (anyTabRunning()) {
556
- abortAllTabs();
566
+ if (activeTabRunning()) {
567
+ abortMessage();
557
568
  } else {
558
569
  handleSend();
559
570
  }
@@ -899,6 +910,10 @@ async function handleSend() {
899
910
  let text = input.value.trim();
900
911
  const images = Array.isArray(tab.attachedImages) ? [...tab.attachedImages] : [];
901
912
  if (!text && !images.length) return;
913
+ if (tab.showPostPlanActions) {
914
+ setStatus('working', 'Choose Continue, Compact, or Edit plan first.');
915
+ return;
916
+ }
902
917
 
903
918
  // Parse and execute slash commands. If executeSlashCommand returns false
904
919
  // (e.g. a SynaBun skill that the server will expand), keep `text` in flight
@@ -991,14 +1006,16 @@ function syncSendButton() {
991
1006
  const tab = activeTab();
992
1007
  if (!sendBtn) return;
993
1008
 
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) {
1009
+ if (tab?.running) {
998
1010
  sendBtn.disabled = false;
999
1011
  sendBtn.classList.add('running');
1000
1012
  sendBtn.setAttribute('data-tooltip', 'Stop turn (Esc)');
1001
1013
  sendBtn.setAttribute('aria-label', 'Stop turn');
1014
+ } else if (tab?.showPostPlanActions) {
1015
+ sendBtn.classList.remove('running');
1016
+ sendBtn.disabled = true;
1017
+ sendBtn.setAttribute('data-tooltip', 'Choose a plan action first');
1018
+ sendBtn.setAttribute('aria-label', 'Choose a plan action first');
1002
1019
  } else {
1003
1020
  sendBtn.classList.remove('running');
1004
1021
  sendBtn.disabled = (!input?.value.trim() && !(tab?.attachedImages?.length)) || !_serverReady;
@@ -1478,12 +1495,12 @@ function syncReservedWidth() {
1478
1495
  function _ocpDocEscHandler(event) {
1479
1496
  if (event.key !== 'Escape') return;
1480
1497
  if (!_visible) return;
1481
- if (!anyTabRunning()) return;
1498
+ if (!activeTabRunning()) return;
1482
1499
  const hintsEl = panelEl('#ocp-slash-hints');
1483
1500
  if (hintsEl && hintsEl.classList.contains('open')) return; // let input handler dismiss hints first
1484
1501
  event.preventDefault();
1485
1502
  event.stopImmediatePropagation();
1486
- abortAllTabs();
1503
+ abortMessage();
1487
1504
  }
1488
1505
 
1489
1506
  function setVisible(nextVisible) {
@@ -1599,32 +1616,31 @@ function ensurePanel() {
1599
1616
  if (tab.planFilePath) { openPlan(tab.planFilePath); return; }
1600
1617
  if (!tab.planContent) return;
1601
1618
  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
- }
1619
+ const planPath = await ensurePlanFile(tab);
1620
+ if (planPath) openPlan(planPath);
1613
1621
  } catch (e) {
1614
1622
  console.error('[ocp-panel] Edit plan failed:', e);
1623
+ showPostPlanUI(tab);
1615
1624
  }
1616
1625
  });
1617
1626
 
1618
1627
  // Listen for edited plan content from file explorer
1619
- on('plan-saved', ({ content, source, tabId }) => {
1628
+ on('plan-saved', ({ filePath, content, source, tabId }) => {
1620
1629
  if (source !== 'opencode') return;
1621
1630
  const tab = getTabs().find(t => t.id === tabId) || activeTab();
1622
1631
  if (tab && content) {
1623
- tab.editedPlanContent = content;
1624
- saveTabs();
1632
+ applySavedPlan(tab, content, filePath);
1625
1633
  }
1626
1634
  });
1627
1635
 
1636
+ on('plan-edit-cancelled', ({ source, tabId } = {}) => {
1637
+ if (source !== 'opencode') return;
1638
+ const tab = getTabs().find(t => t.id === tabId) || activeTab();
1639
+ if (!tab) return;
1640
+ tab.showPostPlanActions = true;
1641
+ showPostPlanUI(tab, tab.postPlanHeader || 'PLAN COMPLETE');
1642
+ });
1643
+
1628
1644
  wireEvents();
1629
1645
  autosizeInput();
1630
1646
 
@@ -1632,6 +1648,7 @@ function ensurePanel() {
1632
1648
  if (!restoreTabs()) {
1633
1649
  createTab({ project: storage.getItem(STOR.project) || '' });
1634
1650
  }
1651
+ window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'opencode' } }));
1635
1652
 
1636
1653
  syncSendButton();
1637
1654
 
@@ -229,27 +229,6 @@ function linkifyFilePaths(container) {
229
229
  attachFileLinkHandlers(container);
230
230
  }
231
231
 
232
- // ── Throttle helper for streaming ──
233
- function throttle(fn, ms) {
234
- let last = 0;
235
- let timer = null;
236
- return function (...args) {
237
- const now = Date.now();
238
- const remaining = ms - (now - last);
239
- if (remaining <= 0) {
240
- if (timer) { clearTimeout(timer); timer = null; }
241
- last = now;
242
- fn.apply(this, args);
243
- } else if (!timer) {
244
- timer = setTimeout(() => {
245
- last = Date.now();
246
- timer = null;
247
- fn.apply(this, args);
248
- }, remaining);
249
- }
250
- };
251
- }
252
-
253
232
  function thinkBlockHtml(content, partial) {
254
233
  const label = partial ? 'Thinking…' : 'Thought';
255
234
  return `<details class="ocp-think-block"${partial ? ' open' : ''}><summary><span class="ocp-think-icon">${THINK_ICON_SVG}</span><span class="ocp-think-label">${label}</span><span class="ocp-think-chevron">&#x203A;</span></summary><div class="ocp-think-content">${esc(content)}</div></details>`;
@@ -842,19 +821,46 @@ export function getOrCreateStreamingEl(container) {
842
821
  return el;
843
822
  }
844
823
 
824
+ function renderStreamingElement(container, el, { partialThinking = true } = {}) {
825
+ if (!el) return;
826
+ let html = '';
827
+ let hasContent = false;
828
+ if (el.dataset.thinkText) {
829
+ html += thinkBlockHtml(el.dataset.thinkText, partialThinking);
830
+ hasContent = true;
831
+ }
832
+ if (el.dataset.rawText) {
833
+ const rendered = renderAssistantMarkdown(el.dataset.rawText);
834
+ if (rendered.hasContent) {
835
+ html += rendered.html;
836
+ hasContent = true;
837
+ }
838
+ }
839
+ el.innerHTML = html;
840
+ clearThinkingTimer(el);
841
+ el.classList.toggle('ocp-msg-empty', !hasContent);
842
+ postProcessRenderedHtml(el);
843
+ scrollToBottom(container);
844
+ }
845
+
846
+ function scheduleStreamingRender(container, el) {
847
+ if (el._ocpStreamRaf) return;
848
+ const schedule = typeof requestAnimationFrame === 'function'
849
+ ? requestAnimationFrame
850
+ : (fn) => setTimeout(fn, 16);
851
+ el._ocpStreamRaf = schedule(() => {
852
+ el._ocpStreamRaf = null;
853
+ renderStreamingElement(container, el, { partialThinking: true });
854
+ });
855
+ }
856
+
845
857
  /** Append a text chunk to the streaming element. */
846
858
  export function appendStreamChunk(container, text) {
847
859
  const chunk = normalizeTextContent(text, { stringifyObjects: false });
848
860
  if (!chunk) return getOrCreateStreamingEl(container);
849
861
  const el = getOrCreateStreamingEl(container);
850
862
  el.dataset.rawText = (el.dataset.rawText || '') + chunk;
851
- const doRender = throttle(() => {
852
- if (el.dataset.rawText != null) {
853
- setAssistantHtml(el, el.dataset.rawText);
854
- scrollToBottom(container);
855
- }
856
- }, 32);
857
- doRender();
863
+ scheduleStreamingRender(container, el);
858
864
  return el;
859
865
  }
860
866
 
@@ -864,12 +870,7 @@ export function appendThinkChunk(container, text) {
864
870
  if (!chunk) return getOrCreateStreamingEl(container);
865
871
  const el = getOrCreateStreamingEl(container);
866
872
  el.dataset.thinkText = (el.dataset.thinkText || '') + chunk;
867
- // Re-render: think block first, then any accumulated response text
868
- const thinkHtml = thinkBlockHtml(el.dataset.thinkText, true);
869
- const responseHtml = el.dataset.rawText ? renderAssistantMarkdown(el.dataset.rawText).html : '';
870
- el.innerHTML = thinkHtml + responseHtml;
871
- clearThinkingTimer(el);
872
- scrollToBottom(container);
873
+ scheduleStreamingRender(container, el);
873
874
  return el;
874
875
  }
875
876
 
@@ -878,10 +879,9 @@ export function finalizeStreamingMessage(container) {
878
879
  const el = container.querySelector('.ocp-msg-assistant.streaming');
879
880
  if (el) {
880
881
  el.classList.remove('streaming');
881
- if (el.dataset.rawText != null) {
882
- setAssistantHtml(el, el.dataset.rawText);
883
- }
882
+ renderStreamingElement(container, el, { partialThinking: false });
884
883
  delete el.dataset.rawText;
884
+ delete el.dataset.thinkText;
885
885
  }
886
886
  const pending = container.querySelector('.ocp-msg-assistant.pending');
887
887
  if (pending) {
@@ -1137,6 +1137,7 @@ function extractAllQuestions(input) {
1137
1137
  : (input.question || input.text || input.options) ? [input]
1138
1138
  : [input];
1139
1139
  return questions.map(q => ({
1140
+ id: q.id || q.key || q.name || '',
1140
1141
  question: q.question || q.text || q.message || '',
1141
1142
  header: q.header || '',
1142
1143
  options: normalizeOptions(q.options || q.choices),
@@ -1192,7 +1193,7 @@ export function renderQuestionCard(container, toolName, toolInput, toolId, { onA
1192
1193
  // Render each question block
1193
1194
  for (let qi = 0; qi < questions.length; qi++) {
1194
1195
  const q = questions[qi];
1195
- const questionKey = q.question || q.header || `question_${qi}`;
1196
+ const questionKey = q.id || q.question || q.header || `question_${qi}`;
1196
1197
  const block = document.createElement('div');
1197
1198
  block.className = 'ocp-question-block';
1198
1199
 
@@ -1255,6 +1256,7 @@ export function renderQuestionCard(container, toolName, toolInput, toolId, { onA
1255
1256
  const isOther = opt.value === '__other__' || /^other$/i.test(opt.label);
1256
1257
  const btn = document.createElement('button');
1257
1258
  btn.className = `ocp-question-option${q.multiSelect ? ' multi' : ''}`;
1259
+ btn.dataset.value = opt.value;
1258
1260
  btn.innerHTML = `
1259
1261
  <span class="ocp-question-option-radio"></span>
1260
1262
  <span>
@@ -1268,9 +1270,9 @@ export function renderQuestionCard(container, toolName, toolInput, toolId, { onA
1268
1270
  const selected = [];
1269
1271
  optsEl.querySelectorAll('.ocp-question-option.selected').forEach(b => {
1270
1272
  const lbl = b.querySelector('.ocp-question-option-label');
1271
- if (lbl && !/^other$/i.test(lbl.textContent)) selected.push(lbl.textContent);
1273
+ if (lbl && !/^other$/i.test(lbl.textContent)) selected.push(b.dataset.value || lbl.textContent);
1272
1274
  });
1273
- if (selected.length > 0) pendingAnswers[questionKey] = selected.join(', ');
1275
+ if (selected.length > 0) pendingAnswers[questionKey] = selected;
1274
1276
  else delete pendingAnswers[questionKey];
1275
1277
  } else {
1276
1278
  optsEl.querySelectorAll('.ocp-question-option').forEach(b => b.classList.remove('selected'));
@@ -1283,7 +1285,7 @@ export function renderQuestionCard(container, toolName, toolInput, toolId, { onA
1283
1285
  queueMicrotask(() => otherInput.focus());
1284
1286
  } else {
1285
1287
  otherWrap.hidden = true;
1286
- pendingAnswers[questionKey] = opt.label;
1288
+ pendingAnswers[questionKey] = opt.value;
1287
1289
  }
1288
1290
  }
1289
1291
  updateSubmitState();
@@ -1339,7 +1341,7 @@ export function lockQuestionCard(container, toolId) {
1339
1341
 
1340
1342
  // ── Post-plan action card ──
1341
1343
 
1342
- export function renderPostPlanCard(container, { onContinue, onCompact, onEditPlan } = {}) {
1344
+ export function renderPostPlanCard(container, { headerText = 'PLAN COMPLETE', onContinue, onCompact, onEditPlan, onContinuePlanning } = {}) {
1343
1345
  const card = document.createElement('div');
1344
1346
  card.className = 'ocp-post-plan-card';
1345
1347
  card.innerHTML = `
@@ -1347,11 +1349,12 @@ export function renderPostPlanCard(container, { onContinue, onCompact, onEditPla
1347
1349
  <svg class="ocp-post-plan-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1348
1350
  <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
1349
1351
  </svg>
1350
- PLAN COMPLETE
1352
+ ${esc(headerText || 'PLAN COMPLETE')}
1351
1353
  </div>
1352
1354
  <div class="ocp-post-plan-note">Review the plan above, then choose how to proceed.</div>
1353
1355
  <div class="ocp-post-plan-actions">
1354
1356
  <button class="ocp-post-plan-btn primary" data-action="continue">Continue with implementation</button>
1357
+ <button class="ocp-post-plan-btn" data-action="continue-planning">Continue planning</button>
1355
1358
  <button class="ocp-post-plan-btn" data-action="compact">Compact context</button>
1356
1359
  <button class="ocp-post-plan-btn" data-action="edit">Edit plan</button>
1357
1360
  </div>
@@ -1367,6 +1370,10 @@ export function renderPostPlanCard(container, { onContinue, onCompact, onEditPla
1367
1370
  card.querySelector('[data-action="edit"]').addEventListener('click', () => {
1368
1371
  if (typeof onEditPlan === 'function') onEditPlan();
1369
1372
  });
1373
+ card.querySelector('[data-action="continue-planning"]').addEventListener('click', () => {
1374
+ card.remove();
1375
+ if (typeof onContinuePlanning === 'function') onContinuePlanning();
1376
+ });
1370
1377
 
1371
1378
  container.appendChild(card);
1372
1379
  scrollToBottom(container);