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
|
@@ -111,7 +111,7 @@ export function buildJudgmentsTab() {
|
|
|
111
111
|
</div>
|
|
112
112
|
|
|
113
113
|
${section({ id: 'jv-sec-connection', title: kit.tx('settings.redesign.judgments.connection'), badgeId: 'jv-key-source-badge', body: `
|
|
114
|
-
<div class="settings-hint" style="margin-bottom:10px">${kit.te('settings.redesign.judgments.jevTypesafeSystemOneReturnsTyped')} <code>~/.synabun/.env</code> ${kit.te('settings.redesign.judgments.andNeverLeavesTheServerOnly')}</div>
|
|
114
|
+
<div class="settings-hint" style="margin-bottom:10px">${kit.te('settings.redesign.judgments.jevTypesafeSystemOneReturnsTyped')} <code>~/.synabun/.env</code> ${kit.te('settings.redesign.judgments.andNeverLeavesTheServerOnly')} ${kit.te('settings.redesign.judgments.learnMoreAtTypesafe')} <a href="https://typesafe.ai" target="_blank" rel="noopener noreferrer">typesafe.ai</a>.</div>
|
|
115
115
|
<div class="settings-field">
|
|
116
116
|
<label for="jv-api-key">${kit.te('settings.redesign.judgments.apiKey')}</label>
|
|
117
117
|
<div class="settings-key-row" style="display:flex;gap:6px">
|
|
@@ -6,12 +6,40 @@
|
|
|
6
6
|
import { fetchHealth, startHealth } from './api.js';
|
|
7
7
|
import { emit } from './state.js';
|
|
8
8
|
import { t } from './i18n.js';
|
|
9
|
+
import {
|
|
10
|
+
START_STORAGE, createStartBridge, fetchStartBeacon, isMacPlatform, isWindowsPlatform,
|
|
11
|
+
manualStartCommands, openStartLink, startBeaconTarget, startButtonUsable, startStatusView,
|
|
12
|
+
} from './start-bridge.js';
|
|
13
|
+
// Imported with the app, not when it is needed: once the server is down
|
|
14
|
+
// nothing more can be fetched, and that is exactly when the overlay wants it.
|
|
15
|
+
import { createMascot } from './synabun-mascot.js';
|
|
9
16
|
|
|
10
17
|
const $ = (id) => document.getElementById(id);
|
|
11
18
|
|
|
12
19
|
// ── Internal refs (resolved once on init) ──
|
|
13
20
|
let _statusDot = null;
|
|
14
21
|
let _initCallback = null;
|
|
22
|
+
// The server-offline state: watches for the server and owns the Start button.
|
|
23
|
+
let _offlineBridge = null;
|
|
24
|
+
// The title the overlay was given for "offline" (a start replaces it while it runs).
|
|
25
|
+
let _offlineTitle = '';
|
|
26
|
+
// The mascot rig in the overlay, mounted the first time the server is offline.
|
|
27
|
+
let _mascot = null;
|
|
28
|
+
|
|
29
|
+
// What the mascot does for each thing the page knows about a start
|
|
30
|
+
// (startStatusView's `mascot`), as poses of the shared rig.
|
|
31
|
+
const MASCOT_POSE = {
|
|
32
|
+
asleep: 'sleep',
|
|
33
|
+
waking: 'wait',
|
|
34
|
+
working: 'think',
|
|
35
|
+
ready: 'success',
|
|
36
|
+
failed: 'error',
|
|
37
|
+
stalled: 'offline',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function stored(key) {
|
|
41
|
+
try { return localStorage.getItem(key); } catch { return null; }
|
|
42
|
+
}
|
|
15
43
|
|
|
16
44
|
// ═══════════════════════════════════════════
|
|
17
45
|
// PUBLIC API
|
|
@@ -38,18 +66,31 @@ export function showLoadingError(title, sub, canStart, serverOffline) {
|
|
|
38
66
|
const $sub = $('loading-sub');
|
|
39
67
|
if ($text) $text.textContent = title;
|
|
40
68
|
if ($sub) $sub.textContent = sub;
|
|
69
|
+
if (serverOffline) _offlineTitle = title;
|
|
41
70
|
|
|
71
|
+
// Offline, the button asks the OS to start the server (synabun://start); it
|
|
72
|
+
// is left out only when that launcher is known not to be registered.
|
|
73
|
+
const offlineStart = !!serverOffline && startButtonUsable(stored(START_STORAGE.launcher));
|
|
42
74
|
const $action = $('loading-action');
|
|
43
|
-
|
|
75
|
+
const $btn = $('loading-action-btn');
|
|
76
|
+
if (canStart || offlineStart) {
|
|
44
77
|
if ($action) $action.style.display = 'block';
|
|
78
|
+
if ($btn) $btn.disabled = false;
|
|
45
79
|
const $label = $('loading-action-label');
|
|
46
|
-
if ($label) $label.textContent = t('common.start');
|
|
80
|
+
if ($label) $label.textContent = serverOffline ? t('loading.startServer') : t('common.start');
|
|
47
81
|
} else {
|
|
48
82
|
if ($action) $action.style.display = 'none';
|
|
49
83
|
}
|
|
50
84
|
|
|
51
|
-
|
|
52
|
-
|
|
85
|
+
clearStartStatus();
|
|
86
|
+
|
|
87
|
+
if (serverOffline) {
|
|
88
|
+
const $hint = $('loading-server-cmd-hint');
|
|
89
|
+
if ($hint) $hint.textContent = offlineStart ? t('loading.runCommandOr') : t('loading.runCommandSetup');
|
|
90
|
+
watchForServer();
|
|
91
|
+
} else {
|
|
92
|
+
stopWatchingForServer();
|
|
93
|
+
}
|
|
53
94
|
}
|
|
54
95
|
|
|
55
96
|
/**
|
|
@@ -78,6 +119,7 @@ function resetLoadingToConnecting() {
|
|
|
78
119
|
if (!$overlay) return;
|
|
79
120
|
|
|
80
121
|
$overlay.classList.remove('error', 'server-offline');
|
|
122
|
+
stopWatchingForServer();
|
|
81
123
|
|
|
82
124
|
const $text = $('loading-text');
|
|
83
125
|
const $sub = $('loading-sub');
|
|
@@ -88,6 +130,7 @@ function resetLoadingToConnecting() {
|
|
|
88
130
|
if ($sub) $sub.textContent = t('loading.initializingNeural');
|
|
89
131
|
if ($action) $action.style.display = 'none';
|
|
90
132
|
if ($actionStatus) $actionStatus.textContent = '';
|
|
133
|
+
clearStartStatus();
|
|
91
134
|
if (_statusDot) _statusDot.classList.remove('error');
|
|
92
135
|
}
|
|
93
136
|
|
|
@@ -95,6 +138,24 @@ function resetLoadingToConnecting() {
|
|
|
95
138
|
// HEALTH CHECK (called during init)
|
|
96
139
|
// ═══════════════════════════════════════════
|
|
97
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Keep what a health answer says about this install for the moment the server
|
|
143
|
+
* is down and cannot be asked: where it lives, how it was installed, and
|
|
144
|
+
* whether the Start Server launcher is registered.
|
|
145
|
+
*/
|
|
146
|
+
export function rememberHealth(health) {
|
|
147
|
+
if (!health) return;
|
|
148
|
+
try {
|
|
149
|
+
if (health.projectDir) {
|
|
150
|
+
localStorage.setItem('synabun-project-dir', health.projectDir);
|
|
151
|
+
}
|
|
152
|
+
if (health.install) localStorage.setItem(START_STORAGE.install, health.install);
|
|
153
|
+
if (health.startLauncher) localStorage.setItem(START_STORAGE.launcher, health.startLauncher);
|
|
154
|
+
if (health.entry) localStorage.setItem(START_STORAGE.entry, health.entry);
|
|
155
|
+
} catch {}
|
|
156
|
+
if (health.projectDir) updateCmdText(health.projectDir);
|
|
157
|
+
}
|
|
158
|
+
|
|
98
159
|
/**
|
|
99
160
|
* Run the health check pre-flight. If the database is unhealthy,
|
|
100
161
|
* show the appropriate loading error. Returns true if healthy (or
|
|
@@ -104,9 +165,7 @@ function resetLoadingToConnecting() {
|
|
|
104
165
|
export async function checkHealth() {
|
|
105
166
|
try {
|
|
106
167
|
const health = await fetchHealth();
|
|
107
|
-
|
|
108
|
-
localStorage.setItem('synabun-project-dir', health.projectDir);
|
|
109
|
-
}
|
|
168
|
+
rememberHealth(health);
|
|
110
169
|
if (!health.ok) {
|
|
111
170
|
const messages = {
|
|
112
171
|
db_missing: [t('loading.health.databaseUnreachable.title'), health.detail || t('loading.health.databaseUnreachable.sub')],
|
|
@@ -132,7 +191,158 @@ export async function checkHealth() {
|
|
|
132
191
|
*/
|
|
133
192
|
function updateCmdText(projectDir) {
|
|
134
193
|
const cmdEl = $('loading-cmd-text');
|
|
135
|
-
if (cmdEl)
|
|
194
|
+
if (!cmdEl) return;
|
|
195
|
+
const commands = manualStartCommands({
|
|
196
|
+
projectDir,
|
|
197
|
+
install: stored(START_STORAGE.install),
|
|
198
|
+
entry: stored(START_STORAGE.entry),
|
|
199
|
+
windows: isWindowsPlatform(navigator),
|
|
200
|
+
});
|
|
201
|
+
// One box: the command for this install (the checkout's when it is not known).
|
|
202
|
+
cmdEl.textContent = commands[commands.length - 1].command;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ═══════════════════════════════════════════
|
|
206
|
+
// INTERNAL — Server offline: Start Server + reconnect
|
|
207
|
+
// ═══════════════════════════════════════════
|
|
208
|
+
|
|
209
|
+
function stopWatchingForServer() {
|
|
210
|
+
if (_offlineBridge) _offlineBridge.stop();
|
|
211
|
+
_offlineBridge = null;
|
|
212
|
+
poseMascot('asleep');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The lines a start writes under the button, emptied. */
|
|
216
|
+
function clearStartStatus() {
|
|
217
|
+
for (const id of ['loading-action-status', 'loading-action-elapsed', 'loading-action-hint', 'loading-action-log']) {
|
|
218
|
+
const el = $(id);
|
|
219
|
+
if (el) el.textContent = '';
|
|
220
|
+
}
|
|
221
|
+
const $trail = $('loading-action-trail');
|
|
222
|
+
if ($trail) $trail.replaceChildren();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The overlay's mascot as the shared rig, so a start can be acted out. The
|
|
227
|
+
* markup's own still drawing stays as the fallback when the rig cannot mount.
|
|
228
|
+
*/
|
|
229
|
+
function mountMascot() {
|
|
230
|
+
if (_mascot) return;
|
|
231
|
+
const host = $('loading-mascot');
|
|
232
|
+
if (!host) return;
|
|
233
|
+
try {
|
|
234
|
+
const still = host.querySelector('svg');
|
|
235
|
+
_mascot = createMascot(host, { width: 160, height: 80, pose: 'sleep', active: false });
|
|
236
|
+
if (still && still !== _mascot.el) still.style.display = 'none';
|
|
237
|
+
} catch {
|
|
238
|
+
_mascot = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 'asleep' is the still sleeping face (the overlay's own bob and z carry it); every other state plays its pose. */
|
|
243
|
+
function poseMascot(state) {
|
|
244
|
+
const host = $('loading-mascot');
|
|
245
|
+
if (host) host.setAttribute('data-state', state);
|
|
246
|
+
if (!_mascot) return;
|
|
247
|
+
try {
|
|
248
|
+
_mascot.setPose(MASCOT_POSE[state] || 'sleep');
|
|
249
|
+
_mascot.setActive(state !== 'asleep');
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Draw one snapshot of the start: the sentence (a live region, written only
|
|
255
|
+
* when it changes), the page's own clock, and the steps the launcher reported
|
|
256
|
+
* with the time each one happened. Nothing here is estimated.
|
|
257
|
+
*/
|
|
258
|
+
function renderStart(snap) {
|
|
259
|
+
const platform = isWindowsPlatform(navigator) ? 'windows' : isMacPlatform(navigator) ? 'mac' : 'linux';
|
|
260
|
+
const view = startStatusView(snap, { platform });
|
|
261
|
+
const say = (line) => (line ? t(`loading.start.${line.key}`, line.params) : '');
|
|
262
|
+
|
|
263
|
+
poseMascot(view.mascot);
|
|
264
|
+
|
|
265
|
+
const $text = $('loading-text');
|
|
266
|
+
if ($text) {
|
|
267
|
+
const title = view.mascot === 'waking' || view.mascot === 'working' ? t('loading.start.headingStarting')
|
|
268
|
+
: view.mascot === 'ready' ? t('loading.start.headingReady')
|
|
269
|
+
: view.mascot === 'failed' ? t('loading.start.headingFailed')
|
|
270
|
+
: _offlineTitle || t('loading.serverOffline');
|
|
271
|
+
if ($text.textContent !== title) $text.textContent = title;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const btn = $('loading-action-btn');
|
|
275
|
+
const $label = $('loading-action-label');
|
|
276
|
+
if (btn) btn.disabled = view.busy || view.state === 'online';
|
|
277
|
+
if ($label) {
|
|
278
|
+
$label.textContent = view.button === 'starting' ? t('loading.starting')
|
|
279
|
+
: view.button === 'retry' ? t('common.retry') : t('loading.startServer');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const $status = $('loading-action-status');
|
|
283
|
+
const line = say(view.headline);
|
|
284
|
+
if ($status && $status.textContent !== line) $status.textContent = line;
|
|
285
|
+
|
|
286
|
+
const $elapsed = $('loading-action-elapsed');
|
|
287
|
+
if ($elapsed) {
|
|
288
|
+
$elapsed.textContent = view.elapsed;
|
|
289
|
+
$elapsed.setAttribute('aria-label', view.elapsed ? `${t('loading.start.elapsedLabel')}: ${view.elapsed}` : '');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const $hint = $('loading-action-hint');
|
|
293
|
+
if ($hint) $hint.textContent = say(view.hint);
|
|
294
|
+
|
|
295
|
+
const $trail = $('loading-action-trail');
|
|
296
|
+
if ($trail) {
|
|
297
|
+
$trail.replaceChildren(...view.trail.map((row) => {
|
|
298
|
+
const item = document.createElement('li');
|
|
299
|
+
const what = document.createElement('span');
|
|
300
|
+
what.textContent = t(`loading.start.${row.key}`);
|
|
301
|
+
const when = document.createElement('span');
|
|
302
|
+
when.className = 'at';
|
|
303
|
+
when.textContent = row.time;
|
|
304
|
+
item.append(what, when);
|
|
305
|
+
return item;
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const $log = $('loading-action-log');
|
|
310
|
+
if ($log) $log.textContent = view.error?.log ? t('loading.start.failedLog', { path: view.error.log }) : '';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* While the overlay says the server is offline: keep asking for it, and let
|
|
315
|
+
* the Start button hand the OS a synabun://start link. Reconnects by itself
|
|
316
|
+
* the moment the server answers, however it was started. While a start is
|
|
317
|
+
* under way it shows what the launcher itself reports (its beacon), never a
|
|
318
|
+
* guess: see start-bridge.js.
|
|
319
|
+
*/
|
|
320
|
+
function watchForServer() {
|
|
321
|
+
if (_offlineBridge) return;
|
|
322
|
+
mountMascot();
|
|
323
|
+
const beaconAt = startBeaconTarget(window.location);
|
|
324
|
+
const bridge = createStartBridge({
|
|
325
|
+
probe: async () => {
|
|
326
|
+
const health = await fetchHealth();
|
|
327
|
+
return !!health && health.ok !== false;
|
|
328
|
+
},
|
|
329
|
+
launch: () => openStartLink({ document, location: window.location, userAgent: navigator.userAgent }),
|
|
330
|
+
beacon: beaconAt ? () => fetchStartBeacon(beaconAt) : null,
|
|
331
|
+
port: beaconAt ? beaconAt.port : 0,
|
|
332
|
+
onUpdate(snap) {
|
|
333
|
+
if (_offlineBridge !== bridge) return;
|
|
334
|
+
renderStart(snap);
|
|
335
|
+
},
|
|
336
|
+
onState(state) {
|
|
337
|
+
if (_offlineBridge !== bridge) return;
|
|
338
|
+
if (state !== 'online') return;
|
|
339
|
+
resetLoadingToConnecting();
|
|
340
|
+
emit('loading:retried');
|
|
341
|
+
if (_initCallback) _initCallback();
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
_offlineBridge = bridge;
|
|
345
|
+
bridge.watch();
|
|
136
346
|
}
|
|
137
347
|
|
|
138
348
|
// ═══════════════════════════════════════════
|
|
@@ -144,6 +354,9 @@ function updateCmdText(projectDir) {
|
|
|
144
354
|
* Calls /api/health/start and re-triggers init on success.
|
|
145
355
|
*/
|
|
146
356
|
async function handleStartAction() {
|
|
357
|
+
// Server offline: nothing to call, the OS starts it.
|
|
358
|
+
if (_offlineBridge) { _offlineBridge.start(); return; }
|
|
359
|
+
|
|
147
360
|
const btn = $('loading-action-btn');
|
|
148
361
|
const $status = $('loading-action-status');
|
|
149
362
|
const $label = $('loading-action-label');
|
|
@@ -165,8 +378,6 @@ async function handleStartAction() {
|
|
|
165
378
|
if ($label) $label.textContent = t('common.retry');
|
|
166
379
|
}
|
|
167
380
|
} catch (err) {
|
|
168
|
-
// Server is unreachable — try protocol handler as fallback
|
|
169
|
-
window.location.href = 'synabun://start';
|
|
170
381
|
if ($status) $status.textContent = t('loading.somethingWrong');
|
|
171
382
|
if (btn) btn.disabled = false;
|
|
172
383
|
if ($label) $label.textContent = t('common.retry');
|
|
@@ -9,6 +9,7 @@ import { storage } from './storage.js';
|
|
|
9
9
|
import { KEYS } from './constants.js';
|
|
10
10
|
import { getMenuItems } from './registry.js';
|
|
11
11
|
import { openHelp } from './ui-help.js';
|
|
12
|
+
import { initPwaInstall, isStandalone, requestPwaInstall } from './pwa-install.js';
|
|
12
13
|
import { registerAction } from './ui-keybinds.js';
|
|
13
14
|
import { isGuest, hasPermission } from './ui-sync.js';
|
|
14
15
|
import { sendToPanel } from './ui-claude-panel.js';
|
|
@@ -83,7 +84,7 @@ export function initMenubar() {
|
|
|
83
84
|
const dropdown = document.querySelector('.menubar-item.open .menubar-dropdown');
|
|
84
85
|
if (!dropdown) return;
|
|
85
86
|
|
|
86
|
-
const allItems = [...dropdown.querySelectorAll('.menu-item:not(.disabled)')];
|
|
87
|
+
const allItems = [...dropdown.querySelectorAll('.menu-item:not(.disabled):not([hidden])')];
|
|
87
88
|
if (allItems.length === 0) return;
|
|
88
89
|
|
|
89
90
|
const focused = dropdown.querySelector('.menu-item.focused');
|
|
@@ -413,16 +414,9 @@ function wireTerminalMenu() {
|
|
|
413
414
|
'menu-command-runner': () => emit('command-runner:open'),
|
|
414
415
|
'menu-terminal-link': () => emit('link:toggle'),
|
|
415
416
|
'menu-terminal-toggle': () => emit('terminal:toggle'),
|
|
416
|
-
'menu-install-app':
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const { outcome } = await window._pwaInstallPrompt.userChoice;
|
|
420
|
-
if (outcome === 'accepted') {
|
|
421
|
-
window._pwaInstallPrompt = null;
|
|
422
|
-
const el = $('menu-install-app');
|
|
423
|
-
if (el) el.style.display = 'none';
|
|
424
|
-
}
|
|
425
|
-
}
|
|
417
|
+
'menu-install-app': async () => {
|
|
418
|
+
const result = await requestPwaInstall();
|
|
419
|
+
if (result === 'instructions') location.assign('/install-app.html');
|
|
426
420
|
},
|
|
427
421
|
'menu-restart-server': async () => {
|
|
428
422
|
try {
|
|
@@ -478,23 +472,13 @@ function wireTerminalMenu() {
|
|
|
478
472
|
registerAction('toggle-terminal', () => emit('terminal:toggle'));
|
|
479
473
|
registerAction('toggle-terminal-alt', () => emit('terminal:toggle'));
|
|
480
474
|
|
|
481
|
-
|
|
482
|
-
window.addEventListener('beforeinstallprompt', (e) => {
|
|
483
|
-
e.preventDefault();
|
|
484
|
-
window._pwaInstallPrompt = e;
|
|
485
|
-
const el = $('menu-install-app');
|
|
486
|
-
if (el) el.style.display = '';
|
|
487
|
-
});
|
|
488
|
-
window.addEventListener('appinstalled', () => {
|
|
489
|
-
window._pwaInstallPrompt = null;
|
|
475
|
+
initPwaInstall({ changed: value => {
|
|
490
476
|
const el = $('menu-install-app');
|
|
491
|
-
if (el) el.style.display = 'none';
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
if (el) el.style.display = 'none';
|
|
497
|
-
}
|
|
477
|
+
if (el) el.style.display = value === 'installed' ? 'none' : '';
|
|
478
|
+
} });
|
|
479
|
+
const installItem = $('menu-install-app');
|
|
480
|
+
if (installItem && !isStandalone()) installItem.style.display = '';
|
|
481
|
+
|
|
498
482
|
}
|
|
499
483
|
|
|
500
484
|
// ── Variant menu items injection ──
|
|
@@ -3,6 +3,7 @@ import { buildJudgmentsTab, wireJudgmentsTab } from './ui-judgments.js';
|
|
|
3
3
|
import { buildWhatsAppTab, wireWhatsAppTab } from './ui-whatsapp.js';
|
|
4
4
|
import { SETTINGS_GROUPS, SETTINGS_PAGES, DEFAULT_SETTINGS_PAGE, pageKeys, getSettingsPage, resolveSettingsTarget, homeOfControl } from './settings/settings-ia.js';
|
|
5
5
|
import * as kit from './settings/settings-kit.js';
|
|
6
|
+
import { RULES_HOST_LABELS, describeRulesHost, describeRulesInstall, describeRulesInstallAll } from './settings/settings-rules.js';
|
|
6
7
|
import { initI18n, isReady as i18nReady, SUPPORTED_LOCALES, LOCALE_NAMES, SYSTEM_LOCALE, getLocale, getLocaleChoice, setLocaleChoice, getSystemLocale, englishTranslator, loadEnglishTranslator } from './i18n.js';
|
|
7
8
|
import { wireSettingsShell, wireSettingsSearch, collectSearchRows, revealSettingsTarget, followSettingsTarget, onSettingsTakeover, applyStoredSections, storeSectionState } from './settings/settings-shell.js';
|
|
8
9
|
import { buildSettingsSearchIndex } from './settings/settings-search.js';
|
|
@@ -44,7 +45,8 @@ const TAB_ICONS = {
|
|
|
44
45
|
collections: '<svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/></svg>',
|
|
45
46
|
projects: '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
|
|
46
47
|
memory: '<svg viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 0-7 7c0 2.38 1.19 4.47 3 5.74V17a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-2.26c1.81-1.27 3-3.36 3-5.74a7 7 0 0 0-7-7z"/><line x1="9" y1="21" x2="15" y2="21"/><line x1="10" y1="24" x2="14" y2="24"/></svg>',
|
|
47
|
-
|
|
48
|
+
// TypeSafe's official mark (traced from their 400px favicon, filled: the nav CSS strokes every other icon)
|
|
49
|
+
judgments: '<svg viewBox="0 0 24 24"><path style="fill:currentColor;stroke:none" fill-rule="evenodd" d="M11.88 0 3.74 5.35 3.71 14.28 7.47 16.85 7.55 21.12 12.04 24 20.26 18.62 20.29 9.53 16.53 6.96 16.45 2.86 12.2 0.05ZM12.93 2.36 12.8 2.45 12.8 4.55 14.94 5.89 14.95 3.74ZM10.99 2.44 5.24 6.23 5.24 12.35 5.36 12.47 7.47 11.06 7.54 7.02 11.2 4.55 11.2 2.53ZM11.96 5.98 9.75 7.44 11.96 8.93 14.25 7.44ZM9.14 8.96 9.02 9.05 9.02 11.06 11.16 12.45 11.2 10.34ZM14.86 8.96 12.83 10.34 12.76 14.38 9.05 16.85 9.02 19.03 9.14 19.17 14.98 15.33 14.98 9.05ZM16.63 8.96 16.51 9.05 16.51 15.24 16.71 15.53 18.64 16.76 18.76 16.61 18.76 10.42ZM8.18 12.5 5.99 13.96 8.26 15.45 10.5 13.96ZM15.58 16.84 9.85 20.55 9.79 20.74 12.04 22.18 17.91 18.37 17.99 18.2 17.35 17.75 15.98 16.87Z"/></svg>',
|
|
48
50
|
interface: '<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="9" y1="9" x2="21" y2="9"/></svg>',
|
|
49
51
|
graphics: '<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
|
|
50
52
|
icons: '<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>',
|
|
@@ -5199,13 +5201,9 @@ export async function openSettingsModal(options = {}) {
|
|
|
5199
5201
|
// every action. `null` means the route is missing or failed, which is what
|
|
5200
5202
|
// a server that was not restarted after the update answers: the tab keeps
|
|
5201
5203
|
// View and Copy and says what to do, and offers no control that cannot work.
|
|
5204
|
+
// What a state is called, the action it calls for and the sentence next to it come from
|
|
5205
|
+
// settings/settings-rules.js, which the onboarding wizard reads too.
|
|
5202
5206
|
const RESTART_HINT = kit.tx('settings.redesign.setup.restartSynabunToFinishThisUpdate');
|
|
5203
|
-
const RULES_HOST_LABELS = { claude: 'Claude Code', codex: 'Codex', opencode: 'OpenCode', gemini: 'Gemini', cursor: 'Cursor', coexistence: 'coexistence' };
|
|
5204
|
-
const RULES_BADGES = {
|
|
5205
|
-
installed: [kit.tx('settings.redesign.setup.installed'), true], newer: [kit.tx('settings.redesign.setup.newer'), true], outdated: [kit.tx('settings.redesign.setup.updateAvailable'), false], modified: [kit.tx('settings.redesign.setup.edited2'), false],
|
|
5206
|
-
conflict: [kit.tx('settings.redesign.setup.conflict'), false], error: [kit.tx('settings.redesign.setup.error'), false], shadowed: [kit.tx('settings.redesign.setup.shadowed'), false],
|
|
5207
|
-
'not-installed': [kit.tx('settings.redesign.setup.notInstalled'), false], manual: [kit.tx('settings.redesign.setup.copyOnly'), false],
|
|
5208
|
-
};
|
|
5209
5207
|
let rulesStatus = setupStatus.rules?.ok ? setupStatus.rules : null;
|
|
5210
5208
|
// What the last "install for all" wrote and where, shown under that button until the next rules action.
|
|
5211
5209
|
let installNote = '';
|
|
@@ -5277,40 +5275,18 @@ export async function openSettingsModal(options = {}) {
|
|
|
5277
5275
|
const row = el('row');
|
|
5278
5276
|
|
|
5279
5277
|
rulesRenderers.push(() => {
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
row.classList.toggle('enabled', good);
|
|
5293
|
-
pathEl.textContent = info.installedVersion ? `${info.path} (v${info.installedVersion})` : (info.path || '');
|
|
5294
|
-
pathEl.title = info.path || '';
|
|
5295
|
-
// The action the state calls for; conflict, error and shadowed only explain themselves.
|
|
5296
|
-
const action = info.detected === false ? '' : ({ 'not-installed': kit.tx('settings.redesign.setup.install'), outdated: kit.tx('settings.redesign.setup.update'), modified: kit.tx('settings.redesign.setup.replace') }[info.state] || '');
|
|
5297
|
-
// A file without SynaBun's markers is the user's own: the server refuses to replace it, so no button.
|
|
5298
|
-
const canAct = !!action && info.replaceable !== false;
|
|
5299
|
-
primary.textContent = canAct ? action : '';
|
|
5300
|
-
setShown(primary, canAct);
|
|
5301
|
-
setShown(removeBtn, ['installed', 'newer', 'outdated', 'modified', 'shadowed'].includes(info.state));
|
|
5302
|
-
// An edited copy is the user's: nothing replaces or removes it without the confirm behind each button.
|
|
5303
|
-
const editedNote = kit.tx('settings.redesign.setup.thisCopyWasEditedSoSynabun');
|
|
5304
|
-
const stateNote = info.detected === false ? kit.tx('settings.redesign.setup.wasNotFoundOnThisMachine', { label: label })
|
|
5305
|
-
: info.state === 'modified' ? (info.replaceable === false ? (info.detail || '') : editedNote)
|
|
5306
|
-
: info.state === 'outdated' ? (rulesStatus.version ? kit.tx('settings.redesign.setup.versionOfTheRulesIsAvailable', { version: rulesStatus.version }) : kit.tx('settings.redesign.setup.aNewerVersionOfTheRules'))
|
|
5307
|
-
: ['conflict', 'error', 'shadowed', 'newer'].includes(info.state) ? (info.error || info.detail || '')
|
|
5308
|
-
: '';
|
|
5309
|
-
// A removal that could not finish left rules behind: say which, until a remove or an install completes.
|
|
5310
|
-
const partial = info.partialRemoval?.error ? kit.tx('settings.redesign.setup.theLastRemovalDidNotFinish', { error: info.partialRemoval.error }) : '';
|
|
5311
|
-
const note = partial && stateNote && !partial.includes(stateNote) ? `${partial} ${stateNote}` : (partial || stateNote);
|
|
5312
|
-
message.textContent = note;
|
|
5313
|
-
setShown(message, !!note);
|
|
5278
|
+
// No status (the route is missing): "Unavailable", the restart hint as the path, and no action.
|
|
5279
|
+
const view = describeRulesHost(rulesStatus?.hosts?.[host], { label, version: rulesStatus?.version, tx: kit.tx });
|
|
5280
|
+
badge.className = `setup-status-badge ${view.good ? 'active' : 'inactive'}`;
|
|
5281
|
+
badge.textContent = view.badge;
|
|
5282
|
+
row.classList.toggle('enabled', view.good);
|
|
5283
|
+
pathEl.textContent = view.pathText;
|
|
5284
|
+
pathEl.title = view.path;
|
|
5285
|
+
primary.textContent = view.actionLabel;
|
|
5286
|
+
setShown(primary, !!view.action);
|
|
5287
|
+
setShown(removeBtn, view.removable);
|
|
5288
|
+
message.textContent = view.note;
|
|
5289
|
+
setShown(message, !!view.note);
|
|
5314
5290
|
});
|
|
5315
5291
|
|
|
5316
5292
|
primary.addEventListener('click', () => {
|
|
@@ -5318,9 +5294,7 @@ export async function openSettingsModal(options = {}) {
|
|
|
5318
5294
|
const force = state === 'modified';
|
|
5319
5295
|
if (force && !confirm(kit.tx('settings.redesign.setup.replaceYourEditedRulesWithSynabun', { label: label }))) return;
|
|
5320
5296
|
cachedText = '';
|
|
5321
|
-
runRulesAction(primary, () => installRules(host, { force }), (result) => (result.
|
|
5322
|
-
? kit.tx(state === 'not-installed' ? 'settings.redesign.setup.rulesInstalled2' : 'settings.redesign.setup.rulesUpdated2', { label })
|
|
5323
|
-
: kit.tx('settings.redesign.setup.rulesAreAlreadyCurrent', { label: label })));
|
|
5297
|
+
runRulesAction(primary, () => installRules(host, { force }), (result) => describeRulesInstall(result, state, { label, tx: kit.tx }));
|
|
5324
5298
|
});
|
|
5325
5299
|
removeBtn.addEventListener('click', () => {
|
|
5326
5300
|
const info = rulesStatus?.hosts?.[host];
|
|
@@ -5399,15 +5373,10 @@ export async function openSettingsModal(options = {}) {
|
|
|
5399
5373
|
});
|
|
5400
5374
|
installAllBtn.addEventListener('click', () => {
|
|
5401
5375
|
runRulesAction(installAllBtn, () => installAllRules(), (result) => {
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
if (where) installNote = kit.tx('settings.redesign.setup.rulesInstalledAt', { where: where });
|
|
5407
|
-
const failed = rows.filter(([, row]) => row.ok === false);
|
|
5408
|
-
if (failed.length) return kit.tx('settings.redesign.setup.installedWithFailure', { count: rows.length - failed.length, host: hostOf[failed[0][0]] || failed[0][0], error: failed[0][1].error || failed[0][1].state });
|
|
5409
|
-
const map = rows.map(([host]) => hostOf[host] || host).join(', ');
|
|
5410
|
-
return kit.tx(rows.some(([, row]) => row.changed) ? 'settings.redesign.setup.rulesInstalledFor' : 'settings.redesign.setup.rulesAlreadyCurrentFor', { map: map });
|
|
5376
|
+
// The sentence for the toast, and under the button the file each tool reads.
|
|
5377
|
+
const report = describeRulesInstallAll(result, { tx: kit.tx });
|
|
5378
|
+
installNote = report.note;
|
|
5379
|
+
return report.message;
|
|
5411
5380
|
});
|
|
5412
5381
|
});
|
|
5413
5382
|
}
|
|
@@ -5418,6 +5387,15 @@ export async function openSettingsModal(options = {}) {
|
|
|
5418
5387
|
wireCopyBtn('setup-claude-cli-copy', () => setupStatus.claude?.cliCommand || ccIntegrations.mcp?.cliCommand || '', kit.tx('settings.redesign.setup.copyCliCommand'));
|
|
5419
5388
|
wireRulesControls('claude', 'claude');
|
|
5420
5389
|
|
|
5390
|
+
// What an MCP client runs: `node <script>` for an npm or Git install, the
|
|
5391
|
+
// application's own executable for a packaged one. The server says which.
|
|
5392
|
+
const mcpLaunch = () => {
|
|
5393
|
+
const paths = setupStatus.paths || {};
|
|
5394
|
+
return paths.mcpCommand && paths.mcpCommand !== 'node' && Array.isArray(paths.mcpArgs)
|
|
5395
|
+
? { command: paths.mcpCommand, args: paths.mcpArgs }
|
|
5396
|
+
: { command: 'node', args: [paths.mcpIndexPath || '<path-to>/mcp-server/run.mjs'] };
|
|
5397
|
+
};
|
|
5398
|
+
|
|
5421
5399
|
// ── Gemini ──
|
|
5422
5400
|
wireSetupMcpToggle('gemini', '/api/setup/gemini/mcp', '~/.gemini/settings.json');
|
|
5423
5401
|
// Config preview
|
|
@@ -5430,7 +5408,7 @@ export async function openSettingsModal(options = {}) {
|
|
|
5430
5408
|
cachedConfig = JSON.stringify(data.config, null, 2);
|
|
5431
5409
|
preview.textContent = cachedConfig;
|
|
5432
5410
|
} else if (data.ok) {
|
|
5433
|
-
cachedConfig = JSON.stringify({ mcpServers: { SynaBun: {
|
|
5411
|
+
cachedConfig = JSON.stringify({ mcpServers: { SynaBun: { ...mcpLaunch(), env: { DOTENV_PATH: setupStatus.paths?.envPath || '<path-to>/synabun/.env' } } } }, null, 2);
|
|
5434
5412
|
preview.textContent = cachedConfig;
|
|
5435
5413
|
} else { preview.textContent = kit.tx('settings.redesign.setup.couldNotLoadConfig'); }
|
|
5436
5414
|
}).catch(() => { preview.textContent = kit.tx('settings.redesign.setup.failedToLoad'); });
|
|
@@ -5452,10 +5430,11 @@ export async function openSettingsModal(options = {}) {
|
|
|
5452
5430
|
cachedConfig = data.toml;
|
|
5453
5431
|
preview.textContent = cachedConfig;
|
|
5454
5432
|
} else if (data.ok) {
|
|
5455
|
-
const
|
|
5433
|
+
const launch = mcpLaunch();
|
|
5456
5434
|
const ep = setupStatus.paths?.envPath || '<path-to>/synabun/.env';
|
|
5457
5435
|
const dataHome = setupStatus.paths?.dataHome || '<path-to>/synabun';
|
|
5458
|
-
|
|
5436
|
+
// A JSON string is a TOML basic string: the same quotes and escapes.
|
|
5437
|
+
cachedConfig = `[mcp_servers.SynaBun]\ncommand = ${JSON.stringify(launch.command)}\nargs = [${launch.args.map(arg => JSON.stringify(arg)).join(', ')}]\nenv = { DOTENV_PATH = "${ep}", SYNABUN_DATA_HOME = "${dataHome}", MEMORY_DATA_DIR = "${dataHome}/mcp-data", SYNABUN_PROFILE = "full", SYNABUN_BROWSER_FAST = "1", SYNABUN_BROWSER_COMPACT = "1", SYNABUN_TOOL_CATALOG_MODE = "deferred" }`;
|
|
5459
5438
|
preview.textContent = cachedConfig;
|
|
5460
5439
|
} else { preview.textContent = kit.tx('settings.redesign.setup.couldNotLoadConfig'); }
|
|
5461
5440
|
}).catch(() => { preview.textContent = kit.tx('settings.redesign.setup.failedToLoad'); });
|
|
@@ -5471,7 +5450,6 @@ export async function openSettingsModal(options = {}) {
|
|
|
5471
5450
|
const preview = overlay.querySelector('#setup-opencode-config-preview');
|
|
5472
5451
|
let cachedConfig = '';
|
|
5473
5452
|
if (preview) {
|
|
5474
|
-
const mp = setupStatus.paths?.mcpIndexPath || '<path-to>/mcp-server/run.mjs';
|
|
5475
5453
|
const ep = setupStatus.paths?.envPath || '<path-to>/synabun/.env';
|
|
5476
5454
|
fetch('/api/opencode/mcp').then(r => r.json()).then(data => {
|
|
5477
5455
|
if (data.ok && data.data && Object.keys(data.data).length) {
|
|
@@ -5482,8 +5460,7 @@ export async function openSettingsModal(options = {}) {
|
|
|
5482
5460
|
mcp: {
|
|
5483
5461
|
SynaBun: {
|
|
5484
5462
|
type: 'stdio',
|
|
5485
|
-
|
|
5486
|
-
args: [mp],
|
|
5463
|
+
...mcpLaunch(),
|
|
5487
5464
|
env: { DOTENV_PATH: ep },
|
|
5488
5465
|
},
|
|
5489
5466
|
},
|