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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/cli.js +2 -0
- package/config/agent-chains.json +16 -0
- package/config/agent-dags.json +16 -0
- package/config/agents.json +46 -0
- package/config/bot-prompt.json +3 -0
- package/config/folders.json +66 -0
- package/config/prompts.json +92 -0
- package/config/repos.json +86 -0
- package/config/telegram-config.json +17 -0
- package/config/workflows.json +90 -0
- package/db.js +1198 -0
- package/package.json +55 -0
- package/plugins/claude-editor/client.css +171 -0
- package/plugins/claude-editor/client.js +183 -0
- package/plugins/event-stream/client.css +207 -0
- package/plugins/event-stream/client.js +271 -0
- package/plugins/linear/client.css +345 -0
- package/plugins/linear/client.js +380 -0
- package/plugins/linear/config.json +5 -0
- package/plugins/linear/server.js +312 -0
- package/plugins/repos/client.css +549 -0
- package/plugins/repos/client.js +663 -0
- package/plugins/repos/server.js +232 -0
- package/plugins/sudoku/client.css +196 -0
- package/plugins/sudoku/client.js +329 -0
- package/plugins/tasks/client.css +414 -0
- package/plugins/tasks/client.js +394 -0
- package/plugins/tasks/server.js +116 -0
- package/plugins/tic-tac-toe/client.css +167 -0
- package/plugins/tic-tac-toe/client.js +241 -0
- package/public/css/core/components.css +232 -0
- package/public/css/core/layout.css +330 -0
- package/public/css/core/print.css +18 -0
- package/public/css/core/reset.css +36 -0
- package/public/css/core/responsive.css +378 -0
- package/public/css/core/theme.css +116 -0
- package/public/css/core/variables.css +93 -0
- package/public/css/features/agent-monitor.css +297 -0
- package/public/css/features/agent-sidebar.css +525 -0
- package/public/css/features/agents.css +996 -0
- package/public/css/features/analytics.css +181 -0
- package/public/css/features/background-sessions.css +321 -0
- package/public/css/features/cost-dashboard.css +168 -0
- package/public/css/features/home.css +313 -0
- package/public/css/features/retro-terminal.css +88 -0
- package/public/css/features/telegram.css +127 -0
- package/public/css/features/tour.css +148 -0
- package/public/css/features/voice-input.css +60 -0
- package/public/css/features/welcome.css +241 -0
- package/public/css/panels/assistant-bot.css +442 -0
- package/public/css/panels/dev-docs.css +292 -0
- package/public/css/panels/file-explorer.css +322 -0
- package/public/css/panels/git-panel.css +221 -0
- package/public/css/panels/mcp-manager.css +199 -0
- package/public/css/panels/tips-feed.css +353 -0
- package/public/css/ui/commands.css +273 -0
- package/public/css/ui/context-gauge.css +76 -0
- package/public/css/ui/file-picker.css +69 -0
- package/public/css/ui/image-attachments.css +106 -0
- package/public/css/ui/messages.css +884 -0
- package/public/css/ui/modals.css +122 -0
- package/public/css/ui/parallel.css +217 -0
- package/public/css/ui/permissions.css +110 -0
- package/public/css/ui/right-panel.css +481 -0
- package/public/css/ui/sessions.css +689 -0
- package/public/css/ui/status-bar.css +425 -0
- package/public/css/ui/toolbox.css +206 -0
- package/public/data/tips.json +218 -0
- package/public/icons/favicon.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/whaly.png +0 -0
- package/public/index.html +1140 -0
- package/public/js/core/api.js +591 -0
- package/public/js/core/constants.js +3 -0
- package/public/js/core/dom.js +270 -0
- package/public/js/core/events.js +10 -0
- package/public/js/core/plugin-loader.js +153 -0
- package/public/js/core/store.js +39 -0
- package/public/js/core/utils.js +25 -0
- package/public/js/core/ws.js +64 -0
- package/public/js/features/agent-monitor.js +222 -0
- package/public/js/features/agents.js +1209 -0
- package/public/js/features/analytics.js +397 -0
- package/public/js/features/attachments.js +251 -0
- package/public/js/features/background-sessions.js +475 -0
- package/public/js/features/chat.js +589 -0
- package/public/js/features/cost-dashboard.js +152 -0
- package/public/js/features/dag-editor.js +399 -0
- package/public/js/features/easter-egg.js +46 -0
- package/public/js/features/home.js +270 -0
- package/public/js/features/projects.js +372 -0
- package/public/js/features/prompts.js +228 -0
- package/public/js/features/sessions.js +332 -0
- package/public/js/features/telegram.js +131 -0
- package/public/js/features/tour.js +210 -0
- package/public/js/features/voice-input.js +185 -0
- package/public/js/features/welcome.js +43 -0
- package/public/js/features/workflows.js +277 -0
- package/public/js/main.js +51 -0
- package/public/js/panels/assistant-bot.js +445 -0
- package/public/js/panels/dev-docs.js +380 -0
- package/public/js/panels/file-explorer.js +486 -0
- package/public/js/panels/git-panel.js +285 -0
- package/public/js/panels/mcp-manager.js +311 -0
- package/public/js/panels/tips-feed.js +303 -0
- package/public/js/ui/commands.js +114 -0
- package/public/js/ui/context-gauge.js +100 -0
- package/public/js/ui/diff.js +124 -0
- package/public/js/ui/disabled-tools.js +36 -0
- package/public/js/ui/export.js +74 -0
- package/public/js/ui/formatting.js +206 -0
- package/public/js/ui/header-dropdowns.js +72 -0
- package/public/js/ui/input-meta.js +71 -0
- package/public/js/ui/max-turns.js +21 -0
- package/public/js/ui/messages.js +387 -0
- package/public/js/ui/model-selector.js +20 -0
- package/public/js/ui/notifications.js +232 -0
- package/public/js/ui/parallel.js +176 -0
- package/public/js/ui/permissions.js +168 -0
- package/public/js/ui/right-panel.js +173 -0
- package/public/js/ui/shortcuts.js +143 -0
- package/public/js/ui/sidebar-toggle.js +29 -0
- package/public/js/ui/status-bar.js +172 -0
- package/public/js/ui/tab-sdk.js +623 -0
- package/public/js/ui/theme.js +38 -0
- package/public/manifest.json +13 -0
- package/public/offline.html +190 -0
- package/public/style.css +42 -0
- package/public/sw.js +91 -0
- package/server/agent-loop.js +385 -0
- package/server/dag-executor.js +265 -0
- package/server/orchestrator.js +514 -0
- package/server/paths.js +61 -0
- package/server/plugin-mount.js +56 -0
- package/server/push-sender.js +31 -0
- package/server/routes/agents.js +294 -0
- package/server/routes/bot.js +45 -0
- package/server/routes/exec.js +35 -0
- package/server/routes/files.js +218 -0
- package/server/routes/mcp.js +82 -0
- package/server/routes/messages.js +36 -0
- package/server/routes/notifications.js +37 -0
- package/server/routes/projects.js +207 -0
- package/server/routes/prompts.js +53 -0
- package/server/routes/sessions.js +103 -0
- package/server/routes/stats.js +143 -0
- package/server/routes/telegram.js +71 -0
- package/server/routes/tips.js +135 -0
- package/server/routes/workflows.js +81 -0
- package/server/summarizer.js +55 -0
- package/server/telegram-poller.js +205 -0
- package/server/telegram-sender.js +304 -0
- package/server/ws-handler.js +926 -0
- 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();
|