synabun 2.0.0-beta → 2.0.0
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 +8 -0
- package/THIRD-PARTY-LICENSES.md +5 -0
- package/launcher.mjs +40 -0
- package/lib/claude-hooks.js +61 -8
- package/lib/client-config-repair.js +4 -3
- package/lib/data-home-migration.js +4 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +5 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/start-launcher.js +1309 -0
- package/neural-interface/i18n/en.json +41 -2
- package/neural-interface/i18n/pt-BR.json +41 -2
- package/neural-interface/lib/assistant-envelope.js +9 -6
- package/neural-interface/lib/augmented-path.js +23 -3
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/claude-agent-bridge.js +54 -147
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-model-catalog.js +52 -91
- package/neural-interface/lib/native-binary-runtime.js +69 -337
- package/neural-interface/lib/native-loop-providers.js +30 -44
- package/neural-interface/lib/offline-page.js +54 -0
- package/neural-interface/lib/session-title-generator.js +5 -0
- package/neural-interface/lib/synabun-update-plan.js +19 -0
- package/neural-interface/package-lock.json +0 -251
- package/neural-interface/public/install-app.html +31 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/offline.html +401 -66
- package/neural-interface/public/onboarding.html +252 -146
- package/neural-interface/public/shared/assistant/asst-mascot.js +12 -10
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
- package/neural-interface/public/shared/assistant/asst-render.js +58 -49
- package/neural-interface/public/shared/assistant/asst-styles.js +4 -2
- package/neural-interface/public/shared/command-runner-model.js +30 -0
- package/neural-interface/public/shared/html-shell.js +9 -5
- package/neural-interface/public/shared/pwa-install-page.js +34 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +28 -0
- package/neural-interface/public/shared/settings/settings-rules.js +160 -0
- package/neural-interface/public/shared/start-bridge.js +583 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
- package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
- package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
- package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
- package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
- package/neural-interface/public/shared/styles.css +801 -392
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/ui-claude-panel.js +3 -3
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-loading.js +221 -10
- package/neural-interface/public/shared/ui-menubar.js +11 -27
- package/neural-interface/public/shared/ui-settings.js +36 -59
- package/neural-interface/public/shared/ui-terminal.js +130 -144
- package/neural-interface/public/sw.js +3 -1
- package/neural-interface/public/variant/2d/main.js +2 -1
- package/neural-interface/public/variant/3d/main.js +2 -1
- package/neural-interface/scripts/rebuild-pty.js +15 -12
- package/neural-interface/server.js +220 -185
- package/package.json +13 -1
- package/postinstall.js +11 -81
- package/setup.js +72 -4
- package/docs/browser-v2.md +0 -109
- package/docs/hook-integrity.md +0 -89
- package/docs/judgments.md +0 -182
- package/docs/style-guide.md +0 -246
- package/scripts/audit-claude-hook-usage.mjs +0 -12
- package/scripts/audit-memory-duplicates.mjs +0 -44
- package/scripts/measure-claude-hook-overhead.mjs +0 -77
- package/scripts/recover-user-config.mjs +0 -254
|
@@ -16,6 +16,7 @@ import { getWhiteboardElementById } from './ui-whiteboard.js';
|
|
|
16
16
|
import { createFrameRenderer } from './utils.js';
|
|
17
17
|
import { notify, NOTIF_TYPE } from './ui-notifications.js';
|
|
18
18
|
import { getProviderMeta } from './provider-icons.js';
|
|
19
|
+
import { readinessKind, typeWhenReady } from './term-ready.js';
|
|
19
20
|
import { initRendererManager, acquireRenderer, disposeRenderer, hasWebgl, noteFocus, adoptRenderer, rendererState, rendererStats } from './term-renderer.js';
|
|
20
21
|
import { mountAssistant } from './assistant/asst-panel.js';
|
|
21
22
|
import { getAssistantSession } from './assistant/asst-api.js';
|
|
@@ -1424,10 +1425,22 @@ function _accumReplay(sess, text) {
|
|
|
1424
1425
|
if (sess._replayBuf.length > 16384) sess._replayBuf = sess._replayBuf.slice(-8192);
|
|
1425
1426
|
}
|
|
1426
1427
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1428
|
+
// `replaced`: a snapshot just became the whole screen (connect / reconnect),
|
|
1429
|
+
// so `text` is everything there is rather than more of the stream.
|
|
1430
|
+
function _notifyOutputWatchers(sess, text, replaced = false) {
|
|
1431
|
+
if (!sess?._outputWatchers?.size) return;
|
|
1432
|
+
if (!text && !replaced) return;
|
|
1429
1433
|
for (const w of [...sess._outputWatchers]) {
|
|
1430
|
-
try { w(text); } catch {}
|
|
1434
|
+
try { w(text || '', replaced); } catch {}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// A socket of this session opened (first connect or a reconnect). The wait for
|
|
1439
|
+
// readiness in _sendOnceReady starts its clock here, not when it was asked.
|
|
1440
|
+
function _notifySocketOpen(sess) {
|
|
1441
|
+
if (!sess?._openWatchers?.size) return;
|
|
1442
|
+
for (const w of [...sess._openWatchers]) {
|
|
1443
|
+
try { w(); } catch {}
|
|
1431
1444
|
}
|
|
1432
1445
|
}
|
|
1433
1446
|
|
|
@@ -1500,7 +1513,7 @@ function _handleTermWsEvent(e, ctx) {
|
|
|
1500
1513
|
else term?.write(msg.data);
|
|
1501
1514
|
}
|
|
1502
1515
|
if (NOTIF_TRACKED_CLI_PROFILES.has(profile)) _scheduleCliStatusCheck(sessionId);
|
|
1503
|
-
_notifyOutputWatchers(sess, text);
|
|
1516
|
+
_notifyOutputWatchers(sess, text, !!wantsReset);
|
|
1504
1517
|
}
|
|
1505
1518
|
if (msg.type === 'exit') {
|
|
1506
1519
|
if (sess) sess._exitReceived = true;
|
|
@@ -1538,6 +1551,7 @@ function _onTermWsOpen(sessionId, ws, fallback = null) {
|
|
|
1538
1551
|
try { (s?._terminalWriter || fallback?._terminalWriter)?.reset?.(); } catch {}
|
|
1539
1552
|
send({ type: 'hello', flow: 1 });
|
|
1540
1553
|
if (document.hidden) send({ type: 'visibility', visible: false });
|
|
1554
|
+
_notifySocketOpen(s);
|
|
1541
1555
|
const term = s?.term;
|
|
1542
1556
|
if (!term) return;
|
|
1543
1557
|
// Hidden container (restore, parked dock) → no resize now; _scheduleFit
|
|
@@ -3381,6 +3395,7 @@ async function reconnectBrowserSession(sessionId, liveData, saved) {
|
|
|
3381
3395
|
ws, viewport, ro, renderer: null,
|
|
3382
3396
|
dead: false, pinned: saved?.pinned || false,
|
|
3383
3397
|
_userRenamed: saved?.userRenamed || false,
|
|
3398
|
+
_floatColor: saved?.floatColor || null,
|
|
3384
3399
|
_isBrowser: true, _browserUrl: currentUrl, _browserTitle: liveData?.title || '',
|
|
3385
3400
|
_browserCanvas: canvas, _browserCtx: ctx, _frameRenderer: frameRenderer, _visibilityHandler,
|
|
3386
3401
|
_idleTimer: null, _screencastIdlePaused: false,
|
|
@@ -4488,13 +4503,29 @@ function _applyFloatColor(win, colorId) {
|
|
|
4488
4503
|
win.style.setProperty('--fh', String(c.h));
|
|
4489
4504
|
win.style.setProperty('--fs', c.s + '%');
|
|
4490
4505
|
}
|
|
4491
|
-
// Update color strip
|
|
4506
|
+
// Update color strip. Only the color: the `background` shorthand would reset
|
|
4507
|
+
// the stylesheet's background-clip, and the dot would fill its whole hit area.
|
|
4492
4508
|
const strip = win.querySelector('.term-float-color-strip');
|
|
4493
4509
|
if (strip) {
|
|
4494
|
-
strip.style.
|
|
4510
|
+
strip.style.backgroundColor = c.s === 0 ? 'rgba(255,255,255,0.15)' : `hsl(${c.h}, ${Math.round(c.s * 0.5)}%, 30%)`;
|
|
4495
4511
|
}
|
|
4496
4512
|
}
|
|
4497
4513
|
|
|
4514
|
+
/** Place the palette popup by the color button: above it, or below when there
|
|
4515
|
+
* is no room, with its first swatch over the dot and inside the viewport. */
|
|
4516
|
+
function _placeFloatColorPicker(picker, colorBtn) {
|
|
4517
|
+
const btn = colorBtn.getBoundingClientRect();
|
|
4518
|
+
const box = picker.getBoundingClientRect();
|
|
4519
|
+
const first = picker.firstElementChild?.getBoundingClientRect();
|
|
4520
|
+
const inset = first ? first.left + first.width / 2 - box.left : 0;
|
|
4521
|
+
const margin = 8, gap = 6;
|
|
4522
|
+
const left = btn.left + btn.width / 2 - inset;
|
|
4523
|
+
let top = btn.top - gap - box.height;
|
|
4524
|
+
if (top < margin) top = btn.bottom + gap;
|
|
4525
|
+
picker.style.left = Math.max(margin, Math.min(left, window.innerWidth - box.width - margin)) + 'px';
|
|
4526
|
+
picker.style.top = Math.max(margin, Math.min(top, window.innerHeight - box.height - margin)) + 'px';
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4498
4529
|
function getRightPanelReservedWidth(cs = getComputedStyle(document.documentElement)) {
|
|
4499
4530
|
return parseFloat(cs.getPropertyValue('--right-panel-width'))
|
|
4500
4531
|
|| parseFloat(cs.getPropertyValue('--claude-panel-width'))
|
|
@@ -4813,19 +4844,18 @@ function detachTab(idx, opts = {}) {
|
|
|
4813
4844
|
if (existing) { existing.remove(); return; }
|
|
4814
4845
|
const picker = document.createElement('div');
|
|
4815
4846
|
picker.className = 'float-color-picker';
|
|
4816
|
-
//
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
picker.style.bottom = (window.innerHeight - btnRect.top + 6) + 'px';
|
|
4820
|
-
picker.style.top = 'auto';
|
|
4847
|
+
// The session object may have been replaced by _pushSession after a
|
|
4848
|
+
// reconnection — read and write the color on the one in _sessions
|
|
4849
|
+
const live = () => _sessions.find(s => s.id === session.id) || session;
|
|
4821
4850
|
FLOAT_COLORS.forEach(c => {
|
|
4822
4851
|
const swatch = document.createElement('button');
|
|
4823
4852
|
swatch.className = 'float-color-swatch';
|
|
4824
|
-
if (c.id === (
|
|
4853
|
+
if (c.id === (live()._floatColor || 'blue')) swatch.classList.add('active');
|
|
4825
4854
|
swatch.style.background = c.s === 0 ? 'rgba(255,255,255,0.15)' : `hsl(${c.h}, ${Math.round(c.s * 0.5)}%, 25%)`;
|
|
4826
4855
|
swatch.addEventListener('click', (ev) => {
|
|
4827
4856
|
ev.stopPropagation();
|
|
4828
4857
|
session._floatColor = c.id;
|
|
4858
|
+
live()._floatColor = c.id;
|
|
4829
4859
|
_applyFloatColor(win, c.id);
|
|
4830
4860
|
saveSessionRegistry();
|
|
4831
4861
|
saveTerminalLayout();
|
|
@@ -4834,8 +4864,10 @@ function detachTab(idx, opts = {}) {
|
|
|
4834
4864
|
picker.appendChild(swatch);
|
|
4835
4865
|
});
|
|
4836
4866
|
document.body.appendChild(picker);
|
|
4837
|
-
|
|
4867
|
+
_placeFloatColorPicker(picker, colorBtn);
|
|
4868
|
+
// Close on outside click (the dot closes it through the toggle above)
|
|
4838
4869
|
const close = (ev) => {
|
|
4870
|
+
if (colorBtn.contains(ev.target) && picker.isConnected) return;
|
|
4839
4871
|
if (!picker.contains(ev.target)) { picker.remove(); document.removeEventListener('mousedown', close); }
|
|
4840
4872
|
};
|
|
4841
4873
|
setTimeout(() => document.addEventListener('mousedown', close), 0);
|
|
@@ -5722,6 +5754,8 @@ export async function initTerminal() {
|
|
|
5722
5754
|
window.addEventListener('keydown', (e) => {
|
|
5723
5755
|
if (e.key !== 'Escape') return;
|
|
5724
5756
|
const target = e.target;
|
|
5757
|
+
// The command library owns Escape for its dialogs, search and window.
|
|
5758
|
+
if (target?.closest?.('.command-runner-panel')) return;
|
|
5725
5759
|
let session = null;
|
|
5726
5760
|
if (target?.classList?.contains?.('xterm-helper-textarea')) {
|
|
5727
5761
|
const sid = target.closest('.term-viewport')?.dataset?.sessionId;
|
|
@@ -6337,8 +6371,19 @@ async function launchDetached(profile, initialMessage, autoSubmit, opts = {}) {
|
|
|
6337
6371
|
async function runCommandInNewTab({ command, cwd, label }) {
|
|
6338
6372
|
if (!command) return;
|
|
6339
6373
|
await _whenOpenIdle();
|
|
6340
|
-
|
|
6341
|
-
if (!session)
|
|
6374
|
+
let session = await openSession('shell', cwd || null);
|
|
6375
|
+
if (!session) {
|
|
6376
|
+
// Another open slipped in between the queue and this call (openSession
|
|
6377
|
+
// returns null instead of stacking): take one more turn rather than
|
|
6378
|
+
// dropping the command without a word.
|
|
6379
|
+
await _whenOpenIdle();
|
|
6380
|
+
session = await openSession('shell', cwd || null);
|
|
6381
|
+
}
|
|
6382
|
+
if (!session) {
|
|
6383
|
+
ensurePanel();
|
|
6384
|
+
showTermToast(`Could not open a terminal for ${label || 'this command'}. Try again`);
|
|
6385
|
+
return;
|
|
6386
|
+
}
|
|
6342
6387
|
if (label) {
|
|
6343
6388
|
session.label = label;
|
|
6344
6389
|
renderTabBar();
|
|
@@ -6414,140 +6459,72 @@ async function attachDetached(terminalSessionId, profile, initialMessage, autoSu
|
|
|
6414
6459
|
}
|
|
6415
6460
|
}
|
|
6416
6461
|
|
|
6417
|
-
/**
|
|
6418
|
-
*
|
|
6419
|
-
*
|
|
6420
|
-
*
|
|
6462
|
+
/** Type a message into a session once it can take it.
|
|
6463
|
+
* A shell tab (Command Runner) is ready when its line editor is: zsh, bash 5
|
|
6464
|
+
* and fish announce it (bracketed paste on), every other prompt is read off
|
|
6465
|
+
* the last line, and a prompt nothing recognises is taken once the output
|
|
6466
|
+
* goes quiet. A CLI keeps its own reading: watch for its input prompt (">" /
|
|
6467
|
+
* ❯) and fall back after 15 s, because a CLI swallows what is typed before it
|
|
6468
|
+
* is up. The rules, and the sequence wait → settle → type, are in term-ready.js.
|
|
6421
6469
|
*
|
|
6422
|
-
*
|
|
6423
|
-
*
|
|
6424
|
-
*
|
|
6425
|
-
*
|
|
6470
|
+
* The wait lives on the session, not on one socket: a socket that fails
|
|
6471
|
+
* before it opens, or is swapped by a reconnect, changes nothing, and the
|
|
6472
|
+
* readiness window only starts once a socket of the session is open. Output
|
|
6473
|
+
* that arrived before this call is read from session._replayBuf, and the
|
|
6474
|
+
* terminal's own parser state covers a prompt that only came back inside a
|
|
6475
|
+
* snapshot. Resolves { ok, reason } once the message is typed (or could not be). */
|
|
6426
6476
|
function _sendOnceReady(session, message, autoSubmit) {
|
|
6427
6477
|
if (!session?.ws) {
|
|
6428
6478
|
console.warn('[SynaBun] _sendOnceReady: no session.ws — aborting');
|
|
6429
|
-
return;
|
|
6430
|
-
}
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
function doSend() {
|
|
6455
|
-
if (_sent) return;
|
|
6456
|
-
_sent = true;
|
|
6457
|
-
// Clean up watcher and timer
|
|
6458
|
-
if (_listener) session._outputWatchers?.delete(_listener);
|
|
6459
|
-
if (_fallbackTimer) clearTimeout(_fallbackTimer);
|
|
6460
|
-
console.log('[SynaBun] _sendOnceReady: doSend triggered, ws.readyState =', session.ws?.readyState);
|
|
6461
|
-
// Small delay after detecting ready — let the CLI fully settle
|
|
6462
|
-
setTimeout(() => {
|
|
6463
|
-
if (session.ws?.readyState !== WebSocket.OPEN) {
|
|
6464
|
-
console.warn('[SynaBun] _sendOnceReady: WS not OPEN at send time, readyState =', session.ws?.readyState);
|
|
6465
|
-
return;
|
|
6466
|
-
}
|
|
6467
|
-
// Chunk the input to avoid ConPTY input buffer overflow on Windows.
|
|
6468
|
-
// Writing 1000+ chars in a single pty.write() can silently drop data.
|
|
6469
|
-
const full = message + '\r';
|
|
6470
|
-
const CHUNK = 256;
|
|
6471
|
-
const DELAY = 30; // ms between chunks
|
|
6472
|
-
console.log('[SynaBun] _sendOnceReady: sending prompt in chunks (' + message.length + ' chars, ' + Math.ceil(full.length / CHUNK) + ' chunks)');
|
|
6473
|
-
for (let i = 0; i < full.length; i += CHUNK) {
|
|
6474
|
-
const chunk = full.slice(i, i + CHUNK);
|
|
6475
|
-
const delay = (i / CHUNK) * DELAY;
|
|
6476
|
-
setTimeout(() => {
|
|
6477
|
-
if (session.ws?.readyState === WebSocket.OPEN) {
|
|
6478
|
-
session.ws.send(JSON.stringify({ type: 'input', data: chunk }));
|
|
6479
|
-
}
|
|
6480
|
-
}, delay);
|
|
6481
|
-
}
|
|
6482
|
-
if (autoSubmit) {
|
|
6483
|
-
// Send a second Enter after all chunks + extra delay to auto-confirm Claude Code's prompt
|
|
6484
|
-
const totalChunkTime = Math.ceil(full.length / CHUNK) * DELAY;
|
|
6485
|
-
setTimeout(() => {
|
|
6486
|
-
if (session.ws?.readyState === WebSocket.OPEN) {
|
|
6487
|
-
console.log('[SynaBun] _sendOnceReady: sending auto-submit Enter');
|
|
6488
|
-
session.ws.send(JSON.stringify({ type: 'input', data: '\r' }));
|
|
6489
|
-
}
|
|
6490
|
-
}, totalChunkTime + 4000);
|
|
6491
|
-
}
|
|
6492
|
-
}, 500);
|
|
6493
|
-
}
|
|
6494
|
-
|
|
6495
|
-
// Check if the CLI prompt already appeared in replay data that arrived
|
|
6496
|
-
// before this function was called (the core race condition fix).
|
|
6497
|
-
function checkReplayBuffer() {
|
|
6498
|
-
if (session._replayBuf) {
|
|
6499
|
-
const clean = session._replayBuf.replace(ANSI_RE, '');
|
|
6500
|
-
if (READY_PATTERNS.some(p => p.test(clean))) {
|
|
6501
|
-
console.log('[SynaBun] _sendOnceReady: ready pattern found in existing replay buffer — sending immediately');
|
|
6502
|
-
doSend();
|
|
6503
|
-
return true;
|
|
6479
|
+
return Promise.resolve({ ok: false, reason: 'no-socket' });
|
|
6480
|
+
}
|
|
6481
|
+
if (!message) return Promise.resolve({ ok: false, reason: 'empty' });
|
|
6482
|
+
|
|
6483
|
+
const kind = readinessKind(session.profile);
|
|
6484
|
+
const alive = () => !session.dead && !session._exitReceived && _sessions.includes(session);
|
|
6485
|
+
|
|
6486
|
+
return typeWhenReady({
|
|
6487
|
+
kind,
|
|
6488
|
+
text: message + '\r',
|
|
6489
|
+
socket: () => session.ws,
|
|
6490
|
+
alive,
|
|
6491
|
+
OPEN: WebSocket.OPEN,
|
|
6492
|
+
// The emulator's own state: true while the shell's line editor is reading.
|
|
6493
|
+
lineEditorActive: () => session.term?.modes?.bracketedPasteMode === true,
|
|
6494
|
+
// What already arrived (replay data lands before an attach calls this).
|
|
6495
|
+
buffered: session._replayBuf || '',
|
|
6496
|
+
subscribe: ({ output, opened, parsed }) => {
|
|
6497
|
+
(session._outputWatchers ||= new Set()).add(output);
|
|
6498
|
+
(session._openWatchers ||= new Set()).add(opened);
|
|
6499
|
+
// Bracketed paste can arrive inside a snapshot's escape data, where the
|
|
6500
|
+
// watcher only sees plain text: ask the parser after each write instead.
|
|
6501
|
+
let onParsed = null;
|
|
6502
|
+
if (kind === 'shell') {
|
|
6503
|
+
try { onParsed = session.term?.onWriteParsed?.(parsed) || null; } catch {}
|
|
6504
6504
|
}
|
|
6505
|
+
return () => {
|
|
6506
|
+
session._outputWatchers?.delete(output);
|
|
6507
|
+
session._openWatchers?.delete(opened);
|
|
6508
|
+
try { onParsed?.dispose(); } catch {}
|
|
6509
|
+
};
|
|
6510
|
+
},
|
|
6511
|
+
onReady: (reason, waitedMs) => console.log(`[SynaBun] _sendOnceReady: ${kind} ready (${reason}) after ${waitedMs} ms`),
|
|
6512
|
+
}).then((result) => {
|
|
6513
|
+
if (!result.ok) {
|
|
6514
|
+
console.warn(`[SynaBun] _sendOnceReady: not delivered (${result.reason}, ${result.sent}/${result.chunks} chunks)`);
|
|
6515
|
+
if (alive()) showTermToast('The command did not reach the terminal. Run it again');
|
|
6516
|
+
return result;
|
|
6517
|
+
}
|
|
6518
|
+
if (autoSubmit) {
|
|
6519
|
+
// A second Enter confirms Claude Code's prompt once the paste settled
|
|
6520
|
+
setTimeout(() => {
|
|
6521
|
+
if (session.ws?.readyState === WebSocket.OPEN) {
|
|
6522
|
+
session.ws.send(JSON.stringify({ type: 'input', data: '\r' }));
|
|
6523
|
+
}
|
|
6524
|
+
}, 4000);
|
|
6505
6525
|
}
|
|
6506
|
-
return
|
|
6507
|
-
}
|
|
6508
|
-
|
|
6509
|
-
function onOutput(text) {
|
|
6510
|
-
_outputBuf += text;
|
|
6511
|
-
// Strip ANSI codes before matching — CLI prompts are colorized
|
|
6512
|
-
const clean = _outputBuf.replace(ANSI_RE, '');
|
|
6513
|
-
if (READY_PATTERNS.some(p => p.test(clean))) {
|
|
6514
|
-
console.log('[SynaBun] _sendOnceReady: ready pattern matched in live output');
|
|
6515
|
-
doSend();
|
|
6516
|
-
}
|
|
6517
|
-
}
|
|
6518
|
-
|
|
6519
|
-
function attach() {
|
|
6520
|
-
// Before watching future output, check what already arrived.
|
|
6521
|
-
// The replay from ws.onopen fires synchronously before this function runs
|
|
6522
|
-
// (reconnectSession awaits, replay lands in ws.onmessage, accumulates
|
|
6523
|
-
// in session._replayBuf). If the CLI prompt is already there, send now.
|
|
6524
|
-
if (checkReplayBuffer()) return;
|
|
6525
|
-
|
|
6526
|
-
console.log('[SynaBun] _sendOnceReady: attach() — adding output watcher (no prompt in replay buffer yet)');
|
|
6527
|
-
// Session-level output watcher (fed by _handleTermWsEvent) — unlike a raw
|
|
6528
|
-
// ws 'message' listener, this survives _reconnectTerminalWs swapping session.ws.
|
|
6529
|
-
_listener = onOutput;
|
|
6530
|
-
(session._outputWatchers ||= new Set()).add(_listener);
|
|
6531
|
-
// Fallback: if no prompt detected within MAX_WAIT, send anyway
|
|
6532
|
-
_fallbackTimer = setTimeout(() => {
|
|
6533
|
-
console.warn('[SynaBun] _sendOnceReady: CLI prompt not detected, sending after timeout');
|
|
6534
|
-
doSend();
|
|
6535
|
-
}, MAX_WAIT);
|
|
6536
|
-
}
|
|
6537
|
-
|
|
6538
|
-
if (session.ws.readyState === WebSocket.OPEN) attach();
|
|
6539
|
-
else {
|
|
6540
|
-
session.ws.addEventListener('open', attach, { once: true });
|
|
6541
|
-
// Safety net: if the WS never opens, force-send after MAX_WAIT to avoid silent failure
|
|
6542
|
-
setTimeout(() => {
|
|
6543
|
-
if (!_sent) {
|
|
6544
|
-
// One last check of the replay buffer before force-sending
|
|
6545
|
-
if (checkReplayBuffer()) return;
|
|
6546
|
-
console.warn('[SynaBun] _sendOnceReady: WS never opened — forcing send attempt');
|
|
6547
|
-
doSend();
|
|
6548
|
-
}
|
|
6549
|
-
}, MAX_WAIT + 2000);
|
|
6550
|
-
}
|
|
6526
|
+
return result;
|
|
6527
|
+
});
|
|
6551
6528
|
}
|
|
6552
6529
|
|
|
6553
6530
|
/** Snapshot terminal state for workspace save */
|
|
@@ -6566,6 +6543,7 @@ export function getTerminalSnapshot() {
|
|
|
6566
6543
|
isDetached: _detachedTabs.has(s.id),
|
|
6567
6544
|
userRenamed: s._userRenamed || false,
|
|
6568
6545
|
claudeSessionId: s._claudeSessionId || null,
|
|
6546
|
+
floatColor: s._floatColor || null,
|
|
6569
6547
|
assistantSessionId: s._isAssistant ? s.id : null,
|
|
6570
6548
|
brain: s._isAssistant ? (s._brain || null) : null,
|
|
6571
6549
|
})),
|
|
@@ -6622,12 +6600,20 @@ function applyTerminalLayout(snap) {
|
|
|
6622
6600
|
}
|
|
6623
6601
|
if (!session) continue;
|
|
6624
6602
|
|
|
6603
|
+
// The snapshot's color goes onto the session before detachTab reads it:
|
|
6604
|
+
// a session a workspace restore rebuilt has none and would get a random one
|
|
6605
|
+
const savedColor = FLOAT_COLORS.some(c => c.id === dt.floatColor) ? dt.floatColor : null;
|
|
6606
|
+
if (savedColor) session._floatColor = savedColor;
|
|
6607
|
+
|
|
6625
6608
|
const idx = _sessions.indexOf(session);
|
|
6626
6609
|
if (!_detachedTabs.has(session.id)) {
|
|
6627
6610
|
detachTab(idx);
|
|
6628
6611
|
}
|
|
6629
6612
|
const tabState = _detachedTabs.get(session.id);
|
|
6630
6613
|
if (tabState) {
|
|
6614
|
+
// Already floating (browser tabs detach while reconnecting): recolor it
|
|
6615
|
+
if (savedColor) _applyFloatColor(tabState.el, savedColor);
|
|
6616
|
+
|
|
6631
6617
|
// Validate saved dimensions — enforce minimums and keep on screen
|
|
6632
6618
|
const minW = 280, minH = 160;
|
|
6633
6619
|
const w = Math.max(minW, dt.width || minW);
|
|
@@ -6745,7 +6731,7 @@ export async function restoreTerminalSnapshot(snap) {
|
|
|
6745
6731
|
}
|
|
6746
6732
|
if (liveIds.has(saved.id)) {
|
|
6747
6733
|
const live = liveMap.get(saved.id);
|
|
6748
|
-
const opts = { label: saved.label, pinned: saved.pinned, cwd: live?.cwd || null, userRenamed: saved.userRenamed, claudeSessionId: saved.claudeSessionId };
|
|
6734
|
+
const opts = { label: saved.label, pinned: saved.pinned, cwd: live?.cwd || null, userRenamed: saved.userRenamed, claudeSessionId: saved.claudeSessionId, floatColor: saved.floatColor };
|
|
6749
6735
|
if (CLI_PROFILES.has(saved.profile)) {
|
|
6750
6736
|
await reconnectSession(saved.id, saved.profile, opts);
|
|
6751
6737
|
} else {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// Bump when offline.html changes shape: the new worker drops the old cache and
|
|
2
|
+
// stores the page again (v12: the start command of a packaged application).
|
|
3
|
+
const CACHE_NAME = 'synabun-offline-v12';
|
|
2
4
|
const OFFLINE_URL = '/offline.html';
|
|
3
5
|
|
|
4
6
|
self.addEventListener('install', (event) => {
|
|
@@ -21,7 +21,7 @@ import { initI18n, t } from '../../shared/i18n.js';
|
|
|
21
21
|
// ── Shared UI modules (side-effect: each registers its own event listeners) ──
|
|
22
22
|
import { initTooltip } from '../../shared/ui-tooltip.js';
|
|
23
23
|
import { initPanelSystem, initPinToggle, clampPanelsToViewport } from '../../shared/ui-panels.js';
|
|
24
|
-
import { initLoading, showLoadingError, hideLoading } from '../../shared/ui-loading.js';
|
|
24
|
+
import { initLoading, showLoadingError, hideLoading, rememberHealth } from '../../shared/ui-loading.js';
|
|
25
25
|
import { initNavbar } from '../../shared/ui-navbar.js';
|
|
26
26
|
import { initMenubar } from '../../shared/ui-menubar.js';
|
|
27
27
|
import { initSearch } from '../../shared/ui-search.js';
|
|
@@ -304,6 +304,7 @@ async function boot() {
|
|
|
304
304
|
try {
|
|
305
305
|
const healthRes = await fetch('/api/health');
|
|
306
306
|
const health = await healthRes.json();
|
|
307
|
+
rememberHealth(health);
|
|
307
308
|
if (!health.ok) {
|
|
308
309
|
const messages = {
|
|
309
310
|
db_missing: [t('loading.health.databaseUnreachable.title'), health.detail || t('loading.health.databaseUnreachable.sub')],
|
|
@@ -21,7 +21,7 @@ import { initI18n, t } from '../../shared/i18n.js';
|
|
|
21
21
|
// ── Shared UI modules (side-effect: each registers its own event listeners) ──
|
|
22
22
|
import { initTooltip } from '../../shared/ui-tooltip.js';
|
|
23
23
|
import { initPanelSystem, initPinToggle, clampPanelsToViewport } from '../../shared/ui-panels.js';
|
|
24
|
-
import { initLoading, showLoadingError, hideLoading } from '../../shared/ui-loading.js';
|
|
24
|
+
import { initLoading, showLoadingError, hideLoading, rememberHealth } from '../../shared/ui-loading.js';
|
|
25
25
|
import { initNavbar } from '../../shared/ui-navbar.js';
|
|
26
26
|
import { initMenubar, closeMenubar } from '../../shared/ui-menubar.js';
|
|
27
27
|
import { initSearch } from '../../shared/ui-search.js';
|
|
@@ -389,6 +389,7 @@ async function boot() {
|
|
|
389
389
|
try {
|
|
390
390
|
const healthRes = await fetch('/api/health');
|
|
391
391
|
const health = await healthRes.json();
|
|
392
|
+
rememberHealth(health);
|
|
392
393
|
if (!health.ok) {
|
|
393
394
|
const messages = {
|
|
394
395
|
db_missing: [t('loading.health.databaseUnreachable.title'), health.detail || t('loading.health.databaseUnreachable.sub')],
|
|
@@ -15,27 +15,30 @@ import { existsSync, cpSync, mkdirSync } from 'node:fs';
|
|
|
15
15
|
import { resolve, dirname } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { ensureVendoredExecutables } from '../lib/native-binary-runtime.js';
|
|
18
|
+
import { pruneExternalTools } from '../../lib/external-tools.js';
|
|
18
19
|
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
21
|
const neuralRoot = resolve(__dirname, '..');
|
|
21
22
|
const ptyDir = resolve(neuralRoot, 'node_modules', 'node-pty');
|
|
22
23
|
|
|
23
|
-
// Step
|
|
24
|
+
// Step 0: Claude Code, Codex, OpenCode and Gemini CLI are installed by the user,
|
|
25
|
+
// never by SynaBun. The lockfile holds none of them; an `npm install` that
|
|
26
|
+
// brought one anyway (a lockfile made again without scripts/strip-external-tools.mjs,
|
|
27
|
+
// a package an older SynaBun depended on) has it removed here, right after the
|
|
28
|
+
// install. See lib/external-tools.js at the package root.
|
|
29
|
+
for (const item of pruneExternalTools(resolve(neuralRoot, 'node_modules')).removed) {
|
|
30
|
+
console.log(`[rebuild-pty] removed ${item.name}: ${item.tool} is installed separately, not with SynaBun`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Step 1a: Restore execute permissions on SynaBun's own native helpers.
|
|
24
34
|
//
|
|
25
35
|
// npm does not preserve the execute bit for files shipped via a package's
|
|
26
|
-
// `files` array with no `bin` entry
|
|
27
|
-
//
|
|
28
|
-
// a checkout can materialize them 0644 as well.
|
|
36
|
+
// `files` array with no `bin` entry, and a checkout with core.filemode=false
|
|
37
|
+
// can materialize them 0644 as well.
|
|
29
38
|
//
|
|
30
39
|
// The previous version of this step hardcoded four prebuild directories (naming
|
|
31
|
-
// linux dirs that do not exist here while missing ones that do)
|
|
32
|
-
//
|
|
33
|
-
// which exists any more, since that package was replaced by
|
|
34
|
-
// @anthropic-ai/claude-agent-sdk. The sweep below enumerates instead.
|
|
35
|
-
//
|
|
36
|
-
// This runs BEFORE the node-pty guard below: the sweep also covers the Claude
|
|
37
|
-
// and Codex binaries, which must be repaired even in an install that has no
|
|
38
|
-
// node-pty at all.
|
|
40
|
+
// linux dirs that do not exist here while missing ones that do). The sweep
|
|
41
|
+
// below enumerates instead.
|
|
39
42
|
function sweepVendoredBinaries(label) {
|
|
40
43
|
const sweep = ensureVendoredExecutables({ root: neuralRoot, log: console.log });
|
|
41
44
|
for (const failure of sweep.failed) {
|