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.
Files changed (70) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/THIRD-PARTY-LICENSES.md +5 -0
  3. package/launcher.mjs +40 -0
  4. package/lib/claude-hooks.js +61 -8
  5. package/lib/client-config-repair.js +4 -3
  6. package/lib/data-home-migration.js +4 -0
  7. package/lib/desktop-pwa.js +206 -0
  8. package/lib/external-tools.js +221 -0
  9. package/lib/first-launch-protection.js +5 -0
  10. package/lib/packaged-runtime.js +110 -0
  11. package/lib/start-launcher.js +1309 -0
  12. package/neural-interface/i18n/en.json +41 -2
  13. package/neural-interface/i18n/pt-BR.json +41 -2
  14. package/neural-interface/lib/assistant-envelope.js +9 -6
  15. package/neural-interface/lib/augmented-path.js +23 -3
  16. package/neural-interface/lib/browser-language.js +83 -0
  17. package/neural-interface/lib/claude-agent-bridge.js +54 -147
  18. package/neural-interface/lib/claude-executable.js +241 -0
  19. package/neural-interface/lib/claude-model-catalog.js +52 -91
  20. package/neural-interface/lib/native-binary-runtime.js +69 -337
  21. package/neural-interface/lib/native-loop-providers.js +30 -44
  22. package/neural-interface/lib/offline-page.js +54 -0
  23. package/neural-interface/lib/session-title-generator.js +5 -0
  24. package/neural-interface/lib/synabun-update-plan.js +19 -0
  25. package/neural-interface/package-lock.json +0 -251
  26. package/neural-interface/public/install-app.html +31 -0
  27. package/neural-interface/public/manifest.json +2 -0
  28. package/neural-interface/public/offline.html +401 -66
  29. package/neural-interface/public/onboarding.html +252 -146
  30. package/neural-interface/public/shared/assistant/asst-mascot.js +12 -10
  31. package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
  32. package/neural-interface/public/shared/assistant/asst-render.js +58 -49
  33. package/neural-interface/public/shared/assistant/asst-styles.js +4 -2
  34. package/neural-interface/public/shared/command-runner-model.js +30 -0
  35. package/neural-interface/public/shared/html-shell.js +9 -5
  36. package/neural-interface/public/shared/pwa-install-page.js +34 -0
  37. package/neural-interface/public/shared/pwa-install.css +12 -0
  38. package/neural-interface/public/shared/pwa-install.js +28 -0
  39. package/neural-interface/public/shared/settings/settings-rules.js +160 -0
  40. package/neural-interface/public/shared/start-bridge.js +583 -0
  41. package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
  42. package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
  43. package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
  44. package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
  45. package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
  46. package/neural-interface/public/shared/styles.css +801 -392
  47. package/neural-interface/public/shared/term-ready.js +307 -0
  48. package/neural-interface/public/shared/ui-claude-panel.js +3 -3
  49. package/neural-interface/public/shared/ui-command-runner.js +251 -468
  50. package/neural-interface/public/shared/ui-judgments.js +1 -1
  51. package/neural-interface/public/shared/ui-loading.js +221 -10
  52. package/neural-interface/public/shared/ui-menubar.js +11 -27
  53. package/neural-interface/public/shared/ui-settings.js +36 -59
  54. package/neural-interface/public/shared/ui-terminal.js +130 -144
  55. package/neural-interface/public/sw.js +3 -1
  56. package/neural-interface/public/variant/2d/main.js +2 -1
  57. package/neural-interface/public/variant/3d/main.js +2 -1
  58. package/neural-interface/scripts/rebuild-pty.js +15 -12
  59. package/neural-interface/server.js +220 -185
  60. package/package.json +13 -1
  61. package/postinstall.js +11 -81
  62. package/setup.js +72 -4
  63. package/docs/browser-v2.md +0 -109
  64. package/docs/hook-integrity.md +0 -89
  65. package/docs/judgments.md +0 -182
  66. package/docs/style-guide.md +0 -246
  67. package/scripts/audit-claude-hook-usage.mjs +0 -12
  68. package/scripts/audit-memory-duplicates.mjs +0 -44
  69. package/scripts/measure-claude-hook-overhead.mjs +0 -77
  70. 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 problems (e.g. the bundled Claude binary lost its execute
5657
- // bit and was repaired, or we fell back to the globally installed CLI). The
5658
- // session continues — this must not render as a fatal error.
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');