synabun 2.0.0-beta → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/THIRD-PARTY-LICENSES.md +5 -0
  3. package/launcher.mjs +40 -0
  4. package/lib/claude-hooks.js +61 -8
  5. package/lib/client-config-repair.js +4 -3
  6. package/lib/data-home-migration.js +4 -0
  7. package/lib/desktop-pwa.js +206 -0
  8. package/lib/external-tools.js +221 -0
  9. package/lib/first-launch-protection.js +5 -0
  10. package/lib/packaged-runtime.js +110 -0
  11. package/lib/start-launcher.js +1309 -0
  12. package/neural-interface/i18n/en.json +41 -2
  13. package/neural-interface/i18n/pt-BR.json +41 -2
  14. package/neural-interface/lib/assistant-envelope.js +9 -6
  15. package/neural-interface/lib/augmented-path.js +23 -3
  16. package/neural-interface/lib/browser-language.js +83 -0
  17. package/neural-interface/lib/claude-agent-bridge.js +54 -147
  18. package/neural-interface/lib/claude-executable.js +241 -0
  19. package/neural-interface/lib/claude-model-catalog.js +52 -91
  20. package/neural-interface/lib/native-binary-runtime.js +69 -337
  21. package/neural-interface/lib/native-loop-providers.js +30 -44
  22. package/neural-interface/lib/offline-page.js +54 -0
  23. package/neural-interface/lib/session-title-generator.js +5 -0
  24. package/neural-interface/lib/synabun-update-plan.js +19 -0
  25. package/neural-interface/package-lock.json +0 -251
  26. package/neural-interface/public/install-app.html +31 -0
  27. package/neural-interface/public/manifest.json +2 -0
  28. package/neural-interface/public/offline.html +401 -66
  29. package/neural-interface/public/onboarding.html +252 -146
  30. package/neural-interface/public/shared/assistant/asst-mascot.js +12 -10
  31. package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
  32. package/neural-interface/public/shared/assistant/asst-render.js +58 -49
  33. package/neural-interface/public/shared/assistant/asst-styles.js +4 -2
  34. package/neural-interface/public/shared/command-runner-model.js +30 -0
  35. package/neural-interface/public/shared/html-shell.js +9 -5
  36. package/neural-interface/public/shared/pwa-install-page.js +34 -0
  37. package/neural-interface/public/shared/pwa-install.css +12 -0
  38. package/neural-interface/public/shared/pwa-install.js +28 -0
  39. package/neural-interface/public/shared/settings/settings-rules.js +160 -0
  40. package/neural-interface/public/shared/start-bridge.js +583 -0
  41. package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
  42. package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
  43. package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
  44. package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
  45. package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
  46. package/neural-interface/public/shared/styles.css +801 -392
  47. package/neural-interface/public/shared/term-ready.js +307 -0
  48. package/neural-interface/public/shared/ui-claude-panel.js +3 -3
  49. package/neural-interface/public/shared/ui-command-runner.js +251 -468
  50. package/neural-interface/public/shared/ui-judgments.js +1 -1
  51. package/neural-interface/public/shared/ui-loading.js +221 -10
  52. package/neural-interface/public/shared/ui-menubar.js +11 -27
  53. package/neural-interface/public/shared/ui-settings.js +36 -59
  54. package/neural-interface/public/shared/ui-terminal.js +130 -144
  55. package/neural-interface/public/sw.js +3 -1
  56. package/neural-interface/public/variant/2d/main.js +2 -1
  57. package/neural-interface/public/variant/3d/main.js +2 -1
  58. package/neural-interface/scripts/rebuild-pty.js +15 -12
  59. package/neural-interface/server.js +220 -185
  60. package/package.json +13 -1
  61. package/postinstall.js +11 -81
  62. package/setup.js +72 -4
  63. package/docs/browser-v2.md +0 -109
  64. package/docs/hook-integrity.md +0 -89
  65. package/docs/judgments.md +0 -182
  66. package/docs/style-guide.md +0 -246
  67. package/scripts/audit-claude-hook-usage.mjs +0 -12
  68. package/scripts/audit-memory-duplicates.mjs +0 -44
  69. package/scripts/measure-claude-hook-overhead.mjs +0 -77
  70. package/scripts/recover-user-config.mjs +0 -254
@@ -539,7 +539,7 @@
539
539
  .mcp-other-section.open .mcp-other-chevron { transform: rotate(180deg); }
540
540
  .mcp-other-body { padding: 0 14px 12px; }
541
541
 
542
- /* ── Rules (Step 5 option, Step 6 list): one row per connected tool ── */
542
+ /* ── Rules (Step 5 option, Step 6 installer) ── */
543
543
  .rules-option {
544
544
  display: flex;
545
545
  align-items: center;
@@ -550,12 +550,37 @@
550
550
  cursor: pointer;
551
551
  user-select: none;
552
552
  }
