clay-server 2.5.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 +281 -0
- package/bin/cli.js +2385 -0
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +237 -0
- package/lib/daemon.js +489 -0
- package/lib/ipc.js +112 -0
- package/lib/notes.js +120 -0
- package/lib/pages.js +664 -0
- package/lib/project.js +1433 -0
- package/lib/public/app.js +2795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +264 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +1114 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/icon-strip.css +296 -0
- package/lib/public/css/input.css +573 -0
- package/lib/public/css/menus.css +856 -0
- package/lib/public/css/messages.css +1445 -0
- package/lib/public/css/mobile-nav.css +354 -0
- package/lib/public/css/overlays.css +697 -0
- package/lib/public/css/rewind.css +505 -0
- package/lib/public/css/server-settings.css +761 -0
- package/lib/public/css/sidebar.css +936 -0
- package/lib/public/css/sticky-notes.css +358 -0
- package/lib/public/css/title-bar.css +314 -0
- package/lib/public/favicon-dark.svg +1 -0
- package/lib/public/favicon.svg +1 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +762 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1411 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +584 -0
- package/lib/public/modules/markdown.js +356 -0
- package/lib/public/modules/notifications.js +649 -0
- package/lib/public/modules/qrcode.js +70 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/server-settings.js +510 -0
- package/lib/public/modules/sidebar.js +1083 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +688 -0
- package/lib/public/modules/terminal.js +697 -0
- package/lib/public/modules/theme.js +738 -0
- package/lib/public/modules/tools.js +1608 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +15 -0
- package/lib/public/sw.js +75 -0
- package/lib/push.js +124 -0
- package/lib/sdk-bridge.js +989 -0
- package/lib/server.js +582 -0
- package/lib/sessions.js +424 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +24 -0
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/clay-light.json +10 -0
- package/lib/themes/clay.json +10 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/lib/updater.js +97 -0
- package/package.json +47 -0
|
@@ -0,0 +1,2795 @@
|
|
|
1
|
+
import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
|
|
2
|
+
import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
|
|
3
|
+
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal } from './modules/markdown.js';
|
|
4
|
+
import { initSidebar, renderSessionList, handleSearchResults, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, initIconStrip } from './modules/sidebar.js';
|
|
5
|
+
import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
|
|
6
|
+
import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
|
|
7
|
+
import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands } from './modules/input.js';
|
|
8
|
+
import { initQrCode } from './modules/qrcode.js';
|
|
9
|
+
import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer } from './modules/filebrowser.js';
|
|
10
|
+
import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed } from './modules/terminal.js';
|
|
11
|
+
import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted } from './modules/sticky-notes.js';
|
|
12
|
+
import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
|
|
13
|
+
import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
|
|
14
|
+
import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleShutdownResult } from './modules/server-settings.js';
|
|
15
|
+
|
|
16
|
+
// --- Base path for multi-project routing ---
|
|
17
|
+
var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
18
|
+
var basePath = slugMatch ? "/p/" + slugMatch[1] + "/" : "/";
|
|
19
|
+
var wsPath = slugMatch ? "/p/" + slugMatch[1] + "/ws" : "/ws";
|
|
20
|
+
|
|
21
|
+
// --- DOM refs ---
|
|
22
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
23
|
+
var messagesEl = $("messages");
|
|
24
|
+
var inputEl = $("input");
|
|
25
|
+
var sendBtn = $("send-btn");
|
|
26
|
+
function getStatusDot() {
|
|
27
|
+
var active = document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status");
|
|
28
|
+
if (active) return active;
|
|
29
|
+
// Fallback: home icon status dot
|
|
30
|
+
return document.querySelector(".icon-strip-home .icon-strip-status");
|
|
31
|
+
}
|
|
32
|
+
var headerTitleEl = $("header-title");
|
|
33
|
+
var headerRenameBtn = $("header-rename-btn");
|
|
34
|
+
var slashMenu = $("slash-menu");
|
|
35
|
+
var suggestionChipsEl = $("suggestion-chips");
|
|
36
|
+
var sidebar = $("sidebar");
|
|
37
|
+
var sidebarOverlay = $("sidebar-overlay");
|
|
38
|
+
var sessionListEl = $("session-list");
|
|
39
|
+
var newSessionBtn = $("new-session-btn");
|
|
40
|
+
var hamburgerBtn = $("hamburger-btn");
|
|
41
|
+
var sidebarToggleBtn = $("sidebar-toggle-btn");
|
|
42
|
+
var sidebarExpandBtn = $("sidebar-expand-btn");
|
|
43
|
+
var resumeSessionBtn = $("resume-session-btn");
|
|
44
|
+
var imagePreviewBar = $("image-preview-bar");
|
|
45
|
+
var connectOverlay = $("connect-overlay");
|
|
46
|
+
|
|
47
|
+
// --- Project List ---
|
|
48
|
+
var projectListSection = $("project-list-section");
|
|
49
|
+
var projectListEl = $("project-list");
|
|
50
|
+
var projectListAddBtn = $("project-list-add");
|
|
51
|
+
var projectHint = $("project-hint");
|
|
52
|
+
var projectHintDismiss = $("project-hint-dismiss");
|
|
53
|
+
var cachedProjects = [];
|
|
54
|
+
var cachedProjectCount = 0;
|
|
55
|
+
var currentSlug = slugMatch ? slugMatch[1] : null;
|
|
56
|
+
|
|
57
|
+
function updateProjectList(msg) {
|
|
58
|
+
if (typeof msg.projectCount === "number") cachedProjectCount = msg.projectCount;
|
|
59
|
+
if (msg.projects) cachedProjects = msg.projects;
|
|
60
|
+
var count = cachedProjectCount || 0;
|
|
61
|
+
renderProjectList();
|
|
62
|
+
if (count === 1 && projectHint) {
|
|
63
|
+
try {
|
|
64
|
+
if (!localStorage.getItem("clay-project-hint-dismissed")) {
|
|
65
|
+
projectHint.classList.remove("hidden");
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
} else if (projectHint) {
|
|
69
|
+
projectHint.classList.add("hidden");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderProjectList() {
|
|
74
|
+
// Render icon strip projects
|
|
75
|
+
var iconStripProjects = cachedProjects.map(function (p) {
|
|
76
|
+
return { slug: p.slug, name: p.title || p.project, isProcessing: p.isProcessing };
|
|
77
|
+
});
|
|
78
|
+
renderIconStrip(iconStripProjects, currentSlug);
|
|
79
|
+
// Re-apply current socket status to the active icon's dot
|
|
80
|
+
var dot = getStatusDot();
|
|
81
|
+
if (dot) {
|
|
82
|
+
if (connected && processing) { dot.classList.add("connected"); dot.classList.add("processing"); }
|
|
83
|
+
else if (connected) { dot.classList.add("connected"); }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (projectListAddBtn) {
|
|
88
|
+
projectListAddBtn.addEventListener("click", function () {
|
|
89
|
+
openAddProjectModal();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
document.addEventListener("keydown", function (e) {
|
|
94
|
+
if (e.key === "Escape") {
|
|
95
|
+
closeImageModal();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (projectHintDismiss) {
|
|
100
|
+
projectHintDismiss.addEventListener("click", function () {
|
|
101
|
+
projectHint.classList.add("hidden");
|
|
102
|
+
try { localStorage.setItem("clay-project-hint-dismissed", "1"); } catch (e) {}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Modal close handlers (replaces inline onclick)
|
|
107
|
+
$("paste-modal").querySelector(".confirm-backdrop").addEventListener("click", function() {
|
|
108
|
+
$("paste-modal").classList.add("hidden");
|
|
109
|
+
});
|
|
110
|
+
$("paste-modal").querySelector(".paste-modal-close").addEventListener("click", function() {
|
|
111
|
+
$("paste-modal").classList.add("hidden");
|
|
112
|
+
});
|
|
113
|
+
$("mermaid-modal").querySelector(".confirm-backdrop").addEventListener("click", closeMermaidModal);
|
|
114
|
+
$("mermaid-modal").querySelector(".mermaid-modal-btn[title='Close']").addEventListener("click", closeMermaidModal);
|
|
115
|
+
$("image-modal").querySelector(".confirm-backdrop").addEventListener("click", closeImageModal);
|
|
116
|
+
$("image-modal").querySelector(".image-modal-close").addEventListener("click", closeImageModal);
|
|
117
|
+
|
|
118
|
+
function showImageModal(src) {
|
|
119
|
+
var modal = $("image-modal");
|
|
120
|
+
var img = $("image-modal-img");
|
|
121
|
+
if (!modal || !img) return;
|
|
122
|
+
img.src = src;
|
|
123
|
+
modal.classList.remove("hidden");
|
|
124
|
+
refreshIcons(modal);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function closeImageModal() {
|
|
128
|
+
var modal = $("image-modal");
|
|
129
|
+
if (modal) modal.classList.add("hidden");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- State ---
|
|
133
|
+
var ws = null;
|
|
134
|
+
var connected = false;
|
|
135
|
+
var wasConnected = false;
|
|
136
|
+
var processing = false;
|
|
137
|
+
// isComposing -> modules/input.js
|
|
138
|
+
var reconnectTimer = null;
|
|
139
|
+
var reconnectDelay = 1000;
|
|
140
|
+
var disconnectNotifTimer = null;
|
|
141
|
+
var disconnectNotifShown = false;
|
|
142
|
+
var activityEl = null;
|
|
143
|
+
var currentMsgEl = null;
|
|
144
|
+
var currentFullText = "";
|
|
145
|
+
// tools, currentThinking -> modules/tools.js
|
|
146
|
+
var highlightTimer = null;
|
|
147
|
+
var activeSessionId = null;
|
|
148
|
+
var sessionDrafts = {};
|
|
149
|
+
var slashCommands = [];
|
|
150
|
+
// slashActiveIdx, slashFiltered, pendingImages, pendingPastes -> modules/input.js
|
|
151
|
+
// pendingPermissions -> modules/tools.js
|
|
152
|
+
var cliSessionId = null;
|
|
153
|
+
var projectName = "";
|
|
154
|
+
var turnCounter = 0;
|
|
155
|
+
|
|
156
|
+
// Restore cached project name for instant display (before WS connects)
|
|
157
|
+
try {
|
|
158
|
+
var _cachedProjectName = localStorage.getItem("clay-project-name-" + (currentSlug || "default"));
|
|
159
|
+
if (_cachedProjectName) {
|
|
160
|
+
projectName = _cachedProjectName;
|
|
161
|
+
if (headerTitleEl) headerTitleEl.textContent = _cachedProjectName;
|
|
162
|
+
var _tbp = $("title-bar-project-name");
|
|
163
|
+
if (_tbp) _tbp.textContent = _cachedProjectName;
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {}
|
|
166
|
+
var messageUuidMap = [];
|
|
167
|
+
// pendingRewindUuid is now in modules/rewind.js
|
|
168
|
+
// rewindMode is now in modules/rewind.js
|
|
169
|
+
|
|
170
|
+
// --- Progressive history loading ---
|
|
171
|
+
var historyFrom = 0;
|
|
172
|
+
var historyTotal = 0;
|
|
173
|
+
var prependAnchor = null;
|
|
174
|
+
var loadingMore = false;
|
|
175
|
+
var historySentinelObserver = null;
|
|
176
|
+
var replayingHistory = false;
|
|
177
|
+
|
|
178
|
+
// --- Scroll lock ---
|
|
179
|
+
var isUserScrolledUp = false;
|
|
180
|
+
var scrollThreshold = 150;
|
|
181
|
+
|
|
182
|
+
// builtinCommands -> modules/input.js
|
|
183
|
+
|
|
184
|
+
// --- Header session rename ---
|
|
185
|
+
if (headerRenameBtn) {
|
|
186
|
+
headerRenameBtn.addEventListener("click", function () {
|
|
187
|
+
if (!activeSessionId) return;
|
|
188
|
+
var currentText = headerTitleEl.textContent;
|
|
189
|
+
var input = document.createElement("input");
|
|
190
|
+
input.type = "text";
|
|
191
|
+
input.className = "header-rename-input";
|
|
192
|
+
input.value = currentText;
|
|
193
|
+
headerTitleEl.style.display = "none";
|
|
194
|
+
headerRenameBtn.style.display = "none";
|
|
195
|
+
headerTitleEl.parentNode.insertBefore(input, headerTitleEl.nextSibling);
|
|
196
|
+
input.focus();
|
|
197
|
+
input.select();
|
|
198
|
+
|
|
199
|
+
function commit() {
|
|
200
|
+
var newTitle = input.value.trim();
|
|
201
|
+
if (newTitle && newTitle !== currentText && ws && ws.readyState === 1) {
|
|
202
|
+
ws.send(JSON.stringify({ type: "rename_session", id: activeSessionId, title: newTitle }));
|
|
203
|
+
headerTitleEl.textContent = newTitle;
|
|
204
|
+
}
|
|
205
|
+
input.remove();
|
|
206
|
+
headerTitleEl.style.display = "";
|
|
207
|
+
headerRenameBtn.style.display = "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
input.addEventListener("keydown", function (e) {
|
|
211
|
+
if (e.key === "Enter") { e.preventDefault(); commit(); }
|
|
212
|
+
if (e.key === "Escape") {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
input.remove();
|
|
215
|
+
headerTitleEl.style.display = "";
|
|
216
|
+
headerRenameBtn.style.display = "";
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
input.addEventListener("blur", commit);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- Session info popover ---
|
|
224
|
+
var headerInfoBtn = $("header-info-btn");
|
|
225
|
+
var sessionInfoPopover = null;
|
|
226
|
+
|
|
227
|
+
function closeSessionInfoPopover() {
|
|
228
|
+
if (sessionInfoPopover) {
|
|
229
|
+
sessionInfoPopover.remove();
|
|
230
|
+
sessionInfoPopover = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (headerInfoBtn) {
|
|
235
|
+
headerInfoBtn.addEventListener("click", function (e) {
|
|
236
|
+
e.stopPropagation();
|
|
237
|
+
if (sessionInfoPopover) { closeSessionInfoPopover(); return; }
|
|
238
|
+
|
|
239
|
+
var pop = document.createElement("div");
|
|
240
|
+
pop.className = "session-info-popover";
|
|
241
|
+
|
|
242
|
+
function addRow(label, value) {
|
|
243
|
+
var val = value == null ? "-" : String(value);
|
|
244
|
+
var row = document.createElement("div");
|
|
245
|
+
row.className = "info-row";
|
|
246
|
+
row.innerHTML =
|
|
247
|
+
'<span class="info-label">' + label + '</span>' +
|
|
248
|
+
'<span class="info-value">' + escapeHtml(val) + '</span>' +
|
|
249
|
+
'<button class="info-copy-btn" title="Copy">' + iconHtml("copy") + '</button>';
|
|
250
|
+
var btn = row.querySelector(".info-copy-btn");
|
|
251
|
+
btn.addEventListener("click", function () {
|
|
252
|
+
copyToClipboard(value || "").then(function () {
|
|
253
|
+
btn.innerHTML = iconHtml("check");
|
|
254
|
+
refreshIcons();
|
|
255
|
+
setTimeout(function () { btn.innerHTML = iconHtml("copy"); refreshIcons(); }, 1200);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
pop.appendChild(row);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (cliSessionId) addRow("Session ID", cliSessionId);
|
|
262
|
+
if (activeSessionId) addRow("Local ID", activeSessionId);
|
|
263
|
+
if (cliSessionId) addRow("Resume", "claude --resume " + cliSessionId);
|
|
264
|
+
|
|
265
|
+
document.body.appendChild(pop);
|
|
266
|
+
sessionInfoPopover = pop;
|
|
267
|
+
refreshIcons();
|
|
268
|
+
|
|
269
|
+
var btnRect = headerInfoBtn.getBoundingClientRect();
|
|
270
|
+
pop.style.top = (btnRect.bottom + 6) + "px";
|
|
271
|
+
pop.style.left = btnRect.left + "px";
|
|
272
|
+
var popRect = pop.getBoundingClientRect();
|
|
273
|
+
if (popRect.right > window.innerWidth - 8) {
|
|
274
|
+
pop.style.left = (window.innerWidth - popRect.width - 8) + "px";
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
document.addEventListener("click", function (e) {
|
|
279
|
+
if (sessionInfoPopover && !sessionInfoPopover.contains(e.target) && !e.target.closest("#header-info-btn")) {
|
|
280
|
+
closeSessionInfoPopover();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- Confirm modal ---
|
|
286
|
+
var confirmModal = $("confirm-modal");
|
|
287
|
+
var confirmText = $("confirm-text");
|
|
288
|
+
var confirmOk = $("confirm-ok");
|
|
289
|
+
var confirmCancel = $("confirm-cancel");
|
|
290
|
+
// --- Paste content viewer modal ---
|
|
291
|
+
function showPasteModal(text) {
|
|
292
|
+
var modal = $("paste-modal");
|
|
293
|
+
var body = $("paste-modal-body");
|
|
294
|
+
if (!modal || !body) return;
|
|
295
|
+
body.textContent = text;
|
|
296
|
+
modal.classList.remove("hidden");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function closePasteModal() {
|
|
300
|
+
var modal = $("paste-modal");
|
|
301
|
+
if (modal) modal.classList.add("hidden");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
var confirmCallback = null;
|
|
305
|
+
|
|
306
|
+
function showConfirm(text, onConfirm) {
|
|
307
|
+
confirmText.textContent = text;
|
|
308
|
+
confirmCallback = onConfirm;
|
|
309
|
+
confirmModal.classList.remove("hidden");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function hideConfirm() {
|
|
313
|
+
confirmModal.classList.add("hidden");
|
|
314
|
+
confirmCallback = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
confirmOk.addEventListener("click", function () {
|
|
318
|
+
if (confirmCallback) confirmCallback();
|
|
319
|
+
hideConfirm();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
confirmCancel.addEventListener("click", hideConfirm);
|
|
323
|
+
confirmModal.querySelector(".confirm-backdrop").addEventListener("click", hideConfirm);
|
|
324
|
+
|
|
325
|
+
// --- Rewind (module) ---
|
|
326
|
+
initRewind({
|
|
327
|
+
$: $,
|
|
328
|
+
get ws() { return ws; },
|
|
329
|
+
get connected() { return connected; },
|
|
330
|
+
get processing() { return processing; },
|
|
331
|
+
messagesEl: messagesEl,
|
|
332
|
+
addSystemMessage: addSystemMessage,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// --- Theme (module) ---
|
|
336
|
+
initTheme();
|
|
337
|
+
|
|
338
|
+
// --- Sidebar (module) ---
|
|
339
|
+
var sidebarCtx = {
|
|
340
|
+
$: $,
|
|
341
|
+
get ws() { return ws; },
|
|
342
|
+
get connected() { return connected; },
|
|
343
|
+
get projectName() { return projectName; },
|
|
344
|
+
messagesEl: messagesEl,
|
|
345
|
+
sessionListEl: sessionListEl,
|
|
346
|
+
sidebar: sidebar,
|
|
347
|
+
sidebarOverlay: sidebarOverlay,
|
|
348
|
+
sidebarToggleBtn: sidebarToggleBtn,
|
|
349
|
+
sidebarExpandBtn: sidebarExpandBtn,
|
|
350
|
+
hamburgerBtn: hamburgerBtn,
|
|
351
|
+
newSessionBtn: newSessionBtn,
|
|
352
|
+
resumeSessionBtn: resumeSessionBtn,
|
|
353
|
+
headerTitleEl: headerTitleEl,
|
|
354
|
+
showConfirm: showConfirm,
|
|
355
|
+
onFilesTabOpen: function () { loadRootDirectory(); },
|
|
356
|
+
switchProject: function (slug) { switchProject(slug); },
|
|
357
|
+
openTerminal: function () { openTerminal(); },
|
|
358
|
+
};
|
|
359
|
+
initSidebar(sidebarCtx);
|
|
360
|
+
initIconStrip(sidebarCtx);
|
|
361
|
+
|
|
362
|
+
// --- Connect overlay (logo + wordmark only) ---
|
|
363
|
+
function startVerbCycle() {}
|
|
364
|
+
function stopVerbCycle() {}
|
|
365
|
+
|
|
366
|
+
// Reset favicon cache when theme changes (variant may switch light ↔ dark)
|
|
367
|
+
onThemeChange(function () {
|
|
368
|
+
faviconSvgLight = null;
|
|
369
|
+
faviconSvgDark = null;
|
|
370
|
+
faviconOrigHref = null;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
function startPixelAnim() {}
|
|
374
|
+
function stopPixelAnim() {}
|
|
375
|
+
|
|
376
|
+
// --- Dynamic favicon ---
|
|
377
|
+
var faviconLink = document.querySelector('link[rel="icon"]');
|
|
378
|
+
var faviconSvgLight = null;
|
|
379
|
+
var faviconSvgDark = null;
|
|
380
|
+
var faviconOrigHref = null;
|
|
381
|
+
|
|
382
|
+
// Background fill colors in each favicon variant (terracotta / dark-brown)
|
|
383
|
+
var LIGHT_BG_FILLS = ["#E3D0CC", "#C0A9A4", "#D6B6B0", "#DAC7C4", "#D4C0BD", "#CBB8B2"];
|
|
384
|
+
var DARK_BG_FILLS = ["#3A3535", "#252121", "#2E2929", "#332E2E", "#312C2C", "#292525"];
|
|
385
|
+
|
|
386
|
+
function getFaviconSvg() {
|
|
387
|
+
var theme = getCurrentTheme();
|
|
388
|
+
var isLight = theme.variant === "light";
|
|
389
|
+
var src = isLight ? "favicon.svg" : "favicon-dark.svg";
|
|
390
|
+
var cached = isLight ? faviconSvgLight : faviconSvgDark;
|
|
391
|
+
if (cached) return cached;
|
|
392
|
+
var xhr = new XMLHttpRequest();
|
|
393
|
+
xhr.open("GET", basePath + src, false);
|
|
394
|
+
xhr.send();
|
|
395
|
+
if (xhr.status !== 200) return null;
|
|
396
|
+
if (isLight) { faviconSvgLight = xhr.responseText; return faviconSvgLight; }
|
|
397
|
+
faviconSvgDark = xhr.responseText;
|
|
398
|
+
return faviconSvgDark;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function updateFavicon(bgColor) {
|
|
402
|
+
if (!faviconLink) return;
|
|
403
|
+
if (!bgColor) {
|
|
404
|
+
// Restore original
|
|
405
|
+
if (faviconOrigHref) { faviconLink.href = faviconOrigHref; faviconOrigHref = null; }
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
var raw = getFaviconSvg();
|
|
409
|
+
if (!raw) return;
|
|
410
|
+
if (!faviconOrigHref) faviconOrigHref = faviconLink.href;
|
|
411
|
+
var theme = getCurrentTheme();
|
|
412
|
+
var fills = theme.variant === "light" ? LIGHT_BG_FILLS : DARK_BG_FILLS;
|
|
413
|
+
var svg = raw;
|
|
414
|
+
for (var i = 0; i < fills.length; i++) {
|
|
415
|
+
svg = svg.split(fills[i]).join(bgColor);
|
|
416
|
+
}
|
|
417
|
+
faviconLink.href = "data:image/svg+xml," + encodeURIComponent(svg);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// --- Status & Activity ---
|
|
421
|
+
function setSendBtnMode(mode) {
|
|
422
|
+
if (mode === "stop") {
|
|
423
|
+
sendBtn.disabled = false;
|
|
424
|
+
sendBtn.classList.add("stop");
|
|
425
|
+
sendBtn.innerHTML = '<i data-lucide="square"></i>';
|
|
426
|
+
} else {
|
|
427
|
+
sendBtn.classList.remove("stop");
|
|
428
|
+
sendBtn.innerHTML = '<i data-lucide="arrow-up"></i>';
|
|
429
|
+
}
|
|
430
|
+
refreshIcons();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
var ioTimer = null;
|
|
434
|
+
function blinkIO() {
|
|
435
|
+
if (!connected) return;
|
|
436
|
+
var dot = getStatusDot();
|
|
437
|
+
if (dot) dot.classList.add("io");
|
|
438
|
+
clearTimeout(ioTimer);
|
|
439
|
+
ioTimer = setTimeout(function () { var d = getStatusDot(); if (d) d.classList.remove("io"); }, 80);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// --- Urgent favicon blink (permission / ask user) ---
|
|
443
|
+
var urgentBlinkTimer = null;
|
|
444
|
+
var savedTitle = null;
|
|
445
|
+
function startUrgentBlink() {
|
|
446
|
+
if (urgentBlinkTimer) return;
|
|
447
|
+
savedTitle = document.title;
|
|
448
|
+
var tick = 0;
|
|
449
|
+
urgentBlinkTimer = setInterval(function () {
|
|
450
|
+
var on = tick % 2 === 0;
|
|
451
|
+
updateFavicon(on ? getComputedVar("--error") : null);
|
|
452
|
+
document.title = on ? "\u26A0 Input needed" : savedTitle;
|
|
453
|
+
tick++;
|
|
454
|
+
}, 180);
|
|
455
|
+
}
|
|
456
|
+
function stopUrgentBlink() {
|
|
457
|
+
if (!urgentBlinkTimer) return;
|
|
458
|
+
clearInterval(urgentBlinkTimer);
|
|
459
|
+
urgentBlinkTimer = null;
|
|
460
|
+
updateFavicon(null);
|
|
461
|
+
if (savedTitle) document.title = savedTitle;
|
|
462
|
+
savedTitle = null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function setStatus(status) {
|
|
466
|
+
var dot = getStatusDot();
|
|
467
|
+
if (dot) dot.className = "icon-strip-status";
|
|
468
|
+
if (status === "connected") {
|
|
469
|
+
if (dot) dot.classList.add("connected");
|
|
470
|
+
connected = true;
|
|
471
|
+
processing = false;
|
|
472
|
+
sendBtn.disabled = false;
|
|
473
|
+
setSendBtnMode("send");
|
|
474
|
+
connectOverlay.classList.add("hidden");
|
|
475
|
+
stopVerbCycle();
|
|
476
|
+
} else if (status === "processing") {
|
|
477
|
+
if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
|
|
478
|
+
processing = true;
|
|
479
|
+
setSendBtnMode("stop");
|
|
480
|
+
} else {
|
|
481
|
+
connected = false;
|
|
482
|
+
sendBtn.disabled = true;
|
|
483
|
+
connectOverlay.classList.remove("hidden");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function setActivity(text) {
|
|
488
|
+
if (text) {
|
|
489
|
+
if (!activityEl) {
|
|
490
|
+
activityEl = document.createElement("div");
|
|
491
|
+
activityEl.className = "activity-inline";
|
|
492
|
+
activityEl.innerHTML =
|
|
493
|
+
'<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
|
|
494
|
+
'<span class="activity-text"></span>';
|
|
495
|
+
addToMessages(activityEl);
|
|
496
|
+
refreshIcons();
|
|
497
|
+
}
|
|
498
|
+
activityEl.querySelector(".activity-text").textContent = text;
|
|
499
|
+
scrollToBottom();
|
|
500
|
+
} else {
|
|
501
|
+
if (activityEl) {
|
|
502
|
+
activityEl.remove();
|
|
503
|
+
activityEl = null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// --- Config chip (model + mode + effort) ---
|
|
509
|
+
var configChipWrap = $("config-chip-wrap");
|
|
510
|
+
var configChip = $("config-chip");
|
|
511
|
+
var configChipLabel = $("config-chip-label");
|
|
512
|
+
var configPopover = $("config-popover");
|
|
513
|
+
var configModelList = $("config-model-list");
|
|
514
|
+
var configModeList = $("config-mode-list");
|
|
515
|
+
var configEffortSection = $("config-effort-section");
|
|
516
|
+
var configEffortBar = $("config-effort-bar");
|
|
517
|
+
|
|
518
|
+
var configBetaSection = $("config-beta-section");
|
|
519
|
+
var configBeta1mBtn = $("config-beta-1m");
|
|
520
|
+
|
|
521
|
+
var currentModels = [];
|
|
522
|
+
var currentModel = "";
|
|
523
|
+
var currentMode = "default";
|
|
524
|
+
var currentEffort = "high";
|
|
525
|
+
var currentBetas = [];
|
|
526
|
+
var skipPermsEnabled = false;
|
|
527
|
+
|
|
528
|
+
var MODE_OPTIONS = [
|
|
529
|
+
{ value: "default", label: "Default" },
|
|
530
|
+
{ value: "plan", label: "Plan" },
|
|
531
|
+
{ value: "acceptEdits", label: "Auto-accept edits" },
|
|
532
|
+
];
|
|
533
|
+
var MODE_FULL_AUTO = { value: "bypassPermissions", label: "Full auto" };
|
|
534
|
+
|
|
535
|
+
var EFFORT_LEVELS = ["low", "medium", "high", "max"];
|
|
536
|
+
|
|
537
|
+
function modelDisplayName(value, models) {
|
|
538
|
+
if (!value) return "";
|
|
539
|
+
if (models) {
|
|
540
|
+
for (var i = 0; i < models.length; i++) {
|
|
541
|
+
if (models[i].value === value && models[i].displayName) return models[i].displayName;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return value;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function modeDisplayName(value) {
|
|
548
|
+
for (var i = 0; i < MODE_OPTIONS.length; i++) {
|
|
549
|
+
if (MODE_OPTIONS[i].value === value) return MODE_OPTIONS[i].label;
|
|
550
|
+
}
|
|
551
|
+
if (value === "bypassPermissions") return "Full auto";
|
|
552
|
+
if (value === "dontAsk") return "Don\u2019t ask";
|
|
553
|
+
return value;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function effortDisplayName(value) {
|
|
557
|
+
if (!value) return "";
|
|
558
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isSonnetModel(model) {
|
|
562
|
+
if (!model) return false;
|
|
563
|
+
var lower = model.toLowerCase();
|
|
564
|
+
return lower.indexOf("sonnet") !== -1;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function hasBeta(name) {
|
|
568
|
+
for (var i = 0; i < currentBetas.length; i++) {
|
|
569
|
+
if (currentBetas[i].indexOf(name) !== -1) return true;
|
|
570
|
+
}
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function updateConfigChip() {
|
|
575
|
+
if (!configChipWrap || !configChip) return;
|
|
576
|
+
configChipWrap.classList.remove("hidden");
|
|
577
|
+
var parts = [modelDisplayName(currentModel, currentModels)];
|
|
578
|
+
parts.push(modeDisplayName(currentMode));
|
|
579
|
+
// Only show effort if model supports it
|
|
580
|
+
var modelSupportsEffort = getModelSupportsEffort();
|
|
581
|
+
if (modelSupportsEffort) {
|
|
582
|
+
parts.push(effortDisplayName(currentEffort));
|
|
583
|
+
}
|
|
584
|
+
if (hasBeta("context-1m")) {
|
|
585
|
+
parts.push("1M");
|
|
586
|
+
}
|
|
587
|
+
configChipLabel.textContent = parts.join(" \u00b7 ");
|
|
588
|
+
rebuildModelList();
|
|
589
|
+
rebuildModeList();
|
|
590
|
+
rebuildEffortBar();
|
|
591
|
+
rebuildBetaSection();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function getModelSupportsEffort() {
|
|
595
|
+
if (!currentModels || currentModels.length === 0) return true; // assume yes if no info
|
|
596
|
+
for (var i = 0; i < currentModels.length; i++) {
|
|
597
|
+
if (currentModels[i].value === currentModel) {
|
|
598
|
+
if (currentModels[i].supportsEffort === false) return false;
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function getModelEffortLevels() {
|
|
606
|
+
if (!currentModels || currentModels.length === 0) return EFFORT_LEVELS;
|
|
607
|
+
for (var i = 0; i < currentModels.length; i++) {
|
|
608
|
+
if (currentModels[i].value === currentModel) {
|
|
609
|
+
if (currentModels[i].supportedEffortLevels && currentModels[i].supportedEffortLevels.length > 0) {
|
|
610
|
+
return currentModels[i].supportedEffortLevels;
|
|
611
|
+
}
|
|
612
|
+
return EFFORT_LEVELS;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return EFFORT_LEVELS;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function rebuildModelList() {
|
|
619
|
+
if (!configModelList) return;
|
|
620
|
+
configModelList.innerHTML = "";
|
|
621
|
+
var list = currentModels.length > 0 ? currentModels : (currentModel ? [{ value: currentModel, displayName: currentModel }] : []);
|
|
622
|
+
for (var i = 0; i < list.length; i++) {
|
|
623
|
+
var item = list[i];
|
|
624
|
+
var value = item.value || "";
|
|
625
|
+
var label = item.displayName || value;
|
|
626
|
+
var btn = document.createElement("button");
|
|
627
|
+
btn.className = "config-radio-item";
|
|
628
|
+
if (value === currentModel) btn.classList.add("active");
|
|
629
|
+
btn.dataset.model = value;
|
|
630
|
+
btn.textContent = label;
|
|
631
|
+
btn.addEventListener("click", function () {
|
|
632
|
+
var model = this.dataset.model;
|
|
633
|
+
if (ws && ws.readyState === 1) {
|
|
634
|
+
ws.send(JSON.stringify({ type: "set_model", model: model }));
|
|
635
|
+
}
|
|
636
|
+
configPopover.classList.add("hidden");
|
|
637
|
+
configChip.classList.remove("active");
|
|
638
|
+
});
|
|
639
|
+
configModelList.appendChild(btn);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function rebuildModeList() {
|
|
644
|
+
if (!configModeList) return;
|
|
645
|
+
configModeList.innerHTML = "";
|
|
646
|
+
var options = MODE_OPTIONS.slice();
|
|
647
|
+
if (skipPermsEnabled) {
|
|
648
|
+
options.push(MODE_FULL_AUTO);
|
|
649
|
+
}
|
|
650
|
+
for (var i = 0; i < options.length; i++) {
|
|
651
|
+
var opt = options[i];
|
|
652
|
+
var btn = document.createElement("button");
|
|
653
|
+
btn.className = "config-radio-item";
|
|
654
|
+
if (opt.value === currentMode) btn.classList.add("active");
|
|
655
|
+
btn.dataset.mode = opt.value;
|
|
656
|
+
btn.textContent = opt.label;
|
|
657
|
+
btn.addEventListener("click", function () {
|
|
658
|
+
var mode = this.dataset.mode;
|
|
659
|
+
if (ws && ws.readyState === 1) {
|
|
660
|
+
ws.send(JSON.stringify({ type: "set_permission_mode", mode: mode }));
|
|
661
|
+
}
|
|
662
|
+
configPopover.classList.add("hidden");
|
|
663
|
+
configChip.classList.remove("active");
|
|
664
|
+
});
|
|
665
|
+
configModeList.appendChild(btn);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function rebuildEffortBar() {
|
|
670
|
+
if (!configEffortBar || !configEffortSection) return;
|
|
671
|
+
var supportsEffort = getModelSupportsEffort();
|
|
672
|
+
if (!supportsEffort) {
|
|
673
|
+
configEffortSection.style.display = "none";
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
configEffortSection.style.display = "";
|
|
677
|
+
configEffortBar.innerHTML = "";
|
|
678
|
+
var levels = getModelEffortLevels();
|
|
679
|
+
for (var i = 0; i < levels.length; i++) {
|
|
680
|
+
var level = levels[i];
|
|
681
|
+
var btn = document.createElement("button");
|
|
682
|
+
btn.className = "config-segment-btn";
|
|
683
|
+
if (level === currentEffort) btn.classList.add("active");
|
|
684
|
+
btn.dataset.effort = level;
|
|
685
|
+
btn.textContent = effortDisplayName(level);
|
|
686
|
+
btn.addEventListener("click", function () {
|
|
687
|
+
var effort = this.dataset.effort;
|
|
688
|
+
if (ws && ws.readyState === 1) {
|
|
689
|
+
ws.send(JSON.stringify({ type: "set_effort", effort: effort }));
|
|
690
|
+
}
|
|
691
|
+
configPopover.classList.add("hidden");
|
|
692
|
+
configChip.classList.remove("active");
|
|
693
|
+
});
|
|
694
|
+
configEffortBar.appendChild(btn);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function rebuildBetaSection() {
|
|
699
|
+
if (!configBetaSection || !configBeta1mBtn) return;
|
|
700
|
+
// Only show for Sonnet models
|
|
701
|
+
if (!isSonnetModel(currentModel)) {
|
|
702
|
+
configBetaSection.style.display = "none";
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
configBetaSection.style.display = "";
|
|
706
|
+
var active = hasBeta("context-1m");
|
|
707
|
+
configBeta1mBtn.classList.toggle("active", active);
|
|
708
|
+
configBeta1mBtn.setAttribute("aria-checked", active ? "true" : "false");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
configBeta1mBtn.addEventListener("click", function (e) {
|
|
712
|
+
e.stopPropagation();
|
|
713
|
+
var active = hasBeta("context-1m");
|
|
714
|
+
var newBetas;
|
|
715
|
+
if (active) {
|
|
716
|
+
// Remove context-1m beta
|
|
717
|
+
newBetas = [];
|
|
718
|
+
for (var i = 0; i < currentBetas.length; i++) {
|
|
719
|
+
if (currentBetas[i].indexOf("context-1m") === -1) {
|
|
720
|
+
newBetas.push(currentBetas[i]);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
// Add context-1m beta
|
|
725
|
+
newBetas = currentBetas.slice();
|
|
726
|
+
newBetas.push("context-1m-2025-08-07");
|
|
727
|
+
}
|
|
728
|
+
if (ws && ws.readyState === 1) {
|
|
729
|
+
ws.send(JSON.stringify({ type: "set_betas", betas: newBetas }));
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
configChip.addEventListener("click", function (e) {
|
|
734
|
+
e.stopPropagation();
|
|
735
|
+
var wasHidden = configPopover.classList.toggle("hidden");
|
|
736
|
+
configChip.classList.toggle("active", !wasHidden);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
document.addEventListener("click", function (e) {
|
|
740
|
+
if (!configPopover.contains(e.target) && e.target !== configChip) {
|
|
741
|
+
configPopover.classList.add("hidden");
|
|
742
|
+
configChip.classList.remove("active");
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// --- Usage panel ---
|
|
747
|
+
var usagePanel = $("usage-panel");
|
|
748
|
+
var usagePanelClose = $("usage-panel-close");
|
|
749
|
+
var usageCostEl = $("usage-cost");
|
|
750
|
+
var usageInputEl = $("usage-input");
|
|
751
|
+
var usageOutputEl = $("usage-output");
|
|
752
|
+
var usageCacheReadEl = $("usage-cache-read");
|
|
753
|
+
var usageCacheWriteEl = $("usage-cache-write");
|
|
754
|
+
var usageTurnsEl = $("usage-turns");
|
|
755
|
+
var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
756
|
+
|
|
757
|
+
function formatTokens(n) {
|
|
758
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
|
|
759
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
|
|
760
|
+
return String(n);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function updateUsagePanel() {
|
|
764
|
+
if (!usageCostEl) return;
|
|
765
|
+
usageCostEl.textContent = "$" + sessionUsage.cost.toFixed(4);
|
|
766
|
+
usageInputEl.textContent = formatTokens(sessionUsage.input);
|
|
767
|
+
usageOutputEl.textContent = formatTokens(sessionUsage.output);
|
|
768
|
+
usageCacheReadEl.textContent = formatTokens(sessionUsage.cacheRead);
|
|
769
|
+
usageCacheWriteEl.textContent = formatTokens(sessionUsage.cacheWrite);
|
|
770
|
+
usageTurnsEl.textContent = String(sessionUsage.turns);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function accumulateUsage(cost, usage) {
|
|
774
|
+
if (cost != null) sessionUsage.cost += cost;
|
|
775
|
+
if (usage) {
|
|
776
|
+
sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
|
|
777
|
+
sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
|
|
778
|
+
sessionUsage.cacheRead += usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
|
|
779
|
+
sessionUsage.cacheWrite += usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
|
|
780
|
+
}
|
|
781
|
+
sessionUsage.turns++;
|
|
782
|
+
if (!replayingHistory) updateUsagePanel();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function resetUsage() {
|
|
786
|
+
sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
787
|
+
updateUsagePanel();
|
|
788
|
+
if (usagePanel) usagePanel.classList.add("hidden");
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function toggleUsagePanel() {
|
|
792
|
+
if (!usagePanel) return;
|
|
793
|
+
usagePanel.classList.toggle("hidden");
|
|
794
|
+
refreshIcons();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (usagePanelClose) {
|
|
798
|
+
usagePanelClose.addEventListener("click", function () {
|
|
799
|
+
usagePanel.classList.add("hidden");
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// --- Status panel ---
|
|
804
|
+
var statusPanel = $("status-panel");
|
|
805
|
+
var statusPanelClose = $("status-panel-close");
|
|
806
|
+
var statusPidEl = $("status-pid");
|
|
807
|
+
var statusUptimeEl = $("status-uptime");
|
|
808
|
+
var statusRssEl = $("status-rss");
|
|
809
|
+
var statusHeapUsedEl = $("status-heap-used");
|
|
810
|
+
var statusHeapTotalEl = $("status-heap-total");
|
|
811
|
+
var statusExternalEl = $("status-external");
|
|
812
|
+
var statusSessionsEl = $("status-sessions");
|
|
813
|
+
var statusProcessingEl = $("status-processing");
|
|
814
|
+
var statusClientsEl = $("status-clients");
|
|
815
|
+
var statusTerminalsEl = $("status-terminals");
|
|
816
|
+
var statusRefreshTimer = null;
|
|
817
|
+
|
|
818
|
+
function formatBytes(n) {
|
|
819
|
+
if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
|
|
820
|
+
if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
|
|
821
|
+
if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
|
|
822
|
+
return n + " B";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function formatUptime(seconds) {
|
|
826
|
+
var d = Math.floor(seconds / 86400);
|
|
827
|
+
var h = Math.floor((seconds % 86400) / 3600);
|
|
828
|
+
var m = Math.floor((seconds % 3600) / 60);
|
|
829
|
+
var s = Math.floor(seconds % 60);
|
|
830
|
+
if (d > 0) return d + "d " + h + "h " + m + "m";
|
|
831
|
+
if (h > 0) return h + "h " + m + "m " + s + "s";
|
|
832
|
+
return m + "m " + s + "s";
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function updateStatusPanel(data) {
|
|
836
|
+
if (!statusPidEl) return;
|
|
837
|
+
statusPidEl.textContent = String(data.pid);
|
|
838
|
+
statusUptimeEl.textContent = formatUptime(data.uptime);
|
|
839
|
+
statusRssEl.textContent = formatBytes(data.memory.rss);
|
|
840
|
+
statusHeapUsedEl.textContent = formatBytes(data.memory.heapUsed);
|
|
841
|
+
statusHeapTotalEl.textContent = formatBytes(data.memory.heapTotal);
|
|
842
|
+
statusExternalEl.textContent = formatBytes(data.memory.external);
|
|
843
|
+
statusSessionsEl.textContent = String(data.sessions);
|
|
844
|
+
statusProcessingEl.textContent = String(data.processing);
|
|
845
|
+
statusClientsEl.textContent = String(data.clients);
|
|
846
|
+
statusTerminalsEl.textContent = String(data.terminals);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function requestProcessStats() {
|
|
850
|
+
if (ws && ws.readyState === 1) {
|
|
851
|
+
ws.send(JSON.stringify({ type: "process_stats" }));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function toggleStatusPanel() {
|
|
856
|
+
if (!statusPanel) return;
|
|
857
|
+
var opening = statusPanel.classList.contains("hidden");
|
|
858
|
+
statusPanel.classList.toggle("hidden");
|
|
859
|
+
if (opening) {
|
|
860
|
+
requestProcessStats();
|
|
861
|
+
statusRefreshTimer = setInterval(requestProcessStats, 5000);
|
|
862
|
+
} else {
|
|
863
|
+
if (statusRefreshTimer) {
|
|
864
|
+
clearInterval(statusRefreshTimer);
|
|
865
|
+
statusRefreshTimer = null;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
refreshIcons();
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (statusPanelClose) {
|
|
872
|
+
statusPanelClose.addEventListener("click", function () {
|
|
873
|
+
statusPanel.classList.add("hidden");
|
|
874
|
+
if (statusRefreshTimer) {
|
|
875
|
+
clearInterval(statusRefreshTimer);
|
|
876
|
+
statusRefreshTimer = null;
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// --- Context panel ---
|
|
882
|
+
var contextPanel = $("context-panel");
|
|
883
|
+
var contextPanelClose = $("context-panel-close");
|
|
884
|
+
var contextPanelMinimize = $("context-panel-minimize");
|
|
885
|
+
var contextBarFill = $("context-bar-fill");
|
|
886
|
+
var contextBarPct = $("context-bar-pct");
|
|
887
|
+
var contextUsedEl = $("context-used");
|
|
888
|
+
var contextWindowEl = $("context-window");
|
|
889
|
+
var contextMaxOutputEl = $("context-max-output");
|
|
890
|
+
var contextInputEl = $("context-input");
|
|
891
|
+
var contextOutputEl = $("context-output");
|
|
892
|
+
var contextCacheReadEl = $("context-cache-read");
|
|
893
|
+
var contextCacheWriteEl = $("context-cache-write");
|
|
894
|
+
var contextModelEl = $("context-model");
|
|
895
|
+
var contextCostEl = $("context-cost");
|
|
896
|
+
var contextTurnsEl = $("context-turns");
|
|
897
|
+
var contextMini = $("context-mini");
|
|
898
|
+
var contextMiniFill = $("context-mini-fill");
|
|
899
|
+
var contextMiniLabel = $("context-mini-label");
|
|
900
|
+
var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
901
|
+
var headerContextEl = null;
|
|
902
|
+
|
|
903
|
+
// Known context window sizes per model (fallback when SDK omits feature flag)
|
|
904
|
+
var KNOWN_CONTEXT_WINDOWS = {
|
|
905
|
+
"opus-4-6": 1000000,
|
|
906
|
+
"claude-sonnet-4": 1000000
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
function resolveContextWindow(model, sdkValue) {
|
|
910
|
+
var lc = (model || "").toLowerCase();
|
|
911
|
+
for (var key in KNOWN_CONTEXT_WINDOWS) {
|
|
912
|
+
if (lc.includes(key)) return Math.max(sdkValue || 0, KNOWN_CONTEXT_WINDOWS[key]);
|
|
913
|
+
}
|
|
914
|
+
return sdkValue || 200000;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function contextPctClass(pct) {
|
|
918
|
+
return pct >= 85 ? " danger" : pct >= 60 ? " warn" : "";
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function updateContextPanel() {
|
|
922
|
+
if (!contextUsedEl) return;
|
|
923
|
+
// Context window usage = input tokens (includes cache read/write) + output tokens
|
|
924
|
+
var used = contextData.input + contextData.output;
|
|
925
|
+
var win = contextData.contextWindow;
|
|
926
|
+
var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
|
|
927
|
+
var cls = contextPctClass(pct);
|
|
928
|
+
// Panel bar
|
|
929
|
+
contextBarFill.style.width = pct.toFixed(1) + "%";
|
|
930
|
+
contextBarFill.className = "context-bar-fill" + cls;
|
|
931
|
+
contextBarPct.textContent = pct.toFixed(0) + "%";
|
|
932
|
+
// Mini bar
|
|
933
|
+
if (contextMiniFill) {
|
|
934
|
+
contextMiniFill.style.width = pct.toFixed(1) + "%";
|
|
935
|
+
contextMiniFill.className = "context-mini-fill" + cls;
|
|
936
|
+
}
|
|
937
|
+
if (contextMiniLabel) {
|
|
938
|
+
contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
|
|
939
|
+
}
|
|
940
|
+
// Header bar
|
|
941
|
+
if (pct > 0) {
|
|
942
|
+
var statusArea = document.querySelector(".title-bar-content .status");
|
|
943
|
+
if (statusArea && !headerContextEl) {
|
|
944
|
+
headerContextEl = document.createElement("div");
|
|
945
|
+
headerContextEl.className = "header-context";
|
|
946
|
+
headerContextEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
|
|
947
|
+
statusArea.insertBefore(headerContextEl, statusArea.firstChild);
|
|
948
|
+
}
|
|
949
|
+
if (headerContextEl) {
|
|
950
|
+
var hFill = headerContextEl.querySelector(".header-context-fill");
|
|
951
|
+
var hLabel = headerContextEl.querySelector(".header-context-label");
|
|
952
|
+
hFill.style.width = pct.toFixed(1) + "%";
|
|
953
|
+
hFill.className = "header-context-fill" + cls;
|
|
954
|
+
hLabel.textContent = pct.toFixed(0) + "%";
|
|
955
|
+
headerContextEl.dataset.tip = "Context window " + pct.toFixed(0) + "% used (" + formatTokens(used) + " / " + formatTokens(win) + " tokens)";
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
contextUsedEl.textContent = formatTokens(used);
|
|
959
|
+
contextWindowEl.textContent = win > 0 ? formatTokens(win) : "-";
|
|
960
|
+
contextMaxOutputEl.textContent = contextData.maxOutputTokens > 0 ? formatTokens(contextData.maxOutputTokens) : "-";
|
|
961
|
+
contextInputEl.textContent = formatTokens(contextData.input);
|
|
962
|
+
contextOutputEl.textContent = formatTokens(contextData.output);
|
|
963
|
+
contextCacheReadEl.textContent = formatTokens(contextData.cacheRead);
|
|
964
|
+
contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
|
|
965
|
+
contextModelEl.textContent = contextData.model;
|
|
966
|
+
contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
|
|
967
|
+
contextTurnsEl.textContent = String(contextData.turns);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function accumulateContext(cost, usage, modelUsage) {
|
|
971
|
+
if (cost != null) contextData.cost += cost;
|
|
972
|
+
// Use latest turn values (not cumulative) since each turn's input_tokens
|
|
973
|
+
// already includes the full conversation context up to that point
|
|
974
|
+
if (usage) {
|
|
975
|
+
contextData.input = (usage.input_tokens || usage.inputTokens || 0)
|
|
976
|
+
+ (usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
|
|
977
|
+
contextData.output = usage.output_tokens || usage.outputTokens || 0;
|
|
978
|
+
contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
|
|
979
|
+
contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
|
|
980
|
+
}
|
|
981
|
+
contextData.turns++;
|
|
982
|
+
if (modelUsage) {
|
|
983
|
+
var models = Object.keys(modelUsage);
|
|
984
|
+
if (models.length > 0) {
|
|
985
|
+
var m = models[0];
|
|
986
|
+
var mu = modelUsage[m];
|
|
987
|
+
contextData.model = m;
|
|
988
|
+
contextData.contextWindow = resolveContextWindow(m, mu.contextWindow);
|
|
989
|
+
if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (!replayingHistory) updateContextPanel();
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// contextView: "off" | "mini" | "panel"
|
|
996
|
+
function getContextView() {
|
|
997
|
+
try { return localStorage.getItem("clay-context-view") || "off"; } catch (e) { return "off"; }
|
|
998
|
+
}
|
|
999
|
+
function setContextView(v) {
|
|
1000
|
+
try { localStorage.setItem("clay-context-view", v); } catch (e) {}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function applyContextView(view) {
|
|
1004
|
+
if (contextPanel) contextPanel.classList.toggle("hidden", view !== "panel");
|
|
1005
|
+
if (contextMini) contextMini.classList.toggle("hidden", view !== "mini");
|
|
1006
|
+
if (view === "panel") refreshIcons();
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function resetContextData() {
|
|
1010
|
+
contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
1011
|
+
updateContextPanel();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function resetContext() {
|
|
1015
|
+
resetContextData();
|
|
1016
|
+
// Keep view state, just reset data
|
|
1017
|
+
applyContextView(getContextView());
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function minimizeContext() {
|
|
1021
|
+
setContextView("mini");
|
|
1022
|
+
applyContextView("mini");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function expandContext() {
|
|
1026
|
+
setContextView("panel");
|
|
1027
|
+
applyContextView("panel");
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function toggleContextPanel() {
|
|
1031
|
+
if (!contextPanel) return;
|
|
1032
|
+
var view = getContextView();
|
|
1033
|
+
if (view === "panel") {
|
|
1034
|
+
setContextView("mini");
|
|
1035
|
+
applyContextView("mini");
|
|
1036
|
+
} else {
|
|
1037
|
+
setContextView("panel");
|
|
1038
|
+
applyContextView("panel");
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (contextPanelClose) {
|
|
1043
|
+
contextPanelClose.addEventListener("click", function () {
|
|
1044
|
+
setContextView("off");
|
|
1045
|
+
applyContextView("off");
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (contextPanelMinimize) {
|
|
1050
|
+
contextPanelMinimize.addEventListener("click", minimizeContext);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Restore context view on load
|
|
1054
|
+
applyContextView(getContextView());
|
|
1055
|
+
|
|
1056
|
+
if (contextMini) {
|
|
1057
|
+
contextMini.addEventListener("click", expandContext);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function addToMessages(el) {
|
|
1061
|
+
if (prependAnchor) messagesEl.insertBefore(el, prependAnchor);
|
|
1062
|
+
else messagesEl.appendChild(el);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
var newMsgBtn = $("new-msg-btn");
|
|
1066
|
+
var newMsgBtnDefault = "\u2193 Latest";
|
|
1067
|
+
var newMsgBtnActivity = "\u2193 New activity";
|
|
1068
|
+
|
|
1069
|
+
messagesEl.addEventListener("scroll", function () {
|
|
1070
|
+
var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
|
|
1071
|
+
isUserScrolledUp = distFromBottom > scrollThreshold;
|
|
1072
|
+
if (isUserScrolledUp) {
|
|
1073
|
+
if (newMsgBtn.classList.contains("hidden")) {
|
|
1074
|
+
newMsgBtn.textContent = newMsgBtnDefault;
|
|
1075
|
+
}
|
|
1076
|
+
newMsgBtn.classList.remove("hidden");
|
|
1077
|
+
} else {
|
|
1078
|
+
newMsgBtn.classList.add("hidden");
|
|
1079
|
+
newMsgBtn.textContent = newMsgBtnDefault;
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
newMsgBtn.addEventListener("click", function () {
|
|
1084
|
+
forceScrollToBottom();
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
function scrollToBottom() {
|
|
1088
|
+
if (prependAnchor) return;
|
|
1089
|
+
if (isUserScrolledUp) {
|
|
1090
|
+
newMsgBtn.textContent = newMsgBtnActivity;
|
|
1091
|
+
newMsgBtn.classList.remove("hidden");
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
requestAnimationFrame(function () {
|
|
1095
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function forceScrollToBottom() {
|
|
1100
|
+
if (prependAnchor) return;
|
|
1101
|
+
isUserScrolledUp = false;
|
|
1102
|
+
newMsgBtn.classList.add("hidden");
|
|
1103
|
+
newMsgBtn.textContent = newMsgBtnDefault;
|
|
1104
|
+
requestAnimationFrame(function () {
|
|
1105
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// --- Tools module ---
|
|
1110
|
+
initTools({
|
|
1111
|
+
$: $,
|
|
1112
|
+
get ws() { return ws; },
|
|
1113
|
+
get connected() { return connected; },
|
|
1114
|
+
get turnCounter() { return turnCounter; },
|
|
1115
|
+
messagesEl: messagesEl,
|
|
1116
|
+
inputEl: inputEl,
|
|
1117
|
+
finalizeAssistantBlock: function() { finalizeAssistantBlock(); },
|
|
1118
|
+
addToMessages: function(el) { addToMessages(el); },
|
|
1119
|
+
scrollToBottom: function() { scrollToBottom(); },
|
|
1120
|
+
setActivity: function(text) { setActivity(text); },
|
|
1121
|
+
stopUrgentBlink: function() { stopUrgentBlink(); },
|
|
1122
|
+
getContextPercent: function() {
|
|
1123
|
+
var used = contextData.input + contextData.output;
|
|
1124
|
+
var win = contextData.contextWindow;
|
|
1125
|
+
return win > 0 ? Math.round((used / win) * 100) : 0;
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// isPlanFile, toolSummary, toolActivityText, shortPath -> modules/tools.js
|
|
1130
|
+
|
|
1131
|
+
// AskUserQuestion, PermissionRequest, Plan, Todo, Thinking, Tool items -> modules/tools.js
|
|
1132
|
+
|
|
1133
|
+
// --- DOM: Messages ---
|
|
1134
|
+
function addUserMessage(text, images, pastes) {
|
|
1135
|
+
var div = document.createElement("div");
|
|
1136
|
+
div.className = "msg-user";
|
|
1137
|
+
div.dataset.turn = ++turnCounter;
|
|
1138
|
+
var bubble = document.createElement("div");
|
|
1139
|
+
bubble.className = "bubble";
|
|
1140
|
+
bubble.dir = "auto";
|
|
1141
|
+
|
|
1142
|
+
if (images && images.length > 0) {
|
|
1143
|
+
var imgRow = document.createElement("div");
|
|
1144
|
+
imgRow.className = "bubble-images";
|
|
1145
|
+
for (var i = 0; i < images.length; i++) {
|
|
1146
|
+
var img = document.createElement("img");
|
|
1147
|
+
img.src = "data:" + images[i].mediaType + ";base64," + images[i].data;
|
|
1148
|
+
img.className = "bubble-img";
|
|
1149
|
+
img.addEventListener("click", function () { showImageModal(this.src); });
|
|
1150
|
+
imgRow.appendChild(img);
|
|
1151
|
+
}
|
|
1152
|
+
bubble.appendChild(imgRow);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (pastes && pastes.length > 0) {
|
|
1156
|
+
var pasteRow = document.createElement("div");
|
|
1157
|
+
pasteRow.className = "bubble-pastes";
|
|
1158
|
+
for (var p = 0; p < pastes.length; p++) {
|
|
1159
|
+
(function (pasteText) {
|
|
1160
|
+
var chip = document.createElement("div");
|
|
1161
|
+
chip.className = "bubble-paste";
|
|
1162
|
+
var preview = pasteText.substring(0, 60).replace(/\n/g, " ");
|
|
1163
|
+
if (pasteText.length > 60) preview += "...";
|
|
1164
|
+
chip.innerHTML = '<span class="bubble-paste-preview">' + escapeHtml(preview) + '</span><span class="bubble-paste-label">PASTED</span>';
|
|
1165
|
+
chip.addEventListener("click", function (e) {
|
|
1166
|
+
e.stopPropagation();
|
|
1167
|
+
showPasteModal(pasteText);
|
|
1168
|
+
});
|
|
1169
|
+
pasteRow.appendChild(chip);
|
|
1170
|
+
})(pastes[p]);
|
|
1171
|
+
}
|
|
1172
|
+
bubble.appendChild(pasteRow);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (text) {
|
|
1176
|
+
var textEl = document.createElement("span");
|
|
1177
|
+
textEl.textContent = text;
|
|
1178
|
+
bubble.appendChild(textEl);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
div.appendChild(bubble);
|
|
1182
|
+
|
|
1183
|
+
// Action bar below bubble (icons visible on hover)
|
|
1184
|
+
var actions = document.createElement("div");
|
|
1185
|
+
actions.className = "msg-actions";
|
|
1186
|
+
var now = new Date();
|
|
1187
|
+
var timeStr = String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0");
|
|
1188
|
+
actions.innerHTML =
|
|
1189
|
+
'<span class="msg-action-time">' + timeStr + '</span>' +
|
|
1190
|
+
'<button class="msg-action-btn msg-action-copy" type="button" title="Copy">' + iconHtml("copy") + '</button>' +
|
|
1191
|
+
'<button class="msg-action-btn msg-action-hidden msg-action-fork" type="button" title="Fork">' + iconHtml("git-branch") + '</button>' +
|
|
1192
|
+
'<button class="msg-action-btn msg-action-rewind msg-user-rewind-btn" type="button" title="Rewind">' + iconHtml("rotate-ccw") + '</button>' +
|
|
1193
|
+
'<button class="msg-action-btn msg-action-hidden msg-action-edit" type="button" title="Edit">' + iconHtml("pencil") + '</button>';
|
|
1194
|
+
div.appendChild(actions);
|
|
1195
|
+
|
|
1196
|
+
// Copy handler
|
|
1197
|
+
actions.querySelector(".msg-action-copy").addEventListener("click", function () {
|
|
1198
|
+
var self = this;
|
|
1199
|
+
copyToClipboard(text || "");
|
|
1200
|
+
self.innerHTML = iconHtml("check");
|
|
1201
|
+
refreshIcons();
|
|
1202
|
+
setTimeout(function () { self.innerHTML = iconHtml("copy"); refreshIcons(); }, 1200);
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
addToMessages(div);
|
|
1206
|
+
refreshIcons();
|
|
1207
|
+
forceScrollToBottom();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function ensureAssistantBlock() {
|
|
1211
|
+
if (!currentMsgEl) {
|
|
1212
|
+
currentMsgEl = document.createElement("div");
|
|
1213
|
+
currentMsgEl.className = "msg-assistant";
|
|
1214
|
+
currentMsgEl.dataset.turn = turnCounter;
|
|
1215
|
+
currentMsgEl.innerHTML = '<div class="md-content" dir="auto"></div>';
|
|
1216
|
+
addToMessages(currentMsgEl);
|
|
1217
|
+
currentFullText = "";
|
|
1218
|
+
}
|
|
1219
|
+
return currentMsgEl;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function addCopyHandler(msgEl, rawText) {
|
|
1223
|
+
var primed = false;
|
|
1224
|
+
var resetTimer = null;
|
|
1225
|
+
|
|
1226
|
+
var isTouchDevice = "ontouchstart" in window;
|
|
1227
|
+
|
|
1228
|
+
var hint = document.createElement("div");
|
|
1229
|
+
hint.className = "msg-copy-hint";
|
|
1230
|
+
hint.textContent = (isTouchDevice ? "Tap" : "Click") + " to grab this";
|
|
1231
|
+
msgEl.appendChild(hint);
|
|
1232
|
+
|
|
1233
|
+
function reset() {
|
|
1234
|
+
primed = false;
|
|
1235
|
+
msgEl.classList.remove("copy-primed", "copy-done");
|
|
1236
|
+
hint.textContent = (isTouchDevice ? "Tap" : "Click") + " to grab this";
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
msgEl.addEventListener("click", function (e) {
|
|
1240
|
+
// Don't intercept clicks on links or code blocks
|
|
1241
|
+
if (e.target.closest("a, pre, code")) return;
|
|
1242
|
+
// Don't intercept text selection
|
|
1243
|
+
var sel = window.getSelection();
|
|
1244
|
+
if (sel && sel.toString().length > 0) return;
|
|
1245
|
+
|
|
1246
|
+
if (!primed) {
|
|
1247
|
+
primed = true;
|
|
1248
|
+
msgEl.classList.add("copy-primed");
|
|
1249
|
+
hint.textContent = isTouchDevice ? "Tap again to grab" : "Click again to grab";
|
|
1250
|
+
clearTimeout(resetTimer);
|
|
1251
|
+
resetTimer = setTimeout(reset, 3000);
|
|
1252
|
+
} else {
|
|
1253
|
+
clearTimeout(resetTimer);
|
|
1254
|
+
copyToClipboard(rawText).then(function () {
|
|
1255
|
+
msgEl.classList.remove("copy-primed");
|
|
1256
|
+
msgEl.classList.add("copy-done");
|
|
1257
|
+
hint.textContent = "Grabbed!";
|
|
1258
|
+
resetTimer = setTimeout(reset, 1500);
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
document.addEventListener("click", function (e) {
|
|
1264
|
+
if (primed && !msgEl.contains(e.target)) reset();
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// --- Stream smoothing: buffer deltas and drain at a steady frame rate ---
|
|
1269
|
+
var streamBuffer = "";
|
|
1270
|
+
var streamDrainTimer = null;
|
|
1271
|
+
|
|
1272
|
+
function appendDelta(text) {
|
|
1273
|
+
ensureAssistantBlock();
|
|
1274
|
+
streamBuffer += text;
|
|
1275
|
+
if (!streamDrainTimer) {
|
|
1276
|
+
streamDrainTimer = requestAnimationFrame(drainStreamTick);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function drainStreamTick() {
|
|
1281
|
+
streamDrainTimer = null;
|
|
1282
|
+
if (!currentMsgEl || streamBuffer.length === 0) return;
|
|
1283
|
+
|
|
1284
|
+
// Adaptive chunk size: drain just enough per frame to keep up
|
|
1285
|
+
// without letting the buffer grow unbounded.
|
|
1286
|
+
// At 60fps, typical streaming (~300 chars/sec) needs ~5 chars/frame.
|
|
1287
|
+
var n;
|
|
1288
|
+
var len = streamBuffer.length;
|
|
1289
|
+
if (len > 200) { n = Math.ceil(len / 4); }
|
|
1290
|
+
else if (len > 80) { n = 8; }
|
|
1291
|
+
else if (len > 30) { n = 5; }
|
|
1292
|
+
else if (len > 10) { n = 2; }
|
|
1293
|
+
else { n = 1; }
|
|
1294
|
+
|
|
1295
|
+
var chunk = streamBuffer.slice(0, n);
|
|
1296
|
+
streamBuffer = streamBuffer.slice(n);
|
|
1297
|
+
currentFullText += chunk;
|
|
1298
|
+
|
|
1299
|
+
// Full markdown render every frame — keeps structure (tables, lists)
|
|
1300
|
+
// intact and avoids cursor-span jumping artifacts.
|
|
1301
|
+
var contentEl = currentMsgEl.querySelector(".md-content");
|
|
1302
|
+
contentEl.innerHTML = renderMarkdown(currentFullText);
|
|
1303
|
+
|
|
1304
|
+
if (highlightTimer) clearTimeout(highlightTimer);
|
|
1305
|
+
highlightTimer = setTimeout(function () {
|
|
1306
|
+
highlightCodeBlocks(contentEl);
|
|
1307
|
+
}, 150);
|
|
1308
|
+
|
|
1309
|
+
scrollToBottom();
|
|
1310
|
+
|
|
1311
|
+
if (streamBuffer.length > 0) {
|
|
1312
|
+
streamDrainTimer = requestAnimationFrame(drainStreamTick);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function flushStreamBuffer() {
|
|
1317
|
+
if (streamDrainTimer) { cancelAnimationFrame(streamDrainTimer); streamDrainTimer = null; }
|
|
1318
|
+
if (streamBuffer.length > 0) {
|
|
1319
|
+
currentFullText += streamBuffer;
|
|
1320
|
+
streamBuffer = "";
|
|
1321
|
+
}
|
|
1322
|
+
if (currentMsgEl) {
|
|
1323
|
+
var contentEl = currentMsgEl.querySelector(".md-content");
|
|
1324
|
+
if (contentEl) {
|
|
1325
|
+
contentEl.innerHTML = renderMarkdown(currentFullText);
|
|
1326
|
+
highlightCodeBlocks(contentEl);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function finalizeAssistantBlock() {
|
|
1332
|
+
flushStreamBuffer();
|
|
1333
|
+
if (currentMsgEl) {
|
|
1334
|
+
var contentEl = currentMsgEl.querySelector(".md-content");
|
|
1335
|
+
if (contentEl) {
|
|
1336
|
+
highlightCodeBlocks(contentEl);
|
|
1337
|
+
renderMermaidBlocks(contentEl);
|
|
1338
|
+
}
|
|
1339
|
+
if (currentFullText) {
|
|
1340
|
+
addCopyHandler(currentMsgEl, currentFullText);
|
|
1341
|
+
}
|
|
1342
|
+
// Assistant text appeared, so break the current tool group
|
|
1343
|
+
closeToolGroup();
|
|
1344
|
+
}
|
|
1345
|
+
currentMsgEl = null;
|
|
1346
|
+
currentFullText = "";
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function addSystemMessage(text, isError) {
|
|
1350
|
+
var div = document.createElement("div");
|
|
1351
|
+
div.className = "sys-msg" + (isError ? " error" : "");
|
|
1352
|
+
div.innerHTML = '<span class="sys-text"></span>';
|
|
1353
|
+
div.querySelector(".sys-text").textContent = text;
|
|
1354
|
+
addToMessages(div);
|
|
1355
|
+
scrollToBottom();
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function addConflictMessage(msg) {
|
|
1359
|
+
var div = document.createElement("div");
|
|
1360
|
+
div.className = "conflict-msg";
|
|
1361
|
+
var header = document.createElement("div");
|
|
1362
|
+
header.className = "conflict-header";
|
|
1363
|
+
header.textContent = msg.text || "Another Claude Code process is already running.";
|
|
1364
|
+
div.appendChild(header);
|
|
1365
|
+
|
|
1366
|
+
var hint = document.createElement("div");
|
|
1367
|
+
hint.className = "conflict-hint";
|
|
1368
|
+
hint.textContent = "Kill the conflicting process to continue, or use the existing Claude Code session.";
|
|
1369
|
+
div.appendChild(hint);
|
|
1370
|
+
|
|
1371
|
+
for (var i = 0; i < msg.processes.length; i++) {
|
|
1372
|
+
var p = msg.processes[i];
|
|
1373
|
+
var row = document.createElement("div");
|
|
1374
|
+
row.className = "conflict-process";
|
|
1375
|
+
|
|
1376
|
+
var info = document.createElement("span");
|
|
1377
|
+
info.className = "conflict-pid";
|
|
1378
|
+
info.textContent = "PID " + p.pid;
|
|
1379
|
+
row.appendChild(info);
|
|
1380
|
+
|
|
1381
|
+
var cmd = document.createElement("code");
|
|
1382
|
+
cmd.className = "conflict-cmd";
|
|
1383
|
+
cmd.textContent = p.command.length > 80 ? p.command.substring(0, 80) + "..." : p.command;
|
|
1384
|
+
cmd.title = p.command;
|
|
1385
|
+
row.appendChild(cmd);
|
|
1386
|
+
|
|
1387
|
+
var killBtn = document.createElement("button");
|
|
1388
|
+
killBtn.className = "conflict-kill-btn";
|
|
1389
|
+
killBtn.textContent = "Kill Process";
|
|
1390
|
+
killBtn.setAttribute("data-pid", p.pid);
|
|
1391
|
+
killBtn.addEventListener("click", function() {
|
|
1392
|
+
var pid = parseInt(this.getAttribute("data-pid"), 10);
|
|
1393
|
+
ws.send(JSON.stringify({ type: "kill_process", pid: pid }));
|
|
1394
|
+
this.disabled = true;
|
|
1395
|
+
this.textContent = "Killing...";
|
|
1396
|
+
});
|
|
1397
|
+
row.appendChild(killBtn);
|
|
1398
|
+
div.appendChild(row);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
addToMessages(div);
|
|
1402
|
+
scrollToBottom();
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function addContextOverflowMessage(msg) {
|
|
1406
|
+
var div = document.createElement("div");
|
|
1407
|
+
div.className = "context-overflow-msg";
|
|
1408
|
+
|
|
1409
|
+
var header = document.createElement("div");
|
|
1410
|
+
header.className = "context-overflow-header";
|
|
1411
|
+
header.textContent = msg.text || "Conversation too long to continue.";
|
|
1412
|
+
div.appendChild(header);
|
|
1413
|
+
|
|
1414
|
+
var hint = document.createElement("div");
|
|
1415
|
+
hint.className = "context-overflow-hint";
|
|
1416
|
+
hint.textContent = "The conversation has exceeded the model's context limit. Please start a new conversation to continue.";
|
|
1417
|
+
div.appendChild(hint);
|
|
1418
|
+
|
|
1419
|
+
var btn = document.createElement("button");
|
|
1420
|
+
btn.className = "context-overflow-btn";
|
|
1421
|
+
btn.textContent = "New Conversation";
|
|
1422
|
+
btn.addEventListener("click", function() {
|
|
1423
|
+
ws.send(JSON.stringify({ type: "new_session" }));
|
|
1424
|
+
});
|
|
1425
|
+
div.appendChild(btn);
|
|
1426
|
+
|
|
1427
|
+
addToMessages(div);
|
|
1428
|
+
scrollToBottom();
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// --- Rate Limit ---
|
|
1432
|
+
|
|
1433
|
+
var rateLimitCountdownTimer = null;
|
|
1434
|
+
var rateLimitIndicatorEl = null;
|
|
1435
|
+
|
|
1436
|
+
function rateLimitTypeLabel(type) {
|
|
1437
|
+
if (!type) return "Usage";
|
|
1438
|
+
var labels = {
|
|
1439
|
+
"five_hour": "5-hour",
|
|
1440
|
+
"seven_day": "7-day",
|
|
1441
|
+
"seven_day_opus": "7-day Opus",
|
|
1442
|
+
"seven_day_sonnet": "7-day Sonnet",
|
|
1443
|
+
"overage": "Overage",
|
|
1444
|
+
};
|
|
1445
|
+
return labels[type] || type;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function startRateLimitCountdown(el, resetsAt, cardEl) {
|
|
1449
|
+
if (rateLimitCountdownTimer) clearInterval(rateLimitCountdownTimer);
|
|
1450
|
+
|
|
1451
|
+
function tick() {
|
|
1452
|
+
var remaining = resetsAt - Date.now();
|
|
1453
|
+
if (remaining <= 0) {
|
|
1454
|
+
clearInterval(rateLimitCountdownTimer);
|
|
1455
|
+
rateLimitCountdownTimer = null;
|
|
1456
|
+
clearRateLimitIndicator();
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
// Update pill text with countdown
|
|
1460
|
+
if (rateLimitIndicatorEl) {
|
|
1461
|
+
var pillText = rateLimitIndicatorEl.querySelector(".header-pill-text");
|
|
1462
|
+
if (pillText) {
|
|
1463
|
+
var mins = Math.floor(remaining / 60000);
|
|
1464
|
+
var secs = Math.floor((remaining % 60000) / 1000);
|
|
1465
|
+
if (mins >= 60) {
|
|
1466
|
+
var hrs = Math.floor(mins / 60);
|
|
1467
|
+
mins = mins % 60;
|
|
1468
|
+
pillText.textContent = hrs + "h " + mins + "m";
|
|
1469
|
+
} else {
|
|
1470
|
+
pillText.textContent = mins + "m " + secs + "s";
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
tick();
|
|
1477
|
+
rateLimitCountdownTimer = setInterval(tick, 1000);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function updateRateLimitIndicator(msg) {
|
|
1481
|
+
var statusArea = document.querySelector(".title-bar-content .status");
|
|
1482
|
+
if (!statusArea) return;
|
|
1483
|
+
|
|
1484
|
+
if (!rateLimitIndicatorEl) {
|
|
1485
|
+
rateLimitIndicatorEl = document.createElement("span");
|
|
1486
|
+
rateLimitIndicatorEl.className = "header-rate-limit-wrap";
|
|
1487
|
+
statusArea.insertBefore(rateLimitIndicatorEl, statusArea.firstChild);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
var isRejected = msg.status === "rejected";
|
|
1491
|
+
var pillClass = "header-rate-limit" + (isRejected ? " rejected" : " warning");
|
|
1492
|
+
var label = isRejected ? "Rate limited" : "Rate warning";
|
|
1493
|
+
rateLimitIndicatorEl.innerHTML =
|
|
1494
|
+
'<span class="' + pillClass + '">' +
|
|
1495
|
+
iconHtml("alert-triangle") +
|
|
1496
|
+
'<span class="header-pill-text">' + label + "</span>" +
|
|
1497
|
+
'<a href="https://claude.ai/settings/usage" target="_blank" rel="noopener" class="rate-limit-link">' +
|
|
1498
|
+
iconHtml("external-link") +
|
|
1499
|
+
"</a>" +
|
|
1500
|
+
"</span>";
|
|
1501
|
+
refreshIcons();
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function showRateLimitPopover(text, isRejected) {
|
|
1505
|
+
if (!rateLimitIndicatorEl) return;
|
|
1506
|
+
// Remove existing popover
|
|
1507
|
+
var old = rateLimitIndicatorEl.querySelector(".rate-limit-popover");
|
|
1508
|
+
if (old) old.remove();
|
|
1509
|
+
|
|
1510
|
+
var pop = document.createElement("div");
|
|
1511
|
+
pop.className = "rate-limit-popover" + (isRejected ? " rejected" : "");
|
|
1512
|
+
pop.textContent = text;
|
|
1513
|
+
rateLimitIndicatorEl.appendChild(pop);
|
|
1514
|
+
|
|
1515
|
+
// Auto-dismiss after 5s
|
|
1516
|
+
setTimeout(function () {
|
|
1517
|
+
pop.classList.add("fade-out");
|
|
1518
|
+
setTimeout(function () { if (pop.parentNode) pop.remove(); }, 300);
|
|
1519
|
+
}, 5000);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function clearRateLimitIndicator() {
|
|
1523
|
+
if (rateLimitIndicatorEl) {
|
|
1524
|
+
rateLimitIndicatorEl.remove();
|
|
1525
|
+
rateLimitIndicatorEl = null;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function handleRateLimitEvent(msg) {
|
|
1530
|
+
var isRejected = msg.status === "rejected";
|
|
1531
|
+
var typeLabel = rateLimitTypeLabel(msg.rateLimitType);
|
|
1532
|
+
var popoverText = "";
|
|
1533
|
+
|
|
1534
|
+
if (isRejected && msg.resetsAt) {
|
|
1535
|
+
// Check if already expired (history replay) — skip popover
|
|
1536
|
+
if (msg.resetsAt < Date.now()) {
|
|
1537
|
+
updateRateLimitIndicator(msg);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
popoverText = typeLabel + " limit exceeded";
|
|
1541
|
+
updateRateLimitIndicator(msg);
|
|
1542
|
+
startRateLimitCountdown(null, msg.resetsAt, null);
|
|
1543
|
+
} else {
|
|
1544
|
+
var pct = msg.utilization ? Math.round(msg.utilization * 100) : null;
|
|
1545
|
+
popoverText = typeLabel + " warning" + (pct ? " (" + pct + "% used)" : "");
|
|
1546
|
+
updateRateLimitIndicator(msg);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
showRateLimitPopover(popoverText, isRejected);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// --- Fast Mode State ---
|
|
1553
|
+
|
|
1554
|
+
var fastModeIndicatorEl = null;
|
|
1555
|
+
|
|
1556
|
+
function handleFastModeState(state) {
|
|
1557
|
+
var statusArea = document.querySelector(".title-bar-content .status");
|
|
1558
|
+
if (!statusArea) return;
|
|
1559
|
+
|
|
1560
|
+
if (state === "off") {
|
|
1561
|
+
if (fastModeIndicatorEl) {
|
|
1562
|
+
fastModeIndicatorEl.remove();
|
|
1563
|
+
fastModeIndicatorEl = null;
|
|
1564
|
+
}
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (!fastModeIndicatorEl) {
|
|
1569
|
+
fastModeIndicatorEl = document.createElement("span");
|
|
1570
|
+
statusArea.insertBefore(fastModeIndicatorEl, statusArea.firstChild);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (state === "cooldown") {
|
|
1574
|
+
fastModeIndicatorEl.className = "header-fast-mode cooldown";
|
|
1575
|
+
fastModeIndicatorEl.innerHTML = iconHtml("timer") + '<span class="header-pill-text">Cooldown</span>';
|
|
1576
|
+
} else if (state === "on") {
|
|
1577
|
+
fastModeIndicatorEl.className = "header-fast-mode active";
|
|
1578
|
+
fastModeIndicatorEl.innerHTML = iconHtml("zap") + '<span class="header-pill-text">Fast mode</span>';
|
|
1579
|
+
}
|
|
1580
|
+
refreshIcons();
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// --- Prompt suggestion chips ---
|
|
1584
|
+
function showSuggestionChips(suggestion) {
|
|
1585
|
+
if (!suggestion || processing) return;
|
|
1586
|
+
suggestionChipsEl.innerHTML = "";
|
|
1587
|
+
var chip = document.createElement("button");
|
|
1588
|
+
chip.className = "suggestion-chip";
|
|
1589
|
+
chip.innerHTML = iconHtml("sparkles") + " " + escapeHtml(suggestion);
|
|
1590
|
+
chip.addEventListener("click", function () {
|
|
1591
|
+
inputEl.value = suggestion;
|
|
1592
|
+
inputEl.focus();
|
|
1593
|
+
inputEl.select();
|
|
1594
|
+
autoResize();
|
|
1595
|
+
hideSuggestionChips();
|
|
1596
|
+
});
|
|
1597
|
+
suggestionChipsEl.appendChild(chip);
|
|
1598
|
+
suggestionChipsEl.classList.remove("hidden");
|
|
1599
|
+
refreshIcons();
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function hideSuggestionChips() {
|
|
1603
|
+
suggestionChipsEl.innerHTML = "";
|
|
1604
|
+
suggestionChipsEl.classList.add("hidden");
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function acceptSuggestionChip() {
|
|
1608
|
+
if (suggestionChipsEl.classList.contains("hidden")) return false;
|
|
1609
|
+
var chip = suggestionChipsEl.querySelector(".suggestion-chip");
|
|
1610
|
+
if (!chip) return false;
|
|
1611
|
+
chip.click();
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function resetClientState() {
|
|
1616
|
+
messagesEl.innerHTML = "";
|
|
1617
|
+
currentMsgEl = null;
|
|
1618
|
+
currentFullText = "";
|
|
1619
|
+
resetToolState();
|
|
1620
|
+
clearPendingImages();
|
|
1621
|
+
activityEl = null;
|
|
1622
|
+
processing = false;
|
|
1623
|
+
turnCounter = 0;
|
|
1624
|
+
messageUuidMap = [];
|
|
1625
|
+
historyFrom = 0;
|
|
1626
|
+
historyTotal = 0;
|
|
1627
|
+
prependAnchor = null;
|
|
1628
|
+
loadingMore = false;
|
|
1629
|
+
isUserScrolledUp = false;
|
|
1630
|
+
newMsgBtn.classList.add("hidden");
|
|
1631
|
+
setRewindMode(false);
|
|
1632
|
+
removeSearchTimeline();
|
|
1633
|
+
setActivity(null);
|
|
1634
|
+
setStatus("connected");
|
|
1635
|
+
enableMainInput();
|
|
1636
|
+
resetUsage();
|
|
1637
|
+
resetContext();
|
|
1638
|
+
// Clear header indicators
|
|
1639
|
+
clearRateLimitIndicator();
|
|
1640
|
+
if (rateLimitCountdownTimer) { clearInterval(rateLimitCountdownTimer); rateLimitCountdownTimer = null; }
|
|
1641
|
+
if (fastModeIndicatorEl) { fastModeIndicatorEl.remove(); fastModeIndicatorEl = null; }
|
|
1642
|
+
if (headerContextEl) { headerContextEl.remove(); headerContextEl = null; }
|
|
1643
|
+
hideSuggestionChips();
|
|
1644
|
+
closeSessionInfoPopover();
|
|
1645
|
+
stopUrgentBlink();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// --- Project switching (no full reload) ---
|
|
1649
|
+
function switchProject(slug) {
|
|
1650
|
+
if (!slug || slug === currentSlug) return;
|
|
1651
|
+
currentSlug = slug;
|
|
1652
|
+
basePath = "/p/" + slug + "/";
|
|
1653
|
+
wsPath = "/p/" + slug + "/ws";
|
|
1654
|
+
history.pushState(null, "", basePath);
|
|
1655
|
+
resetClientState();
|
|
1656
|
+
connect();
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
window.addEventListener("popstate", function () {
|
|
1660
|
+
var m = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
1661
|
+
var newSlug = m ? m[1] : null;
|
|
1662
|
+
if (newSlug && newSlug !== currentSlug) {
|
|
1663
|
+
currentSlug = newSlug;
|
|
1664
|
+
basePath = "/p/" + newSlug + "/";
|
|
1665
|
+
wsPath = "/p/" + newSlug + "/ws";
|
|
1666
|
+
resetClientState();
|
|
1667
|
+
connect();
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
// --- WebSocket ---
|
|
1672
|
+
var connectTimeoutId = null;
|
|
1673
|
+
|
|
1674
|
+
function connect() {
|
|
1675
|
+
if (ws) { ws.onclose = null; ws.close(); }
|
|
1676
|
+
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
1677
|
+
|
|
1678
|
+
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
1679
|
+
ws = new WebSocket(protocol + "//" + location.host + wsPath);
|
|
1680
|
+
|
|
1681
|
+
|
|
1682
|
+
// If not connected within 3s, force retry
|
|
1683
|
+
connectTimeoutId = setTimeout(function () {
|
|
1684
|
+
if (!connected) {
|
|
1685
|
+
ws.onclose = null;
|
|
1686
|
+
ws.onerror = null;
|
|
1687
|
+
ws.close();
|
|
1688
|
+
connect();
|
|
1689
|
+
}
|
|
1690
|
+
}, 3000);
|
|
1691
|
+
|
|
1692
|
+
ws.onopen = function () {
|
|
1693
|
+
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
1694
|
+
// Cancel pending "connection lost" notification if reconnected quickly
|
|
1695
|
+
if (disconnectNotifTimer) {
|
|
1696
|
+
clearTimeout(disconnectNotifTimer);
|
|
1697
|
+
disconnectNotifTimer = null;
|
|
1698
|
+
}
|
|
1699
|
+
// Only show "restored" notification if "lost" was actually shown
|
|
1700
|
+
if (wasConnected && disconnectNotifShown && !document.hasFocus() && "serviceWorker" in navigator) {
|
|
1701
|
+
navigator.serviceWorker.ready.then(function (reg) {
|
|
1702
|
+
reg.showNotification("Clay", {
|
|
1703
|
+
body: "Server connection restored",
|
|
1704
|
+
tag: "claude-disconnect",
|
|
1705
|
+
});
|
|
1706
|
+
}).catch(function () {});
|
|
1707
|
+
}
|
|
1708
|
+
disconnectNotifShown = false;
|
|
1709
|
+
wasConnected = true;
|
|
1710
|
+
setStatus("connected");
|
|
1711
|
+
reconnectDelay = 1000;
|
|
1712
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1713
|
+
|
|
1714
|
+
// Wrap ws.send to blink LED on outgoing traffic
|
|
1715
|
+
var _origSend = ws.send.bind(ws);
|
|
1716
|
+
ws.send = function (data) {
|
|
1717
|
+
blinkIO();
|
|
1718
|
+
return _origSend(data);
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// Reset terminal xterm instances (server will send fresh term_list)
|
|
1722
|
+
resetTerminals();
|
|
1723
|
+
|
|
1724
|
+
// Re-send push subscription on reconnect
|
|
1725
|
+
if (window._pushSubscription) {
|
|
1726
|
+
try {
|
|
1727
|
+
ws.send(JSON.stringify({
|
|
1728
|
+
type: "push_subscribe",
|
|
1729
|
+
subscription: window._pushSubscription.toJSON(),
|
|
1730
|
+
}));
|
|
1731
|
+
} catch(e) {}
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
ws.onclose = function (e) {
|
|
1736
|
+
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
1737
|
+
setStatus("disconnected");
|
|
1738
|
+
processing = false;
|
|
1739
|
+
setActivity(null);
|
|
1740
|
+
// Delay "connection lost" notification by 5s to suppress brief disconnects
|
|
1741
|
+
if (!disconnectNotifTimer) {
|
|
1742
|
+
disconnectNotifTimer = setTimeout(function () {
|
|
1743
|
+
disconnectNotifTimer = null;
|
|
1744
|
+
disconnectNotifShown = true;
|
|
1745
|
+
if (!document.hasFocus() && "serviceWorker" in navigator) {
|
|
1746
|
+
navigator.serviceWorker.ready.then(function (reg) {
|
|
1747
|
+
reg.showNotification("Clay", {
|
|
1748
|
+
body: "Server connection lost",
|
|
1749
|
+
tag: "claude-disconnect",
|
|
1750
|
+
});
|
|
1751
|
+
}).catch(function () {});
|
|
1752
|
+
}
|
|
1753
|
+
}, 5000);
|
|
1754
|
+
}
|
|
1755
|
+
scheduleReconnect();
|
|
1756
|
+
};
|
|
1757
|
+
|
|
1758
|
+
ws.onerror = function () {
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
ws.onmessage = function (event) {
|
|
1762
|
+
// Backup: if we're receiving messages, we're connected
|
|
1763
|
+
if (!connected) {
|
|
1764
|
+
setStatus("connected");
|
|
1765
|
+
reconnectDelay = 1000;
|
|
1766
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
blinkIO();
|
|
1770
|
+
var msg;
|
|
1771
|
+
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
1772
|
+
processMessage(msg);
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function processMessage(msg) {
|
|
1777
|
+
switch (msg.type) {
|
|
1778
|
+
case "history_meta":
|
|
1779
|
+
historyFrom = msg.from;
|
|
1780
|
+
historyTotal = msg.total;
|
|
1781
|
+
replayingHistory = true;
|
|
1782
|
+
updateHistorySentinel();
|
|
1783
|
+
break;
|
|
1784
|
+
|
|
1785
|
+
case "history_prepend":
|
|
1786
|
+
prependOlderHistory(msg.items, msg.meta);
|
|
1787
|
+
break;
|
|
1788
|
+
|
|
1789
|
+
case "history_done":
|
|
1790
|
+
replayingHistory = false;
|
|
1791
|
+
// Restore accurate context data from the last result in full history
|
|
1792
|
+
if (msg.lastUsage || msg.lastModelUsage) {
|
|
1793
|
+
accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage);
|
|
1794
|
+
}
|
|
1795
|
+
updateContextPanel();
|
|
1796
|
+
updateUsagePanel();
|
|
1797
|
+
// Render + finalize any incomplete turn from the replayed history
|
|
1798
|
+
if (currentMsgEl && currentFullText) {
|
|
1799
|
+
var replayContentEl = currentMsgEl.querySelector(".md-content");
|
|
1800
|
+
if (replayContentEl) {
|
|
1801
|
+
replayContentEl.innerHTML = renderMarkdown(currentFullText);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
markAllToolsDone();
|
|
1805
|
+
finalizeAssistantBlock();
|
|
1806
|
+
stopUrgentBlink();
|
|
1807
|
+
scrollToBottom();
|
|
1808
|
+
var pendingQuery = getActiveSearchQuery();
|
|
1809
|
+
if (pendingQuery) {
|
|
1810
|
+
requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
|
|
1811
|
+
}
|
|
1812
|
+
// Scroll to tool element if navigating from file edit history
|
|
1813
|
+
var nav = getPendingNavigate();
|
|
1814
|
+
if (nav && (nav.toolId || nav.assistantUuid)) {
|
|
1815
|
+
requestAnimationFrame(function() {
|
|
1816
|
+
// Prefer scrolling to the exact tool element
|
|
1817
|
+
var target = nav.toolId ? messagesEl.querySelector('[data-tool-id="' + nav.toolId + '"]') : null;
|
|
1818
|
+
if (!target && nav.assistantUuid) {
|
|
1819
|
+
target = messagesEl.querySelector('[data-uuid="' + nav.assistantUuid + '"]');
|
|
1820
|
+
}
|
|
1821
|
+
if (target) {
|
|
1822
|
+
// Auto-expand parent tool group if collapsed
|
|
1823
|
+
var parentGroup = target.closest(".tool-group");
|
|
1824
|
+
if (parentGroup) parentGroup.classList.remove("collapsed");
|
|
1825
|
+
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1826
|
+
target.classList.add("message-blink");
|
|
1827
|
+
setTimeout(function() { target.classList.remove("message-blink"); }, 2000);
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
break;
|
|
1832
|
+
|
|
1833
|
+
case "info":
|
|
1834
|
+
projectName = msg.project || msg.cwd;
|
|
1835
|
+
if (msg.slug) currentSlug = msg.slug;
|
|
1836
|
+
try { localStorage.setItem("clay-project-name-" + (currentSlug || "default"), projectName); } catch (e) {}
|
|
1837
|
+
headerTitleEl.textContent = projectName;
|
|
1838
|
+
var tbProjectName = $("title-bar-project-name");
|
|
1839
|
+
if (tbProjectName) tbProjectName.textContent = msg.title || projectName;
|
|
1840
|
+
updatePageTitle();
|
|
1841
|
+
if (msg.version) {
|
|
1842
|
+
var vEl = $("footer-version");
|
|
1843
|
+
if (vEl) vEl.textContent = "v" + msg.version;
|
|
1844
|
+
}
|
|
1845
|
+
if (msg.debug) {
|
|
1846
|
+
var debugWrap = $("debug-menu-wrap");
|
|
1847
|
+
if (debugWrap) debugWrap.classList.remove("hidden");
|
|
1848
|
+
}
|
|
1849
|
+
if (msg.lanHost) window.__lanHost = msg.lanHost;
|
|
1850
|
+
if (msg.dangerouslySkipPermissions) {
|
|
1851
|
+
skipPermsEnabled = true;
|
|
1852
|
+
var spBanner = $("skip-perms-banner");
|
|
1853
|
+
if (spBanner) spBanner.classList.remove("hidden");
|
|
1854
|
+
}
|
|
1855
|
+
updateProjectList(msg);
|
|
1856
|
+
break;
|
|
1857
|
+
|
|
1858
|
+
case "update_available":
|
|
1859
|
+
var updateBanner = $("update-banner");
|
|
1860
|
+
var updateVersion = $("update-version");
|
|
1861
|
+
if (updateBanner && updateVersion && msg.version) {
|
|
1862
|
+
updateVersion.textContent = "v" + msg.version;
|
|
1863
|
+
updateBanner.classList.remove("hidden");
|
|
1864
|
+
// Reset button state (may be stuck on "Updating..." after restart)
|
|
1865
|
+
var updResetBtn = $("update-now");
|
|
1866
|
+
if (updResetBtn) {
|
|
1867
|
+
updResetBtn.textContent = "Update now";
|
|
1868
|
+
updResetBtn.disabled = false;
|
|
1869
|
+
}
|
|
1870
|
+
refreshIcons();
|
|
1871
|
+
}
|
|
1872
|
+
// Update the settings check-for-updates button
|
|
1873
|
+
var settingsUpdBtn = $("settings-update-check");
|
|
1874
|
+
if (settingsUpdBtn && msg.version) {
|
|
1875
|
+
settingsUpdBtn.innerHTML = "";
|
|
1876
|
+
var ic = document.createElement("i");
|
|
1877
|
+
ic.setAttribute("data-lucide", "arrow-up-circle");
|
|
1878
|
+
settingsUpdBtn.appendChild(ic);
|
|
1879
|
+
settingsUpdBtn.appendChild(document.createTextNode(" Update available (v" + msg.version + ")"));
|
|
1880
|
+
settingsUpdBtn.classList.add("settings-btn-update-available");
|
|
1881
|
+
settingsUpdBtn.disabled = false;
|
|
1882
|
+
refreshIcons();
|
|
1883
|
+
}
|
|
1884
|
+
break;
|
|
1885
|
+
|
|
1886
|
+
case "update_started":
|
|
1887
|
+
var updNowBtn = $("update-now");
|
|
1888
|
+
if (updNowBtn) {
|
|
1889
|
+
updNowBtn.textContent = "Updating...";
|
|
1890
|
+
updNowBtn.disabled = true;
|
|
1891
|
+
}
|
|
1892
|
+
// Block the entire screen with the connect overlay
|
|
1893
|
+
connectOverlay.classList.remove("hidden");
|
|
1894
|
+
break;
|
|
1895
|
+
|
|
1896
|
+
case "slash_commands":
|
|
1897
|
+
var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
|
|
1898
|
+
slashCommands = (msg.commands || []).filter(function (name) {
|
|
1899
|
+
return !reserved.has(name);
|
|
1900
|
+
}).map(function (name) {
|
|
1901
|
+
return { name: name, desc: "Skill" };
|
|
1902
|
+
});
|
|
1903
|
+
break;
|
|
1904
|
+
|
|
1905
|
+
case "model_info":
|
|
1906
|
+
currentModel = msg.model || currentModel;
|
|
1907
|
+
currentModels = msg.models || [];
|
|
1908
|
+
updateConfigChip();
|
|
1909
|
+
updateSettingsModels(msg.model, msg.models || []);
|
|
1910
|
+
break;
|
|
1911
|
+
|
|
1912
|
+
case "config_state":
|
|
1913
|
+
if (msg.model) currentModel = msg.model;
|
|
1914
|
+
if (msg.mode) currentMode = msg.mode;
|
|
1915
|
+
if (msg.effort) currentEffort = msg.effort;
|
|
1916
|
+
if (msg.betas) currentBetas = msg.betas;
|
|
1917
|
+
// Validate effort against current model's supported levels
|
|
1918
|
+
if (currentModels.length > 0) {
|
|
1919
|
+
var levels = getModelEffortLevels();
|
|
1920
|
+
var effortValid = false;
|
|
1921
|
+
for (var ei = 0; ei < levels.length; ei++) {
|
|
1922
|
+
if (levels[ei] === currentEffort) { effortValid = true; break; }
|
|
1923
|
+
}
|
|
1924
|
+
if (!effortValid) currentEffort = "high";
|
|
1925
|
+
}
|
|
1926
|
+
updateConfigChip();
|
|
1927
|
+
break;
|
|
1928
|
+
|
|
1929
|
+
case "client_count":
|
|
1930
|
+
var countEl = document.getElementById("client-count");
|
|
1931
|
+
if (countEl) {
|
|
1932
|
+
if (msg.count > 1) {
|
|
1933
|
+
countEl.textContent = msg.count;
|
|
1934
|
+
countEl.dataset.tip = msg.count + " devices connected";
|
|
1935
|
+
countEl.classList.remove("hidden");
|
|
1936
|
+
} else {
|
|
1937
|
+
countEl.classList.add("hidden");
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
break;
|
|
1941
|
+
|
|
1942
|
+
case "toast":
|
|
1943
|
+
showToast(msg.message, msg.level, msg.detail);
|
|
1944
|
+
break;
|
|
1945
|
+
|
|
1946
|
+
case "input_sync":
|
|
1947
|
+
handleInputSync(msg.text);
|
|
1948
|
+
break;
|
|
1949
|
+
|
|
1950
|
+
case "session_list":
|
|
1951
|
+
renderSessionList(msg.sessions || []);
|
|
1952
|
+
break;
|
|
1953
|
+
|
|
1954
|
+
case "search_results":
|
|
1955
|
+
handleSearchResults(msg);
|
|
1956
|
+
break;
|
|
1957
|
+
|
|
1958
|
+
case "cli_session_list":
|
|
1959
|
+
populateCliSessionList(msg.sessions || []);
|
|
1960
|
+
break;
|
|
1961
|
+
|
|
1962
|
+
case "session_switched":
|
|
1963
|
+
// Save draft from outgoing session
|
|
1964
|
+
if (activeSessionId && inputEl.value) {
|
|
1965
|
+
sessionDrafts[activeSessionId] = inputEl.value;
|
|
1966
|
+
} else if (activeSessionId) {
|
|
1967
|
+
delete sessionDrafts[activeSessionId];
|
|
1968
|
+
}
|
|
1969
|
+
activeSessionId = msg.id;
|
|
1970
|
+
cliSessionId = msg.cliSessionId || null;
|
|
1971
|
+
resetClientState();
|
|
1972
|
+
// Restore draft for incoming session
|
|
1973
|
+
var draft = sessionDrafts[activeSessionId] || "";
|
|
1974
|
+
inputEl.value = draft;
|
|
1975
|
+
autoResize();
|
|
1976
|
+
if (!("ontouchstart" in window)) {
|
|
1977
|
+
inputEl.focus();
|
|
1978
|
+
}
|
|
1979
|
+
break;
|
|
1980
|
+
|
|
1981
|
+
case "session_id":
|
|
1982
|
+
cliSessionId = msg.cliSessionId;
|
|
1983
|
+
break;
|
|
1984
|
+
|
|
1985
|
+
case "message_uuid":
|
|
1986
|
+
var uuidTarget;
|
|
1987
|
+
if (msg.messageType === "user") {
|
|
1988
|
+
var allUsers = messagesEl.querySelectorAll(".msg-user:not([data-uuid])");
|
|
1989
|
+
if (allUsers.length > 0) uuidTarget = allUsers[allUsers.length - 1];
|
|
1990
|
+
} else {
|
|
1991
|
+
var allAssistants = messagesEl.querySelectorAll(".msg-assistant:not([data-uuid])");
|
|
1992
|
+
if (allAssistants.length > 0) uuidTarget = allAssistants[allAssistants.length - 1];
|
|
1993
|
+
}
|
|
1994
|
+
if (uuidTarget) {
|
|
1995
|
+
uuidTarget.dataset.uuid = msg.uuid;
|
|
1996
|
+
if (msg.messageType === "user") addRewindButton(uuidTarget);
|
|
1997
|
+
}
|
|
1998
|
+
messageUuidMap.push({ uuid: msg.uuid, type: msg.messageType });
|
|
1999
|
+
break;
|
|
2000
|
+
|
|
2001
|
+
case "user_message":
|
|
2002
|
+
resetThinkingGroup();
|
|
2003
|
+
if (msg.planContent) {
|
|
2004
|
+
setPlanContent(msg.planContent);
|
|
2005
|
+
renderPlanCard(msg.planContent);
|
|
2006
|
+
addUserMessage("Execute the following plan. Do NOT re-enter plan mode — just implement it step by step.", msg.images || null, msg.pastes || null);
|
|
2007
|
+
} else {
|
|
2008
|
+
addUserMessage(msg.text, msg.images || null, msg.pastes || null);
|
|
2009
|
+
}
|
|
2010
|
+
break;
|
|
2011
|
+
|
|
2012
|
+
case "status":
|
|
2013
|
+
if (msg.status === "processing") {
|
|
2014
|
+
setStatus("processing");
|
|
2015
|
+
setActivity(randomThinkingVerb() + "...");
|
|
2016
|
+
}
|
|
2017
|
+
break;
|
|
2018
|
+
|
|
2019
|
+
case "compacting":
|
|
2020
|
+
if (msg.active) {
|
|
2021
|
+
setActivity("Compacting conversation...");
|
|
2022
|
+
} else {
|
|
2023
|
+
setActivity(randomThinkingVerb() + "...");
|
|
2024
|
+
}
|
|
2025
|
+
break;
|
|
2026
|
+
|
|
2027
|
+
case "thinking_start":
|
|
2028
|
+
startThinking();
|
|
2029
|
+
break;
|
|
2030
|
+
|
|
2031
|
+
case "thinking_delta":
|
|
2032
|
+
if (typeof msg.text === "string") appendThinking(msg.text);
|
|
2033
|
+
break;
|
|
2034
|
+
|
|
2035
|
+
case "thinking_stop":
|
|
2036
|
+
stopThinking(msg.duration);
|
|
2037
|
+
setActivity(randomThinkingVerb() + "...");
|
|
2038
|
+
break;
|
|
2039
|
+
|
|
2040
|
+
case "delta":
|
|
2041
|
+
if (typeof msg.text !== "string") break;
|
|
2042
|
+
stopThinking();
|
|
2043
|
+
resetThinkingGroup();
|
|
2044
|
+
setActivity(null);
|
|
2045
|
+
appendDelta(msg.text);
|
|
2046
|
+
break;
|
|
2047
|
+
|
|
2048
|
+
case "tool_start":
|
|
2049
|
+
stopThinking();
|
|
2050
|
+
markAllToolsDone();
|
|
2051
|
+
if (msg.name === "EnterPlanMode") {
|
|
2052
|
+
renderPlanBanner("enter");
|
|
2053
|
+
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
2054
|
+
} else if (msg.name === "ExitPlanMode") {
|
|
2055
|
+
if (getPlanContent()) {
|
|
2056
|
+
renderPlanCard(getPlanContent());
|
|
2057
|
+
}
|
|
2058
|
+
renderPlanBanner("exit");
|
|
2059
|
+
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
2060
|
+
} else if (getTodoTools()[msg.name]) {
|
|
2061
|
+
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
2062
|
+
} else {
|
|
2063
|
+
createToolItem(msg.id, msg.name);
|
|
2064
|
+
}
|
|
2065
|
+
break;
|
|
2066
|
+
|
|
2067
|
+
case "tool_executing":
|
|
2068
|
+
if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
|
|
2069
|
+
var askTool = getTools()[msg.id];
|
|
2070
|
+
if (askTool) {
|
|
2071
|
+
if (askTool.el) askTool.el.style.display = "none";
|
|
2072
|
+
askTool.done = true;
|
|
2073
|
+
removeToolFromGroup(msg.id);
|
|
2074
|
+
}
|
|
2075
|
+
renderAskUserQuestion(msg.id, msg.input);
|
|
2076
|
+
startUrgentBlink();
|
|
2077
|
+
} else if (msg.name === "Write" && msg.input && isPlanFilePath(msg.input.file_path)) {
|
|
2078
|
+
setPlanContent(msg.input.content || "");
|
|
2079
|
+
updateToolExecuting(msg.id, msg.name, msg.input);
|
|
2080
|
+
} else if (msg.name === "Edit" && msg.input && isPlanFilePath(msg.input.file_path)) {
|
|
2081
|
+
var pc = getPlanContent() || "";
|
|
2082
|
+
if (msg.input.old_string && pc.indexOf(msg.input.old_string) !== -1) {
|
|
2083
|
+
if (msg.input.replace_all) {
|
|
2084
|
+
setPlanContent(pc.split(msg.input.old_string).join(msg.input.new_string || ""));
|
|
2085
|
+
} else {
|
|
2086
|
+
setPlanContent(pc.replace(msg.input.old_string, msg.input.new_string || ""));
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
updateToolExecuting(msg.id, msg.name, msg.input);
|
|
2090
|
+
} else if (msg.name === "TodoWrite") {
|
|
2091
|
+
handleTodoWrite(msg.input);
|
|
2092
|
+
} else if (msg.name === "TaskCreate") {
|
|
2093
|
+
handleTaskCreate(msg.input);
|
|
2094
|
+
} else if (msg.name === "TaskUpdate") {
|
|
2095
|
+
handleTaskUpdate(msg.input);
|
|
2096
|
+
} else if (getTodoTools()[msg.name]) {
|
|
2097
|
+
// TaskList, TaskGet - silently skip
|
|
2098
|
+
} else {
|
|
2099
|
+
var t = getTools()[msg.id];
|
|
2100
|
+
if (t && t.hidden) break;
|
|
2101
|
+
updateToolExecuting(msg.id, msg.name, msg.input);
|
|
2102
|
+
}
|
|
2103
|
+
break;
|
|
2104
|
+
|
|
2105
|
+
case "tool_result": {
|
|
2106
|
+
var tr = getTools()[msg.id];
|
|
2107
|
+
if (tr && tr.hidden) break; // skip hidden plan tools
|
|
2108
|
+
// Always call updateToolResult for Edit (to show diff from input), or when content exists
|
|
2109
|
+
if (msg.content != null || (tr && tr.name === "Edit" && tr.input && tr.input.old_string)) {
|
|
2110
|
+
updateToolResult(msg.id, msg.content || "", msg.is_error || false);
|
|
2111
|
+
}
|
|
2112
|
+
// Refresh file browser if an Edit/Write tool modified the open file
|
|
2113
|
+
if (!msg.is_error && tr && (tr.name === "Edit" || tr.name === "Write") && tr.input && tr.input.file_path) {
|
|
2114
|
+
refreshIfOpen(tr.input.file_path);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
break;
|
|
2118
|
+
|
|
2119
|
+
case "ask_user_answered":
|
|
2120
|
+
markAskUserAnswered(msg.toolId);
|
|
2121
|
+
stopUrgentBlink();
|
|
2122
|
+
break;
|
|
2123
|
+
|
|
2124
|
+
case "permission_request":
|
|
2125
|
+
renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
|
|
2126
|
+
startUrgentBlink();
|
|
2127
|
+
break;
|
|
2128
|
+
|
|
2129
|
+
case "permission_cancel":
|
|
2130
|
+
markPermissionCancelled(msg.requestId);
|
|
2131
|
+
stopUrgentBlink();
|
|
2132
|
+
break;
|
|
2133
|
+
|
|
2134
|
+
case "permission_resolved":
|
|
2135
|
+
markPermissionResolved(msg.requestId, msg.decision);
|
|
2136
|
+
stopUrgentBlink();
|
|
2137
|
+
break;
|
|
2138
|
+
|
|
2139
|
+
case "permission_request_pending":
|
|
2140
|
+
renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
|
|
2141
|
+
startUrgentBlink();
|
|
2142
|
+
break;
|
|
2143
|
+
|
|
2144
|
+
case "slash_command_result":
|
|
2145
|
+
finalizeAssistantBlock();
|
|
2146
|
+
var cmdBlock = document.createElement("div");
|
|
2147
|
+
cmdBlock.className = "assistant-block";
|
|
2148
|
+
cmdBlock.style.maxWidth = "var(--content-width)";
|
|
2149
|
+
cmdBlock.style.margin = "12px auto";
|
|
2150
|
+
cmdBlock.style.padding = "0 20px";
|
|
2151
|
+
var pre = document.createElement("pre");
|
|
2152
|
+
pre.style.cssText = "background:var(--code-bg);border:1px solid var(--border-subtle);border-radius:10px;padding:12px 14px;font-family:'SF Mono',Menlo,Monaco,monospace;font-size:12px;line-height:1.55;color:var(--text-secondary);white-space:pre-wrap;word-break:break-word;max-height:400px;overflow-y:auto;margin:0";
|
|
2153
|
+
pre.textContent = msg.text;
|
|
2154
|
+
cmdBlock.appendChild(pre);
|
|
2155
|
+
addToMessages(cmdBlock);
|
|
2156
|
+
scrollToBottom();
|
|
2157
|
+
break;
|
|
2158
|
+
|
|
2159
|
+
case "subagent_activity":
|
|
2160
|
+
updateSubagentActivity(msg.parentToolId, msg.text);
|
|
2161
|
+
break;
|
|
2162
|
+
|
|
2163
|
+
case "subagent_tool":
|
|
2164
|
+
addSubagentToolEntry(msg.parentToolId, msg.toolName, msg.toolId, msg.text);
|
|
2165
|
+
break;
|
|
2166
|
+
|
|
2167
|
+
case "subagent_done":
|
|
2168
|
+
markSubagentDone(msg.parentToolId, msg.status, msg.summary, msg.usage);
|
|
2169
|
+
break;
|
|
2170
|
+
|
|
2171
|
+
case "task_started":
|
|
2172
|
+
initSubagentStop(msg.parentToolId, msg.taskId);
|
|
2173
|
+
break;
|
|
2174
|
+
|
|
2175
|
+
case "task_progress":
|
|
2176
|
+
updateSubagentProgress(msg.parentToolId, msg.usage, msg.lastToolName);
|
|
2177
|
+
break;
|
|
2178
|
+
|
|
2179
|
+
case "result":
|
|
2180
|
+
setActivity(null);
|
|
2181
|
+
stopThinking();
|
|
2182
|
+
markAllToolsDone();
|
|
2183
|
+
closeToolGroup();
|
|
2184
|
+
finalizeAssistantBlock();
|
|
2185
|
+
addTurnMeta(msg.cost, msg.duration);
|
|
2186
|
+
accumulateUsage(msg.cost, msg.usage);
|
|
2187
|
+
accumulateContext(msg.cost, msg.usage, msg.modelUsage);
|
|
2188
|
+
break;
|
|
2189
|
+
|
|
2190
|
+
case "done":
|
|
2191
|
+
setActivity(null);
|
|
2192
|
+
stopThinking();
|
|
2193
|
+
markAllToolsDone();
|
|
2194
|
+
closeToolGroup();
|
|
2195
|
+
finalizeAssistantBlock();
|
|
2196
|
+
processing = false;
|
|
2197
|
+
setStatus("connected");
|
|
2198
|
+
enableMainInput();
|
|
2199
|
+
resetToolState();
|
|
2200
|
+
stopUrgentBlink();
|
|
2201
|
+
if (document.hidden) {
|
|
2202
|
+
if (isNotifAlertEnabled() && !window._pushSubscription) showDoneNotification();
|
|
2203
|
+
if (isNotifSoundEnabled()) playDoneSound();
|
|
2204
|
+
}
|
|
2205
|
+
break;
|
|
2206
|
+
|
|
2207
|
+
case "stderr":
|
|
2208
|
+
addSystemMessage(msg.text, false);
|
|
2209
|
+
break;
|
|
2210
|
+
|
|
2211
|
+
case "info":
|
|
2212
|
+
addSystemMessage(msg.text, false);
|
|
2213
|
+
break;
|
|
2214
|
+
|
|
2215
|
+
case "error":
|
|
2216
|
+
setActivity(null);
|
|
2217
|
+
addSystemMessage(msg.text, true);
|
|
2218
|
+
break;
|
|
2219
|
+
|
|
2220
|
+
case "process_conflict":
|
|
2221
|
+
setActivity(null);
|
|
2222
|
+
addConflictMessage(msg);
|
|
2223
|
+
break;
|
|
2224
|
+
|
|
2225
|
+
case "context_overflow":
|
|
2226
|
+
setActivity(null);
|
|
2227
|
+
addContextOverflowMessage(msg);
|
|
2228
|
+
break;
|
|
2229
|
+
|
|
2230
|
+
case "rate_limit":
|
|
2231
|
+
handleRateLimitEvent(msg);
|
|
2232
|
+
break;
|
|
2233
|
+
|
|
2234
|
+
case "prompt_suggestion":
|
|
2235
|
+
showSuggestionChips(msg.suggestion);
|
|
2236
|
+
break;
|
|
2237
|
+
|
|
2238
|
+
case "fast_mode_state":
|
|
2239
|
+
handleFastModeState(msg.state);
|
|
2240
|
+
break;
|
|
2241
|
+
|
|
2242
|
+
case "process_killed":
|
|
2243
|
+
addSystemMessage("Process " + msg.pid + " has been terminated. You can retry your message now.", false);
|
|
2244
|
+
break;
|
|
2245
|
+
|
|
2246
|
+
case "rewind_preview_result":
|
|
2247
|
+
showRewindModal(msg);
|
|
2248
|
+
break;
|
|
2249
|
+
|
|
2250
|
+
case "rewind_complete":
|
|
2251
|
+
setRewindMode(false);
|
|
2252
|
+
var rewindText = "Rewound to earlier point. Files have been restored.";
|
|
2253
|
+
if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
|
|
2254
|
+
else if (msg.mode === "files") rewindText = "Files restored to earlier point.";
|
|
2255
|
+
addSystemMessage(rewindText, false);
|
|
2256
|
+
break;
|
|
2257
|
+
|
|
2258
|
+
case "rewind_error":
|
|
2259
|
+
clearPendingRewindUuid();
|
|
2260
|
+
addSystemMessage(msg.text || "Rewind failed.", true);
|
|
2261
|
+
break;
|
|
2262
|
+
|
|
2263
|
+
case "fs_list_result":
|
|
2264
|
+
handleFsList(msg);
|
|
2265
|
+
break;
|
|
2266
|
+
|
|
2267
|
+
case "fs_read_result":
|
|
2268
|
+
handleFsRead(msg);
|
|
2269
|
+
break;
|
|
2270
|
+
|
|
2271
|
+
case "fs_file_changed":
|
|
2272
|
+
handleFileChanged(msg);
|
|
2273
|
+
break;
|
|
2274
|
+
|
|
2275
|
+
case "fs_dir_changed":
|
|
2276
|
+
handleDirChanged(msg);
|
|
2277
|
+
break;
|
|
2278
|
+
|
|
2279
|
+
case "fs_file_history_result":
|
|
2280
|
+
handleFileHistory(msg);
|
|
2281
|
+
break;
|
|
2282
|
+
|
|
2283
|
+
case "fs_git_diff_result":
|
|
2284
|
+
handleGitDiff(msg);
|
|
2285
|
+
break;
|
|
2286
|
+
|
|
2287
|
+
case "fs_file_at_result":
|
|
2288
|
+
handleFileAt(msg);
|
|
2289
|
+
break;
|
|
2290
|
+
|
|
2291
|
+
case "term_list":
|
|
2292
|
+
handleTermList(msg);
|
|
2293
|
+
break;
|
|
2294
|
+
|
|
2295
|
+
case "term_created":
|
|
2296
|
+
handleTermCreated(msg);
|
|
2297
|
+
break;
|
|
2298
|
+
|
|
2299
|
+
case "term_output":
|
|
2300
|
+
handleTermOutput(msg);
|
|
2301
|
+
break;
|
|
2302
|
+
|
|
2303
|
+
case "term_exited":
|
|
2304
|
+
handleTermExited(msg);
|
|
2305
|
+
break;
|
|
2306
|
+
|
|
2307
|
+
case "term_closed":
|
|
2308
|
+
handleTermClosed(msg);
|
|
2309
|
+
break;
|
|
2310
|
+
|
|
2311
|
+
case "notes_list":
|
|
2312
|
+
handleNotesList(msg);
|
|
2313
|
+
break;
|
|
2314
|
+
|
|
2315
|
+
case "note_created":
|
|
2316
|
+
handleNoteCreated(msg);
|
|
2317
|
+
break;
|
|
2318
|
+
|
|
2319
|
+
case "note_updated":
|
|
2320
|
+
handleNoteUpdated(msg);
|
|
2321
|
+
break;
|
|
2322
|
+
|
|
2323
|
+
case "note_deleted":
|
|
2324
|
+
handleNoteDeleted(msg);
|
|
2325
|
+
break;
|
|
2326
|
+
|
|
2327
|
+
case "process_stats":
|
|
2328
|
+
updateStatusPanel(msg);
|
|
2329
|
+
updateSettingsStats(msg);
|
|
2330
|
+
break;
|
|
2331
|
+
|
|
2332
|
+
case "browse_dir_result":
|
|
2333
|
+
handleBrowseDirResult(msg);
|
|
2334
|
+
break;
|
|
2335
|
+
|
|
2336
|
+
case "add_project_result":
|
|
2337
|
+
handleAddProjectResult(msg);
|
|
2338
|
+
break;
|
|
2339
|
+
|
|
2340
|
+
case "remove_project_result":
|
|
2341
|
+
handleRemoveProjectResult(msg);
|
|
2342
|
+
break;
|
|
2343
|
+
|
|
2344
|
+
case "projects_updated":
|
|
2345
|
+
updateProjectList(msg);
|
|
2346
|
+
break;
|
|
2347
|
+
|
|
2348
|
+
case "daemon_config":
|
|
2349
|
+
updateDaemonConfig(msg.config);
|
|
2350
|
+
break;
|
|
2351
|
+
|
|
2352
|
+
case "set_pin_result":
|
|
2353
|
+
handleSetPinResult(msg);
|
|
2354
|
+
break;
|
|
2355
|
+
|
|
2356
|
+
case "set_keep_awake_result":
|
|
2357
|
+
handleKeepAwakeChanged(msg);
|
|
2358
|
+
break;
|
|
2359
|
+
|
|
2360
|
+
case "keep_awake_changed":
|
|
2361
|
+
handleKeepAwakeChanged(msg);
|
|
2362
|
+
break;
|
|
2363
|
+
|
|
2364
|
+
case "shutdown_server_result":
|
|
2365
|
+
handleShutdownResult(msg);
|
|
2366
|
+
break;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// --- Progressive history loading ---
|
|
2371
|
+
function updateHistorySentinel() {
|
|
2372
|
+
var existing = messagesEl.querySelector(".history-sentinel");
|
|
2373
|
+
if (historyFrom > 0) {
|
|
2374
|
+
if (!existing) {
|
|
2375
|
+
var sentinel = document.createElement("div");
|
|
2376
|
+
sentinel.className = "history-sentinel";
|
|
2377
|
+
sentinel.innerHTML = '<button class="load-more-btn">Load earlier messages</button>';
|
|
2378
|
+
sentinel.querySelector(".load-more-btn").addEventListener("click", function () {
|
|
2379
|
+
requestMoreHistory();
|
|
2380
|
+
});
|
|
2381
|
+
messagesEl.insertBefore(sentinel, messagesEl.firstChild);
|
|
2382
|
+
|
|
2383
|
+
// Auto-load when sentinel scrolls into view
|
|
2384
|
+
if (historySentinelObserver) historySentinelObserver.disconnect();
|
|
2385
|
+
historySentinelObserver = new IntersectionObserver(function (entries) {
|
|
2386
|
+
if (entries[0].isIntersecting && !loadingMore && historyFrom > 0) {
|
|
2387
|
+
requestMoreHistory();
|
|
2388
|
+
}
|
|
2389
|
+
}, { root: messagesEl, rootMargin: "200px 0px 0px 0px" });
|
|
2390
|
+
historySentinelObserver.observe(sentinel);
|
|
2391
|
+
}
|
|
2392
|
+
} else {
|
|
2393
|
+
if (existing) existing.remove();
|
|
2394
|
+
if (historySentinelObserver) { historySentinelObserver.disconnect(); historySentinelObserver = null; }
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function requestMoreHistory() {
|
|
2399
|
+
if (loadingMore || historyFrom <= 0 || !ws || !connected) return;
|
|
2400
|
+
loadingMore = true;
|
|
2401
|
+
var btn = messagesEl.querySelector(".load-more-btn");
|
|
2402
|
+
if (btn) btn.classList.add("loading");
|
|
2403
|
+
ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom }));
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function prependOlderHistory(items, meta) {
|
|
2407
|
+
// Save current rendering state
|
|
2408
|
+
var savedMsgEl = currentMsgEl;
|
|
2409
|
+
var savedActivity = activityEl;
|
|
2410
|
+
var savedFullText = currentFullText;
|
|
2411
|
+
var savedTurnCounter = turnCounter;
|
|
2412
|
+
var savedToolsState = saveToolState();
|
|
2413
|
+
// Save context & usage so old result messages don't overwrite current values
|
|
2414
|
+
var savedContext = JSON.parse(JSON.stringify(contextData));
|
|
2415
|
+
var savedUsage = JSON.parse(JSON.stringify(sessionUsage));
|
|
2416
|
+
|
|
2417
|
+
// Reset to initial values for clean rendering
|
|
2418
|
+
currentMsgEl = null;
|
|
2419
|
+
activityEl = null;
|
|
2420
|
+
currentFullText = "";
|
|
2421
|
+
turnCounter = 0;
|
|
2422
|
+
resetToolState();
|
|
2423
|
+
|
|
2424
|
+
// Set prepend anchor to insert before existing content
|
|
2425
|
+
// Skip the sentinel itself when setting anchor
|
|
2426
|
+
var firstReal = messagesEl.querySelector(".history-sentinel");
|
|
2427
|
+
prependAnchor = firstReal ? firstReal.nextSibling : messagesEl.firstChild;
|
|
2428
|
+
|
|
2429
|
+
// Remember the first existing content element and its position
|
|
2430
|
+
var anchorEl = prependAnchor;
|
|
2431
|
+
var anchorOffset = anchorEl ? anchorEl.getBoundingClientRect().top : 0;
|
|
2432
|
+
|
|
2433
|
+
// Process each item through the rendering pipeline
|
|
2434
|
+
for (var i = 0; i < items.length; i++) {
|
|
2435
|
+
processMessage(items[i]);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// Finalize any open assistant block from the batch
|
|
2439
|
+
finalizeAssistantBlock();
|
|
2440
|
+
|
|
2441
|
+
// Clear prepend mode
|
|
2442
|
+
prependAnchor = null;
|
|
2443
|
+
|
|
2444
|
+
// Restore saved state
|
|
2445
|
+
currentMsgEl = savedMsgEl;
|
|
2446
|
+
activityEl = savedActivity;
|
|
2447
|
+
currentFullText = savedFullText;
|
|
2448
|
+
turnCounter = savedTurnCounter;
|
|
2449
|
+
restoreToolState(savedToolsState);
|
|
2450
|
+
// Restore context & usage (old result messages must not overwrite current values)
|
|
2451
|
+
contextData = savedContext;
|
|
2452
|
+
sessionUsage = savedUsage;
|
|
2453
|
+
updateContextPanel();
|
|
2454
|
+
updateUsagePanel();
|
|
2455
|
+
|
|
2456
|
+
// Fix scroll: restore anchor element to same visual position
|
|
2457
|
+
if (anchorEl) {
|
|
2458
|
+
var newTop = anchorEl.getBoundingClientRect().top;
|
|
2459
|
+
messagesEl.scrollTop += (newTop - anchorOffset);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// Update state
|
|
2463
|
+
historyFrom = meta.from;
|
|
2464
|
+
loadingMore = false;
|
|
2465
|
+
|
|
2466
|
+
// Renumber data-turn attributes in DOM order
|
|
2467
|
+
var turnEls = messagesEl.querySelectorAll("[data-turn]");
|
|
2468
|
+
for (var t = 0; t < turnEls.length; t++) {
|
|
2469
|
+
turnEls[t].dataset.turn = t + 1;
|
|
2470
|
+
}
|
|
2471
|
+
turnCounter = turnEls.length;
|
|
2472
|
+
|
|
2473
|
+
// Update sentinel
|
|
2474
|
+
if (meta.hasMore) {
|
|
2475
|
+
var btn = messagesEl.querySelector(".load-more-btn");
|
|
2476
|
+
if (btn) btn.classList.remove("loading");
|
|
2477
|
+
} else {
|
|
2478
|
+
updateHistorySentinel();
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
function scheduleReconnect() {
|
|
2483
|
+
if (reconnectTimer) return;
|
|
2484
|
+
reconnectTimer = setTimeout(function () {
|
|
2485
|
+
reconnectTimer = null;
|
|
2486
|
+
connect();
|
|
2487
|
+
}, reconnectDelay);
|
|
2488
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// --- Input module (sendMessage, autoResize, paste/image, slash menu, input handlers) ---
|
|
2492
|
+
initInput({
|
|
2493
|
+
get ws() { return ws; },
|
|
2494
|
+
get connected() { return connected; },
|
|
2495
|
+
get processing() { return processing; },
|
|
2496
|
+
inputEl: inputEl,
|
|
2497
|
+
sendBtn: sendBtn,
|
|
2498
|
+
slashMenu: slashMenu,
|
|
2499
|
+
messagesEl: messagesEl,
|
|
2500
|
+
imagePreviewBar: imagePreviewBar,
|
|
2501
|
+
slashCommands: function() { return slashCommands; },
|
|
2502
|
+
messageUuidMap: function() { return messageUuidMap; },
|
|
2503
|
+
addUserMessage: addUserMessage,
|
|
2504
|
+
addSystemMessage: addSystemMessage,
|
|
2505
|
+
toggleUsagePanel: toggleUsagePanel,
|
|
2506
|
+
toggleStatusPanel: toggleStatusPanel,
|
|
2507
|
+
toggleContextPanel: toggleContextPanel,
|
|
2508
|
+
resetContextData: resetContextData,
|
|
2509
|
+
showImageModal: showImageModal,
|
|
2510
|
+
hideSuggestionChips: hideSuggestionChips,
|
|
2511
|
+
acceptSuggestionChip: acceptSuggestionChip,
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
// --- Notifications module (viewport, banners, notifications, debug, service worker) ---
|
|
2515
|
+
initNotifications({
|
|
2516
|
+
$: $,
|
|
2517
|
+
get ws() { return ws; },
|
|
2518
|
+
get connected() { return connected; },
|
|
2519
|
+
messagesEl: messagesEl,
|
|
2520
|
+
sessionListEl: sessionListEl,
|
|
2521
|
+
scrollToBottom: scrollToBottom,
|
|
2522
|
+
basePath: basePath,
|
|
2523
|
+
toggleUsagePanel: toggleUsagePanel,
|
|
2524
|
+
toggleStatusPanel: toggleStatusPanel,
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2527
|
+
// --- Server Settings ---
|
|
2528
|
+
initServerSettings({
|
|
2529
|
+
get ws() { return ws; },
|
|
2530
|
+
get projectName() { return projectName; },
|
|
2531
|
+
get currentSlug() { return currentSlug; },
|
|
2532
|
+
wsPath: wsPath,
|
|
2533
|
+
get currentModels() { return currentModels; },
|
|
2534
|
+
set currentModels(v) { currentModels = v; updateConfigChip(); },
|
|
2535
|
+
setContextView: setContextView,
|
|
2536
|
+
applyContextView: applyContextView,
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
// --- QR code ---
|
|
2540
|
+
initQrCode();
|
|
2541
|
+
|
|
2542
|
+
// --- File browser ---
|
|
2543
|
+
initFileBrowser({
|
|
2544
|
+
get ws() { return ws; },
|
|
2545
|
+
get connected() { return connected; },
|
|
2546
|
+
get activeSessionId() { return activeSessionId; },
|
|
2547
|
+
messagesEl: messagesEl,
|
|
2548
|
+
fileTreeEl: $("file-tree"),
|
|
2549
|
+
fileViewerEl: $("file-viewer"),
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
// --- Terminal ---
|
|
2553
|
+
initTerminal({
|
|
2554
|
+
get ws() { return ws; },
|
|
2555
|
+
get connected() { return connected; },
|
|
2556
|
+
terminalContainerEl: $("terminal-container"),
|
|
2557
|
+
terminalBodyEl: $("terminal-body"),
|
|
2558
|
+
fileViewerEl: $("file-viewer"),
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
// --- Sticky Notes ---
|
|
2562
|
+
initStickyNotes({
|
|
2563
|
+
get ws() { return ws; },
|
|
2564
|
+
get connected() { return connected; },
|
|
2565
|
+
});
|
|
2566
|
+
|
|
2567
|
+
// --- Remove project ---
|
|
2568
|
+
function confirmRemoveProject(slug, name) {
|
|
2569
|
+
showConfirm("Remove project \"" + name + "\"?", function () {
|
|
2570
|
+
if (ws && ws.readyState === 1) {
|
|
2571
|
+
ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
|
|
2572
|
+
}
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
function handleRemoveProjectResult(msg) {
|
|
2577
|
+
if (msg.ok) {
|
|
2578
|
+
showToast("Project removed", "success");
|
|
2579
|
+
// If we removed the current project, navigate to first available
|
|
2580
|
+
if (msg.slug === currentSlug) {
|
|
2581
|
+
window.location.href = "/";
|
|
2582
|
+
}
|
|
2583
|
+
} else {
|
|
2584
|
+
showToast(msg.error || "Failed to remove project", "error");
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// --- Add project modal ---
|
|
2589
|
+
var addProjectModal = document.getElementById("add-project-modal");
|
|
2590
|
+
var addProjectInput = document.getElementById("add-project-input");
|
|
2591
|
+
var addProjectSuggestions = document.getElementById("add-project-suggestions");
|
|
2592
|
+
var addProjectError = document.getElementById("add-project-error");
|
|
2593
|
+
var addProjectOk = document.getElementById("add-project-ok");
|
|
2594
|
+
var addProjectCancel = document.getElementById("add-project-cancel");
|
|
2595
|
+
var addProjectDebounce = null;
|
|
2596
|
+
var addProjectActiveIdx = -1;
|
|
2597
|
+
|
|
2598
|
+
function openAddProjectModal() {
|
|
2599
|
+
addProjectModal.classList.remove("hidden");
|
|
2600
|
+
addProjectInput.value = "/";
|
|
2601
|
+
addProjectError.classList.add("hidden");
|
|
2602
|
+
addProjectError.textContent = "";
|
|
2603
|
+
addProjectSuggestions.classList.add("hidden");
|
|
2604
|
+
addProjectSuggestions.innerHTML = "";
|
|
2605
|
+
addProjectActiveIdx = -1;
|
|
2606
|
+
addProjectOk.disabled = false;
|
|
2607
|
+
setTimeout(function () {
|
|
2608
|
+
addProjectInput.focus();
|
|
2609
|
+
addProjectInput.setSelectionRange(1, 1);
|
|
2610
|
+
}, 50);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
function closeAddProjectModal() {
|
|
2614
|
+
addProjectModal.classList.add("hidden");
|
|
2615
|
+
addProjectInput.value = "";
|
|
2616
|
+
addProjectSuggestions.classList.add("hidden");
|
|
2617
|
+
addProjectSuggestions.innerHTML = "";
|
|
2618
|
+
addProjectError.classList.add("hidden");
|
|
2619
|
+
addProjectActiveIdx = -1;
|
|
2620
|
+
if (addProjectDebounce) { clearTimeout(addProjectDebounce); addProjectDebounce = null; }
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
function requestBrowseDir(val) {
|
|
2624
|
+
if (!ws || ws.readyState !== 1) return;
|
|
2625
|
+
ws.send(JSON.stringify({ type: "browse_dir", path: val }));
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
function handleBrowseDirResult(msg) {
|
|
2629
|
+
addProjectSuggestions.innerHTML = "";
|
|
2630
|
+
addProjectActiveIdx = -1;
|
|
2631
|
+
if (msg.error) {
|
|
2632
|
+
addProjectSuggestions.classList.add("hidden");
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
var entries = msg.entries || [];
|
|
2636
|
+
if (entries.length === 0) {
|
|
2637
|
+
addProjectSuggestions.classList.add("hidden");
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
for (var si = 0; si < entries.length; si++) {
|
|
2641
|
+
var entry = entries[si];
|
|
2642
|
+
var item = document.createElement("div");
|
|
2643
|
+
item.className = "add-project-suggestion-item";
|
|
2644
|
+
item.dataset.path = entry.path;
|
|
2645
|
+
item.innerHTML = '<i data-lucide="folder"></i><span class="add-project-suggestion-name">' +
|
|
2646
|
+
escapeHtml(entry.name) + '</span>';
|
|
2647
|
+
item.addEventListener("click", function (e) {
|
|
2648
|
+
var p = this.dataset.path + "/";
|
|
2649
|
+
addProjectInput.value = p;
|
|
2650
|
+
addProjectInput.focus();
|
|
2651
|
+
addProjectError.classList.add("hidden");
|
|
2652
|
+
requestBrowseDir(p);
|
|
2653
|
+
});
|
|
2654
|
+
addProjectSuggestions.appendChild(item);
|
|
2655
|
+
}
|
|
2656
|
+
addProjectSuggestions.classList.remove("hidden");
|
|
2657
|
+
refreshIcons();
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
function handleAddProjectResult(msg) {
|
|
2661
|
+
if (msg.ok) {
|
|
2662
|
+
closeAddProjectModal();
|
|
2663
|
+
if (msg.existing) {
|
|
2664
|
+
showToast("Project already registered", "info");
|
|
2665
|
+
} else {
|
|
2666
|
+
showToast("Project added", "success");
|
|
2667
|
+
// Navigate to the new project
|
|
2668
|
+
if (msg.slug) {
|
|
2669
|
+
switchProject(msg.slug);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
} else {
|
|
2673
|
+
addProjectError.textContent = msg.error || "Failed to add project";
|
|
2674
|
+
addProjectError.classList.remove("hidden");
|
|
2675
|
+
addProjectOk.disabled = false;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function setActiveIdx(idx) {
|
|
2680
|
+
var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
|
|
2681
|
+
addProjectActiveIdx = idx;
|
|
2682
|
+
for (var ai = 0; ai < items.length; ai++) {
|
|
2683
|
+
if (ai === idx) {
|
|
2684
|
+
items[ai].classList.add("active");
|
|
2685
|
+
items[ai].scrollIntoView({ block: "nearest" });
|
|
2686
|
+
} else {
|
|
2687
|
+
items[ai].classList.remove("active");
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
addProjectInput.addEventListener("focus", function () {
|
|
2693
|
+
var val = addProjectInput.value;
|
|
2694
|
+
if (val && addProjectSuggestions.children.length === 0) {
|
|
2695
|
+
requestBrowseDir(val);
|
|
2696
|
+
} else if (addProjectSuggestions.children.length > 0) {
|
|
2697
|
+
addProjectSuggestions.classList.remove("hidden");
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
|
|
2701
|
+
addProjectModal.querySelector(".confirm-dialog").addEventListener("click", function (e) {
|
|
2702
|
+
if (e.target === addProjectInput || addProjectInput.contains(e.target)) return;
|
|
2703
|
+
if (e.target === addProjectSuggestions || addProjectSuggestions.contains(e.target)) return;
|
|
2704
|
+
addProjectSuggestions.classList.add("hidden");
|
|
2705
|
+
addProjectActiveIdx = -1;
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
addProjectInput.addEventListener("input", function () {
|
|
2709
|
+
var val = addProjectInput.value;
|
|
2710
|
+
addProjectError.classList.add("hidden");
|
|
2711
|
+
if (addProjectDebounce) clearTimeout(addProjectDebounce);
|
|
2712
|
+
addProjectDebounce = setTimeout(function () {
|
|
2713
|
+
requestBrowseDir(val);
|
|
2714
|
+
}, 200);
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
addProjectInput.addEventListener("keydown", function (e) {
|
|
2718
|
+
var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
|
|
2719
|
+
|
|
2720
|
+
if (e.key === "ArrowDown") {
|
|
2721
|
+
e.preventDefault();
|
|
2722
|
+
if (items.length > 0) {
|
|
2723
|
+
var next = addProjectActiveIdx < items.length - 1 ? addProjectActiveIdx + 1 : 0;
|
|
2724
|
+
setActiveIdx(next);
|
|
2725
|
+
}
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
if (e.key === "ArrowUp") {
|
|
2730
|
+
e.preventDefault();
|
|
2731
|
+
if (items.length > 0) {
|
|
2732
|
+
var prev = addProjectActiveIdx > 0 ? addProjectActiveIdx - 1 : items.length - 1;
|
|
2733
|
+
setActiveIdx(prev);
|
|
2734
|
+
}
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
if (e.key === "Tab") {
|
|
2739
|
+
e.preventDefault();
|
|
2740
|
+
var target = addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length
|
|
2741
|
+
? items[addProjectActiveIdx]
|
|
2742
|
+
: items.length > 0 ? items[0] : null;
|
|
2743
|
+
if (target) {
|
|
2744
|
+
var p = target.dataset.path + "/";
|
|
2745
|
+
addProjectInput.value = p;
|
|
2746
|
+
addProjectError.classList.add("hidden");
|
|
2747
|
+
requestBrowseDir(p);
|
|
2748
|
+
}
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
if (e.key === "Enter") {
|
|
2753
|
+
e.preventDefault();
|
|
2754
|
+
// If a suggestion is highlighted, pick it first
|
|
2755
|
+
if (addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length) {
|
|
2756
|
+
var picked = items[addProjectActiveIdx].dataset.path + "/";
|
|
2757
|
+
addProjectInput.value = picked;
|
|
2758
|
+
addProjectError.classList.add("hidden");
|
|
2759
|
+
requestBrowseDir(picked);
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
// Otherwise submit
|
|
2763
|
+
submitAddProject();
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
if (e.key === "Escape") {
|
|
2768
|
+
e.preventDefault();
|
|
2769
|
+
closeAddProjectModal();
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
|
|
2774
|
+
function submitAddProject() {
|
|
2775
|
+
var val = addProjectInput.value.replace(/\/+$/, "");
|
|
2776
|
+
if (!val) return;
|
|
2777
|
+
addProjectOk.disabled = true;
|
|
2778
|
+
addProjectError.classList.add("hidden");
|
|
2779
|
+
if (ws && ws.readyState === 1) {
|
|
2780
|
+
ws.send(JSON.stringify({ type: "add_project", path: val }));
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
addProjectOk.addEventListener("click", function () { submitAddProject(); });
|
|
2785
|
+
addProjectCancel.addEventListener("click", function () { closeAddProjectModal(); });
|
|
2786
|
+
|
|
2787
|
+
// Close on backdrop click
|
|
2788
|
+
addProjectModal.querySelector(".confirm-backdrop").addEventListener("click", function () {
|
|
2789
|
+
closeAddProjectModal();
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
// --- Init ---
|
|
2793
|
+
lucide.createIcons();
|
|
2794
|
+
connect();
|
|
2795
|
+
inputEl.focus();
|