synabun 2026.4.21 → 2026.4.25-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +175 -881
- package/README.md +1 -1
- package/hooks/claude-code/prompt-submit.mjs +39 -18
- package/hooks/claude-code/session-start.mjs +10 -3
- package/hooks/claude-code/shared.mjs +39 -2
- package/hooks/claude-code/stop.mjs +33 -6
- package/mcp-server/src/index.ts +9 -1
- package/mcp-server/src/services/neural-interface.ts +42 -14
- package/mcp-server/src/services/profiles.ts +8 -7
- package/mcp-server/src/tools/browser-advanced.ts +12 -10
- package/mcp-server/src/tools/browser-interact.ts +39 -38
- package/mcp-server/src/tools/browser-navigate.ts +12 -4
- package/mcp-server/src/tools/loop.ts +7 -3
- package/mcp-server/src/tools/profile.ts +19 -1
- package/neural-interface/lib/loop-logger.js +103 -0
- package/neural-interface/lib/mcp-installer.js +109 -0
- package/neural-interface/lib/plugin-installer.js +264 -0
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/shared/api.js +66 -17
- package/neural-interface/public/shared/cdx/cdx-icons.js +5 -4
- package/neural-interface/public/shared/cdx/cdx-panel.js +117 -14
- package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
- package/neural-interface/public/shared/cdx/cdx-styles.js +55 -10
- package/neural-interface/public/shared/cdx/cdx-tabs.js +394 -72
- package/neural-interface/public/shared/cli-status.js +157 -0
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/html-shell.js +32 -11
- package/neural-interface/public/shared/ocp/ocp-panel.js +127 -28
- package/neural-interface/public/shared/ocp/ocp-render.js +52 -44
- package/neural-interface/public/shared/ocp/ocp-styles.js +33 -0
- package/neural-interface/public/shared/ocp/ocp-tabs.js +693 -82
- package/neural-interface/public/shared/styles.css +635 -0
- package/neural-interface/public/shared/ui-automation-studio.js +38 -12
- package/neural-interface/public/shared/ui-claude-panel.js +322 -15
- package/neural-interface/public/shared/ui-file-explorer.js +38 -18
- package/neural-interface/public/shared/ui-menubar.js +34 -10
- package/neural-interface/public/shared/ui-navbar.js +8 -6
- package/neural-interface/public/shared/ui-schedules-studio.js +1464 -0
- package/neural-interface/public/shared/ui-settings.js +274 -50
- package/neural-interface/public/shared/ui-sidepanel-layout.js +18 -7
- package/neural-interface/public/shared/ui-sidepanel-tray.js +287 -0
- package/neural-interface/public/shared/ui-sync.js +9 -5
- package/neural-interface/public/shared/ui-terminal.js +76 -27
- package/neural-interface/public/shared/ui-tool-updates.js +49 -15
- package/neural-interface/public/shared/ui-tutorial-steps.js +4 -2
- package/neural-interface/public/shared/ui-tutorial.js +24 -18
- package/neural-interface/public/shared/ui-whiteboard.js +79 -31
- package/neural-interface/public/shared/vterm-buffer.js +9 -8
- package/neural-interface/public/variant/2d/main.js +9 -7
- package/neural-interface/public/variant/3d/main.js +10 -8
- package/neural-interface/server.js +1292 -255
- package/package.json +1 -1
- package/skills/leonardo/SKILL.md +5 -1
- package/skills/leonardo/modules/image-prompter.md +5 -1
- package/skills/leonardo/modules/video-prompter.md +5 -1
- package/skills/synabun/SKILL.md +44 -26
- package/skills/synabun/modules/audit.md +40 -36
- package/skills/synabun/modules/changelog.md +22 -18
- package/skills/synabun/modules/memorize.md +19 -15
- package/skills/synabun/modules/schedule.md +285 -0
- package/CLAUDE.md +0 -217
|
@@ -9,6 +9,7 @@ import { fetchClaudeSessions, fetchBrowserSessions, searchSessions } from './api
|
|
|
9
9
|
import { createFrameRenderer } from './utils.js';
|
|
10
10
|
import { notify, NOTIF_TYPE } from './ui-notifications.js';
|
|
11
11
|
import { reserveRightPanelLayout, clearRightPanelLayout } from './ui-sidepanel-layout.js';
|
|
12
|
+
import { subscribeCliStatus, recheckCliStatus, getCliDocUrl, getCliInstallCommand, getCliLabel } from './cli-status.js';
|
|
12
13
|
|
|
13
14
|
const CLAUDE_ICON = '<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"/></svg>';
|
|
14
15
|
|
|
@@ -120,6 +121,10 @@ function activeTab() { return _tabs[_activeTabIdx] || null; }
|
|
|
120
121
|
let _browserEmbed = null; // { sessionId, ws, canvas, ctx, urlBar, container }
|
|
121
122
|
let _browserEmbedVisible = false;
|
|
122
123
|
|
|
124
|
+
// ── CLI-installed status (claude binary detection) ──
|
|
125
|
+
let _cliInstalled = null; // null = unknown / missing, version string = installed
|
|
126
|
+
let _cliUnsub = null;
|
|
127
|
+
|
|
123
128
|
// Marked.js (lazy loaded)
|
|
124
129
|
let _marked = null;
|
|
125
130
|
(async () => {
|
|
@@ -150,8 +155,24 @@ const STOR = {
|
|
|
150
155
|
windowRegistry: 'synabun-claude-panel-windows', // JSON map of windowId → lastSeen timestamp
|
|
151
156
|
bootId: 'synabun-claude-panel-boot-id', // server boot ID — detect restarts
|
|
152
157
|
defaultModel: 'synabun-claude-panel-default-model', // user's preferred default model
|
|
158
|
+
sessionSnapshots: 'synabun-claude-panel-session-snapshots', // global, per-sessionId rendered HTML cache
|
|
153
159
|
};
|
|
154
160
|
|
|
161
|
+
// ── Session HTML snapshot cache ──
|
|
162
|
+
// Global per-sessionId cache of rendered transcript HTML so resume on a fresh
|
|
163
|
+
// browser load mirrors the exact render that was produced during the live run.
|
|
164
|
+
// Falls back to upstream JSONL rebuild when no snapshot exists or it is stale.
|
|
165
|
+
const MAX_SESSION_SNAPSHOTS = 24;
|
|
166
|
+
const MAX_SESSION_SNAPSHOT_CHARS = 2_500_000;
|
|
167
|
+
let _sessionSnapshots = (() => {
|
|
168
|
+
try {
|
|
169
|
+
const raw = storage.getItem(STOR.sessionSnapshots);
|
|
170
|
+
if (!raw) return {};
|
|
171
|
+
const parsed = JSON.parse(raw);
|
|
172
|
+
return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
|
|
173
|
+
} catch { return {}; }
|
|
174
|
+
})();
|
|
175
|
+
|
|
155
176
|
const EFFORT_LEVELS = ['off', 'low', 'medium', 'high', 'max'];
|
|
156
177
|
const EFFORT_LABELS = { off: 'Think', low: 'lo', medium: 'med', high: 'hi', max: 'max' };
|
|
157
178
|
const EFFORT_TITLES = {
|
|
@@ -411,6 +432,41 @@ function injectStyles() {
|
|
|
411
432
|
.cp-messages:has(.msg) .cp-empty,
|
|
412
433
|
.cp-messages:has(.msg-status) .cp-empty { display: none; }
|
|
413
434
|
|
|
435
|
+
/* ── CLI-not-installed banner (replaces empty-state hint when claude binary missing) ── */
|
|
436
|
+
.cp-cli-missing {
|
|
437
|
+
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
438
|
+
max-width: 320px; padding: 14px 16px; margin-top: 4px;
|
|
439
|
+
border: 1px solid rgba(255,180,80,0.35); border-radius: 8px;
|
|
440
|
+
background: rgba(255,180,80,0.06); pointer-events: auto;
|
|
441
|
+
}
|
|
442
|
+
.cp-cli-missing-title {
|
|
443
|
+
font-size: 12px; font-weight: 600; color: rgba(255,200,120,0.9);
|
|
444
|
+
letter-spacing: 0.02em;
|
|
445
|
+
}
|
|
446
|
+
.cp-cli-missing-body {
|
|
447
|
+
font-size: 11px; color: rgba(255,255,255,0.55); text-align: center; line-height: 1.45;
|
|
448
|
+
}
|
|
449
|
+
.cp-cli-missing-body code {
|
|
450
|
+
display: inline-block; padding: 1px 6px; margin-top: 4px;
|
|
451
|
+
background: rgba(0,0,0,0.35); border-radius: 4px; font-size: 10.5px;
|
|
452
|
+
color: rgba(255,255,255,0.75);
|
|
453
|
+
}
|
|
454
|
+
.cp-cli-missing-actions { display: flex; gap: 8px; margin-top: 4px; }
|
|
455
|
+
.cp-cli-missing-link, .cp-cli-missing-recheck {
|
|
456
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
457
|
+
padding: 5px 10px; font-size: 11px; font-weight: 500;
|
|
458
|
+
border-radius: 5px; border: 1px solid rgba(255,255,255,0.15);
|
|
459
|
+
background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85);
|
|
460
|
+
text-decoration: none; cursor: pointer; user-select: none;
|
|
461
|
+
}
|
|
462
|
+
.cp-cli-missing-link:hover, .cp-cli-missing-recheck:hover {
|
|
463
|
+
background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3);
|
|
464
|
+
}
|
|
465
|
+
.cp-cli-missing-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
466
|
+
/* Block send button visually + functionally when CLI missing */
|
|
467
|
+
.claude-panel.cp-cli-blocked .cp-send { opacity: 0.4 !important; cursor: not-allowed !important; }
|
|
468
|
+
.claude-panel.cp-cli-blocked .cp-input { opacity: 0.7; }
|
|
469
|
+
|
|
414
470
|
/* ── Browser embed (live screencast inside panel) ── */
|
|
415
471
|
.cp-browser-embed {
|
|
416
472
|
display: none;
|
|
@@ -1076,7 +1132,7 @@ function injectStyles() {
|
|
|
1076
1132
|
padding: 1px;
|
|
1077
1133
|
background: rgba(255,255,255,0.05);
|
|
1078
1134
|
transition: background 0.4s;
|
|
1079
|
-
min-width: 0;
|
|
1135
|
+
min-width: 0;
|
|
1080
1136
|
}
|
|
1081
1137
|
.cp-input-wrap::before {
|
|
1082
1138
|
content: '';
|
|
@@ -1476,7 +1532,8 @@ function injectStyles() {
|
|
|
1476
1532
|
backdrop-filter: blur(20px);
|
|
1477
1533
|
border: 1px solid rgba(255,255,255,0.08);
|
|
1478
1534
|
border-radius: 10px; padding: 4px;
|
|
1479
|
-
z-index: 310; max-height:
|
|
1535
|
+
z-index: 310; max-height: 320px; overflow-y: auto;
|
|
1536
|
+
overscroll-behavior: contain;
|
|
1480
1537
|
box-shadow: var(--shadow-lg);
|
|
1481
1538
|
}
|
|
1482
1539
|
.cp-slash-hints.open { display: block; }
|
|
@@ -2632,14 +2689,14 @@ document.addEventListener('click', (e) => {
|
|
|
2632
2689
|
});
|
|
2633
2690
|
|
|
2634
2691
|
// ── Tab lifecycle ──
|
|
2635
|
-
function createTab(sessionId = null, label = 'New chat', { autoSwitch = true } = {}) {
|
|
2692
|
+
function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
|
|
2636
2693
|
if (_tabs.length >= MAX_TABS) return null;
|
|
2637
2694
|
// Capture current dropdown state for the new tab's project/model/effort
|
|
2638
2695
|
const _$proj = _panel?.querySelector('#cp-project');
|
|
2639
2696
|
const _$mod = _panel?.querySelector('#cp-model');
|
|
2640
2697
|
const _$think = _panel?.querySelector('#cp-think-toggle');
|
|
2641
2698
|
const tab = {
|
|
2642
|
-
id: crypto.randomUUID(),
|
|
2699
|
+
id: id || crypto.randomUUID(),
|
|
2643
2700
|
sessionId,
|
|
2644
2701
|
label,
|
|
2645
2702
|
project: ddGetValue(_$proj) || '', // per-tab project isolation
|
|
@@ -2695,6 +2752,7 @@ function createTab(sessionId = null, label = 'New chat', { autoSwitch = true } =
|
|
|
2695
2752
|
msgDiv.innerHTML = `<div class="cp-empty"><div class="cp-empty-logo">${CLAUDE_ICON}</div><span class="cp-empty-name">Claude</span><span class="cp-empty-hint">Send a message to start</span></div>`;
|
|
2696
2753
|
container.appendChild(msgDiv);
|
|
2697
2754
|
tab.messagesEl = msgDiv;
|
|
2755
|
+
if (_cliInstalled === null) _renderCliMissingBanner(msgDiv.querySelector('.cp-empty'));
|
|
2698
2756
|
// Connect WebSocket
|
|
2699
2757
|
connectTab(tab);
|
|
2700
2758
|
// Create pill (rendered in shared tray, not inside panel)
|
|
@@ -2729,6 +2787,7 @@ function switchTab(idx) {
|
|
|
2729
2787
|
if (idx < 0 || idx >= _tabs.length) return;
|
|
2730
2788
|
const prev = activeTab();
|
|
2731
2789
|
if (prev && prev !== _tabs[idx]) {
|
|
2790
|
+
flushSessionSnapshotSave(prev);
|
|
2732
2791
|
const $input = _panel?.querySelector('#cp-input');
|
|
2733
2792
|
if ($input) prev.draft = $input.value;
|
|
2734
2793
|
// Animate out
|
|
@@ -2818,6 +2877,7 @@ function switchTab(idx) {
|
|
|
2818
2877
|
function closeTab(idx) {
|
|
2819
2878
|
if (idx < 0 || idx >= _tabs.length) return;
|
|
2820
2879
|
const tab = _tabs[idx];
|
|
2880
|
+
flushSessionSnapshotSave(tab);
|
|
2821
2881
|
recordHookEvent(tab, 'SessionEnd', tab.sessionId || '(new)');
|
|
2822
2882
|
tab.closed = true;
|
|
2823
2883
|
clearTimeout(tab.reconnectTimer);
|
|
@@ -2956,7 +3016,7 @@ function restoreTabs() {
|
|
|
2956
3016
|
const data = JSON.parse(raw);
|
|
2957
3017
|
if (data.tabs?.length) {
|
|
2958
3018
|
for (const saved of data.tabs) {
|
|
2959
|
-
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false });
|
|
3019
|
+
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
2960
3020
|
if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
|
|
2961
3021
|
if (t && saved.running) t._wasRunning = true; // trigger reattach on WS open
|
|
2962
3022
|
if (t && saved.project) t.project = saved.project;
|
|
@@ -2979,7 +3039,7 @@ function restoreTabs() {
|
|
|
2979
3039
|
const data = JSON.parse(legacyRaw);
|
|
2980
3040
|
if (data.tabs?.length) {
|
|
2981
3041
|
for (const saved of data.tabs) {
|
|
2982
|
-
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false });
|
|
3042
|
+
const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
|
|
2983
3043
|
if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
|
|
2984
3044
|
if (t && saved.running) t._wasRunning = true;
|
|
2985
3045
|
if (t && saved.project) t.project = saved.project;
|
|
@@ -3055,6 +3115,7 @@ async function _releaseAllSessionLocks() {
|
|
|
3055
3115
|
|
|
3056
3116
|
// Release locks when window closes
|
|
3057
3117
|
window.addEventListener('beforeunload', () => {
|
|
3118
|
+
flushAllSessionSnapshots();
|
|
3058
3119
|
_stopHeartbeat();
|
|
3059
3120
|
// Use sendBeacon for reliable delivery on close
|
|
3060
3121
|
navigator.sendBeacon(
|
|
@@ -3063,6 +3124,11 @@ window.addEventListener('beforeunload', () => {
|
|
|
3063
3124
|
);
|
|
3064
3125
|
});
|
|
3065
3126
|
|
|
3127
|
+
window.addEventListener('pagehide', flushAllSessionSnapshots);
|
|
3128
|
+
document.addEventListener('visibilitychange', () => {
|
|
3129
|
+
if (document.hidden) flushAllSessionSnapshots();
|
|
3130
|
+
});
|
|
3131
|
+
|
|
3066
3132
|
// Re-sync UI state when window regains focus (catches stale running states from background tabs)
|
|
3067
3133
|
window.addEventListener('focus', () => {
|
|
3068
3134
|
if (!_panel) return;
|
|
@@ -3510,6 +3576,7 @@ async function selectSession(sid, label) {
|
|
|
3510
3576
|
const btn = _panel?.querySelector('.cp-session-label');
|
|
3511
3577
|
if (btn) btn.textContent = tab.label;
|
|
3512
3578
|
tab.messagesEl.innerHTML = `<div class="cp-empty"><div class="cp-empty-logo">${CLAUDE_ICON}</div><span class="cp-empty-name">Claude</span><span class="cp-empty-hint">Send a message to start</span></div>`;
|
|
3579
|
+
if (_cliInstalled === null) _renderCliMissingBanner(tab.messagesEl.querySelector('.cp-empty'));
|
|
3513
3580
|
if (sid) {
|
|
3514
3581
|
loadSessionHistory(sid, tab.messagesEl);
|
|
3515
3582
|
// Restore session cost from server
|
|
@@ -3713,9 +3780,151 @@ function promptNameNewSession() {
|
|
|
3713
3780
|
});
|
|
3714
3781
|
}
|
|
3715
3782
|
|
|
3783
|
+
// ── Session HTML snapshot helpers ──
|
|
3784
|
+
function _normalizeSnapshotEntry(entry) {
|
|
3785
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
3786
|
+
const html = typeof entry.html === 'string' ? entry.html : '';
|
|
3787
|
+
if (!html) return null;
|
|
3788
|
+
return {
|
|
3789
|
+
html,
|
|
3790
|
+
updatedAt: Number(entry.updatedAt) || 0,
|
|
3791
|
+
label: typeof entry.label === 'string' ? entry.label : '',
|
|
3792
|
+
usage: (entry.usage && typeof entry.usage === 'object') ? entry.usage : null,
|
|
3793
|
+
contextWindow: Number(entry.contextWindow) || 0,
|
|
3794
|
+
turns: Number(entry.turns) || 0,
|
|
3795
|
+
sessionCost: Number(entry.sessionCost) || 0,
|
|
3796
|
+
itemCount: Number(entry.itemCount) || 0,
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
function _persistSessionSnapshots() {
|
|
3801
|
+
try {
|
|
3802
|
+
const source = (_sessionSnapshots && typeof _sessionSnapshots === 'object' && !Array.isArray(_sessionSnapshots))
|
|
3803
|
+
? _sessionSnapshots : {};
|
|
3804
|
+
const entries = Object.entries(source)
|
|
3805
|
+
.map(([sid, e]) => [sid, _normalizeSnapshotEntry(e)])
|
|
3806
|
+
.filter(([, e]) => !!e)
|
|
3807
|
+
.sort(([, a], [, b]) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
3808
|
+
const next = {};
|
|
3809
|
+
let totalChars = 0;
|
|
3810
|
+
for (const [sid, entry] of entries) {
|
|
3811
|
+
const size = entry.html.length;
|
|
3812
|
+
if (Object.keys(next).length >= MAX_SESSION_SNAPSHOTS) break;
|
|
3813
|
+
if (totalChars + size > MAX_SESSION_SNAPSHOT_CHARS && Object.keys(next).length) continue;
|
|
3814
|
+
totalChars += size;
|
|
3815
|
+
next[sid] = entry;
|
|
3816
|
+
}
|
|
3817
|
+
_sessionSnapshots = next;
|
|
3818
|
+
storage.setItem(STOR.sessionSnapshots, JSON.stringify(next));
|
|
3819
|
+
} catch {}
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
function getSessionSnapshot(sid) {
|
|
3823
|
+
if (!sid) return null;
|
|
3824
|
+
return _normalizeSnapshotEntry(_sessionSnapshots?.[sid]);
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
function writeSessionSnapshot(tab) {
|
|
3828
|
+
const sid = tab?.sessionId || null;
|
|
3829
|
+
const $msgs = tab?.messagesEl || null;
|
|
3830
|
+
if (!sid || !$msgs) return;
|
|
3831
|
+
const html = $msgs.innerHTML || '';
|
|
3832
|
+
if (!html.trim()) return;
|
|
3833
|
+
// Skip snapshot if the transcript only contains the empty-state placeholder
|
|
3834
|
+
if ($msgs.children.length === 1 && $msgs.firstElementChild?.classList?.contains('cp-empty')) return;
|
|
3835
|
+
if (!_sessionSnapshots || typeof _sessionSnapshots !== 'object' || Array.isArray(_sessionSnapshots)) {
|
|
3836
|
+
_sessionSnapshots = {};
|
|
3837
|
+
}
|
|
3838
|
+
const itemCount = [...$msgs.children].filter(n =>
|
|
3839
|
+
!n.classList?.contains('cp-empty') && !n.classList?.contains('thinking')
|
|
3840
|
+
).length;
|
|
3841
|
+
_sessionSnapshots[sid] = {
|
|
3842
|
+
html,
|
|
3843
|
+
updatedAt: Date.now(),
|
|
3844
|
+
label: tab.label || '',
|
|
3845
|
+
usage: tab.usage ? { ...tab.usage } : null,
|
|
3846
|
+
contextWindow: tab.contextWindow || 0,
|
|
3847
|
+
turns: tab.turns || 0,
|
|
3848
|
+
sessionCost: tab.sessionCost || 0,
|
|
3849
|
+
itemCount,
|
|
3850
|
+
};
|
|
3851
|
+
_persistSessionSnapshots();
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
function scheduleSessionSnapshotSave(tab, ms = 350) {
|
|
3855
|
+
if (!tab?.sessionId || !tab?.messagesEl) return;
|
|
3856
|
+
if (tab._snapshotTimer) clearTimeout(tab._snapshotTimer);
|
|
3857
|
+
tab._snapshotTimer = setTimeout(() => {
|
|
3858
|
+
tab._snapshotTimer = null;
|
|
3859
|
+
writeSessionSnapshot(tab);
|
|
3860
|
+
}, ms);
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
function flushSessionSnapshotSave(tab) {
|
|
3864
|
+
if (!tab) return;
|
|
3865
|
+
if (tab._snapshotTimer) { clearTimeout(tab._snapshotTimer); tab._snapshotTimer = null; }
|
|
3866
|
+
writeSessionSnapshot(tab);
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
function flushAllSessionSnapshots() {
|
|
3870
|
+
for (const tab of _tabs) flushSessionSnapshotSave(tab);
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
function renderStoredSession(snapshot, $msgs, tab) {
|
|
3874
|
+
const norm = _normalizeSnapshotEntry(snapshot);
|
|
3875
|
+
if (!norm || !$msgs) return false;
|
|
3876
|
+
$msgs.innerHTML = norm.html;
|
|
3877
|
+
// Disable interactive controls — restored DOM has no event handlers and
|
|
3878
|
+
// any pending state (permission prompts, plan cards, ask cards) is stale.
|
|
3879
|
+
$msgs.querySelectorAll('button, input, select, textarea').forEach(node => { node.disabled = true; });
|
|
3880
|
+
// Drop transient overlays that should never resurrect after a fresh load
|
|
3881
|
+
$msgs.querySelectorAll('.thinking, .msg-permission-prompt, .post-plan-card').forEach(n => n.remove());
|
|
3882
|
+
// Re-attach copy buttons + file path links — innerHTML wipes their listeners
|
|
3883
|
+
$msgs.querySelectorAll('pre .cp-copy-btn').forEach(btn => btn.remove());
|
|
3884
|
+
addCopyButtons($msgs);
|
|
3885
|
+
linkifyFilePaths($msgs);
|
|
3886
|
+
if (tab) {
|
|
3887
|
+
if (norm.usage) {
|
|
3888
|
+
tab.usage.inputTokens = norm.usage.inputTokens || 0;
|
|
3889
|
+
tab.usage.outputTokens = norm.usage.outputTokens || 0;
|
|
3890
|
+
tab.usage.cacheRead = norm.usage.cacheRead || 0;
|
|
3891
|
+
tab.usage.cacheWrite = norm.usage.cacheWrite || 0;
|
|
3892
|
+
}
|
|
3893
|
+
if (norm.contextWindow) tab.contextWindow = norm.contextWindow;
|
|
3894
|
+
if (norm.turns) tab.turns = norm.turns;
|
|
3895
|
+
if (norm.sessionCost) tab.sessionCost = norm.sessionCost;
|
|
3896
|
+
renderGauge(tab);
|
|
3897
|
+
}
|
|
3898
|
+
return true;
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3716
3901
|
async function loadSessionHistory(sid, $msgs) {
|
|
3717
3902
|
if (!$msgs) $msgs = activeTab()?.messagesEl;
|
|
3718
3903
|
if (!$msgs) return;
|
|
3904
|
+
// Try local snapshot first — exact replay of the live render, lossless.
|
|
3905
|
+
const snap = getSessionSnapshot(sid);
|
|
3906
|
+
if (snap && snap.html) {
|
|
3907
|
+
const tab = _tabs.find(t => t.messagesEl === $msgs) || activeTab();
|
|
3908
|
+
if (renderStoredSession(snap, $msgs, tab)) {
|
|
3909
|
+
scrollEnd();
|
|
3910
|
+
// Background refresh: pull latest JSONL in case the session continued in
|
|
3911
|
+
// another window. If upstream has more turns than the snapshot, fall back
|
|
3912
|
+
// to the rebuild path below so the resumed view is current.
|
|
3913
|
+
try {
|
|
3914
|
+
const $project = _panel?.querySelector('#cp-project');
|
|
3915
|
+
const project = ddGetValue($project) || undefined;
|
|
3916
|
+
const params = new URLSearchParams({ limit: '1' });
|
|
3917
|
+
if (project) params.set('project', project);
|
|
3918
|
+
const probe = await fetch(`/api/claude-code/sessions/${encodeURIComponent(sid)}/messages?${params}`);
|
|
3919
|
+
const probeData = await probe.json();
|
|
3920
|
+
if ((probeData?.total || 0) > (snap.itemCount || 0) + 2) {
|
|
3921
|
+
// Upstream has materially more content — fall through to full rebuild
|
|
3922
|
+
} else {
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
} catch { return; }
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3719
3928
|
const $project = _panel?.querySelector('#cp-project');
|
|
3720
3929
|
const project = ddGetValue($project) || undefined;
|
|
3721
3930
|
const params = new URLSearchParams({ limit: '500' });
|
|
@@ -3737,6 +3946,13 @@ async function loadSessionHistory(sid, $msgs) {
|
|
|
3737
3946
|
const avatar = document.createElement('div'); avatar.className = 'msg-avatar';
|
|
3738
3947
|
avatar.innerHTML = CLAUDE_ICON; el.appendChild(avatar);
|
|
3739
3948
|
const wrap = document.createElement('div'); wrap.className = 'msg-content';
|
|
3949
|
+
if (m.thinking) {
|
|
3950
|
+
const thinkEl = document.createElement('details');
|
|
3951
|
+
thinkEl.className = 'msg-thinking';
|
|
3952
|
+
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>`;
|
|
3953
|
+
thinkEl.querySelector('.msg-thinking-content').textContent = m.thinking;
|
|
3954
|
+
wrap.appendChild(thinkEl);
|
|
3955
|
+
}
|
|
3740
3956
|
if (m.text) {
|
|
3741
3957
|
const body = document.createElement('div'); body.className = 'msg-body';
|
|
3742
3958
|
body._rawMd = m.text;
|
|
@@ -3788,7 +4004,7 @@ async function loadSessionHistory(sid, $msgs) {
|
|
|
3788
4004
|
$msgs.prepend(note);
|
|
3789
4005
|
}
|
|
3790
4006
|
// Populate context gauge from session history usage
|
|
3791
|
-
const tab = activeTab();
|
|
4007
|
+
const tab = _tabs.find(t => t.messagesEl === $msgs) || activeTab();
|
|
3792
4008
|
if (tab && data.usage) {
|
|
3793
4009
|
tab.usage.inputTokens = data.usage.input_tokens || 0;
|
|
3794
4010
|
tab.usage.outputTokens = data.usage.output_tokens || 0;
|
|
@@ -3799,6 +4015,9 @@ async function loadSessionHistory(sid, $msgs) {
|
|
|
3799
4015
|
renderGauge(tab);
|
|
3800
4016
|
}
|
|
3801
4017
|
scrollEnd();
|
|
4018
|
+
// Cache the rebuilt transcript so the next resume on a fresh browser hits
|
|
4019
|
+
// the snapshot path and avoids paying the JSONL → DOM cost again.
|
|
4020
|
+
if (tab && tab.sessionId === sid) writeSessionSnapshot(tab);
|
|
3802
4021
|
} catch {
|
|
3803
4022
|
$msgs.innerHTML = '<div class="msg-status">Failed to load history</div>';
|
|
3804
4023
|
}
|
|
@@ -4261,6 +4480,7 @@ function handleTabEvent(tab, ev) {
|
|
|
4261
4480
|
}
|
|
4262
4481
|
if (ev.type === 'tool_result') {
|
|
4263
4482
|
updateToolResult(tab, ev);
|
|
4483
|
+
scheduleSessionSnapshotSave(tab);
|
|
4264
4484
|
// Re-show thinking — Claude is processing the tool result
|
|
4265
4485
|
if (tab.running && !tab.pendingAsk) showThinking(tab);
|
|
4266
4486
|
return;
|
|
@@ -4527,6 +4747,7 @@ function renderPostPlanActions(tab, headerText) {
|
|
|
4527
4747
|
|
|
4528
4748
|
const actions = [
|
|
4529
4749
|
{ label: 'Continue with implementation', prompt: 'Continue with the implementation based on the approved plan.', primary: true },
|
|
4750
|
+
{ label: 'Continue planning', action: 'continue-planning' },
|
|
4530
4751
|
{ label: 'Compact context', action: 'compact' },
|
|
4531
4752
|
{ label: 'Edit plan', action: 'plan' },
|
|
4532
4753
|
];
|
|
@@ -4537,7 +4758,11 @@ function renderPostPlanActions(tab, headerText) {
|
|
|
4537
4758
|
btn.addEventListener('click', () => {
|
|
4538
4759
|
card.style.opacity = '0.45';
|
|
4539
4760
|
card.style.pointerEvents = 'none';
|
|
4540
|
-
if (a.action === '
|
|
4761
|
+
if (a.action === 'continue-planning') {
|
|
4762
|
+
tab.showPostPlanActions = false;
|
|
4763
|
+
saveTabs();
|
|
4764
|
+
return;
|
|
4765
|
+
} else if (a.action === 'compact') {
|
|
4541
4766
|
// Guard: cannot compact while running
|
|
4542
4767
|
if (tab.running) { appendStatus(tab, 'Cannot compact while Claude is processing.'); card.style.opacity = '1'; card.style.pointerEvents = 'auto'; return; }
|
|
4543
4768
|
if (tab.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -4879,6 +5104,7 @@ function renderAssistant(tab, msg) {
|
|
|
4879
5104
|
$msgs.appendChild(postPlanMsg);
|
|
4880
5105
|
}
|
|
4881
5106
|
if (tab === activeTab()) scrollEnd();
|
|
5107
|
+
scheduleSessionSnapshotSave(tab);
|
|
4882
5108
|
}
|
|
4883
5109
|
|
|
4884
5110
|
/** Build an interactive AskUserQuestion card from a tool_use block */
|
|
@@ -5993,6 +6219,7 @@ function appendUser(tab, text, images, files) {
|
|
|
5993
6219
|
if (text) { const txt = document.createTextNode(text); bubble.appendChild(txt); }
|
|
5994
6220
|
el.appendChild(bubble); $msgs.appendChild(el);
|
|
5995
6221
|
if (tab === activeTab()) scrollEnd();
|
|
6222
|
+
scheduleSessionSnapshotSave(tab);
|
|
5996
6223
|
}
|
|
5997
6224
|
|
|
5998
6225
|
function showThinking(tab) {
|
|
@@ -6022,11 +6249,13 @@ function appendStatus(tab, text) {
|
|
|
6022
6249
|
const $msgs = tab.messagesEl; if (!$msgs) return;
|
|
6023
6250
|
const el = document.createElement('div'); el.className = 'msg-status'; el.textContent = text;
|
|
6024
6251
|
$msgs.appendChild(el); pruneMessages($msgs); if (tab === activeTab()) scrollEnd();
|
|
6252
|
+
scheduleSessionSnapshotSave(tab);
|
|
6025
6253
|
}
|
|
6026
6254
|
function appendError(tab, text) {
|
|
6027
6255
|
const $msgs = tab.messagesEl; if (!$msgs) return;
|
|
6028
6256
|
const el = document.createElement('div'); el.className = 'msg-error'; el.textContent = text;
|
|
6029
6257
|
$msgs.appendChild(el); pruneMessages($msgs); if (tab === activeTab()) scrollEnd();
|
|
6258
|
+
scheduleSessionSnapshotSave(tab);
|
|
6030
6259
|
}
|
|
6031
6260
|
|
|
6032
6261
|
function scrollEnd() {
|
|
@@ -6357,6 +6586,10 @@ function runSlashCommand(tab, raw) {
|
|
|
6357
6586
|
}
|
|
6358
6587
|
|
|
6359
6588
|
function send({ shift = false } = {}) {
|
|
6589
|
+
if (_cliInstalled === null) {
|
|
6590
|
+
notify({ type: NOTIF_TYPE.WARN, title: 'Claude CLI not installed', body: 'Install the Claude Code CLI before sending a message.' });
|
|
6591
|
+
return;
|
|
6592
|
+
}
|
|
6360
6593
|
const tab = activeTab();
|
|
6361
6594
|
if (!tab) return;
|
|
6362
6595
|
const $input = _panel?.querySelector('#cp-input');
|
|
@@ -7059,6 +7292,70 @@ function syncReservedWidth() {
|
|
|
7059
7292
|
}
|
|
7060
7293
|
}
|
|
7061
7294
|
|
|
7295
|
+
// ── CLI installation status (banner + send-block) ──
|
|
7296
|
+
function _renderCliMissingBanner(emptyEl) {
|
|
7297
|
+
if (!emptyEl) return;
|
|
7298
|
+
const label = getCliLabel('claude-code');
|
|
7299
|
+
const cmd = getCliInstallCommand('claude-code');
|
|
7300
|
+
const url = getCliDocUrl('claude-code');
|
|
7301
|
+
const hint = emptyEl.querySelector('.cp-empty-hint');
|
|
7302
|
+
if (hint) hint.style.display = 'none';
|
|
7303
|
+
let banner = emptyEl.querySelector('.cp-cli-missing');
|
|
7304
|
+
if (!banner) {
|
|
7305
|
+
banner = document.createElement('div');
|
|
7306
|
+
banner.className = 'cp-cli-missing';
|
|
7307
|
+
banner.innerHTML = `
|
|
7308
|
+
<div class="cp-cli-missing-title">${label} CLI not installed</div>
|
|
7309
|
+
<div class="cp-cli-missing-body">Run: <code>${cmd}</code></div>
|
|
7310
|
+
<div class="cp-cli-missing-actions">
|
|
7311
|
+
<a class="cp-cli-missing-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
|
|
7312
|
+
<button class="cp-cli-missing-recheck" type="button">Re-check</button>
|
|
7313
|
+
</div>
|
|
7314
|
+
`;
|
|
7315
|
+
emptyEl.appendChild(banner);
|
|
7316
|
+
const btn = banner.querySelector('.cp-cli-missing-recheck');
|
|
7317
|
+
btn?.addEventListener('click', async () => {
|
|
7318
|
+
btn.disabled = true;
|
|
7319
|
+
btn.textContent = 'Checking…';
|
|
7320
|
+
try { await recheckCliStatus('claude-code'); }
|
|
7321
|
+
finally {
|
|
7322
|
+
if (btn.isConnected) {
|
|
7323
|
+
btn.disabled = false;
|
|
7324
|
+
btn.textContent = 'Re-check';
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
});
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
|
|
7331
|
+
function _refreshCliBanners() {
|
|
7332
|
+
if (!_panel) return;
|
|
7333
|
+
const empties = _panel.querySelectorAll('.cp-empty');
|
|
7334
|
+
if (_cliInstalled === null) {
|
|
7335
|
+
_panel.classList.add('cp-cli-blocked');
|
|
7336
|
+
for (const el of empties) _renderCliMissingBanner(el);
|
|
7337
|
+
} else {
|
|
7338
|
+
_panel.classList.remove('cp-cli-blocked');
|
|
7339
|
+
for (const el of empties) {
|
|
7340
|
+
const banner = el.querySelector('.cp-cli-missing');
|
|
7341
|
+
if (banner) banner.remove();
|
|
7342
|
+
const hint = el.querySelector('.cp-empty-hint');
|
|
7343
|
+
if (hint) hint.style.display = '';
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
// Re-sync send button state — installed transition should re-enable input drafts
|
|
7347
|
+
const $send = _panel.querySelector('#cp-send');
|
|
7348
|
+
if ($send && _cliInstalled === null) $send.disabled = true;
|
|
7349
|
+
}
|
|
7350
|
+
|
|
7351
|
+
function _ensureCliSubscription() {
|
|
7352
|
+
if (_cliUnsub) return;
|
|
7353
|
+
_cliUnsub = subscribeCliStatus('claude-code', (info) => {
|
|
7354
|
+
_cliInstalled = info?.installed || null;
|
|
7355
|
+
_refreshCliBanners();
|
|
7356
|
+
});
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7062
7359
|
// ── Public API ──
|
|
7063
7360
|
export async function toggleClaudePanel() {
|
|
7064
7361
|
if (!_panel) {
|
|
@@ -7073,6 +7370,8 @@ export async function toggleClaudePanel() {
|
|
|
7073
7370
|
syncSessionCosts();
|
|
7074
7371
|
_wireBrowserSync();
|
|
7075
7372
|
_startHeartbeat();
|
|
7373
|
+
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
|
|
7374
|
+
_ensureCliSubscription();
|
|
7076
7375
|
}
|
|
7077
7376
|
_visible = !_visible;
|
|
7078
7377
|
if (_visible) {
|
|
@@ -7081,6 +7380,7 @@ export async function toggleClaudePanel() {
|
|
|
7081
7380
|
_panel.classList.add('open');
|
|
7082
7381
|
syncReservedWidth();
|
|
7083
7382
|
_panel.querySelector('#cp-input')?.focus();
|
|
7383
|
+
_refreshCliBanners();
|
|
7084
7384
|
} else {
|
|
7085
7385
|
const tab = activeTab();
|
|
7086
7386
|
const $input = _panel?.querySelector('#cp-input');
|
|
@@ -7217,13 +7517,24 @@ async function loadSkills() {
|
|
|
7217
7517
|
{ name: 'loop', description: 'Run a command on a recurring interval' },
|
|
7218
7518
|
];
|
|
7219
7519
|
for (const b of extras) { if (!seen.has(b.name)) { _skillsCache.push(b); seen.add(b.name); } }
|
|
7520
|
+
try {
|
|
7521
|
+
const pc = await fetch('/api/claude-code/plugin-commands').then(r => r.json());
|
|
7522
|
+
for (const c of (pc?.commands || [])) {
|
|
7523
|
+
if (!c?.name || seen.has(c.name)) continue;
|
|
7524
|
+
const label = c.plugin ? `${c.description || ''}${c.description ? ' · ' : ''}(plugin: ${c.plugin})` : (c.description || '');
|
|
7525
|
+
_skillsCache.push({ name: c.name, description: label });
|
|
7526
|
+
seen.add(c.name);
|
|
7527
|
+
}
|
|
7528
|
+
} catch {}
|
|
7220
7529
|
_skillsCache.sort((a, b) => a.name.localeCompare(b.name));
|
|
7221
7530
|
} catch { _skillsCache = SLASH_COMMANDS.map(c => ({ name: c.name, description: c.desc })); }
|
|
7222
7531
|
return _skillsCache;
|
|
7223
7532
|
}
|
|
7224
|
-
function showSlashHints(filter) {
|
|
7533
|
+
async function showSlashHints(filter) {
|
|
7225
7534
|
const $hints = _panel?.querySelector('#cp-slash-hints');
|
|
7226
|
-
if (!$hints
|
|
7535
|
+
if (!$hints) return;
|
|
7536
|
+
if (!_skillsCache) await loadSkills();
|
|
7537
|
+
if (!_skillsCache) return;
|
|
7227
7538
|
const q = filter.toLowerCase();
|
|
7228
7539
|
const matches = _skillsCache.filter(s => s.name.toLowerCase().startsWith(q));
|
|
7229
7540
|
if (!matches.length || !filter) { hideSlashHints(); return; }
|
|
@@ -8210,11 +8521,7 @@ function wireEvents() {
|
|
|
8210
8521
|
if (source && source !== 'claude') return;
|
|
8211
8522
|
const tab = activeTab();
|
|
8212
8523
|
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
|
-
});
|
|
8524
|
+
renderPostPlanActions(tab, tab._editedPlanContent ? 'PLAN UPDATED' : 'PLAN COMPLETE');
|
|
8218
8525
|
});
|
|
8219
8526
|
|
|
8220
8527
|
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) {
|