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
@@ -0,0 +1,104 @@
1
+ // Small shared sign-in view; each caller supplies its panel's existing classes
2
+ // and native transport. Nothing here handles provider credentials.
3
+ export function providerAuthUrl(value) {
4
+ try {
5
+ const url = new URL(value);
6
+ return url.protocol === 'https:' && !url.username && !url.password ? url.href : '';
7
+ } catch { return ''; }
8
+ }
9
+
10
+ export function providerAuthSnapshot(container) {
11
+ if (!container?.querySelector?.('[data-provider-auth-card], [data-provider-auth], [data-provider-logged-out]')) return container?.innerHTML || '';
12
+ const clone = container.cloneNode(true);
13
+ clone.querySelectorAll('[data-provider-auth-card], [data-provider-auth], [data-provider-logged-out]').forEach(node => node.remove());
14
+ return clone.innerHTML;
15
+ }
16
+
17
+ // `statusText(account, state)` (optional): what the card says while no sign-in is under way, from the
18
+ // caller's own account model (Claude: its subscription and API key). `providerSignedIn(account)`
19
+ // (optional): whether the provider's own login is there ("Sign in again" instead of "Sign in").
20
+ export function mountProviderAuth({ host, provider, read, start, cancel, refresh, submitCode, buttonClass, inputClass, textClass, isCurrent = () => true, onSuccess = () => {}, onAccountChange = () => {}, pollMs = 1500, statusText = null, providerSignedIn = null }) {
21
+ const root = document.createElement('div');
22
+ root.dataset.providerAuth = provider;
23
+ const status = document.createElement('div'); status.className = textClass; status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite');
24
+ const actions = document.createElement('div');
25
+ const handoff = document.createElement('a'); handoff.className = buttonClass; handoff.target = '_blank'; handoff.rel = 'noopener noreferrer'; handoff.textContent = 'Open sign-in page'; handoff.hidden = true;
26
+ const code = document.createElement('div'); code.className = textClass; code.hidden = true;
27
+ const manual = document.createElement('form'); manual.hidden = true;
28
+ const codeInput = document.createElement('input'); codeInput.type = 'password'; codeInput.autocomplete = 'off'; codeInput.className = inputClass || ''; codeInput.setAttribute('aria-label', 'Claude sign-in code'); codeInput.placeholder = 'Paste code only if Claude asks';
29
+ const codeSubmit = document.createElement('button'); codeSubmit.type = 'submit'; codeSubmit.className = buttonClass; codeSubmit.textContent = 'Submit code';
30
+ manual.append(codeInput, codeSubmit);
31
+ manual.addEventListener('submit', event => { event.preventDefault(); const value = codeInput.value; codeInput.value = ''; if (value && submitCode) void perform(() => submitCode(state, value)); });
32
+ root.append(status, actions, handoff, code, manual); host.append(root);
33
+ let state = { status: 'idle' }, account = null, timer, disposed = false, busy = false, succeeded = false, version = 0, pushed = 0, transportError = '';
34
+ const current = () => !disposed && root.isConnected && isCurrent();
35
+ const pending = () => ['starting', 'pending'].includes(state.status);
36
+ function button(label, work) {
37
+ const el = document.createElement('button'); el.type = 'button'; el.className = buttonClass; el.textContent = label; el.disabled = busy;
38
+ el.addEventListener('click', () => void perform(work)); actions.append(el); return el;
39
+ }
40
+ function apply(result) {
41
+ if (result?.state) { state = result.state; if (pending()) succeeded = false; }
42
+ if (result?.account) {
43
+ account = result.account;
44
+ if (account.loggedIn === false && state.status === 'success') state = { ...state, status: 'idle' };
45
+ onAccountChange(account);
46
+ }
47
+ }
48
+ function render() {
49
+ actions.replaceChildren();
50
+ const signedIn = typeof providerSignedIn === 'function' ? providerSignedIn(account) === true : account?.loggedIn === true;
51
+ // Asked on every render (the caller also learns the state); used only while no sign-in is under way.
52
+ const own = typeof statusText === 'function' ? statusText(account, state) : '';
53
+ status.textContent = transportError || state.error || (['starting', 'pending', 'cancelled'].includes(state.status) ? '' : own) || ({ starting: 'Starting sign-in…', pending: 'Finish sign-in in your browser, then return here.', success: `Signed in${account?.email ? ` as ${account.email}` : ''}.`, cancelled: 'Sign-in cancelled.' }[state.status])
54
+ || (signedIn ? `Signed in${account.email ? ` as ${account.email}` : ''}.` : 'Not signed in.');
55
+ if (pending()) button('Cancel sign-in', () => cancel(state));
56
+ else button(signedIn ? `Sign in again with ${provider}` : `Sign in with ${provider}`, () => start());
57
+ button('Refresh', async () => {
58
+ const result = await refresh();
59
+ return result;
60
+ });
61
+ const url = providerAuthUrl(state.authUrl || state.verificationUrl);
62
+ handoff.hidden = !pending() || !url;
63
+ if (url && pending()) handoff.href = url; else handoff.removeAttribute('href');
64
+ code.hidden = !pending() || !state.userCode;
65
+ code.textContent = state.userCode ? `Enter this code: ${state.userCode}` : '';
66
+ manual.hidden = !submitCode || state.status !== 'pending'; codeSubmit.disabled = busy;
67
+ if (manual.hidden) codeInput.value = '';
68
+ }
69
+ async function perform(work) {
70
+ if (busy || !current()) return;
71
+ busy = true; transportError = ''; const epoch = ++version; const push = pushed; clearTimeout(timer); render();
72
+ try {
73
+ const result = await work();
74
+ if (!current() || epoch !== version) return;
75
+ // Native notifications may have completed while a request was in flight.
76
+ if (push === pushed) apply(result);
77
+ } catch (error) { if (current() && epoch === version) transportError = error.message || 'Sign-in could not continue. Try again.'; }
78
+ finally {
79
+ busy = false;
80
+ if (current() && epoch === version) {
81
+ render();
82
+ schedule();
83
+ }
84
+ }
85
+ }
86
+ function schedule() {
87
+ clearTimeout(timer);
88
+ if (!current()) return;
89
+ if (pending()) timer = setTimeout(poll, pollMs);
90
+ else if (state.status === 'success' && !succeeded) {
91
+ succeeded = true;
92
+ void perform(async () => { const result = await refresh(); await onSuccess(result); return result; });
93
+ }
94
+ }
95
+ async function poll() {
96
+ if (!current()) return;
97
+ const epoch = version, push = pushed;
98
+ try { const result = await read(); if (!current() || epoch !== version || push !== pushed) return; apply(result); render(); schedule(); }
99
+ catch (error) { if (current()) { status.textContent = error.message || 'Could not refresh sign-in. Use Refresh to retry.'; } }
100
+ }
101
+ render();
102
+ void perform(() => refresh());
103
+ return { update(result) { if (!current()) return; ++pushed; apply(result); render(); if (!busy) schedule(); }, dispose() { disposed = true; ++version; clearTimeout(timer); root.remove(); } };
104
+ }
@@ -0,0 +1,223 @@
1
+ import { apiKeyErrorText } from './account-view.js';
2
+
3
+ export const PROVIDER_TERMINAL_AUTH_STYLES = `
4
+ .provider-auth-required { padding: 0 !important; }
5
+ .provider-auth-required::before,
6
+ .provider-auth-required > :not([data-provider-logged-out]) { display: none !important; }
7
+ [data-provider-logged-out] {
8
+ display: flex; flex: 1; min-height: 100%; box-sizing: border-box;
9
+ flex-direction: column; align-items: center; justify-content: center;
10
+ gap: 12px; padding: 24px; text-align: center;
11
+ color: var(--cxp-text-2, var(--t-primary)); font: inherit;
12
+ }
13
+ [data-provider-logged-out][hidden] { display: none; }
14
+ .provider-auth-logo { width: 48px; height: 48px; color: var(--cxp-text, var(--t-bright)); }
15
+ .provider-auth-logo svg { width: 100%; height: 100%; }
16
+ .provider-auth-message { margin: 0; font-size: var(--cxp-fs-md, 13px); }
17
+ .provider-auth-button {
18
+ border: none; border-radius: var(--cxp-radius-sm, var(--r-small)); padding: 8px 12px;
19
+ background: var(--cxp-solid, var(--s-active)); color: var(--cxp-on-solid, var(--t-bright));
20
+ font: inherit; font-size: var(--cxp-fs-sm, 12px); font-weight: 500; cursor: pointer;
21
+ }
22
+ .provider-auth-button:disabled { opacity: .6; cursor: default; }
23
+ .provider-auth-button:focus-visible { outline: 2px solid var(--cxp-focus, var(--t-bright)); outline-offset: 2px; }
24
+ .provider-auth-secondary {
25
+ background: transparent; color: var(--cxp-text-2, var(--t-muted));
26
+ border: 1px solid var(--cxp-border, var(--b-light));
27
+ }
28
+ .provider-auth-key { display: flex; flex-direction: column; gap: 8px; width: 100%; max-width: 280px; }
29
+ .provider-auth-key[hidden], .provider-auth-secondary[hidden] { display: none; }
30
+ .provider-auth-key input {
31
+ min-width: 0; box-sizing: border-box; width: 100%; padding: 8px 12px;
32
+ border: 1px solid var(--cxp-border, var(--b-light)); border-radius: var(--cxp-radius-sm, var(--r-small));
33
+ background: var(--cxp-surface-1, var(--s-subtle)); color: var(--cxp-text, var(--t-bright)); font: inherit;
34
+ }
35
+ .provider-auth-key input:focus-visible { outline: 2px solid var(--cxp-focus, var(--t-bright)); outline-offset: 2px; }
36
+ .provider-auth-key-actions { display: flex; justify-content: center; gap: 8px; }
37
+ .provider-auth-error { margin: 0; color: var(--cxp-danger, var(--t-muted)); font-size: var(--cxp-fs-sm, 12px); }
38
+ `;
39
+
40
+ async function terminalRequest(provider, accountId, method = 'GET') {
41
+ const response = await fetch(`/api/provider-panel-auth/${provider}/${encodeURIComponent(accountId)}/terminal`, {
42
+ method, cache: 'no-store', ...(method === 'POST' ? { headers: { 'Content-Type': 'application/json' }, body: '{}' } : {}),
43
+ });
44
+ const data = await response.json();
45
+ if (!response.ok || data.error) throw new Error(data.error || 'Could not open the sign-in terminal.');
46
+ return data.terminal;
47
+ }
48
+
49
+ // Open a provider's own sign-in for an account in SynaBun's terminal (the guarded route picks the
50
+ // CLI, the account's config directory and its environment; no command text leaves the page).
51
+ export async function startProviderTerminalLogin(provider, accountId, label = '') {
52
+ const terminal = await terminalRequest(provider, accountId, 'POST');
53
+ if (terminal.status === 'error') throw new Error(terminal.error || 'Could not open the sign-in terminal.');
54
+ const { openProviderLoginTerminal } = await import('./ui-terminal.js');
55
+ await openProviderLoginTerminal(terminal.sessionId, label || `${provider === 'claude' ? 'Claude' : 'Codex'} sign-in`);
56
+ return terminal;
57
+ }
58
+
59
+ // Only local, allowlisted messages enter the DOM; never provider error text (account-view.js words them).
60
+ function apiKeyErrorMessage(provider, code) {
61
+ return apiKeyErrorText(provider, code);
62
+ }
63
+
64
+ // `target` is ignored by servers since keys are kept per account (an older page sent its connection's).
65
+ export async function providerApiKeyRequest(provider, accountId, key, target) {
66
+ const failure = code => Object.assign(new Error(apiKeyErrorMessage(provider, code)), { code });
67
+ try {
68
+ let response;
69
+ try {
70
+ response = await fetch(`/api/provider-panel-auth/${provider}/${encodeURIComponent(accountId)}/api-key`, {
71
+ method: 'POST', cache: 'no-store', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, ...(target ? { target } : {}) }),
72
+ });
73
+ } catch { throw failure('server_unreachable'); }
74
+ const data = await response.json().catch(() => null);
75
+ if (!response.ok) {
76
+ let code = { 400: 'invalid_key', 401: 'key_rejected', 403: 'permission_denied', 404: 'account_unavailable', 409: 'account_busy', 413: 'invalid_key', 503: 'verification_unavailable' }[response.status] || 'storage_failed';
77
+ if (response.status === 400 && data?.code === 'oauth_token') code = 'oauth_token';
78
+ if (response.status === 503 && data?.code === 'verification_unreachable') code = 'verification_unreachable';
79
+ throw failure(code);
80
+ }
81
+ if (data?.ok !== true) throw failure('storage_failed');
82
+ } finally { key = null; }
83
+ }
84
+
85
+ // Auth is unknown until a provider says otherwise; a failed read never means logout.
86
+ // `message` (optional): the line under the logo (default "Sign in to Claude to continue.").
87
+ // `moreAction` (optional): { label, run } — one more quiet button under the others (the header account menu).
88
+ export function mountProviderTerminalAuth({ host, provider, accountId, logo, readAccount, isCurrent = () => true, message: messageText = '', moreAction = null,
89
+ onSignedIn = () => {}, requestApiKey = providerApiKeyRequest, pollMs = 3000, requestTerminal = terminalRequest,
90
+ openTerminal = async terminal => {
91
+ const { openProviderLoginTerminal } = await import('./ui-terminal.js');
92
+ await openProviderLoginTerminal(terminal.sessionId, `${provider === 'claude' ? 'Claude' : 'Codex'} sign-in`);
93
+ } }) {
94
+ if (!document.getElementById('provider-terminal-auth-styles')) {
95
+ const style = document.createElement('style'); style.id = 'provider-terminal-auth-styles';
96
+ style.textContent = PROVIDER_TERMINAL_AUTH_STYLES; document.head.append(style);
97
+ }
98
+ const root = document.createElement('div'); root.dataset.providerLoggedOut = provider;
99
+ const icon = document.createElement('div'); icon.className = 'provider-auth-logo'; icon.setAttribute('aria-hidden', 'true');
100
+ icon.innerHTML = logo; // A bundled provider SVG supplied by the owning panel.
101
+ const message = document.createElement('p'); message.className = 'provider-auth-message';
102
+ message.textContent = messageText || `Sign in to ${provider === 'claude' ? 'Claude' : 'Codex'} to continue.`;
103
+ const button = document.createElement('button'); button.type = 'button'; button.className = 'provider-auth-button';
104
+ const error = document.createElement('p'); error.className = 'provider-auth-error'; error.setAttribute('role', 'status'); error.setAttribute('aria-live', 'polite');
105
+ const secondary = document.createElement('button'); secondary.type = 'button';
106
+ secondary.className = 'provider-auth-button provider-auth-secondary'; secondary.textContent = 'Use an API key instead';
107
+ secondary.setAttribute('aria-expanded', 'false');
108
+ const form = document.createElement('form'); form.className = 'provider-auth-key'; form.hidden = true;
109
+ const input = document.createElement('input'); input.type = 'password'; input.autocomplete = 'off'; input.spellcheck = false;
110
+ input.setAttribute('aria-label', 'API key'); input.placeholder = 'Paste API key'; input.required = true;
111
+ const actions = document.createElement('div'); actions.className = 'provider-auth-key-actions';
112
+ const save = document.createElement('button'); save.type = 'submit'; save.className = 'provider-auth-button provider-auth-secondary'; save.textContent = 'Save';
113
+ const cancel = document.createElement('button'); cancel.type = 'button'; cancel.className = 'provider-auth-button provider-auth-secondary'; cancel.textContent = 'Cancel';
114
+ actions.append(save, cancel); form.append(input, actions);
115
+ root.append(icon, message, button, secondary, form, error);
116
+ if (moreAction?.label && typeof moreAction.run === 'function') {
117
+ const more = document.createElement('button'); more.type = 'button';
118
+ more.className = 'provider-auth-button provider-auth-secondary provider-auth-more'; more.textContent = moreAction.label;
119
+ more.addEventListener('click', () => { if (current()) moreAction.run(); });
120
+ root.append(more);
121
+ }
122
+ let loggedIn, terminal = null, busy = false, disposed = false, suspended = false, checking = false, unattached = false, timer, push = 0, issue = '', keyOpen = false, keyBusy = false;
123
+ const current = () => !disposed && isCurrent();
124
+ const pending = () => busy || (!unattached && ['starting', 'pending'].includes(terminal?.status));
125
+ function render() {
126
+ if (!current()) return;
127
+ const visible = loggedIn === false && !suspended;
128
+ root.hidden = !visible; host.classList.toggle('provider-auth-required', visible);
129
+ if (visible) { if (root.parentNode !== host) host.append(root); }
130
+ else root.remove();
131
+ button.disabled = pending() || keyBusy;
132
+ secondary.hidden = keyOpen; secondary.disabled = pending() || keyBusy;
133
+ secondary.setAttribute('aria-expanded', String(keyOpen));
134
+ form.hidden = !keyOpen;
135
+ input.disabled = save.disabled = cancel.disabled = keyBusy || pending();
136
+ save.textContent = keyBusy ? 'Saving…' : 'Save';
137
+ form.setAttribute('aria-busy', String(keyBusy));
138
+ button.textContent = pending() ? 'Finish signing in in the terminal' : `Sign in with ${provider === 'claude' ? 'Claude' : 'Codex'}`;
139
+ error.textContent = issue; error.hidden = !issue;
140
+ }
141
+ function apply(account) {
142
+ if (typeof account?.loggedIn !== 'boolean') return;
143
+ const was = loggedIn; loggedIn = account.loggedIn;
144
+ if (loggedIn) {
145
+ terminal = null; issue = ''; suspended = false; unattached = false; keyOpen = false; input.value = '';
146
+ if (was === false) void onSignedIn();
147
+ }
148
+ }
149
+ function schedule() {
150
+ clearTimeout(timer);
151
+ if (current()) timer = setTimeout(() => void refresh(), pending() ? pollMs : pollMs * 5);
152
+ }
153
+ async function refresh() {
154
+ if (!current() || checking) return;
155
+ if (busy || keyBusy) { schedule(); return; }
156
+ checking = true; const epoch = push;
157
+ try {
158
+ const account = await readAccount();
159
+ if (!current() || epoch !== push) return;
160
+ apply(account);
161
+ if (loggedIn === false) {
162
+ const next = await requestTerminal(provider, accountId);
163
+ if (!current() || epoch !== push) return;
164
+ const ended = terminal?.sessionId && terminal.sessionId === next.sessionId && ['exited', 'error'].includes(next.status);
165
+ terminal = next;
166
+ if (ended || next.status === 'error') issue = next.error || 'Sign-in finished without connecting. Try again.';
167
+ }
168
+ } catch {
169
+ if (current() && epoch === push && pending()) {
170
+ issue = 'Could not check sign-in. Try again.'; terminal = null;
171
+ }
172
+ } finally { checking = false; if (current()) { render(); schedule(); } }
173
+ }
174
+ button.addEventListener('click', async () => {
175
+ if (!current() || pending() || keyBusy || loggedIn !== false) return;
176
+ input.value = ''; keyOpen = false;
177
+ busy = true; issue = ''; unattached = false; ++push; clearTimeout(timer); render();
178
+ try {
179
+ const next = await requestTerminal(provider, accountId, 'POST');
180
+ if (!current() || loggedIn === true) return;
181
+ terminal = next;
182
+ if (next.status === 'error') throw new Error(next.error);
183
+ await openTerminal(next);
184
+ } catch (failure) {
185
+ if (current()) { issue = failure.message || 'Could not open the sign-in terminal.'; unattached = true; }
186
+ } finally { busy = false; if (current()) { render(); schedule(); } }
187
+ });
188
+ secondary.addEventListener('click', () => {
189
+ if (!current() || pending() || keyBusy) return;
190
+ keyOpen = true; issue = ''; render(); input.focus?.();
191
+ });
192
+ cancel.addEventListener('click', () => {
193
+ if (keyBusy) return;
194
+ input.value = ''; keyOpen = false; issue = ''; render(); secondary.focus?.();
195
+ });
196
+ form.addEventListener('submit', async event => {
197
+ event.preventDefault();
198
+ let key = input.value; input.value = '';
199
+ if (!current() || pending() || keyBusy || loggedIn !== false || !key) { key = null; return; }
200
+ keyBusy = true; issue = 'Saving API key…'; ++push; clearTimeout(timer); render();
201
+ let saved = false;
202
+ try {
203
+ await requestApiKey(provider, accountId, key);
204
+ key = null; saved = true;
205
+ if (!current()) return;
206
+ const epoch = push, account = await readAccount();
207
+ if (!current() || epoch !== push) return;
208
+ apply(account);
209
+ if (loggedIn !== true) issue = 'Key saved. Checking sign-in…';
210
+ } catch (failure) {
211
+ if (current() && loggedIn !== true) issue = saved ? 'Key saved. Could not refresh sign-in. Reload the panel.' : apiKeyErrorMessage(provider, failure.code);
212
+ } finally { key = null; keyBusy = false; if (current()) { render(); schedule(); } }
213
+ });
214
+ const observer = new MutationObserver(() => { if (current() && loggedIn === false && !suspended && root.parentNode !== host) render(); });
215
+ observer.observe(host, { childList: true });
216
+ render(); void refresh();
217
+ return {
218
+ refresh,
219
+ update(account) { if (!current()) return; ++push; apply(account); render(); schedule(); },
220
+ suspend() { suspended = true; input.value = ''; render(); },
221
+ dispose() { input.value = ''; disposed = true; ++push; clearTimeout(timer); observer.disconnect(); host.classList.remove('provider-auth-required'); root.remove(); },
222
+ };
223
+ }
@@ -9,7 +9,7 @@
9
9
  // ═══════════════════════════════════════════
