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
@@ -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) {
@@ -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 ──
@@ -1137,6 +1138,7 @@ function extractAllQuestions(input) {
1137
1138
  : (input.question || input.text || input.options) ? [input]
1138
1139
  : [input];
1139
1140
  return questions.map(q => ({
1141
+ id: q.id || q.key || q.name || '',
1140
1142
  question: q.question || q.text || q.message || '',
1141
1143
  header: q.header || '',
1142
1144
  options: normalizeOptions(q.options || q.choices),
@@ -1192,7 +1194,7 @@ export function renderQuestionCard(container, toolName, toolInput, toolId, { onA
1192
1194
  // Render each question block
1193
1195
  for (let qi = 0; qi < questions.length; qi++) {
1194
1196
  const q = questions[qi];
1195
- const questionKey = q.question || q.header || `question_${qi}`;
1197
+ const questionKey = q.id || q.question || q.header || `question_${qi}`;
1196
1198
  const block = document.createElement('div');
1197
1199
  block.className = 'ocp-question-block';
1198
1200
 
@@ -1255,6 +1257,7 @@ export function renderQuestionCard(container, toolName, toolInput, toolId, { onA
1255
1257
  const isOther = opt.value === '__other__' || /^other$/i.test(opt.label);
1256
1258
  const btn = document.createElement('button');
1257
1259
  btn.className = `ocp-question-option${q.multiSelect ? ' multi' : ''}`;
1260
+ btn.dataset.value = opt.value;
1258
1261
  btn.innerHTML = `
1259
1262
  <span class="ocp-question-option-radio"></span>
1260
1263
  <span>
@@ -1268,9 +1271,9 @@ export function renderQuestionCard(container, toolName, toolInput, toolId, { onA
1268
1271
  const selected = [];
1269
1272
  optsEl.querySelectorAll('.ocp-question-option.selected').forEach(b => {
1270
1273
  const lbl = b.querySelector('.ocp-question-option-label');
1271
- if (lbl && !/^other$/i.test(lbl.textContent)) selected.push(lbl.textContent);
1274
+ if (lbl && !/^other$/i.test(lbl.textContent)) selected.push(b.dataset.value || lbl.textContent);
1272
1275
  });
1273
- if (selected.length > 0) pendingAnswers[questionKey] = selected.join(', ');
1276
+ if (selected.length > 0) pendingAnswers[questionKey] = selected;
1274
1277
  else delete pendingAnswers[questionKey];
1275
1278
  } else {
1276
1279
  optsEl.querySelectorAll('.ocp-question-option').forEach(b => b.classList.remove('selected'));
@@ -1283,7 +1286,7 @@ export function renderQuestionCard(container, toolName, toolInput, toolId, { onA
1283
1286
  queueMicrotask(() => otherInput.focus());
1284
1287
  } else {
1285
1288
  otherWrap.hidden = true;
1286
- pendingAnswers[questionKey] = opt.label;
1289
+ pendingAnswers[questionKey] = opt.value;
1287
1290
  }
1288
1291
  }
1289
1292
  updateSubmitState();
@@ -1339,7 +1342,7 @@ export function lockQuestionCard(container, toolId) {
1339
1342
 
1340
1343
  // ── Post-plan action card ──
1341
1344
 
1342
- export function renderPostPlanCard(container, { onContinue, onCompact, onEditPlan } = {}) {
1345
+ export function renderPostPlanCard(container, { headerText = 'PLAN COMPLETE', onContinue, onCompact, onEditPlan, onContinuePlanning } = {}) {
1343
1346
  const card = document.createElement('div');
1344
1347
  card.className = 'ocp-post-plan-card';
1345
1348
  card.innerHTML = `
@@ -1347,11 +1350,12 @@ export function renderPostPlanCard(container, { onContinue, onCompact, onEditPla
1347
1350
  <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
1351
  <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
1349
1352
  </svg>
1350
- PLAN COMPLETE
1353
+ ${esc(headerText || 'PLAN COMPLETE')}
1351
1354
  </div>
1352
1355
  <div class="ocp-post-plan-note">Review the plan above, then choose how to proceed.</div>
1353
1356
  <div class="ocp-post-plan-actions">
1354
1357
  <button class="ocp-post-plan-btn primary" data-action="continue">Continue with implementation</button>
1358
+ <button class="ocp-post-plan-btn" data-action="continue-planning">Continue planning</button>
1355
1359
  <button class="ocp-post-plan-btn" data-action="compact">Compact context</button>
1356
1360
  <button class="ocp-post-plan-btn" data-action="edit">Edit plan</button>
1357
1361
  </div>
@@ -1367,6 +1371,10 @@ export function renderPostPlanCard(container, { onContinue, onCompact, onEditPla
1367
1371
  card.querySelector('[data-action="edit"]').addEventListener('click', () => {
1368
1372
  if (typeof onEditPlan === 'function') onEditPlan();
1369
1373
  });
1374
+ card.querySelector('[data-action="continue-planning"]').addEventListener('click', () => {
1375
+ card.remove();
1376
+ if (typeof onContinuePlanning === 'function') onContinuePlanning();
1377
+ });
1370
1378
 
1371
1379
  container.appendChild(card);
1372
1380
  scrollToBottom(container);
@@ -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 {