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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Terminal readiness
|
|
3
|
+
// When a freshly opened terminal can take a typed command, and how to hand it
|
|
4
|
+
// over. DOM-free: ui-terminal.js feeds it the output stream and does the I/O.
|
|
5
|
+
// ═══════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
// CSI, OSC (BEL or ST terminated), charset selection, and the two-byte escapes.
|
|
8
|
+
const ESCAPES_RE = /\x1b\[[\x20-\x3f]*[\x40-\x7e]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][AB012]|\x1b[=>78MDEHNOc]/g;
|
|
9
|
+
|
|
10
|
+
// What _sendOnceReady has always stripped and matched for the CLIs (Claude
|
|
11
|
+
// Code, Codex, …). Kept as it was: the loops depend on this exact reading.
|
|
12
|
+
const CLI_ANSI_RE = /\x1b\[[\x20-\x3f]*[\x40-\x7e]|\x1b\][^\x07]*\x07|\x1b[()][AB012]/g;
|
|
13
|
+
const CLI_READY_PATTERNS = [
|
|
14
|
+
/^>\s*$/m, // Claude Code prompt: ">" on its own line
|
|
15
|
+
/\n>\s*$/, // ">" after newline at end of output
|
|
16
|
+
/>\s*$/, // ">" at end of buffer (catch partial lines)
|
|
17
|
+
/❯/, // ❯ (some CLI prompts)
|
|
18
|
+
/\$ $/, // Shell prompt fallback
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// The line editor (zsh's ZLE, readline in bash 5, fish) turns bracketed paste
|
|
22
|
+
// on when it starts reading a line and off when a command runs.
|
|
23
|
+
const PASTE_ON = '\x1b[?2004h';
|
|
24
|
+
const PASTE_OFF = '\x1b[?2004l';
|
|
25
|
+
// Shell integration marks (FinalTerm / OSC 133): B = the prompt has ended,
|
|
26
|
+
// C = a command is running.
|
|
27
|
+
const MARK_PROMPT_END = '\x1b]133;B';
|
|
28
|
+
const MARK_RUNNING = '\x1b]133;C';
|
|
29
|
+
|
|
30
|
+
// The character a prompt ends in: zsh %, bash/sh $, root #, cmd.exe / PowerShell
|
|
31
|
+
// / fish >, and the arrows the common themes use. A reconnect snapshot trims the
|
|
32
|
+
// trailing space, so the space is optional.
|
|
33
|
+
const PROMPT_TAIL_RE = /[%$#>❯➜»›→▶λ][ \t]*$/;
|
|
34
|
+
|
|
35
|
+
const BUFFER_MAX = 16384;
|
|
36
|
+
const BUFFER_KEEP = 8192;
|
|
37
|
+
|
|
38
|
+
// capMs runs from the moment the terminal's socket is open, never from the
|
|
39
|
+
// call: a socket that takes its time must not eat into the wait for a prompt.
|
|
40
|
+
// openGraceMs is how much longer than that a socket may take to open at all.
|
|
41
|
+
export const READY_LIMITS = Object.freeze({
|
|
42
|
+
// A shell takes typeahead, so its limits are short: the worst case is a
|
|
43
|
+
// command typed a moment before the prompt is drawn, never a lost one.
|
|
44
|
+
shell: Object.freeze({ quietMs: 350, capMs: 5000, settleMs: 40, openGraceMs: 2000 }),
|
|
45
|
+
// A CLI started inside the shell swallows input typed before its own prompt.
|
|
46
|
+
cli: Object.freeze({ quietMs: 0, capMs: 15000, settleMs: 500, openGraceMs: 2000 }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Which reading a terminal profile gets. */
|
|
50
|
+
export function readinessKind(profile) {
|
|
51
|
+
return profile === 'shell' ? 'shell' : 'cli';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function stripEscapes(text) {
|
|
55
|
+
return String(text || '').replace(ESCAPES_RE, '');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function lastOn(raw, on, off) {
|
|
59
|
+
const at = raw.lastIndexOf(on);
|
|
60
|
+
return at >= 0 && at > raw.lastIndexOf(off);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read everything a terminal printed so far. Returns the reason it is ready
|
|
65
|
+
* ('line-editor', 'prompt-mark', 'prompt') or null.
|
|
66
|
+
*/
|
|
67
|
+
export function readySignal(raw, kind = 'shell') {
|
|
68
|
+
const text = String(raw || '');
|
|
69
|
+
if (!text) return null;
|
|
70
|
+
if (kind !== 'shell') {
|
|
71
|
+
const clean = text.replace(CLI_ANSI_RE, '');
|
|
72
|
+
return CLI_READY_PATTERNS.some(p => p.test(clean)) ? 'prompt' : null;
|
|
73
|
+
}
|
|
74
|
+
if (lastOn(text, PASTE_ON, PASTE_OFF)) return 'line-editor';
|
|
75
|
+
if (lastOn(text, MARK_PROMPT_END, MARK_RUNNING)) return 'prompt-mark';
|
|
76
|
+
// The line the cursor is on: zsh draws its prompt after "\r \r", so a
|
|
77
|
+
// carriage return starts a line here just like a newline does.
|
|
78
|
+
const clean = stripEscapes(text);
|
|
79
|
+
const line = clean.slice(Math.max(clean.lastIndexOf('\n'), clean.lastIndexOf('\r')) + 1);
|
|
80
|
+
return line.trim() && PROMPT_TAIL_RE.test(line) ? 'prompt' : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Watch one terminal until it can take input, then call onReady(reason) once.
|
|
85
|
+
* output(text) — output arrived (live bytes, or what was already buffered)
|
|
86
|
+
* replace(text) — a snapshot replaced the screen (connect / reconnect)
|
|
87
|
+
* poke() — the emulator parsed something: re-read its line-editor mode
|
|
88
|
+
* start() — the wait begins
|
|
89
|
+
* opened() — the terminal's socket is open
|
|
90
|
+
* cancel()
|
|
91
|
+
* Nothing is decided before opened(): the readiness window (capMs, and the
|
|
92
|
+
* quiet rule) runs from there, in full, however late the socket opens. What
|
|
93
|
+
* was fed in earlier is read at that moment. A socket that never opens ends
|
|
94
|
+
* the wait capMs + openGraceMs after start().
|
|
95
|
+
* Reasons: 'line-editor' | 'prompt-mark' | 'prompt' | 'quiet' | 'timeout'.
|
|
96
|
+
*/
|
|
97
|
+
export function createReadyWatch({
|
|
98
|
+
kind = 'shell',
|
|
99
|
+
onReady,
|
|
100
|
+
lineEditorActive = null,
|
|
101
|
+
limits = READY_LIMITS[kind] || READY_LIMITS.cli,
|
|
102
|
+
timers = { set: (fn, ms) => setTimeout(fn, ms), clear: (id) => clearTimeout(id) },
|
|
103
|
+
} = {}) {
|
|
104
|
+
let buf = '';
|
|
105
|
+
let done = false;
|
|
106
|
+
let started = false;
|
|
107
|
+
let open = false;
|
|
108
|
+
let sawOutput = false;
|
|
109
|
+
let quietTimer = null;
|
|
110
|
+
let capTimer = null;
|
|
111
|
+
let openTimer = null;
|
|
112
|
+
|
|
113
|
+
function clearTimers() {
|
|
114
|
+
if (quietTimer) timers.clear(quietTimer);
|
|
115
|
+
if (capTimer) timers.clear(capTimer);
|
|
116
|
+
if (openTimer) timers.clear(openTimer);
|
|
117
|
+
quietTimer = capTimer = openTimer = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function finish(reason) {
|
|
121
|
+
if (done) return;
|
|
122
|
+
done = true;
|
|
123
|
+
clearTimers();
|
|
124
|
+
try { onReady?.(reason); } catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Something a person could see. A connect snapshot of an empty screen and a
|
|
128
|
+
// shell's terminal queries are escape codes only: not a sign of life yet.
|
|
129
|
+
function noteOutput(text) {
|
|
130
|
+
if (!sawOutput && /\S/.test(stripEscapes(text))) sawOutput = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function armQuiet() {
|
|
134
|
+
if (done || !open || !sawOutput || !(limits.quietMs > 0)) return;
|
|
135
|
+
if (quietTimer) timers.clear(quietTimer);
|
|
136
|
+
quietTimer = timers.set(() => { quietTimer = null; finish('quiet'); }, limits.quietMs);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function evaluate() {
|
|
140
|
+
if (done || !open) return;
|
|
141
|
+
const signal = readySignal(buf, kind);
|
|
142
|
+
if (signal) { finish(signal); return; }
|
|
143
|
+
if (kind === 'shell' && lineEditorActive) {
|
|
144
|
+
let active = false;
|
|
145
|
+
try { active = lineEditorActive() === true; } catch {}
|
|
146
|
+
if (active) { finish('line-editor'); return; }
|
|
147
|
+
}
|
|
148
|
+
armQuiet();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
output(text) {
|
|
153
|
+
if (done || !text) return;
|
|
154
|
+
noteOutput(text);
|
|
155
|
+
buf += text;
|
|
156
|
+
if (buf.length > BUFFER_MAX) buf = buf.slice(-BUFFER_KEEP);
|
|
157
|
+
evaluate();
|
|
158
|
+
},
|
|
159
|
+
replace(text) {
|
|
160
|
+
if (done) return;
|
|
161
|
+
buf = String(text || '');
|
|
162
|
+
noteOutput(buf);
|
|
163
|
+
evaluate();
|
|
164
|
+
},
|
|
165
|
+
poke() {
|
|
166
|
+
if (done || !open || kind !== 'shell' || !lineEditorActive) return;
|
|
167
|
+
let active = false;
|
|
168
|
+
try { active = lineEditorActive() === true; } catch {}
|
|
169
|
+
if (active) finish('line-editor');
|
|
170
|
+
},
|
|
171
|
+
start() {
|
|
172
|
+
if (done || started) return;
|
|
173
|
+
started = true;
|
|
174
|
+
if (open) return;
|
|
175
|
+
openTimer = timers.set(() => { openTimer = null; finish('timeout'); }, limits.capMs + (limits.openGraceMs || 0));
|
|
176
|
+
},
|
|
177
|
+
opened() {
|
|
178
|
+
if (done || open) return; // a reconnect later on does not restart the window
|
|
179
|
+
started = true;
|
|
180
|
+
open = true;
|
|
181
|
+
if (openTimer) timers.clear(openTimer);
|
|
182
|
+
openTimer = null;
|
|
183
|
+
capTimer = timers.set(() => { capTimer = null; finish('timeout'); }, limits.capMs);
|
|
184
|
+
evaluate();
|
|
185
|
+
},
|
|
186
|
+
cancel() {
|
|
187
|
+
if (done) return;
|
|
188
|
+
done = true;
|
|
189
|
+
clearTimers();
|
|
190
|
+
},
|
|
191
|
+
get done() { return done; },
|
|
192
|
+
get open() { return open; },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Split typed input into the writes the PTY gets (ConPTY drops long ones). */
|
|
197
|
+
export function inputChunks(text, size = 256) {
|
|
198
|
+
const full = String(text ?? '');
|
|
199
|
+
const out = [];
|
|
200
|
+
for (let i = 0; i < full.length; i += size) out.push(full.slice(i, i + size));
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Type `text` into a terminal: every chunk in order, each through the socket
|
|
206
|
+
* that is current at that moment (a reconnect swaps it). Waits for an open
|
|
207
|
+
* socket instead of dropping the input, and stops for good the moment a chunk
|
|
208
|
+
* cannot be delivered — a command with a hole in it must never reach Enter.
|
|
209
|
+
* socket() → the session's current socket ({ readyState, send }) or null
|
|
210
|
+
* alive() → false once the terminal is gone
|
|
211
|
+
* Resolves { ok, sent, chunks, reason? } with reason 'closed' | 'no-socket'.
|
|
212
|
+
*/
|
|
213
|
+
export async function typeInto({
|
|
214
|
+
socket,
|
|
215
|
+
alive = () => true,
|
|
216
|
+
text,
|
|
217
|
+
chunkSize = 256,
|
|
218
|
+
gapMs = 30,
|
|
219
|
+
openWaitMs = 10000,
|
|
220
|
+
pollMs = 25,
|
|
221
|
+
OPEN = 1,
|
|
222
|
+
sleep = (ms) => new Promise(r => setTimeout(r, ms)),
|
|
223
|
+
now = () => Date.now(),
|
|
224
|
+
} = {}) {
|
|
225
|
+
const chunks = inputChunks(text, chunkSize);
|
|
226
|
+
let sent = 0;
|
|
227
|
+
for (const chunk of chunks) {
|
|
228
|
+
const deadline = now() + openWaitMs;
|
|
229
|
+
let ws = socket();
|
|
230
|
+
while (alive() && (!ws || ws.readyState !== OPEN) && now() < deadline) {
|
|
231
|
+
// Mid-command the wait is over: what follows a gap would be a different command.
|
|
232
|
+
if (sent > 0) return { ok: false, sent, chunks: chunks.length, reason: 'closed' };
|
|
233
|
+
await sleep(pollMs);
|
|
234
|
+
ws = socket();
|
|
235
|
+
}
|
|
236
|
+
if (!alive()) return { ok: false, sent, chunks: chunks.length, reason: 'closed' };
|
|
237
|
+
if (!ws || ws.readyState !== OPEN) return { ok: false, sent, chunks: chunks.length, reason: 'no-socket' };
|
|
238
|
+
try { ws.send(JSON.stringify({ type: 'input', data: chunk })); }
|
|
239
|
+
catch { return { ok: false, sent, chunks: chunks.length, reason: 'closed' }; }
|
|
240
|
+
sent++;
|
|
241
|
+
if (sent < chunks.length) await sleep(gapMs);
|
|
242
|
+
}
|
|
243
|
+
return { ok: true, sent, chunks: chunks.length };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The whole hand-over of one message: wait until the terminal can take it, let
|
|
248
|
+
* it settle, type it. Everything that touches the page is handed in, so this
|
|
249
|
+
* runs the same under a test clock.
|
|
250
|
+
* socket() → the session's current socket (a reconnect swaps it)
|
|
251
|
+
* alive() → false once the terminal is gone
|
|
252
|
+
* lineEditorActive() → the emulator's bracketed-paste mode
|
|
253
|
+
* buffered → output that arrived before this call
|
|
254
|
+
* subscribe({ output(text, replaced), opened(), parsed() }) → unsubscribe
|
|
255
|
+
* output for every chunk, opened whenever a socket of
|
|
256
|
+
* this session opens, parsed after the emulator parsed
|
|
257
|
+
* onReady(reason, waitedMs)
|
|
258
|
+
* Resolves { ok: true, reason, waitedMs } or { ok: false, reason, sent, chunks }.
|
|
259
|
+
*/
|
|
260
|
+
export function typeWhenReady({
|
|
261
|
+
kind = 'shell',
|
|
262
|
+
text,
|
|
263
|
+
socket,
|
|
264
|
+
alive = () => true,
|
|
265
|
+
lineEditorActive = null,
|
|
266
|
+
buffered = '',
|
|
267
|
+
subscribe = null,
|
|
268
|
+
onReady = () => {},
|
|
269
|
+
OPEN = 1,
|
|
270
|
+
limits = READY_LIMITS[kind] || READY_LIMITS.cli,
|
|
271
|
+
timers = { set: (fn, ms) => setTimeout(fn, ms), clear: (id) => clearTimeout(id) },
|
|
272
|
+
now = () => Date.now(),
|
|
273
|
+
sleep = (ms) => new Promise(r => timers.set(r, ms)),
|
|
274
|
+
} = {}) {
|
|
275
|
+
return new Promise((resolve) => {
|
|
276
|
+
const t0 = now();
|
|
277
|
+
let unsubscribe = null;
|
|
278
|
+
const watch = createReadyWatch({
|
|
279
|
+
kind,
|
|
280
|
+
lineEditorActive,
|
|
281
|
+
limits,
|
|
282
|
+
timers,
|
|
283
|
+
onReady: (reason) => {
|
|
284
|
+
try { unsubscribe?.(); } catch {}
|
|
285
|
+
unsubscribe = null;
|
|
286
|
+
const waitedMs = Math.round(now() - t0);
|
|
287
|
+
try { onReady(reason, waitedMs); } catch {}
|
|
288
|
+
timers.set(async () => {
|
|
289
|
+
const result = await typeInto({ socket, alive, text, OPEN, sleep, now });
|
|
290
|
+
resolve(result.ok
|
|
291
|
+
? { ok: true, reason, waitedMs }
|
|
292
|
+
: { ok: false, reason: result.reason, sent: result.sent, chunks: result.chunks });
|
|
293
|
+
}, limits.settleMs);
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
// Subscribed before anything can be decided: no verdict comes before opened().
|
|
297
|
+
unsubscribe = subscribe?.({
|
|
298
|
+
output: (chunk, replaced) => { if (replaced) watch.replace(chunk); else watch.output(chunk); },
|
|
299
|
+
opened: () => watch.opened(),
|
|
300
|
+
parsed: () => watch.poke(),
|
|
301
|
+
}) || null;
|
|
302
|
+
if (buffered) watch.output(buffered);
|
|
303
|
+
watch.start();
|
|
304
|
+
const ws = socket?.();
|
|
305
|
+
if (ws && ws.readyState === OPEN) watch.opened();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
@@ -5653,9 +5653,9 @@ function handleTabEvent(tab, ev) {
|
|
|
5653
5653
|
appendStatus(tab, ev.message || 'Retrying…');
|
|
5654
5654
|
return;
|
|
5655
5655
|
}
|
|
5656
|
-
// Non-fatal runtime
|
|
5657
|
-
//
|
|
5658
|
-
//
|
|
5656
|
+
// Non-fatal runtime notices (e.g. a session setting that could not be
|
|
5657
|
+
// applied, or where this session's debug log is written). The session
|
|
5658
|
+
// continues — this must not render as a fatal error.
|
|
5659
5659
|
if (ev.type === 'system' && ev.subtype === 'runtime_notice') {
|
|
5660
5660
|
if (ev.level === 'error') appendError(tab, ev.message || 'Runtime error');
|
|
5661
5661
|
else if (ev.level === 'info') appendStatus(tab, ev.message || 'Runtime notice');
|