553
+ /* A step whose height follows what the installer reports: on a short window it scrolls (its parts keep their
554
+ height instead of being squeezed) rather than being cut off. */
555
+ .step[data-step="6"] { max-height: calc(90vh - 58px); }
556
+ .step[data-step="6"] > * { flex-shrink: 0; }
557
+ .step[data-step="6"] .subtitle { margin-bottom: 16px; }
558
+ /* The step's one primary action: what it does, then the button. */
559
+ .rules-install-all {
560
+ display: flex;
561
+ align-items: center;
562
+ gap: 14px;
563
+ padding: 12px 14px;
564
+ margin-bottom: 12px;
565
+ border: 1px solid var(--accent-border);
566
+ border-radius: var(--r-card);
567
+ background: var(--accent-bg);
568
+ }
569
+ .rules-install-all .ria-text { flex: 1 1 0; min-width: 0; font-size: 12px; color: var(--t-primary); line-height: 1.5; }
570
+ .rules-install-all .action-btn { flex-shrink: 0; padding: 9px 18px; font-size: 12px; white-space: nowrap; }
571
+ /* What the installer answered, and where the rules live. */
572
+ .rules-result { margin-bottom: 12px; font-size: 12px; color: var(--t-primary); line-height: 1.5; overflow-wrap: anywhere; }
573
+ .rules-result.ok .rr-message::before { content: '\2713'; margin-right: 8px; color: var(--green); }
574
+ .rules-result.fail .rr-message { color: var(--red); }
575
+ .rules-result .rr-where { margin-top: 4px; font-size: 11px; color: var(--t-muted); }
553
576
  .rules-list {
554
577
  display: flex;
555
578
  flex-direction: column;
556
579
  gap: 8px;
557
580
  margin-bottom: 12px;
558
581
  }
582
+ .rules-list:empty { display: none; }
583
+ .rules-list .mcp-toggle-row.has-note { align-items: flex-start; }
559
584
  .rules-list .mtr-state {
560
585
  margin-left: 6px;
561
586
  font-size: 10px;
@@ -563,8 +588,9 @@
563
588
  color: var(--t-muted);
564
589
  }
565
590
  .rules-list .mtr-state.ok { color: var(--green); }
591
+ .rules-list .mtr-note { margin-top: 4px; font-size: 11px; color: var(--t-secondary); line-height: 1.45; overflow-wrap: anywhere; }
566
592
  .rules-actions { display: flex; gap: 6px; flex-shrink: 0; }
