codedeck 0.2.0 → 0.4.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 (125) hide show
  1. package/README.md +45 -16
  2. package/dist/cli/charts/bar-chart.js +104 -0
  3. package/dist/cli/charts/bar-chart.js.map +1 -0
  4. package/dist/cli/charts/sparkline.js +18 -0
  5. package/dist/cli/charts/sparkline.js.map +1 -0
  6. package/dist/cli/charts/stacked-bar.js +59 -0
  7. package/dist/cli/charts/stacked-bar.js.map +1 -0
  8. package/dist/cli/cli-name.js +20 -0
  9. package/dist/cli/cli-name.js.map +1 -0
  10. package/dist/cli/commands/claims.js +170 -0
  11. package/dist/cli/commands/claims.js.map +1 -0
  12. package/dist/cli/commands/doctor.js +22 -1
  13. package/dist/cli/commands/doctor.js.map +1 -1
  14. package/dist/cli/commands/models.js +5 -5
  15. package/dist/cli/commands/models.js.map +1 -1
  16. package/dist/cli/commands/open.js +309 -315
  17. package/dist/cli/commands/open.js.map +1 -1
  18. package/dist/cli/commands/ps.js +226 -23
  19. package/dist/cli/commands/ps.js.map +1 -1
  20. package/dist/cli/commands/rename.js +29 -0
  21. package/dist/cli/commands/rename.js.map +1 -0
  22. package/dist/cli/commands/run.js +50 -18
  23. package/dist/cli/commands/run.js.map +1 -1
  24. package/dist/cli/commands/setup.js +989 -72
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/cli/commands/usage.js +192 -0
  27. package/dist/cli/commands/usage.js.map +1 -0
  28. package/dist/cli/commands/wait.js +3 -2
  29. package/dist/cli/commands/wait.js.map +1 -1
  30. package/dist/cli/commands/web.js +224 -0
  31. package/dist/cli/commands/web.js.map +1 -0
  32. package/dist/cli/index.js +36 -21
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/picker-state.js +75 -8
  35. package/dist/cli/picker-state.js.map +1 -1
  36. package/dist/cli/picker.js +13 -3
  37. package/dist/cli/picker.js.map +1 -1
  38. package/dist/cli/ui.js +54 -4
  39. package/dist/cli/ui.js.map +1 -1
  40. package/dist/cli/usage/dashboard.js +330 -0
  41. package/dist/cli/usage/dashboard.js.map +1 -0
  42. package/dist/cli/usage/snapshot.js +112 -0
  43. package/dist/cli/usage/snapshot.js.map +1 -0
  44. package/dist/config/config.js +326 -5
  45. package/dist/config/config.js.map +1 -1
  46. package/dist/config/orchestrator-mode.js +72 -0
  47. package/dist/config/orchestrator-mode.js.map +1 -0
  48. package/dist/config/paths.js +28 -9
  49. package/dist/config/paths.js.map +1 -1
  50. package/dist/core/autocompact.js +77 -0
  51. package/dist/core/autocompact.js.map +1 -0
  52. package/dist/core/driver.js.map +1 -1
  53. package/dist/core/models.js +166 -3
  54. package/dist/core/models.js.map +1 -1
  55. package/dist/core/pricing.js +83 -0
  56. package/dist/core/pricing.js.map +1 -0
  57. package/dist/core/roles.js +14 -3
  58. package/dist/core/roles.js.map +1 -1
  59. package/dist/core/run-usage.js +46 -0
  60. package/dist/core/run-usage.js.map +1 -0
  61. package/dist/core/session.js +18 -0
  62. package/dist/core/session.js.map +1 -1
  63. package/dist/daemon/daemon.js +286 -8
  64. package/dist/daemon/daemon.js.map +1 -1
  65. package/dist/daemon/ipc.js +10 -3
  66. package/dist/daemon/ipc.js.map +1 -1
  67. package/dist/drivers/antigravity/driver.js +158 -0
  68. package/dist/drivers/antigravity/driver.js.map +1 -0
  69. package/dist/drivers/antigravity/parser.js +168 -0
  70. package/dist/drivers/antigravity/parser.js.map +1 -0
  71. package/dist/drivers/claude/driver.js +13 -3
  72. package/dist/drivers/claude/driver.js.map +1 -1
  73. package/dist/drivers/opencode/driver.js +1 -114
  74. package/dist/drivers/opencode/driver.js.map +1 -1
  75. package/dist/drivers/opencode/parser.js +86 -0
  76. package/dist/drivers/opencode/parser.js.map +1 -0
  77. package/dist/drivers/registry.js +2 -0
  78. package/dist/drivers/registry.js.map +1 -1
  79. package/dist/drivers/session-driver.js +6 -4
  80. package/dist/drivers/session-driver.js.map +1 -1
  81. package/dist/drivers/session-runtime.js +1 -0
  82. package/dist/drivers/session-runtime.js.map +1 -1
  83. package/dist/git/worktree.js +1 -1
  84. package/dist/git/worktree.js.map +1 -1
  85. package/dist/open/contract.js +116 -0
  86. package/dist/open/contract.js.map +1 -0
  87. package/dist/open/injection.js +60 -0
  88. package/dist/open/injection.js.map +1 -0
  89. package/dist/open/launchers/claude.js +215 -0
  90. package/dist/open/launchers/claude.js.map +1 -0
  91. package/dist/open/launchers/opencode.js +213 -0
  92. package/dist/open/launchers/opencode.js.map +1 -0
  93. package/dist/open/orchestrator-prose.js +12 -0
  94. package/dist/open/orchestrator-prose.js.map +1 -0
  95. package/dist/open/pty.js +281 -0
  96. package/dist/open/pty.js.map +1 -0
  97. package/dist/open/runtime.js +632 -0
  98. package/dist/open/runtime.js.map +1 -0
  99. package/dist/plugin/agents/auditor.md +7 -1
  100. package/dist/plugin/agents/general.md +12 -1
  101. package/dist/plugin/agents/orchestrator-edit.md +67 -0
  102. package/dist/plugin/agents/orchestrator-read.md +67 -0
  103. package/dist/plugin/agents/orchestrator.md +74 -17
  104. package/dist/plugin/agents/reviewer.md +3 -0
  105. package/dist/plugin/commands/autonomous.md +58 -0
  106. package/dist/plugin/hooks/hooks.json +24 -0
  107. package/dist/plugin/hooks/session-id.sh +23 -0
  108. package/dist/plugin/hooks/session-name-lib.mjs +40 -0
  109. package/dist/plugin/hooks/session-name-worker.mjs +124 -0
  110. package/dist/plugin/hooks/session-name.mjs +66 -0
  111. package/dist/plugin/hooks/session-name.sh +7 -0
  112. package/dist/plugin/pty-shim.mjs +114 -0
  113. package/dist/plugin/statusline.sh +214 -22
  114. package/dist/plugin/themes/codedeck-rage.json +74 -0
  115. package/dist/plugin/themes/codedeck-ultra.json +52 -44
  116. package/dist/store/claims.js +152 -0
  117. package/dist/store/claims.js.map +1 -0
  118. package/dist/store/database.js +50 -15
  119. package/dist/store/database.js.map +1 -1
  120. package/dist/store/sessions.js +234 -3
  121. package/dist/store/sessions.js.map +1 -1
  122. package/dist/web/canvas-page.js +921 -0
  123. package/dist/web/canvas-page.js.map +1 -0
  124. package/package.json +3 -2
  125. package/dist/plugin/settings.json +0 -7
