claude-relay 2.4.2 → 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/bin/cli.js +1 -2350
- package/package.json +7 -42
- package/LICENSE +0 -21
- package/README.md +0 -281
- package/lib/cli-sessions.js +0 -270
- package/lib/config.js +0 -222
- package/lib/daemon.js +0 -423
- package/lib/ipc.js +0 -112
- package/lib/pages.js +0 -714
- package/lib/project.js +0 -1224
- package/lib/public/app.js +0 -2157
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +0 -145
- package/lib/public/css/diff.css +0 -128
- package/lib/public/css/filebrowser.css +0 -1076
- package/lib/public/css/highlight.css +0 -144
- package/lib/public/css/input.css +0 -512
- package/lib/public/css/menus.css +0 -683
- package/lib/public/css/messages.css +0 -1159
- package/lib/public/css/overlays.css +0 -731
- package/lib/public/css/rewind.css +0 -529
- package/lib/public/css/sidebar.css +0 -794
- package/lib/public/favicon.svg +0 -26
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +0 -19
- package/lib/public/index.html +0 -460
- package/lib/public/manifest.json +0 -27
- package/lib/public/modules/diff.js +0 -398
- package/lib/public/modules/events.js +0 -21
- package/lib/public/modules/filebrowser.js +0 -1375
- package/lib/public/modules/fileicons.js +0 -172
- package/lib/public/modules/icons.js +0 -54
- package/lib/public/modules/input.js +0 -578
- package/lib/public/modules/markdown.js +0 -149
- package/lib/public/modules/notifications.js +0 -643
- package/lib/public/modules/qrcode.js +0 -70
- package/lib/public/modules/rewind.js +0 -334
- package/lib/public/modules/sidebar.js +0 -628
- package/lib/public/modules/state.js +0 -3
- package/lib/public/modules/terminal.js +0 -658
- package/lib/public/modules/theme.js +0 -622
- package/lib/public/modules/tools.js +0 -1410
- package/lib/public/modules/utils.js +0 -56
- package/lib/public/style.css +0 -10
- package/lib/public/sw.js +0 -75
- package/lib/push.js +0 -125
- package/lib/sdk-bridge.js +0 -771
- package/lib/server.js +0 -577
- package/lib/sessions.js +0 -402
- package/lib/terminal-manager.js +0 -187
- package/lib/terminal.js +0 -24
- package/lib/themes/ayu-light.json +0 -9
- package/lib/themes/catppuccin-latte.json +0 -9
- package/lib/themes/catppuccin-mocha.json +0 -9
- package/lib/themes/claude-light.json +0 -9
- package/lib/themes/claude.json +0 -9
- package/lib/themes/dracula.json +0 -9
- package/lib/themes/everforest-light.json +0 -9
- package/lib/themes/everforest.json +0 -9
- package/lib/themes/github-light.json +0 -9
- package/lib/themes/gruvbox-dark.json +0 -9
- package/lib/themes/gruvbox-light.json +0 -9
- package/lib/themes/monokai.json +0 -9
- package/lib/themes/nord-light.json +0 -9
- package/lib/themes/nord.json +0 -9
- package/lib/themes/one-dark.json +0 -9
- package/lib/themes/one-light.json +0 -9
- package/lib/themes/rose-pine-dawn.json +0 -9
- package/lib/themes/rose-pine.json +0 -9
- package/lib/themes/solarized-dark.json +0 -9
- package/lib/themes/solarized-light.json +0 -9
- package/lib/themes/tokyo-night-light.json +0 -9
- package/lib/themes/tokyo-night.json +0 -9
- package/lib/updater.js +0 -96
package/lib/public/app.js
DELETED
|
@@ -1,2157 +0,0 @@
|
|
|
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 } from './modules/sidebar.js';
|
|
5
|
-
import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid } 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 { initTheme, getThemeColor, getComputedVar, onThemeChange } from './modules/theme.js';
|
|
12
|
-
import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
|
|
13
|
-
|
|
14
|
-
// --- Base path for multi-project routing ---
|
|
15
|
-
var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
16
|
-
var basePath = slugMatch ? "/p/" + slugMatch[1] + "/" : "/";
|
|
17
|
-
var wsPath = slugMatch ? "/p/" + slugMatch[1] + "/ws" : "/ws";
|
|
18
|
-
|
|
19
|
-
// --- DOM refs ---
|
|
20
|
-
var $ = function (id) { return document.getElementById(id); };
|
|
21
|
-
var messagesEl = $("messages");
|
|
22
|
-
var inputEl = $("input");
|
|
23
|
-
var sendBtn = $("send-btn");
|
|
24
|
-
var statusDot = $("status-dot");
|
|
25
|
-
var headerTitleEl = $("header-title");
|
|
26
|
-
var headerRenameBtn = $("header-rename-btn");
|
|
27
|
-
var slashMenu = $("slash-menu");
|
|
28
|
-
var sidebar = $("sidebar");
|
|
29
|
-
var sidebarOverlay = $("sidebar-overlay");
|
|
30
|
-
var sessionListEl = $("session-list");
|
|
31
|
-
var newSessionBtn = $("new-session-btn");
|
|
32
|
-
var hamburgerBtn = $("hamburger-btn");
|
|
33
|
-
var sidebarToggleBtn = $("sidebar-toggle-btn");
|
|
34
|
-
var sidebarExpandBtn = $("sidebar-expand-btn");
|
|
35
|
-
var resumeSessionBtn = $("resume-session-btn");
|
|
36
|
-
var imagePreviewBar = $("image-preview-bar");
|
|
37
|
-
var connectOverlay = $("connect-overlay");
|
|
38
|
-
var connectVerbEl = $("connect-verb");
|
|
39
|
-
var connectStatusEl = $("connect-status");
|
|
40
|
-
|
|
41
|
-
// --- Project List ---
|
|
42
|
-
var projectListSection = $("project-list-section");
|
|
43
|
-
var projectListEl = $("project-list");
|
|
44
|
-
var projectListAddBtn = $("project-list-add");
|
|
45
|
-
var projectHint = $("project-hint");
|
|
46
|
-
var projectHintDismiss = $("project-hint-dismiss");
|
|
47
|
-
var cachedProjects = [];
|
|
48
|
-
var cachedProjectCount = 0;
|
|
49
|
-
var currentSlug = slugMatch ? slugMatch[1] : null;
|
|
50
|
-
|
|
51
|
-
function updateProjectList(msg) {
|
|
52
|
-
if (typeof msg.projectCount === "number") cachedProjectCount = msg.projectCount;
|
|
53
|
-
if (msg.projects) cachedProjects = msg.projects;
|
|
54
|
-
var count = cachedProjectCount || 0;
|
|
55
|
-
renderProjectList();
|
|
56
|
-
if (count === 1 && projectHint) {
|
|
57
|
-
try {
|
|
58
|
-
if (!localStorage.getItem("claude-relay-project-hint-dismissed")) {
|
|
59
|
-
projectHint.classList.remove("hidden");
|
|
60
|
-
}
|
|
61
|
-
} catch (e) {}
|
|
62
|
-
} else if (projectHint) {
|
|
63
|
-
projectHint.classList.add("hidden");
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function renderProjectList() {
|
|
68
|
-
if (!projectListEl) return;
|
|
69
|
-
projectListEl.innerHTML = "";
|
|
70
|
-
for (var i = 0; i < cachedProjects.length; i++) {
|
|
71
|
-
var p = cachedProjects[i];
|
|
72
|
-
var isCurrent = p.slug === currentSlug;
|
|
73
|
-
var displayName = p.title || p.project;
|
|
74
|
-
var item = document.createElement("a");
|
|
75
|
-
item.className = "project-list-item" + (isCurrent ? " current" : "");
|
|
76
|
-
item.href = "/p/" + p.slug + "/";
|
|
77
|
-
|
|
78
|
-
var indicator = document.createElement("span");
|
|
79
|
-
indicator.className = "pd-indicator " + (isCurrent ? "active" : "inactive");
|
|
80
|
-
item.appendChild(indicator);
|
|
81
|
-
|
|
82
|
-
var name = document.createElement("span");
|
|
83
|
-
name.className = "pd-name";
|
|
84
|
-
name.textContent = displayName;
|
|
85
|
-
item.appendChild(name);
|
|
86
|
-
|
|
87
|
-
var removeBtn = document.createElement("button");
|
|
88
|
-
removeBtn.className = "pd-remove";
|
|
89
|
-
removeBtn.type = "button";
|
|
90
|
-
removeBtn.title = "Remove project";
|
|
91
|
-
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
|
92
|
-
removeBtn.dataset.slug = p.slug;
|
|
93
|
-
removeBtn.dataset.name = displayName;
|
|
94
|
-
removeBtn.addEventListener("click", function (e) {
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
e.stopPropagation();
|
|
97
|
-
var s = this.dataset.slug;
|
|
98
|
-
var n = this.dataset.name;
|
|
99
|
-
confirmRemoveProject(s, n);
|
|
100
|
-
});
|
|
101
|
-
item.appendChild(removeBtn);
|
|
102
|
-
|
|
103
|
-
projectListEl.appendChild(item);
|
|
104
|
-
}
|
|
105
|
-
refreshIcons();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (projectListAddBtn) {
|
|
109
|
-
projectListAddBtn.addEventListener("click", function () {
|
|
110
|
-
openAddProjectModal();
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
document.addEventListener("keydown", function (e) {
|
|
115
|
-
if (e.key === "Escape") {
|
|
116
|
-
closeImageModal();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (projectHintDismiss) {
|
|
121
|
-
projectHintDismiss.addEventListener("click", function () {
|
|
122
|
-
projectHint.classList.add("hidden");
|
|
123
|
-
try { localStorage.setItem("claude-relay-project-hint-dismissed", "1"); } catch (e) {}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Modal close handlers (replaces inline onclick)
|
|
128
|
-
$("paste-modal").querySelector(".confirm-backdrop").addEventListener("click", function() {
|
|
129
|
-
$("paste-modal").classList.add("hidden");
|
|
130
|
-
});
|
|
131
|
-
$("paste-modal").querySelector(".paste-modal-close").addEventListener("click", function() {
|
|
132
|
-
$("paste-modal").classList.add("hidden");
|
|
133
|
-
});
|
|
134
|
-
$("mermaid-modal").querySelector(".confirm-backdrop").addEventListener("click", closeMermaidModal);
|
|
135
|
-
$("mermaid-modal").querySelector(".mermaid-modal-btn[title='Close']").addEventListener("click", closeMermaidModal);
|
|
136
|
-
$("image-modal").querySelector(".confirm-backdrop").addEventListener("click", closeImageModal);
|
|
137
|
-
$("image-modal").querySelector(".image-modal-close").addEventListener("click", closeImageModal);
|
|
138
|
-
|
|
139
|
-
function showImageModal(src) {
|
|
140
|
-
var modal = $("image-modal");
|
|
141
|
-
var img = $("image-modal-img");
|
|
142
|
-
if (!modal || !img) return;
|
|
143
|
-
img.src = src;
|
|
144
|
-
modal.classList.remove("hidden");
|
|
145
|
-
refreshIcons(modal);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function closeImageModal() {
|
|
149
|
-
var modal = $("image-modal");
|
|
150
|
-
if (modal) modal.classList.add("hidden");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// --- State ---
|
|
154
|
-
var ws = null;
|
|
155
|
-
var connected = false;
|
|
156
|
-
var wasConnected = false;
|
|
157
|
-
var verbCycleTimer = null;
|
|
158
|
-
var processing = false;
|
|
159
|
-
// isComposing -> modules/input.js
|
|
160
|
-
var reconnectTimer = null;
|
|
161
|
-
var reconnectDelay = 1000;
|
|
162
|
-
var disconnectNotifTimer = null;
|
|
163
|
-
var disconnectNotifShown = false;
|
|
164
|
-
var activityEl = null;
|
|
165
|
-
var currentMsgEl = null;
|
|
166
|
-
var currentFullText = "";
|
|
167
|
-
// tools, currentThinking -> modules/tools.js
|
|
168
|
-
var highlightTimer = null;
|
|
169
|
-
var activeSessionId = null;
|
|
170
|
-
var sessionDrafts = {};
|
|
171
|
-
var slashCommands = [];
|
|
172
|
-
// slashActiveIdx, slashFiltered, pendingImages, pendingPastes -> modules/input.js
|
|
173
|
-
// pendingPermissions -> modules/tools.js
|
|
174
|
-
var cliSessionId = null;
|
|
175
|
-
var projectName = "";
|
|
176
|
-
var turnCounter = 0;
|
|
177
|
-
var messageUuidMap = [];
|
|
178
|
-
// pendingRewindUuid is now in modules/rewind.js
|
|
179
|
-
// rewindMode is now in modules/rewind.js
|
|
180
|
-
|
|
181
|
-
// --- Progressive history loading ---
|
|
182
|
-
var historyFrom = 0;
|
|
183
|
-
var historyTotal = 0;
|
|
184
|
-
var prependAnchor = null;
|
|
185
|
-
var loadingMore = false;
|
|
186
|
-
var historySentinelObserver = null;
|
|
187
|
-
|
|
188
|
-
// --- Scroll lock ---
|
|
189
|
-
var isUserScrolledUp = false;
|
|
190
|
-
var scrollThreshold = 150;
|
|
191
|
-
|
|
192
|
-
// builtinCommands -> modules/input.js
|
|
193
|
-
|
|
194
|
-
// --- Header session rename ---
|
|
195
|
-
if (headerRenameBtn) {
|
|
196
|
-
headerRenameBtn.addEventListener("click", function () {
|
|
197
|
-
if (!activeSessionId) return;
|
|
198
|
-
var currentText = headerTitleEl.textContent;
|
|
199
|
-
var input = document.createElement("input");
|
|
200
|
-
input.type = "text";
|
|
201
|
-
input.className = "header-rename-input";
|
|
202
|
-
input.value = currentText;
|
|
203
|
-
headerTitleEl.style.display = "none";
|
|
204
|
-
headerRenameBtn.style.display = "none";
|
|
205
|
-
headerTitleEl.parentNode.insertBefore(input, headerTitleEl.nextSibling);
|
|
206
|
-
input.focus();
|
|
207
|
-
input.select();
|
|
208
|
-
|
|
209
|
-
function commit() {
|
|
210
|
-
var newTitle = input.value.trim();
|
|
211
|
-
if (newTitle && newTitle !== currentText && ws && ws.readyState === 1) {
|
|
212
|
-
ws.send(JSON.stringify({ type: "rename_session", id: activeSessionId, title: newTitle }));
|
|
213
|
-
headerTitleEl.textContent = newTitle;
|
|
214
|
-
}
|
|
215
|
-
input.remove();
|
|
216
|
-
headerTitleEl.style.display = "";
|
|
217
|
-
headerRenameBtn.style.display = "";
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
input.addEventListener("keydown", function (e) {
|
|
221
|
-
if (e.key === "Enter") { e.preventDefault(); commit(); }
|
|
222
|
-
if (e.key === "Escape") {
|
|
223
|
-
e.preventDefault();
|
|
224
|
-
input.remove();
|
|
225
|
-
headerTitleEl.style.display = "";
|
|
226
|
-
headerRenameBtn.style.display = "";
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
input.addEventListener("blur", commit);
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// --- Confirm modal ---
|
|
234
|
-
var confirmModal = $("confirm-modal");
|
|
235
|
-
var confirmText = $("confirm-text");
|
|
236
|
-
var confirmOk = $("confirm-ok");
|
|
237
|
-
var confirmCancel = $("confirm-cancel");
|
|
238
|
-
// --- Paste content viewer modal ---
|
|
239
|
-
function showPasteModal(text) {
|
|
240
|
-
var modal = $("paste-modal");
|
|
241
|
-
var body = $("paste-modal-body");
|
|
242
|
-
if (!modal || !body) return;
|
|
243
|
-
body.textContent = text;
|
|
244
|
-
modal.classList.remove("hidden");
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function closePasteModal() {
|
|
248
|
-
var modal = $("paste-modal");
|
|
249
|
-
if (modal) modal.classList.add("hidden");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
var confirmCallback = null;
|
|
253
|
-
|
|
254
|
-
function showConfirm(text, onConfirm) {
|
|
255
|
-
confirmText.textContent = text;
|
|
256
|
-
confirmCallback = onConfirm;
|
|
257
|
-
confirmModal.classList.remove("hidden");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function hideConfirm() {
|
|
261
|
-
confirmModal.classList.add("hidden");
|
|
262
|
-
confirmCallback = null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
confirmOk.addEventListener("click", function () {
|
|
266
|
-
if (confirmCallback) confirmCallback();
|
|
267
|
-
hideConfirm();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
confirmCancel.addEventListener("click", hideConfirm);
|
|
271
|
-
confirmModal.querySelector(".confirm-backdrop").addEventListener("click", hideConfirm);
|
|
272
|
-
|
|
273
|
-
// --- Rewind (module) ---
|
|
274
|
-
initRewind({
|
|
275
|
-
$: $,
|
|
276
|
-
get ws() { return ws; },
|
|
277
|
-
get connected() { return connected; },
|
|
278
|
-
get processing() { return processing; },
|
|
279
|
-
messagesEl: messagesEl,
|
|
280
|
-
addSystemMessage: addSystemMessage,
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// --- Theme (module) ---
|
|
284
|
-
initTheme();
|
|
285
|
-
|
|
286
|
-
// --- Sidebar (module) ---
|
|
287
|
-
initSidebar({
|
|
288
|
-
$: $,
|
|
289
|
-
get ws() { return ws; },
|
|
290
|
-
get connected() { return connected; },
|
|
291
|
-
get projectName() { return projectName; },
|
|
292
|
-
messagesEl: messagesEl,
|
|
293
|
-
sessionListEl: sessionListEl,
|
|
294
|
-
sidebar: sidebar,
|
|
295
|
-
sidebarOverlay: sidebarOverlay,
|
|
296
|
-
sidebarToggleBtn: sidebarToggleBtn,
|
|
297
|
-
sidebarExpandBtn: sidebarExpandBtn,
|
|
298
|
-
hamburgerBtn: hamburgerBtn,
|
|
299
|
-
newSessionBtn: newSessionBtn,
|
|
300
|
-
resumeSessionBtn: resumeSessionBtn,
|
|
301
|
-
headerTitleEl: headerTitleEl,
|
|
302
|
-
showConfirm: showConfirm,
|
|
303
|
-
onFilesTabOpen: function () { loadRootDirectory(); },
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// --- Connect overlay verb cycling ---
|
|
307
|
-
function startVerbCycle() {
|
|
308
|
-
if (verbCycleTimer) return;
|
|
309
|
-
connectVerbEl.textContent = randomThinkingVerb() + "...";
|
|
310
|
-
connectVerbEl.classList.remove("fade-out");
|
|
311
|
-
connectVerbEl.classList.add("fade-in");
|
|
312
|
-
verbCycleTimer = setInterval(function () {
|
|
313
|
-
connectVerbEl.classList.remove("fade-in");
|
|
314
|
-
connectVerbEl.classList.add("fade-out");
|
|
315
|
-
setTimeout(function () {
|
|
316
|
-
connectVerbEl.textContent = randomThinkingVerb() + "...";
|
|
317
|
-
connectVerbEl.classList.remove("fade-out");
|
|
318
|
-
connectVerbEl.classList.add("fade-in");
|
|
319
|
-
}, 400);
|
|
320
|
-
}, 10000);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function stopVerbCycle() {
|
|
324
|
-
if (verbCycleTimer) {
|
|
325
|
-
clearInterval(verbCycleTimer);
|
|
326
|
-
verbCycleTimer = null;
|
|
327
|
-
}
|
|
328
|
-
stopPixelAnim();
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// --- Pixel character animation ---
|
|
332
|
-
var pixelAnimTimer = null;
|
|
333
|
-
var pixelBlocks = [];
|
|
334
|
-
var antennaBlocks = [];
|
|
335
|
-
|
|
336
|
-
(function initPixelAnim() {
|
|
337
|
-
var canvas = document.getElementById("pixel-canvas");
|
|
338
|
-
if (!canvas) return;
|
|
339
|
-
|
|
340
|
-
// Character grid: 1 = body, 2 = eye, 0 = empty
|
|
341
|
-
// 12 cols x 9 rows
|
|
342
|
-
// 0=empty, 1=body, 2=eye, 3=antenna
|
|
343
|
-
var grid = [
|
|
344
|
-
[0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0],
|
|
345
|
-
[0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0],
|
|
346
|
-
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
|
|
347
|
-
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
|
|
348
|
-
[0, 0, 1, 2, 1, 1, 1, 1, 2, 1, 0, 0],
|
|
349
|
-
[0, 0, 1, 2, 1, 1, 1, 1, 2, 1, 0, 0],
|
|
350
|
-
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
|
351
|
-
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
|
352
|
-
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
|
|
353
|
-
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
|
|
354
|
-
[0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0],
|
|
355
|
-
[0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0],
|
|
356
|
-
];
|
|
357
|
-
|
|
358
|
-
var CELL = 12;
|
|
359
|
-
var accent = getThemeColor("base09");
|
|
360
|
-
var eye = getThemeColor("base00");
|
|
361
|
-
var antenna = getThemeColor("base06");
|
|
362
|
-
|
|
363
|
-
for (var r = 0; r < grid.length; r++) {
|
|
364
|
-
for (var c = 0; c < grid[r].length; c++) {
|
|
365
|
-
if (grid[r][c] === 0) continue;
|
|
366
|
-
var el = document.createElement("div");
|
|
367
|
-
el.className = "px";
|
|
368
|
-
var v = grid[r][c];
|
|
369
|
-
el.style.background = v === 2 ? eye : v === 3 ? antenna : accent;
|
|
370
|
-
el.style.left = c * CELL + "px";
|
|
371
|
-
el.style.top = r * CELL + "px";
|
|
372
|
-
if (v === 3) antennaBlocks.push(el);
|
|
373
|
-
canvas.appendChild(el);
|
|
374
|
-
pixelBlocks.push(el);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
})();
|
|
378
|
-
|
|
379
|
-
// Update pixel mascot colors when theme changes
|
|
380
|
-
onThemeChange(function () {
|
|
381
|
-
var newAccent = getThemeColor("base09");
|
|
382
|
-
var newEye = getThemeColor("base00");
|
|
383
|
-
var newAntenna = getThemeColor("base06");
|
|
384
|
-
for (var i = 0; i < pixelBlocks.length; i++) {
|
|
385
|
-
var el = pixelBlocks[i];
|
|
386
|
-
var bg = el.style.background;
|
|
387
|
-
if (bg === accent || bg === newAccent) el.style.background = newAccent;
|
|
388
|
-
else if (bg === eye || bg === newEye) el.style.background = newEye;
|
|
389
|
-
else if (bg === antenna || bg === newAntenna) el.style.background = newAntenna;
|
|
390
|
-
}
|
|
391
|
-
accent = newAccent;
|
|
392
|
-
eye = newEye;
|
|
393
|
-
antenna = newAntenna;
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
function pixelScatter() {
|
|
397
|
-
stopSpark();
|
|
398
|
-
for (var i = 0; i < pixelBlocks.length; i++) {
|
|
399
|
-
var el = pixelBlocks[i];
|
|
400
|
-
var angle = Math.random() * Math.PI * 2;
|
|
401
|
-
var dist = 80 + Math.random() * 120;
|
|
402
|
-
var dx = Math.cos(angle) * dist;
|
|
403
|
-
var dy = Math.sin(angle) * dist;
|
|
404
|
-
var rot = (Math.random() - 0.5) * 360;
|
|
405
|
-
el.className = "px scatter";
|
|
406
|
-
el.style.transform = "translate(" + dx + "px," + dy + "px) rotate(" + rot + "deg)";
|
|
407
|
-
el.style.opacity = "0";
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
var sparkTimer = null;
|
|
412
|
-
|
|
413
|
-
function pixelAssemble() {
|
|
414
|
-
for (var i = 0; i < pixelBlocks.length; i++) {
|
|
415
|
-
(function (el, delay) {
|
|
416
|
-
setTimeout(function () {
|
|
417
|
-
el.className = "px settle";
|
|
418
|
-
el.style.transform = "translate(0,0) rotate(0deg)";
|
|
419
|
-
el.style.opacity = "1";
|
|
420
|
-
}, delay);
|
|
421
|
-
})(pixelBlocks[i], Math.random() * 300);
|
|
422
|
-
}
|
|
423
|
-
startSpark();
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function startSpark() {
|
|
427
|
-
stopSpark();
|
|
428
|
-
var count = 0;
|
|
429
|
-
sparkTimer = setInterval(function () {
|
|
430
|
-
for (var i = 0; i < antennaBlocks.length; i++) {
|
|
431
|
-
if (Math.random() < 0.4) {
|
|
432
|
-
antennaBlocks[i].style.background = "#FFF";
|
|
433
|
-
antennaBlocks[i].style.boxShadow = "0 0 6px 2px rgba(255,255,255,0.6)";
|
|
434
|
-
} else {
|
|
435
|
-
antennaBlocks[i].style.background = antenna;
|
|
436
|
-
antennaBlocks[i].style.boxShadow = "none";
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
count++;
|
|
440
|
-
if (count > 20) stopSpark();
|
|
441
|
-
}, 80);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function stopSpark() {
|
|
445
|
-
if (sparkTimer) {
|
|
446
|
-
clearInterval(sparkTimer);
|
|
447
|
-
sparkTimer = null;
|
|
448
|
-
}
|
|
449
|
-
for (var i = 0; i < antennaBlocks.length; i++) {
|
|
450
|
-
antennaBlocks[i].style.background = antenna;
|
|
451
|
-
antennaBlocks[i].style.boxShadow = "none";
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function startPixelAnim() {
|
|
456
|
-
if (pixelAnimTimer) return;
|
|
457
|
-
// Start scattered
|
|
458
|
-
for (var i = 0; i < pixelBlocks.length; i++) {
|
|
459
|
-
var angle = Math.random() * Math.PI * 2;
|
|
460
|
-
var dist = 80 + Math.random() * 120;
|
|
461
|
-
pixelBlocks[i].className = "px";
|
|
462
|
-
pixelBlocks[i].style.transform = "translate(" + (Math.cos(angle) * dist) + "px," + (Math.sin(angle) * dist) + "px) rotate(" + ((Math.random() - 0.5) * 360) + "deg)";
|
|
463
|
-
pixelBlocks[i].style.opacity = "0";
|
|
464
|
-
}
|
|
465
|
-
function cycle() {
|
|
466
|
-
pixelAssemble();
|
|
467
|
-
pixelAnimTimer = setTimeout(function () {
|
|
468
|
-
pixelScatter();
|
|
469
|
-
pixelAnimTimer = setTimeout(cycle, 800);
|
|
470
|
-
}, 2200);
|
|
471
|
-
}
|
|
472
|
-
pixelAnimTimer = setTimeout(cycle, 300);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function stopPixelAnim() {
|
|
476
|
-
if (pixelAnimTimer) {
|
|
477
|
-
clearTimeout(pixelAnimTimer);
|
|
478
|
-
pixelAnimTimer = null;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// --- Dynamic favicon ---
|
|
483
|
-
var faviconSvg = null;
|
|
484
|
-
var faviconLink = document.querySelector('link[rel="icon"]');
|
|
485
|
-
|
|
486
|
-
function updateFavicon(bgColor) {
|
|
487
|
-
if (!faviconLink) return;
|
|
488
|
-
if (!faviconSvg) {
|
|
489
|
-
var xhr = new XMLHttpRequest();
|
|
490
|
-
xhr.open("GET", basePath + "favicon.svg", false);
|
|
491
|
-
xhr.send();
|
|
492
|
-
if (xhr.status === 200) faviconSvg = xhr.responseText;
|
|
493
|
-
else return;
|
|
494
|
-
}
|
|
495
|
-
var svg = faviconSvg.replace(/fill="#57AB5A"/g, 'fill="' + bgColor + '"');
|
|
496
|
-
faviconLink.href = "data:image/svg+xml," + encodeURIComponent(svg);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// --- Status & Activity ---
|
|
500
|
-
function setSendBtnMode(mode) {
|
|
501
|
-
if (mode === "stop") {
|
|
502
|
-
sendBtn.disabled = false;
|
|
503
|
-
sendBtn.classList.add("stop");
|
|
504
|
-
sendBtn.innerHTML = '<i data-lucide="square"></i>';
|
|
505
|
-
} else {
|
|
506
|
-
sendBtn.classList.remove("stop");
|
|
507
|
-
sendBtn.innerHTML = '<i data-lucide="arrow-up"></i>';
|
|
508
|
-
}
|
|
509
|
-
refreshIcons();
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
var ioTimer = null;
|
|
513
|
-
var faviconIoTimer = null;
|
|
514
|
-
function blinkIO() {
|
|
515
|
-
if (!processing) return;
|
|
516
|
-
statusDot.classList.add("io");
|
|
517
|
-
clearTimeout(ioTimer);
|
|
518
|
-
ioTimer = setTimeout(function () { statusDot.classList.remove("io"); }, 60);
|
|
519
|
-
|
|
520
|
-
// Blink favicon: dim then restore
|
|
521
|
-
updateFavicon(getComputedVar("--sidebar-bg"));
|
|
522
|
-
clearTimeout(faviconIoTimer);
|
|
523
|
-
faviconIoTimer = setTimeout(function () { updateFavicon(getComputedVar("--success")); }, 60);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// --- Urgent favicon blink (permission / ask user) ---
|
|
527
|
-
var urgentBlinkTimer = null;
|
|
528
|
-
var savedTitle = null;
|
|
529
|
-
function startUrgentBlink() {
|
|
530
|
-
if (urgentBlinkTimer) return;
|
|
531
|
-
savedTitle = document.title;
|
|
532
|
-
var colors = [getComputedVar("--accent"), getComputedVar("--success"), getComputedVar("--accent"), getComputedVar("--text"), getComputedVar("--accent"), getComputedVar("--success")];
|
|
533
|
-
var tick = 0;
|
|
534
|
-
urgentBlinkTimer = setInterval(function () {
|
|
535
|
-
updateFavicon(colors[tick % colors.length]);
|
|
536
|
-
document.title = tick % 2 === 0 ? "\u26A0 Input needed" : savedTitle;
|
|
537
|
-
tick++;
|
|
538
|
-
}, 250);
|
|
539
|
-
}
|
|
540
|
-
function stopUrgentBlink() {
|
|
541
|
-
if (!urgentBlinkTimer) return;
|
|
542
|
-
clearInterval(urgentBlinkTimer);
|
|
543
|
-
urgentBlinkTimer = null;
|
|
544
|
-
updateFavicon(getComputedVar("--success"));
|
|
545
|
-
if (savedTitle) document.title = savedTitle;
|
|
546
|
-
savedTitle = null;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function setStatus(status) {
|
|
550
|
-
statusDot.className = "status-dot";
|
|
551
|
-
if (status === "connected") {
|
|
552
|
-
statusDot.classList.add("connected");
|
|
553
|
-
connected = true;
|
|
554
|
-
processing = false;
|
|
555
|
-
sendBtn.disabled = false;
|
|
556
|
-
setSendBtnMode("send");
|
|
557
|
-
connectOverlay.classList.add("hidden");
|
|
558
|
-
stopVerbCycle();
|
|
559
|
-
updateFavicon(getComputedVar("--success"));
|
|
560
|
-
} else if (status === "processing") {
|
|
561
|
-
statusDot.classList.add("processing");
|
|
562
|
-
processing = true;
|
|
563
|
-
setSendBtnMode("stop");
|
|
564
|
-
updateFavicon(getComputedVar("--success"));
|
|
565
|
-
} else {
|
|
566
|
-
connected = false;
|
|
567
|
-
sendBtn.disabled = true;
|
|
568
|
-
connectOverlay.classList.remove("hidden");
|
|
569
|
-
connectStatusEl.textContent = "Reconnecting...";
|
|
570
|
-
startVerbCycle();
|
|
571
|
-
startPixelAnim();
|
|
572
|
-
updateFavicon(getComputedVar("--error"));
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function setActivity(text) {
|
|
577
|
-
if (text) {
|
|
578
|
-
if (!activityEl) {
|
|
579
|
-
activityEl = document.createElement("div");
|
|
580
|
-
activityEl.className = "activity-inline";
|
|
581
|
-
activityEl.innerHTML =
|
|
582
|
-
'<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
|
|
583
|
-
'<span class="activity-text"></span>';
|
|
584
|
-
addToMessages(activityEl);
|
|
585
|
-
refreshIcons();
|
|
586
|
-
}
|
|
587
|
-
activityEl.querySelector(".activity-text").textContent = text;
|
|
588
|
-
scrollToBottom();
|
|
589
|
-
} else {
|
|
590
|
-
if (activityEl) {
|
|
591
|
-
activityEl.remove();
|
|
592
|
-
activityEl = null;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// --- Model selector ---
|
|
598
|
-
var modelMenuWrap = $("model-menu-wrap");
|
|
599
|
-
var modelBtn = $("model-btn");
|
|
600
|
-
var modelLabel = $("model-label");
|
|
601
|
-
var modelMenu = $("model-menu");
|
|
602
|
-
|
|
603
|
-
function modelDisplayName(value, models) {
|
|
604
|
-
if (!value) return "";
|
|
605
|
-
// Look up displayName from models list
|
|
606
|
-
if (models) {
|
|
607
|
-
for (var i = 0; i < models.length; i++) {
|
|
608
|
-
if (models[i].value === value && models[i].displayName) return models[i].displayName;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
return value;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
var currentModels = [];
|
|
615
|
-
|
|
616
|
-
function updateModelSelector(current, models) {
|
|
617
|
-
if (!modelMenuWrap || !modelBtn || !modelMenu) return;
|
|
618
|
-
currentModels = models;
|
|
619
|
-
modelLabel.textContent = modelDisplayName(current, models);
|
|
620
|
-
modelMenuWrap.classList.remove("hidden");
|
|
621
|
-
|
|
622
|
-
modelMenu.innerHTML = "";
|
|
623
|
-
var list = models.length > 0 ? models : (current ? [{ value: current, displayName: current }] : []);
|
|
624
|
-
for (var i = 0; i < list.length; i++) {
|
|
625
|
-
var item = list[i];
|
|
626
|
-
var value = item.value || "";
|
|
627
|
-
var label = item.displayName || value;
|
|
628
|
-
var btn = document.createElement("button");
|
|
629
|
-
btn.className = "model-menu-item";
|
|
630
|
-
if (value === current) btn.classList.add("active");
|
|
631
|
-
btn.dataset.model = value;
|
|
632
|
-
btn.textContent = label;
|
|
633
|
-
btn.addEventListener("click", function () {
|
|
634
|
-
var model = this.dataset.model;
|
|
635
|
-
if (ws && ws.readyState === 1) {
|
|
636
|
-
ws.send(JSON.stringify({ type: "set_model", model: model }));
|
|
637
|
-
}
|
|
638
|
-
modelMenu.classList.add("hidden");
|
|
639
|
-
modelBtn.classList.remove("active");
|
|
640
|
-
});
|
|
641
|
-
modelMenu.appendChild(btn);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
modelBtn.addEventListener("click", function (e) {
|
|
646
|
-
e.stopPropagation();
|
|
647
|
-
var open = modelMenu.classList.toggle("hidden");
|
|
648
|
-
modelBtn.classList.toggle("active", !open);
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
document.addEventListener("click", function (e) {
|
|
652
|
-
if (!modelMenu.contains(e.target) && e.target !== modelBtn) {
|
|
653
|
-
modelMenu.classList.add("hidden");
|
|
654
|
-
modelBtn.classList.remove("active");
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
// --- Usage panel ---
|
|
659
|
-
var usagePanel = $("usage-panel");
|
|
660
|
-
var usagePanelClose = $("usage-panel-close");
|
|
661
|
-
var usageCostEl = $("usage-cost");
|
|
662
|
-
var usageInputEl = $("usage-input");
|
|
663
|
-
var usageOutputEl = $("usage-output");
|
|
664
|
-
var usageCacheReadEl = $("usage-cache-read");
|
|
665
|
-
var usageCacheWriteEl = $("usage-cache-write");
|
|
666
|
-
var usageTurnsEl = $("usage-turns");
|
|
667
|
-
var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
668
|
-
|
|
669
|
-
function formatTokens(n) {
|
|
670
|
-
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
|
|
671
|
-
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
|
|
672
|
-
return String(n);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function updateUsagePanel() {
|
|
676
|
-
if (!usageCostEl) return;
|
|
677
|
-
usageCostEl.textContent = "$" + sessionUsage.cost.toFixed(4);
|
|
678
|
-
usageInputEl.textContent = formatTokens(sessionUsage.input);
|
|
679
|
-
usageOutputEl.textContent = formatTokens(sessionUsage.output);
|
|
680
|
-
usageCacheReadEl.textContent = formatTokens(sessionUsage.cacheRead);
|
|
681
|
-
usageCacheWriteEl.textContent = formatTokens(sessionUsage.cacheWrite);
|
|
682
|
-
usageTurnsEl.textContent = String(sessionUsage.turns);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function accumulateUsage(cost, usage) {
|
|
686
|
-
if (cost != null) sessionUsage.cost += cost;
|
|
687
|
-
if (usage) {
|
|
688
|
-
sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
|
|
689
|
-
sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
|
|
690
|
-
sessionUsage.cacheRead += usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
|
|
691
|
-
sessionUsage.cacheWrite += usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
|
|
692
|
-
}
|
|
693
|
-
sessionUsage.turns++;
|
|
694
|
-
updateUsagePanel();
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function resetUsage() {
|
|
698
|
-
sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
699
|
-
updateUsagePanel();
|
|
700
|
-
if (usagePanel) usagePanel.classList.add("hidden");
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function toggleUsagePanel() {
|
|
704
|
-
if (!usagePanel) return;
|
|
705
|
-
usagePanel.classList.toggle("hidden");
|
|
706
|
-
refreshIcons();
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
if (usagePanelClose) {
|
|
710
|
-
usagePanelClose.addEventListener("click", function () {
|
|
711
|
-
usagePanel.classList.add("hidden");
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// --- Status panel ---
|
|
716
|
-
var statusPanel = $("status-panel");
|
|
717
|
-
var statusPanelClose = $("status-panel-close");
|
|
718
|
-
var statusPidEl = $("status-pid");
|
|
719
|
-
var statusUptimeEl = $("status-uptime");
|
|
720
|
-
var statusRssEl = $("status-rss");
|
|
721
|
-
var statusHeapUsedEl = $("status-heap-used");
|
|
722
|
-
var statusHeapTotalEl = $("status-heap-total");
|
|
723
|
-
var statusExternalEl = $("status-external");
|
|
724
|
-
var statusSessionsEl = $("status-sessions");
|
|
725
|
-
var statusProcessingEl = $("status-processing");
|
|
726
|
-
var statusClientsEl = $("status-clients");
|
|
727
|
-
var statusTerminalsEl = $("status-terminals");
|
|
728
|
-
var statusRefreshTimer = null;
|
|
729
|
-
|
|
730
|
-
function formatBytes(n) {
|
|
731
|
-
if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
|
|
732
|
-
if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
|
|
733
|
-
if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
|
|
734
|
-
return n + " B";
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function formatUptime(seconds) {
|
|
738
|
-
var d = Math.floor(seconds / 86400);
|
|
739
|
-
var h = Math.floor((seconds % 86400) / 3600);
|
|
740
|
-
var m = Math.floor((seconds % 3600) / 60);
|
|
741
|
-
var s = Math.floor(seconds % 60);
|
|
742
|
-
if (d > 0) return d + "d " + h + "h " + m + "m";
|
|
743
|
-
if (h > 0) return h + "h " + m + "m " + s + "s";
|
|
744
|
-
return m + "m " + s + "s";
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function updateStatusPanel(data) {
|
|
748
|
-
if (!statusPidEl) return;
|
|
749
|
-
statusPidEl.textContent = String(data.pid);
|
|
750
|
-
statusUptimeEl.textContent = formatUptime(data.uptime);
|
|
751
|
-
statusRssEl.textContent = formatBytes(data.memory.rss);
|
|
752
|
-
statusHeapUsedEl.textContent = formatBytes(data.memory.heapUsed);
|
|
753
|
-
statusHeapTotalEl.textContent = formatBytes(data.memory.heapTotal);
|
|
754
|
-
statusExternalEl.textContent = formatBytes(data.memory.external);
|
|
755
|
-
statusSessionsEl.textContent = String(data.sessions);
|
|
756
|
-
statusProcessingEl.textContent = String(data.processing);
|
|
757
|
-
statusClientsEl.textContent = String(data.clients);
|
|
758
|
-
statusTerminalsEl.textContent = String(data.terminals);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function requestProcessStats() {
|
|
762
|
-
if (ws && ws.readyState === 1) {
|
|
763
|
-
ws.send(JSON.stringify({ type: "process_stats" }));
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function toggleStatusPanel() {
|
|
768
|
-
if (!statusPanel) return;
|
|
769
|
-
var opening = statusPanel.classList.contains("hidden");
|
|
770
|
-
statusPanel.classList.toggle("hidden");
|
|
771
|
-
if (opening) {
|
|
772
|
-
requestProcessStats();
|
|
773
|
-
statusRefreshTimer = setInterval(requestProcessStats, 5000);
|
|
774
|
-
} else {
|
|
775
|
-
if (statusRefreshTimer) {
|
|
776
|
-
clearInterval(statusRefreshTimer);
|
|
777
|
-
statusRefreshTimer = null;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
refreshIcons();
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
if (statusPanelClose) {
|
|
784
|
-
statusPanelClose.addEventListener("click", function () {
|
|
785
|
-
statusPanel.classList.add("hidden");
|
|
786
|
-
if (statusRefreshTimer) {
|
|
787
|
-
clearInterval(statusRefreshTimer);
|
|
788
|
-
statusRefreshTimer = null;
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// --- Context panel ---
|
|
794
|
-
var contextPanel = $("context-panel");
|
|
795
|
-
var contextPanelClose = $("context-panel-close");
|
|
796
|
-
var contextPanelMinimize = $("context-panel-minimize");
|
|
797
|
-
var contextBarFill = $("context-bar-fill");
|
|
798
|
-
var contextBarPct = $("context-bar-pct");
|
|
799
|
-
var contextUsedEl = $("context-used");
|
|
800
|
-
var contextWindowEl = $("context-window");
|
|
801
|
-
var contextMaxOutputEl = $("context-max-output");
|
|
802
|
-
var contextInputEl = $("context-input");
|
|
803
|
-
var contextOutputEl = $("context-output");
|
|
804
|
-
var contextCacheReadEl = $("context-cache-read");
|
|
805
|
-
var contextCacheWriteEl = $("context-cache-write");
|
|
806
|
-
var contextModelEl = $("context-model");
|
|
807
|
-
var contextCostEl = $("context-cost");
|
|
808
|
-
var contextTurnsEl = $("context-turns");
|
|
809
|
-
var contextMini = $("context-mini");
|
|
810
|
-
var contextMiniFill = $("context-mini-fill");
|
|
811
|
-
var contextMiniLabel = $("context-mini-label");
|
|
812
|
-
var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
813
|
-
|
|
814
|
-
function contextPctClass(pct) {
|
|
815
|
-
return pct >= 85 ? " danger" : pct >= 60 ? " warn" : "";
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function updateContextPanel() {
|
|
819
|
-
if (!contextUsedEl) return;
|
|
820
|
-
// Context window usage = input tokens (includes cache read/write) + output tokens
|
|
821
|
-
var used = contextData.input + contextData.output;
|
|
822
|
-
var win = contextData.contextWindow;
|
|
823
|
-
var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
|
|
824
|
-
var cls = contextPctClass(pct);
|
|
825
|
-
// Panel bar
|
|
826
|
-
contextBarFill.style.width = pct.toFixed(1) + "%";
|
|
827
|
-
contextBarFill.className = "context-bar-fill" + cls;
|
|
828
|
-
contextBarPct.textContent = pct.toFixed(0) + "%";
|
|
829
|
-
// Mini bar
|
|
830
|
-
if (contextMiniFill) {
|
|
831
|
-
contextMiniFill.style.width = pct.toFixed(1) + "%";
|
|
832
|
-
contextMiniFill.className = "context-mini-fill" + cls;
|
|
833
|
-
}
|
|
834
|
-
if (contextMiniLabel) {
|
|
835
|
-
contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
|
|
836
|
-
}
|
|
837
|
-
contextUsedEl.textContent = formatTokens(used);
|
|
838
|
-
contextWindowEl.textContent = win > 0 ? formatTokens(win) : "-";
|
|
839
|
-
contextMaxOutputEl.textContent = contextData.maxOutputTokens > 0 ? formatTokens(contextData.maxOutputTokens) : "-";
|
|
840
|
-
contextInputEl.textContent = formatTokens(contextData.input);
|
|
841
|
-
contextOutputEl.textContent = formatTokens(contextData.output);
|
|
842
|
-
contextCacheReadEl.textContent = formatTokens(contextData.cacheRead);
|
|
843
|
-
contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
|
|
844
|
-
contextModelEl.textContent = contextData.model;
|
|
845
|
-
contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
|
|
846
|
-
contextTurnsEl.textContent = String(contextData.turns);
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function accumulateContext(cost, usage, modelUsage) {
|
|
850
|
-
if (cost != null) contextData.cost += cost;
|
|
851
|
-
// Use latest turn values (not cumulative) since each turn's input_tokens
|
|
852
|
-
// already includes the full conversation context up to that point
|
|
853
|
-
if (usage) {
|
|
854
|
-
contextData.input = usage.input_tokens || usage.inputTokens || 0;
|
|
855
|
-
contextData.output = usage.output_tokens || usage.outputTokens || 0;
|
|
856
|
-
contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
|
|
857
|
-
contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
|
|
858
|
-
}
|
|
859
|
-
contextData.turns++;
|
|
860
|
-
if (modelUsage) {
|
|
861
|
-
var models = Object.keys(modelUsage);
|
|
862
|
-
if (models.length > 0) {
|
|
863
|
-
var m = models[0];
|
|
864
|
-
var mu = modelUsage[m];
|
|
865
|
-
contextData.model = m;
|
|
866
|
-
if (mu.contextWindow) contextData.contextWindow = mu.contextWindow;
|
|
867
|
-
if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
updateContextPanel();
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// contextView: "off" | "mini" | "panel"
|
|
874
|
-
function getContextView() {
|
|
875
|
-
try { return localStorage.getItem("claude-relay-context-view") || "off"; } catch (e) { return "off"; }
|
|
876
|
-
}
|
|
877
|
-
function setContextView(v) {
|
|
878
|
-
try { localStorage.setItem("claude-relay-context-view", v); } catch (e) {}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
function applyContextView(view) {
|
|
882
|
-
if (contextPanel) contextPanel.classList.toggle("hidden", view !== "panel");
|
|
883
|
-
if (contextMini) contextMini.classList.toggle("hidden", view !== "mini");
|
|
884
|
-
if (view === "panel") refreshIcons();
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function resetContextData() {
|
|
888
|
-
contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
889
|
-
updateContextPanel();
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function resetContext() {
|
|
893
|
-
resetContextData();
|
|
894
|
-
// Keep view state, just reset data
|
|
895
|
-
applyContextView(getContextView());
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function minimizeContext() {
|
|
899
|
-
setContextView("mini");
|
|
900
|
-
applyContextView("mini");
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function expandContext() {
|
|
904
|
-
setContextView("panel");
|
|
905
|
-
applyContextView("panel");
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function toggleContextPanel() {
|
|
909
|
-
if (!contextPanel) return;
|
|
910
|
-
var view = getContextView();
|
|
911
|
-
if (view === "panel") {
|
|
912
|
-
setContextView("mini");
|
|
913
|
-
applyContextView("mini");
|
|
914
|
-
} else {
|
|
915
|
-
setContextView("panel");
|
|
916
|
-
applyContextView("panel");
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
if (contextPanelClose) {
|
|
921
|
-
contextPanelClose.addEventListener("click", function () {
|
|
922
|
-
setContextView("off");
|
|
923
|
-
applyContextView("off");
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
if (contextPanelMinimize) {
|
|
928
|
-
contextPanelMinimize.addEventListener("click", minimizeContext);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// Restore context view on load
|
|
932
|
-
applyContextView(getContextView());
|
|
933
|
-
|
|
934
|
-
if (contextMini) {
|
|
935
|
-
contextMini.addEventListener("click", expandContext);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function addToMessages(el) {
|
|
939
|
-
if (prependAnchor) messagesEl.insertBefore(el, prependAnchor);
|
|
940
|
-
else messagesEl.appendChild(el);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
var newMsgBtn = $("new-msg-btn");
|
|
944
|
-
var newMsgBtnDefault = "\u2193 Latest";
|
|
945
|
-
var newMsgBtnActivity = "\u2193 New activity";
|
|
946
|
-
|
|
947
|
-
messagesEl.addEventListener("scroll", function () {
|
|
948
|
-
var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
|
|
949
|
-
isUserScrolledUp = distFromBottom > scrollThreshold;
|
|
950
|
-
if (isUserScrolledUp) {
|
|
951
|
-
if (newMsgBtn.classList.contains("hidden")) {
|
|
952
|
-
newMsgBtn.textContent = newMsgBtnDefault;
|
|
953
|
-
}
|
|
954
|
-
newMsgBtn.classList.remove("hidden");
|
|
955
|
-
} else {
|
|
956
|
-
newMsgBtn.classList.add("hidden");
|
|
957
|
-
newMsgBtn.textContent = newMsgBtnDefault;
|
|
958
|
-
}
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
newMsgBtn.addEventListener("click", function () {
|
|
962
|
-
forceScrollToBottom();
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
function scrollToBottom() {
|
|
966
|
-
if (prependAnchor) return;
|
|
967
|
-
if (isUserScrolledUp) {
|
|
968
|
-
newMsgBtn.textContent = newMsgBtnActivity;
|
|
969
|
-
newMsgBtn.classList.remove("hidden");
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
requestAnimationFrame(function () {
|
|
973
|
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function forceScrollToBottom() {
|
|
978
|
-
if (prependAnchor) return;
|
|
979
|
-
isUserScrolledUp = false;
|
|
980
|
-
newMsgBtn.classList.add("hidden");
|
|
981
|
-
newMsgBtn.textContent = newMsgBtnDefault;
|
|
982
|
-
requestAnimationFrame(function () {
|
|
983
|
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// --- Tools module ---
|
|
988
|
-
initTools({
|
|
989
|
-
$: $,
|
|
990
|
-
get ws() { return ws; },
|
|
991
|
-
get connected() { return connected; },
|
|
992
|
-
get turnCounter() { return turnCounter; },
|
|
993
|
-
messagesEl: messagesEl,
|
|
994
|
-
inputEl: inputEl,
|
|
995
|
-
finalizeAssistantBlock: function() { finalizeAssistantBlock(); },
|
|
996
|
-
addToMessages: function(el) { addToMessages(el); },
|
|
997
|
-
scrollToBottom: function() { scrollToBottom(); },
|
|
998
|
-
setActivity: function(text) { setActivity(text); },
|
|
999
|
-
stopUrgentBlink: function() { stopUrgentBlink(); },
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
// isPlanFile, toolSummary, toolActivityText, shortPath -> modules/tools.js
|
|
1003
|
-
|
|
1004
|
-
// AskUserQuestion, PermissionRequest, Plan, Todo, Thinking, Tool items -> modules/tools.js
|
|
1005
|
-
|
|
1006
|
-
// --- DOM: Messages ---
|
|
1007
|
-
function addUserMessage(text, images, pastes) {
|
|
1008
|
-
var div = document.createElement("div");
|
|
1009
|
-
div.className = "msg-user";
|
|
1010
|
-
div.dataset.turn = ++turnCounter;
|
|
1011
|
-
var bubble = document.createElement("div");
|
|
1012
|
-
bubble.className = "bubble";
|
|
1013
|
-
bubble.dir = "auto";
|
|
1014
|
-
|
|
1015
|
-
if (images && images.length > 0) {
|
|
1016
|
-
var imgRow = document.createElement("div");
|
|
1017
|
-
imgRow.className = "bubble-images";
|
|
1018
|
-
for (var i = 0; i < images.length; i++) {
|
|
1019
|
-
var img = document.createElement("img");
|
|
1020
|
-
img.src = "data:" + images[i].mediaType + ";base64," + images[i].data;
|
|
1021
|
-
img.className = "bubble-img";
|
|
1022
|
-
img.addEventListener("click", function () { showImageModal(this.src); });
|
|
1023
|
-
imgRow.appendChild(img);
|
|
1024
|
-
}
|
|
1025
|
-
bubble.appendChild(imgRow);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
if (pastes && pastes.length > 0) {
|
|
1029
|
-
var pasteRow = document.createElement("div");
|
|
1030
|
-
pasteRow.className = "bubble-pastes";
|
|
1031
|
-
for (var p = 0; p < pastes.length; p++) {
|
|
1032
|
-
(function (pasteText) {
|
|
1033
|
-
var chip = document.createElement("div");
|
|
1034
|
-
chip.className = "bubble-paste";
|
|
1035
|
-
var preview = pasteText.substring(0, 60).replace(/\n/g, " ");
|
|
1036
|
-
if (pasteText.length > 60) preview += "...";
|
|
1037
|
-
chip.innerHTML = '<span class="bubble-paste-preview">' + escapeHtml(preview) + '</span><span class="bubble-paste-label">PASTED</span>';
|
|
1038
|
-
chip.addEventListener("click", function (e) {
|
|
1039
|
-
e.stopPropagation();
|
|
1040
|
-
showPasteModal(pasteText);
|
|
1041
|
-
});
|
|
1042
|
-
pasteRow.appendChild(chip);
|
|
1043
|
-
})(pastes[p]);
|
|
1044
|
-
}
|
|
1045
|
-
bubble.appendChild(pasteRow);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (text) {
|
|
1049
|
-
var textEl = document.createElement("span");
|
|
1050
|
-
textEl.textContent = text;
|
|
1051
|
-
bubble.appendChild(textEl);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
div.appendChild(bubble);
|
|
1055
|
-
addToMessages(div);
|
|
1056
|
-
forceScrollToBottom();
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function ensureAssistantBlock() {
|
|
1060
|
-
if (!currentMsgEl) {
|
|
1061
|
-
currentMsgEl = document.createElement("div");
|
|
1062
|
-
currentMsgEl.className = "msg-assistant";
|
|
1063
|
-
currentMsgEl.dataset.turn = turnCounter;
|
|
1064
|
-
currentMsgEl.innerHTML = '<div class="md-content" dir="auto"></div>';
|
|
1065
|
-
addToMessages(currentMsgEl);
|
|
1066
|
-
currentFullText = "";
|
|
1067
|
-
}
|
|
1068
|
-
return currentMsgEl;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
function addCopyHandler(msgEl, rawText) {
|
|
1072
|
-
var primed = false;
|
|
1073
|
-
var resetTimer = null;
|
|
1074
|
-
|
|
1075
|
-
var isTouchDevice = "ontouchstart" in window;
|
|
1076
|
-
|
|
1077
|
-
var hint = document.createElement("div");
|
|
1078
|
-
hint.className = "msg-copy-hint";
|
|
1079
|
-
hint.textContent = (isTouchDevice ? "Tap" : "Click") + " to grab this";
|
|
1080
|
-
msgEl.appendChild(hint);
|
|
1081
|
-
|
|
1082
|
-
function reset() {
|
|
1083
|
-
primed = false;
|
|
1084
|
-
msgEl.classList.remove("copy-primed", "copy-done");
|
|
1085
|
-
hint.textContent = (isTouchDevice ? "Tap" : "Click") + " to grab this";
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
msgEl.addEventListener("click", function (e) {
|
|
1089
|
-
// Don't intercept clicks on links or code blocks
|
|
1090
|
-
if (e.target.closest("a, pre, code")) return;
|
|
1091
|
-
// Don't intercept text selection
|
|
1092
|
-
var sel = window.getSelection();
|
|
1093
|
-
if (sel && sel.toString().length > 0) return;
|
|
1094
|
-
|
|
1095
|
-
if (!primed) {
|
|
1096
|
-
primed = true;
|
|
1097
|
-
msgEl.classList.add("copy-primed");
|
|
1098
|
-
hint.textContent = isTouchDevice ? "Tap again to grab" : "Click again to grab";
|
|
1099
|
-
clearTimeout(resetTimer);
|
|
1100
|
-
resetTimer = setTimeout(reset, 3000);
|
|
1101
|
-
} else {
|
|
1102
|
-
clearTimeout(resetTimer);
|
|
1103
|
-
copyToClipboard(rawText).then(function () {
|
|
1104
|
-
msgEl.classList.remove("copy-primed");
|
|
1105
|
-
msgEl.classList.add("copy-done");
|
|
1106
|
-
hint.textContent = "Grabbed!";
|
|
1107
|
-
resetTimer = setTimeout(reset, 1500);
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
document.addEventListener("click", function (e) {
|
|
1113
|
-
if (primed && !msgEl.contains(e.target)) reset();
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
function appendDelta(text) {
|
|
1118
|
-
ensureAssistantBlock();
|
|
1119
|
-
currentFullText += text;
|
|
1120
|
-
var contentEl = currentMsgEl.querySelector(".md-content");
|
|
1121
|
-
contentEl.innerHTML = renderMarkdown(currentFullText);
|
|
1122
|
-
|
|
1123
|
-
if (highlightTimer) clearTimeout(highlightTimer);
|
|
1124
|
-
highlightTimer = setTimeout(function () {
|
|
1125
|
-
highlightCodeBlocks(contentEl);
|
|
1126
|
-
}, 150);
|
|
1127
|
-
|
|
1128
|
-
scrollToBottom();
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function finalizeAssistantBlock() {
|
|
1132
|
-
if (currentMsgEl) {
|
|
1133
|
-
var contentEl = currentMsgEl.querySelector(".md-content");
|
|
1134
|
-
if (contentEl) {
|
|
1135
|
-
highlightCodeBlocks(contentEl);
|
|
1136
|
-
renderMermaidBlocks(contentEl);
|
|
1137
|
-
}
|
|
1138
|
-
if (currentFullText) {
|
|
1139
|
-
addCopyHandler(currentMsgEl, currentFullText);
|
|
1140
|
-
}
|
|
1141
|
-
// Assistant text appeared, so break the current tool group
|
|
1142
|
-
closeToolGroup();
|
|
1143
|
-
}
|
|
1144
|
-
currentMsgEl = null;
|
|
1145
|
-
currentFullText = "";
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function addSystemMessage(text, isError) {
|
|
1149
|
-
var div = document.createElement("div");
|
|
1150
|
-
div.className = "sys-msg" + (isError ? " error" : "");
|
|
1151
|
-
div.innerHTML = '<span class="sys-text"></span>';
|
|
1152
|
-
div.querySelector(".sys-text").textContent = text;
|
|
1153
|
-
addToMessages(div);
|
|
1154
|
-
scrollToBottom();
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function resetClientState() {
|
|
1158
|
-
messagesEl.innerHTML = "";
|
|
1159
|
-
currentMsgEl = null;
|
|
1160
|
-
currentFullText = "";
|
|
1161
|
-
resetToolState();
|
|
1162
|
-
clearPendingImages();
|
|
1163
|
-
activityEl = null;
|
|
1164
|
-
processing = false;
|
|
1165
|
-
turnCounter = 0;
|
|
1166
|
-
messageUuidMap = [];
|
|
1167
|
-
historyFrom = 0;
|
|
1168
|
-
historyTotal = 0;
|
|
1169
|
-
prependAnchor = null;
|
|
1170
|
-
loadingMore = false;
|
|
1171
|
-
isUserScrolledUp = false;
|
|
1172
|
-
newMsgBtn.classList.add("hidden");
|
|
1173
|
-
setRewindMode(false);
|
|
1174
|
-
removeSearchTimeline();
|
|
1175
|
-
setActivity(null);
|
|
1176
|
-
setStatus("connected");
|
|
1177
|
-
enableMainInput();
|
|
1178
|
-
resetUsage();
|
|
1179
|
-
resetContext();
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// --- WebSocket ---
|
|
1183
|
-
var connectTimeoutId = null;
|
|
1184
|
-
|
|
1185
|
-
function connect() {
|
|
1186
|
-
if (ws) { ws.onclose = null; ws.close(); }
|
|
1187
|
-
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
1188
|
-
|
|
1189
|
-
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
1190
|
-
ws = new WebSocket(protocol + "//" + location.host + wsPath);
|
|
1191
|
-
|
|
1192
|
-
connectStatusEl.textContent = "Connecting...";
|
|
1193
|
-
|
|
1194
|
-
// If not connected within 3s, force retry
|
|
1195
|
-
connectTimeoutId = setTimeout(function () {
|
|
1196
|
-
if (!connected) {
|
|
1197
|
-
ws.onclose = null;
|
|
1198
|
-
ws.onerror = null;
|
|
1199
|
-
ws.close();
|
|
1200
|
-
connect();
|
|
1201
|
-
}
|
|
1202
|
-
}, 3000);
|
|
1203
|
-
|
|
1204
|
-
ws.onopen = function () {
|
|
1205
|
-
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
1206
|
-
// Cancel pending "connection lost" notification if reconnected quickly
|
|
1207
|
-
if (disconnectNotifTimer) {
|
|
1208
|
-
clearTimeout(disconnectNotifTimer);
|
|
1209
|
-
disconnectNotifTimer = null;
|
|
1210
|
-
}
|
|
1211
|
-
// Only show "restored" notification if "lost" was actually shown
|
|
1212
|
-
if (wasConnected && disconnectNotifShown && !document.hasFocus() && "serviceWorker" in navigator) {
|
|
1213
|
-
navigator.serviceWorker.ready.then(function (reg) {
|
|
1214
|
-
reg.showNotification("Claude Relay", {
|
|
1215
|
-
body: "Server connection restored",
|
|
1216
|
-
tag: "claude-disconnect",
|
|
1217
|
-
});
|
|
1218
|
-
}).catch(function () {});
|
|
1219
|
-
}
|
|
1220
|
-
disconnectNotifShown = false;
|
|
1221
|
-
wasConnected = true;
|
|
1222
|
-
setStatus("connected");
|
|
1223
|
-
reconnectDelay = 1000;
|
|
1224
|
-
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1225
|
-
|
|
1226
|
-
// Reset terminal xterm instances (server will send fresh term_list)
|
|
1227
|
-
resetTerminals();
|
|
1228
|
-
|
|
1229
|
-
// Re-send push subscription on reconnect
|
|
1230
|
-
if (window._pushSubscription) {
|
|
1231
|
-
try {
|
|
1232
|
-
ws.send(JSON.stringify({
|
|
1233
|
-
type: "push_subscribe",
|
|
1234
|
-
subscription: window._pushSubscription.toJSON(),
|
|
1235
|
-
}));
|
|
1236
|
-
} catch(e) {}
|
|
1237
|
-
}
|
|
1238
|
-
};
|
|
1239
|
-
|
|
1240
|
-
ws.onclose = function (e) {
|
|
1241
|
-
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
1242
|
-
connectStatusEl.textContent = "Connection lost. Retrying...";
|
|
1243
|
-
setStatus("disconnected");
|
|
1244
|
-
processing = false;
|
|
1245
|
-
setActivity(null);
|
|
1246
|
-
// Delay "connection lost" notification by 5s to suppress brief disconnects
|
|
1247
|
-
if (!disconnectNotifTimer) {
|
|
1248
|
-
disconnectNotifTimer = setTimeout(function () {
|
|
1249
|
-
disconnectNotifTimer = null;
|
|
1250
|
-
disconnectNotifShown = true;
|
|
1251
|
-
if (!document.hasFocus() && "serviceWorker" in navigator) {
|
|
1252
|
-
navigator.serviceWorker.ready.then(function (reg) {
|
|
1253
|
-
reg.showNotification("Claude Relay", {
|
|
1254
|
-
body: "Server connection lost",
|
|
1255
|
-
tag: "claude-disconnect",
|
|
1256
|
-
});
|
|
1257
|
-
}).catch(function () {});
|
|
1258
|
-
}
|
|
1259
|
-
}, 5000);
|
|
1260
|
-
}
|
|
1261
|
-
scheduleReconnect();
|
|
1262
|
-
};
|
|
1263
|
-
|
|
1264
|
-
ws.onerror = function () {
|
|
1265
|
-
connectStatusEl.textContent = "Connection error. Retrying...";
|
|
1266
|
-
};
|
|
1267
|
-
|
|
1268
|
-
ws.onmessage = function (event) {
|
|
1269
|
-
// Backup: if we're receiving messages, we're connected
|
|
1270
|
-
if (!connected) {
|
|
1271
|
-
setStatus("connected");
|
|
1272
|
-
reconnectDelay = 1000;
|
|
1273
|
-
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
blinkIO();
|
|
1277
|
-
var msg;
|
|
1278
|
-
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
1279
|
-
processMessage(msg);
|
|
1280
|
-
};
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
function processMessage(msg) {
|
|
1284
|
-
switch (msg.type) {
|
|
1285
|
-
case "history_meta":
|
|
1286
|
-
historyFrom = msg.from;
|
|
1287
|
-
historyTotal = msg.total;
|
|
1288
|
-
updateHistorySentinel();
|
|
1289
|
-
break;
|
|
1290
|
-
|
|
1291
|
-
case "history_prepend":
|
|
1292
|
-
prependOlderHistory(msg.items, msg.meta);
|
|
1293
|
-
break;
|
|
1294
|
-
|
|
1295
|
-
case "history_done":
|
|
1296
|
-
// Render + finalize any incomplete turn from the replayed history
|
|
1297
|
-
if (currentMsgEl && currentFullText) {
|
|
1298
|
-
var replayContentEl = currentMsgEl.querySelector(".md-content");
|
|
1299
|
-
if (replayContentEl) {
|
|
1300
|
-
replayContentEl.innerHTML = renderMarkdown(currentFullText);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
markAllToolsDone();
|
|
1304
|
-
finalizeAssistantBlock();
|
|
1305
|
-
scrollToBottom();
|
|
1306
|
-
var pendingQuery = getActiveSearchQuery();
|
|
1307
|
-
if (pendingQuery) {
|
|
1308
|
-
requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
|
|
1309
|
-
}
|
|
1310
|
-
// Scroll to tool element if navigating from file edit history
|
|
1311
|
-
var nav = getPendingNavigate();
|
|
1312
|
-
if (nav && (nav.toolId || nav.assistantUuid)) {
|
|
1313
|
-
requestAnimationFrame(function() {
|
|
1314
|
-
// Prefer scrolling to the exact tool element
|
|
1315
|
-
var target = nav.toolId ? messagesEl.querySelector('[data-tool-id="' + nav.toolId + '"]') : null;
|
|
1316
|
-
if (!target && nav.assistantUuid) {
|
|
1317
|
-
target = messagesEl.querySelector('[data-uuid="' + nav.assistantUuid + '"]');
|
|
1318
|
-
}
|
|
1319
|
-
if (target) {
|
|
1320
|
-
// Auto-expand parent tool group if collapsed
|
|
1321
|
-
var parentGroup = target.closest(".tool-group");
|
|
1322
|
-
if (parentGroup) parentGroup.classList.remove("collapsed");
|
|
1323
|
-
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1324
|
-
target.classList.add("message-blink");
|
|
1325
|
-
setTimeout(function() { target.classList.remove("message-blink"); }, 2000);
|
|
1326
|
-
}
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
break;
|
|
1330
|
-
|
|
1331
|
-
case "info":
|
|
1332
|
-
projectName = msg.project || msg.cwd;
|
|
1333
|
-
if (msg.slug) currentSlug = msg.slug;
|
|
1334
|
-
headerTitleEl.textContent = projectName;
|
|
1335
|
-
updatePageTitle();
|
|
1336
|
-
if (msg.version) {
|
|
1337
|
-
var vEl = $("footer-version");
|
|
1338
|
-
if (vEl) vEl.textContent = "v" + msg.version;
|
|
1339
|
-
}
|
|
1340
|
-
if (msg.debug) {
|
|
1341
|
-
var debugWrap = $("debug-menu-wrap");
|
|
1342
|
-
if (debugWrap) debugWrap.classList.remove("hidden");
|
|
1343
|
-
}
|
|
1344
|
-
if (msg.lanHost) window.__lanHost = msg.lanHost;
|
|
1345
|
-
if (msg.dangerouslySkipPermissions) {
|
|
1346
|
-
var spBanner = $("skip-perms-banner");
|
|
1347
|
-
if (spBanner) spBanner.classList.remove("hidden");
|
|
1348
|
-
}
|
|
1349
|
-
updateProjectList(msg);
|
|
1350
|
-
break;
|
|
1351
|
-
|
|
1352
|
-
case "update_available":
|
|
1353
|
-
var updateBanner = $("update-banner");
|
|
1354
|
-
var updateVersion = $("update-version");
|
|
1355
|
-
if (updateBanner && updateVersion && msg.version) {
|
|
1356
|
-
updateVersion.textContent = "v" + msg.version;
|
|
1357
|
-
updateBanner.classList.remove("hidden");
|
|
1358
|
-
// Reset button state (may be stuck on "Updating..." after restart)
|
|
1359
|
-
var updResetBtn = $("update-now");
|
|
1360
|
-
if (updResetBtn) {
|
|
1361
|
-
updResetBtn.textContent = "Update now";
|
|
1362
|
-
updResetBtn.disabled = false;
|
|
1363
|
-
}
|
|
1364
|
-
refreshIcons();
|
|
1365
|
-
}
|
|
1366
|
-
// Show badge on footer update check item
|
|
1367
|
-
var footerUpdateBtn = $("footer-update-check");
|
|
1368
|
-
if (footerUpdateBtn && msg.version) {
|
|
1369
|
-
var labelSpan = footerUpdateBtn.querySelector("span");
|
|
1370
|
-
if (labelSpan) labelSpan.textContent = "Update available";
|
|
1371
|
-
footerUpdateBtn.classList.add("has-badge");
|
|
1372
|
-
var existingBadge = footerUpdateBtn.querySelector(".menu-badge");
|
|
1373
|
-
if (!existingBadge) {
|
|
1374
|
-
var badge = document.createElement("span");
|
|
1375
|
-
badge.className = "menu-badge";
|
|
1376
|
-
badge.textContent = "v" + msg.version;
|
|
1377
|
-
footerUpdateBtn.appendChild(badge);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
break;
|
|
1381
|
-
|
|
1382
|
-
case "update_started":
|
|
1383
|
-
var updNowBtn = $("update-now");
|
|
1384
|
-
if (updNowBtn) {
|
|
1385
|
-
updNowBtn.textContent = "Updating...";
|
|
1386
|
-
updNowBtn.disabled = true;
|
|
1387
|
-
}
|
|
1388
|
-
// Block the entire screen with the connect overlay
|
|
1389
|
-
connectStatusEl.textContent = "Updating" + (msg.version ? " to v" + msg.version : "") + "...";
|
|
1390
|
-
connectOverlay.classList.remove("hidden");
|
|
1391
|
-
startPixelAnim();
|
|
1392
|
-
break;
|
|
1393
|
-
|
|
1394
|
-
case "slash_commands":
|
|
1395
|
-
var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
|
|
1396
|
-
slashCommands = (msg.commands || []).filter(function (name) {
|
|
1397
|
-
return !reserved.has(name);
|
|
1398
|
-
}).map(function (name) {
|
|
1399
|
-
return { name: name, desc: "Skill" };
|
|
1400
|
-
});
|
|
1401
|
-
break;
|
|
1402
|
-
|
|
1403
|
-
case "model_info":
|
|
1404
|
-
updateModelSelector(msg.model, msg.models || []);
|
|
1405
|
-
break;
|
|
1406
|
-
|
|
1407
|
-
case "client_count":
|
|
1408
|
-
var countEl = document.getElementById("client-count");
|
|
1409
|
-
if (countEl) {
|
|
1410
|
-
if (msg.count > 1) {
|
|
1411
|
-
countEl.textContent = msg.count;
|
|
1412
|
-
countEl.dataset.tip = msg.count + " devices connected";
|
|
1413
|
-
countEl.classList.remove("hidden");
|
|
1414
|
-
} else {
|
|
1415
|
-
countEl.classList.add("hidden");
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
break;
|
|
1419
|
-
|
|
1420
|
-
case "toast":
|
|
1421
|
-
showToast(msg.message, msg.level, msg.detail);
|
|
1422
|
-
break;
|
|
1423
|
-
|
|
1424
|
-
case "input_sync":
|
|
1425
|
-
handleInputSync(msg.text);
|
|
1426
|
-
break;
|
|
1427
|
-
|
|
1428
|
-
case "session_list":
|
|
1429
|
-
renderSessionList(msg.sessions || []);
|
|
1430
|
-
break;
|
|
1431
|
-
|
|
1432
|
-
case "search_results":
|
|
1433
|
-
handleSearchResults(msg);
|
|
1434
|
-
break;
|
|
1435
|
-
|
|
1436
|
-
case "cli_session_list":
|
|
1437
|
-
populateCliSessionList(msg.sessions || []);
|
|
1438
|
-
break;
|
|
1439
|
-
|
|
1440
|
-
case "session_switched":
|
|
1441
|
-
// Save draft from outgoing session
|
|
1442
|
-
if (activeSessionId && inputEl.value) {
|
|
1443
|
-
sessionDrafts[activeSessionId] = inputEl.value;
|
|
1444
|
-
} else if (activeSessionId) {
|
|
1445
|
-
delete sessionDrafts[activeSessionId];
|
|
1446
|
-
}
|
|
1447
|
-
activeSessionId = msg.id;
|
|
1448
|
-
cliSessionId = msg.cliSessionId || null;
|
|
1449
|
-
resetClientState();
|
|
1450
|
-
// Restore draft for incoming session
|
|
1451
|
-
var draft = sessionDrafts[activeSessionId] || "";
|
|
1452
|
-
inputEl.value = draft;
|
|
1453
|
-
autoResize();
|
|
1454
|
-
if (!("ontouchstart" in window)) {
|
|
1455
|
-
inputEl.focus();
|
|
1456
|
-
}
|
|
1457
|
-
break;
|
|
1458
|
-
|
|
1459
|
-
case "session_id":
|
|
1460
|
-
cliSessionId = msg.cliSessionId;
|
|
1461
|
-
break;
|
|
1462
|
-
|
|
1463
|
-
case "message_uuid":
|
|
1464
|
-
var uuidTarget;
|
|
1465
|
-
if (msg.messageType === "user") {
|
|
1466
|
-
var allUsers = messagesEl.querySelectorAll(".msg-user:not([data-uuid])");
|
|
1467
|
-
if (allUsers.length > 0) uuidTarget = allUsers[allUsers.length - 1];
|
|
1468
|
-
} else {
|
|
1469
|
-
var allAssistants = messagesEl.querySelectorAll(".msg-assistant:not([data-uuid])");
|
|
1470
|
-
if (allAssistants.length > 0) uuidTarget = allAssistants[allAssistants.length - 1];
|
|
1471
|
-
}
|
|
1472
|
-
if (uuidTarget) {
|
|
1473
|
-
uuidTarget.dataset.uuid = msg.uuid;
|
|
1474
|
-
}
|
|
1475
|
-
messageUuidMap.push({ uuid: msg.uuid, type: msg.messageType });
|
|
1476
|
-
break;
|
|
1477
|
-
|
|
1478
|
-
case "user_message":
|
|
1479
|
-
addUserMessage(msg.text, msg.images || null, msg.pastes || null);
|
|
1480
|
-
break;
|
|
1481
|
-
|
|
1482
|
-
case "status":
|
|
1483
|
-
if (msg.status === "processing") {
|
|
1484
|
-
setStatus("processing");
|
|
1485
|
-
setActivity(randomThinkingVerb() + "...");
|
|
1486
|
-
}
|
|
1487
|
-
break;
|
|
1488
|
-
|
|
1489
|
-
case "compacting":
|
|
1490
|
-
if (msg.active) {
|
|
1491
|
-
setActivity("Compacting conversation...");
|
|
1492
|
-
} else {
|
|
1493
|
-
setActivity(randomThinkingVerb() + "...");
|
|
1494
|
-
}
|
|
1495
|
-
break;
|
|
1496
|
-
|
|
1497
|
-
case "thinking_start":
|
|
1498
|
-
startThinking();
|
|
1499
|
-
break;
|
|
1500
|
-
|
|
1501
|
-
case "thinking_delta":
|
|
1502
|
-
if (typeof msg.text === "string") appendThinking(msg.text);
|
|
1503
|
-
break;
|
|
1504
|
-
|
|
1505
|
-
case "thinking_stop":
|
|
1506
|
-
stopThinking();
|
|
1507
|
-
setActivity(randomThinkingVerb() + "...");
|
|
1508
|
-
break;
|
|
1509
|
-
|
|
1510
|
-
case "delta":
|
|
1511
|
-
if (typeof msg.text !== "string") break;
|
|
1512
|
-
stopThinking();
|
|
1513
|
-
setActivity(null);
|
|
1514
|
-
appendDelta(msg.text);
|
|
1515
|
-
break;
|
|
1516
|
-
|
|
1517
|
-
case "tool_start":
|
|
1518
|
-
stopThinking();
|
|
1519
|
-
markAllToolsDone();
|
|
1520
|
-
if (msg.name === "EnterPlanMode") {
|
|
1521
|
-
renderPlanBanner("enter");
|
|
1522
|
-
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
1523
|
-
} else if (msg.name === "ExitPlanMode") {
|
|
1524
|
-
if (getPlanContent()) {
|
|
1525
|
-
renderPlanCard(getPlanContent());
|
|
1526
|
-
}
|
|
1527
|
-
renderPlanBanner("exit");
|
|
1528
|
-
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
1529
|
-
} else if (getTodoTools()[msg.name]) {
|
|
1530
|
-
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
1531
|
-
} else {
|
|
1532
|
-
createToolItem(msg.id, msg.name);
|
|
1533
|
-
}
|
|
1534
|
-
break;
|
|
1535
|
-
|
|
1536
|
-
case "tool_executing":
|
|
1537
|
-
if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
|
|
1538
|
-
var askTool = getTools()[msg.id];
|
|
1539
|
-
if (askTool) {
|
|
1540
|
-
if (askTool.el) askTool.el.style.display = "none";
|
|
1541
|
-
askTool.done = true;
|
|
1542
|
-
removeToolFromGroup(msg.id);
|
|
1543
|
-
}
|
|
1544
|
-
renderAskUserQuestion(msg.id, msg.input);
|
|
1545
|
-
startUrgentBlink();
|
|
1546
|
-
} else if (msg.name === "Write" && msg.input && isPlanFilePath(msg.input.file_path)) {
|
|
1547
|
-
setPlanContent(msg.input.content || "");
|
|
1548
|
-
updateToolExecuting(msg.id, msg.name, msg.input);
|
|
1549
|
-
} else if (msg.name === "TodoWrite") {
|
|
1550
|
-
handleTodoWrite(msg.input);
|
|
1551
|
-
} else if (msg.name === "TaskCreate") {
|
|
1552
|
-
handleTaskCreate(msg.input);
|
|
1553
|
-
} else if (msg.name === "TaskUpdate") {
|
|
1554
|
-
handleTaskUpdate(msg.input);
|
|
1555
|
-
} else if (getTodoTools()[msg.name]) {
|
|
1556
|
-
// TaskList, TaskGet - silently skip
|
|
1557
|
-
} else {
|
|
1558
|
-
var t = getTools()[msg.id];
|
|
1559
|
-
if (t && t.hidden) break;
|
|
1560
|
-
updateToolExecuting(msg.id, msg.name, msg.input);
|
|
1561
|
-
}
|
|
1562
|
-
break;
|
|
1563
|
-
|
|
1564
|
-
case "tool_result": {
|
|
1565
|
-
var tr = getTools()[msg.id];
|
|
1566
|
-
if (tr && tr.hidden) break; // skip hidden plan tools
|
|
1567
|
-
// Always call updateToolResult for Edit (to show diff from input), or when content exists
|
|
1568
|
-
if (msg.content != null || (tr && tr.name === "Edit" && tr.input && tr.input.old_string)) {
|
|
1569
|
-
updateToolResult(msg.id, msg.content || "", msg.is_error || false);
|
|
1570
|
-
}
|
|
1571
|
-
// Refresh file browser if an Edit/Write tool modified the open file
|
|
1572
|
-
if (!msg.is_error && tr && (tr.name === "Edit" || tr.name === "Write") && tr.input && tr.input.file_path) {
|
|
1573
|
-
refreshIfOpen(tr.input.file_path);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
break;
|
|
1577
|
-
|
|
1578
|
-
case "ask_user_answered":
|
|
1579
|
-
markAskUserAnswered(msg.toolId);
|
|
1580
|
-
stopUrgentBlink();
|
|
1581
|
-
break;
|
|
1582
|
-
|
|
1583
|
-
case "permission_request":
|
|
1584
|
-
renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
|
|
1585
|
-
startUrgentBlink();
|
|
1586
|
-
break;
|
|
1587
|
-
|
|
1588
|
-
case "permission_cancel":
|
|
1589
|
-
markPermissionCancelled(msg.requestId);
|
|
1590
|
-
stopUrgentBlink();
|
|
1591
|
-
break;
|
|
1592
|
-
|
|
1593
|
-
case "permission_resolved":
|
|
1594
|
-
markPermissionResolved(msg.requestId, msg.decision);
|
|
1595
|
-
stopUrgentBlink();
|
|
1596
|
-
break;
|
|
1597
|
-
|
|
1598
|
-
case "permission_request_pending":
|
|
1599
|
-
renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
|
|
1600
|
-
startUrgentBlink();
|
|
1601
|
-
break;
|
|
1602
|
-
|
|
1603
|
-
case "slash_command_result":
|
|
1604
|
-
finalizeAssistantBlock();
|
|
1605
|
-
var cmdBlock = document.createElement("div");
|
|
1606
|
-
cmdBlock.className = "assistant-block";
|
|
1607
|
-
cmdBlock.style.maxWidth = "var(--content-width)";
|
|
1608
|
-
cmdBlock.style.margin = "12px auto";
|
|
1609
|
-
cmdBlock.style.padding = "0 20px";
|
|
1610
|
-
var pre = document.createElement("pre");
|
|
1611
|
-
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";
|
|
1612
|
-
pre.textContent = msg.text;
|
|
1613
|
-
cmdBlock.appendChild(pre);
|
|
1614
|
-
addToMessages(cmdBlock);
|
|
1615
|
-
scrollToBottom();
|
|
1616
|
-
break;
|
|
1617
|
-
|
|
1618
|
-
case "subagent_activity":
|
|
1619
|
-
updateSubagentActivity(msg.parentToolId, msg.text);
|
|
1620
|
-
break;
|
|
1621
|
-
|
|
1622
|
-
case "subagent_tool":
|
|
1623
|
-
addSubagentToolEntry(msg.parentToolId, msg.toolName, msg.toolId, msg.text);
|
|
1624
|
-
break;
|
|
1625
|
-
|
|
1626
|
-
case "subagent_done":
|
|
1627
|
-
markSubagentDone(msg.parentToolId);
|
|
1628
|
-
break;
|
|
1629
|
-
|
|
1630
|
-
case "result":
|
|
1631
|
-
setActivity(null);
|
|
1632
|
-
stopThinking();
|
|
1633
|
-
markAllToolsDone();
|
|
1634
|
-
closeToolGroup();
|
|
1635
|
-
finalizeAssistantBlock();
|
|
1636
|
-
addTurnMeta(msg.cost, msg.duration);
|
|
1637
|
-
accumulateUsage(msg.cost, msg.usage);
|
|
1638
|
-
accumulateContext(msg.cost, msg.usage, msg.modelUsage);
|
|
1639
|
-
break;
|
|
1640
|
-
|
|
1641
|
-
case "done":
|
|
1642
|
-
setActivity(null);
|
|
1643
|
-
stopThinking();
|
|
1644
|
-
markAllToolsDone();
|
|
1645
|
-
closeToolGroup();
|
|
1646
|
-
finalizeAssistantBlock();
|
|
1647
|
-
processing = false;
|
|
1648
|
-
setStatus("connected");
|
|
1649
|
-
enableMainInput();
|
|
1650
|
-
resetToolState();
|
|
1651
|
-
if (document.hidden) {
|
|
1652
|
-
if (isNotifAlertEnabled() && !window._pushSubscription) showDoneNotification();
|
|
1653
|
-
if (isNotifSoundEnabled()) playDoneSound();
|
|
1654
|
-
}
|
|
1655
|
-
break;
|
|
1656
|
-
|
|
1657
|
-
case "stderr":
|
|
1658
|
-
addSystemMessage(msg.text, false);
|
|
1659
|
-
break;
|
|
1660
|
-
|
|
1661
|
-
case "info":
|
|
1662
|
-
addSystemMessage(msg.text, false);
|
|
1663
|
-
break;
|
|
1664
|
-
|
|
1665
|
-
case "error":
|
|
1666
|
-
setActivity(null);
|
|
1667
|
-
addSystemMessage(msg.text, true);
|
|
1668
|
-
updateFavicon(getComputedVar("--error"));
|
|
1669
|
-
break;
|
|
1670
|
-
|
|
1671
|
-
case "rewind_preview_result":
|
|
1672
|
-
showRewindModal(msg);
|
|
1673
|
-
break;
|
|
1674
|
-
|
|
1675
|
-
case "rewind_complete":
|
|
1676
|
-
setRewindMode(false);
|
|
1677
|
-
var rewindText = "Rewound to earlier point. Files have been restored.";
|
|
1678
|
-
if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
|
|
1679
|
-
else if (msg.mode === "files") rewindText = "Files restored to earlier point.";
|
|
1680
|
-
addSystemMessage(rewindText, false);
|
|
1681
|
-
break;
|
|
1682
|
-
|
|
1683
|
-
case "rewind_error":
|
|
1684
|
-
clearPendingRewindUuid();
|
|
1685
|
-
addSystemMessage(msg.text || "Rewind failed.", true);
|
|
1686
|
-
break;
|
|
1687
|
-
|
|
1688
|
-
case "fs_list_result":
|
|
1689
|
-
handleFsList(msg);
|
|
1690
|
-
break;
|
|
1691
|
-
|
|
1692
|
-
case "fs_read_result":
|
|
1693
|
-
handleFsRead(msg);
|
|
1694
|
-
break;
|
|
1695
|
-
|
|
1696
|
-
case "fs_file_changed":
|
|
1697
|
-
handleFileChanged(msg);
|
|
1698
|
-
break;
|
|
1699
|
-
|
|
1700
|
-
case "fs_dir_changed":
|
|
1701
|
-
handleDirChanged(msg);
|
|
1702
|
-
break;
|
|
1703
|
-
|
|
1704
|
-
case "fs_file_history_result":
|
|
1705
|
-
handleFileHistory(msg);
|
|
1706
|
-
break;
|
|
1707
|
-
|
|
1708
|
-
case "fs_git_diff_result":
|
|
1709
|
-
handleGitDiff(msg);
|
|
1710
|
-
break;
|
|
1711
|
-
|
|
1712
|
-
case "fs_file_at_result":
|
|
1713
|
-
handleFileAt(msg);
|
|
1714
|
-
break;
|
|
1715
|
-
|
|
1716
|
-
case "term_list":
|
|
1717
|
-
handleTermList(msg);
|
|
1718
|
-
break;
|
|
1719
|
-
|
|
1720
|
-
case "term_created":
|
|
1721
|
-
handleTermCreated(msg);
|
|
1722
|
-
break;
|
|
1723
|
-
|
|
1724
|
-
case "term_output":
|
|
1725
|
-
handleTermOutput(msg);
|
|
1726
|
-
break;
|
|
1727
|
-
|
|
1728
|
-
case "term_exited":
|
|
1729
|
-
handleTermExited(msg);
|
|
1730
|
-
break;
|
|
1731
|
-
|
|
1732
|
-
case "term_closed":
|
|
1733
|
-
handleTermClosed(msg);
|
|
1734
|
-
break;
|
|
1735
|
-
|
|
1736
|
-
case "process_stats":
|
|
1737
|
-
updateStatusPanel(msg);
|
|
1738
|
-
break;
|
|
1739
|
-
|
|
1740
|
-
case "browse_dir_result":
|
|
1741
|
-
handleBrowseDirResult(msg);
|
|
1742
|
-
break;
|
|
1743
|
-
|
|
1744
|
-
case "add_project_result":
|
|
1745
|
-
handleAddProjectResult(msg);
|
|
1746
|
-
break;
|
|
1747
|
-
|
|
1748
|
-
case "remove_project_result":
|
|
1749
|
-
handleRemoveProjectResult(msg);
|
|
1750
|
-
break;
|
|
1751
|
-
|
|
1752
|
-
case "projects_updated":
|
|
1753
|
-
updateProjectList(msg);
|
|
1754
|
-
break;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
// --- Progressive history loading ---
|
|
1759
|
-
function updateHistorySentinel() {
|
|
1760
|
-
var existing = messagesEl.querySelector(".history-sentinel");
|
|
1761
|
-
if (historyFrom > 0) {
|
|
1762
|
-
if (!existing) {
|
|
1763
|
-
var sentinel = document.createElement("div");
|
|
1764
|
-
sentinel.className = "history-sentinel";
|
|
1765
|
-
sentinel.innerHTML = '<button class="load-more-btn">Load earlier messages</button>';
|
|
1766
|
-
sentinel.querySelector(".load-more-btn").addEventListener("click", function () {
|
|
1767
|
-
requestMoreHistory();
|
|
1768
|
-
});
|
|
1769
|
-
messagesEl.insertBefore(sentinel, messagesEl.firstChild);
|
|
1770
|
-
|
|
1771
|
-
// Auto-load when sentinel scrolls into view
|
|
1772
|
-
if (historySentinelObserver) historySentinelObserver.disconnect();
|
|
1773
|
-
historySentinelObserver = new IntersectionObserver(function (entries) {
|
|
1774
|
-
if (entries[0].isIntersecting && !loadingMore && historyFrom > 0) {
|
|
1775
|
-
requestMoreHistory();
|
|
1776
|
-
}
|
|
1777
|
-
}, { root: messagesEl, rootMargin: "200px 0px 0px 0px" });
|
|
1778
|
-
historySentinelObserver.observe(sentinel);
|
|
1779
|
-
}
|
|
1780
|
-
} else {
|
|
1781
|
-
if (existing) existing.remove();
|
|
1782
|
-
if (historySentinelObserver) { historySentinelObserver.disconnect(); historySentinelObserver = null; }
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
function requestMoreHistory() {
|
|
1787
|
-
if (loadingMore || historyFrom <= 0 || !ws || !connected) return;
|
|
1788
|
-
loadingMore = true;
|
|
1789
|
-
var btn = messagesEl.querySelector(".load-more-btn");
|
|
1790
|
-
if (btn) btn.classList.add("loading");
|
|
1791
|
-
ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom }));
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
function prependOlderHistory(items, meta) {
|
|
1795
|
-
// Save current rendering state
|
|
1796
|
-
var savedMsgEl = currentMsgEl;
|
|
1797
|
-
var savedActivity = activityEl;
|
|
1798
|
-
var savedFullText = currentFullText;
|
|
1799
|
-
var savedTurnCounter = turnCounter;
|
|
1800
|
-
var savedToolsState = saveToolState();
|
|
1801
|
-
|
|
1802
|
-
// Reset to initial values for clean rendering
|
|
1803
|
-
currentMsgEl = null;
|
|
1804
|
-
activityEl = null;
|
|
1805
|
-
currentFullText = "";
|
|
1806
|
-
turnCounter = 0;
|
|
1807
|
-
resetToolState();
|
|
1808
|
-
|
|
1809
|
-
// Set prepend anchor to insert before existing content
|
|
1810
|
-
// Skip the sentinel itself when setting anchor
|
|
1811
|
-
var firstReal = messagesEl.querySelector(".history-sentinel");
|
|
1812
|
-
prependAnchor = firstReal ? firstReal.nextSibling : messagesEl.firstChild;
|
|
1813
|
-
|
|
1814
|
-
// Remember the first existing content element and its position
|
|
1815
|
-
var anchorEl = prependAnchor;
|
|
1816
|
-
var anchorOffset = anchorEl ? anchorEl.getBoundingClientRect().top : 0;
|
|
1817
|
-
|
|
1818
|
-
// Process each item through the rendering pipeline
|
|
1819
|
-
for (var i = 0; i < items.length; i++) {
|
|
1820
|
-
processMessage(items[i]);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
// Finalize any open assistant block from the batch
|
|
1824
|
-
finalizeAssistantBlock();
|
|
1825
|
-
|
|
1826
|
-
// Clear prepend mode
|
|
1827
|
-
prependAnchor = null;
|
|
1828
|
-
|
|
1829
|
-
// Restore saved state
|
|
1830
|
-
currentMsgEl = savedMsgEl;
|
|
1831
|
-
activityEl = savedActivity;
|
|
1832
|
-
currentFullText = savedFullText;
|
|
1833
|
-
turnCounter = savedTurnCounter;
|
|
1834
|
-
restoreToolState(savedToolsState);
|
|
1835
|
-
|
|
1836
|
-
// Fix scroll: restore anchor element to same visual position
|
|
1837
|
-
if (anchorEl) {
|
|
1838
|
-
var newTop = anchorEl.getBoundingClientRect().top;
|
|
1839
|
-
messagesEl.scrollTop += (newTop - anchorOffset);
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
// Update state
|
|
1843
|
-
historyFrom = meta.from;
|
|
1844
|
-
loadingMore = false;
|
|
1845
|
-
|
|
1846
|
-
// Renumber data-turn attributes in DOM order
|
|
1847
|
-
var turnEls = messagesEl.querySelectorAll("[data-turn]");
|
|
1848
|
-
for (var t = 0; t < turnEls.length; t++) {
|
|
1849
|
-
turnEls[t].dataset.turn = t + 1;
|
|
1850
|
-
}
|
|
1851
|
-
turnCounter = turnEls.length;
|
|
1852
|
-
|
|
1853
|
-
// Update sentinel
|
|
1854
|
-
if (meta.hasMore) {
|
|
1855
|
-
var btn = messagesEl.querySelector(".load-more-btn");
|
|
1856
|
-
if (btn) btn.classList.remove("loading");
|
|
1857
|
-
} else {
|
|
1858
|
-
updateHistorySentinel();
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
function scheduleReconnect() {
|
|
1863
|
-
if (reconnectTimer) return;
|
|
1864
|
-
reconnectTimer = setTimeout(function () {
|
|
1865
|
-
reconnectTimer = null;
|
|
1866
|
-
connect();
|
|
1867
|
-
}, reconnectDelay);
|
|
1868
|
-
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
// --- Input module (sendMessage, autoResize, paste/image, slash menu, input handlers) ---
|
|
1872
|
-
initInput({
|
|
1873
|
-
get ws() { return ws; },
|
|
1874
|
-
get connected() { return connected; },
|
|
1875
|
-
get processing() { return processing; },
|
|
1876
|
-
inputEl: inputEl,
|
|
1877
|
-
sendBtn: sendBtn,
|
|
1878
|
-
slashMenu: slashMenu,
|
|
1879
|
-
messagesEl: messagesEl,
|
|
1880
|
-
imagePreviewBar: imagePreviewBar,
|
|
1881
|
-
slashCommands: function() { return slashCommands; },
|
|
1882
|
-
messageUuidMap: function() { return messageUuidMap; },
|
|
1883
|
-
addUserMessage: addUserMessage,
|
|
1884
|
-
addSystemMessage: addSystemMessage,
|
|
1885
|
-
toggleUsagePanel: toggleUsagePanel,
|
|
1886
|
-
toggleStatusPanel: toggleStatusPanel,
|
|
1887
|
-
toggleContextPanel: toggleContextPanel,
|
|
1888
|
-
resetContextData: resetContextData,
|
|
1889
|
-
showImageModal: showImageModal,
|
|
1890
|
-
});
|
|
1891
|
-
|
|
1892
|
-
// --- Notifications module (viewport, banners, notifications, debug, service worker) ---
|
|
1893
|
-
initNotifications({
|
|
1894
|
-
$: $,
|
|
1895
|
-
get ws() { return ws; },
|
|
1896
|
-
get connected() { return connected; },
|
|
1897
|
-
messagesEl: messagesEl,
|
|
1898
|
-
sessionListEl: sessionListEl,
|
|
1899
|
-
scrollToBottom: scrollToBottom,
|
|
1900
|
-
basePath: basePath,
|
|
1901
|
-
toggleUsagePanel: toggleUsagePanel,
|
|
1902
|
-
toggleStatusPanel: toggleStatusPanel,
|
|
1903
|
-
});
|
|
1904
|
-
|
|
1905
|
-
// --- QR code ---
|
|
1906
|
-
initQrCode();
|
|
1907
|
-
|
|
1908
|
-
// --- File browser ---
|
|
1909
|
-
initFileBrowser({
|
|
1910
|
-
get ws() { return ws; },
|
|
1911
|
-
get connected() { return connected; },
|
|
1912
|
-
get activeSessionId() { return activeSessionId; },
|
|
1913
|
-
messagesEl: messagesEl,
|
|
1914
|
-
fileTreeEl: $("file-tree"),
|
|
1915
|
-
fileViewerEl: $("file-viewer"),
|
|
1916
|
-
});
|
|
1917
|
-
|
|
1918
|
-
// --- Terminal ---
|
|
1919
|
-
initTerminal({
|
|
1920
|
-
get ws() { return ws; },
|
|
1921
|
-
get connected() { return connected; },
|
|
1922
|
-
terminalContainerEl: $("terminal-container"),
|
|
1923
|
-
terminalBodyEl: $("terminal-body"),
|
|
1924
|
-
fileViewerEl: $("file-viewer"),
|
|
1925
|
-
});
|
|
1926
|
-
|
|
1927
|
-
// --- Remove project ---
|
|
1928
|
-
function confirmRemoveProject(slug, name) {
|
|
1929
|
-
showConfirm("Remove project \"" + name + "\"?", function () {
|
|
1930
|
-
if (ws && ws.readyState === 1) {
|
|
1931
|
-
ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
|
|
1932
|
-
}
|
|
1933
|
-
});
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
function handleRemoveProjectResult(msg) {
|
|
1937
|
-
if (msg.ok) {
|
|
1938
|
-
showToast("Project removed", "success");
|
|
1939
|
-
// If we removed the current project, navigate to first available
|
|
1940
|
-
if (msg.slug === currentSlug) {
|
|
1941
|
-
window.location.href = "/";
|
|
1942
|
-
}
|
|
1943
|
-
} else {
|
|
1944
|
-
showToast(msg.error || "Failed to remove project", "error");
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
// --- Add project modal ---
|
|
1949
|
-
var addProjectModal = document.getElementById("add-project-modal");
|
|
1950
|
-
var addProjectInput = document.getElementById("add-project-input");
|
|
1951
|
-
var addProjectSuggestions = document.getElementById("add-project-suggestions");
|
|
1952
|
-
var addProjectError = document.getElementById("add-project-error");
|
|
1953
|
-
var addProjectOk = document.getElementById("add-project-ok");
|
|
1954
|
-
var addProjectCancel = document.getElementById("add-project-cancel");
|
|
1955
|
-
var addProjectDebounce = null;
|
|
1956
|
-
var addProjectActiveIdx = -1;
|
|
1957
|
-
|
|
1958
|
-
function openAddProjectModal() {
|
|
1959
|
-
addProjectModal.classList.remove("hidden");
|
|
1960
|
-
addProjectInput.value = "/";
|
|
1961
|
-
addProjectError.classList.add("hidden");
|
|
1962
|
-
addProjectError.textContent = "";
|
|
1963
|
-
addProjectSuggestions.classList.add("hidden");
|
|
1964
|
-
addProjectSuggestions.innerHTML = "";
|
|
1965
|
-
addProjectActiveIdx = -1;
|
|
1966
|
-
addProjectOk.disabled = false;
|
|
1967
|
-
setTimeout(function () {
|
|
1968
|
-
addProjectInput.focus();
|
|
1969
|
-
addProjectInput.setSelectionRange(1, 1);
|
|
1970
|
-
}, 50);
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
function closeAddProjectModal() {
|
|
1974
|
-
addProjectModal.classList.add("hidden");
|
|
1975
|
-
addProjectInput.value = "";
|
|
1976
|
-
addProjectSuggestions.classList.add("hidden");
|
|
1977
|
-
addProjectSuggestions.innerHTML = "";
|
|
1978
|
-
addProjectError.classList.add("hidden");
|
|
1979
|
-
addProjectActiveIdx = -1;
|
|
1980
|
-
if (addProjectDebounce) { clearTimeout(addProjectDebounce); addProjectDebounce = null; }
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
function requestBrowseDir(val) {
|
|
1984
|
-
if (!ws || ws.readyState !== 1) return;
|
|
1985
|
-
ws.send(JSON.stringify({ type: "browse_dir", path: val }));
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
function handleBrowseDirResult(msg) {
|
|
1989
|
-
addProjectSuggestions.innerHTML = "";
|
|
1990
|
-
addProjectActiveIdx = -1;
|
|
1991
|
-
if (msg.error) {
|
|
1992
|
-
addProjectSuggestions.classList.add("hidden");
|
|
1993
|
-
return;
|
|
1994
|
-
}
|
|
1995
|
-
var entries = msg.entries || [];
|
|
1996
|
-
if (entries.length === 0) {
|
|
1997
|
-
addProjectSuggestions.classList.add("hidden");
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
for (var si = 0; si < entries.length; si++) {
|
|
2001
|
-
var entry = entries[si];
|
|
2002
|
-
var item = document.createElement("div");
|
|
2003
|
-
item.className = "add-project-suggestion-item";
|
|
2004
|
-
item.dataset.path = entry.path;
|
|
2005
|
-
item.innerHTML = '<i data-lucide="folder"></i><span class="add-project-suggestion-name">' +
|
|
2006
|
-
escapeHtml(entry.name) + '</span>';
|
|
2007
|
-
item.addEventListener("click", function (e) {
|
|
2008
|
-
var p = this.dataset.path + "/";
|
|
2009
|
-
addProjectInput.value = p;
|
|
2010
|
-
addProjectInput.focus();
|
|
2011
|
-
addProjectError.classList.add("hidden");
|
|
2012
|
-
requestBrowseDir(p);
|
|
2013
|
-
});
|
|
2014
|
-
addProjectSuggestions.appendChild(item);
|
|
2015
|
-
}
|
|
2016
|
-
addProjectSuggestions.classList.remove("hidden");
|
|
2017
|
-
refreshIcons();
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
function handleAddProjectResult(msg) {
|
|
2021
|
-
if (msg.ok) {
|
|
2022
|
-
closeAddProjectModal();
|
|
2023
|
-
if (msg.existing) {
|
|
2024
|
-
showToast("Project already registered", "info");
|
|
2025
|
-
} else {
|
|
2026
|
-
showToast("Project added", "success");
|
|
2027
|
-
// Navigate to the new project
|
|
2028
|
-
if (msg.slug) {
|
|
2029
|
-
window.location.href = "/p/" + msg.slug + "/";
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
} else {
|
|
2033
|
-
addProjectError.textContent = msg.error || "Failed to add project";
|
|
2034
|
-
addProjectError.classList.remove("hidden");
|
|
2035
|
-
addProjectOk.disabled = false;
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
function setActiveIdx(idx) {
|
|
2040
|
-
var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
|
|
2041
|
-
addProjectActiveIdx = idx;
|
|
2042
|
-
for (var ai = 0; ai < items.length; ai++) {
|
|
2043
|
-
if (ai === idx) {
|
|
2044
|
-
items[ai].classList.add("active");
|
|
2045
|
-
items[ai].scrollIntoView({ block: "nearest" });
|
|
2046
|
-
} else {
|
|
2047
|
-
items[ai].classList.remove("active");
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
addProjectInput.addEventListener("focus", function () {
|
|
2053
|
-
var val = addProjectInput.value;
|
|
2054
|
-
if (val && addProjectSuggestions.children.length === 0) {
|
|
2055
|
-
requestBrowseDir(val);
|
|
2056
|
-
} else if (addProjectSuggestions.children.length > 0) {
|
|
2057
|
-
addProjectSuggestions.classList.remove("hidden");
|
|
2058
|
-
}
|
|
2059
|
-
});
|
|
2060
|
-
|
|
2061
|
-
addProjectModal.querySelector(".confirm-dialog").addEventListener("click", function (e) {
|
|
2062
|
-
if (e.target === addProjectInput || addProjectInput.contains(e.target)) return;
|
|
2063
|
-
if (e.target === addProjectSuggestions || addProjectSuggestions.contains(e.target)) return;
|
|
2064
|
-
addProjectSuggestions.classList.add("hidden");
|
|
2065
|
-
addProjectActiveIdx = -1;
|
|
2066
|
-
});
|
|
2067
|
-
|
|
2068
|
-
addProjectInput.addEventListener("input", function () {
|
|
2069
|
-
var val = addProjectInput.value;
|
|
2070
|
-
addProjectError.classList.add("hidden");
|
|
2071
|
-
if (addProjectDebounce) clearTimeout(addProjectDebounce);
|
|
2072
|
-
addProjectDebounce = setTimeout(function () {
|
|
2073
|
-
requestBrowseDir(val);
|
|
2074
|
-
}, 200);
|
|
2075
|
-
});
|
|
2076
|
-
|
|
2077
|
-
addProjectInput.addEventListener("keydown", function (e) {
|
|
2078
|
-
var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
|
|
2079
|
-
|
|
2080
|
-
if (e.key === "ArrowDown") {
|
|
2081
|
-
e.preventDefault();
|
|
2082
|
-
if (items.length > 0) {
|
|
2083
|
-
var next = addProjectActiveIdx < items.length - 1 ? addProjectActiveIdx + 1 : 0;
|
|
2084
|
-
setActiveIdx(next);
|
|
2085
|
-
}
|
|
2086
|
-
return;
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
if (e.key === "ArrowUp") {
|
|
2090
|
-
e.preventDefault();
|
|
2091
|
-
if (items.length > 0) {
|
|
2092
|
-
var prev = addProjectActiveIdx > 0 ? addProjectActiveIdx - 1 : items.length - 1;
|
|
2093
|
-
setActiveIdx(prev);
|
|
2094
|
-
}
|
|
2095
|
-
return;
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
if (e.key === "Tab") {
|
|
2099
|
-
e.preventDefault();
|
|
2100
|
-
var target = addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length
|
|
2101
|
-
? items[addProjectActiveIdx]
|
|
2102
|
-
: items.length > 0 ? items[0] : null;
|
|
2103
|
-
if (target) {
|
|
2104
|
-
var p = target.dataset.path + "/";
|
|
2105
|
-
addProjectInput.value = p;
|
|
2106
|
-
addProjectError.classList.add("hidden");
|
|
2107
|
-
requestBrowseDir(p);
|
|
2108
|
-
}
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
if (e.key === "Enter") {
|
|
2113
|
-
e.preventDefault();
|
|
2114
|
-
// If a suggestion is highlighted, pick it first
|
|
2115
|
-
if (addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length) {
|
|
2116
|
-
var picked = items[addProjectActiveIdx].dataset.path + "/";
|
|
2117
|
-
addProjectInput.value = picked;
|
|
2118
|
-
addProjectError.classList.add("hidden");
|
|
2119
|
-
requestBrowseDir(picked);
|
|
2120
|
-
return;
|
|
2121
|
-
}
|
|
2122
|
-
// Otherwise submit
|
|
2123
|
-
submitAddProject();
|
|
2124
|
-
return;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
if (e.key === "Escape") {
|
|
2128
|
-
e.preventDefault();
|
|
2129
|
-
closeAddProjectModal();
|
|
2130
|
-
return;
|
|
2131
|
-
}
|
|
2132
|
-
});
|
|
2133
|
-
|
|
2134
|
-
function submitAddProject() {
|
|
2135
|
-
var val = addProjectInput.value.replace(/\/+$/, "");
|
|
2136
|
-
if (!val) return;
|
|
2137
|
-
addProjectOk.disabled = true;
|
|
2138
|
-
addProjectError.classList.add("hidden");
|
|
2139
|
-
if (ws && ws.readyState === 1) {
|
|
2140
|
-
ws.send(JSON.stringify({ type: "add_project", path: val }));
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
addProjectOk.addEventListener("click", function () { submitAddProject(); });
|
|
2145
|
-
addProjectCancel.addEventListener("click", function () { closeAddProjectModal(); });
|
|
2146
|
-
|
|
2147
|
-
// Close on backdrop click
|
|
2148
|
-
addProjectModal.querySelector(".confirm-backdrop").addEventListener("click", function () {
|
|
2149
|
-
closeAddProjectModal();
|
|
2150
|
-
});
|
|
2151
|
-
|
|
2152
|
-
// --- Init ---
|
|
2153
|
-
lucide.createIcons();
|
|
2154
|
-
startVerbCycle();
|
|
2155
|
-
startPixelAnim();
|
|
2156
|
-
connect();
|
|
2157
|
-
inputEl.focus();
|