claudeck 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
@@ -0,0 +1,152 @@
1
+ // Cost dashboard
2
+ import { $ } from '../core/dom.js';
3
+ import { escapeHtml } from '../core/utils.js';
4
+ import * as api from '../core/api.js';
5
+ import { registerCommand } from '../ui/commands.js';
6
+
7
+ function formatTokenCount(n) {
8
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
9
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
10
+ return String(n);
11
+ }
12
+
13
+ export async function loadStats() {
14
+ try {
15
+ const cwd = $.projectSelect.value;
16
+ const data = await api.fetchStats(cwd || undefined);
17
+ if (data.totalCost != null) {
18
+ $.totalCostEl.textContent = "$" + data.totalCost.toFixed(2);
19
+ }
20
+ if (data.projectCost != null) {
21
+ $.projectCostEl.textContent = "$" + data.projectCost.toFixed(2);
22
+ } else {
23
+ $.projectCostEl.textContent = "$0.00";
24
+ }
25
+ } catch (err) {
26
+ console.error("Failed to load stats:", err);
27
+ }
28
+ }
29
+
30
+ export async function loadAccountInfo() {
31
+ try {
32
+ const data = await api.fetchAccountInfo();
33
+ if (data.email) {
34
+ $.accountEmail.textContent = data.email;
35
+ $.accountPlan.textContent = data.plan ? `[${data.plan}]` : "";
36
+ } else {
37
+ $.accountEmail.textContent = "---";
38
+ $.accountPlan.textContent = "";
39
+ }
40
+ } catch (err) {
41
+ console.error("Failed to load account info:", err);
42
+ }
43
+ }
44
+
45
+ export async function openCostDashboard() {
46
+ $.costDashboardModal.classList.remove("hidden");
47
+ const cwd = $.projectSelect.value;
48
+ try {
49
+ const data = await api.fetchDashboard(cwd || undefined);
50
+ renderCostDashboard(data);
51
+ } catch (err) {
52
+ console.error("Failed to load cost dashboard:", err);
53
+ }
54
+ }
55
+
56
+ function renderCostDashboard(data) {
57
+ const cardsEl = document.getElementById("cost-summary-cards");
58
+ const todayCost = data.timeline
59
+ .filter((t) => t.date === new Date().toISOString().slice(0, 10))
60
+ .reduce((sum, t) => sum + t.cost, 0);
61
+
62
+ const tokens = data.totalTokens || { input_tokens: 0, output_tokens: 0 };
63
+ const totalTok = tokens.input_tokens + tokens.output_tokens;
64
+
65
+ cardsEl.innerHTML = `
66
+ <div class="cost-card">
67
+ <div class="cost-card-label">Total</div>
68
+ <div class="cost-card-value">$${data.totalCost.toFixed(2)}</div>
69
+ </div>
70
+ <div class="cost-card">
71
+ <div class="cost-card-label">Project</div>
72
+ <div class="cost-card-value">$${(data.projectCost ?? data.totalCost).toFixed(2)}</div>
73
+ </div>
74
+ <div class="cost-card">
75
+ <div class="cost-card-label">Today</div>
76
+ <div class="cost-card-value">$${todayCost.toFixed(4)}</div>
77
+ </div>
78
+ <div class="cost-card">
79
+ <div class="cost-card-label">Tokens</div>
80
+ <div class="cost-card-value">${formatTokenCount(totalTok)}</div>
81
+ <div class="cost-card-sub">${formatTokenCount(tokens.input_tokens)} in / ${formatTokenCount(tokens.output_tokens)} out</div>
82
+ </div>
83
+ `;
84
+
85
+ const tbody = document.getElementById("cost-table-body");
86
+ tbody.innerHTML = "";
87
+ for (const s of data.sessions) {
88
+ if (s.total_cost === 0) continue;
89
+ const tr = document.createElement("tr");
90
+ const sTok = (s.input_tokens || 0) + (s.output_tokens || 0);
91
+ tr.innerHTML = `
92
+ <td title="${escapeHtml(s.id)}">${escapeHtml(s.title || s.project_name || "Session")}</td>
93
+ <td>${s.turns}</td>
94
+ <td>${formatTokenCount(sTok)}</td>
95
+ <td>$${s.total_cost.toFixed(4)}</td>
96
+ `;
97
+ tbody.appendChild(tr);
98
+ }
99
+
100
+ const colIndex = { title: 0, turns: 1, tokens: 2, cost: 3 };
101
+ document.querySelectorAll(".cost-table th[data-sort]").forEach((th) => {
102
+ th.onclick = () => {
103
+ const key = th.dataset.sort;
104
+ const idx = colIndex[key] ?? 0;
105
+ const rows = [...tbody.querySelectorAll("tr")];
106
+ rows.sort((a, b) => {
107
+ const aVal = a.children[idx].textContent;
108
+ const bVal = b.children[idx].textContent;
109
+ if (key === "title") return aVal.localeCompare(bVal);
110
+ return parseFloat(bVal.replace(/[$,k]/g, "")) - parseFloat(aVal.replace(/[$,k]/g, ""));
111
+ });
112
+ tbody.innerHTML = "";
113
+ rows.forEach((r) => tbody.appendChild(r));
114
+ };
115
+ });
116
+
117
+ const chartEl = document.getElementById("cost-chart");
118
+ chartEl.innerHTML = "";
119
+ if (data.timeline.length === 0) {
120
+ chartEl.innerHTML = '<div style="color: var(--text-dim); font-size: 12px; padding: 8px;">No cost data yet</div>';
121
+ return;
122
+ }
123
+ const maxCost = Math.max(...data.timeline.map((t) => t.cost), 0.001);
124
+ for (const day of data.timeline) {
125
+ const pct = Math.round((day.cost / maxCost) * 100);
126
+ const row = document.createElement("div");
127
+ row.className = "cost-chart-row";
128
+ row.innerHTML = `
129
+ <span class="cost-chart-label">${day.date.slice(5)}</span>
130
+ <div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width: ${pct}%"></div></div>
131
+ <span class="cost-chart-value">$${day.cost.toFixed(2)}</span>
132
+ `;
133
+ chartEl.appendChild(row);
134
+ }
135
+ }
136
+
137
+ // Event listeners
138
+ document.querySelector(".term-costs").addEventListener("click", openCostDashboard);
139
+ $.costModalClose.addEventListener("click", () => {
140
+ $.costDashboardModal.classList.add("hidden");
141
+ });
142
+ $.costDashboardModal.addEventListener("click", (e) => {
143
+ if (e.target === $.costDashboardModal) $.costDashboardModal.classList.add("hidden");
144
+ });
145
+
146
+ registerCommand("costs", {
147
+ category: "app",
148
+ description: "Open cost dashboard",
149
+ execute() {
150
+ openCostDashboard();
151
+ },
152
+ });
@@ -0,0 +1,399 @@
1
+ // DAG Editor — SVG-based visual editor for agent dependency graphs
2
+ import { $ } from '../core/dom.js';
3
+ import { getState } from '../core/store.js';
4
+ import { escapeHtml } from '../core/utils.js';
5
+ import * as api from '../core/api.js';
6
+
7
+ const NS = "http://www.w3.org/2000/svg";
8
+ const NODE_W = 140;
9
+ const NODE_H = 50;
10
+
11
+ let dagNodes = [];
12
+ let dagEdges = [];
13
+ let dragging = null;
14
+ let connecting = null; // { fromId, startX, startY }
15
+ let tempLine = null;
16
+
17
+ // ── Public API ──
18
+
19
+ export function openDagModal(dag) {
20
+ dagNodes = [];
21
+ dagEdges = [];
22
+
23
+ if (dag) {
24
+ $.dagModalTitle.textContent = "Edit DAG";
25
+ $.dagFormTitle.value = dag.title;
26
+ $.dagFormDesc.value = dag.description || "";
27
+ $.dagFormEditId.value = dag.id;
28
+ dagNodes = dag.nodes.map(n => ({ ...n }));
29
+ dagEdges = dag.edges.map(e => ({ ...e }));
30
+ } else {
31
+ $.dagModalTitle.textContent = "New DAG";
32
+ $.dagFormTitle.value = "";
33
+ $.dagFormDesc.value = "";
34
+ $.dagFormEditId.value = "";
35
+ }
36
+
37
+ renderPalette();
38
+ renderCanvas();
39
+ $.dagModal.classList.remove("hidden");
40
+ $.dagFormTitle.focus();
41
+ }
42
+
43
+ export function closeDagModal() {
44
+ $.dagModal.classList.add("hidden");
45
+ dagNodes = [];
46
+ dagEdges = [];
47
+ }
48
+
49
+ // ── Palette ──
50
+
51
+ function renderPalette() {
52
+ const agents = getState("agents") || [];
53
+ $.dagNodePalette.innerHTML = `<div class="dag-palette-title">Drag to add</div>`;
54
+ for (const agent of agents) {
55
+ const item = document.createElement("div");
56
+ item.className = "dag-palette-item";
57
+ item.draggable = true;
58
+ item.dataset.agentId = agent.id;
59
+ item.textContent = agent.title;
60
+ item.addEventListener("dragstart", (e) => {
61
+ e.dataTransfer.setData("text/plain", agent.id);
62
+ e.dataTransfer.effectAllowed = "copy";
63
+ });
64
+ $.dagNodePalette.appendChild(item);
65
+ }
66
+ }
67
+
68
+ // ── Canvas Rendering ──
69
+
70
+ function renderCanvas() {
71
+ $.dagCanvas.innerHTML = "";
72
+
73
+ // Defs for arrow marker
74
+ const defs = svgEl("defs");
75
+ const marker = svgEl("marker", {
76
+ id: "dag-arrow", viewBox: "0 0 10 10", refX: "10", refY: "5",
77
+ markerWidth: "8", markerHeight: "8", orient: "auto-start-reverse",
78
+ });
79
+ marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "var(--text-dim)" }));
80
+ defs.appendChild(marker);
81
+ $.dagCanvas.appendChild(defs);
82
+
83
+ // Edges
84
+ for (const edge of dagEdges) {
85
+ const fromNode = dagNodes.find(n => n.id === edge.from);
86
+ const toNode = dagNodes.find(n => n.id === edge.to);
87
+ if (!fromNode || !toNode) continue;
88
+
89
+ const coords = {
90
+ x1: fromNode.x + NODE_W, y1: fromNode.y + NODE_H / 2,
91
+ x2: toNode.x, y2: toNode.y + NODE_H / 2,
92
+ };
93
+ // Wider invisible hit target for easier clicking
94
+ const hitArea = svgEl("line", {
95
+ ...coords,
96
+ stroke: "transparent",
97
+ "stroke-width": "12",
98
+ cursor: "pointer",
99
+ });
100
+ const line = svgEl("line", {
101
+ ...coords,
102
+ class: "dag-edge",
103
+ "marker-end": "url(#dag-arrow)",
104
+ "pointer-events": "none",
105
+ });
106
+ // Click or right-click to delete edge
107
+ const removeEdge = (e) => {
108
+ e.preventDefault();
109
+ dagEdges = dagEdges.filter(de => !(de.from === edge.from && de.to === edge.to));
110
+ renderCanvas();
111
+ };
112
+ hitArea.addEventListener("contextmenu", removeEdge);
113
+ hitArea.addEventListener("click", removeEdge);
114
+ hitArea.addEventListener("mouseenter", () => line.classList.add("dag-edge-hover"));
115
+ hitArea.addEventListener("mouseleave", () => line.classList.remove("dag-edge-hover"));
116
+ hitArea.setAttribute("title", "Click to remove connection");
117
+ $.dagCanvas.appendChild(line);
118
+ $.dagCanvas.appendChild(hitArea);
119
+ }
120
+
121
+ // Nodes
122
+ for (const node of dagNodes) {
123
+ const agents = getState("agents") || [];
124
+ const agent = agents.find(a => a.id === node.agentId);
125
+ const title = agent ? agent.title : node.agentId;
126
+
127
+ const g = svgEl("g", { class: "dag-node", transform: `translate(${node.x}, ${node.y})` });
128
+
129
+ const rect = svgEl("rect", {
130
+ width: NODE_W, height: NODE_H, rx: "6",
131
+ class: "dag-node-rect",
132
+ });
133
+ g.appendChild(rect);
134
+
135
+ const text = svgEl("text", {
136
+ x: NODE_W / 2, y: NODE_H / 2 + 1,
137
+ class: "dag-node-text",
138
+ "text-anchor": "middle", "dominant-baseline": "middle",
139
+ });
140
+ text.textContent = title.length > 16 ? title.slice(0, 14) + "…" : title;
141
+ g.appendChild(text);
142
+
143
+ // Output port (right side)
144
+ const outPort = svgEl("circle", {
145
+ cx: NODE_W, cy: NODE_H / 2, r: "6",
146
+ class: "dag-port dag-port-out",
147
+ });
148
+ g.appendChild(outPort);
149
+
150
+ // Input port (left side)
151
+ const inPort = svgEl("circle", {
152
+ cx: 0, cy: NODE_H / 2, r: "6",
153
+ class: "dag-port dag-port-in",
154
+ });
155
+ g.appendChild(inPort);
156
+
157
+ // Delete button
158
+ const del = svgEl("text", {
159
+ x: NODE_W - 8, y: 12,
160
+ class: "dag-node-delete",
161
+ "text-anchor": "middle",
162
+ });
163
+ del.textContent = "×";
164
+ del.addEventListener("click", (e) => {
165
+ e.stopPropagation();
166
+ dagNodes = dagNodes.filter(n => n.id !== node.id);
167
+ dagEdges = dagEdges.filter(de => de.from !== node.id && de.to !== node.id);
168
+ renderCanvas();
169
+ });
170
+ g.appendChild(del);
171
+
172
+ // Drag node
173
+ let dragOffset = null;
174
+ rect.addEventListener("mousedown", (e) => {
175
+ e.preventDefault();
176
+ const pt = svgPoint(e);
177
+ dragOffset = { dx: pt.x - node.x, dy: pt.y - node.y };
178
+ dragging = node.id;
179
+ });
180
+
181
+ // Start connection from output port
182
+ outPort.addEventListener("mousedown", (e) => {
183
+ e.preventDefault();
184
+ e.stopPropagation();
185
+ connecting = { fromId: node.id };
186
+ tempLine = svgEl("line", {
187
+ x1: node.x + NODE_W, y1: node.y + NODE_H / 2,
188
+ x2: node.x + NODE_W, y2: node.y + NODE_H / 2,
189
+ class: "dag-edge dag-edge-temp",
190
+ });
191
+ $.dagCanvas.appendChild(tempLine);
192
+ });
193
+
194
+ // Receive connection on input port
195
+ inPort.addEventListener("mouseup", (e) => {
196
+ if (connecting && connecting.fromId !== node.id) {
197
+ const exists = dagEdges.some(de => de.from === connecting.fromId && de.to === node.id);
198
+ if (!exists) {
199
+ dagEdges.push({ from: connecting.fromId, to: node.id });
200
+ }
201
+ }
202
+ });
203
+
204
+ $.dagCanvas.appendChild(g);
205
+ }
206
+
207
+ // Global mouse handlers for drag and connect
208
+ $.dagCanvas.onmousemove = (e) => {
209
+ const pt = svgPoint(e);
210
+ if (dragging) {
211
+ const node = dagNodes.find(n => n.id === dragging);
212
+ if (node) {
213
+ node.x = Math.max(0, pt.x - (dragOffset?.dx || NODE_W / 2));
214
+ node.y = Math.max(0, pt.y - (dragOffset?.dy || NODE_H / 2));
215
+ renderCanvas();
216
+ }
217
+ }
218
+ if (connecting && tempLine) {
219
+ tempLine.setAttribute("x2", pt.x);
220
+ tempLine.setAttribute("y2", pt.y);
221
+ }
222
+ };
223
+
224
+ $.dagCanvas.onmouseup = () => {
225
+ dragging = null;
226
+ connecting = null;
227
+ if (tempLine) {
228
+ tempLine.remove();
229
+ tempLine = null;
230
+ renderCanvas();
231
+ }
232
+ };
233
+
234
+ // Update viewBox to fit content
235
+ if (dagNodes.length > 0) {
236
+ const maxX = Math.max(...dagNodes.map(n => n.x + NODE_W + 30));
237
+ const maxY = Math.max(...dagNodes.map(n => n.y + NODE_H + 30));
238
+ $.dagCanvas.setAttribute("viewBox", `0 0 ${Math.max(maxX, 500)} ${Math.max(maxY, 300)}`);
239
+ } else {
240
+ $.dagCanvas.setAttribute("viewBox", "0 0 500 300");
241
+ }
242
+ }
243
+
244
+ // keep dragOffset accessible in onmousemove
245
+ let dragOffset = null;
246
+ const origOnMouseMove = null;
247
+
248
+ // Patch: track dragOffset globally
249
+ const origRect = null;
250
+ {
251
+ const _origRenderCanvas = renderCanvas;
252
+ // We already handle dragOffset via closure in rect.mousedown above.
253
+ // Just need to make it available to the onmousemove handler.
254
+ }
255
+
256
+ // ── Drop handler ──
257
+
258
+ $.dagCanvas?.parentElement?.addEventListener("dragover", (e) => {
259
+ e.preventDefault();
260
+ e.dataTransfer.dropEffect = "copy";
261
+ });
262
+
263
+ $.dagCanvas?.parentElement?.addEventListener("drop", (e) => {
264
+ e.preventDefault();
265
+ const agentId = e.dataTransfer.getData("text/plain");
266
+ if (!agentId) return;
267
+
268
+ const rect = $.dagCanvas.getBoundingClientRect();
269
+ const x = e.clientX - rect.left;
270
+ const y = e.clientY - rect.top;
271
+
272
+ // Scale to SVG coords
273
+ const svgRect = $.dagCanvas.viewBox.baseVal;
274
+ const scaleX = svgRect.width / rect.width;
275
+ const scaleY = svgRect.height / rect.height;
276
+
277
+ const nodeId = `n${Date.now()}`;
278
+ dagNodes.push({
279
+ id: nodeId,
280
+ agentId,
281
+ x: Math.max(0, x * scaleX - NODE_W / 2),
282
+ y: Math.max(0, y * scaleY - NODE_H / 2),
283
+ });
284
+ renderCanvas();
285
+ });
286
+
287
+ // ── Auto Layout ──
288
+
289
+ function autoLayout() {
290
+ if (dagNodes.length === 0) return;
291
+
292
+ // Simple left-to-right layout based on topological order
293
+ const inDegree = new Map();
294
+ const adj = new Map();
295
+ for (const n of dagNodes) {
296
+ inDegree.set(n.id, 0);
297
+ adj.set(n.id, []);
298
+ }
299
+ for (const e of dagEdges) {
300
+ adj.get(e.from)?.push(e.to);
301
+ inDegree.set(e.to, (inDegree.get(e.to) || 0) + 1);
302
+ }
303
+
304
+ const levels = [];
305
+ const remaining = new Set(dagNodes.map(n => n.id));
306
+ while (remaining.size > 0) {
307
+ const level = [];
308
+ for (const id of remaining) {
309
+ if (inDegree.get(id) === 0) level.push(id);
310
+ }
311
+ if (level.length === 0) {
312
+ // Remaining nodes are in a cycle — just add them
313
+ level.push(...remaining);
314
+ remaining.clear();
315
+ }
316
+ levels.push(level);
317
+ for (const id of level) {
318
+ remaining.delete(id);
319
+ for (const next of adj.get(id) || []) {
320
+ inDegree.set(next, (inDegree.get(next) || 0) - 1);
321
+ }
322
+ }
323
+ }
324
+
325
+ const xGap = 200;
326
+ const yGap = 80;
327
+ for (let li = 0; li < levels.length; li++) {
328
+ const level = levels[li];
329
+ const totalH = level.length * NODE_H + (level.length - 1) * (yGap - NODE_H);
330
+ const startY = Math.max(20, (300 - totalH) / 2);
331
+ for (let ni = 0; ni < level.length; ni++) {
332
+ const node = dagNodes.find(n => n.id === level[ni]);
333
+ if (node) {
334
+ node.x = 40 + li * xGap;
335
+ node.y = startY + ni * yGap;
336
+ }
337
+ }
338
+ }
339
+ renderCanvas();
340
+ }
341
+
342
+ // ── Save ──
343
+
344
+ async function saveDag() {
345
+ const title = $.dagFormTitle.value.trim();
346
+ if (!title) { alert("Title is required"); return; }
347
+ if (dagNodes.length < 2) { alert("A DAG needs at least 2 nodes"); return; }
348
+
349
+ const editId = $.dagFormEditId.value;
350
+ const data = {
351
+ title,
352
+ description: $.dagFormDesc.value.trim(),
353
+ nodes: dagNodes.map(n => ({ id: n.id, agentId: n.agentId, x: Math.round(n.x), y: Math.round(n.y) })),
354
+ edges: dagEdges.map(e => ({ from: e.from, to: e.to })),
355
+ };
356
+
357
+ try {
358
+ if (editId) {
359
+ await api.updateDag(editId, data);
360
+ } else {
361
+ await api.createDag(data);
362
+ }
363
+ closeDagModal();
364
+ // Reload agents panel (which includes DAGs)
365
+ const { loadAgents } = await import('./agents.js');
366
+ await loadAgents();
367
+ } catch (err) {
368
+ alert(err.message);
369
+ }
370
+ }
371
+
372
+ // ── Helpers ──
373
+
374
+ function svgEl(tag, attrs = {}) {
375
+ const el = document.createElementNS(NS, tag);
376
+ for (const [k, v] of Object.entries(attrs)) {
377
+ el.setAttribute(k, v);
378
+ }
379
+ return el;
380
+ }
381
+
382
+ function svgPoint(e) {
383
+ const rect = $.dagCanvas.getBoundingClientRect();
384
+ const vb = $.dagCanvas.viewBox.baseVal;
385
+ return {
386
+ x: (e.clientX - rect.left) * (vb.width / rect.width),
387
+ y: (e.clientY - rect.top) * (vb.height / rect.height),
388
+ };
389
+ }
390
+
391
+ // ── Event Bindings ──
392
+
393
+ $.dagModalClose?.addEventListener("click", closeDagModal);
394
+ $.dagModalCancel?.addEventListener("click", closeDagModal);
395
+ $.dagModal?.addEventListener("click", (e) => {
396
+ if (e.target === $.dagModal) closeDagModal();
397
+ });
398
+ $.dagModalSave?.addEventListener("click", saveDag);
399
+ $.dagAutoLayout?.addEventListener("click", autoLayout);
@@ -0,0 +1,46 @@
1
+ // Easter Egg — Click Whaly 5 times for a surprise greeting
2
+ const CLICKS_NEEDED = 5;
3
+ const CLICK_WINDOW = 2000;
4
+ const BUBBLE_DURATION = 8000;
5
+
6
+ let clickTimestamps = [];
7
+ let eggActive = false;
8
+
9
+ // Delegate click on Whaly image (it's dynamically created)
10
+ document.addEventListener('click', (e) => {
11
+ const img = e.target.closest('.whaly-placeholder img');
12
+ if (!img || eggActive) return;
13
+
14
+ const now = Date.now();
15
+ clickTimestamps.push(now);
16
+ clickTimestamps = clickTimestamps.filter(t => now - t < CLICK_WINDOW);
17
+
18
+ if (clickTimestamps.length >= CLICKS_NEEDED) {
19
+ clickTimestamps = [];
20
+ activateEgg(img);
21
+ }
22
+ });
23
+
24
+ function activateEgg(img) {
25
+ eggActive = true;
26
+ const placeholder = img.closest('.whaly-placeholder');
27
+
28
+ // Whaly wiggle
29
+ img.classList.add('whaly-wiggle');
30
+ img.addEventListener('animationend', () => img.classList.remove('whaly-wiggle'), { once: true });
31
+
32
+ // Chat bubble
33
+ const bubble = document.createElement('div');
34
+ bubble.className = 'whaly-bubble';
35
+ bubble.innerHTML = '🐋 <strong>Whaly</strong>: Oi! Stop poking me! I was napping between deploys. Yes, I live here. No, I don\'t know why your build failed. Have you tried turning Claude off and on again? ...just kidding, please don\'t. I\'m not paid enough for that. 💤';
36
+ placeholder.appendChild(bubble);
37
+
38
+ // Remove after duration
39
+ setTimeout(() => {
40
+ bubble.classList.add('whaly-bubble-out');
41
+ bubble.addEventListener('animationend', () => {
42
+ bubble.remove();
43
+ eggActive = false;
44
+ }, { once: true });
45
+ }, BUBBLE_DURATION);
46
+ }