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
|
@@ -3,28 +3,40 @@
|
|
|
3
3
|
// Detects whether Claude / Codex / OpenCode CLIs are installed by
|
|
4
4
|
// querying /api/system/tool-versions. Caches the response, fans out
|
|
5
5
|
// updates to subscribers, and auto-polls (every 30s) while at least
|
|
6
|
-
// one subscribed tool
|
|
6
|
+
// one subscribed tool is not installed (missing, or found but broken).
|
|
7
|
+
//
|
|
8
|
+
// Each tool's answer carries `state` ('installed' | 'missing' | 'broken'),
|
|
9
|
+
// the version, where it was found, the error of a CLI that would not start,
|
|
10
|
+
// and `install`: whether the server can install it with one click from the
|
|
11
|
+
// vendor's official source. installCli() runs that install (POST
|
|
12
|
+
// /api/cli/install/:tool, an NDJSON stream) and re-checks when it ends.
|
|
13
|
+
// What the panels show for all of it is cli-install-view.js.
|
|
7
14
|
// ═══════════════════════════════════════════
|
|
8
15
|
|
|
16
|
+
import { cliStateOf, cliInstallOffer, INSTALLABLE_CLIS } from './cli-install-view.js';
|
|
17
|
+
|
|
18
|
+
export { cliStateOf, cliInstallOffer };
|
|
19
|
+
|
|
9
20
|
const TOOL_KEYS = ['claude-code', 'codex', 'opencode', 'gemini'];
|
|
10
21
|
|
|
22
|
+
// The vendors' install pages. The server's `guideUrl` wins when it sends one.
|
|
11
23
|
const CLI_DOC_URLS = {
|
|
12
|
-
'claude-code': 'https://
|
|
13
|
-
'codex': 'https://
|
|
24
|
+
'claude-code': 'https://code.claude.com/docs/en/setup',
|
|
25
|
+
'codex': 'https://learn.chatgpt.com/docs/codex/cli',
|
|
14
26
|
'opencode': 'https://opencode.ai/docs/',
|
|
15
27
|
'gemini': 'https://github.com/google-gemini/gemini-cli',
|
|
16
28
|
};
|
|
17
29
|
|
|
18
|
-
// Fallback only. The server computes the
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
30
|
+
// Fallback only. The server computes the command that fits the machine (the
|
|
31
|
+
// vendor's recommended installer for its system, or the package manager the
|
|
32
|
+
// other CLIs came from: buildInstallCommand in lib/cli-update-planner.js) and
|
|
33
|
+
// returns it as `installCommand`. These apply only before the first
|
|
34
|
+
// tool-versions response.
|
|
23
35
|
const CLI_INSTALL_CMDS = {
|
|
24
|
-
'claude-code': '
|
|
25
|
-
'codex': '
|
|
26
|
-
'opencode': 'npm install -g opencode-ai',
|
|
27
|
-
'gemini': 'npm install -g @google/gemini-cli',
|
|
36
|
+
'claude-code': { posix: 'curl -fsSL https://claude.ai/install.sh | bash', windows: 'irm https://claude.ai/install.ps1 | iex' },
|
|
37
|
+
'codex': { posix: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh', windows: 'powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"' },
|
|
38
|
+
'opencode': { posix: 'curl -fsSL https://opencode.ai/install | bash', windows: 'npm install -g opencode-ai' },
|
|
39
|
+
'gemini': { posix: 'npm install -g @google/gemini-cli', windows: 'npm install -g @google/gemini-cli' },
|
|
28
40
|
};
|
|
29
41
|
|
|
30
42
|
const CLI_LABELS = {
|
|
@@ -36,24 +48,67 @@ const CLI_LABELS = {
|
|
|
36
48
|
|
|
37
49
|
const POLL_INTERVAL_MS = 30000;
|
|
38
50
|
const CACHE_TTL_MS = 10000;
|
|
51
|
+
// How often an install another window (or an earlier page) started is asked
|
|
52
|
+
// about, and for how long at most.
|
|
53
|
+
const INSTALL_POLL_MS = 2000;
|
|
54
|
+
const INSTALL_FOLLOW_MAX_MS = 30 * 60 * 1000;
|
|
39
55
|
|
|
40
56
|
let _cache = { fetchedAt: 0, tools: null };
|
|
57
|
+
// What a subscriber is handed for a CLI the answer says nothing about: not
|
|
58
|
+
// "missing" (nothing was said), and `installed: null` for the callers that only
|
|
59
|
+
// read that field.
|
|
60
|
+
const unknownInfo = () => ({ installed: null, state: 'unknown' });
|
|
41
61
|
let _inflight = null;
|
|
42
62
|
const _subscribers = new Map();
|
|
43
63
|
let _pollTimer = null;
|
|
64
|
+
const _installs = new Map(); // toolKey → { promise, listeners: Set }
|
|
65
|
+
|
|
66
|
+
function isWindowsPage() {
|
|
67
|
+
const nav = globalThis.navigator;
|
|
68
|
+
if (!nav) return false;
|
|
69
|
+
if (nav.userAgentData?.platform && /windows/i.test(nav.userAgentData.platform)) return true;
|
|
70
|
+
return /windows/i.test(nav.userAgent || '');
|
|
71
|
+
}
|
|
44
72
|
|
|
45
73
|
export function getCliDocUrl(toolKey) {
|
|
74
|
+
const fromServer = _cache.tools?.[toolKey]?.guideUrl;
|
|
75
|
+
if (typeof fromServer === 'string' && /^https:\/\/[^\s"'<>]+$/i.test(fromServer)) return fromServer;
|
|
46
76
|
return CLI_DOC_URLS[toolKey] || null;
|
|
47
77
|
}
|
|
48
78
|
|
|
49
79
|
export function getCliInstallCommand(toolKey) {
|
|
50
|
-
|
|
80
|
+
const fromServer = _cache.tools?.[toolKey]?.installCommand;
|
|
81
|
+
if (fromServer) return fromServer;
|
|
82
|
+
const fallback = CLI_INSTALL_CMDS[toolKey];
|
|
83
|
+
if (!fallback) return '';
|
|
84
|
+
return isWindowsPage() ? fallback.windows : fallback.posix;
|
|
51
85
|
}
|
|
52
86
|
|
|
53
87
|
export function getCliLabel(toolKey) {
|
|
54
88
|
return CLI_LABELS[toolKey] || toolKey;
|
|
55
89
|
}
|
|
56
90
|
|
|
91
|
+
/** This CLI in the latest answer, or null before the first one. */
|
|
92
|
+
export function getCliInfo(toolKey) {
|
|
93
|
+
return _cache.tools?.[toolKey] || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 'installed' | 'missing' | 'broken' | 'unknown' (no answer yet), from the latest answer. */
|
|
97
|
+
export function getCliState(toolKey) {
|
|
98
|
+
return cliStateOf(getCliInfo(toolKey));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The error a found-but-broken CLI gave when it was started, or null. */
|
|
102
|
+
export function getCliError(toolKey) {
|
|
103
|
+
const info = getCliInfo(toolKey);
|
|
104
|
+
return cliStateOf(info) === 'broken' && typeof info?.error === 'string' && info.error ? info.error : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The one-click install the server offers for this CLI, or null (none for this system, or an older server). */
|
|
108
|
+
export function getCliInstallOffer(toolKey) {
|
|
109
|
+
return cliInstallOffer(getCliInfo(toolKey));
|
|
110
|
+
}
|
|
111
|
+
|
|
57
112
|
async function _fetchToolVersions(force) {
|
|
58
113
|
if (!force && _cache.tools && (Date.now() - _cache.fetchedAt) < CACHE_TTL_MS) {
|
|
59
114
|
return _cache.tools;
|
|
@@ -74,6 +129,26 @@ async function _fetchToolVersions(force) {
|
|
|
74
129
|
return _inflight;
|
|
75
130
|
}
|
|
76
131
|
|
|
132
|
+
// What a subscriber is told about: a change of state, version or place. A
|
|
133
|
+
// broken CLI and a missing one both have `installed: null`, so the version
|
|
134
|
+
// alone would hide a CLI that went from missing to broken (or back).
|
|
135
|
+
function _signature(info) {
|
|
136
|
+
if (!info) return 'none';
|
|
137
|
+
return [cliStateOf(info), info.installed ?? '', info.binPath ?? '', info.error ?? '', JSON.stringify(info.pathStatus || null)].join('|');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Explicit consent from Add to PATH. The server accepts only a tool key. */
|
|
141
|
+
export async function addCliToPath(toolKey) {
|
|
142
|
+
if (!INSTALLABLE_CLIS.includes(toolKey)) throw new Error('Unknown CLI');
|
|
143
|
+
const res = await fetch(`/api/cli/path/${encodeURIComponent(toolKey)}`, {
|
|
144
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-SynaBun-UI': '1' }, body: '{}',
|
|
145
|
+
});
|
|
146
|
+
const result = await res.json();
|
|
147
|
+
if (!res.ok && !(res.status === 409 && result.code === 'NOT_NEEDED')) throw new Error(result.error || result.code || `HTTP ${res.status}`);
|
|
148
|
+
await recheckCliStatus(toolKey);
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
77
152
|
// `force` makes a deliberate re-check notify subscribers even when the
|
|
78
153
|
// version string is unchanged. Without it the Re-check button was a no-op by
|
|
79
154
|
// construction: a panel that latched a "CLI not installed" banner after a
|
|
@@ -82,12 +157,10 @@ async function _fetchToolVersions(force) {
|
|
|
82
157
|
// The background poller deliberately keeps the change-only behaviour.
|
|
83
158
|
function _emitChange(prevTools, nextTools, { force = false } = {}) {
|
|
84
159
|
for (const key of TOOL_KEYS) {
|
|
85
|
-
const prev = prevTools?.[key]?.installed ?? null;
|
|
86
|
-
const next = nextTools?.[key]?.installed ?? null;
|
|
87
160
|
const subs = _subscribers.get(key);
|
|
88
161
|
if (!subs || !subs.size) continue;
|
|
89
|
-
if (force ||
|
|
90
|
-
const info = nextTools?.[key] ||
|
|
162
|
+
if (force || prevTools == null || _signature(prevTools?.[key]) !== _signature(nextTools?.[key])) {
|
|
163
|
+
const info = nextTools?.[key] || unknownInfo();
|
|
91
164
|
for (const cb of subs) {
|
|
92
165
|
try { cb(info); } catch (e) { console.error('[cli-status] subscriber error:', e); }
|
|
93
166
|
}
|
|
@@ -95,12 +168,18 @@ function _emitChange(prevTools, nextTools, { force = false } = {}) {
|
|
|
95
168
|
}
|
|
96
169
|
}
|
|
97
170
|
|
|
171
|
+
// Asked again every 30 s while a subscribed CLI is missing or broken: one
|
|
172
|
+
// installed (or repaired) meanwhile is found without a click. A CLI the answer
|
|
173
|
+
// says nothing about is not waited for.
|
|
98
174
|
function _hasMissingSubscribed() {
|
|
99
175
|
for (const key of _subscribers.keys()) {
|
|
100
176
|
const subs = _subscribers.get(key);
|
|
101
177
|
if (!subs?.size) continue;
|
|
102
|
-
|
|
103
|
-
|
|
178
|
+
if (!_cache.tools) return true;
|
|
179
|
+
const info = _cache.tools[key];
|
|
180
|
+
if (!info) continue;
|
|
181
|
+
const state = cliStateOf(info);
|
|
182
|
+
if (state === 'missing' || state === 'broken') return true;
|
|
104
183
|
}
|
|
105
184
|
return false;
|
|
106
185
|
}
|
|
@@ -132,7 +211,7 @@ function _stopPoller() {
|
|
|
132
211
|
|
|
133
212
|
export async function getCliStatus(toolKey, { force = false } = {}) {
|
|
134
213
|
const tools = await _fetchToolVersions(force);
|
|
135
|
-
return tools?.[toolKey] || {
|
|
214
|
+
return tools?.[toolKey] || { ...unknownInfo(), latest: null };
|
|
136
215
|
}
|
|
137
216
|
|
|
138
217
|
export function subscribeCliStatus(toolKey, callback) {
|
|
@@ -147,7 +226,7 @@ export function subscribeCliStatus(toolKey, callback) {
|
|
|
147
226
|
_maybeStartPoller();
|
|
148
227
|
} else {
|
|
149
228
|
_fetchToolVersions(false).then((tools) => {
|
|
150
|
-
try { callback(tools?.[toolKey] ||
|
|
229
|
+
try { callback(tools?.[toolKey] || unknownInfo()); } catch {}
|
|
151
230
|
_maybeStartPoller();
|
|
152
231
|
}).catch(() => {});
|
|
153
232
|
}
|
|
@@ -165,5 +244,186 @@ export async function recheckCliStatus(toolKey) {
|
|
|
165
244
|
const prev = _cache.tools;
|
|
166
245
|
const next = await _fetchToolVersions(true);
|
|
167
246
|
_emitChange(prev, next, { force: true });
|
|
168
|
-
return next?.[toolKey] ||
|
|
247
|
+
return next?.[toolKey] || unknownInfo();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── One-click install ──────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
253
|
+
|
|
254
|
+
/** What a refusal before the stream (a JSON answer) means, as a `done` event. */
|
|
255
|
+
function _refusal(status, body) {
|
|
256
|
+
const code = String(body?.code || '');
|
|
257
|
+
let reason = 'internal';
|
|
258
|
+
if (code === 'UNSUPPORTED' || code === 'UNKNOWN_TOOL' || status === 422) reason = 'unsupported';
|
|
259
|
+
else if (status === 403 || status === 415) reason = 'refused';
|
|
260
|
+
return {
|
|
261
|
+
type: 'done', ok: false, code: reason,
|
|
262
|
+
error: typeof body?.error === 'string' && body.error ? body.error : (code || `HTTP ${status}`),
|
|
263
|
+
manualCommand: typeof body?.manualCommand === 'string' ? body.manualCommand : null,
|
|
264
|
+
guideUrl: typeof body?.guideUrl === 'string' ? body.guideUrl : null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The installs the server knows about: { [tool]: { state: 'running' | 'done' | 'failed', step, code, error,
|
|
270
|
+
* log, startedAt, endedAt } }. An older server (no install route) answers {}.
|
|
271
|
+
*/
|
|
272
|
+
export async function getCliInstallJobs() {
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetch('/api/cli/install', { headers: { Accept: 'application/json' } });
|
|
275
|
+
if (!res.ok) return {};
|
|
276
|
+
const data = await res.json();
|
|
277
|
+
return data?.jobs && typeof data.jobs === 'object' ? data.jobs : {};
|
|
278
|
+
} catch {
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Follow an install that is already running on the server (started from another window, or before this page
|
|
285
|
+
* was loaded) through GET /api/cli/install: its steps and new log lines go to `onEvent`, and the end is a
|
|
286
|
+
* `done` event like the stream's. Resolves that event.
|
|
287
|
+
*/
|
|
288
|
+
export async function followCliInstall(toolKey, { onEvent = () => {}, pollMs = INSTALL_POLL_MS, maxMs = INSTALL_FOLLOW_MAX_MS } = {}) {
|
|
289
|
+
const startedAt = Date.now();
|
|
290
|
+
let seenLines = 0;
|
|
291
|
+
let lastStep = null;
|
|
292
|
+
let sawJob = false;
|
|
293
|
+
for (;;) {
|
|
294
|
+
const job = (await getCliInstallJobs())[toolKey];
|
|
295
|
+
if (job && typeof job === 'object') {
|
|
296
|
+
sawJob = true;
|
|
297
|
+
if (job.step && job.step !== lastStep) {
|
|
298
|
+
lastStep = job.step;
|
|
299
|
+
onEvent({ type: 'step', step: job.step });
|
|
300
|
+
}
|
|
301
|
+
const log = Array.isArray(job.log) ? job.log : [];
|
|
302
|
+
if (log.length < seenLines) seenLines = 0; // a new job: its log starts again
|
|
303
|
+
for (const entry of log.slice(seenLines)) onEvent({ type: 'log', line: entry });
|
|
304
|
+
seenLines = log.length;
|
|
305
|
+
if (job.state && job.state !== 'running') {
|
|
306
|
+
if (job.state === 'done') {
|
|
307
|
+
const status = await recheckCliStatus(toolKey).catch(() => null);
|
|
308
|
+
const done = { type: 'done', ok: true, status, path: job.path && typeof job.path === 'object' ? job.path : null };
|
|
309
|
+
onEvent(done);
|
|
310
|
+
return done;
|
|
311
|
+
}
|
|
312
|
+
const failed = { type: 'done', ok: false, code: job.code || 'internal', error: job.error || null, manualCommand: job.manualCommand || null, guideUrl: job.guideUrl || null, log };
|
|
313
|
+
onEvent(failed);
|
|
314
|
+
return failed;
|
|
315
|
+
}
|
|
316
|
+
} else if (sawJob || Date.now() - startedAt > pollMs * 3) {
|
|
317
|
+
// The job is gone (a restarted server), or never showed up: ask about the CLI itself.
|
|
318
|
+
const status = await recheckCliStatus(toolKey).catch(() => null);
|
|
319
|
+
const done = cliStateOf(status) === 'installed'
|
|
320
|
+
? { type: 'done', ok: true, status, path: null }
|
|
321
|
+
: { type: 'done', ok: false, code: 'connection', error: null };
|
|
322
|
+
onEvent(done);
|
|
323
|
+
return done;
|
|
324
|
+
}
|
|
325
|
+
if (Date.now() - startedAt > maxMs) {
|
|
326
|
+
const done = { type: 'done', ok: false, code: 'timeout', error: null };
|
|
327
|
+
onEvent(done);
|
|
328
|
+
return done;
|
|
329
|
+
}
|
|
330
|
+
await wait(pollMs);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function _readNdjson(res, onEvent) {
|
|
335
|
+
let final = null;
|
|
336
|
+
const take = (text) => {
|
|
337
|
+
const trimmed = text.trim();
|
|
338
|
+
if (!trimmed) return;
|
|
339
|
+
let event;
|
|
340
|
+
try { event = JSON.parse(trimmed); } catch { return; }
|
|
341
|
+
if (!event || typeof event !== 'object') return;
|
|
342
|
+
onEvent(event);
|
|
343
|
+
if (event.type === 'done') final = event;
|
|
344
|
+
};
|
|
345
|
+
if (res.body && typeof res.body.getReader === 'function') {
|
|
346
|
+
const reader = res.body.getReader();
|
|
347
|
+
const decoder = new TextDecoder();
|
|
348
|
+
let buffer = '';
|
|
349
|
+
for (;;) {
|
|
350
|
+
const { value, done } = await reader.read();
|
|
351
|
+
if (done) break;
|
|
352
|
+
buffer += decoder.decode(value, { stream: true });
|
|
353
|
+
let at;
|
|
354
|
+
while ((at = buffer.indexOf('\n')) >= 0) {
|
|
355
|
+
take(buffer.slice(0, at));
|
|
356
|
+
buffer = buffer.slice(at + 1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
buffer += decoder.decode();
|
|
360
|
+
take(buffer);
|
|
361
|
+
} else {
|
|
362
|
+
for (const piece of String(await res.text()).split('\n')) take(piece);
|
|
363
|
+
}
|
|
364
|
+
return final;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Install a CLI with one click: the server runs the vendor's official installer for its system and streams what
|
|
369
|
+
* happens (`start`, `step`, `progress`, `log`, then one `done`). Every event goes to `onEvent`; the promise
|
|
370
|
+
* resolves the `done` event. After a successful install the status is asked again (with force), so every panel
|
|
371
|
+
* subscribed to this CLI hears of it. Never rejects.
|
|
372
|
+
*
|
|
373
|
+
* One install per CLI at a time in this page: a second call follows the first one.
|
|
374
|
+
* An install already running on the server (409 BUSY) is followed through GET /api/cli/install.
|
|
375
|
+
*/
|
|
376
|
+
export function installCli(toolKey, { onEvent = () => {}, pollMs = INSTALL_POLL_MS } = {}) {
|
|
377
|
+
const running = _installs.get(toolKey);
|
|
378
|
+
if (running) {
|
|
379
|
+
running.listeners.add(onEvent);
|
|
380
|
+
return running.promise;
|
|
381
|
+
}
|
|
382
|
+
const listeners = new Set([onEvent]);
|
|
383
|
+
const emit = (event) => {
|
|
384
|
+
for (const listener of listeners) {
|
|
385
|
+
try { listener(event); } catch (e) { console.error('[cli-status] install listener error:', e); }
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const promise = (async () => {
|
|
389
|
+
if (!INSTALLABLE_CLIS.includes(toolKey)) {
|
|
390
|
+
const refused = { type: 'done', ok: false, code: 'unsupported', error: null };
|
|
391
|
+
emit(refused);
|
|
392
|
+
return refused;
|
|
393
|
+
}
|
|
394
|
+
let final = null;
|
|
395
|
+
try {
|
|
396
|
+
const res = await fetch(`/api/cli/install/${encodeURIComponent(toolKey)}`, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: { 'Content-Type': 'application/json', 'X-SynaBun-UI': '1', Accept: 'application/x-ndjson' },
|
|
399
|
+
body: '{}',
|
|
400
|
+
});
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
let body = {};
|
|
403
|
+
try { body = await res.json(); } catch { /* no body */ }
|
|
404
|
+
if (res.status === 409 && String(body?.code || '').toUpperCase() === 'BUSY') {
|
|
405
|
+
return await followCliInstall(toolKey, { onEvent: emit, pollMs });
|
|
406
|
+
}
|
|
407
|
+
final = _refusal(res.status, body);
|
|
408
|
+
emit(final);
|
|
409
|
+
return final;
|
|
410
|
+
}
|
|
411
|
+
final = await _readNdjson(res, emit);
|
|
412
|
+
} catch {
|
|
413
|
+
final = null;
|
|
414
|
+
}
|
|
415
|
+
if (!final) {
|
|
416
|
+
// The connection closed without an end: the server may still be at it.
|
|
417
|
+
return await followCliInstall(toolKey, { onEvent: emit, pollMs });
|
|
418
|
+
}
|
|
419
|
+
if (final.ok) await recheckCliStatus(toolKey).catch(() => null);
|
|
420
|
+
return final;
|
|
421
|
+
})().finally(() => { _installs.delete(toolKey); });
|
|
422
|
+
_installs.set(toolKey, { promise, listeners });
|
|
423
|
+
return promise;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Whether this page has an install of this CLI under way. */
|
|
427
|
+
export function isCliInstalling(toolKey) {
|
|
428
|
+
return _installs.has(toolKey);
|
|
169
429
|
}
|
|
@@ -626,7 +626,8 @@ export function statusRows({ init = null, account = null, tab = {} } = {}) {
|
|
|
626
626
|
add('Model', init.model);
|
|
627
627
|
add('Claude Code', init.cliVersion + (tab.sdkVersion ? ` · SDK ${tab.sdkVersion}` : ''));
|
|
628
628
|
add('Working directory', init.cwd);
|
|
629
|
-
if (account?.
|
|
629
|
+
if (account?.authMethod === 'apiKey') add('Account', 'API key');
|
|
630
|
+
else if (account?.email || account?.organization) add('Account', [account.email, account.organization, account.subscriptionType].filter(Boolean).join(' · '));
|
|
630
631
|
add('Credential', KEY_SOURCES[init.apiKeySource] || init.apiKeySource);
|
|
631
632
|
add('Permission mode', init.permissionMode);
|
|
632
633
|
if (init.effort) add('Effort', init.effort);
|
|
@@ -166,6 +166,10 @@ export const CP_STYLES = `
|
|
|
166
166
|
#cp-mode .cp-dd-item-note { margin-left: 6px; font-size: 9.5px; color: rgba(255,255,255,0.35); }
|
|
167
167
|
.cp-plan-btn-bypass { border-color: rgba(239,112,112,0.45) !important; color: #ef7070 !important; }
|
|
168
168
|
|
|
169
|
+
/* ═══ Claude accounts: the header account menu is account-menu.js (shared with the Codex panel, its own styles) ═══ */
|
|
170
|
+
/* Its trigger gives way before the session name does. */
|
|
171
|
+
.cp-header-actions:has(> .acct-menu-trigger) { flex-shrink: 1; min-width: 0; }
|
|
172
|
+
|
|
169
173
|
/* ═══ Rewind ═══ */
|
|
170
174
|
.msg-user { position: relative; }
|
|
171
175
|
.cp-rewind-btn { position: absolute; top: -8px; right: 4px; opacity: 0; pointer-events: none; transition: opacity 0.15s; font: 600 9px/1 Inter, sans-serif; color: rgba(220,195,140,0.9); background: rgba(30,25,18,0.95); border: 1px solid rgba(212,168,72,0.35); border-radius: 8px; padding: 3px 7px; cursor: pointer; z-index: 3; }
|
|
@@ -361,4 +365,46 @@ a.cp-chip:hover { text-decoration: underline; }
|
|
|
361
365
|
.cp-worker-end .post-plan-actions { margin-top: 8px; }
|
|
362
366
|
.cp-worker-end .post-plan-action:disabled { opacity: 0.45; cursor: default; }
|
|
363
367
|
.cp-worker-end .post-plan-action:focus-visible { outline: 1px solid rgba(255,255,255,0.45); outline-offset: 2px; }
|
|
368
|
+
|
|
369
|
+
/* ═══ CLI banner (ui-cli-banner.js): Claude Code not installed / installing / failed to start ═══ */
|
|
370
|
+
/* Above the transcript, warm like the panel; red once something failed, green for where an install put it. */
|
|
371
|
+
.cp-cli-banner { flex-shrink: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; align-items: start; margin: 6px 8px 0; padding: 10px 12px; border: 1px solid rgba(255,180,80,0.45); border-radius: 8px; background: rgba(255,180,80,0.08); color: rgba(255,235,200,0.92); font-size: 11px; line-height: 1.4; pointer-events: auto; position: relative; z-index: 5; }
|
|
372
|
+
.cp-cli-banner [hidden] { display: none !important; }
|
|
373
|
+
.cp-cli-banner--broken, .cp-cli-banner--failed { border-color: rgba(239,112,112,0.45); background: rgba(239,112,112,0.07); }
|
|
374
|
+
.cp-cli-banner--done { border-color: rgba(95,207,131,0.4); background: rgba(95,207,131,0.06); }
|
|
375
|
+
.cp-cli-banner-icon { width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: rgba(255,180,80,0.25); color: rgba(255,210,140,1); font-weight: 700; }
|
|
376
|
+
.cp-cli-banner--broken .cp-cli-banner-icon, .cp-cli-banner--failed .cp-cli-banner-icon { background: rgba(239,112,112,0.2); color: #ef7070; }
|
|
377
|
+
.cp-cli-banner--done .cp-cli-banner-icon { background: rgba(95,207,131,0.18); color: #5fcf83; }
|
|
378
|
+
.cp-cli-banner--installing .cp-cli-banner-icon { animation: cp-cli-working 1.2s ease-in-out infinite; }
|
|
379
|
+
@keyframes cp-cli-working { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
|
|
380
|
+
.cp-cli-banner--nudge { animation: cp-cli-nudge 0.5s ease; }
|
|
381
|
+
@keyframes cp-cli-nudge { 0%, 100% { transform: none; } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
|
|
382
|
+
.cp-cli-banner-text { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
|
383
|
+
.cp-cli-banner-title { font-size: 12px; font-weight: 600; color: rgba(255,210,140,0.95); }
|
|
384
|
+
.cp-cli-banner--broken .cp-cli-banner-title, .cp-cli-banner--failed .cp-cli-banner-title { color: #ef7070; }
|
|
385
|
+
.cp-cli-banner--done .cp-cli-banner-title { color: #5fcf83; }
|
|
386
|
+
.cp-cli-banner-body, .cp-cli-banner-note, .cp-cli-banner-step { font-size: 11px; color: rgba(255,255,255,0.7); overflow-wrap: anywhere; }
|
|
387
|
+
.cp-cli-banner-command { display: flex; align-items: flex-start; gap: 6px; }
|
|
388
|
+
.cp-cli-banner-command code, .cp-cli-banner-error code { flex: 1; min-width: 0; padding: 3px 6px; background: rgba(0,0,0,0.35); border-radius: 4px; font: 10.5px/1.45 "SF Mono", Menlo, monospace; color: rgba(255,255,255,0.85); white-space: pre-wrap; word-break: break-all; user-select: text; }
|
|
389
|
+
.cp-cli-banner-error code { display: block; max-height: 96px; overflow: auto; color: rgba(255,200,200,0.92); }
|
|
390
|
+
.cp-cli-banner-copy { flex-shrink: 0; height: 22px; padding: 0 8px; border: 1px solid rgba(255,255,255,0.14); border-radius: 5px; background: transparent; color: rgba(255,255,255,0.7); font-family: inherit; font-size: 11px; cursor: pointer; }
|
|
391
|
+
.cp-cli-banner-copy:hover { color: rgba(255,255,255,0.92); border-color: rgba(255,255,255,0.3); }
|
|
392
|
+
.cp-cli-banner-progress { display: flex; flex-direction: column; gap: 4px; }
|
|
393
|
+
.cp-cli-banner-bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,0.08); overflow: hidden; }
|
|
394
|
+
.cp-cli-banner-bar > span { display: block; height: 100%; width: 0; background: rgba(212,162,127,0.85); transition: width 0.2s ease; }
|
|
395
|
+
.cp-cli-banner-log { margin: 0; max-height: 84px; overflow: auto; padding: 4px 6px; background: rgba(0,0,0,0.3); border-radius: 4px; font: 10px/1.45 "SF Mono", Menlo, monospace; color: rgba(255,255,255,0.55); white-space: pre-wrap; word-break: break-all; }
|
|
396
|
+
.cp-cli-banner-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
|
|
397
|
+
.cp-cli-banner-install, .cp-cli-banner-link, .cp-cli-banner-path, .cp-cli-banner-recheck, .cp-cli-banner-dismiss { display: inline-flex; align-items: center; gap: 4px; height: 22px; padding: 0 10px; font-family: inherit; font-size: 12px; font-weight: 500; line-height: 1; border-radius: 5px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.9); text-decoration: none; cursor: pointer; user-select: none; }
|
|
398
|
+
.cp-cli-banner-install { background: rgba(212,162,127,0.22); border-color: rgba(212,162,127,0.55); color: rgba(255,236,220,0.96); }
|
|
399
|
+
.cp-cli-banner-install:hover:not(:disabled) { background: rgba(212,162,127,0.34); border-color: rgba(212,162,127,0.75); }
|
|
400
|
+
.cp-cli-banner-link:hover, .cp-cli-banner-path:hover:not(:disabled), .cp-cli-banner-recheck:hover:not(:disabled), .cp-cli-banner-dismiss:hover { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.4); }
|
|
401
|
+
.cp-cli-banner-install:disabled, .cp-cli-banner-path:disabled, .cp-cli-banner-recheck:disabled { opacity: 0.5; cursor: default; }
|
|
402
|
+
.cp-cli-banner button:focus-visible, .cp-cli-banner a:focus-visible { outline: 2px solid rgba(212,162,127,0.9); outline-offset: 2px; }
|
|
403
|
+
@media (prefers-reduced-motion: reduce) {
|
|
404
|
+
.cp-cli-banner--installing .cp-cli-banner-icon, .cp-cli-banner--nudge { animation: none; }
|
|
405
|
+
.cp-cli-banner--nudge { box-shadow: 0 0 0 1px rgba(255,180,80,0.8); }
|
|
406
|
+
.cp-cli-banner-bar > span { transition: none; }
|
|
407
|
+
}
|
|
408
|
+
.claude-panel.cp-cli-blocked .cp-send { opacity: 0.4 !important; cursor: not-allowed !important; }
|
|
409
|
+
.claude-panel.cp-cli-blocked .cp-input { opacity: 0.7; }
|
|
364
410
|
`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — the desktop app's bridge
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Inside the SynaBun desktop app (packaging/desktop) the window's preload puts
|
|
6
|
+
// `window.synabunDesktop` on the pages of the app's own server: the app's
|
|
7
|
+
// version and system, its updater, and Quit. A browser tab has none, and every
|
|
8
|
+
// caller then keeps what an npm or Git install does.
|
|
9
|
+
//
|
|
10
|
+
// No imports and no DOM: `node --test` loads this file as it is
|
|
11
|
+
// (tests/desktop-update-view.test.mjs).
|
|
12
|
+
|
|
13
|
+
/** The bridge of the desktop app, or null in a browser. Only one with the updater the app exposes counts. */
|
|
14
|
+
export function desktopBridge(win = globalThis.window) {
|
|
15
|
+
let bridge;
|
|
16
|
+
try { bridge = win?.synabunDesktop; } catch { return null; }
|
|
17
|
+
if (!bridge || typeof bridge !== 'object') return null;
|
|
18
|
+
const updates = bridge.updates;
|
|
19
|
+
if (!updates || typeof updates !== 'object' || typeof updates.state !== 'function' || typeof updates.check !== 'function') return null;
|
|
20
|
+
return bridge;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** True inside the window of the SynaBun desktop app. */
|
|
24
|
+
export function isDesktopApp(win = globalThis.window) {
|
|
25
|
+
return desktopBridge(win) !== null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The title bar of the desktop app's main window (packaging/desktop/src/titlebar.mjs): its facts
|
|
30
|
+
* ({ style, platform, height, controls, appMenu }), its state, the app's menu and the caption buttons' colours.
|
|
31
|
+
* Null in a browser, in the installed PWA, and in any other window of the app (a detached session has a native frame).
|
|
32
|
+
*/
|
|
33
|
+
export function desktopWindow(win = globalThis.window) {
|
|
34
|
+
const api = desktopBridge(win)?.window;
|
|
35
|
+
if (!api || typeof api !== 'object' || typeof api.state !== 'function' || typeof api.setColors !== 'function') return null;
|
|
36
|
+
const facts = api.titleBar;
|
|
37
|
+
if (!facts || facts.style !== 'overlay' || !['darwin', 'win32', 'linux'].includes(facts.platform)) return null;
|
|
38
|
+
return api;
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — the desktop app's title bar on a page without the navbar
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// The SynaBun desktop app's main window has no native title bar
|
|
6
|
+
// (packaging/desktop/src/titlebar.mjs). A page of it without #title-bar
|
|
7
|
+
// (onboarding.html) declares that it lays out its own top with
|
|
8
|
+
// <meta name="synabun-titlebar">, then calls initDesktopPageTitleBar(): a strip
|
|
9
|
+
// along its top drags the window (the system double-clicks it too) and the
|
|
10
|
+
// caption buttons (Windows, Linux) take the page's background. The strip comes
|
|
11
|
+
// first in the page, under what the page draws there, so a control drawn over
|
|
12
|
+
// it keeps its clicks when it says -webkit-app-region: no-drag.
|
|
13
|
+
//
|
|
14
|
+
// Imports only import-free modules: the wizard still loads nothing of the app.
|
|
15
|
+
// The map pages use desktop-titlebar.js.
|
|
16
|
+
|
|
17
|
+
import { desktopWindow } from './desktop-bridge.js';
|
|
18
|
+
import { colorsKey, overlayColorsFor } from './desktop-titlebar-view.js';
|
|
19
|
+
|
|
20
|
+
/** The bar's text tone for the caption symbols (DESIGN.md nav-bar: text-primary), as the page defines it now. */
|
|
21
|
+
export function textTone(doc, view) {
|
|
22
|
+
return view.getComputedStyle(doc.documentElement).getPropertyValue('--t-primary').trim() || 'rgba(255, 255, 255, 0.7)';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A sender of the caption buttons' colours that only sends a change; the page keeps the last pair on <html>. */
|
|
26
|
+
export function colorSender(api, doc) {
|
|
27
|
+
let last = '';
|
|
28
|
+
return (colors) => {
|
|
29
|
+
const key = colorsKey(colors);
|
|
30
|
+
if (!key || key === last) return;
|
|
31
|
+
last = key;
|
|
32
|
+
doc.documentElement.dataset.desktopTitlebarColors = key;
|
|
33
|
+
Promise.resolve(api.setColors(colors)).catch(() => {});
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A page of the main window without the navbar: the strip, the classes, the caption colours. Null outside the app. */
|
|
38
|
+
export function initDesktopPageTitleBar({ doc = document, view = window } = {}) {
|
|
39
|
+
const api = desktopWindow(view);
|
|
40
|
+
if (!api || !doc.body) return null;
|
|
41
|
+
const facts = api.titleBar;
|
|
42
|
+
const html = doc.documentElement;
|
|
43
|
+
html.classList.add('desktop-titlebar', `desktop-titlebar--${facts.platform}`, 'desktop-titlebar--page');
|
|
44
|
+
let strip = doc.getElementById('synabun-desktop-drag-strip');
|
|
45
|
+
if (!strip) {
|
|
46
|
+
strip = doc.createElement('div');
|
|
47
|
+
strip.id = 'synabun-desktop-drag-strip';
|
|
48
|
+
strip.setAttribute('aria-hidden', 'true');
|
|
49
|
+
strip.style.cssText = `position:fixed;top:0;left:0;right:0;height:env(titlebar-area-height,${facts.height}px);`
|
|
50
|
+
+ '-webkit-app-region:drag;app-region:drag;z-index:1;pointer-events:none;';
|
|
51
|
+
doc.body.prepend(strip);
|
|
52
|
+
}
|
|
53
|
+
const send = colorSender(api, doc);
|
|
54
|
+
const sendColors = () => {
|
|
55
|
+
try { send(overlayColorsFor({ bar: 'transparent', base: view.getComputedStyle(doc.body).backgroundColor, text: textTone(doc, view) })); } catch {}
|
|
56
|
+
};
|
|
57
|
+
sendColors();
|
|
58
|
+
return { facts, strip, refresh: sendColors };
|
|
59
|
+
}
|