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.
Files changed (233) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/shared.mjs +6 -3
  6. package/lib/child-path.js +303 -0
  7. package/lib/claude-hooks.js +2 -1
  8. package/lib/data-home-migration.js +2 -0
  9. package/lib/desktop-app.js +265 -0
  10. package/lib/external-tools.js +7 -3
  11. package/lib/log-redact.js +102 -0
  12. package/lib/packaged-runtime.js +138 -11
  13. package/mcp-server/dist/http.d.ts +3 -1
  14. package/mcp-server/dist/http.d.ts.map +1 -1
  15. package/mcp-server/dist/http.js +15 -4
  16. package/mcp-server/dist/http.js.map +1 -1
  17. package/mcp-server/dist/index.d.ts.map +1 -1
  18. package/mcp-server/dist/index.js +9 -6
  19. package/mcp-server/dist/index.js.map +1 -1
  20. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  21. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  22. package/mcp-server/dist/services/budget-judgments.js +153 -0
  23. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  24. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  25. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  26. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  27. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  28. package/mcp-server/dist/services/identity.d.ts +11 -0
  29. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  30. package/mcp-server/dist/services/identity.js +24 -0
  31. package/mcp-server/dist/services/identity.js.map +1 -1
  32. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  33. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  34. package/mcp-server/dist/services/neural-interface.js +34 -2
  35. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  36. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  37. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  38. package/mcp-server/dist/services/typesafe-config.js +4 -1
  39. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  40. package/mcp-server/dist/tools/agents.d.ts +10 -7
  41. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/agents.js +34 -13
  43. package/mcp-server/dist/tools/agents.js.map +1 -1
  44. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  45. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  46. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  47. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  48. package/mcp-server/dist/tui.js +2 -1
  49. package/mcp-server/dist/tui.js.map +1 -1
  50. package/mcp-server/dist/version.d.ts +5 -0
  51. package/mcp-server/dist/version.d.ts.map +1 -0
  52. package/mcp-server/dist/version.js +21 -0
  53. package/mcp-server/dist/version.js.map +1 -0
  54. package/mcp-server/package-lock.json +2 -2
  55. package/mcp-server/package.json +1 -1
  56. package/mcp-server/run.mjs +15 -9
  57. package/mcp-server/src/http.ts +18 -3
  58. package/mcp-server/src/index.ts +9 -6
  59. package/mcp-server/src/services/budget-judgments.ts +183 -0
  60. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  61. package/mcp-server/src/services/identity.ts +27 -0
  62. package/mcp-server/src/services/neural-interface.ts +34 -2
  63. package/mcp-server/src/services/typesafe-config.ts +4 -1
  64. package/mcp-server/src/tools/agents.ts +33 -14
  65. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  66. package/mcp-server/src/tui.ts +2 -1
  67. package/mcp-server/src/version.ts +22 -0
  68. package/neural-interface/i18n/en.json +627 -57
  69. package/neural-interface/i18n/pt-BR.json +626 -56
  70. package/neural-interface/lib/account-usage.js +163 -0
  71. package/neural-interface/lib/assistant-api.js +255 -52
  72. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  73. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  74. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  75. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  76. package/neural-interface/lib/assistant-budget-split.js +408 -0
  77. package/neural-interface/lib/assistant-budget.js +94 -95
  78. package/neural-interface/lib/assistant-config.js +62 -14
  79. package/neural-interface/lib/assistant-dispatch.js +432 -125
  80. package/neural-interface/lib/assistant-jev.js +26 -3
  81. package/neural-interface/lib/assistant-persona.js +68 -11
  82. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  83. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  84. package/neural-interface/lib/assistant-route-gate.js +19 -3
  85. package/neural-interface/lib/assistant-router.js +373 -42
  86. package/neural-interface/lib/assistant-runtime.js +307 -47
  87. package/neural-interface/lib/assistant-user-writer.js +4 -2
  88. package/neural-interface/lib/augmented-path.js +317 -53
  89. package/neural-interface/lib/claude-accounts.js +342 -31
  90. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  91. package/neural-interface/lib/claude-executable.js +6 -1
  92. package/neural-interface/lib/claude-model-catalog.js +3 -3
  93. package/neural-interface/lib/claude-panel-auth.js +221 -0
  94. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  95. package/neural-interface/lib/cli-install-api.js +137 -0
  96. package/neural-interface/lib/cli-install.js +797 -0
  97. package/neural-interface/lib/cli-path.js +130 -0
  98. package/neural-interface/lib/cli-resolver.js +744 -0
  99. package/neural-interface/lib/cli-update-planner.js +143 -7
  100. package/neural-interface/lib/codex-account-auth.js +178 -0
  101. package/neural-interface/lib/codex-accounts.js +489 -0
  102. package/neural-interface/lib/codex-capabilities.js +2 -1
  103. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  104. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  105. package/neural-interface/lib/codex-panel-auth.js +62 -0
  106. package/neural-interface/lib/codex-session-settings.js +1 -1
  107. package/neural-interface/lib/codex-settings-schema.js +120 -0
  108. package/neural-interface/lib/desktop/config.js +3 -0
  109. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  110. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  111. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  112. package/neural-interface/lib/git-bash/install.js +529 -0
  113. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  114. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  115. package/neural-interface/lib/git-bash/manager.js +268 -0
  116. package/neural-interface/lib/git-bash/pins.js +206 -0
  117. package/neural-interface/lib/git-bash/pins.json +34 -0
  118. package/neural-interface/lib/git-bash/resolver.js +194 -0
  119. package/neural-interface/lib/git-read.js +34 -2
  120. package/neural-interface/lib/install-details.js +145 -0
  121. package/neural-interface/lib/mcp-installer.js +14 -2
  122. package/neural-interface/lib/native-loop-providers.js +16 -3
  123. package/neural-interface/lib/native-loop-runtime.js +9 -4
  124. package/neural-interface/lib/os-path.js +319 -0
  125. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  126. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  127. package/neural-interface/lib/pty-host/core.js +2 -1
  128. package/neural-interface/lib/pty-host/manager.js +27 -2
  129. package/neural-interface/lib/remote-policy.js +20 -11
  130. package/neural-interface/lib/session-title-generator.js +4 -3
  131. package/neural-interface/lib/system-about.js +340 -0
  132. package/neural-interface/lib/system-activity.js +66 -0
  133. package/neural-interface/lib/terminal-env.js +11 -3
  134. package/neural-interface/lib/terminal-path.js +55 -0
  135. package/neural-interface/lib/virtual-socket.js +12 -1
  136. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  137. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  138. package/neural-interface/lib/whatsapp/commands.js +13 -14
  139. package/neural-interface/lib/whatsapp/config.js +134 -20
  140. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  141. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  142. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  143. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  144. package/neural-interface/lib/whatsapp/manager.js +92 -32
  145. package/neural-interface/lib/whatsapp/paths.js +11 -1
  146. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  147. package/neural-interface/lib/whatsapp/redact.js +63 -0
  148. package/neural-interface/lib/whatsapp/service.js +343 -91
  149. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  150. package/neural-interface/package-lock.json +2 -2
  151. package/neural-interface/package.json +1 -1
  152. package/neural-interface/public/index.html +2 -0
  153. package/neural-interface/public/index2d.html +2 -0
  154. package/neural-interface/public/offline.html +16 -1
  155. package/neural-interface/public/onboarding.html +96 -2
  156. package/neural-interface/public/shared/account-i18n.js +7 -0
  157. package/neural-interface/public/shared/account-menu.js +539 -0
  158. package/neural-interface/public/shared/account-usage.js +19 -0
  159. package/neural-interface/public/shared/account-view.js +335 -0
  160. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  161. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  162. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  163. package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
  164. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  165. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  166. package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
  167. package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
  168. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  169. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  170. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  171. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  172. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  173. package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
  174. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  175. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  176. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  177. package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
  178. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  179. package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
  180. package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
  181. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  182. package/neural-interface/public/shared/claude-account-view.js +3 -0
  183. package/neural-interface/public/shared/cli-install-view.js +392 -0
  184. package/neural-interface/public/shared/cli-status.js +282 -22
  185. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  186. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  187. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  188. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  189. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  190. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  191. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  192. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  193. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  194. package/neural-interface/public/shared/git-bash-view.js +145 -0
  195. package/neural-interface/public/shared/html-shell.js +1 -0
  196. package/neural-interface/public/shared/i18n.js +19 -1
  197. package/neural-interface/public/shared/install-details-view.js +86 -0
  198. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
  199. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  200. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
  201. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  202. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  203. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  204. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  205. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  206. package/neural-interface/public/shared/storage.js +30 -3
  207. package/neural-interface/public/shared/styles.css +278 -31
  208. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  209. package/neural-interface/public/shared/ui-claude-panel.js +212 -213
  210. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  211. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  212. package/neural-interface/public/shared/ui-judgments.js +10 -1
  213. package/neural-interface/public/shared/ui-loading.js +30 -6
  214. package/neural-interface/public/shared/ui-menubar.js +13 -3
  215. package/neural-interface/public/shared/ui-navbar.js +3 -1
  216. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  217. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  218. package/neural-interface/public/shared/ui-sessions.js +9 -3
  219. package/neural-interface/public/shared/ui-settings.js +55 -3
  220. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  221. package/neural-interface/public/shared/ui-sync.js +6 -0
  222. package/neural-interface/public/shared/ui-terminal.js +17 -0
  223. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  224. package/neural-interface/public/shared/ui-update.js +594 -5
  225. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  226. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  227. package/neural-interface/public/shared/worker-end.js +2 -0
  228. package/neural-interface/public/sw.js +1 -1
  229. package/neural-interface/public/variant/2d/main.js +10 -1
  230. package/neural-interface/public/variant/3d/main.js +10 -1
  231. package/neural-interface/server.js +1219 -608
  232. package/package.json +4 -1
  233. 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
- if (isStandalone()) location.replace('/');
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
+ }