10
10
 
11
11
  import { emit, on } from './state.js';
12
- import { getSynabunUpdateData, openSynabunUpdateModal, forceCheckSynabunUpdate } from './ui-update.js';
12
+ import { getSynabunUpdateData, openSynabunUpdateModal, syncSynabunUpdateData, synabunUpdateMeta } from './ui-update.js';
13
13
  import { getToolUpdateData, runToolUpdate, TOOL_LABELS } from './ui-tool-updates.js';
14
14
  import { openSettingsModal } from './ui-settings.js';
15
15
  import { getProviderMeta } from './provider-icons.js';
@@ -222,7 +222,7 @@ function computeUpdates() {
222
222
  key: 'synabun',
223
223
  title: 'SynaBun',
224
224
  subtitle: `v${synabun.current} → v${synabun.latest}`,
225
- meta: synabun.source ? `Source: ${synabun.source}` : '',
225
+ meta: synabunUpdateMeta(synabun),
226
226
  action: 'Open updater',
227
227
  severity: 'normal',
228
228
  });
@@ -308,6 +308,12 @@ async function refreshRulesStatus() {
308
308
  emitNotificationsUpdate();
309
309
  }
310
310
 
311
+ // Inside the desktop app its updater reports on its own schedule (ui-update.js): the drawer and its count follow.
312
+ on('synabun-update:changed', () => {
313
+ emitNotificationsUpdate();
314
+ if (_panel) render();
315
+ });
316
+
311
317
  // Settings > Setup re-reads the status after every rules action and passes it on.
