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.
- package/CHANGELOG.md +161 -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 +44 -14
- package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
- package/neural-interface/public/shared/cdx/cdx-styles.js +23 -10
- package/neural-interface/public/shared/cdx/cdx-tabs.js +384 -70
- 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 +45 -28
- package/neural-interface/public/shared/ocp/ocp-render.js +50 -43
- 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 +216 -21
- 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 +1281 -252
- 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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
reorderLoopFolders,
|
|
22
22
|
fetchActiveLoop,
|
|
23
23
|
launchLoop,
|
|
24
|
+
sendClientLoopLog,
|
|
24
25
|
stopLoop,
|
|
25
26
|
launchAgent,
|
|
26
27
|
fetchAgents,
|
|
@@ -181,7 +182,8 @@ const CLI_MODELS = {
|
|
|
181
182
|
{ id: 'claude-haiku-4-5', label: 'Haiku 4.5', desc: 'Fastest' },
|
|
182
183
|
],
|
|
183
184
|
'codex': [
|
|
184
|
-
{ id: 'gpt-5.
|
|
185
|
+
{ id: 'gpt-5.5', label: 'GPT-5.5', desc: 'Most capable', tier: 'top' },
|
|
186
|
+
{ id: 'gpt-5.4', label: 'GPT-5.4', desc: 'Strong general' },
|
|
185
187
|
{ id: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', desc: 'Fast & cheap', tier: 'default' },
|
|
186
188
|
{ id: 'gpt-5.3-codex', label: 'GPT-5.3 Codex', desc: 'Codex-tuned' },
|
|
187
189
|
],
|
|
@@ -1469,15 +1471,30 @@ async function confirmLaunch() {
|
|
|
1469
1471
|
// Launch as floating terminal + floating browser
|
|
1470
1472
|
showToast('Launching loop...');
|
|
1471
1473
|
emit('terminal:expect-managed');
|
|
1474
|
+
sendClientLoopLog(null, 'launch:client', 'confirmLaunch about to POST /api/loop/launch', {
|
|
1475
|
+
profile: params.profile,
|
|
1476
|
+
usesBrowser: !!params.usesBrowser,
|
|
1477
|
+
iterations: params.iterations,
|
|
1478
|
+
maxMinutes: params.maxMinutes,
|
|
1479
|
+
taskLen: (params.task || '').length,
|
|
1480
|
+
cwd: params.cwd || null,
|
|
1481
|
+
browserSessionId: params.browserSessionId || null,
|
|
1482
|
+
});
|
|
1472
1483
|
console.log('[AS] confirmLaunch: calling launchLoop, profile =', params.profile, ', usesBrowser =', params.usesBrowser);
|
|
1473
1484
|
const result = await launchLoop(params);
|
|
1474
1485
|
console.log('[AS] confirmLaunch: launchLoop result =', JSON.stringify(result));
|
|
1486
|
+
sendClientLoopLog(result?.terminalSessionId || null, 'launch:client', 'launchLoop response received', { result });
|
|
1475
1487
|
if (!result?.ok) {
|
|
1476
1488
|
emit('terminal:attach-floating', {});
|
|
1477
1489
|
showToast(result?.error || 'Failed to launch loop');
|
|
1478
1490
|
return;
|
|
1479
1491
|
}
|
|
1480
1492
|
console.log('[AS] confirmLaunch: emitting terminal:attach-floating, terminalSessionId =', result.terminalSessionId);
|
|
1493
|
+
sendClientLoopLog(result.terminalSessionId, 'launch:client', 'emit terminal:attach-floating', {
|
|
1494
|
+
terminalSessionId: result.terminalSessionId,
|
|
1495
|
+
profile: params.profile || 'claude-code',
|
|
1496
|
+
initialMessage: '[SynaBun Loop] Begin task.',
|
|
1497
|
+
});
|
|
1481
1498
|
emit('terminal:attach-floating', {
|
|
1482
1499
|
terminalSessionId: result.terminalSessionId,
|
|
1483
1500
|
profile: params.profile || 'claude-code',
|
|
@@ -1491,7 +1508,9 @@ async function confirmLaunch() {
|
|
|
1491
1508
|
// Clear the managed-terminal flag since we won't emit terminal:attach-floating
|
|
1492
1509
|
emit('terminal:attach-floating', {});
|
|
1493
1510
|
console.error('[AS] confirmLaunch: caught error:', err);
|
|
1511
|
+
sendClientLoopLog(null, 'launch:client', 'confirmLaunch caught error', { err: err?.message || String(err) });
|
|
1494
1512
|
showToast('Launch failed: ' + (err.message || 'unknown error'));
|
|
1513
|
+
try { alert(`[SynaBun] Loop launch failed: ${err?.message || err || 'unknown error'}\n\nOpen DevTools Console for stack trace.`); } catch {}
|
|
1495
1514
|
}
|
|
1496
1515
|
}
|
|
1497
1516
|
|
|
@@ -1541,20 +1560,25 @@ function buildTemplateCommand(template) {
|
|
|
1541
1560
|
// ═══════════════════════════════════════════
|
|
1542
1561
|
|
|
1543
1562
|
export function initAutomationStudio() {
|
|
1544
|
-
on('automations:open', () => openPanel());
|
|
1563
|
+
on('automations:open', (opts) => openPanel(opts || {}));
|
|
1545
1564
|
on('automations:open-wizard', () => { openPanel(); });
|
|
1546
|
-
on('automations:open-schedules', () => { openPanel().then(() => { _view = 'schedules'; _selected = null; loadScheduleData().then(() => renderView()); }); });
|
|
1547
1565
|
on('automations:import', () => { openPanel().then(() => triggerImport()); });
|
|
1548
1566
|
setupAgentWebSocket();
|
|
1549
|
-
setupScheduleWebSocket();
|
|
1550
1567
|
}
|
|
1551
1568
|
|
|
1552
|
-
async function openPanel() {
|
|
1569
|
+
async function openPanel(opts = {}) {
|
|
1553
1570
|
if (isGuest() && !hasPermission('automations')) {
|
|
1554
1571
|
showGuestToast('Automation Studio is disabled by the host');
|
|
1555
1572
|
return;
|
|
1556
1573
|
}
|
|
1557
|
-
if (_panel) {
|
|
1574
|
+
if (_panel) {
|
|
1575
|
+
if (opts.templateId) {
|
|
1576
|
+
const tpl = _templates.find(t => t.id === opts.templateId);
|
|
1577
|
+
if (tpl) switchToDetail(tpl);
|
|
1578
|
+
}
|
|
1579
|
+
_panel.focus();
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1558
1582
|
|
|
1559
1583
|
// Backdrop
|
|
1560
1584
|
_backdrop = document.createElement('div');
|
|
@@ -1574,7 +1598,12 @@ async function openPanel() {
|
|
|
1574
1598
|
|
|
1575
1599
|
wirePanel();
|
|
1576
1600
|
await loadData();
|
|
1577
|
-
|
|
1601
|
+
if (opts.templateId) {
|
|
1602
|
+
const tpl = _templates.find(t => t.id === opts.templateId);
|
|
1603
|
+
if (tpl) switchToDetail(tpl); else renderView();
|
|
1604
|
+
} else {
|
|
1605
|
+
renderView();
|
|
1606
|
+
}
|
|
1578
1607
|
requestAnimationFrame(() => { _backdrop.classList.add('open'); _panel.classList.add('open'); });
|
|
1579
1608
|
startPolling();
|
|
1580
1609
|
}
|
|
@@ -1659,7 +1688,7 @@ function wirePanel() {
|
|
|
1659
1688
|
$('as-close')?.addEventListener('click', closePanel);
|
|
1660
1689
|
$('as-new-btn')?.addEventListener('click', () => { _view = 'picker'; _selected = null; renderView(); });
|
|
1661
1690
|
$('as-new-folder-btn')?.addEventListener('click', (e) => openFolderPopover(e.currentTarget, null));
|
|
1662
|
-
$('as-schedules-btn')?.addEventListener('click', () =>
|
|
1691
|
+
$('as-schedules-btn')?.addEventListener('click', () => emit('automations:open-schedules'));
|
|
1663
1692
|
$('as-import-btn')?.addEventListener('click', () => triggerImport());
|
|
1664
1693
|
|
|
1665
1694
|
$('as-sidebar-toggle')?.addEventListener('click', () => {
|
|
@@ -1721,8 +1750,6 @@ function wirePanel() {
|
|
|
1721
1750
|
const onEsc = (e) => {
|
|
1722
1751
|
if (e.key === 'Escape' && _panel) {
|
|
1723
1752
|
if (_view === 'launch') { closeLaunchInline(); return; }
|
|
1724
|
-
if (_view === 'schedule-editor') { _view = 'schedules'; _editingSchedule = null; renderView(); return; }
|
|
1725
|
-
if (_view === 'schedules') { _view = 'welcome'; renderView(); return; }
|
|
1726
1753
|
if (_view === 'wizard') { _view = _selected ? 'detail' : 'welcome'; renderView(); return; }
|
|
1727
1754
|
if (_view === 'detail' || _view === 'running' || _view === 'picker') {
|
|
1728
1755
|
if (_detailDirty && !confirm('Discard unsaved changes?')) return;
|
|
@@ -2233,8 +2260,6 @@ function renderView() {
|
|
|
2233
2260
|
case 'detail': renderDetailMain(); break;
|
|
2234
2261
|
case 'wizard': renderWizardMain(); break;
|
|
2235
2262
|
case 'running': renderRunningMain(); break;
|
|
2236
|
-
case 'schedules': renderSchedulesMain(); break;
|
|
2237
|
-
case 'schedule-editor': renderScheduleEditorMain(); break;
|
|
2238
2263
|
case 'launch': break; // launch panel is rendered by showLaunchInline
|
|
2239
2264
|
}
|
|
2240
2265
|
}
|
|
@@ -3413,6 +3438,7 @@ function renderRunningMain() {
|
|
|
3413
3438
|
</div>
|
|
3414
3439
|
<div class="as-running-controls">
|
|
3415
3440
|
<button class="as-btn-stop" data-action="force-stop" data-id="${loop.terminalSessionId || ''}">Stop</button>
|
|
3441
|
+
${loop.terminalSessionId ? `<a class="as-btn-stop" style="margin-left:8px;text-decoration:none;display:inline-flex;align-items:center;" href="/api/loop/log/${encodeURIComponent(loop.terminalSessionId)}" target="_blank" rel="noopener">View Loop Log</a>` : ''}
|
|
3416
3442
|
</div>
|
|
3417
3443
|
</div>`;
|
|
3418
3444
|
}
|
|
@@ -150,8 +150,24 @@ const STOR = {
|
|
|
150
150
|
windowRegistry: 'synabun-claude-panel-windows', // JSON map of windowId → lastSeen timestamp
|
|
151
151
|
bootId: 'synabun-claude-panel-boot-id', // server boot ID — detect restarts
|
|
152
152
|
defaultModel: 'synabun-claude-panel-default-model', // user's preferred default model
|
|
153
|
+
sessionSnapshots: 'synabun-claude-panel-session-snapshots', // global, per-sessionId rendered HTML cache
|
|
153
154
|
};
|
|
154
155
|
|
|
156
|
+
// ── Session HTML snapshot cache ──
|
|
157
|
+
// Global per-sessionId cache of rendered transcript HTML so resume on a fresh
|
|
158
|
+
// browser load mirrors the exact render that was produced during the live run.
|
|
159
|
+
// Falls back to upstream JSONL rebuild when no snapshot exists or it is stale.
|
|
160
|
+
const MAX_SESSION_SNAPSHOTS = 24;
|
|
161
|
+
const MAX_SESSION_SNAPSHOT_CHARS = 2_500_000;
|
|
162
|
+
let _sessionSnapshots = (() => {
|
|
163
|
+
try {
|
|
164
|
+
const raw = storage.getItem(STOR.sessionSnapshots);
|
|
165
|
+
if (!raw) return {};
|
|
166
|
+
const parsed = JSON.parse(raw);
|
|
167
|
+
return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
|
|
168
|
+
} catch { return {}; }
|
|
169
|
+
})();
|
|
170
|
+
|
|
155
171
|
const EFFORT_LEVELS = ['off', 'low', 'medium', 'high', 'max'];
|
|
156
172
|
const EFFORT_LABELS = { off: 'Think', low: 'lo', medium: 'med', high: 'hi', max: 'max' };
|
|
157
173
|
const EFFORT_TITLES = {
|
|
@@ -1076,7 +1092,7 @@ function injectStyles() {
|
|
|
1076
1092
|
padding: 1px;
|
|
1077
1093
|
background: rgba(255,255,255,0.05);
|
|
1078
1094
|
transition: background 0.4s;
|
|
1079
|
-
min-width: 0;
|
|
1095
|
+
min-width: 0;
|
|
1080
1096
|
}
|
|
1081
1097
|
.cp-input-wrap::before {
|
|
1082
1098
|
content: '';
|
|
@@ -1476,7 +1492,8 @@ function injectStyles() {
|
|
|
1476
1492
|
backdrop-filter: blur(20px);
|
|
1477
1493
|
border: 1px solid rgba(255,255,255,0.08);
|
|
1478
1494
|
border-radius: 10px; padding: 4px;
|
|
1479
|
-
z-index: 310; max-height:
|
|
1495
|
+
z-index: 310; max-height: 320px; overflow-y: auto;
|
|
1496
|
+
overscroll-behavior: contain;
|
|
1480
1497
|
box-shadow: var(--shadow-lg);
|
|
1481
1498
|
}
|
|
1482
1499
|
.cp-slash-hints.open { display: block; }
|
|
@@ -2632,14 +2649,14 @@ document.addEventListener('click', (e) => {
|
|
|
2632
2649
|
});
|
|
2633
2650
|
|
|
2634
2651
|
// ── Tab lifecycle ──
|
|
2635
|
-
function createTab(sessionId = null, label = 'New chat', { autoSwitch = true } = {}) {
|
|
2652
|
+
function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
|
|
2636
2653
|
if (_tabs.length >= MAX_TABS) return null;
|
|
2637
2654
|
// Capture current dropdown state for the new tab's project/model/effort
|
|
2638
2655
|
const _$proj = _panel?.querySelector('#cp-project');
|
|
2639
2656
|
const _$mod = _panel?.querySelector('#cp-model');
|
|
2640
2657
|
const _$think = _panel?.querySelector('#cp-think-toggle');
|
|
2641
2658
|
const tab = {
|
|
2642
|
-
id: crypto.randomUUID(),
|
|
2659
|
+
id: id || crypto.randomUUID(),
|
|
2643
2660
|
sessionId,
|
|
2644
2661
|
label,
|
|
2645
2662
|
project: ddGetValue(_$proj) || '', // per-tab project isolation
|
|
@@ -2729,6 +2746,7 @@ function switchTab(idx) {
|
|
|
2729
2746
|
if (idx < 0 || idx >= _tabs.length) return;
|
|
2730
2747
|
const prev = activeTab();
|
|
2731
2748
|
if (prev && prev !== _tabs[idx]) {
|
|
2749
|
+
flushSessionSnapshotSave(prev);
|
|
2732
2750
|
const $input = _panel?.querySelector('#cp-input');
|
|
2733
2751
|
if ($input) prev.draft = $input.value;
|
|
2734
2752
|
// Animate out
|
|
@@ -2818,6 +2836,7 @@ function switchTab(idx) {
|
|
|
2818
2836
|
function closeTab(idx) {
|
|
2819
2837
|
if (idx < 0 || idx >= _tabs.length) return;
|
|
2820
2838
|
const tab = _tabs[idx];
|
|
2839
|
+
flushSessionSnapshotSave(tab);
|
|
2821
2840
|
recordHookEvent(tab, 'SessionEnd', tab.sessionId || '(new)');
|
|
2822
2841
|
tab.closed = true;
|
|
2823
2842
|
clearTimeout(tab.reconnectTimer);
|
|
@@ -2956,7 +2975,7 @@ function restoreTabs() {
|
|
|
2956
2975
|
const data = JSON.parse(raw);
|
|
2957
2976
|
if (data.tabs?.length) {
|
|
2958
2977
|
for (const saved of data.tabs) {
|
|
2959
|
-
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false });
|
|
2978
|
+
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
2960
2979
|
if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
|
|
2961
2980
|
if (t && saved.running) t._wasRunning = true; // trigger reattach on WS open
|
|
2962
2981
|
if (t && saved.project) t.project = saved.project;
|
|
@@ -2979,7 +2998,7 @@ function restoreTabs() {
|
|
|
2979
2998
|
const data = JSON.parse(legacyRaw);
|
|
2980
2999
|
if (data.tabs?.length) {
|
|
2981
3000
|
for (const saved of data.tabs) {
|
|
2982
|
-
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false });
|
|
3001
|
+
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
2983
3002
|
if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
|
|
2984
3003
|
if (t && saved.running) t._wasRunning = true;
|
|
2985
3004
|
if (t && saved.project) t.project = saved.project;
|
|
@@ -3055,6 +3074,7 @@ async function _releaseAllSessionLocks() {
|
|
|
3055
3074
|
|
|
3056
3075
|
// Release locks when window closes
|
|
3057
3076
|
window.addEventListener('beforeunload', () => {
|
|
3077
|
+
flushAllSessionSnapshots();
|
|
3058
3078
|
_stopHeartbeat();
|
|
3059
3079
|
// Use sendBeacon for reliable delivery on close
|
|
3060
3080
|
navigator.sendBeacon(
|
|
@@ -3063,6 +3083,11 @@ window.addEventListener('beforeunload', () => {
|
|
|
3063
3083
|
);
|
|
3064
3084
|
});
|
|
3065
3085
|
|
|
3086
|
+
window.addEventListener('pagehide', flushAllSessionSnapshots);
|
|
3087
|
+
document.addEventListener('visibilitychange', () => {
|
|
3088
|
+
if (document.hidden) flushAllSessionSnapshots();
|
|
3089
|
+
});
|
|
3090
|
+
|
|
3066
3091
|
// Re-sync UI state when window regains focus (catches stale running states from background tabs)
|
|
3067
3092
|
window.addEventListener('focus', () => {
|
|
3068
3093
|
if (!_panel) return;
|
|
@@ -3713,9 +3738,151 @@ function promptNameNewSession() {
|
|
|
3713
3738
|
});
|
|
3714
3739
|
}
|
|
3715
3740
|
|
|
3741
|
+
// ── Session HTML snapshot helpers ──
|
|
3742
|
+
function _normalizeSnapshotEntry(entry) {
|
|
3743
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
3744
|
+
const html = typeof entry.html === 'string' ? entry.html : '';
|
|
3745
|
+
if (!html) return null;
|
|
3746
|
+
return {
|
|
3747
|
+
html,
|
|
3748
|
+
updatedAt: Number(entry.updatedAt) || 0,
|
|
3749
|
+
label: typeof entry.label === 'string' ? entry.label : '',
|
|
3750
|
+
usage: (entry.usage && typeof entry.usage === 'object') ? entry.usage : null,
|
|
3751
|
+
contextWindow: Number(entry.contextWindow) || 0,
|
|
3752
|
+
turns: Number(entry.turns) || 0,
|
|
3753
|
+
sessionCost: Number(entry.sessionCost) || 0,
|
|
3754
|
+
itemCount: Number(entry.itemCount) || 0,
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
function _persistSessionSnapshots() {
|
|
3759
|
+
try {
|
|
3760
|
+
const source = (_sessionSnapshots && typeof _sessionSnapshots === 'object' && !Array.isArray(_sessionSnapshots))
|
|
3761
|
+
? _sessionSnapshots : {};
|
|
3762
|
+
const entries = Object.entries(source)
|
|
3763
|
+
.map(([sid, e]) => [sid, _normalizeSnapshotEntry(e)])
|
|
3764
|
+
.filter(([, e]) => !!e)
|
|
3765
|
+
.sort(([, a], [, b]) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
3766
|
+
const next = {};
|
|
3767
|
+
let totalChars = 0;
|
|
3768
|
+
for (const [sid, entry] of entries) {
|
|
3769
|
+
const size = entry.html.length;
|
|
3770
|
+
if (Object.keys(next).length >= MAX_SESSION_SNAPSHOTS) break;
|
|
3771
|
+
if (totalChars + size > MAX_SESSION_SNAPSHOT_CHARS && Object.keys(next).length) continue;
|
|
3772
|
+
totalChars += size;
|
|
3773
|
+
next[sid] = entry;
|
|
3774
|
+
}
|
|
3775
|
+
_sessionSnapshots = next;
|
|
3776
|
+
storage.setItem(STOR.sessionSnapshots, JSON.stringify(next));
|
|
3777
|
+
} catch {}
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
function getSessionSnapshot(sid) {
|
|
3781
|
+
if (!sid) return null;
|
|
3782
|
+
return _normalizeSnapshotEntry(_sessionSnapshots?.[sid]);
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
function writeSessionSnapshot(tab) {
|
|
3786
|
+
const sid = tab?.sessionId || null;
|
|
3787
|
+
const $msgs = tab?.messagesEl || null;
|
|
3788
|
+
if (!sid || !$msgs) return;
|
|
3789
|
+
const html = $msgs.innerHTML || '';
|
|
3790
|
+
if (!html.trim()) return;
|
|
3791
|
+
// Skip snapshot if the transcript only contains the empty-state placeholder
|
|
3792
|
+
if ($msgs.children.length === 1 && $msgs.firstElementChild?.classList?.contains('cp-empty')) return;
|
|
3793
|
+
if (!_sessionSnapshots || typeof _sessionSnapshots !== 'object' || Array.isArray(_sessionSnapshots)) {
|
|
3794
|
+
_sessionSnapshots = {};
|
|
3795
|
+
}
|
|
3796
|
+
const itemCount = [...$msgs.children].filter(n =>
|
|
3797
|
+
!n.classList?.contains('cp-empty') && !n.classList?.contains('thinking')
|
|
3798
|
+
).length;
|
|
3799
|
+
_sessionSnapshots[sid] = {
|
|
3800
|
+
html,
|
|
3801
|
+
updatedAt: Date.now(),
|
|
3802
|
+
label: tab.label || '',
|
|
3803
|
+
usage: tab.usage ? { ...tab.usage } : null,
|
|
3804
|
+
contextWindow: tab.contextWindow || 0,
|
|
3805
|
+
turns: tab.turns || 0,
|
|
3806
|
+
sessionCost: tab.sessionCost || 0,
|
|
3807
|
+
itemCount,
|
|
3808
|
+
};
|
|
3809
|
+
_persistSessionSnapshots();
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
function scheduleSessionSnapshotSave(tab, ms = 350) {
|
|
3813
|
+
if (!tab?.sessionId || !tab?.messagesEl) return;
|
|
3814
|
+
if (tab._snapshotTimer) clearTimeout(tab._snapshotTimer);
|
|
3815
|
+
tab._snapshotTimer = setTimeout(() => {
|
|
3816
|
+
tab._snapshotTimer = null;
|
|
3817
|
+
writeSessionSnapshot(tab);
|
|
3818
|
+
}, ms);
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
function flushSessionSnapshotSave(tab) {
|
|
3822
|
+
if (!tab) return;
|
|
3823
|
+
if (tab._snapshotTimer) { clearTimeout(tab._snapshotTimer); tab._snapshotTimer = null; }
|
|
3824
|
+
writeSessionSnapshot(tab);
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
function flushAllSessionSnapshots() {
|
|
3828
|
+
for (const tab of _tabs) flushSessionSnapshotSave(tab);
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
function renderStoredSession(snapshot, $msgs, tab) {
|
|
3832
|
+
const norm = _normalizeSnapshotEntry(snapshot);
|
|
3833
|
+
if (!norm || !$msgs) return false;
|
|
3834
|
+
$msgs.innerHTML = norm.html;
|
|
3835
|
+
// Disable interactive controls — restored DOM has no event handlers and
|
|
3836
|
+
// any pending state (permission prompts, plan cards, ask cards) is stale.
|
|
3837
|
+
$msgs.querySelectorAll('button, input, select, textarea').forEach(node => { node.disabled = true; });
|
|
3838
|
+
// Drop transient overlays that should never resurrect after a fresh load
|
|
3839
|
+
$msgs.querySelectorAll('.thinking, .msg-permission-prompt, .post-plan-card').forEach(n => n.remove());
|
|
3840
|
+
// Re-attach copy buttons + file path links — innerHTML wipes their listeners
|
|
3841
|
+
$msgs.querySelectorAll('pre .cp-copy-btn').forEach(btn => btn.remove());
|
|
3842
|
+
addCopyButtons($msgs);
|
|
3843
|
+
linkifyFilePaths($msgs);
|
|
3844
|
+
if (tab) {
|
|
3845
|
+
if (norm.usage) {
|
|
3846
|
+
tab.usage.inputTokens = norm.usage.inputTokens || 0;
|
|
3847
|
+
tab.usage.outputTokens = norm.usage.outputTokens || 0;
|
|
3848
|
+
tab.usage.cacheRead = norm.usage.cacheRead || 0;
|
|
3849
|
+
tab.usage.cacheWrite = norm.usage.cacheWrite || 0;
|
|
3850
|
+
}
|
|
3851
|
+
if (norm.contextWindow) tab.contextWindow = norm.contextWindow;
|
|
3852
|
+
if (norm.turns) tab.turns = norm.turns;
|
|
3853
|
+
if (norm.sessionCost) tab.sessionCost = norm.sessionCost;
|
|
3854
|
+
renderGauge(tab);
|
|
3855
|
+
}
|
|
3856
|
+
return true;
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3716
3859
|
async function loadSessionHistory(sid, $msgs) {
|
|
3717
3860
|
if (!$msgs) $msgs = activeTab()?.messagesEl;
|
|
3718
3861
|
if (!$msgs) return;
|
|
3862
|
+
// Try local snapshot first — exact replay of the live render, lossless.
|
|
3863
|
+
const snap = getSessionSnapshot(sid);
|
|
3864
|
+
if (snap && snap.html) {
|
|
3865
|
+
const tab = _tabs.find(t => t.messagesEl === $msgs) || activeTab();
|
|
3866
|
+
if (renderStoredSession(snap, $msgs, tab)) {
|
|
3867
|
+
scrollEnd();
|
|
3868
|
+
// Background refresh: pull latest JSONL in case the session continued in
|
|
3869
|
+
// another window. If upstream has more turns than the snapshot, fall back
|
|
3870
|
+
// to the rebuild path below so the resumed view is current.
|
|
3871
|
+
try {
|
|
3872
|
+
const $project = _panel?.querySelector('#cp-project');
|
|
3873
|
+
const project = ddGetValue($project) || undefined;
|
|
3874
|
+
const params = new URLSearchParams({ limit: '1' });
|
|
3875
|
+
if (project) params.set('project', project);
|
|
3876
|
+
const probe = await fetch(`/api/claude-code/sessions/${encodeURIComponent(sid)}/messages?${params}`);
|
|
3877
|
+
const probeData = await probe.json();
|
|
3878
|
+
if ((probeData?.total || 0) > (snap.itemCount || 0) + 2) {
|
|
3879
|
+
// Upstream has materially more content — fall through to full rebuild
|
|
3880
|
+
} else {
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3883
|
+
} catch { return; }
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3719
3886
|
const $project = _panel?.querySelector('#cp-project');
|
|
3720
3887
|
const project = ddGetValue($project) || undefined;
|
|
3721
3888
|
const params = new URLSearchParams({ limit: '500' });
|
|
@@ -3737,6 +3904,13 @@ async function loadSessionHistory(sid, $msgs) {
|
|
|
3737
3904
|
const avatar = document.createElement('div'); avatar.className = 'msg-avatar';
|
|
3738
3905
|
avatar.innerHTML = CLAUDE_ICON; el.appendChild(avatar);
|
|
3739
3906
|
const wrap = document.createElement('div'); wrap.className = 'msg-content';
|
|
3907
|
+
if (m.thinking) {
|
|
3908
|
+
const thinkEl = document.createElement('details');
|
|
3909
|
+
thinkEl.className = 'msg-thinking';
|
|
3910
|
+
thinkEl.innerHTML = `<summary><span class="msg-thinking-icon">${THINKING_ICON}</span><span class="msg-thinking-label">Thinking</span><span class="msg-thinking-chevron">›</span></summary><div class="msg-thinking-content"></div>`;
|
|
3911
|
+
thinkEl.querySelector('.msg-thinking-content').textContent = m.thinking;
|
|
3912
|
+
wrap.appendChild(thinkEl);
|
|
3913
|
+
}
|
|
3740
3914
|
if (m.text) {
|
|
3741
3915
|
const body = document.createElement('div'); body.className = 'msg-body';
|
|
3742
3916
|
body._rawMd = m.text;
|
|
@@ -3788,7 +3962,7 @@ async function loadSessionHistory(sid, $msgs) {
|
|
|
3788
3962
|
$msgs.prepend(note);
|
|
3789
3963
|
}
|
|
3790
3964
|
// Populate context gauge from session history usage
|
|
3791
|
-
const tab = activeTab();
|
|
3965
|
+
const tab = _tabs.find(t => t.messagesEl === $msgs) || activeTab();
|
|
3792
3966
|
if (tab && data.usage) {
|
|
3793
3967
|
tab.usage.inputTokens = data.usage.input_tokens || 0;
|
|
3794
3968
|
tab.usage.outputTokens = data.usage.output_tokens || 0;
|
|
@@ -3799,6 +3973,9 @@ async function loadSessionHistory(sid, $msgs) {
|
|
|
3799
3973
|
renderGauge(tab);
|
|
3800
3974
|
}
|
|
3801
3975
|
scrollEnd();
|
|
3976
|
+
// Cache the rebuilt transcript so the next resume on a fresh browser hits
|
|
3977
|
+
// the snapshot path and avoids paying the JSONL → DOM cost again.
|
|
3978
|
+
if (tab && tab.sessionId === sid) writeSessionSnapshot(tab);
|
|
3802
3979
|
} catch {
|
|
3803
3980
|
$msgs.innerHTML = '<div class="msg-status">Failed to load history</div>';
|
|
3804
3981
|
}
|
|
@@ -4261,6 +4438,7 @@ function handleTabEvent(tab, ev) {
|
|
|
4261
4438
|
}
|
|
4262
4439
|
if (ev.type === 'tool_result') {
|
|
4263
4440
|
updateToolResult(tab, ev);
|
|
4441
|
+
scheduleSessionSnapshotSave(tab);
|
|
4264
4442
|
// Re-show thinking — Claude is processing the tool result
|
|
4265
4443
|
if (tab.running && !tab.pendingAsk) showThinking(tab);
|
|
4266
4444
|
return;
|
|
@@ -4527,6 +4705,7 @@ function renderPostPlanActions(tab, headerText) {
|
|
|
4527
4705
|
|
|
4528
4706
|
const actions = [
|
|
4529
4707
|
{ label: 'Continue with implementation', prompt: 'Continue with the implementation based on the approved plan.', primary: true },
|
|
4708
|
+
{ label: 'Continue planning', action: 'continue-planning' },
|
|
4530
4709
|
{ label: 'Compact context', action: 'compact' },
|
|
4531
4710
|
{ label: 'Edit plan', action: 'plan' },
|
|
4532
4711
|
];
|
|
@@ -4537,7 +4716,11 @@ function renderPostPlanActions(tab, headerText) {
|
|
|
4537
4716
|
btn.addEventListener('click', () => {
|
|
4538
4717
|
card.style.opacity = '0.45';
|
|
4539
4718
|
card.style.pointerEvents = 'none';
|
|
4540
|
-
if (a.action === '
|
|
4719
|
+
if (a.action === 'continue-planning') {
|
|
4720
|
+
tab.showPostPlanActions = false;
|
|
4721
|
+
saveTabs();
|
|
4722
|
+
return;
|
|
4723
|
+
} else if (a.action === 'compact') {
|
|
4541
4724
|
// Guard: cannot compact while running
|
|
4542
4725
|
if (tab.running) { appendStatus(tab, 'Cannot compact while Claude is processing.'); card.style.opacity = '1'; card.style.pointerEvents = 'auto'; return; }
|
|
4543
4726
|
if (tab.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -4879,6 +5062,7 @@ function renderAssistant(tab, msg) {
|
|
|
4879
5062
|
$msgs.appendChild(postPlanMsg);
|
|
4880
5063
|
}
|
|
4881
5064
|
if (tab === activeTab()) scrollEnd();
|
|
5065
|
+
scheduleSessionSnapshotSave(tab);
|
|
4882
5066
|
}
|
|
4883
5067
|
|
|
4884
5068
|
/** Build an interactive AskUserQuestion card from a tool_use block */
|
|
@@ -5993,6 +6177,7 @@ function appendUser(tab, text, images, files) {
|
|
|
5993
6177
|
if (text) { const txt = document.createTextNode(text); bubble.appendChild(txt); }
|
|
5994
6178
|
el.appendChild(bubble); $msgs.appendChild(el);
|
|
5995
6179
|
if (tab === activeTab()) scrollEnd();
|
|
6180
|
+
scheduleSessionSnapshotSave(tab);
|
|
5996
6181
|
}
|
|
5997
6182
|
|
|
5998
6183
|
function showThinking(tab) {
|
|
@@ -6022,11 +6207,13 @@ function appendStatus(tab, text) {
|
|
|
6022
6207
|
const $msgs = tab.messagesEl; if (!$msgs) return;
|
|
6023
6208
|
const el = document.createElement('div'); el.className = 'msg-status'; el.textContent = text;
|
|
6024
6209
|
$msgs.appendChild(el); pruneMessages($msgs); if (tab === activeTab()) scrollEnd();
|
|
6210
|
+
scheduleSessionSnapshotSave(tab);
|
|
6025
6211
|
}
|
|
6026
6212
|
function appendError(tab, text) {
|
|
6027
6213
|
const $msgs = tab.messagesEl; if (!$msgs) return;
|
|
6028
6214
|
const el = document.createElement('div'); el.className = 'msg-error'; el.textContent = text;
|
|
6029
6215
|
$msgs.appendChild(el); pruneMessages($msgs); if (tab === activeTab()) scrollEnd();
|
|
6216
|
+
scheduleSessionSnapshotSave(tab);
|
|
6030
6217
|
}
|
|
6031
6218
|
|
|
6032
6219
|
function scrollEnd() {
|
|
@@ -7068,12 +7255,13 @@ export async function toggleClaudePanel() {
|
|
|
7068
7255
|
wireEvents();
|
|
7069
7256
|
await loadConfig(); // must complete before restoreTabs — checks server boot ID
|
|
7070
7257
|
loadSkills();
|
|
7071
|
-
restoreTabs();
|
|
7072
|
-
loadMonthlyCost();
|
|
7073
|
-
syncSessionCosts();
|
|
7074
|
-
_wireBrowserSync();
|
|
7075
|
-
_startHeartbeat();
|
|
7076
|
-
|
|
7258
|
+
restoreTabs();
|
|
7259
|
+
loadMonthlyCost();
|
|
7260
|
+
syncSessionCosts();
|
|
7261
|
+
_wireBrowserSync();
|
|
7262
|
+
_startHeartbeat();
|
|
7263
|
+
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
|
|
7264
|
+
}
|
|
7077
7265
|
_visible = !_visible;
|
|
7078
7266
|
if (_visible) {
|
|
7079
7267
|
if (_tabs.length === 0) createTab(null, 'New chat');
|
|
@@ -7217,13 +7405,24 @@ async function loadSkills() {
|
|
|
7217
7405
|
{ name: 'loop', description: 'Run a command on a recurring interval' },
|
|
7218
7406
|
];
|
|
7219
7407
|
for (const b of extras) { if (!seen.has(b.name)) { _skillsCache.push(b); seen.add(b.name); } }
|
|
7408
|
+
try {
|
|
7409
|
+
const pc = await fetch('/api/claude-code/plugin-commands').then(r => r.json());
|
|
7410
|
+
for (const c of (pc?.commands || [])) {
|
|
7411
|
+
if (!c?.name || seen.has(c.name)) continue;
|
|
7412
|
+
const label = c.plugin ? `${c.description || ''}${c.description ? ' · ' : ''}(plugin: ${c.plugin})` : (c.description || '');
|
|
7413
|
+
_skillsCache.push({ name: c.name, description: label });
|
|
7414
|
+
seen.add(c.name);
|
|
7415
|
+
}
|
|
7416
|
+
} catch {}
|
|
7220
7417
|
_skillsCache.sort((a, b) => a.name.localeCompare(b.name));
|
|
7221
7418
|
} catch { _skillsCache = SLASH_COMMANDS.map(c => ({ name: c.name, description: c.desc })); }
|
|
7222
7419
|
return _skillsCache;
|
|
7223
7420
|
}
|
|
7224
|
-
function showSlashHints(filter) {
|
|
7421
|
+
async function showSlashHints(filter) {
|
|
7225
7422
|
const $hints = _panel?.querySelector('#cp-slash-hints');
|
|
7226
|
-
if (!$hints
|
|
7423
|
+
if (!$hints) return;
|
|
7424
|
+
if (!_skillsCache) await loadSkills();
|
|
7425
|
+
if (!_skillsCache) return;
|
|
7227
7426
|
const q = filter.toLowerCase();
|
|
7228
7427
|
const matches = _skillsCache.filter(s => s.name.toLowerCase().startsWith(q));
|
|
7229
7428
|
if (!matches.length || !filter) { hideSlashHints(); return; }
|
|
@@ -8210,11 +8409,7 @@ function wireEvents() {
|
|
|
8210
8409
|
if (source && source !== 'claude') return;
|
|
8211
8410
|
const tab = activeTab();
|
|
8212
8411
|
if (!tab || (tabId && tab.id !== tabId)) return;
|
|
8213
|
-
|
|
8214
|
-
tab.messagesEl?.querySelectorAll('.post-plan-card').forEach(card => {
|
|
8215
|
-
card.style.opacity = '1';
|
|
8216
|
-
card.style.pointerEvents = 'auto';
|
|
8217
|
-
});
|
|
8412
|
+
renderPostPlanActions(tab, tab._editedPlanContent ? 'PLAN UPDATED' : 'PLAN COMPLETE');
|
|
8218
8413
|
});
|
|
8219
8414
|
|
|
8220
8415
|
on('changelog-edit-cancelled', () => {
|
|
@@ -41,11 +41,28 @@ let _planEditMode = false; // true when editing a plan file from Claude p
|
|
|
41
41
|
let _planEditFilePath = null; // path of the plan file being edited
|
|
42
42
|
let _planEditSource = null; // panel that opened the plan editor
|
|
43
43
|
let _planEditTabId = null; // tab id to route plan editor events back
|
|
44
|
-
let _changelogEditMode = false; // true when editing a changelog draft from Claude panel
|
|
45
|
-
let _changelogEditFilePath = null; // path of the changelog draft being edited
|
|
44
|
+
let _changelogEditMode = false; // true when editing a changelog draft from Claude panel
|
|
45
|
+
let _changelogEditFilePath = null; // path of the changelog draft being edited
|
|
46
46
|
|
|
47
47
|
const MIN_WIDTH = 240;
|
|
48
|
-
const MAX_WIDTH = 500;
|
|
48
|
+
const MAX_WIDTH = 500;
|
|
49
|
+
|
|
50
|
+
function planEditorSourceLabel(source) {
|
|
51
|
+
if (source === 'codex') return 'Codex';
|
|
52
|
+
if (source === 'opencode') return 'OpenCode';
|
|
53
|
+
return 'Claude';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cancelPlanEditMode() {
|
|
57
|
+
if (!_planEditMode) return;
|
|
58
|
+
const source = _planEditSource;
|
|
59
|
+
const tabId = _planEditTabId;
|
|
60
|
+
_planEditMode = false;
|
|
61
|
+
_planEditFilePath = null;
|
|
62
|
+
_planEditSource = null;
|
|
63
|
+
_planEditTabId = null;
|
|
64
|
+
emit('plan-edit-cancelled', { source, tabId });
|
|
65
|
+
}
|
|
49
66
|
|
|
50
67
|
// ─── SVG icons ────────────────────────
|
|
51
68
|
const FOLDER_SVG = '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
@@ -1779,12 +1796,14 @@ async function openFileEditor(filePath, opts = {}) {
|
|
|
1779
1796
|
if (!res.ok) {
|
|
1780
1797
|
const isOpencodeConfig = normalizedPath.endsWith('/.config/opencode/config.json');
|
|
1781
1798
|
const isAllowlistMiss = res.status === 403 && data?.error === 'Path outside registered project roots';
|
|
1782
|
-
if (isOpencodeConfig && isAllowlistMiss) {
|
|
1783
|
-
showToast('Restart Synabun to apply OpenCode config editor support');
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1799
|
+
if (isOpencodeConfig && isAllowlistMiss) {
|
|
1800
|
+
showToast('Restart Synabun to apply OpenCode config editor support');
|
|
1801
|
+
cancelPlanEditMode();
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
showToast(data.error || 'Cannot open file');
|
|
1805
|
+
cancelPlanEditMode();
|
|
1806
|
+
return;
|
|
1788
1807
|
}
|
|
1789
1808
|
|
|
1790
1809
|
_editorOpen = true;
|
|
@@ -1843,11 +1862,11 @@ async function openFileEditor(filePath, opts = {}) {
|
|
|
1843
1862
|
// Plan/changelog edit banner
|
|
1844
1863
|
const edPanel = $('fe-editor-panel');
|
|
1845
1864
|
edPanel?.querySelector('.fe-plan-banner')?.remove();
|
|
1846
|
-
if (_planEditMode) {
|
|
1847
|
-
const banner = document.createElement('div');
|
|
1848
|
-
banner.className = 'fe-plan-banner';
|
|
1849
|
-
banner.textContent =
|
|
1850
|
-
edPanel?.prepend(banner);
|
|
1865
|
+
if (_planEditMode) {
|
|
1866
|
+
const banner = document.createElement('div');
|
|
1867
|
+
banner.className = 'fe-plan-banner';
|
|
1868
|
+
banner.textContent = `Editing plan — save to send to ${planEditorSourceLabel(_planEditSource)}`;
|
|
1869
|
+
edPanel?.prepend(banner);
|
|
1851
1870
|
} else if (_changelogEditMode) {
|
|
1852
1871
|
const banner = document.createElement('div');
|
|
1853
1872
|
banner.className = 'fe-plan-banner';
|
|
@@ -1866,10 +1885,11 @@ async function openFileEditor(filePath, opts = {}) {
|
|
|
1866
1885
|
updateScrollmap();
|
|
1867
1886
|
syncHighlightScroll();
|
|
1868
1887
|
});
|
|
1869
|
-
} catch (err) {
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
}
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
cancelPlanEditMode();
|
|
1890
|
+
showToast('Failed to open file');
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1873
1893
|
|
|
1874
1894
|
function closeFileEditor() {
|
|
1875
1895
|
if (_editorDirty) {
|