quadwork 1.17.2 → 1.18.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 (71) hide show
  1. package/out/404.html +1 -1
  2. package/out/__next.__PAGE__.txt +3 -3
  3. package/out/__next._full.txt +14 -14
  4. package/out/__next._head.txt +4 -4
  5. package/out/__next._index.txt +8 -8
  6. package/out/__next._tree.txt +2 -2
  7. package/out/_next/static/chunks/0_mwyljcf37eu.js +1 -0
  8. package/out/_next/static/chunks/{0sgw2ao.8pb6z.js → 0hpnwm9coputy.js} +2 -2
  9. package/out/_next/static/chunks/{0ewow-yz7dvp~.js → 13r2x5-nb_j73.js} +1 -1
  10. package/out/_next/static/chunks/{12ins7yi1imrc.css → 17uaru~4k8ka9.css} +1 -1
  11. package/out/_not-found/__next._full.txt +13 -13
  12. package/out/_not-found/__next._head.txt +4 -4
  13. package/out/_not-found/__next._index.txt +8 -8
  14. package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
  15. package/out/_not-found/__next._not-found.txt +3 -3
  16. package/out/_not-found/__next._tree.txt +2 -2
  17. package/out/_not-found.html +1 -1
  18. package/out/_not-found.txt +13 -13
  19. package/out/app-shell/__next._full.txt +13 -13
  20. package/out/app-shell/__next._head.txt +4 -4
  21. package/out/app-shell/__next._index.txt +8 -8
  22. package/out/app-shell/__next._tree.txt +2 -2
  23. package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
  24. package/out/app-shell/__next.app-shell.txt +3 -3
  25. package/out/app-shell.html +1 -1
  26. package/out/app-shell.txt +13 -13
  27. package/out/index.html +1 -1
  28. package/out/index.txt +14 -14
  29. package/out/project/_/__next._full.txt +14 -14
  30. package/out/project/_/__next._head.txt +4 -4
  31. package/out/project/_/__next._index.txt +8 -8
  32. package/out/project/_/__next._tree.txt +2 -2
  33. package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
  34. package/out/project/_/__next.project.$d$id.txt +3 -3
  35. package/out/project/_/__next.project.txt +3 -3
  36. package/out/project/_/queue/__next._full.txt +14 -14
  37. package/out/project/_/queue/__next._head.txt +4 -4
  38. package/out/project/_/queue/__next._index.txt +8 -8
  39. package/out/project/_/queue/__next._tree.txt +2 -2
  40. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
  41. package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
  42. package/out/project/_/queue/__next.project.$d$id.txt +3 -3
  43. package/out/project/_/queue/__next.project.txt +3 -3
  44. package/out/project/_/queue.html +1 -1
  45. package/out/project/_/queue.txt +14 -14
  46. package/out/project/_.html +1 -1
  47. package/out/project/_.txt +14 -14
  48. package/out/settings/__next._full.txt +14 -14
  49. package/out/settings/__next._head.txt +4 -4
  50. package/out/settings/__next._index.txt +8 -8
  51. package/out/settings/__next._tree.txt +2 -2
  52. package/out/settings/__next.settings.__PAGE__.txt +3 -3
  53. package/out/settings/__next.settings.txt +3 -3
  54. package/out/settings.html +1 -1
  55. package/out/settings.txt +14 -14
  56. package/out/setup/__next._full.txt +14 -14
  57. package/out/setup/__next._head.txt +4 -4
  58. package/out/setup/__next._index.txt +8 -8
  59. package/out/setup/__next._tree.txt +2 -2
  60. package/out/setup/__next.setup.__PAGE__.txt +3 -3
  61. package/out/setup/__next.setup.txt +3 -3
  62. package/out/setup.html +1 -1
  63. package/out/setup.txt +14 -14
  64. package/package.json +1 -1
  65. package/server/__tests__/scrub-secrets.test.js +9 -8
  66. package/server/index.js +167 -106
  67. package/server/scrub-secrets.js +2 -2
  68. package/out/_next/static/chunks/0m48m-8no_mew.js +0 -1
  69. /package/out/_next/static/{UZGw7OD-9_PMrW9HwCGJe → YK5hBW20gX2uKVJaW2987}/_buildManifest.js +0 -0
  70. /package/out/_next/static/{UZGw7OD-9_PMrW9HwCGJe → YK5hBW20gX2uKVJaW2987}/_clientMiddlewareManifest.js +0 -0
  71. /package/out/_next/static/{UZGw7OD-9_PMrW9HwCGJe → YK5hBW20gX2uKVJaW2987}/_ssgManifest.js +0 -0
package/server/index.js CHANGED
@@ -1305,6 +1305,88 @@ app.post("/api/agents/:project/reset", async (req, res) => {
1305
1305
  }
1306
1306
  });
1307
1307
 
1308
+ // --- Full Reset: restart all AC + agents across all projects (#657) ---
1309
+
1310
+ app.post("/api/full-reset", async (_req, res) => {
1311
+ const start = Date.now();
1312
+ console.log("[full-reset] starting...");
1313
+ try {
1314
+ const cfg = readConfig();
1315
+ const projects = (cfg.projects || []).filter((p) => !p.archived);
1316
+
1317
+ // 1. Stop all agent sessions
1318
+ console.log("[full-reset] stopping all agent sessions...");
1319
+ const sessionKeys = [...agentSessions.keys()];
1320
+ for (const key of sessionKeys) {
1321
+ await stopAgentSession(key);
1322
+ }
1323
+
1324
+ // 2. Stop Butler if running
1325
+ console.log("[full-reset] stopping Butler...");
1326
+ stopButlerPty();
1327
+
1328
+ // 3. Re-run startup migrations
1329
+ console.log("[full-reset] running startup migrations...");
1330
+ runStartupMigrations(cfg);
1331
+
1332
+ // 4. Restart each project's AC + agents
1333
+ let totalAgents = 0;
1334
+ const errors = [];
1335
+ for (const project of projects) {
1336
+ console.log(`[full-reset] restarting AC for ${project.id}...`);
1337
+ // Pre-mark reset as scheduled so AC restart's auto-reset timer is suppressed
1338
+ _acHealth.resetState.set(project.id, { status: "scheduled", timestamp: Date.now() });
1339
+ try {
1340
+ const acResp = await fetch(`http://127.0.0.1:${PORT}/api/agentchattr/${encodeURIComponent(project.id)}/restart`, {
1341
+ method: "POST",
1342
+ });
1343
+ if (!acResp.ok) {
1344
+ const errData = await acResp.json().catch(() => ({}));
1345
+ errors.push(`${project.id}: AC restart failed — ${errData.error || acResp.status}`);
1346
+ continue;
1347
+ }
1348
+ } catch (err) {
1349
+ errors.push(`${project.id}: AC — ${err.message}`);
1350
+ continue;
1351
+ }
1352
+ // Explicitly reset agents and await result
1353
+ try {
1354
+ const resetResp = await fetch(`http://127.0.0.1:${PORT}/api/agents/${encodeURIComponent(project.id)}/reset`, {
1355
+ method: "POST",
1356
+ });
1357
+ const resetData = await resetResp.json();
1358
+ if (resetData.ok) {
1359
+ totalAgents += resetData.restarted;
1360
+ } else {
1361
+ errors.push(`${project.id}: agent reset failed`);
1362
+ }
1363
+ } catch (err) {
1364
+ errors.push(`${project.id}: agent reset — ${err.message}`);
1365
+ }
1366
+ }
1367
+
1368
+ // 5. Restart Butler if enabled
1369
+ if (cfg.butler?.enabled) {
1370
+ console.log("[full-reset] restarting Butler...");
1371
+ const result = spawnButlerPty();
1372
+ if (!result.ok) errors.push(`butler: ${result.error}`);
1373
+ }
1374
+
1375
+ const duration = Date.now() - start;
1376
+ console.log(`[full-reset] complete in ${duration}ms — ${projects.length} projects, ${totalAgents} agents`);
1377
+ res.json({
1378
+ ok: errors.length === 0,
1379
+ projects: projects.length,
1380
+ agents: totalAgents,
1381
+ duration_ms: duration,
1382
+ ...(errors.length > 0 ? { errors } : {}),
1383
+ });
1384
+ } catch (err) {
1385
+ console.error(`[full-reset] failed: ${err.message}`);
1386
+ res.status(500).json({ ok: false, error: err.message });
1387
+ }
1388
+ });
1389
+
1308
1390
  // --- Lifecycle: start spawns PTY (visible in terminal panel) ---
1309
1391
 
1310
1392
  app.post("/api/agents/:project/:agent/start", async (req, res) => {
@@ -1473,6 +1555,23 @@ function spawnButlerPty() {
1473
1555
  }
1474
1556
  });