312
318
  on('rules:changed', (status) => {
313
319
  _rulesStatus = status?.ok ? status : null;
@@ -894,7 +900,7 @@ async function refreshSessions() {
894
900
  emitLoopsUpdate();
895
901
  // Refresh notification feed alongside session/loop data
896
902
  refreshRulesStatus().then(() => render()).catch(() => {});
897
- forceCheckSynabunUpdate().then(() => render()).catch(() => {});
903
+ syncSynabunUpdateData().then(() => render()).catch(() => {});
898
904
  render();
899
905
  } catch {}
900
906
  }
@@ -2,6 +2,7 @@ import { mountMemoryMaintenance } from './ui-memory-maintenance.js';
2
2
  import { buildJudgmentsTab, wireJudgmentsTab } from './ui-judgments.js';
3
3
  import { buildWhatsAppTab, wireWhatsAppTab } from './ui-whatsapp.js';
4
4
  import { buildCodeEditorTab, wireCodeEditorTab } from './settings/settings-code-editor-pane.js';
5
+ import { buildAboutTab, wireAboutTab } from './settings/settings-about-pane.js';
5
6
  import { SETTINGS_GROUPS, SETTINGS_PAGES, DEFAULT_SETTINGS_PAGE, pageKeys, getSettingsPage, resolveSettingsTarget, homeOfControl } from './settings/settings-ia.js';
6
7
  import * as kit from './settings/settings-kit.js';
7
8
  import { RULES_HOST_LABELS, describeRulesHost, describeRulesInstall, describeRulesInstallAll } from './settings/settings-rules.js';
@@ -10,6 +11,12 @@ import { readGreetingStatus, describeGreeting, describeGreetingSaveFailure } fro
10
11
  import { renderGreeting, clearGreeting } from './greeting/greeting-card.js';
11
12
  import { createTunnelCard, drawInto, mcpKeyNeededText, readTunnel } from './ui-tunnel.js';
12
13
  import { remoteMcpAddress } from './tunnel-view.js';
14
+ import { recheckCliStatus, getCliStatus, addCliToPath } from './cli-status.js';
15
+ import { cliInstallText, setCliInstallTranslator } from './cli-install-view.js';
16
+ setCliInstallTranslator((key, params) => kit.tx(`cliInstall.${key}`, params));
17
+ import { setGitBashTranslator } from './git-bash-view.js';
18
+ import { gitBashRowHtml, wireGitBashRow } from './ui-git-bash.js';
19
+ setGitBashTranslator((key, params) => kit.tx(`gitBash.${key}`, params));
13
20
  import { initI18n, isReady as i18nReady, SUPPORTED_LOCALES, LOCALE_NAMES, SYSTEM_LOCALE, getLocale, getLocaleChoice, setLocaleChoice, getSystemLocale, englishTranslator, loadEnglishTranslator } from './i18n.js';
14
21
  import { wireSettingsShell, wireSettingsSearch, collectSearchRows, revealSettingsTarget, followSettingsTarget, onSettingsTakeover, applyStoredSections, storeSectionState } from './settings/settings-shell.js';
15
22
  import { buildSettingsSearchIndex } from './settings/settings-search.js';
@@ -18,7 +25,7 @@ import { buildSettingsSearchIndex } from './settings/settings-search.js';
18
25
  // ═══════════════════════════════════════════
19
26
  //
20
27
  // The most complex shared UI module. Builds a floating settings panel with
21
- // twelve task pages in five groups (./settings/settings-ia.js). A page is a
28
+ // thirteen task pages in six groups (./settings/settings-ia.js). A page is a
22
29
  // title, a one-line purpose and section cards (./settings/settings-kit.js);
23
30
  // each builder below owns one pane of a page. Variant-registered tabs (the 2D
24
31
  // Graphics tab) join the Appearance page. Old tab ids still open the right page.
@@ -69,6 +76,7 @@ const TAB_ICONS = {
69
76
  whatsapp: '<svg viewBox="0 0 24 24"><path d="M3.5 20.5l1.3-3.9A8.5 8.5 0 1 1 8 19.6z"/><path d="M9.5 8.5c.4 2.6 2.9 5.2 5.5 5.6l1-1.2 1.6.8c-.3 1-1.1 1.6-2.1 1.5-2.9-.3-5.8-3.2-6.1-6.1-.1-1 .5-1.8 1.5-2.1l.8 1.6z"/></svg>',
70
77
  morelogin: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M12 11.5a1 1 0 0 0-1 1c0 1.8-.3 3.6-1.2 5.2"/><path d="M7.8 14.5a8 8 0 0 0-.6 4.3"/><path d="M12 7.5a4.5 4.5 0 0 1 4.5 4.5c0 .6 0 1.2-.1 1.8"/><path d="M5 9.2a7 7 0 0 1 11-1.9"/><path d="M15 13a3 3 0 0 1-3 3"/></svg>',
71
78
  editor: '<svg viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/><line x1="13.5" y1="5" x2="10.5" y2="19"/></svg>',
79
+ about: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="11"/><line x1="12" y1="7.5" x2="12.01" y2="7.5"/></svg>',
72
80
  };
73
81
 
74
82
  // ── Shared icon constants ──
@@ -1105,6 +1113,7 @@ function buildTerminalTab(cliConfig) {
1105
1113
  <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
1106
1114
  ${kit.L('CLI002')}
1107
1115
  </button>
1116
+ ${p.id === 'gemini' ? '' : `<button type="button" class="cli-detect-btn" data-cli-add-path="${p.id}" hidden>${escapeHtml(cliInstallText('addPath'))}</button>`}
1108
1117
  <span class="cli-path-status${isDefault ? '' : ' custom'}" id="cli-status-${p.id}" ${kit.inv('CLI007')}>${kit.te(isDefault ? 'settings.redesign.terminal.cliStatus.default' : 'settings.redesign.terminal.cliStatus.custom')}</span>
1109
1118
  </div>
1110
1119
  <div class="stg-help" id="cli-path-${p.id}-help">${kit.H(p.inv)}</div>
@@ -1129,9 +1138,11 @@ function buildTerminalTab(cliConfig) {
1129
1138
  <p class="stg-help">${kit.th('settings.redesign.terminal.pathsIntro', { code: '<code class="stg-code">wsl</code>' })}</p>
1130
1139
  ${kit.help('CLI002', { forId: 'cli-paths-detect' })}
1131
1140
  ${cliProfiles.map(pathRow).join('')}
1141
+ ${gitBashRowHtml()}
1132
1142
  <button type="button" class="stg-action-btn stg-save-btn" id="cli-paths-save" ${kit.inv('CLI006')} aria-describedby="cli-paths-save-help">
1133
1143
  ${kit.L('CLI006')}
1134
1144
  </button>
1145
+ <button type="button" class="cli-detect-btn" id="cli-path-diagnostics">${escapeHtml(cliInstallText('copyPathDiagnostics'))}</button>
1135
1146
  ${kit.help('CLI006', { forId: 'cli-paths-save' })}
1136
1147
 
1137
1148
  ${kit.subhead(kit.te('settings.redesign.terminal.examples'))}
@@ -3309,6 +3320,7 @@ export async function openSettingsModal(options = {}) {
3309
3320
  skins: buildSkinsTab(skinsData.skins, skinsData.active),
3310
3321
  interface: buildInterfaceTab(),
3311
3322
  icons: buildIconsTab(customIconsData),
3323
+ about: buildAboutTab(),
3312
3324
  }, variantTabs, initialPage)}
3313
3325
  </div>
3314
3326
  </div>
@@ -4398,6 +4410,9 @@ export async function openSettingsModal(options = {}) {
4398
4410
  // ── Code editor: AI autocomplete (loads and saves itself) ──
4399
4411
  wireCodeEditorTab(overlay);
4400
4412
 
4413
+ // ── About: version, links, system and dependencies (loads itself the first time its page is shown) ──
4414
+ wireAboutTab(overlay, { toast: showCCToast });
4415
+
4401
4416
  // ── Discord tab handlers ──
4402
4417
  {
4403
4418
  // Token save
@@ -4539,6 +4554,36 @@ export async function openSettingsModal(options = {}) {
4539
4554
  const cliDefaults = { 'claude-code': 'claude', 'codex': 'codex', 'gemini': 'gemini', 'opencode': 'opencode' };
4540
4555
  const cliProfileIds = Object.keys(cliDefaults);
4541
4556
 
4557
+ const pathButtons = overlay.querySelectorAll('[data-cli-add-path]');
4558
+ const showPathAction = (tool, info) => {
4559
+ const button = overlay.querySelector(`[data-cli-add-path="${tool}"]`);
4560
+ if (button) button.hidden = !(info?.state === 'installed' && info.pathStatus?.canAdd === true);
4561
+ };
4562
+ pathButtons.forEach(button => {
4563
+ const tool = button.dataset.cliAddPath;
4564
+ getCliStatus(tool).then(info => showPathAction(tool, info)).catch(() => {});
4565
+ button.addEventListener('click', async () => {
4566
+ button.disabled = true; button.textContent = cliInstallText('addingPath');
4567
+ try {
4568
+ const result = await addCliToPath(tool);
4569
+ showCCToast(cliInstallText('doneAddedToPath', { dir: result.dir }) + (result.warning ? ` ${result.warning}` : ''));
4570
+ for (const pathButton of pathButtons) getCliStatus(pathButton.dataset.cliAddPath).then(info => showPathAction(pathButton.dataset.cliAddPath, info)).catch(() => {});
4571
+ } catch (error) { showCCToast(cliInstallText('pathFailed', { error: error.message })); }
4572
+ finally { button.disabled = false; button.textContent = cliInstallText('addPath'); }
4573
+ });
4574
+ });
4575
+ // Windows: the Git Bash every Claude Code SynaBun starts gets (ui-git-bash.js; hidden on macOS and Linux).
4576
+ wireGitBashRow(overlay.querySelector('#cli-git-bash'));
4577
+ overlay.querySelector('#cli-path-diagnostics')?.addEventListener('click', async () => {
4578
+ try {
4579
+ const response = await fetch('/api/cli/path');
4580
+ const diagnostics = await response.json();
4581
+ if (!response.ok) throw new Error(diagnostics.error || `HTTP ${response.status}`);
4582
+ await navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2));
4583
+ showCCToast(cliInstallText('copied'));
4584
+ } catch (error) { showCCToast(error.message); }
4585
+ });
4586
+
4542
4587
  // Detect buttons
4543
4588
  overlay.querySelectorAll('.cli-detect-btn[data-cli-detect]').forEach(btn => {
4544
4589
  btn.addEventListener('click', async () => {
@@ -4552,9 +4597,13 @@ export async function openSettingsModal(options = {}) {
4552
4597
  body: '{}',
4553
4598
  });
4554
4599
  const data = await res.json();
4555
- if (data.ok && data.found && data.path) {
4600
+ showPathAction(profileId, data);
4601
+ if (data.ok && data.found && data.path && data.state === 'broken') {
4602
+ // Found, but it does not start: not put in the field, so a broken path is never saved by accident.
4603
+ showCCToast(kit.tx('settings.redesign.terminal.detectionFailed', { message: data.error ? `${data.path}: ${data.error}` : data.path }));
4604
+ } else if (data.ok && data.found && data.path) {
4556
4605
  input.value = data.path;
4557
- showCCToast(kit.tx('settings.redesign.terminal.found', { path: data.path }));
4606
+ showCCToast(kit.tx('settings.redesign.terminal.found', { path: data.version ? `${data.path} (${data.version})` : data.path }));
4558
4607
  } else {
4559
4608
  showCCToast(kit.tx('settings.redesign.terminal.cliNotFoundInPath'));
4560
4609
  }
@@ -4594,6 +4643,9 @@ export async function openSettingsModal(options = {}) {
4594
4643
  }
4595
4644
  });
4596
4645
  showCCToast(kit.tx('settings.redesign.terminal.cliPathsSaved'));
4646
+ // The panels' CLI banners and the update badges follow the paths just saved (one forced
4647
+ // re-check answers for every CLI).
4648
+ recheckCliStatus('claude-code').catch(() => {});
4597
4649
  } else {
4598
4650
  alert(data.error || kit.tx('settings.redesign.terminal.failedToSave'));
4599
4651
  }
@@ -99,7 +99,9 @@ function show(entry) {
99
99
  entry.unregister = registerSidepanel({ owner: entry.id, layoutKey: entry.id, provider: entry.provider,
100
100
  element, header, actions, buttonClass: 'sp-session-button', applyVisibility: () => renderPill(entry) });
101
101
  if (!storage.getItem(`synabun-sidepanel-layout-${entry.id}`)) detachSidepanel(entry.id);
102
- frame.src = `/sidepanel-session.html?provider=${entry.provider}`;
102
+ // A language the page address forces (?lang=, showcase captures) holds in the session's frame too.
103
+ const lang = new URLSearchParams(window.location.search).get('lang');
104
+ frame.src = `/sidepanel-session.html?provider=${entry.provider}${lang ? `&lang=${encodeURIComponent(lang)}` : ''}`;
103
105
  }
104
106
  setSidepanelVisible(entry.id, true);
105
107
  }
@@ -118,7 +118,13 @@ const SYNC_HANDLERS = {
118
118
  'assistant:permission-resolved': (msg) => emit('sync:assistant:permission-resolved', msg),
119
119
  'assistant:focus': (msg) => emit('sync:assistant:focus', msg),
120
120
  'assistant:accounts-changed': (msg) => emit('sync:assistant:accounts-changed', msg),
121
+ // Claude accounts (lib/claude-accounts.js): any account's state changed — signed in or out, a key saved or removed, its mode, renamed, added, removed
122
+ 'claude:accounts-changed': (msg) => emit('sync:claude:accounts-changed', msg),
123
+ // Codex accounts (lib/codex-accounts.js): the same, for every Codex account
124
+ 'codex:accounts-changed': (msg) => emit('sync:codex:accounts-changed', msg),
121
125
  'assistant:mailbox': (msg) => emit('sync:assistant:mailbox', msg),
126
+ 'assistant:budget': (msg) => emit('sync:assistant:budget', msg),
127
+ 'assistant:budget-changed': (msg) => emit('sync:assistant:budget-changed', msg),
122
128
  'assistant:cost': (msg) => emit('sync:assistant:cost', msg),
123
129
  'assistant:desktop': (msg) => emit('sync:assistant:desktop', msg),
124
130
  'assistant:desktop-setup': (msg) => emit('sync:assistant:desktop-setup', msg),
@@ -6289,6 +6289,23 @@ export function openTerminalPanel(profile) {
6289
6289
  emit('terminal:open', { profile });
6290
6290
  }
6291
6291
 
6292
+ // Attach only: the guarded provider-auth route has already chosen and started
6293
+ // the CLI. No command text is accepted from either panel.
6294
+ export async function openProviderLoginTerminal(sessionId, label) {
6295
+ if (!/^[a-f0-9]{32}$/.test(sessionId || '')) throw new Error('Invalid sign-in terminal session.');
6296
+ await _whenOpenIdle();
6297
+ let session = _sessions.find(s => s.id === sessionId);
6298
+ if (!session) session = await openSession('shell', null, sessionId, { label });
6299
+ if (!session) throw new Error('Could not attach the sign-in terminal. Try again.');
6300
+ showPanel();
6301
+ const index = _sessions.indexOf(session);
6302
+ if (_detachedTabs.has(sessionId)) {
6303
+ bringTabToFront(sessionId);
6304
+ } else if (index >= 0) switchToSession(index);
6305
+ session.term?.focus();
6306
+ return session;
6307
+ }
6308
+
6292
6309
  // ── Diagnostics (no UI) ──
6293
6310
  // window.__synabunTermStats(): keystroke → echo-parsed latency, bytes handed
6294
6311
  // to xterm but not yet parsed, flow-control acks pending, renderer state and
@@ -52,6 +52,11 @@ const SOURCE_LABELS = {
52
52
  'winget': 'winget',
53
53
  'scoop': 'Scoop',
54
54
  'choco': 'Chocolatey',
55
+ 'mise': 'mise',
56
+ 'asdf': 'asdf',
57
+ 'cargo': 'Cargo',
58
+ 'snap': 'Snap',
59
+ 'system': 'your system package manager',
55
60
  };
56
61
 
57
62
  function sourceLabel(src) {