codedeck 0.7.3 → 0.7.4

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.
@@ -1,1118 +0,0 @@
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: 330px; }
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: 6px; }
48
- #feed li { font-size: 12px; color: #98989f; display: flex; align-items: baseline; gap: 7px; animation: feedIn .3s ease-out; line-height: 1.35; }
49
- #feed li b { color: #f5f5f7; white-space: nowrap; font-weight: 600; font-size: 12px; }
50
- #feed li.fresh { color: #f5f5f7; }
51
- #feed li.warn { color: #ff9f0a; }
52
- #feed li.bad { color: #ff453a; }
53
- #feed .feed-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11.5px; }
54
- #feed .feed-count { font-size: 11px; color: #636366; font-weight: 600; margin-left: 2px; white-space: nowrap; flex-shrink: 0; font-family: system-ui, sans-serif; }
55
- #feed li.fresh .feed-count { color: #8e8e93; }
56
- @keyframes feedIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; } }
57
- #hint { left: 50%; transform: translateX(-50%); bottom: 16px; font-size: 12px; color: #98989f; white-space: nowrap; }
58
- #detail { position: fixed; top: 0; right: 0; bottom: 0; width: min(400px, 94vw); z-index: 20;
59
- background: rgba(10, 12, 16, 0.92); border-left: 1px solid rgba(255,255,255,0.1);
60
- backdrop-filter: blur(14px); padding: 18px; overflow-y: auto; display: none; }
61
- #detail.open { display: block; }
62
- #detail h2 { font-size: 15px; margin: 0 0 2px; display: flex; align-items: center; gap: 8px; }
63
- #detail h2 .logo { width: 22px; height: 22px; display: inline-flex; }
64
- #detail h2 .logo svg { width: 22px; height: 22px; fill: #f5f5f7; }
65
- #detail .dsub { font-size: 12.5px; color: #98989f; margin-bottom: 12px; }
66
- #detail dl { margin: 0 0 12px; display: grid; grid-template-columns: auto 1fr; gap: 5px 12px; font-size: 12.5px; }
67
- #detail dt { color: #98989f; }
68
- #detail dd { margin: 0; word-break: break-word; }
69
- #detail h3 { font-size: 11px; letter-spacing: 0.06em; color: #98989f; margin: 14px 0 8px; }
70
- #detail ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; font-size: 12.5px; color: #c7c7cc; }
71
- #detail ul li b { color: #f5f5f7; font-weight: 600; }
72
- #detail .ts { color: #636366; font-size: 11.5px; }
73
- #detailClose { position: absolute; top: 12px; right: 12px; width: 30px; height: 30px; border-radius: 99px;
74
- border: 1px solid rgba(255,255,255,0.12); background: transparent; color: #98989f;
75
- cursor: pointer; font-size: 14px; line-height: 1; font-family: inherit; }
76
- #detailClose:hover { color: #f5f5f7; border-color: rgba(255,255,255,0.25); }
77
- #dChat { border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 10px; max-height: 52vh; min-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 9px; }
78
- /* Scroll fino escuro no painel e no chat (Firefox + WebKit): o padrao
79
- claro do SO quebra o tema escuro da pagina. */
80
- #detail, #dChat { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.24) transparent; }
81
- #detail::-webkit-scrollbar, #dChat::-webkit-scrollbar { width: 10px; }
82
- #detail::-webkit-scrollbar-track, #dChat::-webkit-scrollbar-track { background: transparent; }
83
- #detail::-webkit-scrollbar-thumb, #dChat::-webkit-scrollbar-thumb { background-color: rgba(255,255,255,0.18); border-radius: 8px; border: 3px solid transparent; background-clip: content-box; }
84
- #detail::-webkit-scrollbar-thumb:hover, #dChat::-webkit-scrollbar-thumb:hover { background-color: rgba(255,255,255,0.32); }
85
- #dChat .msg { font-size: 12.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; padding: 7px 9px; border-radius: 9px; }
86
- #dChat .msg .who { display: block; font-size: 10.5px; letter-spacing: 0.04em; color: #98989f; margin-bottom: 2px; }
87
- #dChat .msg.user { background: rgba(10,132,255,0.16); }
88
- #dChat .msg.assistant { background: rgba(255,255,255,0.05); }
89
- #dChat .evt { font-size: 11.5px; color: #636366; }
90
- #dChat .empty { color: #636366; font-size: 12px; }
91
- #dSend { display: flex; gap: 8px; margin-top: 10px; }
92
- #dSendText { flex: 1; min-width: 0; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; color: #f5f5f7; padding: 8px 10px; font-size: 12.5px; font-family: inherit; }
93
- #dSendText:focus { outline: none; border-color: rgba(255,255,255,0.3); }
94
- #dSendText:disabled { opacity: 0.4; }
95
- #dSendBtn, #dArchive { border: 1px solid rgba(255,255,255,0.14); background: rgba(10,132,255,0.25); color: #f5f5f7; border-radius: 10px; padding: 8px 12px; cursor: pointer; font-family: inherit; font-size: 12.5px; }
96
- #dSendBtn:disabled, #dArchive:disabled { opacity: 0.4; cursor: default; }
97
- #dArchive { width: 100%; margin-top: 8px; }
98
- #dSendMsg { font-size: 11.5px; color: #98989f; margin: 6px 0 0; min-height: 15px; }
99
- .node { position: absolute; width: 196px; background: #13161c; border: 1px solid rgba(255,255,255,.09);
100
- border-radius: 14px; padding: 11px 13px; pointer-events: auto; cursor: grab; user-select: none;
101
- box-shadow: 0 8px 28px rgba(0,0,0,.45); }
102
- .node:active { cursor: grabbing; }
103
- .node { transform-origin: 0 0; }
104
- .node.orch { width: 228px; border-color: rgba(255,255,255,.18); }
105
- .node .row { display: flex; align-items: center; gap: 8px; min-width: 0; }
106
- .node .row .dot { flex-shrink: 0; }
107
- .node .logo { width: 22px; height: 22px; display: inline-flex; flex-shrink: 0; }
108
- .node .logo svg { width: 22px; height: 22px; fill: #f5f5f7; }
109
- .node.orch .logo { width: 26px; height: 26px; }
110
- .node.orch .logo svg { width: 26px; height: 26px; }
111
- .node .name { font-size: 13.5px; font-weight: 650; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
112
- .node .sub { font-size: 12px; color: #98989f; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
113
- .node .act { font-size: 12px; color: #98989f; margin-top: 5px; min-height: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
114
- .node .act b { color: #f5f5f7; }
115
- .node.alert { border-color: rgba(255,159,10,.65); animation: alertPulse 1.6s ease-in-out infinite; }
116
- @keyframes alertPulse { 0%,100% { border-color: rgba(255,159,10,.65); } 50% { border-color: rgba(255,159,10,1); } }
117
- </style>
118
- </head>
119
- <body>
120
- <canvas id="scene"></canvas>
121
- <div id="nodes"></div>
122
- <div class="hud" id="title"><h1><span class="live"></span>Run canvas</h1><p id="summary">connecting...</p></div>
123
- <div class="hud" id="tools">
124
- <button id="btnMotion" class="on" type="button" title="Pause motion"><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>
125
- <button id="btnReset" type="button" title="Recenter" aria-label="Recenter"><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>
126
- <button id="btnHide" type="button" title="Hide completed" aria-label="Hide completed"><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>
127
- </div>
128
- <div class="hud" id="legend"><span><i class="dot working"></i>Working</span><span><i class="dot waiting"></i>Waiting for you</span><span><i class="dot resting"></i>Paused</span><span><i class="dot done"></i>Completed</span></div>
129
- <div class="hud" id="feed"><h2>RIGHT NOW</h2><ul id="feedList"></ul></div>
130
- <div class="hud" id="hint">drag to arrange · scroll to zoom · click an agent for details</div>
131
- <aside id="detail" aria-label="Session details">
132
- <button id="detailClose" type="button" aria-label="Close">×</button>
133
- <h2 id="dTitle"></h2>
134
- <p class="dsub" id="dSub"></p>
135
- <dl id="dRows"></dl>
136
- <h3>CONVERSATION</h3>
137
- <div id="dChat"></div>
138
- <form id="dSend">
139
- <input id="dSendText" type="text" autocomplete="off" placeholder="message for this session">
140
- <button id="dSendBtn" type="submit">Send</button>
141
- </form>
142
- <p id="dSendMsg" role="status"></p>
143
- <button id="dArchive" type="button" style="display:none">Mark as completed</button>
144
- </aside>
145
- <script>
146
- "use strict";
147
- 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>';
148
- 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>';
149
- 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>';
150
- 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>';
151
- var LOGO_OPENCODE = '<svg viewBox="0 0 24 24"><path fill-rule="evenodd" d="M16 6H8v12h8V6zm4 16H4V2h16v20z"/></svg>';
152
- function logoFor(agent) {
153
- if (agent === "claude") return LOGO_ANTHROPIC;
154
- if (agent === "codex") return LOGO_OPENAI;
155
- if (agent === "opencode") return LOGO_OPENCODE;
156
- return LOGO_TERM;
157
- }
158
- var AGENT_NAME = { claude: "Claude", codex: "Codex", opencode: "OpenCode", omp: "OMP", antigravity: "Antigravity" };
159
- function agentName(a) { return AGENT_NAME[a] || a || "?"; }
160
- var STATUS_HUMAN = { working: "Working now", starting: "Working now", needs_input: "Waiting for you",
161
- idle: "Paused", completed: "Completed", failed: "Failed", stopped: "Stopped",
162
- orphaned: "Interrupted", interrupted: "Interrupted", dead: "Died" };
163
- var STATUS_DOT = { working: "working", starting: "working", needs_input: "waiting", idle: "resting",
164
- completed: "done", failed: "failed", stopped: "done", orphaned: "waiting", interrupted: "waiting", dead: "done" };
165
- var STATUS_COLOR = { working: "#30d158", starting: "#30d158", needs_input: "#ff9f0a", idle: "#636366",
166
- completed: "#3a3a3c", failed: "#ff453a", stopped: "#3a3a3c", orphaned: "#ff9f0a", interrupted: "#ff9f0a",
167
- dead: "#3a3a3c" };
168
- function lastEventHuman(last) {
169
- if (!last) return "";
170
- if (last === "exit 0") return "finished successfully";
171
- if (last === "agent_end") return "finished its response";
172
- if (last.indexOf("tool: ") === 0) {
173
- var tool = last.slice(6);
174
- if (tool === "bash") return "running terminal commands";
175
- return "using " + tool;
176
- }
177
- if (last.indexOf("send: ") === 0) return "received your message";
178
- return "";
179
- }
180
- function toolHuman(name) {
181
- if (!name) return "a task";
182
- if (name === "bash" || name === "Bash") return "terminal";
183
- return name;
184
- }
185
- function shortPath(s) {
186
- if (!s || typeof s !== "string") return "";
187
- var clean = s.replace(/\\\\/g, "/").replace(/\\/\\/+$/, "");
188
- var parts = clean.split("/").filter(Boolean);
189
- if (parts.length > 2) return parts.slice(-2).join("/");
190
- return parts.join("/") || s;
191
- }
192
- /* Pedaco do input real da ferramenta: primeira string interessante, cortada.
193
- "terminal" sozinho nao diz nada; "podman ps" diz. */
194
- function inputSnippet(input) {
195
- if (!input) return "";
196
- if (typeof input === "string") return clip(input);
197
- if (typeof input !== "object") return "";
198
- if (Array.isArray(input.todos)) return "(" + input.todos.length + " tasks)";
199
- var pathKeys = ["filePath", "file_path", "filename", "file", "path", "target", "dest", "directory", "dir"];
200
- for (var p = 0; p < pathKeys.length; p++) {
201
- var pv = input[pathKeys[p]];
202
- if (typeof pv === "string" && pv.trim() !== "") return clip(shortPath(pv.trim()));
203
- }
204
- var textKeys = ["command", "cmd", "script", "query", "pattern", "regex", "title", "message", "text", "prompt", "url"];
205
- for (var t = 0; t < textKeys.length; t++) {
206
- var tv = input[textKeys[t]];
207
- if (typeof tv === "string" && tv.trim() !== "") return clip(tv.trim());
208
- }
209
- return "";
210
- }
211
- function clip(s) {
212
- var one = String(s).replace(/\\s+/g, " ").trim();
213
- return one.length > 40 ? one.slice(0, 40) + "…" : one;
214
- }
215
- function toolLabel(tool) {
216
- var name = (tool && tool.name) || "";
217
- var snip = (tool && tool.title && typeof tool.title === "string") ? clip(tool.title) : inputSnippet(tool && tool.input);
218
- if (!name) return snip || "task";
219
- if (name === "bash" || name === "Bash") return snip ? "$ " + snip : "terminal";
220
- return snip ? name + " " + snip : name;
221
- }
222
- function timeAgo(iso) {
223
- var t = new Date(iso).getTime();
224
- if (!t) return "";
225
- var m = Math.max(0, Math.round((Date.now() - t) / 60000));
226
- if (m < 1) return "just now";
227
- if (m < 60) return m + " min ago";
228
- var h = Math.floor(m / 60);
229
- if (h < 24) return h + " hr ago";
230
- return Math.floor(h / 24) + " days ago";
231
- }
232
- function ringSlot(wi) {
233
- var j = 0, start = 0;
234
- while (true) {
235
- var R = 300 + j * 230;
236
- var k = Math.max(6, Math.floor(2 * Math.PI * R / 250));
237
- if (wi < start + k) {
238
- return { a: ((wi - start) + j * 0.5) / k * Math.PI * 2 - Math.PI / 2, r: R };
239
- }
240
- start += k;
241
- j++;
242
- }
243
- }
244
- function hexA(hex, a) {
245
- var r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
246
- return "rgba(" + r + "," + g + "," + b + "," + Math.max(0, Math.min(1, a)).toFixed(3) + ")";
247
- }
248
-
249
- var nodesById = {};
250
- var nodes = [];
251
- var canvas = document.getElementById("scene");
252
- var ctx = canvas.getContext("2d");
253
- var cam = { x: 700, y: 330, zoom: 1 };
254
- var motion = true;
255
- function resize() {
256
- var dpr = window.devicePixelRatio || 1;
257
- canvas.width = window.innerWidth * dpr;
258
- canvas.height = window.innerHeight * dpr;
259
- canvas.style.width = window.innerWidth + "px";
260
- canvas.style.height = window.innerHeight + "px";
261
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
262
- }
263
- window.addEventListener("resize", resize);
264
- resize();
265
- function w2s(x, y) { return [(x - cam.x) * cam.zoom + window.innerWidth / 2, (y - cam.y) * cam.zoom + window.innerHeight / 2]; }
266
- function edgeCurve(a, b) {
267
- var mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
268
- var dx = b.x - a.x, dy = b.y - a.y;
269
- var len = Math.sqrt(dx * dx + dy * dy) || 1;
270
- var bow = Math.min(60, len * 0.15);
271
- return { ax: a.x, ay: a.y, bx: b.x, by: b.y, cx: mx - dy / len * bow, cy: my + dx / len * bow };
272
- }
273
- function qpoint(e, t) {
274
- var u = 1 - t;
275
- 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];
276
- }
277
- var particles = [];
278
- var rings = [];
279
- function spawnFlow(from, color, n) {
280
- if (!from || from.runId === "solo") return;
281
- var to = null;
282
- for (var i = 0; i < nodes.length; i++) {
283
- if (nodes[i].runId === from.runId && nodes[i].isOrch) { to = nodes[i]; break; }
284
- }
285
- if (!to) return;
286
- for (var k = 0; k < (n || 2); k++) {
287
- particles.push({ from: from, to: to, t: -k * 0.12, speed: 0.008 + Math.random() * 0.008,
288
- color: color || STATUS_COLOR[from.status] || "#30d158", size: 2 + Math.random() * 1.5 });
289
- }
290
- }
291
- function spawnRing(n, color) {
292
- if (n) rings.push({ x: n.x, y: n.y, r: 20, maxR: 120, alpha: 0.5, color: color });
293
- }
294
-
295
- var nodesLayer = document.getElementById("nodes");
296
- var feedList = document.getElementById("feedList");
297
- function makeNodeEl(n) {
298
- var el = document.createElement("div");
299
- el.className = "node" + (n.isOrch ? " orch" : "");
300
- var row = document.createElement("div");
301
- row.className = "row";
302
- var logo = document.createElement("span");
303
- logo.className = "logo";
304
- logo.innerHTML = n.isOrch ? LOGO_ORCH : logoFor(n.agent);
305
- row.appendChild(logo);
306
- var dot = document.createElement("i");
307
- row.appendChild(dot);
308
- var name = document.createElement("span");
309
- name.className = "name";
310
- row.appendChild(name);
311
- el.appendChild(row);
312
- var sub = document.createElement("div");
313
- sub.className = "sub";
314
- el.appendChild(sub);
315
- var act = document.createElement("div");
316
- act.className = "act";
317
- el.appendChild(act);
318
- n.el = el; n.logoEl = logo; n.dotEl = dot; n.nameEl = name; n.subEl = sub; n.actEl = act;
319
- el.addEventListener("mousedown", function (ev) { startDragNode(ev, n); });
320
- nodesLayer.appendChild(el);
321
- paintNode(n);
322
- }
323
- function paintNode(n) {
324
- n.dotEl.className = "dot " + (STATUS_DOT[n.status] || "done");
325
- if (n.logoEl) n.logoEl.innerHTML = n.isOrch ? LOGO_ORCH : logoFor(n.agent);
326
- n.nameEl.textContent = n.isOrch ? "Orchestrator" : agentName(n.agent);
327
- if (n.isOrch) {
328
- n.subEl.style.display = "";
329
- n.subEl.textContent = n.count + (n.count === 1 ? " session" : " sessions");
330
- } else {
331
- n.subEl.style.display = "none";
332
- n.subEl.textContent = "";
333
- }
334
- n.actEl.innerHTML = "";
335
- var b = document.createElement("b");
336
- b.textContent = STATUS_HUMAN[n.status] || "Completed";
337
- n.actEl.appendChild(b);
338
- n.actEl.appendChild(document.createTextNode(" · " + n.activity));
339
- n.actEl.title = (STATUS_HUMAN[n.status] || "") + (n.activity ? " · " + n.activity : "");
340
- n.el.classList.toggle("orch", !!n.isOrch);
341
- n.el.classList.toggle("alert", n.status === "needs_input");
342
- n.el.title = "session " + n.id + (n.label ? " · " + n.label : "");
343
- }
344
- function feed(agent, text, kind) {
345
- if (!text) return;
346
- var top = feedList.firstElementChild;
347
- if (top && top._agent === agent && top._text === text) {
348
- var count = (top._count || 1) + 1;
349
- top._count = count;
350
- var badge = top.querySelector(".feed-count");
351
- if (!badge) {
352
- badge = document.createElement("span");
353
- badge.className = "feed-count";
354
- top.appendChild(badge);
355
- }
356
- badge.textContent = "×" + count;
357
- top.classList.remove("fresh");
358
- void top.offsetWidth;
359
- top.classList.add("fresh");
360
- return;
361
- }
362
- var li = document.createElement("li");
363
- li.className = "fresh" + (kind ? " " + kind : "");
364
- li._agent = agent;
365
- li._text = text;
366
- li._count = 1;
367
- li.title = agent + ": " + text;
368
- var b = document.createElement("b");
369
- b.textContent = agent;
370
- li.appendChild(b);
371
- var span = document.createElement("span");
372
- span.className = "feed-text";
373
- span.textContent = text;
374
- li.appendChild(span);
375
- feedList.insertBefore(li, feedList.firstChild);
376
- while (feedList.children.length > 6) feedList.removeChild(feedList.lastChild);
377
- var items = feedList.children;
378
- for (var i = 0; i < items.length; i++) items[i].classList.toggle("fresh", i === 0);
379
- }
380
- function paintSummary() {
381
- var working = 0, waiting = 0, hidden = 0;
382
- nodes.forEach(function (n) {
383
- if (n.status === "working" || n.status === "starting") working++;
384
- if (n.status === "needs_input") waiting++;
385
- if (hideDone && isDone(n.status)) hidden++;
386
- });
387
- var el = document.getElementById("summary");
388
- el.innerHTML = "";
389
- el.appendChild(document.createTextNode(nodes.length + " sessions · "));
390
- var b1 = document.createElement("b"); b1.textContent = working + " working"; el.appendChild(b1);
391
- el.appendChild(document.createTextNode(" · "));
392
- var b2 = document.createElement("b"); b2.textContent = waiting + " waiting for you"; el.appendChild(b2);
393
- if (hidden > 0) el.appendChild(document.createTextNode(" · " + hidden + " hidden"));
394
- }
395
-
396
- /* Reconcilia o poll com os nos: preserva posicao arrastada, atualiza status. */
397
- var streams = {};
398
- function baseActivity(s) {
399
- if (s.status === "needs_input") return "waiting for your approval";
400
- var last = lastEventHuman(s.lastEvent);
401
- if (s.status === "failed") return last ? "failed while " + last : "failed, check the logs";
402
- if (s.status === "dead") return last ? "died while " + last : "died, the process disappeared";
403
- if (s.status === "completed") return last ? "completed, " + last : "completed";
404
- if (s.status === "working" || s.status === "starting") return last || "processing...";
405
- if (last) return "last: " + last;
406
- return "no recent activity";
407
- }
408
- var lastSessions = [];
409
- function visibleNodes() {
410
- return nodes.filter(function (n) { return !(hideDone && isDone(n.status)); });
411
- }
412
- /* Raio de conteudo de um grupo a partir dos nos visiveis: grupo de um no
413
- so vira um ponto compacto em vez de pagar o diametro do anel cheio. */
414
- function groupRadius(list) {
415
- var workers = 0;
416
- for (var i = 0; i < list.length; i++) if (!list[i].isOrch) workers++;
417
- if (workers === 0) return 130;
418
- return ringSlot(workers - 1).r + 130;
419
- }
420
- /* Layout deterministico sobre o conjunto VISIVEL: esconder concluidas
421
- reempacota o resto lado a lado em vez de deixar buracos no mundo.
422
- O slot (wi) nasce na criacao em ordem de chegada, entao polls e
423
- chegadas novas nao embaralham ninguem; so o arraste fixa (pinned). */
424
- function layoutVisible() {
425
- var groups = {};
426
- visibleNodes().forEach(function (n) {
427
- if (!groups[n.runId]) groups[n.runId] = [];
428
- groups[n.runId].push(n);
429
- });
430
- var gkeys = Object.keys(groups).sort();
431
- var radii = gkeys.map(function (k) { return groupRadius(groups[k]); });
432
- var total = 0;
433
- for (var i = 0; i < gkeys.length - 1; i++) total += radii[i] + radii[i + 1] + 260;
434
- var cx = -total / 2;
435
- gkeys.forEach(function (key, gi) {
436
- var list = groups[key];
437
- list.forEach(function (n) {
438
- // Aneis concentricos com lotacao pela circunferencia: cada no tem
439
- // ~235px de espaco no anel, entao N workers nunca se encostam.
440
- // (Filotaxia empacota demais para cards de 196px.)
441
- if (n.isOrch) { n.hx = cx; n.hy = 0; }
442
- else {
443
- var slot = ringSlot(Math.max(0, n.wi));
444
- n.hx = cx + Math.cos(slot.a) * slot.r;
445
- n.hy = Math.sin(slot.a) * slot.r;
446
- }
447
- if (!n.pinned) { n.x = n.hx; n.y = n.hy; }
448
- });
449
- if (gi < gkeys.length - 1) cx += radii[gi] + radii[gi + 1] + 260;
450
- });
451
- }
452
- function reconcile(sessions) {
453
- lastSessions = sessions;
454
- var totals = {};
455
- sessions.forEach(function (s) {
456
- var key = s.runId || ("solo:" + s.id);
457
- totals[key] = (totals[key] || 0) + 1;
458
- });
459
- var seen = {};
460
- sessions.forEach(function (s) {
461
- var key = s.runId || ("solo:" + s.id);
462
- var isOrch = s.id === s.runId;
463
- var n = nodesById[s.id];
464
- if (!n) {
465
- var wi = -1;
466
- if (!isOrch) {
467
- wi = 0;
468
- for (var k = 0; k < nodes.length; k++) {
469
- if (nodes[k].runId === key && !nodes[k].isOrch) wi = Math.max(wi, nodes[k].wi + 1);
470
- }
471
- }
472
- n = { id: s.id, runId: key, isOrch: isOrch, agent: s.agent,
473
- x: 0, y: 0, wi: wi, pinned: false, liveUntil: 0 };
474
- nodesById[s.id] = n;
475
- nodes.push(n);
476
- makeNodeEl(n);
477
- }
478
- n.status = s.status;
479
- n.agent = s.agent;
480
- n.isOrch = isOrch;
481
- n.repo = s.repository ? String(s.repository).split("/").pop() : "";
482
- n.count = totals[key];
483
- n.label = s.name || "";
484
- if (Date.now() > n.liveUntil) n.activity = baseActivity(s);
485
- paintNode(n);
486
- seen[s.id] = true;
487
- if (detailId === s.id) {
488
- detailStatus = s.status;
489
- detailOrigin = s.origin || null;
490
- paintSendState();
491
- }
492
- });
493
- for (var id in nodesById) {
494
- if (!seen[id]) {
495
- var n = nodesById[id];
496
- if (n.es) { try { n.es.close(); } catch (e) {} }
497
- if (n.el && n.el.parentNode) n.el.parentNode.removeChild(n.el);
498
- delete nodesById[id];
499
- nodes.splice(nodes.indexOf(n), 1);
500
- }
501
- }
502
- layoutVisible();
503
- ensureStreams();
504
- paintSummary();
505
- if (!window.__fitted && nodes.length) {
506
- window.__fitted = true;
507
- // Enquadra o run mais ativo, nao a frota inteira: 100 nos fixos nunca
508
- // cabem legiveis numa viewport. O resto fica ao redor para o pan.
509
- fitCamera(bestRunFocus());
510
- }
511
- }
512
- function bestRunFocus() {
513
- // So o conjunto visivel participa: com hide ligado, no oculto nao
514
- // puxa a camera nem entra no enquadramento.
515
- var vis = visibleNodes();
516
- // Pouca gente visivel: enquadra todo mundo, nao so o run campeao.
517
- // (Com 5 nos, focar so o melhor corta o vizinho na borda.)
518
- if (vis.length <= 8) return vis;
519
- var activeByRun = {};
520
- vis.forEach(function (n) {
521
- if (isActive(n.status)) activeByRun[n.runId] = (activeByRun[n.runId] || 0) + 1;
522
- });
523
- var bestRun = null, bestCount = 0;
524
- for (var rk in activeByRun) {
525
- if (activeByRun[rk] > bestCount) { bestCount = activeByRun[rk]; bestRun = rk; }
526
- }
527
- return bestRun ? vis.filter(function (n) { return n.runId === bestRun; }) : vis;
528
- }
529
- /* Pills de borda para sessoes ativas fora da viewport: o resumo conta a
530
- frota inteira, entao quem trabalha longe aparece aqui com um atalho. */
531
- var edgeMarkers = [];
532
- function drawMarkers() {
533
- edgeMarkers = [];
534
- nodes.forEach(function (n) {
535
- if (!isActive(n.status)) return;
536
- var p = w2s(n.x, n.y);
537
- var m = 80;
538
- if (p[0] > -40 && p[0] < window.innerWidth + 40 && p[1] > -40 && p[1] < window.innerHeight + 40) return;
539
- var cx = Math.min(window.innerWidth - m, Math.max(m, p[0]));
540
- var cy = Math.min(window.innerHeight - m, Math.max(m, p[1]));
541
- var label = agentName(n.agent);
542
- ctx.font = "12px system-ui, sans-serif";
543
- var w = ctx.measureText(label).width + 36;
544
- var h = 26;
545
- var x = cx - w / 2, y = cy - h / 2;
546
- ctx.beginPath();
547
- if (ctx.roundRect) ctx.roundRect(x, y, w, h, 13);
548
- else ctx.rect(x, y, w, h);
549
- ctx.fillStyle = "rgba(19,22,28,0.92)";
550
- ctx.fill();
551
- ctx.strokeStyle = "rgba(255,255,255,0.14)";
552
- ctx.lineWidth = 1;
553
- ctx.stroke();
554
- ctx.beginPath();
555
- ctx.arc(x + 15, cy, 4, 0, Math.PI * 2);
556
- ctx.fillStyle = STATUS_COLOR[n.status] || "#30d158";
557
- ctx.fill();
558
- ctx.fillStyle = "#f5f5f7";
559
- ctx.textBaseline = "middle";
560
- ctx.fillText(label, x + 25, cy + 1);
561
- edgeMarkers.push({ x: x, y: y, w: w, h: h, node: n });
562
- });
563
- }
564
- function fitCamera(list) {
565
- if (!list.length) return;
566
- var xs = list.map(function (n) { return n.x; });
567
- var ys = list.map(function (n) { return n.y; });
568
- var bw = Math.max.apply(null, xs) - Math.min.apply(null, xs) + 700;
569
- var bh = Math.max.apply(null, ys) - Math.min.apply(null, ys) + 500;
570
- cam.x = (Math.min.apply(null, xs) + Math.max.apply(null, xs)) / 2;
571
- cam.y = (Math.min.apply(null, ys) + Math.max.apply(null, ys)) / 2;
572
- cam.zoom = Math.min(1, Math.max(0.5, Math.min(window.innerWidth / bw, window.innerHeight / bh)));
573
- }
574
- function isDone(s) { return s === "completed" || s === "failed" || s === "stopped" || s === "dead"; }
575
- function isActive(s) { return s === "working" || s === "starting" || s === "needs_input"; }
576
- function ensureStreams() {
577
- var open = 0;
578
- nodes.forEach(function (n) {
579
- if (isActive(n.status) && !n.es && open < 12) {
580
- open++;
581
- var es = new EventSource("api/sessions/" + encodeURIComponent(n.id) + "/stream");
582
- n.es = es;
583
- es.onmessage = function (m) {
584
- try { onEvent(n, JSON.parse(m.data)); } catch (e) {}
585
- };
586
- es.addEventListener("error", function () {
587
- try { n.es.close(); } catch (e) {}
588
- n.es = null;
589
- });
590
- } else if (!isActive(n.status) && n.es) {
591
- try { n.es.close(); } catch (e) {}
592
- n.es = null;
593
- }
594
- });
595
- }
596
- function touch(n, activity) {
597
- n.activity = activity;
598
- n.liveUntil = Date.now() + 8000;
599
- paintNode(n);
600
- }
601
- function onEvent(n, ev) {
602
- if (!ev || !ev.type) return;
603
- if (ev.type === "text.delta") {
604
- spawnFlow(n, "#30d158", 1);
605
- touch(n, "writing a response");
606
- } else if (ev.type === "tool.started") {
607
- spawnRing(n, "#0a84ff");
608
- spawnFlow(n, "#0a84ff", 1);
609
- touch(n, toolLabel(ev.tool));
610
- feed(agentName(n.agent), toolLabel(ev.tool));
611
- } else if (ev.type === "tool.completed") {
612
- var bad = ev.tool && ev.tool.success === false;
613
- touch(n, toolLabel(ev.tool) + (bad ? " (failed)" : " ✓"));
614
- if (bad) feed(agentName(n.agent), "failed: " + toolLabel(ev.tool), "bad");
615
- } else if (ev.type === "message") {
616
- if (ev.role === "assistant") {
617
- touch(n, "responding");
618
- var textSnippet = clip(ev.content || "");
619
- if (textSnippet) feed(agentName(n.agent), textSnippet);
620
- }
621
- } else if (ev.type === "message.queued") {
622
- touch(n, "message queued");
623
- feed(agentName(n.agent), "message queued");
624
- if (n.id && n.id === detailId) { detailPending = ev.prompt || true; paintSendState(); }
625
- } else if (ev.type === "turn.started") {
626
- if (n.id && n.id === detailId && detailPending) { detailPending = null; paintSendState(); }
627
- } else if (ev.type === "turn.completed") {
628
- // turn completion is recorded on node activity
629
- } else if (ev.type === "file.changed") {
630
- feed(agentName(n.agent), "saved " + (shortPath(ev.path === "unknown" ? "" : ev.path) || "file"));
631
- } else if (ev.type === "permission.requested") {
632
- n.status = "needs_input";
633
- touch(n, "waiting for your approval");
634
- paintSummary();
635
- spawnRing(n, "#ff9f0a");
636
- feed(agentName(n.agent), "needs approval", "warn");
637
- } else if (ev.type === "session.completed" || ev.type === "session.failed") {
638
- n.status = ev.type === "session.failed" ? "failed" : "completed";
639
- touch(n, ev.type === "session.failed" ? ("failed: " + (ev.error || "")) : "completed");
640
- paintSummary();
641
- feed(agentName(n.agent), ev.type === "session.failed" ? "failed" : "completed", ev.type === "session.failed" ? "bad" : "");
642
- // Stop/release cancels the queue server-side: drop a stale "na fila"
643
- // hint on the open detail, mirroring the turn.started branch.
644
- if (n.id && n.id === detailId && detailPending) { detailPending = null; paintSendState(); }
645
- if (n.es) { try { n.es.close(); } catch (e) {} n.es = null; }
646
- }
647
- }
648
-
649
- function drawGrid() {
650
- var gap = 34 * cam.zoom;
651
- if (gap < 14) return;
652
- ctx.fillStyle = "rgba(255,255,255,0.055)";
653
- var ox = (window.innerWidth / 2 - cam.x * cam.zoom) % gap;
654
- var oy = (window.innerHeight / 2 - cam.y * cam.zoom) % gap;
655
- if (ox < 0) ox += gap;
656
- if (oy < 0) oy += gap;
657
- for (var x = ox; x < window.innerWidth; x += gap)
658
- for (var y = oy; y < window.innerHeight; y += gap) ctx.fillRect(x, y, 1.4, 1.4);
659
- }
660
- /* Largura externa do card (CSS content-box + padding 13px/lado + borda):
661
- e o numero que centra o card no ponto e dimensiona o container. */
662
- function nodeOuterW(n) { return (n.isOrch ? 228 : 196) + 28; }
663
- /* Container por run: borda tracejada beeeem suave com o nome do repo,
664
- desenhado atras dos cards. A bbox e recalculada a cada frame a partir
665
- da posicao viva dos nos, entao acompanha arraste e polls. */
666
- function drawGroupBoxes() {
667
- var groups = {};
668
- nodes.forEach(function (n) {
669
- if (hideDone && isDone(n.status)) return;
670
- if (!groups[n.runId]) groups[n.runId] = [];
671
- groups[n.runId].push(n);
672
- });
673
- ctx.save();
674
- ctx.setLineDash([5, 7]);
675
- ctx.lineWidth = 1;
676
- ctx.strokeStyle = "rgba(255,255,255,0.10)";
677
- ctx.font = "11px system-ui, sans-serif";
678
- ctx.textBaseline = "bottom";
679
- Object.keys(groups).forEach(function (key) {
680
- var list = groups[key];
681
- var x0 = 1e12, y0 = 1e12, x1 = -1e12, y1 = -1e12, repo = "";
682
- list.forEach(function (n) {
683
- var hw = nodeOuterW(n) / 2 + 28, hh = 44 + 28;
684
- if (n.x - hw < x0) x0 = n.x - hw;
685
- if (n.y - hh < y0) y0 = n.y - hh;
686
- if (n.x + hw > x1) x1 = n.x + hw;
687
- if (n.y + hh > y1) y1 = n.y + hh;
688
- if (n.isOrch && n.repo) repo = n.repo;
689
- });
690
- if (!repo) {
691
- for (var i = 0; i < list.length; i++) {
692
- if (list[i].repo) { repo = list[i].repo; break; }
693
- }
694
- }
695
- var a = w2s(x0, y0), b = w2s(x1, y1);
696
- ctx.beginPath();
697
- if (ctx.roundRect) ctx.roundRect(a[0], a[1], b[0] - a[0], b[1] - a[1], 18);
698
- else ctx.rect(a[0], a[1], b[0] - a[0], b[1] - a[1]);
699
- ctx.stroke();
700
- if (repo) {
701
- ctx.fillStyle = "rgba(235,235,240,0.38)";
702
- ctx.fillText(repo, a[0] + 14, a[1] - 6);
703
- }
704
- });
705
- ctx.restore();
706
- }
707
- function frame() {
708
- ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
709
- drawGrid();
710
- drawGroupBoxes();
711
- nodes.forEach(function (n) {
712
- if (n.isOrch || n.runId === "solo" || n.runId.indexOf("solo:") === 0) return;
713
- if (hideDone && isDone(n.status)) return;
714
- var o = null;
715
- for (var i = 0; i < nodes.length; i++) {
716
- if (nodes[i].runId === n.runId && nodes[i].isOrch) { o = nodes[i]; break; }
717
- }
718
- if (!o) return;
719
- if (hideDone && isDone(o.status)) return;
720
- var e = edgeCurve(n, o);
721
- var pa = w2s(e.ax, e.ay), pb = w2s(e.bx, e.by), pc = w2s(e.cx, e.cy);
722
- ctx.beginPath();
723
- ctx.moveTo(pa[0], pa[1]);
724
- ctx.quadraticCurveTo(pc[0], pc[1], pb[0], pb[1]);
725
- ctx.strokeStyle = n.status === "working" ? "rgba(255,255,255,0.20)" : "rgba(255,255,255,0.10)";
726
- ctx.lineWidth = 1.2;
727
- ctx.stroke();
728
- });
729
- for (var i = particles.length - 1; i >= 0; i--) {
730
- var p = particles[i];
731
- if (hideDone && (isDone(p.from.status) || isDone(p.to.status))) {
732
- particles.splice(i, 1);
733
- continue;
734
- }
735
- p.t += motion ? p.speed : 0;
736
- if (p.t > 1.1) { particles.splice(i, 1); continue; }
737
- if (p.t < 0) continue;
738
- var q = qpoint(edgeCurve(p.from, p.to), Math.min(1, p.t));
739
- var s = w2s(q[0], q[1]);
740
- ctx.beginPath();
741
- ctx.arc(s[0], s[1], p.size * cam.zoom, 0, Math.PI * 2);
742
- ctx.fillStyle = p.color;
743
- ctx.globalAlpha = 0.9;
744
- ctx.fill();
745
- ctx.globalAlpha = 1;
746
- }
747
- for (var j = rings.length - 1; j >= 0; j--) {
748
- var r = rings[j];
749
- r.r += motion ? 2.2 : 0;
750
- r.alpha -= motion ? 0.011 : 0;
751
- if (r.alpha <= 0 || r.r > r.maxR) { rings.splice(j, 1); continue; }
752
- var c = w2s(r.x, r.y);
753
- var rad = Math.max(1, r.r * cam.zoom);
754
- var g = ctx.createRadialGradient(c[0], c[1], 0, c[0], c[1], rad);
755
- g.addColorStop(0, hexA(r.color, r.alpha * 0.55));
756
- g.addColorStop(0.55, hexA(r.color, r.alpha * 0.22));
757
- g.addColorStop(1, hexA(r.color, 0));
758
- ctx.beginPath();
759
- ctx.arc(c[0], c[1], rad, 0, Math.PI * 2);
760
- ctx.fillStyle = g;
761
- ctx.fill();
762
- }
763
- drawMarkers();
764
- nodes.forEach(function (n) {
765
- var hidden = hideDone && isDone(n.status);
766
- n.el.style.display = hidden ? "none" : "";
767
- if (hidden) return;
768
- var p = w2s(n.x, n.y);
769
- // Largura EXTERNA do card: o CSS usa content-box com padding 13px de
770
- // cada lado, entao o centro real fica 13px a direita do 196/228.
771
- var w = nodeOuterW(n);
772
- // O no escala junto com o zoom: sem isso o DOM fica gigante no mundo
773
- // e o layout que e limpo em coordenadas vira pilha na tela.
774
- n.el.style.transform = "translate(" + Math.round(p[0] - (w / 2) * cam.zoom) + "px," +
775
- Math.round(p[1] - 44 * cam.zoom) + "px) scale(" + cam.zoom + ")";
776
- });
777
- requestAnimationFrame(frame);
778
- }
779
-
780
- var dragNode = null, panning = false, lastM = null;
781
- function toWorld(cx, cy) {
782
- return [(cx - window.innerWidth / 2) / cam.zoom + cam.x, (cy - window.innerHeight / 2) / cam.zoom + cam.y];
783
- }
784
- var dragMoved = false;
785
- function startDragNode(ev, n) {
786
- dragNode = n;
787
- dragMoved = false;
788
- lastM = [ev.clientX, ev.clientY];
789
- ev.stopPropagation();
790
- ev.preventDefault();
791
- }
792
- /* Hit-test recalculado na hora a partir dos nos: nao depende do array
793
- desenhado no ultimo frame, entao nunca diverge dele. */
794
- function markerAt(cx, cy) {
795
- var found = null;
796
- nodes.forEach(function (n) {
797
- if (found || !isActive(n.status)) return;
798
- var p = w2s(n.x, n.y);
799
- var m = 80;
800
- if (p[0] > -40 && p[0] < window.innerWidth + 40 && p[1] > -40 && p[1] < window.innerHeight + 40) return;
801
- var px = Math.min(window.innerWidth - m, Math.max(m, p[0]));
802
- var py = Math.min(window.innerHeight - m, Math.max(m, p[1]));
803
- var label = agentName(n.agent);
804
- ctx.font = "12px system-ui, sans-serif";
805
- var w = ctx.measureText(label).width + 36;
806
- var x = px - w / 2, y = py - 13;
807
- if (cx >= x && cx <= x + w && cy >= y && cy <= y + 26) found = n;
808
- });
809
- return found;
810
- }
811
- canvas.addEventListener("mousedown", function (ev) {
812
- var hit = markerAt(ev.clientX, ev.clientY);
813
- if (hit) {
814
- cam.x = hit.x;
815
- cam.y = hit.y;
816
- if (cam.zoom < 0.7) cam.zoom = 0.7;
817
- return;
818
- }
819
- panning = true;
820
- lastM = [ev.clientX, ev.clientY];
821
- });
822
- window.addEventListener("mousemove", function (ev) {
823
- if (dragNode) {
824
- if (Math.abs(ev.clientX - lastM[0]) + Math.abs(ev.clientY - lastM[1]) > 4) {
825
- dragMoved = true;
826
- // Posicao manual sobrevive aos polls: o layout so move no solto.
827
- dragNode.pinned = true;
828
- }
829
- dragNode.x += (ev.clientX - lastM[0]) / cam.zoom;
830
- dragNode.y += (ev.clientY - lastM[1]) / cam.zoom;
831
- lastM = [ev.clientX, ev.clientY];
832
- } else if (panning) {
833
- cam.x -= (ev.clientX - lastM[0]) / cam.zoom;
834
- cam.y -= (ev.clientY - lastM[1]) / cam.zoom;
835
- lastM = [ev.clientX, ev.clientY];
836
- }
837
- });
838
- window.addEventListener("mouseup", function () {
839
- if (dragNode && !dragMoved) selectSession(dragNode.id);
840
- dragNode = null;
841
- panning = false;
842
- });
843
- window.addEventListener("keydown", function (ev) {
844
- if (ev.key === "Escape") closeDetail();
845
- });
846
- window.addEventListener("wheel", function (ev) {
847
- // O painel de detalhe tem scroll proprio (#detail e #dChat): o zoom do
848
- // canvas so vale fora dele, senao rolar o chat daria zoom na cena.
849
- var t = ev.target;
850
- if (t && typeof t.closest === "function" && t.closest("#detail")) return;
851
- ev.preventDefault();
852
- var before = toWorld(ev.clientX, ev.clientY);
853
- cam.zoom = Math.min(2.2, Math.max(0.45, cam.zoom * (ev.deltaY < 0 ? 1.1 : 0.9)));
854
- var after = toWorld(ev.clientX, ev.clientY);
855
- cam.x += before[0] - after[0];
856
- cam.y += before[1] - after[1];
857
- }, { passive: false });
858
- function syncUrl() {
859
- try {
860
- var qp = new URLSearchParams(window.location.search);
861
- // Padrão é esconder concluídas: a URL limpa É o estado escondido, e
862
- // quem quiser ver tudo marca hide=0 explicitamente.
863
- if (hideDone) qp.delete("hide"); else qp.set("hide", "0");
864
- if (!motion) qp.set("motion", "0"); else qp.delete("motion");
865
- var qs = qp.toString();
866
- window.history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : ""));
867
- } catch (e) {}
868
- }
869
- // Filtros sobrevivem ao F5: o estado mora na query string (?hide=0&motion=0).
870
- // hideDone nasce true: concluídas ficam fora até alguém pedir hide=0.
871
- var urlHide = true, urlMotion = true;
872
- try {
873
- var qp0 = new URLSearchParams(window.location.search);
874
- urlHide = qp0.get("hide") !== "0";
875
- urlMotion = qp0.get("motion") !== "0";
876
- } catch (e) {}
877
- motion = urlMotion;
878
- document.getElementById("btnMotion").onclick = function (ev) {
879
- motion = !motion;
880
- // currentTarget, nunca target: o clique cai no SVG interno e a classe
881
- // precisa alternar no botao para o estado visual acompanhar.
882
- ev.currentTarget.classList.toggle("on", motion);
883
- syncUrl();
884
- };
885
- document.getElementById("btnReset").onclick = function () {
886
- // Solta os arrastes manuais, reempacota e volta para a acao (mesmo
887
- // enquadramento da abertura), nunca um zoom cego.
888
- nodes.forEach(function (n) { n.pinned = false; });
889
- layoutVisible();
890
- fitCamera(bestRunFocus());
891
- };
892
- var hideDone = urlHide;
893
- document.getElementById("btnHide").onclick = function (ev) {
894
- hideDone = !hideDone;
895
- ev.currentTarget.classList.toggle("on", hideDone);
896
- ev.currentTarget.title = hideDone ? "Show completed" : "Hide completed";
897
- // Trocar o filtro reempacota o mundo e reenquadra: sem isso os nos
898
- // visiveis ficam presos nos slots antigos, a um oceano de distancia.
899
- layoutVisible();
900
- fitCamera(bestRunFocus());
901
- paintSummary();
902
- syncUrl();
903
- };
904
- document.getElementById("btnMotion").classList.toggle("on", motion);
905
- document.getElementById("btnHide").classList.toggle("on", hideDone);
906
- document.getElementById("btnHide").title = hideDone ? "Show completed" : "Hide completed";
907
-
908
- /* Transcrição da sessão: a mensagem do usuário mora em turn.started.prompt
909
- (o daemon anexa no turno inicial e a cada send), a resposta em
910
- message(role=assistant) com content completo. Ferramentas viram linhas
911
- compactas para a conversa continuar legível. */
912
- var detailId = null, detailStatus = null, detailOrigin = null, detailPending = null;
913
- var sendInFlight = false, sendBlockedNote = false;
914
- function chatMsg(who, text, cls) {
915
- var d = document.createElement("div");
916
- d.className = "msg " + cls;
917
- var w = document.createElement("span");
918
- w.className = "who";
919
- w.textContent = who;
920
- d.appendChild(w);
921
- d.appendChild(document.createTextNode(text));
922
- return d;
923
- }
924
- function chatEvt(text) {
925
- var d = document.createElement("div");
926
- d.className = "evt";
927
- d.textContent = text;
928
- return d;
929
- }
930
- /* Input e botão seguem o estado do daemon: ocupada trava, origin=open
931
- (TUI interativa) trava sempre. Mensagem de sucesso não é apagada aqui. */
932
- function paintSendState() {
933
- var input = document.getElementById("dSendText");
934
- var btn = document.getElementById("dSendBtn");
935
- var msg = document.getElementById("dSendMsg");
936
- if (!detailId) return;
937
- // origin=open (TUI interativa) trava sempre; working/starting entra na
938
- // fila em vez de travar. Mensagem de sucesso não é apagada aqui.
939
- var reason = "";
940
- if (detailOrigin === "open") reason = "interactive sessions do not accept messages";
941
- input.disabled = sendInFlight || reason !== "";
942
- btn.disabled = sendInFlight || reason !== "";
943
- if (reason) { msg.textContent = reason; sendBlockedNote = true; }
944
- else if (detailPending) { msg.textContent = "1 message queued - sends when the turn ends"; sendBlockedNote = true; }
945
- else if (sendBlockedNote) { msg.textContent = ""; sendBlockedNote = false; }
946
- }
947
- function row(dt, dd) {
948
- var d = document.createElement("dt");
949
- d.textContent = dt;
950
- var v = document.createElement("dd");
951
- v.textContent = dd;
952
- return [d, v];
953
- }
954
- function closeDetail() {
955
- detailId = null;
956
- detailStatus = null;
957
- detailOrigin = null;
958
- detailPending = null;
959
- document.getElementById("detail").classList.remove("open");
960
- }
961
- document.getElementById("detailClose").onclick = closeDetail;
962
- function selectSession(id) {
963
- // /logs traz até 1000 eventos; /get limitaria a transcrição aos 10 últimos.
964
- fetch("api/sessions/" + encodeURIComponent(id) + "/logs").then(function (r) { return r.json(); }).then(function (j) {
965
- var s = j.session || j;
966
- if (!s || !s.id) return;
967
- detailId = s.id;
968
- detailStatus = s.status;
969
- detailOrigin = s.origin || null;
970
- detailPending = s.pendingMessage || null;
971
- // Interrupted rows are actionable again: offer archival. Every other
972
- // status keeps the button hidden.
973
- document.getElementById("dArchive").style.display = s.status === "interrupted" ? "" : "none";
974
- var title = document.getElementById("dTitle");
975
- title.innerHTML = "";
976
- var isOrch = (nodesById[s.id] && nodesById[s.id].isOrch) || (s.id && s.id === s.runId);
977
- var logo = document.createElement("span");
978
- logo.className = "logo";
979
- logo.innerHTML = isOrch ? LOGO_ORCH : logoFor(s.agent);
980
- title.appendChild(logo);
981
- title.appendChild(document.createTextNode((isOrch ? "Orchestrator" : agentName(s.agent)) + " · " + (s.name || s.id)));
982
- document.getElementById("dSub").textContent = (STATUS_HUMAN[s.status] || s.status) +
983
- (s.createdAt ? " · started " + timeAgo(s.createdAt) : "");
984
- var rows = document.getElementById("dRows");
985
- rows.innerHTML = "";
986
- var repo = s.repository ? String(s.repository).split("/").pop() : "";
987
- var items = [
988
- ["Model", s.model || "—"],
989
- // effort chega sempre para claude/codex/omp/antigravity; ausente = opencode (ignora) ou sessão legada.
990
- ["Effort", s.effort || "n/a"],
991
- ["Repo", repo || "—"],
992
- ["Activity", s.lastEvent ? (lastEventHuman(s.lastEvent) || s.lastEvent) : "—"],
993
- ];
994
- if (s.pid) items.push(["PID", String(s.pid)]);
995
- if (s.updatedAt) items.push(["Updated", timeAgo(s.updatedAt)]);
996
- var events = j.events || [];
997
- var tools = 0, msgs = 0;
998
- events.forEach(function (ev) {
999
- if (!ev) return;
1000
- if (ev.type === "tool.completed") tools++;
1001
- if (ev.type === "message" && ev.role === "assistant") msgs++;
1002
- });
1003
- items.push(["Tools", String(tools)]);
1004
- items.push(["Replies", String(msgs)]);
1005
- items.forEach(function (kv) {
1006
- row(kv[0], kv[1]).forEach(function (el) { rows.appendChild(el); });
1007
- });
1008
- var chat = document.getElementById("dChat");
1009
- chat.innerHTML = "";
1010
- // Últimos 200 eventos bastam: render além disso só pesa o DOM.
1011
- var start = Math.max(0, events.length - 200);
1012
- for (var i = start; i < events.length; i++) {
1013
- var ev = events[i];
1014
- if (!ev || !ev.type) continue;
1015
- if (ev.type === "turn.started" && ev.prompt) {
1016
- chat.appendChild(chatMsg("you", String(ev.prompt), "user"));
1017
- } else if (ev.type === "message.queued" && ev.prompt) {
1018
- chat.appendChild(chatMsg("you", String(ev.prompt) + " (queued)", "user"));
1019
- } else if (ev.type === "message" && ev.role === "assistant" && ev.content) {
1020
- chat.appendChild(chatMsg(agentName(s.agent), String(ev.content), "assistant"));
1021
- } else if (ev.type === "tool.started") {
1022
- chat.appendChild(chatEvt(toolLabel(ev.tool)));
1023
- } else if (ev.type === "tool.completed") {
1024
- var bad = ev.tool && ev.tool.success === false;
1025
- if (bad) chat.appendChild(chatEvt("failed: " + toolLabel(ev.tool)));
1026
- } else if (ev.type === "permission.requested") {
1027
- chat.appendChild(chatEvt("needs approval: " + (ev.tool || "an action")));
1028
- } else if (ev.type === "file.changed") {
1029
- chat.appendChild(chatEvt("saved " + (shortPath(ev.path === "unknown" ? "" : ev.path) || "file")));
1030
- }
1031
- }
1032
- if (!chat.children.length) {
1033
- var empty = document.createElement("div");
1034
- empty.className = "empty";
1035
- empty.textContent = "No messages yet.";
1036
- chat.appendChild(empty);
1037
- }
1038
- chat.scrollTop = chat.scrollHeight;
1039
- document.getElementById("detail").classList.add("open");
1040
- paintSendState();
1041
- }).catch(function () {});
1042
- }
1043
- document.getElementById("dSend").addEventListener("submit", function (ev) {
1044
- ev.preventDefault();
1045
- if (!detailId || sendInFlight) return;
1046
- var input = document.getElementById("dSendText");
1047
- var text = input.value.trim();
1048
- if (!text) return;
1049
- var msg = document.getElementById("dSendMsg");
1050
- var btn = document.getElementById("dSendBtn");
1051
- sendInFlight = true;
1052
- input.disabled = true;
1053
- btn.disabled = true;
1054
- msg.textContent = "sending...";
1055
- fetch("api/sessions/" + encodeURIComponent(detailId) + "/send", {
1056
- method: "POST",
1057
- headers: { "content-type": "application/json" },
1058
- body: JSON.stringify({ message: text })
1059
- }).then(function (r) {
1060
- return r.json().then(function (j) { return { ok: r.ok, body: j }; });
1061
- }).then(function (out) {
1062
- if (out.ok) {
1063
- input.value = "";
1064
- if (out.body && out.body.queued) {
1065
- detailPending = text;
1066
- msg.textContent = "message queued - sends when the turn ends";
1067
- } else {
1068
- detailPending = null;
1069
- msg.textContent = "message sent - new turn started";
1070
- }
1071
- poll();
1072
- } else {
1073
- msg.textContent = (out.body && out.body.error) || "could not send";
1074
- }
1075
- }).catch(function () {
1076
- msg.textContent = "daemon is offline?";
1077
- }).then(function () {
1078
- sendInFlight = false;
1079
- paintSendState();
1080
- });
1081
- });
1082
- document.getElementById("dArchive").addEventListener("click", function () {
1083
- if (!detailId) return;
1084
- var msg = document.getElementById("dSendMsg");
1085
- var btn = document.getElementById("dArchive");
1086
- btn.disabled = true;
1087
- msg.textContent = "archiving...";
1088
- fetch("api/sessions/" + encodeURIComponent(detailId) + "/release", { method: "POST" }).then(function (r) {
1089
- return r.json().then(function (j) { return { ok: r.ok, body: j }; });
1090
- }).then(function (out) {
1091
- if (out.ok) {
1092
- msg.textContent = "session archived as completed";
1093
- selectSession(detailId);
1094
- poll();
1095
- } else {
1096
- msg.textContent = (out.body && out.body.error) || "could not archive";
1097
- }
1098
- }).catch(function () {
1099
- msg.textContent = "daemon is offline?";
1100
- }).then(function () {
1101
- btn.disabled = false;
1102
- });
1103
- });
1104
-
1105
- function poll() {
1106
- fetch("api/sessions").then(function (r) { return r.json(); }).then(function (j) {
1107
- reconcile(j.sessions || []);
1108
- }).catch(function () {
1109
- document.getElementById("summary").textContent = "daemon is offline?";
1110
- });
1111
- }
1112
- poll();
1113
- setInterval(poll, 2000);
1114
- frame();
1115
- </script>
1116
- </body>
1117
- </html>`;
1118
- //# sourceMappingURL=canvas-page.js.map