synabun 2.0.5 → 2.0.7
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 +40 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/shared.mjs +6 -3
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +627 -57
- package/neural-interface/i18n/pt-BR.json +626 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +255 -52
- package/neural-interface/lib/assistant-brains/claude.js +15 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-config.js +62 -14
- package/neural-interface/lib/assistant-dispatch.js +432 -125
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-persona.js +68 -11
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +307 -47
- package/neural-interface/lib/assistant-user-writer.js +4 -2
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +90 -3
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +3 -3
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-read.js +34 -2
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +16 -3
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/offline.html +16 -1
- package/neural-interface/public/onboarding.html +96 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
- package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
- package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
- package/neural-interface/public/shared/assistant/asst-render.js +38 -7
- package/neural-interface/public/shared/assistant/asst-route.js +72 -14
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +1 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +46 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-panel.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +1 -1
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/html-shell.js +1 -0
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
- package/neural-interface/public/shared/pwa-install-page.js +3 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-ia.js +15 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +278 -31
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +212 -213
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-loading.js +30 -6
- package/neural-interface/public/shared/ui-menubar.js +13 -3
- package/neural-interface/public/shared/ui-navbar.js +3 -1
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +55 -3
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +6 -0
- package/neural-interface/public/shared/ui-terminal.js +17 -0
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +2 -0
- package/neural-interface/public/sw.js +1 -1
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/server.js +1219 -608
- package/package.json +4 -1
- package/setup.js +9 -8
|
@@ -174,11 +174,29 @@ export function getLocaleChoice() {
|
|
|
174
174
|
return matchLocale(saved) || SYSTEM_LOCALE;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The locale the page address asks for with `?lang=<tag>`, matched like a stored choice
|
|
179
|
+
* ('pt' → 'pt-BR'), or null (no tag, or one no locale answers). It holds for this page
|
|
180
|
+
* load only and is never stored: showcase captures use it to get an English UI without
|
|
181
|
+
* touching the user's own choice.
|
|
182
|
+
*/
|
|
183
|
+
export function getAddressLocale() {
|
|
184
|
+
const search = globalThis.window?.location?.search;
|
|
185
|
+
if (!search) return null;
|
|
186
|
+
try {
|
|
187
|
+
return matchLocale(new URLSearchParams(search).get('lang'));
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
177
193
|
/**
|
|
178
194
|
* Detect the user's preferred locale.
|
|
179
|
-
* Priority: stored explicit choice → system language → 'en'
|
|
195
|
+
* Priority: the page address (?lang=) → stored explicit choice → system language → 'en'
|
|
180
196
|
*/
|
|
181
197
|
function detectLocale() {
|
|
198
|
+
const address = getAddressLocale();
|
|
199
|
+
if (address) return address;
|
|
182
200
|
const choice = getLocaleChoice();
|
|
183
201
|
return choice === SYSTEM_LOCALE ? getSystemLocale() : choice;
|
|
184
202
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — the setup wizard's Details: what they say
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Inside the SynaBun desktop app, the setup wizard (onboarding.html) shows
|
|
6
|
+
// what it checked and what this copy has, step by step, in a Details area
|
|
7
|
+
// under Environment Check and under Install & Build: the commands SynaBun ran
|
|
8
|
+
// and what they answered, where `npm i -g` puts packages, and every part's
|
|
9
|
+
// dependencies with the version installed. The facts are
|
|
10
|
+
// GET /api/setup/install-details (lib/install-details.js); this turns them into
|
|
11
|
+
// the lines shown. English, like the rest of the wizard.
|
|
12
|
+
//
|
|
13
|
+
// No imports and no DOM: `node --test` loads this file as it is
|
|
14
|
+
// (tests/install-details-view.test.mjs).
|
|
15
|
+
|
|
16
|
+
const said = (tool) => (tool.ok ? tool.output || '(no answer)' : `did not run: ${tool.output || 'no output'}`);
|
|
17
|
+
|
|
18
|
+
/** Environment Check: the Node.js SynaBun runs on, its npm and npx (the command and the answer), and the user's own npm. */
|
|
19
|
+
export function environmentLines(details) {
|
|
20
|
+
if (!details || typeof details !== 'object') return [];
|
|
21
|
+
const lines = [];
|
|
22
|
+
const node = details.node;
|
|
23
|
+
if (node) {
|
|
24
|
+
lines.push('$ node --version');
|
|
25
|
+
lines.push(`${node.version} ${node.path}${node.bundled ? ' (the Node.js that came with SynaBun)' : ''}`);
|
|
26
|
+
}
|
|
27
|
+
for (const name of ['npm', 'npx']) {
|
|
28
|
+
const tool = details[name];
|
|
29
|
+
if (!tool) { lines.push(`${name}: not found`); continue; }
|
|
30
|
+
lines.push(`$ ${tool.command}`);
|
|
31
|
+
lines.push(`${said(tool)}${tool.ok && tool.bundled ? ' (came with SynaBun)' : ''}`);
|
|
32
|
+
}
|
|
33
|
+
const prefix = details.globalPrefix;
|
|
34
|
+
if (prefix?.path) {
|
|
35
|
+
const setIn = prefix.from === 'npm_config_prefix' ? ', from npm_config_prefix' : prefix.from ? `, set in ${prefix.from}` : '';
|
|
36
|
+
const why = prefix.source === 'user' ? `your own npm prefix${setIn}` : 'a folder of SynaBun\'s, outside the app';
|
|
37
|
+
lines.push(`npm i -g installs into ${prefix.path} (${why})${prefix.bin ? `, commands in ${prefix.bin}` : ''}`);
|
|
38
|
+
}
|
|
39
|
+
if (details.kind === 'app') {
|
|
40
|
+
const user = details.userNpm;
|
|
41
|
+
lines.push(user
|
|
42
|
+
? `Your own npm: ${user.path} ${user.ok ? user.output : '(did not answer)'}. SynaBun's terminals find it first.`
|
|
43
|
+
: 'No npm of your own: SynaBun\'s terminals use the one that came with SynaBun.');
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One part's packages: how many, then each dependency with the version installed. */
|
|
49
|
+
function partLines(part, bundled) {
|
|
50
|
+
const lines = [`${part.name}: ${part.packages} package${part.packages === 1 ? '' : 's'} installed${bundled ? ' (came with SynaBun)' : ''}`];
|
|
51
|
+
for (const dep of part.dependencies || []) {
|
|
52
|
+
lines.push(` ${dep.name} ${dep.installed || `not installed (wants ${dep.wanted})`}`);
|
|
53
|
+
}
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Install & Build: what this copy has, from the runtime to every dependency, and what is still to come. */
|
|
58
|
+
export function installLines(details) {
|
|
59
|
+
if (!details || typeof details !== 'object') return [];
|
|
60
|
+
const app = details.kind === 'app';
|
|
61
|
+
const lines = [];
|
|
62
|
+
if (app) {
|
|
63
|
+
const where = details.app?.target ? ` for ${details.app.target}` : '';
|
|
64
|
+
lines.push(`SynaBun ${details.app?.version || ''}${where}: nothing to install, this copy came with its dependencies.`.replace(/\s+:/, ':'));
|
|
65
|
+
}
|
|
66
|
+
if (details.node) {
|
|
67
|
+
const npm = details.npm?.ok ? `, npm ${details.npm.output}` : '';
|
|
68
|
+
const npx = details.npx?.ok ? `, npx ${details.npx.output}` : '';
|
|
69
|
+
lines.push(`Node.js ${String(details.node.version).replace(/^v/, '')}${npm}${npx}${details.node.bundled ? ' (the runtime that came with SynaBun)' : ''}`);
|
|
70
|
+
}
|
|
71
|
+
for (const part of details.parts || []) lines.push(...partLines(part, app));
|
|
72
|
+
lines.push(details.mcp?.built ? `MCP server: built (${details.mcp.file})` : 'MCP server: not built yet');
|
|
73
|
+
const bundled = details.bundled;
|
|
74
|
+
if (bundled) {
|
|
75
|
+
lines.push(bundled.embeddingModel ? `Local embedding model: ${bundled.embeddingModel} (came with SynaBun)` : 'Local embedding model: not included (keyword recall only)');
|
|
76
|
+
if (bundled.cloudflared) lines.push(`cloudflared ${bundled.cloudflared}, for invite links and remote MCP (came with SynaBun)`);
|
|
77
|
+
if (bundled.gitBash) lines.push(`Git for Windows ${bundled.gitBash}, the Git Bash Claude Code needs (came with SynaBun)`);
|
|
78
|
+
if (details.app?.electron) lines.push(`The window: Electron ${details.app.electron}`);
|
|
79
|
+
}
|
|
80
|
+
if (details.browser) {
|
|
81
|
+
lines.push(details.browser.installed
|
|
82
|
+
? `Automation browser (Chromium for Playwright): installed, ${details.browser.path}`
|
|
83
|
+
: 'Automation browser (Chromium for Playwright): not downloaded yet; SynaBun downloads it at its next start.');
|
|
84
|
+
}
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
@@ -50,6 +50,7 @@ import { registerSidepanel, syncSidepanelLayout, focusSidepanel, openAnotherSide
|
|
|
50
50
|
import { nativeLoopWindowId } from '../ui-native-window-id.js';
|
|
51
51
|
import { requestGeneratedSessionTitle } from '../session-title.js';
|
|
52
52
|
import { wireFileLinks } from '../ui-file-links.js';
|
|
53
|
+
import { mountCliBanner } from '../ui-cli-banner.js';
|
|
53
54
|
import { backToAssistant, buildWorkerEndCard, endWorkerRunOnClose, forgetWorkerEnd, trackWorkerEnd } from '../ui-worker-end.js';
|
|
54
55
|
import { isDispatchedRun } from '../worker-end.js';
|
|
55
56
|
import {
|
|
@@ -121,6 +122,8 @@ const _titleReassertTimers = new Map();
|
|
|
121
122
|
// rename carries is not the session's: nothing shows it or pushes it again.
|
|
122
123
|
const _pendingRenames = createPendingRenames();
|
|
123
124
|
let _composer = null; // the primary panel's composer (mountCompose)
|
|
125
|
+
let _cliBanner = null; // the CLI banner and send-block (ui-cli-banner.js)
|
|
126
|
+
let _cliWasBlocked = false; // the banner held sends back last time it said (a change to usable heals the serve)
|
|
124
127
|
// Where the panel goes. Every choice of the user (a tab, a session in the
|
|
125
128
|
// menu, New session, a project, a worktree) begins an intent, and a navigation
|
|
126
129
|
// binds the panel only while its intent is the latest one: a later choice wins
|
|
@@ -430,6 +433,30 @@ function buildPanel() {
|
|
|
430
433
|
// path is a path in the session's directory.
|
|
431
434
|
wireFileLinks(messagesContainer, { cwd: () => getState().cwd || '' });
|
|
432
435
|
|
|
436
|
+
// ── The CLI banner (ui-cli-banner.js) ───────────────────────────────────
|
|
437
|
+
// Above the transcript while OpenCode is not usable: not installed (with
|
|
438
|
+
// Install, which runs OpenCode's own installer or official release on the
|
|
439
|
+
// server), being installed, failed to install, or found but failed to start
|
|
440
|
+
// (with the error it gave). The panel gets `ocp-cli-blocked` meanwhile, and
|
|
441
|
+
// the composer holds sends back (sendBlocked below).
|
|
442
|
+
_cliBanner = mountCliBanner({
|
|
443
|
+
toolKey: 'opencode',
|
|
444
|
+
prefix: 'ocp',
|
|
445
|
+
panel: () => panel,
|
|
446
|
+
anchor: () => messagesContainer,
|
|
447
|
+
// OpenCode was installed (or found again) while its serve could not start: start it now rather than at the
|
|
448
|
+
// next retry of the auto-heal, whose wait has grown by then.
|
|
449
|
+
onBlockedChange: (blocked) => {
|
|
450
|
+
const usableAgain = _cliWasBlocked && !blocked;
|
|
451
|
+
_cliWasBlocked = blocked;
|
|
452
|
+
if (usableAgain && getState().serverStatus !== 'ready') {
|
|
453
|
+
clearTimeout(_healTimer);
|
|
454
|
+
_healTimer = null;
|
|
455
|
+
scheduleAutoHeal('cli-usable', 0);
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
433
460
|
// ── What the session changed on disk (session.diff) ─────────────────────
|
|
434
461
|
mountChangesBar(panel, getDefaultStore());
|
|
435
462
|
|
|
@@ -519,6 +546,10 @@ function buildPanel() {
|
|
|
519
546
|
// (ocp-v2-binding.js): the session the prompt or the Stop was for, frozen
|
|
520
547
|
// before the first await. It is never the live state.
|
|
521
548
|
canSend: (session) => !_automationRunning.has(session?.sessionId),
|
|
549
|
+
// While the CLI banner shows (OpenCode missing, broken or being installed),
|
|
550
|
+
// a send stays in the box and the banner is pointed at instead.
|
|
551
|
+
sendBlocked: () => !!_cliBanner?.blocked(),
|
|
552
|
+
onSendBlocked: () => _cliBanner?.nudge(),
|
|
522
553
|
onManualSend: (session, phase, meta) => {
|
|
523
554
|
const sessionId = session?.sessionId;
|
|
524
555
|
if (phase === 'started') requestOpenCodeSessionTitle(session, meta);
|
|
@@ -2210,6 +2241,7 @@ function applyVisibility(nextVisible) {
|
|
|
2210
2241
|
_panelEl.classList.toggle('ocpv2-open', _visible);
|
|
2211
2242
|
if (_visible) {
|
|
2212
2243
|
state.lastActivePanel = 'opencode';
|
|
2244
|
+
_cliBanner?.refresh();
|
|
2213
2245
|
} else {
|
|
2214
2246
|
// The popover is a child of <body>: it does not go with the panel by itself.
|
|
2215
2247
|
_contextMenu?.close();
|
|
@@ -966,6 +966,16 @@ export function mountCompose(rootEl, store = getDefaultStore(), opts = {}) {
|
|
|
966
966
|
syncButton();
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
+
// The panel holds sends back while its CLI banner shows (opts.sendBlocked,
|
|
970
|
+
// ui-cli-banner.js): nothing is sent, nothing is cleared, the banner is
|
|
971
|
+
// pointed at. A composer mounted without the option (a sub-agent's) never is.
|
|
972
|
+
function sendHeldBack() {
|
|
973
|
+
let held = false;
|
|
974
|
+
try { held = !!opts.sendBlocked?.(); } catch { held = false; }
|
|
975
|
+
if (held) { try { opts.onSendBlocked?.(); } catch {} }
|
|
976
|
+
return held;
|
|
977
|
+
}
|
|
978
|
+
|
|
969
979
|
function rememberPrompt(text) {
|
|
970
980
|
if (!text) return;
|
|
971
981
|
_history.push(text);
|
|
@@ -1038,6 +1048,7 @@ export function mountCompose(rootEl, store = getDefaultStore(), opts = {}) {
|
|
|
1038
1048
|
// read this snapshot.
|
|
1039
1049
|
const turn = captureBinding(store);
|
|
1040
1050
|
if (!turn.sessionId) { store.pushError({ message: 'No session. Reopen the panel to retry.' }); return false; }
|
|
1051
|
+
if (sendHeldBack()) return false;
|
|
1041
1052
|
if (opts.canSend && opts.canSend(turn) === false) {
|
|
1042
1053
|
store.pushError({ message: 'Stop the running automation before sending a manual turn.' });
|
|
1043
1054
|
return false;
|
|
@@ -1140,6 +1151,11 @@ export function mountCompose(rootEl, store = getDefaultStore(), opts = {}) {
|
|
|
1140
1151
|
const hasPaths = (s.pendingPaths?.length || 0) > 0;
|
|
1141
1152
|
|
|
1142
1153
|
if (_shell) {
|
|
1154
|
+
// The CLI is missing, broken or being installed (the panel's CLI banner
|
|
1155
|
+
// says so): the command stays in the box, in shell mode. (Prompts and
|
|
1156
|
+
// server commands are held back where they are sent, and come back into
|
|
1157
|
+
// the box from there; a local slash command needs no CLI.)
|
|
1158
|
+
if (sendHeldBack()) return false;
|
|
1143
1159
|
// Only what the user typed (or pasted) into the shell-mode composer runs.
|
|
1144
1160
|
// Anything that got into the box another way drops back to a prompt.
|
|
1145
1161
|
const allowed = shellRunAllowed({ shell: _shell, value: _input.value, typedValue: _shellTypedValue });
|
|
@@ -1224,6 +1240,7 @@ export function mountCompose(rootEl, store = getDefaultStore(), opts = {}) {
|
|
|
1224
1240
|
// first await; everything after it reads `turn`.
|
|
1225
1241
|
const turn = captureBinding(store);
|
|
1226
1242
|
const s = store.getState();
|
|
1243
|
+
if (sendHeldBack()) return false;
|
|
1227
1244
|
if (opts.canSend && opts.canSend(turn) === false) {
|
|
1228
1245
|
store.pushError({ message: 'Stop the running automation before sending a manual turn.' });
|
|
1229
1246
|
return false;
|
|
@@ -3567,6 +3567,124 @@ const PARITY_CSS = `
|
|
|
3567
3567
|
.ocpv2-help-name { flex-shrink: 0; color: rgba(232,224,220,0.95); font-family: 'JetBrains Mono', ui-monospace, monospace; }
|
|
3568
3568
|
.ocpv2-help-text { flex: 1; min-width: 0; color: rgba(255,255,255,0.62); overflow-wrap: anywhere; }
|
|
3569
3569
|
.ocpv2-help-row .ocpv2-session-item-meta { flex-shrink: 0; margin-left: 0; }
|
|
3570
|
+
|
|
3571
|
+
/* ── CLI banner (ui-cli-banner.js): OpenCode not installed / installing / failed to start ── */
|
|
3572
|
+
/* Mono like the rest of the panel; the panel's own amber, error red and green. */
|
|
3573
|
+
.ocp-cli-banner {
|
|
3574
|
+
flex-shrink: 0;
|
|
3575
|
+
display: grid;
|
|
3576
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
3577
|
+
gap: 10px;
|
|
3578
|
+
align-items: start;
|
|
3579
|
+
margin: 6px 8px 0;
|
|
3580
|
+
padding: 10px 12px;
|
|
3581
|
+
border: 1px solid rgba(232,180,80,0.32);
|
|
3582
|
+
border-radius: 8px;
|
|
3583
|
+
background: rgba(232,180,80,0.04);
|
|
3584
|
+
color: rgba(255,255,255,0.86);
|
|
3585
|
+
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
|
3586
|
+
font-size: 11px;
|
|
3587
|
+
line-height: 1.45;
|
|
3588
|
+
position: relative;
|
|
3589
|
+
z-index: 5;
|
|
3590
|
+
}
|
|
3591
|
+
.ocp-cli-banner [hidden] { display: none !important; }
|
|
3592
|
+
.ocp-cli-banner--broken, .ocp-cli-banner--failed { border-color: rgba(220,100,100,0.32); background: rgba(220,100,100,0.05); }
|
|
3593
|
+
.ocp-cli-banner--done { border-color: rgba(140,210,170,0.32); background: rgba(140,210,170,0.05); }
|
|
3594
|
+
.ocp-cli-banner-icon {
|
|
3595
|
+
width: 22px; height: 22px;
|
|
3596
|
+
display: flex; align-items: center; justify-content: center;
|
|
3597
|
+
border-radius: 50%;
|
|
3598
|
+
background: rgba(232,180,80,0.14);
|
|
3599
|
+
border: 1px solid rgba(232,180,80,0.28);
|
|
3600
|
+
color: rgba(255,200,120,0.95);
|
|
3601
|
+
font-weight: 700;
|
|
3602
|
+
}
|
|
3603
|
+
.ocp-cli-banner--broken .ocp-cli-banner-icon, .ocp-cli-banner--failed .ocp-cli-banner-icon { background: rgba(220,100,100,0.12); border-color: rgba(220,100,100,0.3); color: rgba(255,166,166,0.92); }
|
|
3604
|
+
.ocp-cli-banner--done .ocp-cli-banner-icon { background: rgba(140,210,170,0.14); border-color: rgba(140,210,170,0.28); color: rgba(180,230,200,0.92); }
|
|
3605
|
+
.ocp-cli-banner--installing .ocp-cli-banner-icon { animation: ocpv2-abort-sweep 1.2s ease-in-out infinite; }
|
|
3606
|
+
.ocp-cli-banner--nudge { animation: ocp-cli-nudge 0.5s ease; }
|
|
3607
|
+
@keyframes ocp-cli-nudge { 0%, 100% { transform: none; } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
|
|
3608
|
+
.ocp-cli-banner-text { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
|
3609
|
+
.ocp-cli-banner-title { font-size: 11.5px; font-weight: 600; color: rgba(255,200,120,0.95); }
|
|
3610
|
+
.ocp-cli-banner--broken .ocp-cli-banner-title, .ocp-cli-banner--failed .ocp-cli-banner-title { color: rgba(255,166,166,0.92); }
|
|
3611
|
+
.ocp-cli-banner--done .ocp-cli-banner-title { color: rgba(180,230,200,0.92); }
|
|
3612
|
+
.ocp-cli-banner-body, .ocp-cli-banner-note, .ocp-cli-banner-step { color: rgba(255,255,255,0.62); overflow-wrap: anywhere; }
|
|
3613
|
+
.ocp-cli-banner-command { display: flex; align-items: flex-start; gap: 6px; }
|
|
3614
|
+
.ocp-cli-banner-command code, .ocp-cli-banner-error code {
|
|
3615
|
+
flex: 1;
|
|
3616
|
+
min-width: 0;
|
|
3617
|
+
padding: 3px 6px;
|
|
3618
|
+
background: rgba(0,0,0,0.3);
|
|
3619
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
3620
|
+
border-radius: 4px;
|
|
3621
|
+
font: inherit;
|
|
3622
|
+
font-size: 10.5px;
|
|
3623
|
+
color: rgba(232,224,220,0.95);
|
|
3624
|
+
white-space: pre-wrap;
|
|
3625
|
+
word-break: break-all;
|
|
3626
|
+
user-select: text;
|
|
3627
|
+
}
|
|
3628
|
+
.ocp-cli-banner-error code { display: block; max-height: 96px; overflow: auto; color: rgba(255,166,166,0.92); }
|
|
3629
|
+
.ocp-cli-banner-copy {
|
|
3630
|
+
flex-shrink: 0;
|
|
3631
|
+
height: 22px;
|
|
3632
|
+
padding: 0 8px;
|
|
3633
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
3634
|
+
border-radius: 5px;
|
|
3635
|
+
background: transparent;
|
|
3636
|
+
color: rgba(255,255,255,0.62);
|
|
3637
|
+
font: inherit;
|
|
3638
|
+
font-size: 10.5px;
|
|
3639
|
+
cursor: pointer;
|
|
3640
|
+
}
|
|
3641
|
+
.ocp-cli-banner-copy:hover { color: rgba(252,246,240,0.98); border-color: rgba(232,224,220,0.3); }
|
|
3642
|
+
.ocp-cli-banner-progress { display: flex; flex-direction: column; gap: 4px; }
|
|
3643
|
+
.ocp-cli-banner-bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,0.08); overflow: hidden; }
|
|
3644
|
+
.ocp-cli-banner-bar > span { display: block; height: 100%; width: 0; background: rgba(232,224,220,0.7); transition: width 0.2s ease; }
|
|
3645
|
+
.ocp-cli-banner-log {
|
|
3646
|
+
margin: 0;
|
|
3647
|
+
max-height: 84px;
|
|
3648
|
+
overflow: auto;
|
|
3649
|
+
padding: 4px 6px;
|
|
3650
|
+
background: rgba(0,0,0,0.25);
|
|
3651
|
+
border-radius: 4px;
|
|
3652
|
+
font: inherit;
|
|
3653
|
+
font-size: 10px;
|
|
3654
|
+
line-height: 1.45;
|
|
3655
|
+
color: rgba(255,255,255,0.5);
|
|
3656
|
+
white-space: pre-wrap;
|
|
3657
|
+
word-break: break-all;
|
|
3658
|
+
}
|
|
3659
|
+
.ocp-cli-banner-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
|
|
3660
|
+
.ocp-cli-banner-install, .ocp-cli-banner-link, .ocp-cli-banner-path, .ocp-cli-banner-recheck, .ocp-cli-banner-dismiss {
|
|
3661
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
3662
|
+
height: 22px;
|
|
3663
|
+
padding: 0 10px;
|
|
3664
|
+
font: inherit;
|
|
3665
|
+
font-size: 11.5px;
|
|
3666
|
+
font-weight: 500;
|
|
3667
|
+
line-height: 1;
|
|
3668
|
+
border-radius: 5px;
|
|
3669
|
+
border: 1px solid rgba(255,255,255,0.14);
|
|
3670
|
+
background: rgba(255,255,255,0.04);
|
|
3671
|
+
color: rgba(255,255,255,0.86);
|
|
3672
|
+
text-decoration: none;
|
|
3673
|
+
cursor: pointer;
|
|
3674
|
+
user-select: none;
|
|
3675
|
+
}
|
|
3676
|
+
.ocp-cli-banner-install { background: rgba(232,224,220,0.15); border-color: rgba(232,224,220,0.3); color: rgba(245,239,232,0.95); }
|
|
3677
|
+
.ocp-cli-banner-install:hover:not(:disabled) { background: rgba(232,224,220,0.25); border-color: rgba(232,224,220,0.45); }
|
|
3678
|
+
.ocp-cli-banner-link:hover, .ocp-cli-banner-path:hover:not(:disabled), .ocp-cli-banner-recheck:hover:not(:disabled), .ocp-cli-banner-dismiss:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.26); }
|
|
3679
|
+
.ocp-cli-banner-install:disabled, .ocp-cli-banner-path:disabled, .ocp-cli-banner-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
3680
|
+
.ocp-cli-banner button:focus-visible, .ocp-cli-banner a:focus-visible { outline: 2px solid rgba(255,255,255,0.7); outline-offset: 2px; }
|
|
3681
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3682
|
+
.ocp-cli-banner--installing .ocp-cli-banner-icon, .ocp-cli-banner--nudge { animation: none; }
|
|
3683
|
+
.ocp-cli-banner--nudge { box-shadow: 0 0 0 1px rgba(232,180,80,0.6); }
|
|
3684
|
+
.ocp-cli-banner-bar > span { transition: none; }
|
|
3685
|
+
}
|
|
3686
|
+
.ocpv2-panel.ocp-cli-blocked .ocpv2-compose-btn:not(.ocpv2-compose-abort) { opacity: 0.4; cursor: not-allowed; }
|
|
3687
|
+
.ocpv2-panel.ocp-cli-blocked .ocpv2-compose-input { opacity: 0.7; }
|
|
3570
3688
|
`;
|
|
3571
3689
|
|
|
3572
3690
|
export function injectStyles() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canPromptPwaInstall, initPwaInstall, installGuideMode, installGuideRequested, isStandalone,
|
|
2
2
|
leavePwaInstallHandoff, requestPwaInstall } from './pwa-install.js';
|
|
3
|
+
import { isDesktopApp } from './desktop-bridge.js';
|
|
3
4
|
|
|
4
5
|
// The wizard (first run) and the main UI (setup done) open the browser's dialog from a press the user makes
|
|
5
6
|
// anyway, so a browser that has one shows this page only when Apps > Install as App asked for it. true, false,
|
|
@@ -12,7 +13,8 @@ async function setupComplete() {
|
|
|
12
13
|
} catch { return null; }
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
// Already the installed app, or the SynaBun desktop app's own window: nothing to install here.
|
|
17
|
+
if (isStandalone() || isDesktopApp()) location.replace('/');
|
|
16
18
|
else {
|
|
17
19
|
const pt = navigator.language.toLowerCase().startsWith('pt');
|
|
18
20
|
const copy = pt ? {
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SETTINGS — About (the pane)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// The last page of Settings: who SynaBun is and what it runs on. A header that is always shown (the app's icon,
|
|
6
|
+
// name, version and channel, how it was installed, the package's one-line description, "Check for updates" and
|
|
7
|
+
// "Copy info"), then three sections: Links, System, Dependencies, and the license line at the foot.
|
|
8
|
+
//
|
|
9
|
+
// It asks GET /api/system/about once, when the page is first shown. A server from before that route (404, or not
|
|
10
|
+
// JSON) gets the header and the version from /api/health and /api/system/version, and a note that the rest comes
|
|
11
|
+
// with a restart. settings-about.js decides what is said; this draws it.
|
|
12
|
+
//
|
|
13
|
+
// "Check for updates" is the Apps menu's own item (#menu-check-cli-updates, ui-menubar.js): the same check, the
|
|
14
|
+
// same toast. Links open in a new window, which the desktop app hands to the default browser.
|
|
15
|
+
|
|
16
|
+
import * as kit from './settings-kit.js';
|
|
17
|
+
import { revealSettingsTarget } from './settings-shell.js';
|
|
18
|
+
import { desktopBridge } from '../desktop-bridge.js';
|
|
19
|
+
import {
|
|
20
|
+
ABOUT_URL, ABOUT_FILTER_MIN, buildLabel, diagnosticsText, filterDependencies, installLine, isExternalUrl, linkHost, linkLabel,
|
|
21
|
+
npmUrl, osLine, packageLabel, readAboutAnswer, readClientRuntime, versionLine,
|
|
22
|
+
} from './settings-about.js';
|
|
23
|
+
|
|
24
|
+
const UPDATE_MENU_ITEM = 'menu-check-cli-updates';
|
|
25
|
+
const EXTERNAL = 'target="_blank" rel="noopener noreferrer"';
|
|
26
|
+
const ARROW = '<svg class="stg-about-arrow" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 4.5h6v6M11.5 4.5l-7 7"/></svg>';
|
|
27
|
+
|
|
28
|
+
export function buildAboutTab() {
|
|
29
|
+
return `
|
|
30
|
+
<div class="stg-pane stg-about" data-stg-pane="about">
|
|
31
|
+
<div class="stg-about-hero" id="stg-about-hero">
|
|
32
|
+
<img class="stg-about-logo" src="/icon-192.png" alt="" width="64" height="64" decoding="async">
|
|
33
|
+
<h3 class="stg-about-name">SynaBun</h3>
|
|
34
|
+
<p class="stg-about-version" id="stg-about-version" aria-live="polite">${kit.te('settings.redesign.about.loading')}</p>
|
|
35
|
+
<p class="stg-about-line" id="stg-about-install" hidden></p>
|
|
36
|
+
<p class="stg-about-line stg-about-desc" id="stg-about-desc" hidden></p>
|
|
37
|
+
<div class="stg-action-row stg-about-actions">
|
|
38
|
+
<button type="button" class="stg-action-btn" id="stg-about-update" ${kit.inv('ABT001')} title="${kit.H('ABT001')}">${kit.L('ABT001')}</button>
|
|
39
|
+
<button type="button" class="stg-action-btn" id="stg-about-copy" ${kit.inv('ABT002')} title="${kit.H('ABT002')}">${kit.L('ABT002')}</button>
|
|
40
|
+
</div>
|
|
41
|
+
<p class="stg-help stg-about-note" id="stg-about-note" role="status" hidden></p>
|
|
42
|
+
</div>
|
|
43
|
+
${kit.card({
|
|
44
|
+
id: 'stg-sec-about-links', section: 'about.links',
|
|
45
|
+
body: `<ul class="stg-about-links" id="stg-about-links" data-stg-search="off"></ul>`,
|
|
46
|
+
})}
|
|
47
|
+
${kit.card({
|
|
48
|
+
id: 'stg-sec-about-system', section: 'about.system',
|
|
49
|
+
body: `<dl class="stg-about-facts" id="stg-about-facts" data-stg-search="off"></dl>`,
|
|
50
|
+
})}
|
|
51
|
+
${kit.card({
|
|
52
|
+
id: 'stg-sec-about-dependencies', section: 'about.dependencies',
|
|
53
|
+
// how many dependencies the lists hold; empty until the server has answered
|
|
54
|
+
status: '<span class="stg-about-count" id="stg-about-deps-count"></span>',
|
|
55
|
+
body: `
|
|
56
|
+
<div class="stg-about-filter" ${kit.inv('ABT003')} hidden>
|
|
57
|
+
<input type="search" class="stg-input stg-about-filter-input" id="stg-about-filter" placeholder="${kit.te('settings.redesign.about.deps.filterPlaceholder')}" aria-label="${kit.L('ABT003')}" aria-describedby="stg-about-filter-help" autocomplete="off" autocapitalize="off" spellcheck="false" enterkeyhint="search">
|
|
58
|
+
<span class="stg-sr" id="stg-about-filter-help">${kit.H('ABT003')}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="stg-about-deps" id="stg-about-deps" data-stg-search="off"></div>`,
|
|
61
|
+
})}
|
|
62
|
+
<footer class="stg-about-footer" id="stg-about-footer" hidden></footer>
|
|
63
|
+
</div>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Drawing ──
|
|
67
|
+
|
|
68
|
+
function linkRow(link) {
|
|
69
|
+
return `<li><a class="stg-about-link" href="${kit.esc(link.url)}" ${EXTERNAL}>
|
|
70
|
+
<span class="stg-about-link-label">${kit.esc(linkLabel(link, kit.tx))}</span>
|
|
71
|
+
<span class="stg-about-link-url">${kit.esc(linkHost(link.url))}</span>${ARROW}</a></li>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fact(label, value, { mono = false } = {}) {
|
|
75
|
+
if (value == null || value === '') return '';
|
|
76
|
+
return `<div class="stg-about-fact"><dt>${label}</dt><dd${mono ? ' class="stg-about-mono"' : ''}>${kit.esc(String(value))}</dd></div>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function depRow(dep) {
|
|
80
|
+
const url = npmUrl(dep.name);
|
|
81
|
+
const name = url ? `<a class="stg-about-dep-name" href="${kit.esc(url)}" ${EXTERNAL}>${kit.esc(dep.name)}</a>` : `<span class="stg-about-dep-name">${kit.esc(dep.name)}</span>`;
|
|
82
|
+
const version = dep.installed
|
|
83
|
+
? `<span class="stg-about-dep-version">${kit.esc(dep.version)}</span>`
|
|
84
|
+
: `<span class="stg-about-dep-version" data-state="missing">${kit.esc(dep.declared)} · ${kit.te('settings.redesign.about.deps.notInstalled')}</span>`;
|
|
85
|
+
const meta = [
|
|
86
|
+
dep.installed && dep.declared && dep.declared !== dep.version ? kit.te('settings.redesign.about.deps.declared', { range: dep.declared }) : '',
|
|
87
|
+
dep.optional ? kit.te('settings.redesign.about.deps.optional') : '',
|
|
88
|
+
dep.license ? kit.esc(dep.license) : '',
|
|
89
|
+
].filter(Boolean).join(' · ');
|
|
90
|
+
return `<li class="stg-about-dep">${name}${version}${meta ? `<span class="stg-about-dep-meta">${meta}</span>` : ''}</li>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function depGroup(title, sub, deps, { note = '' } = {}) {
|
|
94
|
+
if (!deps.length) return '';
|
|
95
|
+
return `<div class="stg-about-group">
|
|
96
|
+
<h4 class="stg-subhead stg-about-group-head"><span>${title}</span>${sub ? `<span class="stg-about-group-sub">${sub}</span>` : ''}<span class="stg-about-count">${deps.length}</span></h4>
|
|
97
|
+
${note ? `<p class="stg-help stg-about-group-note">${note}</p>` : ''}
|
|
98
|
+
<ul class="stg-about-dep-list">${deps.map(depRow).join('')}</ul>
|
|
99
|
+
</div>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function drawDependencies(host, shown) {
|
|
103
|
+
const { packages, vendored, counts } = shown;
|
|
104
|
+
const runtime = packages.map((p) => depGroup(kit.esc(packageLabel(p, kit.tx)), kit.esc(p.name), p.runtime, { note: p.onDemand ? kit.te('settings.redesign.about.deps.onDemand') : '' })).join('');
|
|
105
|
+
const singles = vendored.filter((b) => b.packages.length === 1).flatMap((b) => b.packages);
|
|
106
|
+
const bundles = vendored.filter((b) => b.packages.length > 1)
|
|
107
|
+
.map((b) => depGroup(kit.esc(b.label), kit.esc(b.version || ''), b.packages)).join('');
|
|
108
|
+
const bundled = singles.length || bundles
|
|
109
|
+
? `<div class="stg-about-bundled">${depGroup(kit.te('settings.redesign.about.deps.vendored'), '', singles)}${bundles}</div>`
|
|
110
|
+
: '';
|
|
111
|
+
// A filter opens the group to show its matches; one the user opened stays open while they type.
|
|
112
|
+
const devOpen = !!shown.query || !!host.querySelector('.stg-about-dev')?.open;
|
|
113
|
+
const dev = counts.dev
|
|
114
|
+
? `<details class="stg-advanced stg-about-dev"${devOpen ? ' open' : ''}>
|
|
115
|
+
<summary>${kit.CHEVRON}<span>${kit.te('settings.redesign.about.deps.dev', { count: counts.dev })}</span></summary>
|
|
116
|
+
<div class="stg-advanced-body">${packages.map((p) => depGroup(kit.esc(packageLabel(p, kit.tx)), kit.esc(p.name), p.dev)).join('')}</div>
|
|
117
|
+
</details>`
|
|
118
|
+
: '';
|
|
119
|
+
const empty = counts.total ? '' : `<p class="stg-help">${shown.query ? kit.te('settings.redesign.about.deps.noMatch', { query: shown.query }) : kit.te('settings.redesign.about.deps.none')}</p>`;
|
|
120
|
+
host.innerHTML = `${runtime}${bundled}${dev}${empty}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load and draw the page the first time it is shown, and wire its buttons.
|
|
125
|
+
* @param {HTMLElement} overlay the Settings panel
|
|
126
|
+
* @param {Object} [o]
|
|
127
|
+
* @param {Function} [o.fetchImpl] fetch (tests)
|
|
128
|
+
* @param {(msg: string) => void} [o.toast] the panel's toast
|
|
129
|
+
* @param {Window} [o.win] where the user agent and the desktop app's bridge are read
|
|
130
|
+
*/
|
|
131
|
+
export function wireAboutTab(overlay, { fetchImpl = (...args) => fetch(...args), toast = () => {}, win = window } = {}) {
|
|
132
|
+
const pane = kit.paneOf(overlay, 'about');
|
|
133
|
+
if (!pane) return;
|
|
134
|
+
const $ = (id) => pane.querySelector(`#${id}`);
|
|
135
|
+
const client = readClientRuntime({ userAgent: win.navigator?.userAgent, desktop: desktopBridge(win) });
|
|
136
|
+
let model = null;
|
|
137
|
+
let loading = null;
|
|
138
|
+
|
|
139
|
+
const getJson = async (url) => {
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetchImpl(url, { headers: { Accept: 'application/json' } });
|
|
142
|
+
if (!res.ok) return null;
|
|
143
|
+
return await res.json().catch(() => null);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const show = (node, text) => {
|
|
150
|
+
if (!node) return;
|
|
151
|
+
node.textContent = text || '';
|
|
152
|
+
node.hidden = !text;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function drawHeader() {
|
|
156
|
+
const { app } = model;
|
|
157
|
+
show($('stg-about-version'), versionLine(app, kit.tx));
|
|
158
|
+
show($('stg-about-install'), installLine(app, kit.tx));
|
|
159
|
+
show($('stg-about-desc'), app.description);
|
|
160
|
+
show($('stg-about-note'), model.full ? '' : kit.tx('settings.redesign.about.restartNote'));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function drawLinks() {
|
|
164
|
+
const host = $('stg-about-links');
|
|
165
|
+
if (!host) return;
|
|
166
|
+
host.innerHTML = model.links.length
|
|
167
|
+
? model.links.filter((l) => isExternalUrl(l.url)).map(linkRow).join('')
|
|
168
|
+
: `<li class="stg-help stg-about-empty">${model.full ? kit.te('settings.redesign.about.links.none') : kit.te('settings.redesign.about.afterRestart')}</li>`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function drawFacts() {
|
|
172
|
+
const host = $('stg-about-facts');
|
|
173
|
+
if (!host) return;
|
|
174
|
+
const { app, runtime } = model;
|
|
175
|
+
host.innerHTML = [
|
|
176
|
+
fact(kit.te('settings.redesign.about.row.version'), app.version ? `${app.version} (${buildLabel(app, kit.tx)})` : ''),
|
|
177
|
+
fact(kit.te('settings.redesign.about.row.install'), installLine(app, kit.tx)),
|
|
178
|
+
fact('Node.js', runtime.node),
|
|
179
|
+
fact('SQLite', runtime.sqlite),
|
|
180
|
+
fact('Electron', client.electron),
|
|
181
|
+
fact('Chromium', client.chromium),
|
|
182
|
+
fact(kit.te('settings.redesign.about.row.os'), osLine(runtime, client)),
|
|
183
|
+
fact(kit.te('settings.redesign.about.row.dataFolder'), runtime.dataHome, { mono: true }),
|
|
184
|
+
fact(kit.te('settings.redesign.about.row.port'), runtime.port),
|
|
185
|
+
].join('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function drawDeps() {
|
|
189
|
+
const host = $('stg-about-deps');
|
|
190
|
+
const count = $('stg-about-deps-count');
|
|
191
|
+
const filter = pane.querySelector('.stg-about-filter');
|
|
192
|
+
const deps = model.dependencies;
|
|
193
|
+
if (!host) return;
|
|
194
|
+
if (!deps) {
|
|
195
|
+
host.innerHTML = `<p class="stg-help stg-about-empty">${kit.te('settings.redesign.about.afterRestart')}</p>`;
|
|
196
|
+
if (filter) filter.hidden = true;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (count) count.textContent = deps.counts.total ? String(deps.counts.total) : '';
|
|
200
|
+
if (filter) filter.hidden = deps.counts.total <= ABOUT_FILTER_MIN;
|
|
201
|
+
drawDependencies(host, filterDependencies(deps, $('stg-about-filter')?.value || ''));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function drawFooter() {
|
|
205
|
+
const host = $('stg-about-footer');
|
|
206
|
+
if (!host) return;
|
|
207
|
+
const { app } = model;
|
|
208
|
+
const lines = [];
|
|
209
|
+
if (app.license) lines.push(`<p>${kit.te('settings.redesign.about.footer.license', { license: app.license })}</p>`);
|
|
210
|
+
if (app.copyright) lines.push(`<p>${kit.te('settings.redesign.about.footer.copyright', { years: app.copyright.years, holder: app.copyright.holder })}</p>`);
|
|
211
|
+
if (model.dependencies?.counts.total) lines.push(`<p><button type="button" class="stg-about-builtwith" id="stg-about-builtwith">${kit.te('settings.redesign.about.footer.builtWith')}</button></p>`);
|
|
212
|
+
host.innerHTML = lines.join('');
|
|
213
|
+
host.hidden = !lines.length;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function draw() {
|
|
217
|
+
drawHeader();
|
|
218
|
+
drawLinks();
|
|
219
|
+
drawFacts();
|
|
220
|
+
drawDeps();
|
|
221
|
+
drawFooter();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function load() {
|
|
225
|
+
const about = await getJson(ABOUT_URL);
|
|
226
|
+
let answer = readAboutAnswer({ about });
|
|
227
|
+
if (!answer.full) {
|
|
228
|
+
// An older server: what it has always said.
|
|
229
|
+
const [health, version] = await Promise.all([getJson('/api/health'), getJson('/api/system/version')]);
|
|
230
|
+
answer = readAboutAnswer({ health, version });
|
|
231
|
+
}
|
|
232
|
+
if (!document.body.contains(overlay)) return;
|
|
233
|
+
model = answer;
|
|
234
|
+
draw();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ensure = () => { if (!loading) loading = load(); return loading; };
|
|
238
|
+
if (kit.paneActive(overlay, 'about')) ensure();
|
|
239
|
+
overlay.addEventListener('stg-page-change', (e) => { if (e.detail?.page === 'about') ensure(); });
|
|
240
|
+
|
|
241
|
+
// ── Buttons ──
|
|
242
|
+
|
|
243
|
+
const updateBtn = $('stg-about-update');
|
|
244
|
+
// The Apps menu's own item: no menu (an embed, a test page), no button.
|
|
245
|
+
if (updateBtn && !document.getElementById(UPDATE_MENU_ITEM)) updateBtn.hidden = true;
|
|
246
|
+
updateBtn?.addEventListener('click', () => document.getElementById(UPDATE_MENU_ITEM)?.click());
|
|
247
|
+
|
|
248
|
+
$('stg-about-copy')?.addEventListener('click', async () => {
|
|
249
|
+
if (!model) await ensure();
|
|
250
|
+
const body = diagnosticsText(model, client, { tx: kit.tx });
|
|
251
|
+
try {
|
|
252
|
+
await win.navigator.clipboard.writeText(body);
|
|
253
|
+
toast(kit.tx('settings.redesign.about.copied'));
|
|
254
|
+
} catch {
|
|
255
|
+
toast(kit.tx('settings.redesign.about.copyFailed'));
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
$('stg-about-filter')?.addEventListener('input', () => { if (model) drawDeps(); });
|
|
260
|
+
|
|
261
|
+
pane.addEventListener('click', (e) => {
|
|
262
|
+
if (!e.target.closest?.('#stg-about-builtwith')) return;
|
|
263
|
+
const section = $('stg-sec-about-dependencies');
|
|
264
|
+
const content = overlay.querySelector('.settings-content');
|
|
265
|
+
if (!section || !content) return;
|
|
266
|
+
revealSettingsTarget(overlay, section);
|
|
267
|
+
// Only the page scrolls, never the panel around it.
|
|
268
|
+
const top = section.getBoundingClientRect().top - content.getBoundingClientRect().top + content.scrollTop - 8;
|
|
269
|
+
content.scrollTo({ top: Math.max(0, top), behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
|
|
270
|
+
});
|
|
271
|
+
}
|