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,387 @@
|
|
|
1
|
+
// Message rendering
|
|
2
|
+
import { escapeHtml, getToolDetail, scrollToBottom } from '../core/utils.js';
|
|
3
|
+
import { renderMarkdown, highlightCodeBlocks, addCopyButtons, renderMermaidBlocks } from './formatting.js';
|
|
4
|
+
import { renderDiffView, renderAdditionsView } from './diff.js';
|
|
5
|
+
import { getState, setState } from '../core/store.js';
|
|
6
|
+
import { $ } from '../core/dom.js';
|
|
7
|
+
import { getPane } from './parallel.js';
|
|
8
|
+
|
|
9
|
+
export function showWhalyPlaceholder(pane) {
|
|
10
|
+
pane = pane || getPane(null);
|
|
11
|
+
removeWhalyPlaceholder(pane);
|
|
12
|
+
const el = document.createElement("div");
|
|
13
|
+
el.className = "whaly-placeholder";
|
|
14
|
+
el.innerHTML = `<img src="/icons/whaly.png" alt="Whaly" draggable="false"><div class="whaly-text">~ start chatting with claude ~</div><div class="whaly-hint">Type a message or select a prompt template</div>`;
|
|
15
|
+
pane.messagesDiv.appendChild(el);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function removeWhalyPlaceholder(pane) {
|
|
19
|
+
pane = pane || getPane(null);
|
|
20
|
+
const existing = pane.messagesDiv.querySelector(".whaly-placeholder");
|
|
21
|
+
if (existing) existing.remove();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function addUserMessage(text, pane, images = [], filePaths = []) {
|
|
25
|
+
pane = pane || getPane(null);
|
|
26
|
+
removeWhalyPlaceholder(pane);
|
|
27
|
+
pane.currentAssistantMsg = null;
|
|
28
|
+
const div = document.createElement("div");
|
|
29
|
+
div.className = "msg msg-user";
|
|
30
|
+
|
|
31
|
+
const label = document.createElement("span");
|
|
32
|
+
label.className = "msg-user-label";
|
|
33
|
+
label.textContent = "YOU";
|
|
34
|
+
|
|
35
|
+
div.appendChild(label);
|
|
36
|
+
|
|
37
|
+
if (filePaths && filePaths.length > 0) {
|
|
38
|
+
const filesDiv = document.createElement("div");
|
|
39
|
+
filesDiv.className = "msg-user-files";
|
|
40
|
+
for (const fp of filePaths) {
|
|
41
|
+
const fileTag = document.createElement("span");
|
|
42
|
+
fileTag.className = "msg-user-file-tag";
|
|
43
|
+
fileTag.textContent = fp;
|
|
44
|
+
fileTag.title = fp;
|
|
45
|
+
filesDiv.appendChild(fileTag);
|
|
46
|
+
}
|
|
47
|
+
div.appendChild(filesDiv);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const body = document.createElement("span");
|
|
51
|
+
body.className = "msg-user-body";
|
|
52
|
+
body.textContent = text;
|
|
53
|
+
|
|
54
|
+
div.appendChild(body);
|
|
55
|
+
|
|
56
|
+
if (images && images.length > 0) {
|
|
57
|
+
renderChatImages(images, div);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pane.messagesDiv.appendChild(div);
|
|
61
|
+
scrollToBottom(pane);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderChatImages(images, container) {
|
|
65
|
+
const strip = document.createElement("div");
|
|
66
|
+
strip.className = "chat-image-strip";
|
|
67
|
+
|
|
68
|
+
for (const img of images) {
|
|
69
|
+
const imgEl = document.createElement("img");
|
|
70
|
+
imgEl.className = "chat-image-thumb";
|
|
71
|
+
imgEl.src = `data:${img.mimeType};base64,${img.data}`;
|
|
72
|
+
imgEl.alt = img.name || "attached image";
|
|
73
|
+
imgEl.title = img.name || "attached image";
|
|
74
|
+
imgEl.addEventListener("click", () => {
|
|
75
|
+
const overlay = document.createElement("div");
|
|
76
|
+
overlay.className = "chat-image-overlay";
|
|
77
|
+
const fullImg = document.createElement("img");
|
|
78
|
+
fullImg.src = imgEl.src;
|
|
79
|
+
overlay.appendChild(fullImg);
|
|
80
|
+
overlay.addEventListener("click", () => overlay.remove());
|
|
81
|
+
document.body.appendChild(overlay);
|
|
82
|
+
});
|
|
83
|
+
strip.appendChild(imgEl);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
container.appendChild(strip);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function appendAssistantText(text, pane) {
|
|
90
|
+
pane = pane || getPane(null);
|
|
91
|
+
if (!pane.currentAssistantMsg) {
|
|
92
|
+
const div = document.createElement("div");
|
|
93
|
+
div.className = "msg msg-assistant";
|
|
94
|
+
const content = document.createElement("div");
|
|
95
|
+
content.className = "text-content";
|
|
96
|
+
div.appendChild(content);
|
|
97
|
+
pane.messagesDiv.appendChild(div);
|
|
98
|
+
pane.currentAssistantMsg = content;
|
|
99
|
+
}
|
|
100
|
+
pane.currentAssistantMsg.innerHTML = renderMarkdown(
|
|
101
|
+
(pane.currentAssistantMsg.dataset.raw || "") + text
|
|
102
|
+
);
|
|
103
|
+
pane.currentAssistantMsg.dataset.raw =
|
|
104
|
+
(pane.currentAssistantMsg.dataset.raw || "") + text;
|
|
105
|
+
highlightCodeBlocks(pane.currentAssistantMsg);
|
|
106
|
+
addCopyButtons(pane.currentAssistantMsg);
|
|
107
|
+
renderMermaidBlocks(pane.currentAssistantMsg);
|
|
108
|
+
scrollToBottom(pane);
|
|
109
|
+
|
|
110
|
+
// Update streaming token counter
|
|
111
|
+
let count = getState("streamingCharCount") + text.length;
|
|
112
|
+
setState("streamingCharCount", count);
|
|
113
|
+
const tokenEst = Math.round(count / 4);
|
|
114
|
+
if ($.streamingTokens) {
|
|
115
|
+
if ($.streamingTokensValue) $.streamingTokensValue.textContent = `~${tokenEst} tokens`;
|
|
116
|
+
$.streamingTokens.classList.remove("hidden");
|
|
117
|
+
if ($.streamingTokensSep) $.streamingTokensSep.classList.remove("hidden");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function appendToolIndicator(name, input, pane, toolId, isLive = true) {
|
|
122
|
+
pane = pane || getPane(null);
|
|
123
|
+
const div = document.createElement("div");
|
|
124
|
+
div.className = "msg";
|
|
125
|
+
|
|
126
|
+
// Diff view for Edit tool
|
|
127
|
+
if (name === "Edit" && input && input.old_string != null && input.new_string != null) {
|
|
128
|
+
const diffEl = renderDiffView(input.old_string, input.new_string, input.file_path);
|
|
129
|
+
if (diffEl) {
|
|
130
|
+
div.appendChild(diffEl);
|
|
131
|
+
pane.messagesDiv.appendChild(div);
|
|
132
|
+
pane.currentAssistantMsg = null;
|
|
133
|
+
scrollToBottom(pane);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Additions view for Write tool
|
|
139
|
+
if (name === "Write" && input && input.content != null) {
|
|
140
|
+
const addEl = renderAdditionsView(input.content, input.file_path);
|
|
141
|
+
if (addEl) {
|
|
142
|
+
div.appendChild(addEl);
|
|
143
|
+
pane.messagesDiv.appendChild(div);
|
|
144
|
+
pane.currentAssistantMsg = null;
|
|
145
|
+
scrollToBottom(pane);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Default tool indicator — show spinner only for live streaming tools
|
|
151
|
+
const indicator = document.createElement("div");
|
|
152
|
+
indicator.className = isLive ? "tool-indicator tool-running" : "tool-indicator";
|
|
153
|
+
if (toolId) indicator.dataset.toolId = toolId;
|
|
154
|
+
indicator.innerHTML = `
|
|
155
|
+
<span class="tool-spinner" ${!isLive ? 'style="display:none;"' : ""}></span>
|
|
156
|
+
<span class="tool-status-icon" style="display:none;"></span>
|
|
157
|
+
<span class="tool-name">${escapeHtml(name)}</span>
|
|
158
|
+
<span class="tool-detail">${getToolDetail(name, input)}</span>
|
|
159
|
+
<div class="tool-body">${escapeHtml(JSON.stringify(input, null, 2))}</div>
|
|
160
|
+
<div class="tool-result-preview" style="display:none;"></div>
|
|
161
|
+
`;
|
|
162
|
+
indicator.addEventListener("click", () => {
|
|
163
|
+
indicator.classList.toggle("expanded");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
div.appendChild(indicator);
|
|
167
|
+
pane.messagesDiv.appendChild(div);
|
|
168
|
+
pane.currentAssistantMsg = null;
|
|
169
|
+
scrollToBottom(pane);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function appendToolResult(toolUseId, content, isError, pane) {
|
|
173
|
+
pane = pane || getPane(null);
|
|
174
|
+
|
|
175
|
+
// Try to find the matching tool indicator and update it in-place
|
|
176
|
+
const existing = toolUseId
|
|
177
|
+
? pane.messagesDiv.querySelector(`.tool-indicator[data-tool-id="${toolUseId}"]`)
|
|
178
|
+
: null;
|
|
179
|
+
|
|
180
|
+
if (existing) {
|
|
181
|
+
// Update the existing indicator: stop spinner, show status icon + result
|
|
182
|
+
existing.classList.remove("tool-running");
|
|
183
|
+
existing.classList.add(isError ? "tool-error" : "tool-done");
|
|
184
|
+
|
|
185
|
+
const spinner = existing.querySelector(".tool-spinner");
|
|
186
|
+
if (spinner) spinner.style.display = "none";
|
|
187
|
+
|
|
188
|
+
const statusIcon = existing.querySelector(".tool-status-icon");
|
|
189
|
+
if (statusIcon) {
|
|
190
|
+
statusIcon.style.display = "";
|
|
191
|
+
statusIcon.style.color = isError ? "var(--error)" : "var(--success)";
|
|
192
|
+
statusIcon.innerHTML = isError ? "✗" : "✓";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Show result preview inline
|
|
196
|
+
const resultPreview = existing.querySelector(".tool-result-preview");
|
|
197
|
+
if (resultPreview && content) {
|
|
198
|
+
const preview = typeof content === "string" ? content.slice(0, 150) : "";
|
|
199
|
+
resultPreview.textContent = preview;
|
|
200
|
+
resultPreview.style.display = "";
|
|
201
|
+
resultPreview.className = "tool-result-preview" + (isError ? " error" : "");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Append full result to tool-body
|
|
205
|
+
const body = existing.querySelector(".tool-body");
|
|
206
|
+
if (body && content) {
|
|
207
|
+
body.innerHTML += "\n\n─── Result ───\n" + escapeHtml(content || "");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
scrollToBottom(pane);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Fallback: create a standalone result element (for old messages without tool IDs)
|
|
215
|
+
const div = document.createElement("div");
|
|
216
|
+
div.className = "msg";
|
|
217
|
+
|
|
218
|
+
const indicator = document.createElement("div");
|
|
219
|
+
indicator.className = "tool-indicator " + (isError ? "tool-error" : "tool-done");
|
|
220
|
+
const preview = typeof content === "string" ? content.slice(0, 120) : "";
|
|
221
|
+
const iconColor = isError ? "var(--error)" : "var(--success)";
|
|
222
|
+
const icon = isError ? "✗" : "✓";
|
|
223
|
+
indicator.innerHTML = `
|
|
224
|
+
<span class="tool-status-icon" style="color: ${iconColor};">${icon}</span>
|
|
225
|
+
<span class="tool-name">${isError ? "Error" : "Result"}</span>
|
|
226
|
+
<span class="tool-detail">${escapeHtml(preview)}</span>
|
|
227
|
+
<div class="tool-body">${escapeHtml(content || "")}</div>
|
|
228
|
+
`;
|
|
229
|
+
indicator.addEventListener("click", () => {
|
|
230
|
+
indicator.classList.toggle("expanded");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
div.appendChild(indicator);
|
|
234
|
+
pane.messagesDiv.appendChild(div);
|
|
235
|
+
pane.currentAssistantMsg = null;
|
|
236
|
+
scrollToBottom(pane);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function showThinking(label, pane) {
|
|
240
|
+
pane = pane || getPane(null);
|
|
241
|
+
removeThinking(pane);
|
|
242
|
+
const div = document.createElement("div");
|
|
243
|
+
div.className = "thinking-bar";
|
|
244
|
+
div.dataset.thinkingBar = "true";
|
|
245
|
+
div.innerHTML = `
|
|
246
|
+
<div class="thinking-dot-container">
|
|
247
|
+
<span class="thinking-dot"></span>
|
|
248
|
+
<span class="thinking-dot"></span>
|
|
249
|
+
<span class="thinking-dot"></span>
|
|
250
|
+
</div>
|
|
251
|
+
<span class="thinking-label">${escapeHtml(label)}</span>
|
|
252
|
+
`;
|
|
253
|
+
pane.messagesDiv.appendChild(div);
|
|
254
|
+
if (pane.statusEl) {
|
|
255
|
+
pane.statusEl.textContent = "streaming";
|
|
256
|
+
pane.statusEl.className = "chat-pane-status streaming";
|
|
257
|
+
}
|
|
258
|
+
scrollToBottom(pane);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function removeThinking(pane) {
|
|
262
|
+
pane = pane || getPane(null);
|
|
263
|
+
const el = pane.messagesDiv.querySelector('[data-thinking-bar="true"]');
|
|
264
|
+
if (el) el.remove();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function addResultSummary(msg, pane) {
|
|
268
|
+
pane = pane || getPane(null);
|
|
269
|
+
const parts = [];
|
|
270
|
+
if (msg.model) parts.push(msg.model);
|
|
271
|
+
if (msg.num_turns != null) parts.push(`${msg.num_turns} turn${msg.num_turns !== 1 ? "s" : ""}`);
|
|
272
|
+
if (msg.duration_ms != null) {
|
|
273
|
+
const secs = (msg.duration_ms / 1000).toFixed(1);
|
|
274
|
+
parts.push(`${secs}s`);
|
|
275
|
+
}
|
|
276
|
+
if (msg.cost_usd != null) parts.push(`$${msg.cost_usd.toFixed(4)}`);
|
|
277
|
+
const inTok = msg.input_tokens || 0;
|
|
278
|
+
const outTok = msg.output_tokens || 0;
|
|
279
|
+
if (inTok > 0 || outTok > 0) {
|
|
280
|
+
const fmtTok = (n) => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
281
|
+
parts.push(`${fmtTok(inTok)} in / ${fmtTok(outTok)} out`);
|
|
282
|
+
}
|
|
283
|
+
if (msg.stop_reason && msg.stop_reason !== "success") {
|
|
284
|
+
parts.push(`[${msg.stop_reason}]`);
|
|
285
|
+
}
|
|
286
|
+
if (parts.length > 0) {
|
|
287
|
+
addStatus(parts.join(" \u00b7 "), false, pane);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function addStatus(text, isError, pane) {
|
|
292
|
+
pane = pane || getPane(null);
|
|
293
|
+
const div = document.createElement("div");
|
|
294
|
+
div.className = "status" + (isError ? " error" : "");
|
|
295
|
+
div.textContent = text;
|
|
296
|
+
pane.messagesDiv.appendChild(div);
|
|
297
|
+
scrollToBottom(pane);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function appendCliOutput(data, pane) {
|
|
301
|
+
pane = pane || getPane(null);
|
|
302
|
+
const div = document.createElement("div");
|
|
303
|
+
div.className = "msg";
|
|
304
|
+
|
|
305
|
+
const block = document.createElement("div");
|
|
306
|
+
block.className = "cli-output";
|
|
307
|
+
|
|
308
|
+
const isOk = data.exitCode === 0;
|
|
309
|
+
block.innerHTML = `
|
|
310
|
+
<div class="cli-output-header">
|
|
311
|
+
<span class="cli-icon ${isOk ? "success" : "error"}">${isOk ? "✓" : "✗"}</span>
|
|
312
|
+
<span class="cli-cmd">${escapeHtml(data.command)}</span>
|
|
313
|
+
<span class="cli-exit">exit ${data.exitCode}</span>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="cli-output-body">
|
|
316
|
+
${data.stdout ? `<pre>${escapeHtml(data.stdout)}</pre>` : ""}
|
|
317
|
+
${data.stderr ? `<pre class="cli-output-stderr">${escapeHtml(data.stderr)}</pre>` : ""}
|
|
318
|
+
${!data.stdout && !data.stderr ? `<pre>(no output)</pre>` : ""}
|
|
319
|
+
</div>
|
|
320
|
+
`;
|
|
321
|
+
|
|
322
|
+
div.appendChild(block);
|
|
323
|
+
pane.messagesDiv.appendChild(div);
|
|
324
|
+
pane.currentAssistantMsg = null;
|
|
325
|
+
scrollToBottom(pane);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function renderMessagesIntoPane(messages, pane) {
|
|
329
|
+
pane.messagesDiv.innerHTML = "";
|
|
330
|
+
pane.currentAssistantMsg = null;
|
|
331
|
+
// Reset streaming counter — we're loading saved messages, not streaming
|
|
332
|
+
setState("streamingCharCount", 0);
|
|
333
|
+
if (!messages || messages.length === 0) {
|
|
334
|
+
showWhalyPlaceholder(pane);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
for (const msg of messages) {
|
|
338
|
+
const data = JSON.parse(msg.content);
|
|
339
|
+
switch (msg.role) {
|
|
340
|
+
case "user": {
|
|
341
|
+
// Extract file paths from saved <file path="..."> blocks
|
|
342
|
+
const filePathMatches = (data.text || "").match(/<file path="([^"]+)">/g);
|
|
343
|
+
const savedFilePaths = filePathMatches
|
|
344
|
+
? filePathMatches.map(m => m.match(/<file path="([^"]+)">/)[1])
|
|
345
|
+
: [];
|
|
346
|
+
// Show only the user's actual text, not the file content blocks
|
|
347
|
+
const cleanText = savedFilePaths.length > 0
|
|
348
|
+
? (data.text || "").replace(/<file path="[^"]*">[\s\S]*?<\/file>\s*/g, "").trim()
|
|
349
|
+
: (data.text || "");
|
|
350
|
+
addUserMessage(cleanText, pane, data.images || [], savedFilePaths);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "assistant":
|
|
354
|
+
appendAssistantText(data.text, pane);
|
|
355
|
+
break;
|
|
356
|
+
case "tool":
|
|
357
|
+
appendToolIndicator(data.name, data.input, pane, data.id, false);
|
|
358
|
+
break;
|
|
359
|
+
case "tool_result":
|
|
360
|
+
appendToolResult(data.toolUseId, data.content, data.isError, pane);
|
|
361
|
+
break;
|
|
362
|
+
case "result":
|
|
363
|
+
addResultSummary(data, pane);
|
|
364
|
+
break;
|
|
365
|
+
case "error": {
|
|
366
|
+
const errorParts = [];
|
|
367
|
+
if (data.subtype) errorParts.push(`[${data.subtype}]`);
|
|
368
|
+
if (data.error) errorParts.push(data.error);
|
|
369
|
+
if (data.cost_usd != null) errorParts.push(`$${data.cost_usd.toFixed(4)}`);
|
|
370
|
+
if (data.model) errorParts.push(data.model);
|
|
371
|
+
addStatus(errorParts.join(" \u00b7 ") || "Error", true, pane);
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "aborted":
|
|
375
|
+
addStatus("Aborted", true, pane);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
pane.currentAssistantMsg = null;
|
|
380
|
+
// Hide token counter and reset — loading saved messages shouldn't show streaming stats
|
|
381
|
+
setState("streamingCharCount", 0);
|
|
382
|
+
if ($.streamingTokens) $.streamingTokens.classList.add("hidden");
|
|
383
|
+
if ($.streamingTokensSep) $.streamingTokensSep.classList.add("hidden");
|
|
384
|
+
highlightCodeBlocks(pane.messagesDiv);
|
|
385
|
+
addCopyButtons(pane.messagesDiv);
|
|
386
|
+
renderMermaidBlocks(pane.messagesDiv);
|
|
387
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Model selector — localStorage persistence + getter
|
|
2
|
+
import { $ } from '../core/dom.js';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'claudeck-model';
|
|
5
|
+
|
|
6
|
+
export function getSelectedModel() {
|
|
7
|
+
return $.modelSelect?.value || '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function init() {
|
|
11
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
12
|
+
if (saved && $.modelSelect) {
|
|
13
|
+
$.modelSelect.value = saved;
|
|
14
|
+
}
|
|
15
|
+
$.modelSelect?.addEventListener('change', () => {
|
|
16
|
+
localStorage.setItem(STORAGE_KEY, $.modelSelect.value);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
init();
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Browser push notifications
|
|
2
|
+
import { getState, setState } from '../core/store.js';
|
|
3
|
+
import { registerCommand } from './commands.js';
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = 'claudeck-notifications';
|
|
6
|
+
const SOUND_KEY = 'claudeck-notifications-sound';
|
|
7
|
+
|
|
8
|
+
// ── Audio ──
|
|
9
|
+
let audioCtx = null;
|
|
10
|
+
function getAudioCtx() {
|
|
11
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
12
|
+
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
13
|
+
return audioCtx;
|
|
14
|
+
}
|
|
15
|
+
// Unlock AudioContext on first user interaction (browser autoplay policy)
|
|
16
|
+
document.addEventListener('click', () => getAudioCtx(), { once: true });
|
|
17
|
+
document.addEventListener('keydown', () => getAudioCtx(), { once: true });
|
|
18
|
+
|
|
19
|
+
function playNotificationSound() {
|
|
20
|
+
try {
|
|
21
|
+
const ctx = getAudioCtx();
|
|
22
|
+
const now = ctx.currentTime;
|
|
23
|
+
// Two-tone chime: C5 → E5
|
|
24
|
+
[[523, 0], [659, 0.15]].forEach(([freq, offset]) => {
|
|
25
|
+
const osc = ctx.createOscillator();
|
|
26
|
+
const gain = ctx.createGain();
|
|
27
|
+
osc.connect(gain);
|
|
28
|
+
gain.connect(ctx.destination);
|
|
29
|
+
osc.frequency.value = freq;
|
|
30
|
+
osc.type = 'sine';
|
|
31
|
+
gain.gain.setValueAtTime(0.25, now + offset);
|
|
32
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.3);
|
|
33
|
+
osc.start(now + offset);
|
|
34
|
+
osc.stop(now + offset + 0.3);
|
|
35
|
+
});
|
|
36
|
+
} catch { /* audio unavailable */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isNotificationSoundEnabled() {
|
|
40
|
+
return localStorage.getItem(SOUND_KEY) !== '0';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Request notification permission from the browser.
|
|
45
|
+
* Returns true if granted.
|
|
46
|
+
*/
|
|
47
|
+
export async function requestNotificationPermission() {
|
|
48
|
+
if (!('Notification' in window)) return false;
|
|
49
|
+
if (Notification.permission === 'granted') return true;
|
|
50
|
+
if (Notification.permission === 'denied') return false;
|
|
51
|
+
const result = await Notification.requestPermission();
|
|
52
|
+
return result === 'granted';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if notifications are supported and enabled by user preference.
|
|
57
|
+
*/
|
|
58
|
+
export function isNotificationsEnabled() {
|
|
59
|
+
if (!('Notification' in window)) return false;
|
|
60
|
+
if (Notification.permission !== 'granted') return false;
|
|
61
|
+
return getState('notificationsEnabled');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Send a browser notification (only when tab/app is not focused).
|
|
66
|
+
* Uses ServiceWorker.showNotification() for PWA support — works in
|
|
67
|
+
* standalone mode, background tabs, and regular browser tabs.
|
|
68
|
+
* Falls back to `new Notification()` if SW is unavailable.
|
|
69
|
+
* @param {string} title
|
|
70
|
+
* @param {string} body
|
|
71
|
+
* @param {string} [tag] - dedup tag (same tag replaces previous)
|
|
72
|
+
*/
|
|
73
|
+
export async function sendNotification(title, body, tag) {
|
|
74
|
+
if (!isNotificationsEnabled()) return;
|
|
75
|
+
if (document.hasFocus()) return;
|
|
76
|
+
|
|
77
|
+
// Play sound (works even when tab is unfocused, as long as page is loaded)
|
|
78
|
+
if (isNotificationSoundEnabled()) playNotificationSound();
|
|
79
|
+
|
|
80
|
+
const opts = {
|
|
81
|
+
body,
|
|
82
|
+
tag: tag || undefined,
|
|
83
|
+
icon: '/icons/icon-192.png',
|
|
84
|
+
silent: true, // suppress OS sound — we play our own
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Prefer SW-based notification (works in PWA standalone + background)
|
|
88
|
+
if ('serviceWorker' in navigator) {
|
|
89
|
+
try {
|
|
90
|
+
const reg = await navigator.serviceWorker.ready;
|
|
91
|
+
await reg.showNotification(title, opts);
|
|
92
|
+
return;
|
|
93
|
+
} catch { /* fall through to basic Notification */ }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback for browsers without active SW
|
|
97
|
+
const notification = new Notification(title, opts);
|
|
98
|
+
notification.addEventListener('click', () => {
|
|
99
|
+
window.focus();
|
|
100
|
+
notification.close();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Web Push helpers ──
|
|
105
|
+
|
|
106
|
+
function urlBase64ToUint8Array(base64String) {
|
|
107
|
+
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
108
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
109
|
+
const raw = atob(base64);
|
|
110
|
+
const arr = new Uint8Array(raw.length);
|
|
111
|
+
for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
|
|
112
|
+
return arr;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function subscribeToPush() {
|
|
116
|
+
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch('/api/notifications/vapid-public-key');
|
|
119
|
+
if (!res.ok) return;
|
|
120
|
+
const { key } = await res.json();
|
|
121
|
+
|
|
122
|
+
const reg = await navigator.serviceWorker.ready;
|
|
123
|
+
const subscription = await reg.pushManager.subscribe({
|
|
124
|
+
userVisibleOnly: true,
|
|
125
|
+
applicationServerKey: urlBase64ToUint8Array(key),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const sub = subscription.toJSON();
|
|
129
|
+
await fetch('/api/notifications/subscribe', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'Content-Type': 'application/json' },
|
|
132
|
+
body: JSON.stringify({ endpoint: sub.endpoint, keys: sub.keys }),
|
|
133
|
+
});
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn('Push subscription failed:', err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function unsubscribeFromPush() {
|
|
140
|
+
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
|
141
|
+
try {
|
|
142
|
+
const reg = await navigator.serviceWorker.ready;
|
|
143
|
+
const subscription = await reg.pushManager.getSubscription();
|
|
144
|
+
if (!subscription) return;
|
|
145
|
+
|
|
146
|
+
const endpoint = subscription.endpoint;
|
|
147
|
+
await subscription.unsubscribe();
|
|
148
|
+
|
|
149
|
+
await fetch('/api/notifications/unsubscribe', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ endpoint }),
|
|
153
|
+
});
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.warn('Push unsubscribe failed:', err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Toggle notifications on/off. Requests permission if enabling for the first time.
|
|
161
|
+
*/
|
|
162
|
+
export async function toggleNotifications() {
|
|
163
|
+
const current = getState('notificationsEnabled');
|
|
164
|
+
if (!current) {
|
|
165
|
+
const granted = await requestNotificationPermission();
|
|
166
|
+
if (!granted) return false;
|
|
167
|
+
setState('notificationsEnabled', true);
|
|
168
|
+
localStorage.setItem(STORAGE_KEY, '1');
|
|
169
|
+
await subscribeToPush();
|
|
170
|
+
return true;
|
|
171
|
+
} else {
|
|
172
|
+
setState('notificationsEnabled', false);
|
|
173
|
+
localStorage.setItem(STORAGE_KEY, '0');
|
|
174
|
+
await unsubscribeFromPush();
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function updateLabel() {
|
|
180
|
+
const label = document.getElementById('notifications-label');
|
|
181
|
+
if (label) {
|
|
182
|
+
label.textContent = getState('notificationsEnabled') ? 'Notifications (on)' : 'Notifications';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Listen for SW messages (push-triggered sound) ──
|
|
187
|
+
if ('serviceWorker' in navigator) {
|
|
188
|
+
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
189
|
+
if (event.data?.type === 'play-notification-sound' && isNotificationSoundEnabled()) {
|
|
190
|
+
playNotificationSound();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Init ──
|
|
196
|
+
function init() {
|
|
197
|
+
// Restore preference
|
|
198
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
199
|
+
if (saved === '1' && Notification.permission === 'granted') {
|
|
200
|
+
setState('notificationsEnabled', true);
|
|
201
|
+
// Re-subscribe to push on load (ensures server has current subscription)
|
|
202
|
+
subscribeToPush();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Wire up toggle button
|
|
206
|
+
const btn = document.getElementById('notifications-toggle-btn');
|
|
207
|
+
if (btn) {
|
|
208
|
+
btn.addEventListener('click', async () => {
|
|
209
|
+
await toggleNotifications();
|
|
210
|
+
updateLabel();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
updateLabel();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Commands ──
|
|
218
|
+
registerCommand('notifications', {
|
|
219
|
+
category: 'app',
|
|
220
|
+
description: 'Toggle browser notifications',
|
|
221
|
+
async execute(args, pane) {
|
|
222
|
+
const { addStatus } = await import('./messages.js');
|
|
223
|
+
const enabled = await toggleNotifications();
|
|
224
|
+
addStatus(
|
|
225
|
+
enabled ? 'Notifications enabled' : 'Notifications disabled',
|
|
226
|
+
false,
|
|
227
|
+
pane
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
init();
|