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,589 @@
1
+ // Send/Stop logic + message handler + boot sequence
2
+ import { $ } from '../core/dom.js';
3
+ import { getState, setState } from '../core/store.js';
4
+ import { CHAT_IDS, BOT_CHAT_ID } from '../core/constants.js';
5
+ import { on } from '../core/events.js';
6
+ import { commandRegistry, dismissAutocomplete, handleAutocompleteKeydown, handleSlashAutocomplete, registerCommand } from '../ui/commands.js';
7
+ import { addUserMessage, appendAssistantText, appendToolIndicator, appendToolResult, showThinking, removeThinking, addResultSummary, addStatus, showWhalyPlaceholder } from '../ui/messages.js';
8
+ import { getPane, panes, _setChatFns } from '../ui/parallel.js';
9
+ import { loadSessions } from './sessions.js';
10
+ import { loadStats, loadAccountInfo } from './cost-dashboard.js';
11
+ import { loadProjects } from './projects.js';
12
+ import { loadPrompts } from './prompts.js';
13
+ import { loadWorkflows } from './workflows.js';
14
+ import { loadAgents, handleAgentMessage } from './agents.js';
15
+ import './agent-monitor.js';
16
+ import { connectWebSocket } from '../core/ws.js';
17
+ import { updateAttachmentBadge, getImageAttachments, clearImageAttachments } from './attachments.js';
18
+ import { applyTheme } from '../ui/theme.js';
19
+ import { exportAsMarkdown, exportAsHtml } from '../ui/export.js';
20
+ import * as api from '../core/api.js';
21
+ import { isBackgroundSession, removeBackgroundSession, showCompletionToast, showErrorToast, showInputNeededToast, reconcileBackgroundSessions } from './background-sessions.js';
22
+ import { enqueuePermissionRequest, getPermissionMode, clearSessionPermissions, handleExternalPermissionResponse } from '../ui/permissions.js';
23
+ import { getSelectedModel } from '../ui/model-selector.js';
24
+ import { getMaxTurns } from '../ui/max-turns.js';
25
+ import { getDisabledTools } from '../ui/disabled-tools.js';
26
+ import { updateContextGauge, resetContextGauge, loadContextGauge } from '../ui/context-gauge.js';
27
+
28
+ // ── "Waiting for input" indicator ──
29
+ const inputWaitingEl = document.getElementById("input-waiting");
30
+
31
+ function isQuestionText(text) {
32
+ if (!text) return false;
33
+ // Get the last meaningful line (skip empty lines, code blocks, lists)
34
+ const lines = text.trim().split('\n').filter(l => l.trim());
35
+ const last = lines[lines.length - 1]?.trim() || '';
36
+ // Check if it ends with a question mark (ignoring trailing markdown/whitespace)
37
+ return /\?\s*[`*_)}\]]*\s*$/.test(last);
38
+ }
39
+
40
+ function showWaitingForInput(pane) {
41
+ pane = pane || getPane(null);
42
+ const parallelMode = getState("parallelMode");
43
+
44
+ if (inputWaitingEl) inputWaitingEl.classList.remove("hidden");
45
+
46
+ const inputBar = parallelMode
47
+ ? pane.messageInput?.closest('.input-bar')
48
+ : $.messageInput?.closest('.input-bar');
49
+ if (inputBar) inputBar.classList.add("waiting-for-input");
50
+ }
51
+
52
+ function hideWaitingForInput(pane) {
53
+ pane = pane || getPane(null);
54
+ const parallelMode = getState("parallelMode");
55
+
56
+ if (inputWaitingEl) inputWaitingEl.classList.add("hidden");
57
+
58
+ const inputBar = parallelMode
59
+ ? pane.messageInput?.closest('.input-bar')
60
+ : $.messageInput?.closest('.input-bar');
61
+ if (inputBar) inputBar.classList.remove("waiting-for-input");
62
+ }
63
+
64
+ export function sendMessage(pane) {
65
+ pane = pane || getPane(null);
66
+ const text = pane.messageInput.value.trim();
67
+ const cwd = $.projectSelect.value;
68
+
69
+ if (!text || !cwd) {
70
+ if (text && text.startsWith("/")) {
71
+ const match = text.match(/^\/(\S+)\s*(.*)/s);
72
+ if (match) {
73
+ const [, cmdName, args] = match;
74
+ const cmd = commandRegistry[cmdName];
75
+ if (cmd) {
76
+ if (cmdName === "run" && !cwd) {
77
+ // /run needs a project, fall through
78
+ } else {
79
+ pane.messageInput.value = "";
80
+ pane.messageInput.style.height = "auto";
81
+ dismissAutocomplete(pane);
82
+ cmd.execute(args, pane);
83
+ return;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ if (!cwd) {
89
+ $.projectSelect.focus();
90
+ $.projectSelect.style.borderColor = "var(--error)";
91
+ setTimeout(() => ($.projectSelect.style.borderColor = ""), 2000);
92
+ }
93
+ return;
94
+ }
95
+
96
+ const ws = getState("ws");
97
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
98
+ addStatus("Not connected. Reconnecting...", true, pane);
99
+ connectWebSocket();
100
+ return;
101
+ }
102
+
103
+ // Slash command intercept
104
+ if (text.startsWith("/")) {
105
+ const match = text.match(/^\/(\S+)\s*(.*)/s);
106
+ if (match) {
107
+ const [, cmdName, args] = match;
108
+ const cmd = commandRegistry[cmdName];
109
+ if (cmd) {
110
+ pane.messageInput.value = "";
111
+ pane.messageInput.style.height = "auto";
112
+ dismissAutocomplete(pane);
113
+ cmd.execute(args, pane);
114
+ return;
115
+ }
116
+ }
117
+ }
118
+
119
+ // Prepend attached files
120
+ let fullMessage = text;
121
+ const attachedFiles = getState("attachedFiles");
122
+ if (attachedFiles.length > 0) {
123
+ const fileBlocks = attachedFiles.map(
124
+ (f) => `<file path="${f.path}">\n${f.content}\n</file>`
125
+ ).join("\n\n");
126
+ fullMessage = fileBlocks + "\n\n" + text;
127
+ }
128
+
129
+ const images = getImageAttachments();
130
+ const filePaths = attachedFiles.map(f => f.path);
131
+ addUserMessage(text, pane, images, filePaths);
132
+ pane.messageInput.value = "";
133
+ pane.messageInput.style.height = "auto";
134
+ setState("streamingCharCount", 0);
135
+
136
+ // Clear attachments
137
+ if (attachedFiles.length > 0) {
138
+ setState("attachedFiles", []);
139
+ updateAttachmentBadge();
140
+ }
141
+ if (images.length > 0) {
142
+ clearImageAttachments();
143
+ }
144
+
145
+ hideWaitingForInput(pane);
146
+ pane.isStreaming = true;
147
+ const parallelMode = getState("parallelMode");
148
+
149
+ if (parallelMode) {
150
+ pane.sendBtn.classList.add("hidden");
151
+ pane.stopBtn.classList.remove("hidden");
152
+ } else {
153
+ $.sendBtn.classList.add("hidden");
154
+ $.stopBtn.classList.remove("hidden");
155
+ }
156
+
157
+ const selectedOption = $.projectSelect.options[$.projectSelect.selectedIndex];
158
+ const projectName = selectedOption?.textContent || "Session";
159
+
160
+ const model = getSelectedModel();
161
+ const payload = {
162
+ type: "chat",
163
+ message: fullMessage,
164
+ cwd,
165
+ sessionId: getState("sessionId"),
166
+ projectName,
167
+ permissionMode: getPermissionMode(),
168
+ };
169
+ if (images.length > 0) {
170
+ payload.images = images.map(({ name, data, mimeType }) => ({ name, data, mimeType }));
171
+ }
172
+ if (model) payload.model = model;
173
+ const maxTurns = getMaxTurns();
174
+ if (maxTurns) payload.maxTurns = maxTurns;
175
+ const disabledTools = getDisabledTools();
176
+ if (disabledTools.length > 0) payload.disabledTools = disabledTools;
177
+
178
+ if (parallelMode && pane.chatId) {
179
+ payload.chatId = pane.chatId;
180
+ }
181
+
182
+ ws.send(JSON.stringify(payload));
183
+ showThinking("Connecting to Claude...", pane);
184
+ }
185
+
186
+ export function stopGeneration(pane) {
187
+ pane = pane || getPane(null);
188
+ const ws = getState("ws");
189
+ if (ws && ws.readyState === WebSocket.OPEN) {
190
+ const payload = { type: "abort" };
191
+ const parallelMode = getState("parallelMode");
192
+ if (parallelMode && pane.chatId) {
193
+ payload.chatId = pane.chatId;
194
+ }
195
+ ws.send(JSON.stringify(payload));
196
+ }
197
+ }
198
+
199
+ export function finishStreamingHandler(pane) {
200
+ pane = pane || getPane(null);
201
+ pane.isStreaming = false;
202
+ pane.currentAssistantMsg = null;
203
+ removeThinking(pane);
204
+
205
+ if ($.streamingTokens) $.streamingTokens.classList.add("hidden");
206
+ if ($.streamingTokensSep) $.streamingTokensSep.classList.add("hidden");
207
+
208
+ const parallelMode = getState("parallelMode");
209
+ if (parallelMode) {
210
+ pane.sendBtn.classList.remove("hidden");
211
+ pane.stopBtn.classList.add("hidden");
212
+ pane.messageInput.focus();
213
+ if (pane.statusEl) {
214
+ pane.statusEl.textContent = "idle";
215
+ pane.statusEl.className = "chat-pane-status";
216
+ }
217
+ } else {
218
+ $.sendBtn.classList.remove("hidden");
219
+ $.stopBtn.classList.add("hidden");
220
+ $.sendBtn.disabled = false;
221
+ $.messageInput.focus();
222
+ }
223
+ }
224
+
225
+ // Register the chat functions with parallel.js to break circular dependency
226
+ _setChatFns({ sendMessage, stopGeneration });
227
+
228
+ // Handle WebSocket messages
229
+ function handleServerMessage(msg) {
230
+ // Ignore assistant-bot messages — handled by assistant-bot.js
231
+ if (msg.chatId === BOT_CHAT_ID) return;
232
+
233
+ // Route background session messages — skip rendering, only handle terminal states
234
+ // Permission requests must pass through so the user can approve/deny tools
235
+ if (msg.sessionId && isBackgroundSession(msg.sessionId)) {
236
+ if (msg.type === "permission_request") {
237
+ const bgMap = getState("backgroundSessions");
238
+ const bgInfo = bgMap.get(msg.sessionId);
239
+ msg._bgSessionTitle = bgInfo?.title || "Background session";
240
+ enqueuePermissionRequest(msg);
241
+ return;
242
+ }
243
+ if (msg.type === "permission_response_external") {
244
+ handleExternalPermissionResponse(msg.id, msg.behavior);
245
+ return;
246
+ }
247
+ // Track last assistant text for question detection
248
+ if (msg.type === "text") {
249
+ const bgMap = getState("backgroundSessions");
250
+ const info = bgMap.get(msg.sessionId);
251
+ if (info) info._lastText = (info._lastText || '') + msg.text;
252
+ }
253
+ if (msg.type === "done") {
254
+ const bgMap = getState("backgroundSessions");
255
+ const info = bgMap.get(msg.sessionId);
256
+ const title = info?.title || "Background session";
257
+ const projectPath = info?.projectPath || "";
258
+ if (info?._lastText && isQuestionText(info._lastText)) {
259
+ showInputNeededToast(msg.sessionId, title, projectPath);
260
+ } else {
261
+ showCompletionToast(msg.sessionId, title, projectPath);
262
+ }
263
+ removeBackgroundSession(msg.sessionId);
264
+ loadSessions();
265
+ }
266
+ if (msg.type === "error") {
267
+ const bgMap = getState("backgroundSessions");
268
+ const info = bgMap.get(msg.sessionId);
269
+ const title = info?.title || "Background session";
270
+ showErrorToast(msg.sessionId, title, msg.error || "Unknown error");
271
+ removeBackgroundSession(msg.sessionId);
272
+ loadSessions();
273
+ }
274
+ // Silently ignore all other message types — server saves to DB
275
+ return;
276
+ }
277
+
278
+ // Drop messages from a stale session that isn't the active one
279
+ // (and wasn't explicitly backgrounded — those are caught above).
280
+ // Allow: "session" (sets the new id), "permission_request", workflow messages.
281
+ const currentSessionId = getState("sessionId");
282
+ if (
283
+ msg.sessionId &&
284
+ msg.sessionId !== currentSessionId &&
285
+ msg.type !== "session" &&
286
+ msg.type !== "permission_request"
287
+ ) {
288
+ // Server already saved to DB — safe to discard on the client
289
+ return;
290
+ }
291
+
292
+ const pane = getPane(msg.chatId || null);
293
+ removeThinking(pane);
294
+
295
+ switch (msg.type) {
296
+ case "session":
297
+ setState("sessionId", msg.sessionId);
298
+ resetContextGauge();
299
+ hideWaitingForInput(pane);
300
+ loadSessions();
301
+ showThinking("Thinking...", pane);
302
+ break;
303
+
304
+ case "text":
305
+ appendAssistantText(msg.text, pane);
306
+ break;
307
+
308
+ case "tool":
309
+ appendToolIndicator(msg.name, msg.input, pane, msg.id);
310
+ showThinking(`Running ${msg.name}...`, pane);
311
+ break;
312
+
313
+ case "tool_result":
314
+ appendToolResult(msg.toolUseId, msg.content, msg.isError, pane);
315
+ showThinking("Thinking...", pane);
316
+ break;
317
+
318
+ case "result":
319
+ removeThinking(pane);
320
+ addResultSummary(msg, pane);
321
+ updateContextGauge(msg.input_tokens, msg.output_tokens, msg.cache_read_tokens, msg.cache_creation_tokens);
322
+ if (msg.totalCost != null) {
323
+ $.totalCostEl.textContent = "$" + msg.totalCost.toFixed(2);
324
+ }
325
+ loadStats();
326
+ if ($.streamingTokens) $.streamingTokens.classList.add("hidden");
327
+ if ($.streamingTokensSep) $.streamingTokensSep.classList.add("hidden");
328
+ break;
329
+
330
+ case "done": {
331
+ // Check if the last assistant message ends with a question
332
+ const lastMsg = pane.currentAssistantMsg;
333
+ const rawText = lastMsg?.dataset?.raw || lastMsg?.textContent || '';
334
+ finishStreamingHandler(pane);
335
+ if (isQuestionText(rawText)) {
336
+ showWaitingForInput(pane);
337
+ }
338
+ break;
339
+ }
340
+
341
+ case "aborted":
342
+ finishStreamingHandler(pane);
343
+ addStatus("Aborted", false, pane);
344
+ break;
345
+
346
+ case "error":
347
+ finishStreamingHandler(pane);
348
+ addStatus("Error: " + msg.error, true, pane);
349
+ break;
350
+
351
+ case "workflow_started":
352
+ showThinking(`Workflow: ${msg.workflow?.title || "Running"}...`, pane);
353
+ break;
354
+
355
+ case "workflow_step": {
356
+ const dot = document.querySelector(`.workflow-step[data-step="${msg.stepIndex}"] .workflow-step-dot`);
357
+ if (dot) {
358
+ dot.className = `workflow-step-dot ${msg.status}`;
359
+ }
360
+ if (msg.status === "running") {
361
+ const label = document.querySelector(`.workflow-step[data-step="${msg.stepIndex}"] .workflow-step-label`);
362
+ showThinking(`Running: ${label?.textContent || "step"}...`, pane);
363
+ }
364
+ break;
365
+ }
366
+
367
+ case "workflow_completed":
368
+ removeThinking(pane);
369
+ addStatus(msg.aborted ? "Workflow aborted" : "Workflow completed", !!msg.aborted, pane);
370
+ break;
371
+
372
+ case "agent_started":
373
+ case "agent_progress":
374
+ case "agent_completed":
375
+ case "agent_error":
376
+ case "agent_aborted":
377
+ case "agent_chain_started":
378
+ case "agent_chain_step":
379
+ case "agent_chain_completed":
380
+ case "orchestrator_started":
381
+ case "orchestrator_phase":
382
+ case "orchestrator_dispatching":
383
+ case "orchestrator_dispatch":
384
+ case "orchestrator_dispatch_skip":
385
+ case "orchestrator_error":
386
+ case "orchestrator_completed":
387
+ case "dag_started":
388
+ case "dag_level":
389
+ case "dag_node":
390
+ case "dag_completed":
391
+ case "dag_error":
392
+ handleAgentMessage(msg, pane);
393
+ break;
394
+
395
+ case "permission_request":
396
+ enqueuePermissionRequest(msg);
397
+ break;
398
+
399
+ case "permission_response_external":
400
+ handleExternalPermissionResponse(msg.id, msg.behavior);
401
+ break;
402
+ }
403
+ }
404
+
405
+ // Listen for WebSocket messages via event bus
406
+ on("ws:message", handleServerMessage);
407
+
408
+ // ── Background session reconciliation ──
409
+ // Shared helper — reconcile bg sessions against the server's active list.
410
+ async function reconcileBgSessionsFromServer() {
411
+ try {
412
+ const activeSessionIds = await api.fetchActiveSessionIds();
413
+ reconcileBackgroundSessions(activeSessionIds);
414
+ } catch (err) {
415
+ console.error("Background session reconciliation failed:", err);
416
+ }
417
+ }
418
+
419
+ // Reconnect state sync — recover from connection drops
420
+ on("ws:reconnected", async () => {
421
+ console.log("WebSocket reconnected — syncing state...");
422
+ try {
423
+ // 1. Reconcile background sessions
424
+ await reconcileBgSessionsFromServer();
425
+
426
+ // 2. If any foreground pane was streaming, reset it and reload from DB
427
+ for (const pane of panes.values()) {
428
+ if (pane.isStreaming) {
429
+ finishStreamingHandler(pane);
430
+ }
431
+ }
432
+
433
+ const currentSessionId = getState("sessionId");
434
+ if (currentSessionId) {
435
+ const { loadMessages } = await import('./sessions.js');
436
+ await loadMessages(currentSessionId);
437
+ }
438
+
439
+ // 3. Refresh session list
440
+ loadSessions();
441
+ } catch (err) {
442
+ console.error("Reconnect sync failed:", err);
443
+ }
444
+ });
445
+
446
+ // Initial connect — reconcile stale bg sessions from localStorage (PWA cold start)
447
+ on("ws:connected", () => {
448
+ reconcileBgSessionsFromServer();
449
+ });
450
+
451
+ // PWA visibility — reconcile when app returns to foreground.
452
+ // Handles cases where the WS stayed "connected" but messages were lost
453
+ // while the PWA was suspended by the OS.
454
+ document.addEventListener("visibilitychange", () => {
455
+ if (document.visibilityState === "visible" && getState("ws")?.readyState === WebSocket.OPEN) {
456
+ reconcileBgSessionsFromServer();
457
+ }
458
+ });
459
+
460
+ // Register built-in commands
461
+ registerCommand("clear", {
462
+ category: "app",
463
+ description: "Clear current pane messages",
464
+ execute(args, pane) {
465
+ pane.messagesDiv.innerHTML = "";
466
+ pane.currentAssistantMsg = null;
467
+ },
468
+ });
469
+
470
+ registerCommand("new", {
471
+ category: "app",
472
+ description: "Start a new session",
473
+ execute() {
474
+ clearSessionPermissions();
475
+ $.newSessionBtn.click();
476
+ },
477
+ });
478
+
479
+ registerCommand("parallel", {
480
+ category: "app",
481
+ description: "Toggle parallel mode",
482
+ execute() {
483
+ $.toggleParallelBtn.checked = !$.toggleParallelBtn.checked;
484
+ $.toggleParallelBtn.dispatchEvent(new Event("change"));
485
+ },
486
+ });
487
+
488
+ registerCommand("export", {
489
+ category: "app",
490
+ description: "Download chat (/export md or /export html)",
491
+ execute(args, pane) {
492
+ const format = args.trim().toLowerCase() || "md";
493
+ const msgs = pane.messagesDiv.querySelectorAll(".msg");
494
+ if (format === "html") {
495
+ exportAsHtml(msgs);
496
+ } else {
497
+ exportAsMarkdown(msgs);
498
+ }
499
+ },
500
+ });
501
+
502
+ registerCommand("help", {
503
+ category: "app",
504
+ description: "Show all available commands",
505
+ execute(args, pane) {
506
+ const grouped = { app: [], cli: [], agent: [], workflow: [], prompt: [] };
507
+ for (const [name, cmd] of Object.entries(commandRegistry)) {
508
+ (grouped[cmd.category] || []).push({ name, ...cmd });
509
+ }
510
+ let text = "Available commands:\n";
511
+ for (const [cat, cmds] of Object.entries(grouped)) {
512
+ if (cmds.length === 0) continue;
513
+ text += `\n[${cat.toUpperCase()}]\n`;
514
+ cmds.forEach((c) => (text += ` /${c.name} — ${c.description}\n`));
515
+ }
516
+ addStatus(text, false, pane);
517
+ },
518
+ });
519
+
520
+ registerCommand("run", {
521
+ category: "cli",
522
+ description: "Run a shell command on the server",
523
+ async execute(args, pane) {
524
+ if (!args.trim()) {
525
+ addStatus("Usage: /run <command>", true, pane);
526
+ return;
527
+ }
528
+ const cwd = $.projectSelect.value || undefined;
529
+ addStatus("Running: " + args, false, pane);
530
+ try {
531
+ const data = await api.execCommand(args, cwd);
532
+ // Inline CLI output rendering
533
+ const { appendCliOutput } = await import('../ui/messages.js');
534
+ appendCliOutput(data, pane);
535
+ } catch (err) {
536
+ addStatus("Exec error: " + err.message, true, pane);
537
+ }
538
+ },
539
+ });
540
+
541
+ registerCommand("system-prompt", {
542
+ category: "app",
543
+ description: "Edit system prompt for current project",
544
+ execute() {
545
+ import('./projects.js').then(({ openSystemPromptModal }) => openSystemPromptModal());
546
+ },
547
+ });
548
+
549
+ registerCommand("theme", {
550
+ category: "app",
551
+ description: "Toggle dark/light theme",
552
+ execute() {
553
+ const current = document.documentElement.getAttribute("data-theme") || "dark";
554
+ applyTheme(current === "dark" ? "light" : "dark");
555
+ },
556
+ });
557
+
558
+ // Event listeners — single mode
559
+ $.sendBtn.addEventListener("click", () => sendMessage(getPane(null)));
560
+ $.stopBtn.addEventListener("click", () => stopGeneration(getPane(null)));
561
+
562
+ $.messageInput.addEventListener("keydown", (e) => {
563
+ if (handleAutocompleteKeydown(e, getPane(null))) return;
564
+ if (e.key === "Enter" && !e.shiftKey) {
565
+ e.preventDefault();
566
+ sendMessage(getPane(null));
567
+ }
568
+ });
569
+
570
+ $.messageInput.addEventListener("input", () => {
571
+ $.messageInput.style.height = "auto";
572
+ $.messageInput.style.height = Math.min($.messageInput.scrollHeight, 200) + "px";
573
+ handleSlashAutocomplete(getPane(null));
574
+ });
575
+
576
+ // Initialize mermaid
577
+ if (typeof mermaid !== "undefined") {
578
+ mermaid.initialize({ startOnLoad: false, theme: "dark" });
579
+ }
580
+
581
+ // ── Boot sequence ──
582
+ showWhalyPlaceholder();
583
+ loadProjects(); // loadSessions() is called inside loadProjects() after dropdown is populated
584
+ loadAccountInfo();
585
+ loadStats();
586
+ loadPrompts();
587
+ connectWebSocket();
588
+ loadWorkflows();
589
+ loadAgents();