567
- .rules-actions button {
593
+ .rules-actions button, .rules-note button {
568
594
  padding: 5px 10px;
569
595
  border-radius: 8px;
570
596
  border: 1px solid var(--b-light);
@@ -576,11 +602,16 @@
576
602
  white-space: nowrap;
577
603
  transition: all var(--ease);
578
604
  }
579
- .rules-actions button:hover { background: var(--s-medium); color: var(--t-bright); border-color: var(--b-hover); }
580
- .rules-actions button:disabled { opacity: 0.4; cursor: default; }
605
+ .rules-actions button:hover, .rules-note button:hover { background: var(--s-medium); color: var(--t-bright); border-color: var(--b-hover); }
606
+ .rules-actions button.primary { border-color: var(--accent-border); background: var(--s-active); color: var(--t-bright); font-weight: 600; }
607
+ .rules-actions button:disabled, .rules-note button:disabled { opacity: 0.4; cursor: default; }
581
608
  .rules-note { font-size: 11px; color: var(--t-muted); margin-bottom: 12px; line-height: 1.5; }
609
+ .rules-note button { margin-left: 8px; }
610
+ /* Under the installer: the text SynaBun cannot install, to paste by hand. */
611
+ .rules-manual-label { margin: 4px 0 8px; font-size: 10px; color: var(--t-muted); text-transform: uppercase; letter-spacing: 0.5px; }
582
612
  .rules-extra { margin-top: 8px; }
583
613
  #rulesViewCode, #rulesCursorCode, #rulesCoexistCode { max-height: 160px; }
614
+ #rulesViewCode { margin-bottom: 12px; }
584
615
 
585
616
  /* ── Dependency List ── */
586
617
  .dep-list {
@@ -1188,14 +1219,14 @@
1188
1219
  <span class="mascot-toast" id="mascotToast"></span>
1189
1220
  </div>
1190
1221
  <h2>AI Tool Integration</h2>
1191
- <p class="subtitle">Toggle on the AI coding tools you use. SynaBun registers itself with each one automatically.</p>
1222
+ <p class="subtitle">Toggle on the AI coding tools you use. SynaBun registers itself with each one and installs its rules automatically.</p>
1192
1223
  <div class="mcp-toggle-list" id="mcpToggleList"></div>
1193
1224
  <label class="rules-option" for="rulesInstallOpt">
1194
1225
  <span class="key-backup-checkbox-wrap">
1195
1226
  <input type="checkbox" id="rulesInstallOpt" checked>
1196
1227
  <span class="key-backup-checkmark"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
1197
1228
  </span>
1198
- <span>Install SynaBun's rules for this tool</span>
1229
+ <span>Install SynaBun's rules with each tool I connect</span>
1199
1230
  </label>
1200
1231
  <div class="mcp-other-section" id="mcpOtherSection">
1201
1232
  <div class="mcp-other-header" id="mcpOtherHeader">
@@ -1213,27 +1244,31 @@
1213
1244
  </div>
1214
1245
  </div>
1215
1246
 
1216
- <!-- Step 6: SynaBun rules -->
1247
+ <!-- Step 6: Install SynaBun rules. The installer leads; the copy boxes at the bottom are only for what it cannot write. -->
1217
1248
  <div class="step right" data-step="6">
1218
- <div style="position:relative;display:flex;justify-content:center;align-items:center;">
1219
- <div class="step-eyes"></div>
1220
- <span class="mascot-toast" id="rulesToast"></span>
1249
+ <div class="step-eyes"></div>
1250
+ <h2>Install SynaBun Rules</h2>
1251
+ <p class="subtitle">SynaBun installs its rules where each connected tool loads them for every project, and keeps them up to date. There is nothing to paste.</p>
1252
+ <div class="rules-note" id="rulesNote" role="status" style="display:none;"></div>
1253
+ <div class="rules-install-all" id="rulesInstallAll" style="display:none;">
1254
+ <div class="ria-text" id="rulesInstallAllHelp"></div>
1255
+ <button type="button" class="action-btn" id="rulesInstallAllBtn" aria-describedby="rulesInstallAllHelp"></button>
1221
1256
  </div>
1222
- <h2>SynaBun Rules</h2>
1223
- <p class="subtitle">SynaBun puts its rules where each tool loads them for every project, and keeps them up to date. There is nothing to paste.</p>
1224
- <div class="rules-note" id="rulesNote" style="display:none;"></div>
1225
- <div class="rules-list" id="rulesList"></div>
1226
- <div class="code-copy-wrap" id="rulesViewWrap" style="display:none;margin-bottom:12px;">
1227
- <div class="instructions-preview" id="rulesViewCode"></div>
1228
- <span class="code-copy-label">copy</span>
1257
+ <div class="rules-result" id="rulesResult" role="status" style="display:none;">
1258
+ <div class="rr-message" id="rulesResultMessage"></div>
1259
+ <div class="rr-where" id="rulesResultWhere"></div>
1229
1260
  </div>
1261
+ <div class="rules-list" id="rulesList"></div>
1262
+ <div class="instructions-preview" id="rulesViewCode" role="region" tabindex="0" style="display:none;"></div>
1263
+ <div class="rules-note" id="rulesIdle" style="display:none;"></div>
1264
+ <div class="rules-manual-label">Manual setup</div>
1230
1265
  <div class="mcp-other-section" id="rulesCursorSection">
1231
1266
  <div class="mcp-other-header" id="rulesCursorHeader">
1232
- <span>Cursor (copy and paste)</span>
1267
+ <span>Cursor (its rules are pasted by hand)</span>
1233
1268
  <svg class="mcp-other-chevron" viewBox="0 0 24 24" width="14" height="14"><polyline points="6 9 12 15 18 9" fill="none" stroke="currentColor" stroke-width="2"/></svg>
1234
1269
  </div>
1235
1270
  <div class="mcp-other-body" id="rulesCursorBody" style="display:none;">
1236
- <div style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Cursor keeps User Rules inside the app. Paste this into Cursor Settings &rarr; Rules:</div>
1271
+ <div id="rulesCursorHelp" style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Cursor keeps its User Rules inside the app, so SynaBun cannot install them. Copy the rules and paste them into Cursor Settings &rarr; Rules.</div>
1237
1272
  <div class="code-copy-wrap" id="rulesCursorWrap">
1238
1273
  <div class="instructions-preview" id="rulesCursorCode"></div>
1239
1274
  <span class="code-copy-label">copy</span>
@@ -1246,7 +1281,7 @@
1246
1281
  <svg class="mcp-other-chevron" viewBox="0 0 24 24" width="14" height="14"><polyline points="6 9 12 15 18 9" fill="none" stroke="currentColor" stroke-width="2"/></svg>
1247
1282
  </div>
1248
1283
  <div class="mcp-other-body" id="rulesCoexistBody" style="display:none;">
1249
- <div style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Copy this, name the other tool in it, and paste it into your instructions file. SynaBun never installs it.</div>
1284
+ <div id="rulesCoexistHelp" style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Copy this, name the other tool in it, and paste it into your instructions file. SynaBun never installs it.</div>
1250
1285
  <div class="code-copy-wrap" id="rulesCoexistWrap">
1251
1286
  <div class="instructions-preview" id="rulesCoexistCode"></div>
1252
1287
  <span class="code-copy-label">copy</span>
@@ -1261,7 +1296,7 @@
1261
1296
  <h2 style="text-align:center;margin-bottom:12px;">Setup Complete!</h2>
1262
1297
  <ul class="checklist" id="checklist"></ul>
1263
1298
  <div id="restartNotice" style="margin:16px 0 0;padding:12px 14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.10);border-radius:var(--r-card);font-size:12px;color:var(--t-primary);line-height:1.5;">
1264
- <span style="color:var(--t-bright);font-weight:600;">Restart required</span> — Relaunch <span id="restartToolName" style="color:var(--t-bright);font-weight:500;"></span> for the MCP changes to be picked up.
1299
+ <span style="color:var(--t-bright);font-weight:600;">Restart required</span> — Relaunch <span id="restartToolName" style="color:var(--t-bright);font-weight:500;"></span> for the MCP changes and SynaBun's rules to be picked up.
1265
1300
  </div>
1266
1301
  <div style="text-align:center;margin-top:16px;">
1267
1302
  <button class="action-btn" id="launchBtn" style="font-size:15px;padding:12px 32px;">Launch Neural Interface</button>
@@ -1371,12 +1406,21 @@
1371
1406
 
1372
1407
  <script type="module">
1373
1408
 
1409
+ // The rules step is the installer of Settings → AI connections: the same client calls and the same reading
1410
+ // of the status (see STEP 6). Both modules are import-free, so the wizard still loads nothing of the app.
1411
+ import { fetchRulesStatus, fetchRulesTextWithFallback, installRules, installAllRules } from './shared/api.js';
1412
+ import {
1413
+ describeRulesHost, describeRulesInstall, describeRulesInstallAll, describeRulesInPlace, rulesInstallPlan, catalogueTranslator,
1414
+ } from './shared/settings/settings-rules.js';
1415
+
1374
1416
  // ═══════════════════════════════════════════
1375
1417
  // STATE
1376
1418
  // ═══════════════════════════════════════════
1377
1419
 
1378
1420
  const TOTAL_STEPS = 8;
1379
1421
  let currentStep = 0;
1422
+ // Step 6: a connected tool still has no rules. Leaving the step then is a skip, and the Next button says so.
1423
+ let rulesPending = false;
1380
1424
 
1381
1425
  const wizardState = {
1382
1426
  collectionName: 'claude_memory',
@@ -1386,6 +1430,8 @@ const wizardState = {
1386
1430
  enabledTools: [], // tool ids toggled on via Step 5 install toggles
1387
1431
  dataHome: '',
1388
1432
  packageRoot: '',
1433
+ mcpCommand: '',
1434
+ mcpArgs: [],
1389
1435
  buildReady: false,
1390
1436
  mcpWritten: false,
1391
1437
  projects: [],
@@ -1479,7 +1525,8 @@ function updateNav() {
1479
1525
  $prevBtn.disabled = currentStep === 0;
1480
1526
 
1481
1527
  const labels = ['Begin', 'Next', 'Next', 'Next', 'Next', 'Next', 'Next', 'Finish'];
1482
- $nextBtn.textContent = (labels[currentStep] || 'Next') + ' \u2192';
1528
+ const label = currentStep === 6 && rulesPending ? 'Skip' : (labels[currentStep] || 'Next');
1529
+ $nextBtn.textContent = label + ' \u2192';
1483
1530
 
1484
1531
  if (currentStep === TOTAL_STEPS - 1) {
1485
1532
  $nextBtn.style.display = 'none';
@@ -2541,7 +2588,11 @@ function getGenericMcpJson() {
2541
2588
  const mcpPath = pkgRoot + '/mcp-server/run.mjs';
2542
2589
  const envPath = dataHome + '/.env';
2543
2590
  const env = { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: dataHome, MEMORY_DATA_DIR: dataHome + '/mcp-data' };
2544
- return JSON.stringify({ mcpServers: { SynaBun: { command: 'node', args: [mcpPath], env } } }, null, 2);
2591
+ // A packaged application is started through its own executable; the server says which.
2592
+ const packaged = wizardState.mcpCommand && wizardState.mcpCommand !== 'node';
2593
+ const command = packaged ? wizardState.mcpCommand : 'node';
2594
+ const args = packaged ? wizardState.mcpArgs : [mcpPath];
2595
+ return JSON.stringify({ mcpServers: { SynaBun: { command, args, env } } }, null, 2);
2545
2596
  }
2546
2597
 
2547
2598
  // "Other providers" collapsible — shows the generic MCP JSON for paste-based setups.
@@ -2576,156 +2627,207 @@ document.getElementById('mcpCodeWrap').addEventListener('click', () => {
2576
2627
  });
2577
2628
 
2578
2629
  // ═══════════════════════════════════════════
2579
- // STEP 6: SynaBun rules
2630
+ // STEP 6: Install SynaBun rules
2580
2631
  // ═══════════════════════════════════════════
2581
- // One row per connected tool with where its rules are and what state they are
2582
- // in (GET /api/setup/rules), plus copy boxes for Cursor and the coexistence
2583
- // snippet. Nothing here has to be clicked to continue.
2632
+ // The installer, as Settings → AI connections → SynaBun rules shows it: one
2633
+ // button that installs the rules for every connected tool, then one row per
2634
+ // tool with where its rules live, what state they are in (GET /api/setup/rules)
2635
+ // and the action that state calls for, and under them what the installer
2636
+ // answered. The calls are the shared client's (shared/api.js). What a state is
2637
+ // called, which action it gets and every sentence about it come from
2638
+ // shared/settings/settings-rules.js and the Settings strings (i18n/en.json), so
2639
+ // the two surfaces cannot drift apart. Nothing is pasted in this step: the two
2640
+ // boxes under "Manual setup" hold what SynaBun cannot install (Cursor's User
2641
+ // Rules, the coexistence snippet). Nothing here has to be clicked to continue.
2584
2642
 
2585
2643
  const RESTART_HINT = 'Restart SynaBun to finish this update.';
2586
- const RULES_STATE_TEXT = {
2587
- installed: 'Installed', newer: 'Installed by a newer SynaBun', outdated: 'Update available', modified: 'Edited', conflict: 'Conflict',
2588
- error: 'Error', shadowed: 'Shadowed', 'not-installed': 'Not installed',
2589
- };
2590
- const RULES_PRIMARY = { 'not-installed': 'Install', outdated: 'Update', modified: 'Replace' };
2644
+ const RULES_SETTINGS_PATH = 'Settings → AI connections → SynaBun rules';
2591
2645
  let rulesStatus = null; // null: the route is missing or failed
2646
+ let rulesTx = null; // the Settings strings; null until they are loaded
2647
+ let rulesReport = null; // what the last install answered: { message, note, ok }
2648
+ let rulesBusy = false; // an install is on its way: every button of the step waits
2649
+ let rulesViewHost = ''; // the tool whose rules text is open under the list
2592
2650
  const rulesTextCache = {};
2593
2651
 
2594
- async function loadRulesStatus() {
2652
+ // The wizard's names for the tools (the ones Step 5 shows), for every sentence that names one.
2653
+ const RULES_TOOL_NAMES = Object.fromEntries(TOOLS.map(t => [t.id, t.name]));
2654
+ const ruleToolNames = (hosts) => hosts.map(host => RULES_TOOL_NAMES[host] || host).join(', ');
2655
+
2656
+ // The words of this step are the Settings ones. One request; a failed one is tried again on the next visit.
2657
+ async function loadRulesStrings() {
2658
+ if (rulesTx) return rulesTx;
2595
2659
  try {
2596
- const res = await fetch('/api/setup/rules');
2597
- if (!res.ok) return null;
2598
- const data = await res.json();
2599
- return data && data.ok ? data : null;
2600
- } catch { return null; }
2660
+ const res = await fetch('/i18n/en.json');
2661
+ if (res.ok) rulesTx = catalogueTranslator(await res.json());
2662
+ } catch { /* the step says so and offers to try again */ }
2663
+ return rulesTx;
2601
2664
  }
2602
2665
 
2603
2666
  // The rendered text for a host, from the rules route or the older read-only endpoint.
2604
2667
  async function loadRulesText(host) {
2605
- if (rulesTextCache[host]) return rulesTextCache[host];
2606
- let text = '';
2607
- try {
2608
- const res = await fetch(`/api/setup/rules/${host}/text`);
2609
- if (res.ok) text = (await res.json()).text || '';
2610
- } catch { /* try the older endpoint */ }
2611
- if (!text) {
2612
- const format = host === 'opencode' ? 'generic' : host;
2613
- const res = await fetch(`/api/claude-code/ruleset?format=${format}`);
2614
- const data = res.ok ? await res.json() : {};
2615
- text = data.ruleset || '';
2616
- }
2617
- if (!text) throw new Error('Rules text not available');
2618
- rulesTextCache[host] = text;
2619
- return text;
2620
- }
2621
-
2622
- function showRulesToast(msg, duration = 2600) {
2623
- const toast = document.getElementById('rulesToast');
2624
- if (!toast) return;
2625
- toast.textContent = msg;
2626
- toast.classList.add('show');
2627
- clearTimeout(toast._timer);
2628
- toast._timer = setTimeout(() => toast.classList.remove('show'), duration);
2668
+ if (!rulesTextCache[host]) rulesTextCache[host] = await fetchRulesTextWithFallback(host);
2669
+ return rulesTextCache[host];
2629
2670
  }
2630
2671
 
2631
2672
  function enabledRuleTools() {
2632
2673
  return wizardState.enabledTools.map(id => TOOLS.find(t => t.id === id)).filter(Boolean);
2633
2674
  }
2634
2675
 
2635
- function renderRulesList() {
2636
- const $list = document.getElementById('rulesList');
2637
- const $note = document.getElementById('rulesNote');
2638
- const tools = enabledRuleTools();
2676
+ function renderRulesStep() {
2677
+ const el = (id) => document.getElementById(id);
2678
+ const tx = rulesTx;
2679
+ const plan = rulesInstallPlan(rulesStatus);
2680
+ const ready = !!tx && !!rulesStatus;
2681
+
2682
+ // What stands in the way, if anything: the step's own text, the installer, or no connected tool.
2683
+ const note = !tx ? 'This step could not load its text. Check that SynaBun is running, then try again.'
2684
+ : !rulesStatus ? `The rules installer did not answer. ${RESTART_HINT} The rules can also be installed later in ${RULES_SETTINGS_PATH}.`
2685
+ : !plan.connected.length ? 'No tool is connected yet. Connect one in the previous step and its rules are installed with it.'
2686
+ : '';
2687
+ const $note = el('rulesNote');
2688
+ $note.textContent = note;
2689
+ if (note && !ready) {
2690
+ const retry = document.createElement('button');
2691
+ retry.type = 'button';
2692
+ retry.textContent = 'Try again';
2693
+ retry.disabled = rulesBusy;
2694
+ retry.addEventListener('click', () => onRulesetStep());
2695
+ $note.appendChild(retry);
2696
+ }
2697
+ $note.style.display = note ? '' : 'none';
2698
+
2699
+ // The primary action, while a connected tool still has rules to install or update.
2700
+ rulesPending = ready && plan.pending.length > 0;
2701
+ el('rulesInstallAll').style.display = rulesPending ? '' : 'none';
2702
+ if (tx) {
2703
+ el('rulesInstallAllHelp').textContent = tx('settings.redesign.control.set002.help');
2704
+ el('rulesInstallAllBtn').textContent = tx('settings.redesign.control.set002.label');
2705
+ el('rulesCursorHelp').textContent = tx('settings.redesign.setup.cursorHelp');
2706
+ el('rulesCoexistHelp').textContent = tx('settings.redesign.setup.coexistHelp');
2707
+ }
2708
+ el('rulesInstallAllBtn').disabled = rulesBusy;
2709
+
2710
+ // The result: what the last install answered, or, with nothing left to install, the rules that are in place
2711
+ // (the toggles of Step 5 installed them) and the file each tool reads.
2712
+ const report = rulesReport || (ready && !rulesPending ? describeRulesInPlace(rulesStatus, { tx, labels: RULES_TOOL_NAMES }) : null);
2713
+ const $result = el('rulesResult');
2714
+ $result.style.display = report ? '' : 'none';
2715
+ if (report) {
2716
+ $result.className = 'rules-result ' + (report.ok === false ? 'fail' : 'ok');
2717
+ el('rulesResultMessage').textContent = report.message;
2718
+ el('rulesResultWhere').textContent = report.note || '';
2719
+ el('rulesResultWhere').style.display = report.note ? '' : 'none';
2720
+ }
2721
+
2722
+ // One row per tool that has rules or is connected. Without a status only the tools connected in Step 5 are known.
2723
+ const $list = el('rulesList');
2639
2724
  $list.innerHTML = '';
2640
-
2641
- const notes = [];
2642
- if (!rulesStatus) notes.push(`${RESTART_HINT} Until then the rules can be viewed and copied here.`);
2643
- if (!tools.length) notes.push('No tool was connected in the previous step. Connect one there, or copy the rules below.');
2644
- $note.textContent = notes.join(' ');
2645
- $note.style.display = notes.length ? '' : 'none';
2646
-
2647
- for (const tool of tools) {
2648
- const info = rulesStatus && rulesStatus.hosts ? rulesStatus.hosts[tool.id] : null;
2649
- const state = info ? info.state : '';
2650
- const good = state === 'installed' || state === 'newer';
2651
- // A file without SynaBun's markers is the user's own: the server refuses to replace it, so no button.
2652
- const primary = info && info.detected !== false && info.replaceable !== false ? (RULES_PRIMARY[state] || '') : '';
2653
- const detail = !info ? RESTART_HINT
2654
- : info.detected === false ? `${tool.name} was not found on this machine`
2655
- : ((['conflict', 'error', 'shadowed'].includes(state) || info.replaceable === false) && (info.error || info.detail)) || info.path || '';
2656
-
2725
+ const idle = [];
2726
+ for (const tool of (tx ? (rulesStatus ? TOOLS : enabledRuleTools()) : [])) {
2727
+ const info = rulesStatus?.hosts?.[tool.id];
2728
+ const view = describeRulesHost(info, { label: tool.name, version: rulesStatus?.version, tx });
2729
+ // Rules are for a tool that talks to SynaBun. One that is not connected and has none is only named, under
2730
+ // the list: connecting it in Step 5 installs them.
2731
+ if (view.known && view.state === 'not-installed' && !plan.connected.includes(tool.id)) { idle.push(tool.name); continue; }
2657
2732
  const row = document.createElement('div');
2658
- row.className = 'mcp-toggle-row' + (good ? ' connected' : '');
2733
+ row.className = 'mcp-toggle-row' + (view.good ? ' connected' : '') + (view.note ? ' has-note' : '');
2659
2734
  row.dataset.host = tool.id;
2660
2735
  row.innerHTML = `
2661
2736
  <div class="mtr-body">
2662
- <div class="mtr-name">${tool.name}<span class="mtr-state${good ? ' ok' : ''}" data-role="state"></span></div>
2737
+ <div class="mtr-name">${tool.name}<span class="mtr-state${view.good ? ' ok' : ''}" data-role="state"></span></div>
2663
2738
  <div class="mtr-path" data-role="path"></div>
2739
+ <div class="mtr-note" data-role="note" style="display:none"></div>
2664
2740
  </div>
2665
2741
  <div class="rules-actions">
2666
- ${primary ? `<button type="button" data-role="primary">${primary}</button>` : ''}
2667
- <button type="button" data-role="view">View</button>
2668
- <button type="button" data-role="copy">Copy</button>
2742
+ ${view.action ? '<button type="button" class="primary" data-role="primary"></button>' : ''}
2743
+ ${view.known ? '<button type="button" data-role="view"></button>' : ''}
2669
2744
  </div>
2670
2745
  `;
2671
- row.querySelector('[data-role="state"]').textContent = info ? (RULES_STATE_TEXT[state] || state) : 'Unavailable';
2746
+ row.querySelector('[data-role="state"]').textContent = view.badge;
2672
2747
  const $path = row.querySelector('[data-role="path"]');
2673
- $path.textContent = detail;
2674
- $path.title = detail;
2675
- row.querySelector('[data-role="primary"]')?.addEventListener('click', (e) => installToolRules(tool, state, e.currentTarget));
2676
- row.querySelector('[data-role="view"]').addEventListener('click', () => viewRules(tool.id, tool.name));
2677
- row.querySelector('[data-role="copy"]').addEventListener('click', () => copyRules(tool.id, tool.name));
2748
+ $path.textContent = view.pathText;
2749
+ $path.title = view.path;
2750
+ const $rowNote = row.querySelector('[data-role="note"]');
2751
+ $rowNote.textContent = view.note;
2752
+ $rowNote.style.display = view.note ? '' : 'none';
2753
+ const $primary = row.querySelector('[data-role="primary"]');
2754
+ if ($primary) {
2755
+ $primary.textContent = view.actionLabel;
2756
+ $primary.disabled = rulesBusy;
2757
+ $primary.addEventListener('click', () => installToolRules(tool));
2758
+ }
2759
+ const $view = row.querySelector('[data-role="view"]');
2760
+ if ($view) {
2761
+ $view.textContent = tx(rulesViewHost === tool.id ? 'settings.redesign.setup.hide' : 'settings.redesign.setup.view');
2762
+ $view.setAttribute('aria-expanded', String(rulesViewHost === tool.id));
2763
+ $view.setAttribute('aria-controls', 'rulesViewCode');
2764
+ $view.addEventListener('click', () => viewRules(tool));
2765
+ }
2678
2766
  $list.appendChild(row);
2679
2767
  }
2768
+ el('rulesViewCode').style.display = rulesViewHost && $list.querySelector(`[data-host="${rulesViewHost}"] [data-role="view"]`) ? '' : 'none';
2769
+ // The other tools, in one line. With no tool connected at all, the note at the top already says it.
2770
+ const $idle = el('rulesIdle');
2771
+ $idle.textContent = idle.length && plan.connected.length ? `${tx('settings.redesign.setup.notConnected')}: ${idle.join(', ')}. Connecting a tool in the previous step installs its rules.` : '';
2772
+ $idle.style.display = $idle.textContent ? '' : 'none';
2773
+
2774
+ updateNav();
2680
2775
  requestAnimationFrame(() => resizeViewport(currentStep));
2681
2776
  }
2682
2777
 
2683
- async function installToolRules(tool, state, btn) {
2684
- const force = state === 'modified';
2685
- if (force && !confirm(`Replace your edited ${tool.name} rules with SynaBun's current version?\n\nYour edited copy is backed up first.`)) return;
2686
- btn.disabled = true;
2778
+ // Run one install, keep what it answered for the result line, then read the status again.
2779
+ async function runRulesInstall(work) {
2780
+ if (rulesBusy) return;
2781
+ rulesBusy = true;
2782
+ renderRulesStep();
2687
2783
  try {
2688
- const res = await fetch(`/api/setup/rules/${tool.id}`, {
2689
- method: 'POST',
2690
- headers: { 'Content-Type': 'application/json' },
2691
- body: JSON.stringify({ force }),
2692
- });
2693
- const data = await res.json().catch(() => ({}));
2694
- if (!res.ok || data.ok === false) showRulesToast(data.error || (res.status === 404 ? RESTART_HINT : `${tool.name} rules were not installed`), 5000);
2695
- else showRulesToast(`${tool.name} rules installed`);
2784
+ rulesReport = await work();
2696
2785
  } catch (err) {
2697
- showRulesToast(`Failed: ${err.message}`, 5000);
2698
- }
2699
- rulesStatus = await loadRulesStatus();
2700
- renderRulesList();
2786
+ // The server's own sentence (an edited copy, damaged markers, another SynaBun at work). A route that is not
2787
+ // there is the server that was not restarted.
2788
+ const message = err?.message === 'HTTP 404' ? RESTART_HINT : (err?.message || rulesTx('settings.redesign.setup.rulesActionFailed'));
2789
+ rulesReport = { message, note: '', ok: false };
2790
+ }
2791
+ rulesStatus = await fetchRulesStatus();
2792
+ rulesBusy = false;
2793
+ renderRulesStep();
2794
+ }
2795
+
2796
+ // "Install for all": every connected tool, by the installer's own rule. Same call as the button in Settings.
2797
+ function installAllToolRules() {
2798
+ runRulesInstall(async () => {
2799
+ const report = describeRulesInstallAll(await installAllRules(), { tx: rulesTx, labels: RULES_TOOL_NAMES });
2800
+ return { message: report.message, note: report.note, ok: report.failed.length === 0 };
2801
+ });
2802
+ }
2803
+
2804
+ // One tool's Install, Update or Replace. Replace is behind the confirm that says the edited copy is backed up.
2805
+ function installToolRules(tool) {
2806
+ const view = describeRulesHost(rulesStatus?.hosts?.[tool.id], { label: tool.name, version: rulesStatus?.version, tx: rulesTx });
2807
+ if (!view.action) return;
2808
+ if (view.force && !confirm(rulesTx('settings.redesign.setup.replaceYourEditedRulesWithSynabun', { label: tool.name }))) return;
2809
+ delete rulesTextCache[tool.id];
2810
+ runRulesInstall(async () => {
2811
+ const result = await installRules(tool.id, { force: view.force });
2812
+ return { message: describeRulesInstall(result, view.state, { label: tool.name, tx: rulesTx }), note: '', ok: true };
2813
+ });
2701
2814
  }
2702
2815
 
2703
- async function viewRules(host, name) {
2704
- const $wrap = document.getElementById('rulesViewWrap');
2816
+ // View shows the exact text a tool receives, under the list; a second press hides it.
2817
+ async function viewRules(tool) {
2705
2818
  const $code = document.getElementById('rulesViewCode');
2706
- if ($wrap.style.display !== 'none' && $wrap.dataset.host === host) {
2707
- $wrap.style.display = 'none';
2819
+ if (rulesViewHost === tool.id) {
2820
+ rulesViewHost = '';
2708
2821
  } else {
2709
- try { $code.textContent = await loadRulesText(host); }
2710
- catch { $code.textContent = `The ${name} rules could not be loaded. ${RESTART_HINT}`; }
2711
- $wrap.dataset.host = host;
2712
- $wrap.style.display = '';
2822
+ try { $code.textContent = await loadRulesText(tool.id); }
2823
+ catch { $code.textContent = rulesTx('settings.redesign.setup.couldNotLoadTheRules', { label: tool.name, RESTART_HINT }); }
2824
+ $code.setAttribute('aria-label', rulesTx('settings.redesign.setup.previewRules', { name: tool.name }));
2825
+ rulesViewHost = tool.id;
2713
2826
  }
2714
- requestAnimationFrame(() => resizeViewport(currentStep));
2827
+ renderRulesStep();
2715
2828
  }
2716
2829
 
2717
- async function copyRules(host, name) {
2718
- let text;
2719
- try { text = await loadRulesText(host); }
2720
- catch { showRulesToast(`Could not load the ${name} rules. ${RESTART_HINT}`, 5000); return; }
2721
- try {
2722
- await navigator.clipboard.writeText(text);
2723
- showRulesToast(`${name} rules copied`);
2724
- } catch {
2725
- // A refused clipboard is the browser's doing; restarting SynaBun would not help.
2726
- showRulesToast('Could not copy: the browser refused access to the clipboard.', 5000);
2727
- }
2728
- }
2830
+ document.getElementById('rulesInstallAllBtn').addEventListener('click', () => installAllToolRules());
2729
2831
 
2730
2832
  // A click-to-copy code box (the pattern of the MCP JSON box in Step 5). The
2731
2833
  // label says "copied!" only once the clipboard took the text; a browser that
@@ -2747,7 +2849,7 @@ function wireRulesCopyBox(wrapId, codeId) {
2747
2849
  });
2748
2850
  }
2749
2851
 
2750
- // A collapsible copy-only section: loads its text the first time it opens.
2852
+ // A collapsible copy-only section, for text SynaBun cannot install: loads it the first time it opens.
2751
2853
  function wireRulesExtra(sectionId, headerId, bodyId, wrapId, codeId, host, name) {
2752
2854
  const $section = document.getElementById(sectionId);
2753
2855
  const $body = document.getElementById(bodyId);
@@ -2768,15 +2870,10 @@ function wireRulesExtra(sectionId, headerId, bodyId, wrapId, codeId, host, name)
2768
2870
  wireRulesExtra('rulesCursorSection', 'rulesCursorHeader', 'rulesCursorBody', 'rulesCursorWrap', 'rulesCursorCode', 'cursor', 'Cursor rules');
2769
2871
  wireRulesExtra('rulesCoexistSection', 'rulesCoexistHeader', 'rulesCoexistBody', 'rulesCoexistWrap', 'rulesCoexistCode', 'coexistence', 'coexistence snippet');
2770
2872
 
2771
- document.getElementById('rulesViewWrap').addEventListener('click', () => {
2772
- const $wrap = document.getElementById('rulesViewWrap');
2773
- const tool = TOOLS.find(t => t.id === $wrap.dataset.host);
2774
- if (tool) copyRules(tool.id, tool.name);
2775
- });
2776
-
2777
2873
  async function onRulesetStep() {
2778
- rulesStatus = await loadRulesStatus();
2779
- renderRulesList();
2874
+ rulesReport = null;
2875
+ [rulesStatus] = await Promise.all([fetchRulesStatus(), loadRulesStrings()]);
2876
+ renderRulesStep();
2780
2877
  if (currentStep === 6) resizeViewport(6);
2781
2878
  }
2782
2879
 
@@ -2805,18 +2902,23 @@ async function onCompleteStep() {
2805
2902
  } catch { /* non-critical — user can enable via Settings later */ }
2806
2903
  }
2807
2904
 
2808
- // Rules count as installed when every connected tool has its copy in place.
2809
- rulesStatus = await loadRulesStatus();
2810
- const ruleTools = enabledRuleTools();
2811
- const rulesInstalled = ruleTools.length > 0 && !!rulesStatus
2812
- && ruleTools.every(t => ['installed', 'newer', 'shadowed'].includes(rulesStatus.hosts?.[t.id]?.state));
2905
+ // Rules count as installed when every connected tool has its copy in place. When they are not, the item says
2906
+ // for which tools and where the installer is from now on.
2907
+ rulesStatus = await fetchRulesStatus();
2908
+ const rulesPlan = rulesInstallPlan(rulesStatus);
2909
+ const rulesMissing = rulesPlan.connected.filter(host => !rulesPlan.inPlace.includes(host));
2910
+ const rulesInstalled = rulesPlan.connected.length > 0 && rulesMissing.length === 0;
2911
+ const rulesLabel = rulesInstalled ? 'Rules installed'
2912
+ : !rulesStatus ? `Rules not installed: the installer did not answer (${RULES_SETTINGS_PATH})`
2913
+ : !rulesPlan.connected.length ? 'Rules not installed: no tool is connected'
2914
+ : `Rules not installed for ${ruleToolNames(rulesMissing)} (${RULES_SETTINGS_PATH})`;
2813
2915
 
2814
2916
  const items = [
2815
2917
  { label: 'Memory name set', done: !!wizardState.collectionName },
2816
2918
  { label: 'Dependencies installed & MCP built', done: wizardState.buildReady },
2817
2919
  { label: 'Projects registered', done: (wizardState.projects || []).length > 0 },
2818
2920
  { label: 'AI tool integration enabled', done: wizardState.enabledTools.length > 0 || wizardState.mcpWritten },
2819
- { label: 'Rules installed', done: rulesInstalled },
2921
+ { label: rulesLabel, done: rulesInstalled },
2820
2922
  ];
2821
2923
 
2822
2924
  const $list = document.getElementById('checklist');
@@ -2853,6 +2955,10 @@ async function init() {
2853
2955
  if (data.packageRoot) {
2854
2956
  wizardState.packageRoot = data.packageRoot;
2855
2957
  }
2958
+ if (data.mcpCommand && Array.isArray(data.mcpArgs)) {
2959
+ wizardState.mcpCommand = data.mcpCommand;
2960
+ wizardState.mcpArgs = data.mcpArgs;
2961
+ }
2856
2962
  } catch {}
2857
2963
 
2858
2964
  // Enable the entrance animation only after every top-level initializer has