@@ -0,0 +1,921 @@
1
+ /** Realtime runs canvas served by `codedeck web`.
2
+ *
3
+ * Single self-contained page (no external requests): the script polls
4
+ * `api/sessions` and opens one `EventSource` per active session.
5
+ * Promoted from spikes/web-top.mjs; keep the spike behavior in sync
6
+ * only by re-running the Playwright check, never by importing it.
7
+ */
8
+ export const CANVAS_PAGE = `<!doctype html>
9
+ <html lang="pt-BR">
10
+ <head>
11
+ <meta charset="utf-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1">
13
+ <title>CodeDeck canvas realtime</title>
14
+ <link rel="icon" href="data:,">
15
+ <style>
16
+ body { font-family: system-ui, sans-serif; margin: 0; background: #08090c; color: #f5f5f7; overflow: hidden; }
17
+ #scene { position: fixed; inset: 0; display: block; }
18
+ #nodes { position: fixed; inset: 0; overflow: hidden; pointer-events: none; }
19
+ .hud { position: fixed; z-index: 10; }
20
+ #title, #feed, #legend, #hint { background: transparent; border: none; text-shadow: 0 1px 10px rgba(0,0,0,.8); }
21
+ /* Sem reacao a mouse: sao texto flutuante, e os cliques precisam varar
22
+ ate o canvas (pills de borda, pan e nos embaixo deles). */
23
+ #title, #feed, #legend, #hint { pointer-events: none; }
24
+ #title { top: 16px; left: 16px; }
25
+ #title h1 { font-size: 15px; margin: 0 0 2px; }
26
+ #title h1 .live { display: inline-block; width: 8px; height: 8px; border-radius: 99px; background: #30d158; margin-right: 8px; animation: breathe 2s ease-in-out infinite; }
27
+ @keyframes breathe { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
28
+ #title p { margin: 0; color: #98989f; font-size: 12.5px; }
29
+ #title p b { color: #f5f5f7; }
30
+ #tools { top: 16px; right: 16px; display: flex; gap: 8px; }
31
+ #tools button { width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center;
32
+ border: 1px solid rgba(255,255,255,.12); background: rgba(19,22,28,.6); color: #98989f;
33
+ border-radius: 99px; cursor: pointer; font-family: inherit; }
34
+ #tools button:hover { color: #f5f5f7; border-color: rgba(255,255,255,.25); }
35
+ #tools button.on { color: #f5f5f7; border-color: rgba(255,255,255,.25); }
36
+ #tools button svg { width: 17px; height: 17px; }
37
+ #legend { left: 16px; bottom: 16px; display: flex; gap: 14px; font-size: 12px; color: #98989f; }
38
+ #legend span { display: inline-flex; align-items: center; gap: 6px; }
39
+ .dot { width: 8px; height: 8px; border-radius: 99px; }
40
+ .dot.working { background: #30d158; box-shadow: 0 0 8px rgba(48,209,88,.8); }
41
+ .dot.waiting { background: #ff9f0a; box-shadow: 0 0 8px rgba(255,159,10,.8); }
42
+ .dot.resting { background: #636366; }
43
+ .dot.done { background: #3a3a3c; }
44
+ .dot.failed { background: #ff453a; box-shadow: 0 0 8px rgba(255,69,58,.8); }
45
+ #feed { left: 16px; top: 112px; width: 300px; }
46
+ #feed h2 { font-size: 11px; letter-spacing: .06em; color: #98989f; margin: 0 0 8px; }
47
+ #feed ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
48
+ #feed li { font-size: 12.5px; color: #98989f; display: flex; gap: 8px; animation: feedIn .3s ease-out; }
49
+ #feed li b { color: #f5f5f7; white-space: nowrap; }
50
+ #feed li.fresh { color: #f5f5f7; }
51
+ @keyframes feedIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; } }
52
+ #hint { left: 50%; transform: translateX(-50%); bottom: 16px; font-size: 12px; color: #98989f; white-space: nowrap; }
53
+ #detail { position: fixed; top: 0; right: 0; bottom: 0; width: min(360px, 92vw); z-index: 20;
54
+ background: rgba(10, 12, 16, 0.92); border-left: 1px solid rgba(255,255,255,0.1);
55
+ backdrop-filter: blur(14px); padding: 18px; overflow-y: auto; display: none; }
56
+ #detail.open { display: block; }
57
+ #detail h2 { font-size: 15px; margin: 0 0 2px; display: flex; align-items: center; gap: 8px; }
58
+ #detail h2 .logo { width: 22px; height: 22px; display: inline-flex; }
59
+ #detail h2 .logo svg { width: 22px; height: 22px; fill: #f5f5f7; }
60
+ #detail .dsub { font-size: 12.5px; color: #98989f; margin-bottom: 12px; }
61
+ #detail dl { margin: 0 0 12px; display: grid; grid-template-columns: auto 1fr; gap: 5px 12px; font-size: 12.5px; }
62
+ #detail dt { color: #98989f; }
63
+ #detail dd { margin: 0; word-break: break-word; }
64
+ #detail h3 { font-size: 11px; letter-spacing: 0.06em; color: #98989f; margin: 14px 0 8px; }
65
+ #detail ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; font-size: 12.5px; color: #c7c7cc; }
66
+ #detail ul li b { color: #f5f5f7; font-weight: 600; }
67
+ #detail .ts { color: #636366; font-size: 11.5px; }
68
+ #detailClose { position: absolute; top: 12px; right: 12px; width: 30px; height: 30px; border-radius: 99px;
69
+ border: 1px solid rgba(255,255,255,0.12); background: transparent; color: #98989f;
70
+ cursor: pointer; font-size: 14px; line-height: 1; font-family: inherit; }
71
+ #detailClose:hover { color: #f5f5f7; border-color: rgba(255,255,255,0.25); }
72
+ .node { position: absolute; width: 196px; background: #13161c; border: 1px solid rgba(255,255,255,.09);
73
+ border-radius: 14px; padding: 11px 13px; pointer-events: auto; cursor: grab; user-select: none;
74
+ box-shadow: 0 8px 28px rgba(0,0,0,.45); }
75
+ .node:active { cursor: grabbing; }
76
+ .node { transform-origin: 0 0; }
77
+ .node.orch { width: 228px; border-color: rgba(255,255,255,.18); }
78
+ .node .row { display: flex; align-items: center; gap: 8px; }
79
+ .node .logo { width: 22px; height: 22px; display: inline-flex; flex-shrink: 0; }
80
+ .node .logo svg { width: 22px; height: 22px; fill: #f5f5f7; }
81
+ .node.orch .logo { width: 26px; height: 26px; }
82
+ .node.orch .logo svg { width: 26px; height: 26px; }
83
+ .node .name { font-size: 13.5px; font-weight: 650; }
84
+ .node .sub { font-size: 12px; color: #98989f; margin-top: 3px; }
85
+ .node .act { font-size: 12px; color: #98989f; margin-top: 5px; min-height: 15px; }
86
+ .node .act b { color: #f5f5f7; }
87
+ .node.alert { border-color: rgba(255,159,10,.65); animation: alertPulse 1.6s ease-in-out infinite; }
88
+ @keyframes alertPulse { 0%,100% { border-color: rgba(255,159,10,.65); } 50% { border-color: rgba(255,159,10,1); } }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <canvas id="scene"></canvas>
93
+ <div id="nodes"></div>
94
+ <div class="hud" id="title"><h1><span class="live"></span>Canvas das runs</h1><p id="summary">conectando...</p></div>
95
+ <div class="hud" id="tools">
96
+ <button id="btnMotion" class="on" type="button" title="Pausar movimento"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h3l2.5-6 3.5 12 3-9 2 3H22"/></svg></button>
97
+ <button id="btnReset" type="button" title="Recentralizar" aria-label="Recentralizar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="7"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg></button>
98
+ <button id="btnHide" type="button" title="Ocultar concluídas" aria-label="Ocultar concluídas"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l18 18"/><path d="M10.6 5.1A9.8 9.8 0 0 1 12 5c7 0 10 7 10 7a17 17 0 0 1-2.9 3.6M6.6 6.6C3.6 8.2 2 12 2 12s3 7 10 7c1.5 0 2.8-.3 4-.8"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg></button>
99
+ </div>
100
+ <div class="hud" id="legend"><span><i class="dot working"></i>Trabalhando</span><span><i class="dot waiting"></i>Esperando você</span><span><i class="dot resting"></i>Em pausa</span><span><i class="dot done"></i>Concluída</span></div>
101
+ <div class="hud" id="feed"><h2>AGORA MESMO</h2><ul id="feedList"></ul></div>
102
+ <div class="hud" id="hint">arraste para organizar · scroll dá zoom · clique num agente para o detalhe</div>
103
+ <aside id="detail" aria-label="Detalhe da sessão">
104
+ <button id="detailClose" type="button" aria-label="Fechar">×</button>
105
+ <h2 id="dTitle"></h2>
106
+ <p class="dsub" id="dSub"></p>
107
+ <dl id="dRows"></dl>
108
+ <h3>HISTÓRICO</h3>
109
+ <ul id="dHist"></ul>
110
+ </aside>
111
+ <script>
112
+ "use strict";
113
+ var LOGO_ANTHROPIC = '<svg viewBox="0 0 24 24"><path d="M17.304 3.541h-3.672l6.696 16.918H24Zm-10.608 0L0 20.459h3.744l1.369-3.553h7.005l1.37 3.553h3.744L10.536 3.541Zm-.371 10.223 2.291-5.945 2.292 5.945Z"/></svg>';
114
+ var LOGO_OPENAI = '<svg viewBox="0 0 24 24"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>';
115
+ var LOGO_TERM = '<svg viewBox="0 0 24 24" fill="none" stroke="#f5f5f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="4" width="19" height="16" rx="4"/><path d="M7 9.5l3 3-3 3M12.5 15.5H17"/></svg>';
116
+ var LOGO_ORCH = '<svg viewBox="0 0 24 24" fill="none" stroke="#0a84ff" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="5" r="2.4" fill="#0a84ff" stroke="none"/><circle cx="5" cy="19" r="2.4" fill="#0a84ff" stroke="none"/><circle cx="19" cy="19" r="2.4" fill="#0a84ff" stroke="none"/><path d="M12 7.5 5.8 16.7M12 7.5l6.2 9.2M7.4 19h9.2"/></svg>';
117
+ function logoFor(agent) {
118
+ if (agent === "claude") return LOGO_ANTHROPIC;
119
+ if (agent === "codex") return LOGO_OPENAI;
120
+ if (agent === "opencode") return LOGO_ORCH;
121
+ return LOGO_TERM;
122
+ }
123
+ var AGENT_NAME = { claude: "Claude", codex: "Codex", opencode: "OpenCode", omp: "OMP", antigravity: "Antigravity" };
124
+ function agentName(a) { return AGENT_NAME[a] || a || "?"; }
125
+ var STATUS_HUMAN = { working: "Trabalhando agora", starting: "Trabalhando agora", needs_input: "Esperando você",
126
+ idle: "Em pausa", completed: "Concluída", failed: "Falhou", stopped: "Parada",
127
+ orphaned: "Interrompida", interrupted: "Interrompida", dead: "Morreu" };
128
+ var STATUS_DOT = { working: "working", starting: "working", needs_input: "waiting", idle: "resting",
129
+ completed: "done", failed: "failed", stopped: "done", orphaned: "waiting", interrupted: "waiting", dead: "done" };
130
+ var STATUS_COLOR = { working: "#30d158", starting: "#30d158", needs_input: "#ff9f0a", idle: "#636366",
131
+ completed: "#3a3a3c", failed: "#ff453a", stopped: "#3a3a3c", orphaned: "#ff9f0a", interrupted: "#ff9f0a",
132
+ dead: "#3a3a3c" };
133
+ function lastEventHuman(last) {
134
+ if (!last) return "";
135
+ if (last === "exit 0") return "terminou bem";
136
+ if (last === "agent_end") return "terminou a resposta";
137
+ if (last.indexOf("tool: ") === 0) {
138
+ var tool = last.slice(6);
139
+ if (tool === "bash") return "rodando comandos no terminal";
140
+ return "usando " + tool;
141
+ }
142
+ if (last.indexOf("send: ") === 0) return "recebeu sua mensagem";
143
+ return "";
144
+ }
145
+ function toolHuman(name) {
146
+ if (!name) return "uma tarefa";
147
+ if (name === "bash" || name === "Bash") return "terminal";
148
+ return name;
149
+ }
150
+ /* Pedaco do input real da ferramenta: primeira string interessante, cortada.
151
+ "terminal" sozinho nao diz nada; "podman ps" diz. */
152
+ function inputSnippet(input) {
153
+ if (!input) return "";
154
+ if (typeof input === "string") return clip(input);
155
+ if (typeof input !== "object") return "";
156
+ var keys = ["command", "cmd", "file", "path", "query", "pattern", "message", "text", "url"];
157
+ for (var i = 0; i < keys.length; i++) {
158
+ var v = input[keys[i]];
159
+ if (typeof v === "string" && v.trim() !== "") return clip(v);
160
+ }
161
+ return "";
162
+ }
163
+ function clip(s) {
164
+ var one = String(s).replace(/\s+/g, " ").trim();
165
+ return one.length > 42 ? one.slice(0, 42) + "…" : one;
166
+ }
167
+ function toolLabel(tool) {
168
+ var name = (tool && tool.name) || "";
169
+ var snip = inputSnippet(tool && tool.input);
170
+ if (!name) return snip || "uma tarefa";
171
+ if (name === "bash" || name === "Bash") return snip ? '"' + snip + '"' : "terminal";
172
+ return snip ? name + " " + snip : name;
173
+ }
174
+ function timeAgo(iso) {
175
+ var t = new Date(iso).getTime();
176
+ if (!t) return "";
177
+ var m = Math.max(0, Math.round((Date.now() - t) / 60000));
178
+ if (m < 1) return "agora mesmo";
179
+ if (m < 60) return "há " + m + " min";
180
+ var h = Math.floor(m / 60);
181
+ if (h < 24) return "há " + h + " h";
182
+ return "há " + Math.floor(h / 24) + " dias";
183
+ }
184
+ function ringSlot(wi) {
185
+ var j = 0, start = 0;
186
+ while (true) {
187
+ var R = 300 + j * 230;
188
+ var k = Math.max(6, Math.floor(2 * Math.PI * R / 250));
189
+ if (wi < start + k) {
190
+ return { a: ((wi - start) + j * 0.5) / k * Math.PI * 2 - Math.PI / 2, r: R };
191
+ }
192
+ start += k;
193
+ j++;
194
+ }
195
+ }
196
+ function hexA(hex, a) {
197
+ var r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
198
+ return "rgba(" + r + "," + g + "," + b + "," + Math.max(0, Math.min(1, a)).toFixed(3) + ")";
199
+ }
200
+
201
+ var nodesById = {};
202
+ var nodes = [];
203
+ var canvas = document.getElementById("scene");
204
+ var ctx = canvas.getContext("2d");
205
+ var cam = { x: 700, y: 330, zoom: 1 };
206
+ var motion = true;
207
+ function resize() {
208
+ var dpr = window.devicePixelRatio || 1;
209
+ canvas.width = window.innerWidth * dpr;
210
+ canvas.height = window.innerHeight * dpr;
211
+ canvas.style.width = window.innerWidth + "px";
212
+ canvas.style.height = window.innerHeight + "px";
213
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
214
+ }
215
+ window.addEventListener("resize", resize);
216
+ resize();
217
+ function w2s(x, y) { return [(x - cam.x) * cam.zoom + window.innerWidth / 2, (y - cam.y) * cam.zoom + window.innerHeight / 2]; }
218
+ function edgeCurve(a, b) {
219
+ var mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
220
+ var dx = b.x - a.x, dy = b.y - a.y;
221
+ var len = Math.sqrt(dx * dx + dy * dy) || 1;
222
+ var bow = Math.min(60, len * 0.15);
223
+ return { ax: a.x, ay: a.y, bx: b.x, by: b.y, cx: mx - dy / len * bow, cy: my + dx / len * bow };
224
+ }
225
+ function qpoint(e, t) {
226
+ var u = 1 - t;
227
+ return [u * u * e.ax + 2 * u * t * e.cx + t * t * e.bx, u * u * e.ay + 2 * u * t * e.cy + t * t * e.by];
228
+ }
229
+ var particles = [];
230
+ var rings = [];
231
+ function spawnFlow(from, color, n) {
232
+ if (!from || from.runId === "solo") return;
233
+ var to = null;
234
+ for (var i = 0; i < nodes.length; i++) {
235
+ if (nodes[i].runId === from.runId && nodes[i].isOrch) { to = nodes[i]; break; }
236
+ }
237
+ if (!to) return;
238
+ for (var k = 0; k < (n || 2); k++) {
239
+ particles.push({ from: from, to: to, t: -k * 0.12, speed: 0.008 + Math.random() * 0.008,
240
+ color: color || STATUS_COLOR[from.status] || "#30d158", size: 2 + Math.random() * 1.5 });
241
+ }
242
+ }
243
+ function spawnRing(n, color) {
244
+ if (n) rings.push({ x: n.x, y: n.y, r: 20, maxR: 120, alpha: 0.5, color: color });
245
+ }
246
+
247
+ var nodesLayer = document.getElementById("nodes");
248
+ var feedList = document.getElementById("feedList");
249
+ function makeNodeEl(n) {
250
+ var el = document.createElement("div");
251
+ el.className = "node" + (n.isOrch ? " orch" : "");
252
+ var row = document.createElement("div");
253
+ row.className = "row";
254
+ var logo = document.createElement("span");
255
+ logo.className = "logo";
256
+ logo.innerHTML = logoFor(n.agent);
257
+ row.appendChild(logo);
258
+ var dot = document.createElement("i");
259
+ row.appendChild(dot);
260
+ var name = document.createElement("span");
261
+ name.className = "name";
262
+ row.appendChild(name);
263
+ el.appendChild(row);
264
+ var sub = document.createElement("div");
265
+ sub.className = "sub";
266
+ el.appendChild(sub);
267
+ var act = document.createElement("div");
268
+ act.className = "act";
269
+ el.appendChild(act);
270
+ n.el = el; n.dotEl = dot; n.nameEl = name; n.subEl = sub; n.actEl = act;
271
+ el.addEventListener("mousedown", function (ev) { startDragNode(ev, n); });
272
+ nodesLayer.appendChild(el);
273
+ paintNode(n);
274
+ }
275
+ function paintNode(n) {
276
+ n.dotEl.className = "dot " + (STATUS_DOT[n.status] || "done");
277
+ n.nameEl.textContent = n.isOrch ? "Orquestrador" : agentName(n.agent);
278
+ if (n.isOrch) {
279
+ n.subEl.style.display = "";
280
+ n.subEl.textContent = n.count + (n.count === 1 ? " sessão" : " sessões");
281
+ } else {
282
+ n.subEl.style.display = "none";
283
+ n.subEl.textContent = "";
284
+ }
285
+ n.actEl.innerHTML = "";
286
+ var b = document.createElement("b");
287
+ b.textContent = STATUS_HUMAN[n.status] || "Encerrada";
288
+ n.actEl.appendChild(b);
289
+ n.actEl.appendChild(document.createTextNode(" · " + n.activity));
290
+ n.el.classList.toggle("orch", !!n.isOrch);
291
+ n.el.classList.toggle("alert", n.status === "needs_input");
292
+ n.el.title = "sessão " + n.id + (n.label ? " · " + n.label : "");
293
+ }
294
+ function feed(agent, text) {
295
+ var li = document.createElement("li");
296
+ li.className = "fresh";
297
+ var b = document.createElement("b");
298
+ b.textContent = agent;
299
+ li.appendChild(b);
300
+ li.appendChild(document.createTextNode(text));
301
+ feedList.insertBefore(li, feedList.firstChild);
302
+ while (feedList.children.length > 6) feedList.removeChild(feedList.lastChild);
303
+ var items = feedList.children;
304
+ for (var i = 0; i < items.length; i++) items[i].classList.toggle("fresh", i === 0);
305
+ }
306
+ function paintSummary() {
307
+ var working = 0, waiting = 0, hidden = 0;
308
+ nodes.forEach(function (n) {
309
+ if (n.status === "working" || n.status === "starting") working++;
310
+ if (n.status === "needs_input") waiting++;
311
+ if (hideDone && isDone(n.status)) hidden++;
312
+ });
313
+ var el = document.getElementById("summary");
314
+ el.innerHTML = "";
315
+ el.appendChild(document.createTextNode(nodes.length + " sessões · "));
316
+ var b1 = document.createElement("b"); b1.textContent = working + " trabalhando"; el.appendChild(b1);
317
+ el.appendChild(document.createTextNode(" · "));
318
+ var b2 = document.createElement("b"); b2.textContent = waiting + " esperando você"; el.appendChild(b2);
319
+ if (hidden > 0) el.appendChild(document.createTextNode(" · " + hidden + " ocultas"));
320
+ }
321
+
322
+ /* Reconcilia o poll com os nos: preserva posicao arrastada, atualiza status. */
323
+ var streams = {};
324
+ function baseActivity(s) {
325
+ if (s.status === "needs_input") return "quer sua aprovação para continuar";
326
+ var last = lastEventHuman(s.lastEvent);
327
+ if (s.status === "failed") return last ? "falhou quando estava " + last : "falhou, abra os logs para ver";
328
+ if (s.status === "dead") return last ? "morreu quando estava " + last : "morreu, o processo sumiu";
329
+ if (s.status === "completed") return last ? "concluída, " + last : "concluída";
330
+ if (s.status === "working" || s.status === "starting") return last || "processando...";
331
+ if (last) return "por último: " + last;
332
+ return "sem atividade recente";
333
+ }
334
+ var lastSessions = [];
335
+ function visibleNodes() {
336
+ return nodes.filter(function (n) { return !(hideDone && isDone(n.status)); });
337
+ }
338
+ /* Raio de conteudo de um grupo a partir dos nos visiveis: grupo de um no
339
+ so vira um ponto compacto em vez de pagar o diametro do anel cheio. */
340
+ function groupRadius(list) {
341
+ var workers = 0;
342
+ for (var i = 0; i < list.length; i++) if (!list[i].isOrch) workers++;
343
+ if (workers === 0) return 130;
344
+ return ringSlot(workers - 1).r + 130;
345
+ }
346
+ /* Layout deterministico sobre o conjunto VISIVEL: esconder concluidas
347
+ reempacota o resto lado a lado em vez de deixar buracos no mundo.
348
+ O slot (wi) nasce na criacao em ordem de chegada, entao polls e
349
+ chegadas novas nao embaralham ninguem; so o arraste fixa (pinned). */
350
+ function layoutVisible() {
351
+ var groups = {};
352
+ visibleNodes().forEach(function (n) {
353
+ if (!groups[n.runId]) groups[n.runId] = [];
354
+ groups[n.runId].push(n);
355
+ });
356
+ var gkeys = Object.keys(groups).sort();
357
+ var radii = gkeys.map(function (k) { return groupRadius(groups[k]); });
358
+ var total = 0;
359
+ for (var i = 0; i < gkeys.length - 1; i++) total += radii[i] + radii[i + 1] + 260;
360
+ var cx = -total / 2;
361
+ gkeys.forEach(function (key, gi) {
362
+ var list = groups[key];
363
+ list.forEach(function (n) {
364
+ // Aneis concentricos com lotacao pela circunferencia: cada no tem
365
+ // ~235px de espaco no anel, entao N workers nunca se encostam.
366
+ // (Filotaxia empacota demais para cards de 196px.)
367
+ if (n.isOrch) { n.hx = cx; n.hy = 0; }
368
+ else {
369
+ var slot = ringSlot(Math.max(0, n.wi));
370
+ n.hx = cx + Math.cos(slot.a) * slot.r;
371
+ n.hy = Math.sin(slot.a) * slot.r;
372
+ }
373
+ if (!n.pinned) { n.x = n.hx; n.y = n.hy; }
374
+ });
375
+ if (gi < gkeys.length - 1) cx += radii[gi] + radii[gi + 1] + 260;
376
+ });
377
+ }
378
+ function reconcile(sessions) {
379
+ lastSessions = sessions;
380
+ var totals = {};
381
+ sessions.forEach(function (s) {
382
+ var key = s.runId || ("solo:" + s.id);
383
+ totals[key] = (totals[key] || 0) + 1;
384
+ });
385
+ var seen = {};
386
+ sessions.forEach(function (s) {
387
+ var key = s.runId || ("solo:" + s.id);
388
+ var isOrch = s.id === s.runId;
389
+ var n = nodesById[s.id];
390
+ if (!n) {
391
+ var wi = -1;
392
+ if (!isOrch) {
393
+ wi = 0;
394
+ for (var k = 0; k < nodes.length; k++) {
395
+ if (nodes[k].runId === key && !nodes[k].isOrch) wi = Math.max(wi, nodes[k].wi + 1);
396
+ }
397
+ }
398
+ n = { id: s.id, runId: key, isOrch: isOrch, agent: s.agent,
399
+ x: 0, y: 0, wi: wi, pinned: false, liveUntil: 0 };
400
+ nodesById[s.id] = n;
401
+ nodes.push(n);
402
+ makeNodeEl(n);
403
+ }
404
+ n.status = s.status;
405
+ n.agent = s.agent;
406
+ n.isOrch = isOrch;
407
+ n.repo = s.repository ? String(s.repository).split("/").pop() : "";
408
+ n.count = totals[key];
409
+ n.label = s.name || "";
410
+ if (Date.now() > n.liveUntil) n.activity = baseActivity(s);
411
+ paintNode(n);
412
+ seen[s.id] = true;
413
+ });
414
+ for (var id in nodesById) {
415
+ if (!seen[id]) {
416
+ var n = nodesById[id];
417
+ if (n.es) { try { n.es.close(); } catch (e) {} }
418
+ if (n.el && n.el.parentNode) n.el.parentNode.removeChild(n.el);
419
+ delete nodesById[id];
420
+ nodes.splice(nodes.indexOf(n), 1);
421
+ }
422
+ }
423
+ layoutVisible();
424
+ ensureStreams();
425
+ paintSummary();
426
+ if (!window.__fitted && nodes.length) {
427
+ window.__fitted = true;
428
+ // Enquadra o run mais ativo, nao a frota inteira: 100 nos fixos nunca
429
+ // cabem legiveis numa viewport. O resto fica ao redor para o pan.
430
+ fitCamera(bestRunFocus());
431
+ }
432
+ }
433
+ function bestRunFocus() {
434
+ // So o conjunto visivel participa: com hide ligado, no oculto nao
435
+ // puxa a camera nem entra no enquadramento.
436
+ var vis = visibleNodes();
437
+ // Pouca gente visivel: enquadra todo mundo, nao so o run campeao.
438
+ // (Com 5 nos, focar so o melhor corta o vizinho na borda.)
439
+ if (vis.length <= 8) return vis;
440
+ var activeByRun = {};
441
+ vis.forEach(function (n) {
442
+ if (isActive(n.status)) activeByRun[n.runId] = (activeByRun[n.runId] || 0) + 1;
443
+ });
444
+ var bestRun = null, bestCount = 0;
445
+ for (var rk in activeByRun) {
446
+ if (activeByRun[rk] > bestCount) { bestCount = activeByRun[rk]; bestRun = rk; }
447
+ }
448
+ return bestRun ? vis.filter(function (n) { return n.runId === bestRun; }) : vis;
449
+ }
450
+ /* Pills de borda para sessoes ativas fora da viewport: o resumo conta a
451
+ frota inteira, entao quem trabalha longe aparece aqui com um atalho. */
452
+ var edgeMarkers = [];
453
+ function drawMarkers() {
454
+ edgeMarkers = [];
455
+ nodes.forEach(function (n) {
456
+ if (!isActive(n.status)) return;
457
+ var p = w2s(n.x, n.y);
458
+ var m = 80;
459
+ if (p[0] > -40 && p[0] < window.innerWidth + 40 && p[1] > -40 && p[1] < window.innerHeight + 40) return;
460
+ var cx = Math.min(window.innerWidth - m, Math.max(m, p[0]));
461
+ var cy = Math.min(window.innerHeight - m, Math.max(m, p[1]));
462
+ var label = agentName(n.agent);
463
+ ctx.font = "12px system-ui, sans-serif";
464
+ var w = ctx.measureText(label).width + 36;
465
+ var h = 26;
466
+ var x = cx - w / 2, y = cy - h / 2;
467
+ ctx.beginPath();
468
+ if (ctx.roundRect) ctx.roundRect(x, y, w, h, 13);
469
+ else ctx.rect(x, y, w, h);
470
+ ctx.fillStyle = "rgba(19,22,28,0.92)";
471
+ ctx.fill();
472
+ ctx.strokeStyle = "rgba(255,255,255,0.14)";
473
+ ctx.lineWidth = 1;
474
+ ctx.stroke();
475
+ ctx.beginPath();
476
+ ctx.arc(x + 15, cy, 4, 0, Math.PI * 2);
477
+ ctx.fillStyle = STATUS_COLOR[n.status] || "#30d158";
478
+ ctx.fill();
479
+ ctx.fillStyle = "#f5f5f7";
480
+ ctx.textBaseline = "middle";
481
+ ctx.fillText(label, x + 25, cy + 1);
482
+ edgeMarkers.push({ x: x, y: y, w: w, h: h, node: n });
483
+ });
484
+ }
485
+ function fitCamera(list) {
486
+ if (!list.length) return;
487
+ var xs = list.map(function (n) { return n.x; });
488
+ var ys = list.map(function (n) { return n.y; });
489
+ var bw = Math.max.apply(null, xs) - Math.min.apply(null, xs) + 700;
490
+ var bh = Math.max.apply(null, ys) - Math.min.apply(null, ys) + 500;
491
+ cam.x = (Math.min.apply(null, xs) + Math.max.apply(null, xs)) / 2;
492
+ cam.y = (Math.min.apply(null, ys) + Math.max.apply(null, ys)) / 2;
493
+ cam.zoom = Math.min(1, Math.max(0.5, Math.min(window.innerWidth / bw, window.innerHeight / bh)));
494
+ }
495
+ function isDone(s) { return s === "completed" || s === "failed" || s === "stopped" || s === "dead"; }
496
+ function isActive(s) { return s === "working" || s === "starting" || s === "needs_input"; }
497
+ function ensureStreams() {
498
+ var open = 0;
499
+ nodes.forEach(function (n) {
500
+ if (isActive(n.status) && !n.es && open < 12) {
501
+ open++;
502
+ var es = new EventSource("api/sessions/" + encodeURIComponent(n.id) + "/stream");
503
+ n.es = es;
504
+ es.onmessage = function (m) {
505
+ try { onEvent(n, JSON.parse(m.data)); } catch (e) {}
506
+ };
507
+ es.addEventListener("error", function () {
508
+ try { n.es.close(); } catch (e) {}
509
+ n.es = null;
510
+ });
511
+ } else if (!isActive(n.status) && n.es) {
512
+ try { n.es.close(); } catch (e) {}
513
+ n.es = null;
514
+ }
515
+ });
516
+ }
517
+ function touch(n, activity) {
518
+ n.activity = activity;
519
+ n.liveUntil = Date.now() + 8000;
520
+ paintNode(n);
521
+ }
522
+ function onEvent(n, ev) {
523
+ if (!ev || !ev.type) return;
524
+ if (ev.type === "text.delta") {
525
+ spawnFlow(n, "#30d158", 1);
526
+ touch(n, "escrevendo resposta");
527
+ } else if (ev.type === "tool.started") {
528
+ spawnRing(n, "#0a84ff");
529
+ spawnFlow(n, "#0a84ff", 1);
530
+ touch(n, toolLabel(ev.tool));
531
+ feed(agentName(n.agent), "começou: " + toolLabel(ev.tool));
532
+ } else if (ev.type === "tool.completed") {
533
+ var bad = ev.tool && ev.tool.success === false;
534
+ touch(n, toolLabel(ev.tool) + (bad ? " (com erro)" : " ✓"));
535
+ if (bad) feed(agentName(n.agent), "ferramenta falhou");
536
+ } else if (ev.type === "message") {
537
+ if (ev.role === "assistant") {
538
+ touch(n, "respondendo");
539
+ feed(agentName(n.agent), String(ev.content || "").slice(0, 90));
540
+ }
541
+ } else if (ev.type === "turn.completed") {
542
+ feed(agentName(n.agent), "etapa concluída");
543
+ } else if (ev.type === "file.changed") {
544
+ feed(agentName(n.agent), "mudou " + (ev.path || "um arquivo"));
545
+ } else if (ev.type === "permission.requested") {
546
+ n.status = "needs_input";
547
+ touch(n, "quer sua aprovação para continuar");
548
+ paintSummary();
549
+ spawnRing(n, "#ff9f0a");
550
+ feed(agentName(n.agent), "pedindo sua aprovação");
551
+ } else if (ev.type === "session.completed" || ev.type === "session.failed") {
552
+ n.status = ev.type === "session.failed" ? "failed" : "completed";
553
+ touch(n, ev.type === "session.failed" ? ("falhou: " + (ev.error || "")) : "concluída");
554
+ paintSummary();
555
+ feed(agentName(n.agent), ev.type === "session.failed" ? "falhou" : "concluída");
556
+ if (n.es) { try { n.es.close(); } catch (e) {} n.es = null; }
557
+ }
558
+ }
559
+
560
+ function drawGrid() {
561
+ var gap = 34 * cam.zoom;
562
+ if (gap < 14) return;
563
+ ctx.fillStyle = "rgba(255,255,255,0.055)";
564
+ var ox = (window.innerWidth / 2 - cam.x * cam.zoom) % gap;
565
+ var oy = (window.innerHeight / 2 - cam.y * cam.zoom) % gap;
566
+ if (ox < 0) ox += gap;
567
+ if (oy < 0) oy += gap;
568
+ for (var x = ox; x < window.innerWidth; x += gap)
569
+ for (var y = oy; y < window.innerHeight; y += gap) ctx.fillRect(x, y, 1.4, 1.4);
570
+ }
571
+ /* Largura externa do card (CSS content-box + padding 13px/lado + borda):
572
+ e o numero que centra o card no ponto e dimensiona o container. */
573
+ function nodeOuterW(n) { return (n.isOrch ? 228 : 196) + 28; }
574
+ /* Container por run: borda tracejada beeeem suave com o nome do repo,
575
+ desenhado atras dos cards. A bbox e recalculada a cada frame a partir
576
+ da posicao viva dos nos, entao acompanha arraste e polls. */
577
+ function drawGroupBoxes() {
578
+ var groups = {};
579
+ nodes.forEach(function (n) {
580
+ if (hideDone && isDone(n.status)) return;
581
+ if (!groups[n.runId]) groups[n.runId] = [];
582
+ groups[n.runId].push(n);
583
+ });
584
+ ctx.save();
585
+ ctx.setLineDash([5, 7]);
586
+ ctx.lineWidth = 1;
587
+ ctx.strokeStyle = "rgba(255,255,255,0.10)";
588
+ ctx.font = "11px system-ui, sans-serif";
589
+ ctx.textBaseline = "bottom";
590
+ Object.keys(groups).forEach(function (key) {
591
+ var list = groups[key];
592
+ var x0 = 1e12, y0 = 1e12, x1 = -1e12, y1 = -1e12, repo = "";
593
+ list.forEach(function (n) {
594
+ var hw = nodeOuterW(n) / 2 + 28, hh = 44 + 28;
595
+ if (n.x - hw < x0) x0 = n.x - hw;
596
+ if (n.y - hh < y0) y0 = n.y - hh;
597
+ if (n.x + hw > x1) x1 = n.x + hw;
598
+ if (n.y + hh > y1) y1 = n.y + hh;
599
+ if (n.isOrch && n.repo) repo = n.repo;
600
+ });
601
+ if (!repo) {
602
+ for (var i = 0; i < list.length; i++) {
603
+ if (list[i].repo) { repo = list[i].repo; break; }
604
+ }
605
+ }
606
+ var a = w2s(x0, y0), b = w2s(x1, y1);
607
+ ctx.beginPath();
608
+ if (ctx.roundRect) ctx.roundRect(a[0], a[1], b[0] - a[0], b[1] - a[1], 18);
609
+ else ctx.rect(a[0], a[1], b[0] - a[0], b[1] - a[1]);
610
+ ctx.stroke();
611
+ if (repo) {
612
+ ctx.fillStyle = "rgba(235,235,240,0.38)";
613
+ ctx.fillText(repo, a[0] + 14, a[1] - 6);
614
+ }
615
+ });
616
+ ctx.restore();
617
+ }
618
+ function frame() {
619
+ ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
620
+ drawGrid();
621
+ drawGroupBoxes();
622
+ nodes.forEach(function (n) {
623
+ if (n.isOrch || n.runId === "solo" || n.runId.indexOf("solo:") === 0) return;
624
+ if (hideDone && isDone(n.status)) return;
625
+ var o = null;
626
+ for (var i = 0; i < nodes.length; i++) {
627
+ if (nodes[i].runId === n.runId && nodes[i].isOrch) { o = nodes[i]; break; }
628
+ }
629
+ if (!o) return;
630
+ var e = edgeCurve(n, o);
631
+ var pa = w2s(e.ax, e.ay), pb = w2s(e.bx, e.by), pc = w2s(e.cx, e.cy);
632
+ ctx.beginPath();
633
+ ctx.moveTo(pa[0], pa[1]);
634
+ ctx.quadraticCurveTo(pc[0], pc[1], pb[0], pb[1]);
635
+ ctx.strokeStyle = n.status === "working" ? "rgba(255,255,255,0.20)" : "rgba(255,255,255,0.10)";
636
+ ctx.lineWidth = 1.2;
637
+ ctx.stroke();
638
+ });
639
+ for (var i = particles.length - 1; i >= 0; i--) {
640
+ var p = particles[i];
641
+ if (hideDone && (isDone(p.from.status) || isDone(p.to.status))) {
642
+ particles.splice(i, 1);
643
+ continue;
644
+ }
645
+ p.t += motion ? p.speed : 0;
646
+ if (p.t > 1.1) { particles.splice(i, 1); continue; }
647
+ if (p.t < 0) continue;
648
+ var q = qpoint(edgeCurve(p.from, p.to), Math.min(1, p.t));
649
+ var s = w2s(q[0], q[1]);
650
+ ctx.beginPath();
651
+ ctx.arc(s[0], s[1], p.size * cam.zoom, 0, Math.PI * 2);
652
+ ctx.fillStyle = p.color;
653
+ ctx.globalAlpha = 0.9;
654
+ ctx.fill();
655
+ ctx.globalAlpha = 1;
656
+ }
657
+ for (var j = rings.length - 1; j >= 0; j--) {
658
+ var r = rings[j];
659
+ r.r += motion ? 2.2 : 0;
660
+ r.alpha -= motion ? 0.011 : 0;
661
+ if (r.alpha <= 0 || r.r > r.maxR) { rings.splice(j, 1); continue; }
662
+ var c = w2s(r.x, r.y);
663
+ var rad = Math.max(1, r.r * cam.zoom);
664
+ var g = ctx.createRadialGradient(c[0], c[1], 0, c[0], c[1], rad);
665
+ g.addColorStop(0, hexA(r.color, r.alpha * 0.55));
666
+ g.addColorStop(0.55, hexA(r.color, r.alpha * 0.22));
667
+ g.addColorStop(1, hexA(r.color, 0));
668
+ ctx.beginPath();
669
+ ctx.arc(c[0], c[1], rad, 0, Math.PI * 2);
670
+ ctx.fillStyle = g;
671
+ ctx.fill();
672
+ }
673
+ drawMarkers();
674
+ nodes.forEach(function (n) {
675
+ var hidden = hideDone && isDone(n.status);
676
+ n.el.style.display = hidden ? "none" : "";
677
+ if (hidden) return;
678
+ var p = w2s(n.x, n.y);
679
+ // Largura EXTERNA do card: o CSS usa content-box com padding 13px de
680
+ // cada lado, entao o centro real fica 13px a direita do 196/228.
681
+ var w = nodeOuterW(n);
682
+ // O no escala junto com o zoom: sem isso o DOM fica gigante no mundo
683
+ // e o layout que e limpo em coordenadas vira pilha na tela.
684
+ n.el.style.transform = "translate(" + Math.round(p[0] - (w / 2) * cam.zoom) + "px," +
685
+ Math.round(p[1] - 44 * cam.zoom) + "px) scale(" + cam.zoom + ")";
686
+ });
687
+ requestAnimationFrame(frame);
688
+ }
689
+
690
+ var dragNode = null, panning = false, lastM = null;
691
+ function toWorld(cx, cy) {
692
+ return [(cx - window.innerWidth / 2) / cam.zoom + cam.x, (cy - window.innerHeight / 2) / cam.zoom + cam.y];
693
+ }
694
+ var dragMoved = false;
695
+ function startDragNode(ev, n) {
696
+ dragNode = n;
697
+ dragMoved = false;
698
+ lastM = [ev.clientX, ev.clientY];
699
+ ev.stopPropagation();
700
+ ev.preventDefault();
701
+ }
702
+ /* Hit-test recalculado na hora a partir dos nos: nao depende do array
703
+ desenhado no ultimo frame, entao nunca diverge dele. */
704
+ function markerAt(cx, cy) {
705
+ var found = null;
706
+ nodes.forEach(function (n) {
707
+ if (found || !isActive(n.status)) return;
708
+ var p = w2s(n.x, n.y);
709
+ var m = 80;
710
+ if (p[0] > -40 && p[0] < window.innerWidth + 40 && p[1] > -40 && p[1] < window.innerHeight + 40) return;
711
+ var px = Math.min(window.innerWidth - m, Math.max(m, p[0]));
712
+ var py = Math.min(window.innerHeight - m, Math.max(m, p[1]));
713
+ var label = agentName(n.agent);
714
+ ctx.font = "12px system-ui, sans-serif";
715
+ var w = ctx.measureText(label).width + 36;
716
+ var x = px - w / 2, y = py - 13;
717
+ if (cx >= x && cx <= x + w && cy >= y && cy <= y + 26) found = n;
718
+ });
719
+ return found;
720
+ }
721
+ canvas.addEventListener("mousedown", function (ev) {
722
+ var hit = markerAt(ev.clientX, ev.clientY);
723
+ if (hit) {
724
+ cam.x = hit.x;
725
+ cam.y = hit.y;
726
+ if (cam.zoom < 0.7) cam.zoom = 0.7;
727
+ return;
728
+ }
729
+ panning = true;
730
+ lastM = [ev.clientX, ev.clientY];
731
+ });
732
+ window.addEventListener("mousemove", function (ev) {
733
+ if (dragNode) {
734
+ if (Math.abs(ev.clientX - lastM[0]) + Math.abs(ev.clientY - lastM[1]) > 4) {
735
+ dragMoved = true;
736
+ // Posicao manual sobrevive aos polls: o layout so move no solto.
737
+ dragNode.pinned = true;
738
+ }
739
+ dragNode.x += (ev.clientX - lastM[0]) / cam.zoom;
740
+ dragNode.y += (ev.clientY - lastM[1]) / cam.zoom;
741
+ lastM = [ev.clientX, ev.clientY];
742
+ } else if (panning) {
743
+ cam.x -= (ev.clientX - lastM[0]) / cam.zoom;
744
+ cam.y -= (ev.clientY - lastM[1]) / cam.zoom;
745
+ lastM = [ev.clientX, ev.clientY];
746
+ }
747
+ });
748
+ window.addEventListener("mouseup", function () {
749
+ if (dragNode && !dragMoved) selectSession(dragNode.id);
750
+ dragNode = null;
751
+ panning = false;
752
+ });
753
+ window.addEventListener("keydown", function (ev) {
754
+ if (ev.key === "Escape") closeDetail();
755
+ });
756
+ window.addEventListener("wheel", function (ev) {
757
+ ev.preventDefault();
758
+ var before = toWorld(ev.clientX, ev.clientY);
759
+ cam.zoom = Math.min(2.2, Math.max(0.45, cam.zoom * (ev.deltaY < 0 ? 1.1 : 0.9)));
760
+ var after = toWorld(ev.clientX, ev.clientY);
761
+ cam.x += before[0] - after[0];
762
+ cam.y += before[1] - after[1];
763
+ }, { passive: false });
764
+ function syncUrl() {
765
+ try {
766
+ var qp = new URLSearchParams(window.location.search);
767
+ if (hideDone) qp.set("hide", "1"); else qp.delete("hide");
768
+ if (!motion) qp.set("motion", "0"); else qp.delete("motion");
769
+ var qs = qp.toString();
770
+ window.history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : ""));
771
+ } catch (e) {}
772
+ }
773
+ // Filtros sobrevivem ao F5: o estado mora na query string (?hide=1&motion=0).
774
+ var urlHide = false, urlMotion = true;
775
+ try {
776
+ var qp0 = new URLSearchParams(window.location.search);
777
+ urlHide = qp0.get("hide") === "1";
778
+ urlMotion = qp0.get("motion") !== "0";
779
+ } catch (e) {}
780
+ motion = urlMotion;
781
+ document.getElementById("btnMotion").onclick = function (ev) {
782
+ motion = !motion;
783
+ // currentTarget, nunca target: o clique cai no SVG interno e a classe
784
+ // precisa alternar no botao para o estado visual acompanhar.
785
+ ev.currentTarget.classList.toggle("on", motion);
786
+ syncUrl();
787
+ };
788
+ document.getElementById("btnReset").onclick = function () {
789
+ // Solta os arrastes manuais, reempacota e volta para a acao (mesmo
790
+ // enquadramento da abertura), nunca um zoom cego.
791
+ nodes.forEach(function (n) { n.pinned = false; });
792
+ layoutVisible();
793
+ fitCamera(bestRunFocus());
794
+ };
795
+ var hideDone = urlHide;
796
+ document.getElementById("btnHide").onclick = function (ev) {
797
+ hideDone = !hideDone;
798
+ ev.currentTarget.classList.toggle("on", hideDone);
799
+ ev.currentTarget.title = hideDone ? "Mostrar concluídas" : "Ocultar concluídas";
800
+ // Trocar o filtro reempacota o mundo e reenquadra: sem isso os nos
801
+ // visiveis ficam presos nos slots antigos, a um oceano de distancia.
802
+ layoutVisible();
803
+ fitCamera(bestRunFocus());
804
+ paintSummary();
805
+ syncUrl();
806
+ };
807
+ document.getElementById("btnMotion").classList.toggle("on", motion);
808
+ document.getElementById("btnHide").classList.toggle("on", hideDone);
809
+ document.getElementById("btnHide").title = hideDone ? "Mostrar concluídas" : "Ocultar concluídas";
810
+
811
+ function fmtTime(iso) {
812
+ try {
813
+ var d = new Date(iso);
814
+ return d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
815
+ } catch (e) { return ""; }
816
+ }
817
+ function fmtEvent(ev) {
818
+ if (!ev || !ev.type) return "";
819
+ if (ev.type === "text.delta") return null;
820
+ if (ev.type === "tool.started") return "Começou: " + toolLabel(ev.tool);
821
+ if (ev.type === "tool.completed") {
822
+ var bad = ev.tool && ev.tool.success === false;
823
+ return "Terminou: " + toolLabel(ev.tool) + (bad ? " (com erro)" : "");
824
+ }
825
+ if (ev.type === "message" && ev.role === "assistant") return "Respondeu: " + String(ev.content || "").slice(0, 120);
826
+ if (ev.type === "message") return null;
827
+ if (ev.type === "turn.started") return "Nova etapa";
828
+ if (ev.type === "turn.completed") return "Etapa concluída";
829
+ if (ev.type === "file.changed") return "Mudou " + (ev.path || "um arquivo");
830
+ if (ev.type === "permission.requested") return "Pediu aprovação: " + (ev.tool || "uma ação");
831
+ if (ev.type === "permission.resolved") return ev.approved ? "Aprovado" : "Negado";
832
+ if (ev.type === "session.completed") return "Sessão concluída";
833
+ if (ev.type === "session.failed") return "Sessão falhou: " + (ev.error || "");
834
+ if (ev.type === "session.started") return "Sessão iniciada";
835
+ return null;
836
+ }
837
+ function row(dt, dd) {
838
+ var d = document.createElement("dt");
839
+ d.textContent = dt;
840
+ var v = document.createElement("dd");
841
+ v.textContent = dd;
842
+ return [d, v];
843
+ }
844
+ function closeDetail() {
845
+ document.getElementById("detail").classList.remove("open");
846
+ }
847
+ document.getElementById("detailClose").onclick = closeDetail;
848
+ function selectSession(id) {
849
+ fetch("api/sessions/" + encodeURIComponent(id)).then(function (r) { return r.json(); }).then(function (j) {
850
+ var s = j.session || j;
851
+ if (!s || !s.id) return;
852
+ var title = document.getElementById("dTitle");
853
+ title.innerHTML = "";
854
+ var logo = document.createElement("span");
855
+ logo.className = "logo";
856
+ logo.innerHTML = logoFor(s.agent);
857
+ title.appendChild(logo);
858
+ title.appendChild(document.createTextNode(agentName(s.agent) + " · " + (s.name || s.id)));
859
+ document.getElementById("dSub").textContent = (STATUS_HUMAN[s.status] || s.status) +
860
+ (s.createdAt ? " · começou " + timeAgo(s.createdAt) : "");
861
+ var rows = document.getElementById("dRows");
862
+ rows.innerHTML = "";
863
+ var repo = s.repository ? String(s.repository).split("/").pop() : "";
864
+ var items = [
865
+ ["Modelo", s.model || "—"],
866
+ // effort so existe quando a sessao nasceu com --effort; ausente = padrao.
867
+ ["Esforço", s.effort || "padrão"],
868
+ ["Repo", repo || "—"],
869
+ ["Atividade", s.lastEvent ? (lastEventHuman(s.lastEvent) || s.lastEvent) : "—"],
870
+ ];
871
+ if (s.pid) items.push(["PID", String(s.pid)]);
872
+ if (s.updatedAt) items.push(["Atualizado", timeAgo(s.updatedAt)]);
873
+ var tools = 0, msgs = 0;
874
+ (j.events || []).forEach(function (ev) {
875
+ if (!ev) return;
876
+ if (ev.type === "tool.completed") tools++;
877
+ if (ev.type === "message" && ev.role === "assistant") msgs++;
878
+ });
879
+ items.push(["Ferramentas", String(tools)]);
880
+ items.push(["Respostas", String(msgs)]);
881
+ items.forEach(function (kv) {
882
+ row(kv[0], kv[1]).forEach(function (el) { rows.appendChild(el); });
883
+ });
884
+ var hist = document.getElementById("dHist");
885
+ hist.innerHTML = "";
886
+ var shown = 0;
887
+ for (var i = (j.events || []).length - 1; i >= 0 && shown < 30; i--) {
888
+ var line = fmtEvent(j.events[i]);
889
+ if (!line) continue;
890
+ var li = document.createElement("li");
891
+ var ts = document.createElement("span");
892
+ ts.className = "ts";
893
+ ts.textContent = fmtTime(j.events[i].timestamp) + " ";
894
+ li.appendChild(ts);
895
+ li.appendChild(document.createTextNode(line));
896
+ hist.appendChild(li);
897
+ shown++;
898
+ }
899
+ if (!shown) {
900
+ var empty = document.createElement("li");
901
+ empty.textContent = "Sem histórico ainda.";
902
+ hist.appendChild(empty);
903
+ }
904
+ document.getElementById("detail").classList.add("open");
905
+ }).catch(function () {});
906
+ }
907
+
908
+ function poll() {
909
+ fetch("api/sessions").then(function (r) { return r.json(); }).then(function (j) {
910
+ reconcile(j.sessions || []);
911
+ }).catch(function () {
912
+ document.getElementById("summary").textContent = "daemon fora do ar?";
913
+ });
914
+ }
915
+ poll();
916
+ setInterval(poll, 2000);
917
+ frame();
918
+ </script>
919
+ </body>
920
+ </html>`;
921
+ //# sourceMappingURL=canvas-page.js.map