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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// SynaBun sidepanel tray placeholders.
|
|
2
|
+
// Renders minimized pills for persisted sidepanel tabs before a panel is booted.
|
|
3
|
+
|
|
4
|
+
import { storage } from './storage.js';
|
|
5
|
+
import { emit } from './state.js';
|
|
6
|
+
import { getProviderMeta } from './provider-icons.js';
|
|
7
|
+
import { toggleClaudePanel, isClaudePanelOpen } from './ui-claude-panel.js';
|
|
8
|
+
import { toggleCodexPanel, isCodexPanelOpen } from './ui-codex-panel.js';
|
|
9
|
+
import { toggleOpencodePanel, isOpencodePanelOpen } from './ui-opencode-panel.js';
|
|
10
|
+
|
|
11
|
+
const PLACEHOLDER_CLASS = 'sidepanel-tray-placeholder';
|
|
12
|
+
|
|
13
|
+
const PROVIDERS = {
|
|
14
|
+
claude: {
|
|
15
|
+
providerId: 'claude-code',
|
|
16
|
+
windowKey: 'cp-window-id',
|
|
17
|
+
tabsKeyPrefix: 'synabun-claude-panel-tabs',
|
|
18
|
+
pillClass: 'cp-session-pill',
|
|
19
|
+
eventName: 'claude-panel:show',
|
|
20
|
+
},
|
|
21
|
+
codex: {
|
|
22
|
+
providerId: 'codex',
|
|
23
|
+
windowKey: 'cxp-window-id',
|
|
24
|
+
tabsKeyPrefix: 'synabun-codex-panel-tabs',
|
|
25
|
+
pillClass: 'cxp-session-pill',
|
|
26
|
+
eventName: 'codex-panel:show',
|
|
27
|
+
},
|
|
28
|
+
opencode: {
|
|
29
|
+
providerId: 'opencode',
|
|
30
|
+
windowKey: 'ocp-window-id',
|
|
31
|
+
tabsKeyPrefix: 'synabun-ocp-tabs',
|
|
32
|
+
pillClass: 'ocp-session-pill',
|
|
33
|
+
eventName: 'opencode-panel:show',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const _loadedProviders = new Set();
|
|
38
|
+
let _initialized = false;
|
|
39
|
+
|
|
40
|
+
function esc(s) {
|
|
41
|
+
return String(s)
|
|
42
|
+
.replace(/&/g, '&')
|
|
43
|
+
.replace(/</g, '<')
|
|
44
|
+
.replace(/>/g, '>')
|
|
45
|
+
.replace(/"/g, '"');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseJson(raw, fallback = null) {
|
|
49
|
+
try { return raw ? JSON.parse(raw) : fallback; } catch { return fallback; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureWindowId(key) {
|
|
53
|
+
let id = sessionStorage.getItem(key);
|
|
54
|
+
if (!id) {
|
|
55
|
+
id = crypto.randomUUID();
|
|
56
|
+
sessionStorage.setItem(key, id);
|
|
57
|
+
}
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function scopedTabsKey(meta) {
|
|
62
|
+
return `${meta.tabsKeyPrefix}-${ensureWindowId(meta.windowKey)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeLabel(value) {
|
|
66
|
+
return String(value || '').trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isDefaultLabel(label, defaults) {
|
|
70
|
+
const normalized = normalizeLabel(label).toLowerCase();
|
|
71
|
+
return !normalized || defaults.includes(normalized);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasClaudeState(tab) {
|
|
75
|
+
const label = normalizeLabel(tab.label);
|
|
76
|
+
return !!(
|
|
77
|
+
tab.sessionId
|
|
78
|
+
|| tab.running
|
|
79
|
+
|| tab.sessionCost
|
|
80
|
+
|| tab.planFilePath
|
|
81
|
+
|| tab.queue?.length
|
|
82
|
+
|| !isDefaultLabel(label, ['new chat', 'new session', 'claude'])
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasCodexState(tab) {
|
|
87
|
+
const label = normalizeLabel(tab.pendingSessionLabel || tab.title || tab.sessionLabel);
|
|
88
|
+
const draft = normalizeLabel(tab.draft);
|
|
89
|
+
return !!(
|
|
90
|
+
tab.threadId
|
|
91
|
+
|| tab.running
|
|
92
|
+
|| tab.startingThread
|
|
93
|
+
|| draft
|
|
94
|
+
|| tab.planContent
|
|
95
|
+
|| tab.editedPlanContent
|
|
96
|
+
|| tab.showPostPlanActions
|
|
97
|
+
|| tab.planApprovalPending
|
|
98
|
+
|| tab.showPostCompactionPrompt
|
|
99
|
+
|| tab.postCompactionPending
|
|
100
|
+
|| tab.queue?.length
|
|
101
|
+
|| !isDefaultLabel(label, ['new session', 'saved session', 'untitled session', 'codex'])
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasOpenCodeState(tab) {
|
|
106
|
+
const label = normalizeLabel(tab.sessionTitle);
|
|
107
|
+
const draft = normalizeLabel(tab.draft);
|
|
108
|
+
return !!(
|
|
109
|
+
tab.sessionId
|
|
110
|
+
|| tab.running
|
|
111
|
+
|| draft
|
|
112
|
+
|| tab.planContent
|
|
113
|
+
|| tab.editedPlanContent
|
|
114
|
+
|| tab.showPostPlanActions
|
|
115
|
+
|| tab.threadTokenUsage
|
|
116
|
+
|| !isDefaultLabel(label, ['new session', 'opencode'])
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function tabLabel(provider, tab) {
|
|
121
|
+
if (provider === 'claude') return normalizeLabel(tab.label) || 'New chat';
|
|
122
|
+
if (provider === 'codex') return normalizeLabel(tab.pendingSessionLabel || tab.title || tab.sessionLabel) || 'New session';
|
|
123
|
+
return normalizeLabel(tab.sessionTitle || tab.title) || 'New session';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function tabId(provider, tab, index) {
|
|
127
|
+
if (tab.id) return String(tab.id);
|
|
128
|
+
if (provider === 'claude' && tab.sessionId) return String(tab.sessionId);
|
|
129
|
+
if (provider === 'codex' && tab.threadId) return String(tab.threadId);
|
|
130
|
+
if (provider === 'opencode' && tab.sessionId) return String(tab.sessionId);
|
|
131
|
+
return `${provider}-${index}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isRunning(tab) {
|
|
135
|
+
return !!(tab.running || tab.startingThread);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function providerPayload(provider) {
|
|
139
|
+
const meta = PROVIDERS[provider];
|
|
140
|
+
if (!meta) return null;
|
|
141
|
+
const key = scopedTabsKey(meta);
|
|
142
|
+
const data = parseJson(storage.getItem(key), null);
|
|
143
|
+
if (Array.isArray(data?.tabs) && data.tabs.length) {
|
|
144
|
+
return { key, activeIdx: Number(data.activeIdx) || 0, tabs: data.tabs };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (provider === 'claude') {
|
|
148
|
+
const legacy = parseJson(storage.getItem('synabun-claude-panel-tabs'), null);
|
|
149
|
+
if (Array.isArray(legacy?.tabs) && legacy.tabs.length) {
|
|
150
|
+
return { key: 'synabun-claude-panel-tabs', activeIdx: Number(legacy.activeIdx) || 0, tabs: legacy.tabs };
|
|
151
|
+
}
|
|
152
|
+
const sessionId = storage.getItem('synabun-claude-panel-session');
|
|
153
|
+
if (sessionId) {
|
|
154
|
+
const label = storage.getItem(`synabun-session-label:${sessionId}`) || `${sessionId.slice(0, 8)}...`;
|
|
155
|
+
return { key: 'synabun-claude-panel-session', activeIdx: 0, tabs: [{ sessionId, label }], legacySingle: true };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (provider === 'codex') {
|
|
160
|
+
const threadId = storage.getItem('synabun-codex-panel-thread');
|
|
161
|
+
const title = storage.getItem('synabun-codex-panel-title') || '';
|
|
162
|
+
if (threadId || title) {
|
|
163
|
+
return {
|
|
164
|
+
key: 'synabun-codex-panel-thread',
|
|
165
|
+
activeIdx: 0,
|
|
166
|
+
tabs: [{
|
|
167
|
+
id: 'legacy-codex-thread',
|
|
168
|
+
threadId,
|
|
169
|
+
title,
|
|
170
|
+
project: storage.getItem('synabun-codex-panel-project') || '',
|
|
171
|
+
}],
|
|
172
|
+
legacySingle: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function meaningfulTabs(provider, payload) {
|
|
181
|
+
const predicate = provider === 'claude'
|
|
182
|
+
? hasClaudeState
|
|
183
|
+
: provider === 'codex'
|
|
184
|
+
? hasCodexState
|
|
185
|
+
: hasOpenCodeState;
|
|
186
|
+
return (payload?.tabs || [])
|
|
187
|
+
.map((tab, index) => ({ tab, index }))
|
|
188
|
+
.filter(({ tab }) => predicate(tab));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function removePlaceholders(provider = '') {
|
|
192
|
+
const selector = provider
|
|
193
|
+
? `.${PLACEHOLDER_CLASS}[data-sidepanel-provider="${provider}"]`
|
|
194
|
+
: `.${PLACEHOLDER_CLASS}`;
|
|
195
|
+
document.querySelectorAll(selector).forEach((node) => node.remove());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeProviderPayload(provider, payload, removeIndex) {
|
|
199
|
+
if (!payload || removeIndex < 0) return;
|
|
200
|
+
if (payload.legacySingle) {
|
|
201
|
+
if (provider === 'claude') storage.removeItem('synabun-claude-panel-session');
|
|
202
|
+
if (provider === 'codex') {
|
|
203
|
+
storage.removeItem('synabun-codex-panel-thread');
|
|
204
|
+
storage.removeItem('synabun-codex-panel-title');
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const tabs = payload.tabs.filter((_, index) => index !== removeIndex);
|
|
210
|
+
if (!tabs.length) {
|
|
211
|
+
storage.removeItem(payload.key);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const activeIdx = Math.min(
|
|
215
|
+
Math.max((Number(payload.activeIdx) || 0) - (removeIndex < payload.activeIdx ? 1 : 0), 0),
|
|
216
|
+
tabs.length - 1,
|
|
217
|
+
);
|
|
218
|
+
storage.setItem(payload.key, JSON.stringify({ activeIdx, tabs }));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function openProvider(provider, tabIdValue) {
|
|
222
|
+
if (provider === 'claude') {
|
|
223
|
+
if (!isClaudePanelOpen()) await toggleClaudePanel();
|
|
224
|
+
} else if (provider === 'codex') {
|
|
225
|
+
if (!isCodexPanelOpen()) await toggleCodexPanel();
|
|
226
|
+
} else if (provider === 'opencode') {
|
|
227
|
+
if (!isOpencodePanelOpen()) await toggleOpencodePanel();
|
|
228
|
+
}
|
|
229
|
+
emit(PROVIDERS[provider]?.eventName, { tabId: tabIdValue });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function createPlaceholder(provider, payload, tab, index) {
|
|
233
|
+
const tray = document.getElementById('term-minimized-tray');
|
|
234
|
+
const meta = PROVIDERS[provider];
|
|
235
|
+
if (!tray || !meta) return null;
|
|
236
|
+
const providerMeta = getProviderMeta(meta.providerId);
|
|
237
|
+
const id = tabId(provider, tab, index);
|
|
238
|
+
const pill = document.createElement('div');
|
|
239
|
+
pill.className = `term-minimized-pill ${meta.pillClass} ${PLACEHOLDER_CLASS}`;
|
|
240
|
+
pill.dataset.sidepanelProvider = provider;
|
|
241
|
+
pill.dataset.tabId = id;
|
|
242
|
+
pill.innerHTML = `
|
|
243
|
+
<span class="term-minimized-pill-icon" style="color:${providerMeta.color}">${providerMeta.icon}</span>
|
|
244
|
+
<span class="term-minimized-pill-label">${esc(tabLabel(provider, tab))}</span>
|
|
245
|
+
<button class="term-minimized-pill-close" data-tooltip="Close">×</button>
|
|
246
|
+
`;
|
|
247
|
+
pill.classList.toggle(`${meta.pillClass.replace('-session-pill', '')}-pill-running`, isRunning(tab));
|
|
248
|
+
pill.addEventListener('click', () => openProvider(provider, id));
|
|
249
|
+
pill.querySelector('.term-minimized-pill-close')?.addEventListener('click', (event) => {
|
|
250
|
+
event.stopPropagation();
|
|
251
|
+
writeProviderPayload(provider, payload, index);
|
|
252
|
+
pill.remove();
|
|
253
|
+
renderSidepanelTrayPlaceholders();
|
|
254
|
+
});
|
|
255
|
+
tray.appendChild(pill);
|
|
256
|
+
return pill;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function renderSidepanelTrayPlaceholders() {
|
|
260
|
+
for (const provider of Object.keys(PROVIDERS)) {
|
|
261
|
+
if (_loadedProviders.has(provider)) {
|
|
262
|
+
removePlaceholders(provider);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
removePlaceholders(provider);
|
|
266
|
+
const payload = providerPayload(provider);
|
|
267
|
+
for (const { tab, index } of meaningfulTabs(provider, payload)) {
|
|
268
|
+
createPlaceholder(provider, payload, tab, index);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function markSidepanelTrayProviderLoaded(provider) {
|
|
274
|
+
if (!PROVIDERS[provider]) return;
|
|
275
|
+
_loadedProviders.add(provider);
|
|
276
|
+
removePlaceholders(provider);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function initSidepanelTrayPlaceholders() {
|
|
280
|
+
if (_initialized) return;
|
|
281
|
+
_initialized = true;
|
|
282
|
+
window.addEventListener('sidepanel-tray:provider-loaded', (event) => {
|
|
283
|
+
markSidepanelTrayProviderLoaded(event.detail?.provider);
|
|
284
|
+
});
|
|
285
|
+
window.addEventListener('sidepanel-tray:refresh', renderSidepanelTrayPlaceholders);
|
|
286
|
+
renderSidepanelTrayPlaceholders();
|
|
287
|
+
}
|
|
@@ -79,11 +79,15 @@ const SYNC_HANDLERS = {
|
|
|
79
79
|
'schedule:fired': (msg) => emit('sync:schedule:fired', msg),
|
|
80
80
|
'schedule:completed': (msg) => emit('sync:schedule:completed', msg),
|
|
81
81
|
'schedule:failed': (msg) => emit('sync:schedule:failed', msg),
|
|
82
|
-
'schedule:timer-set': (msg) => emit('sync:schedule:timer-set', msg),
|
|
83
|
-
'schedule:timer-fired': (msg) => emit('sync:schedule:timer-fired', msg),
|
|
84
|
-
'schedule:timer-cancelled': (msg) => emit('sync:schedule:timer-cancelled', msg),
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
'schedule:timer-set': (msg) => emit('sync:schedule:timer-set', msg),
|
|
83
|
+
'schedule:timer-fired': (msg) => emit('sync:schedule:timer-fired', msg),
|
|
84
|
+
'schedule:timer-cancelled': (msg) => emit('sync:schedule:timer-cancelled', msg),
|
|
85
|
+
'schedule-group:created': (msg) => emit('sync:schedule-group:created', msg),
|
|
86
|
+
'schedule-group:updated': (msg) => emit('sync:schedule-group:updated', msg),
|
|
87
|
+
'schedule-group:deleted': (msg) => emit('sync:schedule-group:deleted', msg),
|
|
88
|
+
'schedule-group:reordered': (msg) => emit('sync:schedule-group:reordered', msg),
|
|
89
|
+
|
|
90
|
+
// Quick timer events
|
|
87
91
|
'quick-timer:set': (msg) => emit('sync:quick-timer:set', msg),
|
|
88
92
|
'quick-timer:fired': (msg) => emit('sync:quick-timer:fired', msg),
|
|
89
93
|
'quick-timer:fired-now': (msg) => emit('sync:quick-timer:fired-now', msg),
|
|
@@ -682,6 +682,41 @@ function _sendResize(session) {
|
|
|
682
682
|
} catch {}
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
+
const TERMINAL_WRITE_CHUNK = 64 * 1024;
|
|
686
|
+
|
|
687
|
+
function _createTerminalWriter(term) {
|
|
688
|
+
const queue = [];
|
|
689
|
+
let writing = false;
|
|
690
|
+
let disposed = false;
|
|
691
|
+
|
|
692
|
+
const pump = () => {
|
|
693
|
+
if (disposed || writing || !queue.length) return;
|
|
694
|
+
let chunk = queue.shift();
|
|
695
|
+
if (chunk.length > TERMINAL_WRITE_CHUNK) {
|
|
696
|
+
queue.unshift(chunk.slice(TERMINAL_WRITE_CHUNK));
|
|
697
|
+
chunk = chunk.slice(0, TERMINAL_WRITE_CHUNK);
|
|
698
|
+
}
|
|
699
|
+
writing = true;
|
|
700
|
+
try {
|
|
701
|
+
term.write(chunk, () => {
|
|
702
|
+
writing = false;
|
|
703
|
+
queueMicrotask(pump);
|
|
704
|
+
});
|
|
705
|
+
} catch {
|
|
706
|
+
writing = false;
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const write = (data) => {
|
|
711
|
+
if (disposed || data == null || data === '') return;
|
|
712
|
+
queue.push(String(data));
|
|
713
|
+
pump();
|
|
714
|
+
};
|
|
715
|
+
write.clear = () => { queue.length = 0; writing = false; };
|
|
716
|
+
write.dispose = () => { disposed = true; queue.length = 0; };
|
|
717
|
+
return write;
|
|
718
|
+
}
|
|
719
|
+
|
|
685
720
|
|
|
686
721
|
// ── Session registry (persists across page refresh) ──
|
|
687
722
|
|
|
@@ -856,23 +891,24 @@ async function loadXterm() {
|
|
|
856
891
|
_xtermCSS = true;
|
|
857
892
|
}
|
|
858
893
|
|
|
859
|
-
// Dynamic ESM imports from CDN
|
|
894
|
+
// Dynamic ESM imports from CDN. Intentionally avoid addon-webgl/addon-canvas
|
|
895
|
+
// for CLI/TUI sessions: multiple long-lived terminals can exhaust/corrupt
|
|
896
|
+
// glyph atlases or WebGL contexts. xterm's DOM renderer is slower, but it is
|
|
897
|
+
// the most deterministic renderer for agent TUIs.
|
|
860
898
|
try {
|
|
861
|
-
const [xtermMod, fitMod, linksMod, searchMod,
|
|
899
|
+
const [xtermMod, fitMod, linksMod, searchMod, unicodeMod] = await Promise.all([
|
|
862
900
|
import('https://esm.sh/@xterm/xterm@5.5.0'),
|
|
863
901
|
import('https://esm.sh/@xterm/addon-fit@0.10.0'),
|
|
864
902
|
import('https://esm.sh/@xterm/addon-web-links@0.11.0'),
|
|
865
903
|
import('https://esm.sh/@xterm/addon-search@0.15.0'),
|
|
866
|
-
import('https://esm.sh/@xterm/addon-webgl@0.18.0').catch(() => null),
|
|
867
|
-
import('https://esm.sh/@xterm/addon-canvas@0.7.0').catch(() => null),
|
|
868
904
|
import('https://esm.sh/@xterm/addon-unicode11@0.8.0'),
|
|
869
905
|
]);
|
|
870
906
|
_Terminal = xtermMod.Terminal;
|
|
871
907
|
_FitAddon = fitMod.FitAddon;
|
|
872
908
|
_WebLinksAddon = linksMod.WebLinksAddon;
|
|
873
909
|
_SearchAddon = searchMod.SearchAddon;
|
|
874
|
-
_WebglAddon =
|
|
875
|
-
_CanvasAddon =
|
|
910
|
+
_WebglAddon = null;
|
|
911
|
+
_CanvasAddon = null;
|
|
876
912
|
_Unicode11Addon = unicodeMod.Unicode11Addon;
|
|
877
913
|
} catch (err) {
|
|
878
914
|
// Show error in terminal container if it exists, or throw
|
|
@@ -1608,6 +1644,7 @@ async function openSession(profile, cwd, existingSessionId, options = {}) {
|
|
|
1608
1644
|
// and TUI layouts (box-drawing, spinners) are permanently misaligned.
|
|
1609
1645
|
if (document.fonts?.ready) await document.fonts.ready;
|
|
1610
1646
|
term.open(viewport);
|
|
1647
|
+
const writeTerminal = _createTerminalWriter(term);
|
|
1611
1648
|
|
|
1612
1649
|
// Find xterm's internal textarea (used for paste handler below, after ws is created)
|
|
1613
1650
|
const xtermTextarea = viewport.querySelector('.xterm-helper-textarea');
|
|
@@ -1832,14 +1869,15 @@ async function openSession(profile, cwd, existingSessionId, options = {}) {
|
|
|
1832
1869
|
ws.onmessage = (e) => {
|
|
1833
1870
|
try {
|
|
1834
1871
|
const msg = JSON.parse(e.data);
|
|
1835
|
-
if (msg.type === 'output' || msg.type === 'replay') {
|
|
1836
|
-
|
|
1872
|
+
if (msg.type === 'output' || msg.type === 'replay' || msg.type === 'snapshot') {
|
|
1873
|
+
writeTerminal(msg.data);
|
|
1874
|
+
const replayData = (msg.type === 'snapshot' && msg.plain) ? msg.plain : msg.data;
|
|
1837
1875
|
if (NOTIF_TRACKED_CLI_PROFILES.has(profile)) _scheduleCliStatusCheck(sessionId);
|
|
1838
1876
|
// Accumulate output for _sendOnceReady replay buffer check
|
|
1839
1877
|
const sess = _sessions.find(s => s.id === sessionId);
|
|
1840
1878
|
if (sess) {
|
|
1841
1879
|
if (!sess._replayBuf) sess._replayBuf = '';
|
|
1842
|
-
sess._replayBuf +=
|
|
1880
|
+
sess._replayBuf += replayData;
|
|
1843
1881
|
if (sess._replayBuf.length > 16384) sess._replayBuf = sess._replayBuf.slice(-8192);
|
|
1844
1882
|
}
|
|
1845
1883
|
}
|
|
@@ -1850,7 +1888,7 @@ async function openSession(profile, cwd, existingSessionId, options = {}) {
|
|
|
1850
1888
|
}
|
|
1851
1889
|
if (msg.type === 'error') {
|
|
1852
1890
|
if (msg.message === 'Session not found') { markSessionDead(sessionId); return; }
|
|
1853
|
-
|
|
1891
|
+
writeTerminal(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`);
|
|
1854
1892
|
}
|
|
1855
1893
|
if (msg.type === 'image_saved' && msg.path) {
|
|
1856
1894
|
_clipCopy(msg.path);
|
|
@@ -1903,6 +1941,7 @@ async function openSession(profile, cwd, existingSessionId, options = {}) {
|
|
|
1903
1941
|
cwd: cwd || null,
|
|
1904
1942
|
label: options.label || (cwdLabel ? `${profileDef?.label || profile} · ${cwdLabel}` : (profileDef?.label || profile)),
|
|
1905
1943
|
term, fitAddon, searchAddon, ws, viewport, ro,
|
|
1944
|
+
_terminalWriter: writeTerminal,
|
|
1906
1945
|
renderer,
|
|
1907
1946
|
_webgl: webgl, // GPU renderer handle — disposed before term.dispose() on close
|
|
1908
1947
|
dead: false,
|
|
@@ -2485,6 +2524,7 @@ async function reconnectSession(sessionId, profile, options = {}) {
|
|
|
2485
2524
|
// Wait for fonts before opening (same FOUT fix as openSession)
|
|
2486
2525
|
if (document.fonts?.ready) await document.fonts.ready;
|
|
2487
2526
|
term.open(viewport);
|
|
2527
|
+
const writeTerminal = _createTerminalWriter(term);
|
|
2488
2528
|
|
|
2489
2529
|
// Find xterm's internal textarea (used for paste handler below, after ws is created)
|
|
2490
2530
|
const xtermTextarea = viewport.querySelector('.xterm-helper-textarea');
|
|
@@ -2659,13 +2699,14 @@ async function reconnectSession(sessionId, profile, options = {}) {
|
|
|
2659
2699
|
ws.onmessage = (e) => {
|
|
2660
2700
|
try {
|
|
2661
2701
|
const msg = JSON.parse(e.data);
|
|
2662
|
-
if (msg.type === 'output' || msg.type === 'replay') {
|
|
2663
|
-
|
|
2702
|
+
if (msg.type === 'output' || msg.type === 'replay' || msg.type === 'snapshot') {
|
|
2703
|
+
writeTerminal(msg.data);
|
|
2704
|
+
const replayData = (msg.type === 'snapshot' && msg.plain) ? msg.plain : msg.data;
|
|
2664
2705
|
// Accumulate output for _sendOnceReady replay buffer check
|
|
2665
2706
|
const sess = _sessions.find(s => s.id === sessionId);
|
|
2666
2707
|
if (sess) {
|
|
2667
2708
|
if (!sess._replayBuf) sess._replayBuf = '';
|
|
2668
|
-
sess._replayBuf +=
|
|
2709
|
+
sess._replayBuf += replayData;
|
|
2669
2710
|
if (sess._replayBuf.length > 16384) sess._replayBuf = sess._replayBuf.slice(-8192);
|
|
2670
2711
|
}
|
|
2671
2712
|
}
|
|
@@ -2679,7 +2720,7 @@ async function reconnectSession(sessionId, profile, options = {}) {
|
|
|
2679
2720
|
markSessionDead(sessionId);
|
|
2680
2721
|
return;
|
|
2681
2722
|
}
|
|
2682
|
-
|
|
2723
|
+
writeTerminal(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`);
|
|
2683
2724
|
}
|
|
2684
2725
|
if (msg.type === 'image_saved' && msg.path) {
|
|
2685
2726
|
_clipCopy(msg.path);
|
|
@@ -2726,6 +2767,7 @@ async function reconnectSession(sessionId, profile, options = {}) {
|
|
|
2726
2767
|
cwd: options.cwd || null,
|
|
2727
2768
|
label: options.label || (profileDef?.label || profile),
|
|
2728
2769
|
term, fitAddon, searchAddon, ws, viewport, ro,
|
|
2770
|
+
_terminalWriter: writeTerminal,
|
|
2729
2771
|
renderer,
|
|
2730
2772
|
_webgl: webgl, // GPU renderer handle — disposed before term.dispose() on close
|
|
2731
2773
|
dead: false,
|
|
@@ -2825,8 +2867,9 @@ async function closeSession(idx) {
|
|
|
2825
2867
|
if (session.ro) session.ro.disconnect();
|
|
2826
2868
|
if (session._frameRenderer) session._frameRenderer.destroy();
|
|
2827
2869
|
if (session.ws) session.ws.close();
|
|
2828
|
-
|
|
2829
|
-
//
|
|
2870
|
+
if (session._terminalWriter) session._terminalWriter.dispose();
|
|
2871
|
+
// Historical sessions may still have a WebGL addon handle from older code;
|
|
2872
|
+
// dispose it before term.dispose() so GPU resources are released.
|
|
2830
2873
|
if (session._webgl) { try { session._webgl.dispose(); } catch {} session._webgl = null; }
|
|
2831
2874
|
if (session.term) session.term.dispose();
|
|
2832
2875
|
if (session._visibilityHandler) document.removeEventListener('visibilitychange', session._visibilityHandler);
|
|
@@ -2930,6 +2973,7 @@ export function disconnectAllSessions() {
|
|
|
2930
2973
|
session.dead = true;
|
|
2931
2974
|
if (session.ro) session.ro.disconnect();
|
|
2932
2975
|
session.ws.close();
|
|
2976
|
+
if (session._terminalWriter) session._terminalWriter.dispose();
|
|
2933
2977
|
if (session._webgl) { try { session._webgl.dispose(); } catch {} session._webgl = null; }
|
|
2934
2978
|
if (session.term) session.term.dispose();
|
|
2935
2979
|
session.viewport.remove();
|
|
@@ -2987,10 +3031,8 @@ function _reconnectTerminalWs(session, attempt = 0) {
|
|
|
2987
3031
|
ws.onopen = () => {
|
|
2988
3032
|
session._reconnecting = false;
|
|
2989
3033
|
session.ws = ws;
|
|
2990
|
-
// Server
|
|
2991
|
-
//
|
|
2992
|
-
// replay, producing a visibly duplicated banner. Clear the screen and
|
|
2993
|
-
// scrollback so the replay paints onto a clean slate.
|
|
3034
|
+
// Server restores reconnects with a terminal-state snapshot. Clear any
|
|
3035
|
+
// stale queued writes so the snapshot becomes the new canonical screen.
|
|
2994
3036
|
session._needsReplayReset = true;
|
|
2995
3037
|
session._replayBuf = '';
|
|
2996
3038
|
if (session.term) {
|
|
@@ -3007,15 +3049,16 @@ function _reconnectTerminalWs(session, attempt = 0) {
|
|
|
3007
3049
|
if (session.ws !== ws) return;
|
|
3008
3050
|
try {
|
|
3009
3051
|
const msg = JSON.parse(e.data);
|
|
3010
|
-
if (msg.type === 'output' || msg.type === 'replay') {
|
|
3011
|
-
if (msg.type === 'replay' && session._needsReplayReset) {
|
|
3052
|
+
if (msg.type === 'output' || msg.type === 'replay' || msg.type === 'snapshot') {
|
|
3053
|
+
if ((msg.type === 'replay' || msg.type === 'snapshot') && session._needsReplayReset) {
|
|
3012
3054
|
session._needsReplayReset = false;
|
|
3013
3055
|
if (session.term) {
|
|
3014
|
-
try { session.term.reset(); } catch {}
|
|
3056
|
+
try { session._terminalWriter?.clear?.(); session.term.reset(); } catch {}
|
|
3015
3057
|
}
|
|
3016
3058
|
}
|
|
3017
3059
|
if (session.term) {
|
|
3018
|
-
session.
|
|
3060
|
+
if (session._terminalWriter) session._terminalWriter(msg.data);
|
|
3061
|
+
else session.term.write(msg.data);
|
|
3019
3062
|
if (NOTIF_TRACKED_CLI_PROFILES.has(session.profile)) _scheduleCliStatusCheck(session.id);
|
|
3020
3063
|
}
|
|
3021
3064
|
}
|
|
@@ -3025,7 +3068,8 @@ function _reconnectTerminalWs(session, attempt = 0) {
|
|
|
3025
3068
|
}
|
|
3026
3069
|
if (msg.type === 'error') {
|
|
3027
3070
|
if (msg.message === 'Session not found') { markSessionDead(session.id); return; }
|
|
3028
|
-
if (session.
|
|
3071
|
+
if (session._terminalWriter) session._terminalWriter(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`);
|
|
3072
|
+
else if (session.term) session.term.write(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`);
|
|
3029
3073
|
}
|
|
3030
3074
|
if (msg.type === 'image_saved' && msg.path) {
|
|
3031
3075
|
_clipCopy(msg.path);
|
|
@@ -5089,6 +5133,11 @@ async function attachDetached(terminalSessionId, profile, initialMessage, autoSu
|
|
|
5089
5133
|
_pendingSessionIds.delete(terminalSessionId);
|
|
5090
5134
|
console.error('[SynaBun] attachDetached failed:', err);
|
|
5091
5135
|
showTermToast(`Failed to attach terminal: ${err.message || 'unknown error'}`);
|
|
5136
|
+
const termContainer = $('term-container');
|
|
5137
|
+
const termHidden = !termContainer || termContainer.offsetParent === null;
|
|
5138
|
+
if (termHidden) {
|
|
5139
|
+
try { alert(`[SynaBun] Launch failed: ${err.message || 'unknown error'}\n\nOpen DevTools Console for stack trace.`); } catch {}
|
|
5140
|
+
}
|
|
5092
5141
|
}
|
|
5093
5142
|
}
|
|
5094
5143
|
|
|
@@ -5187,8 +5236,8 @@ function _sendOnceReady(session, message, autoSubmit) {
|
|
|
5187
5236
|
function onMessage(e) {
|
|
5188
5237
|
try {
|
|
5189
5238
|
const msg = JSON.parse(e.data);
|
|
5190
|
-
if (msg.type === 'output' || msg.type === 'replay') {
|
|
5191
|
-
_outputBuf += msg.data;
|
|
5239
|
+
if (msg.type === 'output' || msg.type === 'replay' || msg.type === 'snapshot') {
|
|
5240
|
+
_outputBuf += (msg.type === 'snapshot' && msg.plain) ? msg.plain : msg.data;
|
|
5192
5241
|
// Strip ANSI codes before matching — CLI prompts are colorized
|
|
5193
5242
|
const clean = _outputBuf.replace(ANSI_RE, '');
|
|
5194
5243
|
if (READY_PATTERNS.some(p => p.test(clean))) {
|
|
@@ -53,22 +53,56 @@ function runUpdate(key, badge) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
emit('terminal:run-command', { command: cmd, label: `Update ${TOOL_LABELS[key] || key}` });
|
|
56
|
-
// Optimistically clear
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
// Poll for the new installed version after the install completes.
|
|
66
|
-
// npm-global installs typically take 5–45s; opencode upgrade similar.
|
|
67
|
-
// Re-check a few times, bail once the badge is cleared or retries run out.
|
|
68
|
-
const delays = [20000, 45000, 90000];
|
|
69
|
-
for (const ms of delays) {
|
|
70
|
-
setTimeout(() => { forceCheckToolUpdates(); }, ms);
|
|
56
|
+
// Optimistically clear BOTH toolbar + menu badges and restore parent tooltips.
|
|
57
|
+
const ids = TOOL_ELEMENTS[key];
|
|
58
|
+
const parents = PARENT_ELEMENTS[key];
|
|
59
|
+
for (const which of ['toolbar', 'menu']) {
|
|
60
|
+
const b = ids?.[which] && $(ids[which]);
|
|
61
|
+
if (b) b.textContent = '';
|
|
62
|
+
const pBtn = parents?.[which] && $(parents[which]);
|
|
63
|
+
if (pBtn?.dataset.tooltipOriginal) pBtn.dataset.tooltip = pBtn.dataset.tooltipOriginal;
|
|
71
64
|
}
|
|
65
|
+
// Poll repeatedly until the server confirms the new version is installed
|
|
66
|
+
// (updateAvailable flips to false for this tool) or we hit the max window.
|
|
67
|
+
// Native installer updates can take 1–3min; npm-global 5–60s.
|
|
68
|
+
schedulePostUpdatePolling(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Poll /api/system/tool-versions?force=1 until the specified tool reports
|
|
72
|
+
// updateAvailable=false, or a max window elapses. Safe to call repeatedly —
|
|
73
|
+
// latest call supersedes any prior poll loop for the same tool.
|
|
74
|
+
const _pollTimers = new Map();
|
|
75
|
+
function schedulePostUpdatePolling(key) {
|
|
76
|
+
const prior = _pollTimers.get(key);
|
|
77
|
+
if (prior) clearTimeout(prior);
|
|
78
|
+
|
|
79
|
+
const startedAt = Date.now();
|
|
80
|
+
const maxMs = 10 * 60 * 1000;
|
|
81
|
+
const initialDelay = 10000;
|
|
82
|
+
const interval = 10000;
|
|
83
|
+
|
|
84
|
+
const tick = async () => {
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch('/api/system/tool-versions?force=1');
|
|
87
|
+
if (res.ok) {
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
applyBadges(data);
|
|
90
|
+
const info = data?.tools?.[key];
|
|
91
|
+
if (info && !info.updateAvailable) {
|
|
92
|
+
_pollTimers.delete(key);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch { /* ignore, will retry */ }
|
|
97
|
+
|
|
98
|
+
if (Date.now() - startedAt >= maxMs) {
|
|
99
|
+
_pollTimers.delete(key);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
_pollTimers.set(key, setTimeout(tick, interval));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
_pollTimers.set(key, setTimeout(tick, initialDelay));
|
|
72
106
|
}
|
|
73
107
|
|
|
74
108
|
function wireParentIntercept(parentId, badgeId, key) {
|
|
@@ -483,8 +483,10 @@ export const TUTORIAL_STEPS = [
|
|
|
483
483
|
{ id: 'claude-haiku-4-5', label: 'Haiku 4.5', desc: 'Fastest', tier: 'light' },
|
|
484
484
|
],
|
|
485
485
|
'codex': [
|
|
486
|
-
{ id: '
|
|
487
|
-
{ id: '
|
|
486
|
+
{ id: 'gpt-5.5', label: 'GPT-5.5', desc: 'Most capable', tier: 'top' },
|
|
487
|
+
{ id: 'gpt-5.4', label: 'GPT-5.4', desc: 'Strong general' },
|
|
488
|
+
{ id: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', desc: 'Fast & cheap', tier: 'default' },
|
|
489
|
+
{ id: 'gpt-5.3-codex', label: 'GPT-5.3 Codex', desc: 'Codex-tuned' },
|
|
488
490
|
],
|
|
489
491
|
'gemini': [
|
|
490
492
|
{ id: 'gemini-2.5-pro', label: '2.5 Pro', desc: 'Most capable', tier: 'default' },
|