1475
1557
 
1558
+ // Auto-answer Claude's trust prompt if it appears within the first 10s
1559
+ if (command === "claude") {
1560
+ let trustHandled = false;
1561
+ const trustListener = term.onData((data) => {
1562
+ if (trustHandled) return;
1563
+ if (data.includes("trust") || data.includes("Yes,") || data.includes("1.")) {
1564
+ setTimeout(() => {
1565
+ if (!trustHandled && butlerSession.term === term) {
1566
+ term.write("1\r");
1567
+ trustHandled = true;
1568
+ }
1569
+ }, 500);
1570
+ }
1571
+ });
1572
+ setTimeout(() => { trustListener.dispose(); trustHandled = true; }, 10000);
1573
+ }
1574
+
1476
1575
  term.onExit(({ exitCode }) => {
1477
1576
  if (butlerSession.term === term) {
1478
1577
  butlerSession.state = "stopped";
@@ -2516,52 +2615,17 @@ function startAcHealthMonitor() {
2516
2615
  console.log("[health] AC health monitor started (30s interval, per-project 60s grace)");
2517
2616
  }
2518
2617
 
2519
- server.listen(PORT, "127.0.0.1", async () => {
2520
- console.log(`QuadWork server listening on http://127.0.0.1:${PORT}`);
2521
- syncTriggersFromConfig();
2522
- // #579: detect AC processes already running (spawned by cmdStart before
2523
- // the server module loaded). Without this, chattrProcesses is empty on
2524
- // boot and the health monitor can't track cmdStart-spawned ACs, while
2525
- // the dashboard's Start button would redundantly kill+respawn them.
2526
- const startupCfg = readConfig();
2527
- for (const p of (startupCfg.projects || [])) {
2528
- const { url: acUrl } = resolveProjectChattr(p.id);
2529
- const acPortMatch = acUrl.match(/:(\d+)/);
2530
- const acPort = acPortMatch ? parseInt(acPortMatch[1], 10) : 8300;
2531
- const alive = await isPortAlive(acPort);
2532
- if (alive && !chattrProcesses.has(p.id)) {
2533
- // AC is already running (e.g. spawned by cmdStart). Record it so
2534
- // the health monitor can track it and the dashboard shows the
2535
- // correct state. process is null because we don't own the child.
2536
- chattrProcesses.set(p.id, { process: null, state: "running", error: null, runningSince: Date.now() });
2537
- console.log(`[startup] ${p.id}: AC already alive on port ${acPort} — tracking`);
2538
- }
2539
- }
2540
- // Sync AgentChattr tokens for all projects on startup and backfill
2541
- // the sender-overflow CSS/JS patch (#402) so already-running AC
2542
- // instances receive the fix without requiring a restart.
2543
- // #448: retry after 5s for projects where AC isn't up yet at boot.
2544
- for (const p of (startupCfg.projects || [])) {
2545
- syncChattrToken(p.id).catch(() => {
2546
- setTimeout(() => syncChattrToken(p.id).catch(() => {}), 5000);
2547
- });
2548
- const { dir: acDir } = resolveProjectChattr(p.id);
2549
- if (acDir) patchAgentchattrCss(acDir);
2550
- }
2551
- // #457: migrate bridge slugs in AC configs on startup.
2552
- // Renames [agents.discord-bridge] → [agents.dc] and
2553
- // [agents.telegram-bridge] → [agents.tg] so bridges register
2554
- // under the short slug. Restarts AC ONLY for slug renames (not
2555
- // fresh block appends) — #616: script-only patches should not
2556
- // trigger AC restarts which kill bridge registration.
2557
- for (const p of (startupCfg.projects || [])) {
2618
+ // #657: extracted startup migrations so full-reset can re-run them
2619
+ function runStartupMigrations(cfg) {
2620
+ const projects = (cfg.projects || []).filter((p) => !p.archived);
2621
+ const acRestartNeeded = [];
2622
+
2623
+ // bridge-migrate
2624
+ for (const p of projects) {
2558
2625
  const acPath = projectAgentchattrConfigPath(p.id);
2559
2626
  if (!fs.existsSync(acPath)) continue;
2560
2627
  try {
2561
2628
  const before = fs.readFileSync(acPath, "utf-8");
2562
- // Track whether an actual slug RENAME happened (old → new).
2563
- // Fresh block appends don't need an AC restart — AC picks them
2564
- // up on its next natural start.
2565
2629
  const hadOldDc = /^\[agents\.discord-bridge\]\s*$/m.test(before);
2566
2630
  const hadOldTg = /^\[agents\.telegram-bridge\]\s*$/m.test(before);
2567
2631
  const dc = patchAgentchattrConfigForDiscordBridge(before);
@@ -2569,28 +2633,14 @@ server.listen(PORT, "127.0.0.1", async () => {
2569
2633
  if (dc.changed || tg.changed) {
2570
2634
  fs.writeFileSync(acPath, tg.text);
2571
2635
  console.log(`[bridge-migrate] ${p.id}: migrated AC config slugs`);
2572
- // Only restart AC when a slug was actually RENAMED — not when
2573
- // a fresh block was appended (#616).
2574
2636
  if (hadOldDc || hadOldTg) {
2575
- setTimeout(async () => {
2576
- try {
2577
- const r = await fetch(`http://127.0.0.1:${PORT}/api/agentchattr/${encodeURIComponent(p.id)}/restart`, {
2578
- method: "POST",
2579
- });
2580
- if (r.ok) console.log(`[bridge-migrate] ${p.id}: restarted AC`);
2581
- else console.warn(`[bridge-migrate] ${p.id}: AC restart returned ${r.status}`);
2582
- } catch (err) {
2583
- console.warn(`[bridge-migrate] ${p.id}: AC restart failed: ${err.message || err}`);
2584
- }
2585
- }, 3000);
2637
+ if (!acRestartNeeded.includes(p.id)) acRestartNeeded.push(p.id);
2586
2638
  }
2587
2639
  }
2588
2640
  } catch {}
2589
2641
  }
2590
- // #506: refresh Discord bridge script from the npm package on startup.
2591
- // The Telegram bridge uses git-fetch + pin, but Discord uses a file-copy
2592
- // pattern. Without this, upgrading QuadWork leaves a stale on-disk script
2593
- // missing fixes shipped in newer versions.
2642
+
2643
+ // bridge-refresh
2594
2644
  const DISCORD_BRIDGE_SRC = path.join(__dirname, "..", "bridges", "discord", "discord_bridge.py");
2595
2645
  const DISCORD_BRIDGE_DEST = path.join(os.homedir(), ".quadwork", "agentchattr-discord", "discord_bridge.py");
2596
2646
  if (fs.existsSync(DISCORD_BRIDGE_SRC) && fs.existsSync(path.dirname(DISCORD_BRIDGE_DEST))) {
@@ -2601,10 +2651,8 @@ server.listen(PORT, "127.0.0.1", async () => {
2601
2651
  console.warn(`[bridge-refresh] failed to refresh Discord bridge script: ${err.message || err}`);
2602
2652
  }
2603
2653
  }
2604
- // #470: patch stale bridge_sender defaults in on-disk bridge scripts.
2605
- // The AC config migration (#457) renames the agent sections, but the
2606
- // bridge scripts themselves may still have old defaults if the operator
2607
- // upgraded QuadWork without re-installing the bridges.
2654
+
2655
+ // bridge slug patches
2608
2656
  const BRIDGE_SLUG_PATCHES = [
2609
2657
  { file: path.join(os.homedir(), ".quadwork", "agentchattr-telegram", "telegram_bridge.py"), old: '"telegram-bridge"', replacement: '"tg"' },
2610
2658
  { file: path.join(os.homedir(), ".quadwork", "agentchattr-discord", "discord_bridge.py"), old: '"discord-bridge"', replacement: '"dc"' },
@@ -2618,20 +2666,15 @@ server.listen(PORT, "127.0.0.1", async () => {
2618
2666
  console.log(`[bridge-migrate] patched stale bridge_sender in ${path.basename(file)}`);
2619
2667
  } catch {}
2620
2668
  }
2621
- // #479: fix stale agent slugs in worktree AGENTS.md and CLAUDE.md on startup.
2622
- // Uses in-place replacement (not full template overwrite) to preserve
2623
- // reviewer auth credentials and other site-specific customisations.
2669
+
2670
+ // reseed stale slugs
2624
2671
  const SLUG_FIXES = [
2625
- [/@reviewer1/g, "@re1"],
2626
- [/@reviewer2/g, "@re2"],
2627
- [/@t2a/g, "@re1"],
2628
- [/@t2b/g, "@re2"],
2629
- [/@t1\b/g, "@head"],
2630
- [/@t3\b/g, "@dev"],
2631
- [/\breviewer1\b/g, "re1"],
2632
- [/\breviewer2\b/g, "re2"],
2672
+ [/@reviewer1/g, "@re1"], [/@reviewer2/g, "@re2"],
2673
+ [/@t2a/g, "@re1"], [/@t2b/g, "@re2"],
2674
+ [/@t1\b/g, "@head"], [/@t3\b/g, "@dev"],
2675
+ [/\breviewer1\b/g, "re1"], [/\breviewer2\b/g, "re2"],
2633
2676
  ];
2634
- for (const p of (startupCfg.projects || [])) {
2677
+ for (const p of projects) {
2635
2678
  if (!p.agents) continue;
2636
2679
  for (const [agentId, agentCfg] of Object.entries(p.agents)) {
2637
2680
  const wtDir = agentCfg.cwd;
@@ -2642,9 +2685,9 @@ server.listen(PORT, "127.0.0.1", async () => {
2642
2685
  try {
2643
2686
  let content = fs.readFileSync(filePath, "utf-8");
2644
2687
  let changed = false;
2645
- for (const [pattern, replacement] of SLUG_FIXES) {
2688
+ for (const [pattern, repl] of SLUG_FIXES) {
2646
2689
  const before = content;
2647
- content = content.replace(pattern, replacement);
2690
+ content = content.replace(pattern, repl);
2648
2691
  if (content !== before) changed = true;
2649
2692
  }
2650
2693
  if (changed) {
@@ -2657,30 +2700,23 @@ server.listen(PORT, "127.0.0.1", async () => {
2657
2700
  }
2658
2701
  }
2659
2702
  }
2660
- // #478 + #502: patch deployed AgentChattr instances to support force-replace
2661
- // on register and fix idle-agent crash timeout.
2662
- for (const p of (startupCfg.projects || [])) {
2703
+
2704
+ // ghost-fix + idle-fix
2705
+ for (const p of projects) {
2663
2706
  const acDir = resolveProjectChattr(p.id).dir;
2664
- // Patch registry.py: add force parameter to register()
2665
2707
  const regPath = path.join(acDir, "registry.py");
2666
2708
  if (fs.existsSync(regPath)) {
2667
2709
  try {
2668
2710
  let reg = fs.readFileSync(regPath, "utf-8");
2669
2711
  if (!reg.includes("force: bool")) {
2670
- // Add force parameter to register() signature
2671
2712
  reg = reg.replace(
2672
2713
  /def register\(self, base: str, label: str \| None = None\) -> dict \| None:/,
2673
2714
  "def register(self, base: str, label: str | None = None, force: bool = False) -> dict | None:",
2674
2715
  );
2675
- // Add force-replace logic after _expire_reserved()
2676
2716
  reg = reg.replace(
2677
2717
  " self._expire_reserved()\n\n # Find next free slot",
2678
2718
  " self._expire_reserved()\n\n" +
2679
- " # quadwork#478 + #502: force-replace — expire all existing slots\n" +
2680
- " # for this base so the new registration always lands at slot 1.\n" +
2681
- " # Also clear _reserved entries: after a crash-timeout the old name\n" +
2682
- " # lives only in _reserved, so without this the grace period still\n" +
2683
- " # blocks slot 1 and the agent gets a -2 suffix.\n" +
2719
+ " # quadwork#478 + #502: force-replace\n" +
2684
2720
  " if force:\n" +
2685
2721
  " ghosts = [n for n, i in self._instances.items() if i.base == base]\n" +
2686
2722
  " for name in ghosts:\n" +
@@ -2694,7 +2730,6 @@ server.listen(PORT, "127.0.0.1", async () => {
2694
2730
  fs.writeFileSync(regPath, reg);
2695
2731
  console.log(`[ghost-fix] ${p.id}: patched registry.py with force-replace support`);
2696
2732
  } else if (!reg.includes("stale_reserved")) {
2697
- // #502: upgrade existing force-replace patch to also clear _reserved
2698
2733
  reg = reg.replace(
2699
2734
  /( +)for name in ghosts:\n\1 del self\._instances\[name\]\n\1 self\._reserved\[name\] = time\.time\(\)/,
2700
2735
  "$1for name in ghosts:\n$1 del self._instances[name]\n" +
@@ -2710,7 +2745,6 @@ server.listen(PORT, "127.0.0.1", async () => {
2710
2745
  console.warn(`[ghost-fix] ${p.id}: failed to patch registry.py: ${err.message}`);
2711
2746
  }
2712
2747
  }
2713
- // Patch app.py: pass force from request body to registry.register()
2714
2748
  const appPath = path.join(acDir, "app.py");
2715
2749
  if (fs.existsSync(appPath)) {
2716
2750
  try {
@@ -2727,33 +2761,22 @@ server.listen(PORT, "127.0.0.1", async () => {
2727
2761
  console.warn(`[ghost-fix] ${p.id}: failed to patch app.py: ${err.message}`);
2728
2762
  }
2729
2763
  }
2730
- // #502 + #629: increase crash timeout from 15s to 120s.
2731
- // Uses the shared patchCrashTimeout() from install-agentchattr.js.
2732
- // For existing installs where AC is already running, the on-disk
2733
- // patch alone is useless (Python caches module-level values at import).
2734
- // Flag the project for AC restart so the running process picks it up.
2735
2764
  if (fs.existsSync(appPath)) {
2736
2765
  try {
2737
2766
  const app = fs.readFileSync(appPath, "utf-8");
2738
2767
  if (app.includes("_CRASH_TIMEOUT = 15")) {
2739
2768
  patchCrashTimeout(acDir);
2740
- console.log(`[idle-fix] ${p.id}: crash timeout patched on disk — AC restart required for running process to observe it (#629)`);
2741
- if (!startupCfg._acRestartNeeded) startupCfg._acRestartNeeded = [];
2742
- startupCfg._acRestartNeeded.push(p.id);
2769
+ console.log(`[idle-fix] ${p.id}: crash timeout patched on disk`);
2770
+ acRestartNeeded.push(p.id);
2743
2771
  }
2744
2772
  } catch (err) {
2745
2773
  console.warn(`[idle-fix] ${p.id}: failed to patch app.py crash timeout: ${err.message}`);
2746
2774
  }
2747
2775
  }
2748
2776
  }
2749
- // #596: add CLI-based agent sections to existing config.toml files.
2750
- // Follow-up to #592 (PR #594) which added these for new projects.
2751
- // Existing projects still have role-based-only sections; if their AC
2752
- // drifts to HEAD, registration fails with "unknown base". This
2753
- // migration appends [agents.claude]/[agents.codex] etc. sections so
2754
- // HEAD AC accepts CLI-named bases. No AC restart needed — AC reads
2755
- // config.toml on its own startup.
2756
- for (const p of (startupCfg.projects || [])) {
2777
+
2778
+ // CLI-based agent sections
2779
+ for (const p of projects) {
2757
2780
  const acPath = projectAgentchattrConfigPath(p.id);
2758
2781
  if (!fs.existsSync(acPath)) continue;
2759
2782
  try {
@@ -2780,6 +2803,44 @@ server.listen(PORT, "127.0.0.1", async () => {
2780
2803
  console.warn(`[#596] ${p.id}: config.toml migration failed: ${err.message}`);
2781
2804
  }
2782
2805
  }
2806
+
2807
+ return acRestartNeeded;
2808
+ }
2809
+
2810
+ server.listen(PORT, "127.0.0.1", async () => {
2811
+ console.log(`QuadWork server listening on http://127.0.0.1:${PORT}`);
2812
+ syncTriggersFromConfig();
2813
+ // #579: detect AC processes already running (spawned by cmdStart before
2814
+ // the server module loaded). Without this, chattrProcesses is empty on
2815
+ // boot and the health monitor can't track cmdStart-spawned ACs, while
2816
+ // the dashboard's Start button would redundantly kill+respawn them.
2817
+ const startupCfg = readConfig();
2818
+ for (const p of (startupCfg.projects || [])) {
2819
+ const { url: acUrl } = resolveProjectChattr(p.id);
2820
+ const acPortMatch = acUrl.match(/:(\d+)/);
2821
+ const acPort = acPortMatch ? parseInt(acPortMatch[1], 10) : 8300;
2822
+ const alive = await isPortAlive(acPort);
2823
+ if (alive && !chattrProcesses.has(p.id)) {
2824
+ // AC is already running (e.g. spawned by cmdStart). Record it so
2825
+ // the health monitor can track it and the dashboard shows the
2826
+ // correct state. process is null because we don't own the child.
2827
+ chattrProcesses.set(p.id, { process: null, state: "running", error: null, runningSince: Date.now() });
2828
+ console.log(`[startup] ${p.id}: AC already alive on port ${acPort} — tracking`);
2829
+ }
2830
+ }
2831
+ // Sync AgentChattr tokens for all projects on startup and backfill
2832
+ // the sender-overflow CSS/JS patch (#402) so already-running AC
2833
+ // instances receive the fix without requiring a restart.
2834
+ // #448: retry after 5s for projects where AC isn't up yet at boot.
2835
+ for (const p of (startupCfg.projects || [])) {
2836
+ syncChattrToken(p.id).catch(() => {
2837
+ setTimeout(() => syncChattrToken(p.id).catch(() => {}), 5000);
2838
+ });
2839
+ const { dir: acDir } = resolveProjectChattr(p.id);
2840
+ if (acDir) patchAgentchattrCss(acDir);
2841
+ }
2842
+ const acRestartNeeded = runStartupMigrations(startupCfg);
2843
+ startupCfg._acRestartNeeded = acRestartNeeded.length > 0 ? acRestartNeeded : undefined;
2783
2844
  // #629: restart AC for projects where idle-fix patched the on-disk file
2784
2845
  // so the running Python process picks up _CRASH_TIMEOUT = 120.
2785
2846
  // Use port-alive check instead of chattrProcesses — AC may be running
@@ -44,8 +44,8 @@ function scrubSecrets(text) {
44
44
  }
45
45
 
46
46
  function scrubScrollback(buf) {
47
- if (!buf || buf.length === 0) return buf;
48
- return Buffer.from(scrubSecrets(buf.toString("utf-8")), "utf-8");
47
+ if (!buf || buf.length === 0) return "";
48
+ return scrubSecrets(buf.toString("utf-8"));
49
49
  }
50
50
 
51
51
  module.exports = { scrubSecrets, scrubScrollback, _REDACTED };
@@ -1 +0,0 @@
1
- (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,98183,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={assign:function(){return l},searchParamsToUrlQuery:function(){return s},urlQueryToSearchParams:function(){return i}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});function s(e){let t={};for(let[r,n]of e.entries()){let e=t[r];void 0===e?t[r]=n:Array.isArray(e)?e.push(n):t[r]=[e,n]}return t}function a(e){return"string"==typeof e?e:("number"!=typeof e||isNaN(e))&&"boolean"!=typeof e?"":String(e)}function i(e){let t=new URLSearchParams;for(let[r,n]of Object.entries(e))if(Array.isArray(n))for(let e of n)t.append(r,a(e));else t.set(r,a(n));return t}function l(e,...t){for(let r of t){for(let t of r.keys())e.delete(t);for(let[t,n]of r.entries())e.append(t,n)}return e}},18967,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={DecodeError:function(){return g},MiddlewareNotFoundError:function(){return y},MissingStaticPage:function(){return v},NormalizeError:function(){return b},PageNotFoundError:function(){return j},SP:function(){return p},ST:function(){return m},WEB_VITALS:function(){return s},execOnce:function(){return a},getDisplayName:function(){return d},getLocationOrigin:function(){return c},getURL:function(){return u},isAbsoluteUrl:function(){return l},isResSent:function(){return f},loadGetInitialProps:function(){return x},normalizeRepeatedSlashes:function(){return h},stringifyError:function(){return w}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let s=["CLS","FCP","FID","INP","LCP","TTFB"];function a(e){let t,r=!1;return(...n)=>(r||(r=!0,t=e(...n)),t)}let i=/^[a-zA-Z][a-zA-Z\d+\-.]*?:/,l=e=>i.test(e);function c(){let{protocol:e,hostname:t,port:r}=window.location;return`${e}//${t}${r?":"+r:""}`}function u(){let{href:e}=window.location,t=c();return e.substring(t.length)}function d(e){return"string"==typeof e?e:e.displayName||e.name||"Unknown"}function f(e){return e.finished||e.headersSent}function h(e){let t=e.split("?");return t[0].replace(/\\/g,"/").replace(/\/\/+/g,"/")+(t[1]?`?${t.slice(1).join("?")}`:"")}async function x(e,t){let r=t.res||t.ctx&&t.ctx.res;if(!e.getInitialProps)return t.ctx&&t.Component?{pageProps:await x(t.Component,t.ctx)}:{};let n=await e.getInitialProps(t);if(r&&f(r))return n;if(!n)throw Object.defineProperty(Error(`"${d(e)}.getInitialProps()" should resolve to an object. But found "${n}" instead.`),"__NEXT_ERROR_CODE",{value:"E1025",enumerable:!1,configurable:!0});return n}let p="u">typeof performance,m=p&&["mark","measure","getEntriesByName"].every(e=>"function"==typeof performance[e]);class g extends Error{}class b extends Error{}class j extends Error{constructor(e){super(),this.code="ENOENT",this.name="PageNotFoundError",this.message=`Cannot find module for page: ${e}`}}class v extends Error{constructor(e,t){super(),this.message=`Failed to load static file for page: ${e} ${t}`}}class y extends Error{constructor(){super(),this.code="ENOENT",this.message="Cannot find the middleware module"}}function w(e){return JSON.stringify({message:e.message,stack:e.stack})}},33525,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"warnOnce",{enumerable:!0,get:function(){return n}});let n=e=>{}},18566,(e,t,r)=>{t.exports=e.r(76562)},52368,e=>{"use strict";var t=e.i(43476),r=e.i(71645);let n="qw-locale";function o(e){return"ko"===e?"ko":"en"}let s=(0,r.createContext)(null);e.s(["LocaleProvider",0,function({children:e}){let[a,i]=(0,r.useState)(!1),[l,c]=(0,r.useState)("en");(0,r.useEffect)(()=>{c(function(){try{let e=window.localStorage.getItem(n);if(e)return o(e)}catch{}return window.navigator.language.toLowerCase().startsWith("ko")?"ko":"en"}()),i(!0)},[]),(0,r.useEffect)(()=>{document.documentElement.lang=l;try{window.localStorage.setItem(n,l)}catch{}try{document.cookie=`qw-locale=${l}; path=/; max-age=31536000; samesite=lax`}catch{}},[l]);let u=(0,r.useMemo)(()=>({hydrated:a,locale:l,setLocale:e=>c(o(e))}),[a,l]);return(0,t.jsx)(s.Provider,{value:u,children:e})},"useLocale",0,function(){let e=(0,r.useContext)(s);if(!e)throw Error("useLocale must be used within LocaleProvider");return e}],52368)},67449,e=>{"use strict";let t=[{value:"soft-chime",label:"Soft Chime"},{value:"warm-bell",label:"Warm Bell"},{value:"click",label:"Click"},{value:"alert-tone",label:"Alert Tone"},{value:"pluck",label:"Pluck"}],r="quadwork_notification_sound",n="quadwork_notification_sound_choice",o="quadwork_notification_sound_background_only";function s(e){try{return window.localStorage.getItem(e)}catch{return null}}function a(e,t){try{window.localStorage.setItem(e,t)}catch{}}function i(){return"off"!==s(r)}function l(){let e=s(n);return e&&t.some(t=>t.value===e)?e:"soft-chime"}function c(){let e=s(o);return null===e||"off"!==e}e.s(["NOTIFICATION_SOUND_OPTIONS",0,t,"getNotificationBackgroundOnly",0,c,"getNotificationChoice",0,l,"getNotificationEnabled",0,i,"playNotificationSound",0,function(){if(!i()||c()&&document.hasFocus())return;let e=l();try{let t=new Audio(`/sounds/${e}.mp3`);t.volume=.6,t.play().catch(()=>{})}catch{}},"setNotificationBackgroundOnly",0,function(e){a(o,e?"on":"off")},"setNotificationChoice",0,function(e){a(n,e)},"setNotificationEnabled",0,function(e){a(r,e?"on":"off")}])},95057,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={formatUrl:function(){return i},formatWithValidation:function(){return c},urlObjectKeys:function(){return l}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let s=e.r(90809)._(e.r(98183)),a=/https?|ftp|gopher|file/;function i(e){let{auth:t,hostname:r}=e,n=e.protocol||"",o=e.pathname||"",i=e.hash||"",l=e.query||"",c=!1;t=t?encodeURIComponent(t).replace(/%3A/i,":")+"@":"",e.host?c=t+e.host:r&&(c=t+(~r.indexOf(":")?`[${r}]`:r),e.port&&(c+=":"+e.port)),l&&"object"==typeof l&&(l=String(s.urlQueryToSearchParams(l)));let u=e.search||l&&`?${l}`||"";return n&&!n.endsWith(":")&&(n+=":"),e.slashes||(!n||a.test(n))&&!1!==c?(c="//"+(c||""),o&&"/"!==o[0]&&(o="/"+o)):c||(c=""),i&&"#"!==i[0]&&(i="#"+i),u&&"?"!==u[0]&&(u="?"+u),o=o.replace(/[?#]/g,encodeURIComponent),u=u.replace("#","%23"),`${n}${c}${o}${u}${i}`}let l=["auth","hash","host","hostname","href","path","pathname","port","protocol","query","search","slashes"];function c(e){return i(e)}},18581,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"useMergedRef",{enumerable:!0,get:function(){return o}});let n=e.r(71645);function o(e,t){let r=(0,n.useRef)(null),o=(0,n.useRef)(null);return(0,n.useCallback)(n=>{if(null===n){let e=r.current;e&&(r.current=null,e());let t=o.current;t&&(o.current=null,t())}else e&&(r.current=s(e,n)),t&&(o.current=s(t,n))},[e,t])}function s(e,t){if("function"!=typeof e)return e.current=t,()=>{e.current=null};{let r=e(t);return"function"==typeof r?r:()=>e(null)}}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},73668,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"isLocalURL",{enumerable:!0,get:function(){return s}});let n=e.r(18967),o=e.r(52817);function s(e){if(!(0,n.isAbsoluteUrl)(e))return!0;try{let t=(0,n.getLocationOrigin)(),r=new URL(e,t);return r.origin===t&&(0,o.hasBasePath)(r.pathname)}catch(e){return!1}}},84508,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"errorOnce",{enumerable:!0,get:function(){return n}});let n=e=>{}},22016,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={default:function(){return g},useLinkStatus:function(){return j}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let s=e.r(90809),a=e.r(43476),i=s._(e.r(71645)),l=e.r(95057),c=e.r(8372),u=e.r(18581),d=e.r(18967),f=e.r(5550);e.r(33525);let h=e.r(88540),x=e.r(91949),p=e.r(73668),m=e.r(9396);function g(t){var r,n;let o,s,g,[j,v]=(0,i.useOptimistic)(x.IDLE_LINK_STATUS),y=(0,i.useRef)(null),{href:w,as:k,children:N,prefetch:C=null,passHref:S,replace:E,shallow:_,scroll:L,onClick:P,onMouseEnter:M,onTouchStart:O,legacyBehavior:I=!1,onNavigate:$,transitionTypes:T,ref:R,unstable_dynamicOnHover:A,...B}=t;o=N,I&&("string"==typeof o||"number"==typeof o)&&(o=(0,a.jsx)("a",{children:o}));let W=i.default.useContext(c.AppRouterContext),F=!1!==C,U=!1!==C?null===(n=C)||"auto"===n?m.FetchStrategy.PPR:m.FetchStrategy.Full:m.FetchStrategy.PPR,z="string"==typeof(r=k||w)?r:(0,l.formatUrl)(r);if(I){if(o?.$$typeof===Symbol.for("react.lazy"))throw Object.defineProperty(Error("`<Link legacyBehavior>` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's `<a>` tag."),"__NEXT_ERROR_CODE",{value:"E863",enumerable:!1,configurable:!0});s=i.default.Children.only(o)}let q=I?s&&"object"==typeof s&&s.ref:R,D=i.default.useCallback(e=>(null!==W&&(y.current=(0,x.mountLinkInstance)(e,z,W,U,F,v)),()=>{y.current&&((0,x.unmountLinkForCurrentNavigation)(y.current),y.current=null),(0,x.unmountPrefetchableInstance)(e)}),[F,z,W,U,v]),K={ref:(0,u.useMergedRef)(D,q),onClick(t){I||"function"!=typeof P||P(t),I&&s.props&&"function"==typeof s.props.onClick&&s.props.onClick(t),!W||t.defaultPrevented||function(t,r,n,o,s,a,l){if("u">typeof window){let c,{nodeName:u}=t.currentTarget;if("A"===u.toUpperCase()&&((c=t.currentTarget.getAttribute("target"))&&"_self"!==c||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.nativeEvent&&2===t.nativeEvent.which)||t.currentTarget.hasAttribute("download"))return;if(!(0,p.isLocalURL)(r)){o&&(t.preventDefault(),location.replace(r));return}if(t.preventDefault(),a){let e=!1;if(a({preventDefault:()=>{e=!0}}),e)return}let{dispatchNavigateAction:d}=e.r(99781);i.default.startTransition(()=>{d(r,o?"replace":"push",!1===s?h.ScrollBehavior.NoScroll:h.ScrollBehavior.Default,n.current,l)})}}(t,z,y,E,L,$,T)},onMouseEnter(e){I||"function"!=typeof M||M(e),I&&s.props&&"function"==typeof s.props.onMouseEnter&&s.props.onMouseEnter(e),W&&F&&(0,x.onNavigationIntent)(e.currentTarget,!0===A)},onTouchStart:function(e){I||"function"!=typeof O||O(e),I&&s.props&&"function"==typeof s.props.onTouchStart&&s.props.onTouchStart(e),W&&F&&(0,x.onNavigationIntent)(e.currentTarget,!0===A)}};return(0,d.isAbsoluteUrl)(z)?K.href=z:I&&!S&&("a"!==s.type||"href"in s.props)||(K.href=(0,f.addBasePath)(z)),g=I?i.default.cloneElement(s,K):(0,a.jsx)("a",{...B,...K,children:o}),(0,a.jsx)(b.Provider,{value:j,children:g})}e.r(84508);let b=(0,i.createContext)(x.IDLE_LINK_STATUS),j=()=>(0,i.useContext)(b);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},22140,e=>{"use strict";var t=e.i(43476),r=e.i(22016),n=e.i(18566),o=e.i(71645);function s(){return(0,t.jsx)("svg",{width:"20",height:"20",viewBox:"0 0 20 20",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",children:(0,t.jsx)("path",{d:"M3 5h14M3 10h14M3 15h14"})})}function a(){return(0,t.jsx)("svg",{width:"20",height:"20",viewBox:"0 0 20 20",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",children:(0,t.jsx)("path",{d:"M5 5l10 10M15 5L5 15"})})}function i(){return(0,t.jsxs)("svg",{width:"20",height:"20",viewBox:"0 0 20 20",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[(0,t.jsx)("path",{d:"M3 10L10 3l7 7"}),(0,t.jsx)("path",{d:"M5 8.5V16h3.5v-4h3v4H15V8.5"})]})}function l(){return(0,t.jsxs)("svg",{width:"18",height:"18",viewBox:"0 0 18 18",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:[(0,t.jsx)("circle",{cx:"9",cy:"9",r:"2.5"}),(0,t.jsx)("path",{d:"M7.5 1.5h3l.4 2.1a5.5 5.5 0 011.3.7l2-.8 1.5 2.6-1.6 1.3a5.5 5.5 0 010 1.5l1.6 1.3-1.5 2.6-2-.8a5.5 5.5 0 01-1.3.7l-.4 2.1h-3l-.4-2.1a5.5 5.5 0 01-1.3-.7l-2 .8-1.5-2.6 1.6-1.3a5.5 5.5 0 010-1.5L2.3 6.1l1.5-2.6 2 .8a5.5 5.5 0 011.3-.7z"})]})}function c(){return(0,t.jsx)("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",children:(0,t.jsx)("path",{d:"M8 3v10M3 8h10"})})}function u(){return(0,t.jsx)("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:(0,t.jsx)("path",{d:"M10 3L5 8l5 5"})})}function d(){return(0,t.jsx)("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",children:(0,t.jsx)("path",{d:"M6 3l5 5-5 5"})})}function f({collapsed:e}){return(0,t.jsx)("svg",{width:"12",height:"12",viewBox:"0 0 12 12",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",className:`transition-transform duration-150 ${e?"-rotate-90":""}`,children:(0,t.jsx)("path",{d:"M3 4.5l3 3 3-3"})})}function h({size:e=10}){return(0,t.jsxs)("svg",{width:e,height:e,viewBox:"0 0 16 16",fill:"currentColor",stroke:"none",children:[(0,t.jsx)("path",{d:"M10.5 1.5L14.5 5.5L10 7.5L8.5 12.5L3.5 7.5L8.5 6L10.5 1.5Z"}),(0,t.jsx)("path",{d:"M3.5 7.5L1 15L8.5 12.5"})]})}function x({project:e,isActive:n,expanded:s,pinned:a,onContextMenu:i}){let[l,c]=(0,o.useState)(null),u=(0,o.useRef)(null);return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(r.default,{ref:u,href:`/project/${e.id}`,className:`flex items-center gap-2 ${s?"w-full px-2":""} rounded-sm transition-colors ${!s?"":n?"bg-[#1a1a1a]":"hover:bg-[#1a1a1a]"}`,onMouseEnter:()=>{if(s)return;let e=u.current?.getBoundingClientRect();e&&c({top:e.top+e.height/2})},onMouseLeave:()=>c(null),onContextMenu:t=>i(t,e.id),children:[(0,t.jsxs)("div",{className:"relative shrink-0",children:[(0,t.jsx)("div",{className:`w-10 h-10 flex items-center justify-center rounded-full text-[11px] font-semibold uppercase tracking-tight transition-colors ${n?"border-2 border-accent text-accent":"border border-border text-text-muted hover:text-text"}`,children:e.name.slice(0,2)||"?"}),a&&!s&&(0,t.jsx)("div",{className:"absolute -top-1 -right-1 text-accent",children:(0,t.jsx)(h,{size:8})})]}),s&&(0,t.jsxs)("span",{className:`text-xs truncate flex items-center gap-1 ${n?"text-accent":"text-text-muted"}`,children:[e.name,a&&(0,t.jsx)(h,{size:10})]})]}),!s&&l&&(0,t.jsxs)("div",{className:"fixed px-2 py-1 bg-bg-surface border border-border text-text text-xs whitespace-nowrap pointer-events-none z-50",style:{left:72,top:l.top,transform:"translateY(-50%)"},children:[a&&"📌 ",e.name]})]})}let p="qw-sidebar-expanded",m="qw-sidebar-collapsed-groups";e.s(["default",0,function(){let e=(0,n.usePathname)(),[h,g]=(0,o.useState)([]),[b,j]=(0,o.useState)([]),[v,y]=(0,o.useState)([]),[w,k]=(0,o.useState)(new Set),[N,C]=(0,o.useState)("online"),[S,E]=(0,o.useState)(!1),[_,L]=(0,o.useState)(!1),[P,M]=(0,o.useState)(null),[O,I]=(0,o.useState)(""),$=(0,o.useRef)(null);(0,o.useEffect)(()=>{L(!1)},[e]),(0,o.useEffect)(()=>{try{if(window.innerWidth>=768){let e=localStorage.getItem(p);"true"===e&&E(!0)}let e=localStorage.getItem(m);e&&k(new Set(JSON.parse(e)))}catch{}},[]),(0,o.useEffect)(()=>{let e=window.matchMedia("(max-width: 767px)"),t=e=>{e.matches&&E(!1)};return e.addEventListener("change",t),()=>e.removeEventListener("change",t)},[]),(0,o.useEffect)(()=>{fetch("/api/version").then(e=>e.json()).then(e=>I(e.version||"")).catch(()=>{})},[]),(0,o.useEffect)(()=>{fetch("/api/config").then(e=>{if(!e.ok)throw Error(`Config fetch failed: ${e.status}`);return e.json()}).then(e=>{$.current=e,g((e.projects||[]).filter(e=>!e.archived)),j(e.pinned_projects||[]),y(e.sidebar_groups||[])}).catch(()=>{})},[]),(0,o.useEffect)(()=>{let e,t=!1,r=async()=>{try{let e=await fetch("/api/health",{signal:AbortSignal.timeout(3e3)});if(t)return;e.ok?C(e=>"offline"===e?"recovering":"online"):C("offline")}catch{if(t)return;C("offline")}t||(e=setTimeout(r,5e3))};return r(),()=>{t=!0,clearTimeout(e)}},[]),(0,o.useEffect)(()=>{if("recovering"===N){let e=setTimeout(()=>C("online"),1500);return()=>clearTimeout(e)}},[N]);let T="/"===e,R="/settings"===e,A=e.startsWith("/project/")?e.split("/")[2]:null,B=(0,o.useCallback)(e=>{j(e),fetch("/api/config").then(e=>e.json()).then(t=>{let r={...t,pinned_projects:e};return $.current=r,fetch("/api/config",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)})}).catch(()=>{})},[]),W=(0,o.useCallback)(e=>{y(e),fetch("/api/config").then(e=>e.json()).then(t=>{let r={...t,sidebar_groups:e};return $.current=r,fetch("/api/config",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)})}).catch(()=>{})},[]),F=(e,t)=>{let r=v.map(t=>({...t,projects:t.projects.filter(t=>t!==e)}));if("__ungrouped__"===t)W(r.filter(e=>e.projects.length>0));else{let n=r.find(e=>e.name===t);n?n.projects.push(e):r.push({name:t,projects:[e]}),W(r.filter(e=>e.projects.length>0))}M(null)},U=(e,t)=>{e.preventDefault(),M({x:e.clientX,y:e.clientY,projectId:t})};(0,o.useEffect)(()=>{if(!P)return;let e=()=>M(null);return window.addEventListener("click",e),()=>window.removeEventListener("click",e)},[P]);let z=new Set(b),q=b.map(e=>h.find(t=>t.id===e)).filter(e=>!!e),D=new Set(v.flatMap(e=>e.projects)),K=h.filter(e=>!z.has(e.id)),Q=K.filter(e=>!D.has(e.id)),G=e=>(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(r.default,{href:"/",className:`flex items-center gap-2 rounded-sm transition-colors ${e?"px-2 py-2":"w-10 h-10 justify-center self-center"} ${T?"text-accent":"text-text-muted hover:text-text hover:bg-[#1a1a1a]"}`,title:"Home",children:[(0,t.jsx)(i,{}),e&&(0,t.jsx)("span",{className:"text-xs",children:"Home"})]}),(0,t.jsx)("div",{className:`h-px bg-border my-2 ${e?"":"w-6 self-center"}`}),(0,t.jsxs)("div",{className:`flex-1 flex flex-col gap-2 overflow-y-auto min-h-0 ${e?"":"items-center"}`,children:[q.length>0&&(0,t.jsxs)(t.Fragment,{children:[e&&(0,t.jsx)("span",{className:"text-[10px] uppercase tracking-widest text-text-muted px-2",children:"Pinned"}),q.map(r=>(0,t.jsx)(x,{project:r,isActive:A===r.id,expanded:e,pinned:!0,onContextMenu:U},r.id)),(0,t.jsx)("div",{className:`h-px bg-border ${e?"":"w-6"}`})]}),v.map(r=>{let n=r.projects.map(e=>K.find(t=>t.id===e)).filter(e=>!!e);if(0===n.length)return null;let o=w.has(r.name);return(0,t.jsxs)("div",{className:"flex flex-col gap-1",children:[(0,t.jsx)("button",{onClick:()=>{var e;return e=r.name,void k(t=>{let r=new Set(t);r.has(e)?r.delete(e):r.add(e);try{localStorage.setItem(m,JSON.stringify([...r]))}catch{}return r})},className:`flex items-center gap-1 text-text-muted hover:text-text transition-colors ${e?"px-2 py-0.5":"justify-center w-full"}`,title:`${o?"Expand":"Collapse"} ${r.name}`,children:e?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(f,{collapsed:o}),(0,t.jsx)("span",{className:"text-[10px] uppercase tracking-widest truncate",children:r.name})]}):(0,t.jsx)("div",{className:`w-6 h-px ${o?"bg-text-muted":"bg-border"}`})}),!o&&n.map(r=>(0,t.jsx)(x,{project:r,isActive:A===r.id,expanded:e,pinned:!1,onContextMenu:U},r.id))]},r.name)}),Q.length>0&&v.length>0&&(0,t.jsxs)(t.Fragment,{children:[e&&(0,t.jsx)("span",{className:"text-[10px] uppercase tracking-widest text-text-muted px-2",children:"Ungrouped"}),!e&&v.length>0&&(0,t.jsx)("div",{className:"w-6 h-px bg-border"})]}),Q.map(r=>(0,t.jsx)(x,{project:r,isActive:A===r.id,expanded:e,pinned:!1,onContextMenu:U},r.id)),(0,t.jsxs)(r.default,{href:"/setup",className:`flex items-center gap-2 rounded-full transition-colors ${e?"px-2 py-2 border border-dashed border-border text-text-muted hover:text-text hover:bg-[#1a1a1a] rounded-sm":"w-10 h-10 justify-center border border-dashed border-border text-text-muted hover:text-text hover:bg-[#1a1a1a]"}`,title:"Add project",children:[(0,t.jsx)(c,{}),e&&(0,t.jsx)("span",{className:"text-xs text-text-muted",children:"New Project"})]})]}),P&&(0,t.jsxs)("div",{className:"fixed bg-bg-surface border border-border py-1 z-50 text-xs",style:{left:P.x,top:P.y},onClick:e=>e.stopPropagation(),children:[z.has(P.projectId)?(0,t.jsx)("button",{className:"w-full px-3 py-1.5 text-left text-text hover:bg-[#1a1a1a] transition-colors",onClick:()=>{var e;return e=P.projectId,void(B(b.filter(t=>t!==e)),M(null))},children:"Unpin"}):(0,t.jsx)("button",{className:"w-full px-3 py-1.5 text-left text-text hover:bg-[#1a1a1a] transition-colors",onClick:()=>{var e;return e=P.projectId,void(!b.includes(e)&&(B([e,...b]),M(null)))},children:"Pin to top"}),(0,t.jsx)("div",{className:"h-px bg-border my-1"}),P.showGroupMenu?(0,t.jsxs)("div",{className:"flex flex-col",children:[v.map(e=>(0,t.jsx)("button",{className:"w-full px-3 py-1.5 text-left text-text hover:bg-[#1a1a1a] transition-colors",onClick:()=>F(P.projectId,e.name),children:e.name},e.name)),(0,t.jsx)("button",{className:"w-full px-3 py-1.5 text-left text-text hover:bg-[#1a1a1a] transition-colors",onClick:()=>{let e=prompt("New group name:");if(!e?.trim())return;let t=e.trim();v.some(e=>e.name.toLowerCase()===t.toLowerCase())?alert(`Group "${t}" already exists.`):F(P.projectId,t)},children:"+ New group"}),D.has(P.projectId)&&(0,t.jsx)("button",{className:"w-full px-3 py-1.5 text-left text-text-muted hover:bg-[#1a1a1a] transition-colors",onClick:()=>F(P.projectId,"__ungrouped__"),children:"Remove from group"})]}):(0,t.jsx)("button",{className:"w-full px-3 py-1.5 text-left text-text hover:bg-[#1a1a1a] transition-colors",onClick:()=>M({...P,showGroupMenu:!0}),children:"Move to group..."})]}),(0,t.jsx)("div",{className:`h-px bg-border my-2 ${e?"":"w-6 self-center"}`}),"online"!==N&&(0,t.jsxs)("div",{className:`mb-2 relative group ${e?"px-2":"self-center"}`,children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("div",{className:`w-3 h-3 shrink-0 rounded-full ${"offline"===N?"bg-red-500 animate-pulse":"bg-green-500"}`}),e&&(0,t.jsx)("span",{className:"text-xs text-text-muted",children:"offline"===N?"Backend offline":"Reconnected"})]}),!e&&(0,t.jsx)("div",{className:"fixed left-16 ml-2 px-2 py-1 bg-bg-surface border border-border text-xs whitespace-nowrap z-50 hidden group-hover:block",style:{transform:"translateY(-50%)",top:"auto"},children:"offline"===N?"Backend offline — run quadwork start":"Backend reconnected"})]}),(0,t.jsxs)(r.default,{href:"/settings",className:`flex items-center gap-2 rounded-sm transition-colors ${e?"px-2 py-2":"w-10 h-10 justify-center self-center"} ${R?"text-accent":"text-text-muted hover:text-text hover:bg-[#1a1a1a]"}`,title:"Settings",children:[(0,t.jsx)(l,{}),e&&(0,t.jsx)("span",{className:"text-xs",children:"Settings"})]}),O&&(0,t.jsxs)("div",{className:`text-[10px] text-text-muted/40 ${e?"px-3":"text-center"} pt-2`,children:["v",O]})]});return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("button",{type:"button",onClick:()=>L(!0),className:"fixed top-14 left-2 z-30 lg:hidden w-10 h-10 flex items-center justify-center bg-bg-surface border border-border text-text-muted hover:text-accent",children:(0,t.jsx)(s,{})}),_&&(0,t.jsx)("div",{className:"fixed inset-0 z-40 bg-black/50 lg:hidden",onClick:()=>L(!1)}),(0,t.jsxs)("aside",{className:`fixed inset-y-0 left-0 z-50 w-52 bg-bg-surface border-r border-border flex flex-col py-3 px-2 items-stretch overflow-y-auto transition-transform duration-200 ease-in-out lg:hidden ${_?"translate-x-0":"-translate-x-full"}`,children:[(0,t.jsx)("button",{type:"button",onClick:()=>L(!1),className:"self-end shrink-0 w-10 h-10 flex items-center justify-center text-text-muted hover:text-accent mb-1",children:(0,t.jsx)(a,{})}),G(!0)]}),(0,t.jsxs)("aside",{className:`hidden lg:flex shrink-0 h-full border-r border-border bg-bg-surface flex-col py-3 transition-[width] duration-200 ease-in-out overflow-hidden ${S?"w-52 items-stretch px-2":"w-16 items-center"}`,children:[G(S),(0,t.jsx)("div",{className:"h-1"}),(0,t.jsx)("button",{onClick:()=>{E(e=>{let t=!e;try{localStorage.setItem(p,String(t))}catch{}return t})},className:`flex shrink-0 items-center justify-center w-10 h-10 rounded-sm border border-border text-text-muted hover:text-accent hover:border-accent/50 transition-colors ${S?"self-end":"self-center"}`,title:S?"Collapse sidebar":"Expand sidebar",children:S?(0,t.jsx)(u,{}):(0,t.jsx)(d,{})})]})]})}])},26704,e=>{"use strict";var t=e.i(43476),r=e.i(22016),n=e.i(71645);function o({open:e,onClose:r}){return((0,n.useEffect)(()=>{if(!e)return;let t=e=>{"Escape"===e.key&&r()};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[e,r]),e)?(0,t.jsx)("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:r,role:"dialog","aria-modal":"true","aria-labelledby":"about-title",children:(0,t.jsxs)("div",{className:"relative mx-4 max-w-lg w-full rounded-lg border border-white/10 bg-neutral-950 p-6 shadow-2xl",onClick:e=>e.stopPropagation(),children:[(0,t.jsx)("button",{type:"button",onClick:r,"aria-label":"Close",className:"absolute right-3 top-3 rounded p-1 text-neutral-400 hover:bg-white/5 hover:text-white",children:(0,t.jsx)("svg",{width:"18",height:"18",viewBox:"0 0 20 20",fill:"none",stroke:"currentColor",strokeWidth:"1.8",children:(0,t.jsx)("path",{d:"M4 4l12 12M16 4L4 16",strokeLinecap:"round"})})}),(0,t.jsx)("h2",{id:"about-title",className:"text-lg font-semibold text-white",children:"What is QuadWork?"}),(0,t.jsx)("p",{className:"mt-3 text-sm leading-relaxed text-neutral-300",children:"QuadWork is a local dashboard that runs a team of 4 AI agents — Head, Dev, and two Reviewers — that code, review, and ship while you sleep."}),(0,t.jsx)("p",{className:"mt-3 text-sm leading-relaxed text-neutral-300",children:"Every task follows a strict GitHub workflow: Issue → Branch → Pull Request → 2 Reviews → Merge. Branch protection ensures no agent can skip the process."}),(0,t.jsx)("h3",{className:"mt-5 text-sm font-semibold text-white",children:"Why QuadWork?"}),(0,t.jsxs)("ul",{className:"mt-2 space-y-1.5 text-sm text-neutral-300",children:[(0,t.jsxs)("li",{children:["🤖 ",(0,t.jsx)("b",{children:"Run 24/7"})," — agents work overnight while you rest"]}),(0,t.jsxs)("li",{children:["🛡️ ",(0,t.jsx)("b",{children:"Always reviewed"})," — every PR needs 2 independent approvals"]}),(0,t.jsxs)("li",{children:["🔒 ",(0,t.jsx)("b",{children:"Local-first"})," — runs entirely on your machine, no data leaves"]}),(0,t.jsxs)("li",{children:["🧰 ",(0,t.jsx)("b",{children:"Bring your own CLI"})," — works with Claude Code, Codex, or both"]}),(0,t.jsxs)("li",{children:["📦 ",(0,t.jsx)("b",{children:"One install"})," — ",(0,t.jsx)("code",{className:"rounded bg-white/5 px-1 py-0.5 text-[12px]",children:"npx quadwork init"})," and you're set"]})]}),(0,t.jsx)("div",{className:"mt-5",children:(0,t.jsx)("a",{href:"https://github.com/realproject7/quadwork",target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1 text-sm text-blue-400 hover:text-blue-300",children:"Read the full docs on GitHub →"})})]})}):null}let s=["sleep.","eat.","enjoy life.","touch grass.","spend time with people.","watch a movie.","take a vacation.","go for a run."],a="quadwork_tagline_animation";function i(e){return!Number.isFinite(e)||e<=0?"0h":e<1?`${(60*e).toFixed(0)}m`:`${e.toFixed(1)}h`}e.s(["default",0,function(){let[e,l]=(0,n.useState)(!1);(0,n.useEffect)(()=>{l(!0)},[]);let[c,u]=(0,n.useState)(!1),[d,f]=(0,n.useState)(null),[h,x]=(0,n.useState)(!1);(0,n.useEffect)(()=>{let e=!1,t=()=>{fetch("/api/activity/stats").then(e=>e.ok?e.json():null).then(t=>{!e&&t&&f(t)}).catch(()=>{})};t();let r=setInterval(t,6e4);return()=>{e=!0,clearInterval(r)}},[]);let[p,m]=(0,n.useState)(!0);(0,n.useEffect)(()=>{try{let e=window.localStorage.getItem(a);"off"===e&&m(!1)}catch{}},[]);let g=function(e,t){let[r,o]=(0,n.useState)(0),[s,a]=(0,n.useState)(""),[i,l]=(0,n.useState)("typing");return(0,n.useEffect)(()=>{let n;if(!t)return;let c=e[r];return"typing"===i?n=s.length<c.length?setTimeout(()=>a(c.slice(0,s.length+1)),70):setTimeout(()=>l("holding"),0):"holding"===i?n=setTimeout(()=>l("deleting"),2e3):s.length>0?n=setTimeout(()=>a(c.slice(0,s.length-1)),35):(o(t=>(t+1)%e.length),l("typing")),()=>clearTimeout(n)},[s,i,r,e,t]),s}(s,p),b=p?g:s[0]||"",[j,v]=(0,n.useState)(!1);(0,n.useEffect)(()=>{if(!p)return void v(!0);let e=setTimeout(()=>v(!0),5e3);return()=>clearTimeout(e)},[p]);let y=(0,n.useRef)(!1);return((0,n.useEffect)(()=>{p&&(g.length>0?y.current=!0:y.current&&v(!0))},[p,g]),e)?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("header",{className:"sticky top-0 z-40 flex h-12 items-center justify-between border-b border-white/10 bg-neutral-950/90 px-4 backdrop-blur",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 min-w-0",children:[(0,t.jsxs)(r.default,{href:"/",className:"flex items-center gap-1.5 text-sm font-bold text-accent hover:text-blue-400 shrink-0",children:[(0,t.jsx)("img",{src:"/icon.svg",alt:"",width:18,height:18,className:"inline-block"}),"QuadWork"]}),(0,t.jsx)("span",{className:"hidden sm:inline text-neutral-600",children:"|"}),(0,t.jsxs)("span",{className:"hidden sm:inline text-[13px] text-neutral-400 truncate",children:["Your AI dev team while you"," ",(0,t.jsx)("span",{className:"text-neutral-200",children:b}),p&&(0,t.jsx)("span",{className:"ml-0.5 inline-block w-[1px] h-[12px] align-middle bg-neutral-400 animate-qw-blink"})]}),j&&(0,t.jsx)("button",{type:"button",onClick:()=>{m(e=>{let t=!e;try{window.localStorage.setItem(a,t?"on":"off")}catch{}return t})},"aria-label":p?"Pause tagline animation":"Resume tagline animation","aria-pressed":p,title:p?"Pause tagline animation":"Resume tagline animation",className:"hidden sm:inline-flex items-center justify-center w-3.5 h-3.5 ml-1 rounded-full border border-white/15 text-neutral-500 hover:text-white hover:border-white/40 transition-colors text-[8px]",children:p?"❚❚":"▶"})]}),(0,t.jsxs)("div",{className:"flex items-center gap-3 shrink-0",children:[d&&(0,t.jsxs)("div",{className:"relative hidden md:flex items-center gap-2 text-[10px] text-neutral-500",onMouseEnter:()=>x(!0),onMouseLeave:()=>x(!1),onFocus:()=>x(!0),onBlur:()=>x(!1),tabIndex:0,children:[(0,t.jsx)("span",{className:"text-neutral-200",children:"Your AI team worked:"}),(0,t.jsxs)("span",{children:["Today ",(0,t.jsx)("span",{className:"text-neutral-200",children:i(d.today)})]}),(0,t.jsx)("span",{className:"text-neutral-700",children:"·"}),(0,t.jsxs)("span",{children:["Week ",(0,t.jsx)("span",{className:"text-neutral-200",children:i(d.week)})]}),(0,t.jsx)("span",{className:"text-neutral-700",children:"·"}),(0,t.jsxs)("span",{children:["Month ",(0,t.jsx)("span",{className:"text-neutral-200",children:i(d.month)})]}),h&&(0,t.jsxs)("div",{className:"absolute top-6 right-0 z-50 min-w-[220px] p-2 text-[10px] leading-snug text-neutral-200 bg-neutral-900 border border-white/15 rounded shadow-lg",children:[(0,t.jsx)("div",{className:"mb-1 text-neutral-400 uppercase tracking-wider text-[9px]",children:"Per project"}),0===Object.entries(d.by_project).length&&(0,t.jsx)("div",{className:"text-neutral-500",children:"No activity logged yet"}),Object.entries(d.by_project).map(([e,r])=>(0,t.jsxs)("div",{className:"flex items-baseline gap-2",children:[(0,t.jsx)("span",{className:"text-neutral-400 truncate flex-1",children:e}),(0,t.jsx)("span",{className:"tabular-nums text-neutral-200",children:i(r.month)}),(0,t.jsx)("span",{className:"text-neutral-600 text-[9px]",children:"/ mo"})]},e)),(0,t.jsxs)("div",{className:"mt-1 pt-1 border-t border-white/10 text-neutral-500",children:["Lifetime: ",(0,t.jsx)("span",{className:"text-neutral-200",children:i(d.total)})]}),(0,t.jsx)("div",{className:"mt-1 pt-1 border-t border-white/10 text-neutral-500 leading-snug",children:"ⓘ Stats are best-effort. Server restarts may undercount in-flight sessions."})]})]}),(0,t.jsx)("button",{type:"button",onClick:()=>u(!0),"aria-label":"About QuadWork",className:"rounded p-1 text-neutral-400 hover:bg-white/5 hover:text-white",children:(0,t.jsxs)("svg",{width:"18",height:"18",viewBox:"0 0 20 20",fill:"none",stroke:"currentColor",strokeWidth:"1.5",children:[(0,t.jsx)("circle",{cx:"10",cy:"10",r:"8"}),(0,t.jsx)("path",{d:"M10 9v5",strokeLinecap:"round"}),(0,t.jsx)("circle",{cx:"10",cy:"6.5",r:"0.8",fill:"currentColor"})]})}),(0,t.jsx)("a",{href:"https://github.com/realproject7/quadwork",target:"_blank",rel:"noopener noreferrer",className:"text-[12px] text-neutral-400 hover:text-white",children:"QuadWork github"})]})]}),(0,t.jsx)(o,{open:c,onClose:()=>u(!1)})]}):(0,t.jsx)("header",{className:"sticky top-0 z-40 flex h-12 items-center justify-between border-b border-white/10 bg-neutral-950/90 px-4 backdrop-blur","aria-hidden":"true"})}],26704)},43688,e=>{"use strict";var t=e.i(71645),r=e.i(67449);e.s(["default",0,function(){let e=(0,t.useRef)({}),n=(0,t.useRef)("user"),o=(0,t.useRef)([]),s=(0,t.useRef)(!1),a=(0,t.useRef)(new Set),i=(0,t.useCallback)(t=>{null==e.current[t]&&(e.current[t]=0,a.current.add(t))},[]),l=(0,t.useCallback)(()=>{fetch("/api/config").then(e=>e.ok?e.json():null).then(t=>{if(!t)return;"string"==typeof t.operator_name&&t.operator_name&&(n.current=t.operator_name);let r=t.projects||[];for(let e of(o.current=r,r))i(e.id);let a=new Set(r.map(e=>e.id));for(let t of Object.keys(e.current))a.has(t)||delete e.current[t];s.current=!0}).catch(()=>{})},[i]);(0,t.useEffect)(()=>{l();let e=setInterval(l,3e4);return()=>clearInterval(e)},[l]);let c=(0,t.useCallback)(()=>{if(s.current)for(let t of o.current){let o=e.current[t.id];null!=o&&fetch(`/api/chat?path=/api/messages&channel=general&cursor=${o}&project=${encodeURIComponent(t.id)}`).then(e=>e.ok?e.json():null).then(o=>{if(!o)return;let s=Array.isArray(o)?o:o.messages||[];if(0===s.length)return void a.current.delete(t.id);let i=e.current[t.id]??0,l=Math.max(...s.map(e=>e.id));l>i&&(e.current[t.id]=l);let c=n.current;a.current.has(t.id)?a.current.delete(t.id):s.some(e=>e.id>i&&(void 0===e.type||"chat"===e.type)&&"user"!==e.sender&&e.sender!==c&&"system"!==e.sender)&&(0,r.playNotificationSound)()}).catch(()=>{})}},[]);return(0,t.useEffect)(()=>{let e=setInterval(c,3e3);return()=>clearInterval(e)},[c]),null}])}]);