claude-remote 0.5.2 → 0.6.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/lib/cli.js +1 -0
- package/lib/http-server.js +5 -0
- package/lib/state.js +1 -0
- package/package.json +3 -2
- package/server.js +6 -0
- package/web/index.html +346 -0
- package/web/main.js +68 -0
- package/web/modules/chat-cache.js +118 -0
- package/web/modules/confirm.js +25 -0
- package/web/modules/constants.js +59 -0
- package/web/modules/debug.js +81 -0
- package/web/modules/dir-picker.js +128 -0
- package/web/modules/hub.js +619 -0
- package/web/modules/image-upload.js +290 -0
- package/web/modules/input.js +279 -0
- package/web/modules/interactions.js +304 -0
- package/web/modules/keyboard.js +78 -0
- package/web/modules/model-picker.js +47 -0
- package/web/modules/permissions.js +94 -0
- package/web/modules/renderer.js +863 -0
- package/web/modules/sessions.js +108 -0
- package/web/modules/settings.js +74 -0
- package/web/modules/state.js +59 -0
- package/web/modules/toast.js +68 -0
- package/web/modules/todo.js +292 -0
- package/web/modules/utils.js +102 -0
- package/web/modules/waiting.js +93 -0
- package/web/modules/websocket.js +483 -0
- package/web/styles.css +1722 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Renderer — message rendering engine
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { JUNK_PATTERNS, HIDDEN_STEP_TOOLS, CHAT_CACHE_MAX_SESSION_BYTES } from './constants.js';
|
|
5
|
+
import { $, esc, trunc, stripImageTags, shortenPath, formatModel, formatTokens } from './utils.js';
|
|
6
|
+
import { S, serverAddr, serverCacheAddr } from './state.js';
|
|
7
|
+
import { debugLog } from './debug.js';
|
|
8
|
+
import { showToast } from './toast.js';
|
|
9
|
+
import {
|
|
10
|
+
buildCacheKey, estimateCacheBytes,
|
|
11
|
+
chatCacheRead, chatCacheWrite, chatCacheDelete, pruneChatCache,
|
|
12
|
+
} from './chat-cache.js';
|
|
13
|
+
import { scrollEnd, updateScrollBtn, setWaiting, switchToWorking, removeWorkingIndicator } from './waiting.js';
|
|
14
|
+
import { setSendButtonMode, updateSendBtn } from './input.js';
|
|
15
|
+
import { resetTodoState, restoreTodoSnapshot, getTodoSnapshot, renderTodoPanel, isTodoTool, handleTodoToolUse, handleTodoToolResult } from './todo.js';
|
|
16
|
+
import { clearPendingImage } from './image-upload.js';
|
|
17
|
+
import { resetInteractionState, registerInteractiveToolUse, resolveInteractiveToolResult, normalizePlanContent, presentNextPendingInteraction } from './interactions.js';
|
|
18
|
+
import { setConnBanner } from './websocket.js';
|
|
19
|
+
import { hideCmdOverlay } from './input.js';
|
|
20
|
+
|
|
21
|
+
const $msgs = $('messages');
|
|
22
|
+
const $chat = $('chat-area');
|
|
23
|
+
const $input = $('input');
|
|
24
|
+
const INPUT_PLACEHOLDER_DEFAULT = 'Reply...';
|
|
25
|
+
|
|
26
|
+
// ---- Tool Icons ----
|
|
27
|
+
const ICONS = {
|
|
28
|
+
Bash: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>',
|
|
29
|
+
Read: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>',
|
|
30
|
+
Edit: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
|
|
31
|
+
Write: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
|
|
32
|
+
Glob: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>',
|
|
33
|
+
Grep: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>',
|
|
34
|
+
WebFetch: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
|
|
35
|
+
WebSearch: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
|
|
36
|
+
Task: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
|
37
|
+
};
|
|
38
|
+
function iconFor(name) { return ICONS[name] || ICONS.Bash; }
|
|
39
|
+
|
|
40
|
+
function toolDesc(name, input) {
|
|
41
|
+
if (!input) return '';
|
|
42
|
+
switch (name) {
|
|
43
|
+
case 'Bash': return input.description || input.command || '';
|
|
44
|
+
case 'Read': return input.file_path || '';
|
|
45
|
+
case 'Write': return input.file_path || '';
|
|
46
|
+
case 'Edit': return input.file_path || '';
|
|
47
|
+
case 'Glob': return input.pattern || '';
|
|
48
|
+
case 'Grep': return input.pattern || '';
|
|
49
|
+
case 'WebFetch': return input.url || '';
|
|
50
|
+
case 'WebSearch': return input.query || '';
|
|
51
|
+
case 'Task': return input.description || input.prompt || '';
|
|
52
|
+
default: return JSON.stringify(input).substring(0, 80);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toolInputFull(name, input) {
|
|
57
|
+
if (!input) return '';
|
|
58
|
+
switch (name) {
|
|
59
|
+
case 'Bash': return input.command || '';
|
|
60
|
+
case 'Read': return input.file_path || '';
|
|
61
|
+
case 'Write': return `${input.file_path || ''}\n${'─'.repeat(30)}\n${trunc(input.content, 800)}`;
|
|
62
|
+
case 'Edit': {
|
|
63
|
+
let s = (input.file_path || '') + '\n';
|
|
64
|
+
if (input.old_string !== undefined) s += `- ${trunc(input.old_string, 300)}\n+ ${trunc(input.new_string, 300)}`;
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
case 'Glob': return `pattern: ${input.pattern}${input.path ? '\npath: ' + input.path : ''}`;
|
|
68
|
+
case 'Grep': return `pattern: ${input.pattern}${input.path ? '\npath: ' + input.path : ''}`;
|
|
69
|
+
case 'WebFetch': return `${input.url}\n${input.prompt || ''}`;
|
|
70
|
+
case 'WebSearch': return input.query || '';
|
|
71
|
+
case 'Task': return `[${input.subagent_type || 'agent'}] ${input.description || ''}\n${trunc(input.prompt, 300)}`;
|
|
72
|
+
default: return JSON.stringify(input, null, 2);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- Markdown ----
|
|
77
|
+
export function renderMd(text) {
|
|
78
|
+
try {
|
|
79
|
+
if (typeof marked === 'undefined') return esc(text);
|
|
80
|
+
const html = marked.parse(text, { breaks: true, gfm: true });
|
|
81
|
+
const d = document.createElement('div');
|
|
82
|
+
d.innerHTML = html;
|
|
83
|
+
if (typeof hljs !== 'undefined') {
|
|
84
|
+
d.querySelectorAll('pre code').forEach(el => { try { hljs.highlightElement(el); } catch {} });
|
|
85
|
+
}
|
|
86
|
+
return d.innerHTML;
|
|
87
|
+
} catch { return esc(text); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- Diff Rendering ----
|
|
91
|
+
function buildDiffHtml(oldStr, newStr, filePath, startLine) {
|
|
92
|
+
const lineOffset = (startLine || 1) - 1;
|
|
93
|
+
const oldLines = (oldStr || '').split('\n');
|
|
94
|
+
const newLines = (newStr || '').split('\n');
|
|
95
|
+
const m = oldLines.length, n = newLines.length;
|
|
96
|
+
if (m * n > 500000) {
|
|
97
|
+
return buildDiffFallback(oldLines, newLines, filePath, lineOffset);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const dp = Array.from({ length: m + 1 }, () => new Uint16Array(n + 1));
|
|
101
|
+
for (let i = 1; i <= m; i++)
|
|
102
|
+
for (let j = 1; j <= n; j++)
|
|
103
|
+
dp[i][j] = oldLines[i - 1] === newLines[j - 1]
|
|
104
|
+
? dp[i - 1][j - 1] + 1
|
|
105
|
+
: Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
106
|
+
|
|
107
|
+
const ops = [];
|
|
108
|
+
let i = m, j = n;
|
|
109
|
+
while (i > 0 || j > 0) {
|
|
110
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
111
|
+
ops.push({ type: 'ctx', text: oldLines[i - 1], oldLn: i, newLn: j });
|
|
112
|
+
i--; j--;
|
|
113
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
114
|
+
ops.push({ type: 'add', text: newLines[j - 1], newLn: j });
|
|
115
|
+
j--;
|
|
116
|
+
} else {
|
|
117
|
+
ops.push({ type: 'del', text: oldLines[i - 1], oldLn: i });
|
|
118
|
+
i--;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
ops.reverse();
|
|
122
|
+
return renderDiffOps(ops, filePath, lineOffset);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildDiffFallback(oldLines, newLines, filePath, lineOffset) {
|
|
126
|
+
const ops = [];
|
|
127
|
+
oldLines.forEach((l, i) => ops.push({ type: 'del', text: l, oldLn: i + 1 }));
|
|
128
|
+
newLines.forEach((l, i) => ops.push({ type: 'add', text: l, newLn: i + 1 }));
|
|
129
|
+
return renderDiffOps(ops, filePath, lineOffset || 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderDiffOps(ops, filePath, lineOffset) {
|
|
133
|
+
const off = lineOffset || 0;
|
|
134
|
+
let addCount = 0, delCount = 0;
|
|
135
|
+
ops.forEach(o => { if (o.type === 'add') addCount++; if (o.type === 'del') delCount++; });
|
|
136
|
+
|
|
137
|
+
let rows = '';
|
|
138
|
+
for (const o of ops) {
|
|
139
|
+
const cls = o.type === 'del' ? 'diff-del' : o.type === 'add' ? 'diff-add' : 'diff-ctx';
|
|
140
|
+
const sign = o.type === 'del' ? '-' : o.type === 'add' ? '+' : ' ';
|
|
141
|
+
const rawLn = o.type === 'del' ? (o.oldLn || '') : (o.newLn || o.oldLn || '');
|
|
142
|
+
const ln = rawLn !== '' ? rawLn + off : '';
|
|
143
|
+
rows += `<tr class="${cls}"><td class="diff-ln">${ln}</td><td class="diff-sign">${sign}</td><td class="diff-code">${esc(o.text)}</td></tr>`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const shortPath = shortenPath(filePath);
|
|
147
|
+
return `<div class="diff-view">
|
|
148
|
+
<div class="diff-header">
|
|
149
|
+
<svg class="diff-file-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
|
|
150
|
+
<span class="diff-file-path" title="${esc(filePath)}">${esc(shortPath)}</span>
|
|
151
|
+
<span class="diff-stats"><span class="ds-add">+${addCount}</span> <span class="ds-del">-${delCount}</span></span>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="diff-body"><table class="diff-table">${rows}</table></div>
|
|
154
|
+
</div>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- Step group management ----
|
|
158
|
+
export function closeGroup() {
|
|
159
|
+
if (S.currentGroup) {
|
|
160
|
+
S.currentGroup.querySelector('.step-count').textContent = S.currentGroupCount + ' steps';
|
|
161
|
+
S.currentGroup = null;
|
|
162
|
+
S.currentGroupCount = 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ensureGroup() {
|
|
167
|
+
if (!S.currentGroup) {
|
|
168
|
+
const g = document.createElement('div');
|
|
169
|
+
g.className = 'step-group open';
|
|
170
|
+
g.innerHTML = `
|
|
171
|
+
<div class="step-group-header">
|
|
172
|
+
<span class="step-chevron"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 18l6-6-6-6"/></svg></span>
|
|
173
|
+
<span class="step-count">steps</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="step-list"></div>
|
|
176
|
+
`;
|
|
177
|
+
$msgs.appendChild(g);
|
|
178
|
+
S.currentGroup = g;
|
|
179
|
+
S.currentGroupCount = 0;
|
|
180
|
+
}
|
|
181
|
+
return S.currentGroup.querySelector('.step-list');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- Welcome ----
|
|
185
|
+
function getWelcome() { return $('welcome'); }
|
|
186
|
+
export function removeWelcome() {
|
|
187
|
+
const w = getWelcome();
|
|
188
|
+
if (w && w.parentNode) w.remove();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function getWelcomeMarkup() {
|
|
192
|
+
return `<div class="welcome" id="welcome">
|
|
193
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><circle cx="12" cy="12" r="10"/><path d="M8 12h8M12 8v8"/></svg>
|
|
194
|
+
<h2>Claude Remote Control</h2>
|
|
195
|
+
<p>Connected. Send a message below to start.</p>
|
|
196
|
+
</div>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---- Junk filtering ----
|
|
200
|
+
function isJunkContent(content) {
|
|
201
|
+
if (typeof content !== 'string') return false;
|
|
202
|
+
const t = content.trim();
|
|
203
|
+
if (/^\/[a-z]+$/i.test(t)) return true;
|
|
204
|
+
if (/^Set model to/i.test(t) || /Set model to/i.test(t.replace(/\x1B\[[0-9;]*m/g, ''))) return true;
|
|
205
|
+
return JUNK_PATTERNS.some(p => p.test(t));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---- Model / Header ----
|
|
209
|
+
export function syncConfirmedModel(nextModel, { allowToast = false } = {}) {
|
|
210
|
+
const normalized = String(nextModel || '').trim();
|
|
211
|
+
if (!normalized) return false;
|
|
212
|
+
const prevModel = S.model || '';
|
|
213
|
+
if (prevModel === normalized) return false;
|
|
214
|
+
S.model = normalized;
|
|
215
|
+
updateHeaderInfo();
|
|
216
|
+
if (allowToast && !S.replaying && prevModel) {
|
|
217
|
+
showToast('Now using ' + formatModel(S.model));
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function updateHeaderInfo() {
|
|
223
|
+
const pathStr = shortenPath(S.cwd);
|
|
224
|
+
const model = formatModel(S.model);
|
|
225
|
+
$('title').textContent = pathStr || 'Claude Remote';
|
|
226
|
+
$('header-model').textContent = model;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- Turn state ----
|
|
230
|
+
export function cacheTurnState(state) {
|
|
231
|
+
if (!state) return;
|
|
232
|
+
const nextVersion = Number.isInteger(state.version) ? state.version : 0;
|
|
233
|
+
const pendingVersion = Number.isInteger(S.pendingTurnState?.version) ? S.pendingTurnState.version : -1;
|
|
234
|
+
if (nextVersion < pendingVersion) return;
|
|
235
|
+
S.pendingTurnState = state;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function applyTurnState(state, reason = '') {
|
|
239
|
+
if (!state) return;
|
|
240
|
+
const nextVersion = Number.isInteger(state.version) ? state.version : 0;
|
|
241
|
+
if (nextVersion < S.turnStateVersion) return;
|
|
242
|
+
S.turnStateVersion = nextVersion;
|
|
243
|
+
S.pendingTurnState = null;
|
|
244
|
+
const shouldWait = state.phase === 'running';
|
|
245
|
+
if (S.waiting !== shouldWait) {
|
|
246
|
+
setWaiting(shouldWait, reason || `turn_state:${state.phase || 'idle'}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- Optimistic bubble ----
|
|
251
|
+
export function finalizeOptimisticBubble() {
|
|
252
|
+
const opt = $msgs.querySelector('[data-optimistic]');
|
|
253
|
+
if (!opt) return false;
|
|
254
|
+
opt.removeAttribute('data-optimistic');
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function hasOptimisticBubble() {
|
|
259
|
+
return !!$msgs.querySelector('[data-optimistic]');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---- rebuildRuntimeStateFromDom ----
|
|
263
|
+
export function rebuildRuntimeStateFromDom() {
|
|
264
|
+
S.messageMap.clear();
|
|
265
|
+
S.toolMap.clear();
|
|
266
|
+
S.currentGroup = null;
|
|
267
|
+
S.currentGroupCount = 0;
|
|
268
|
+
|
|
269
|
+
$msgs.querySelectorAll('[data-message-id]').forEach(el => {
|
|
270
|
+
if (el.dataset.messageId) S.messageMap.set(el.dataset.messageId, el);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
$msgs.querySelectorAll('.step-item[data-tool-id]').forEach(item => {
|
|
274
|
+
const toolId = item.dataset.toolId;
|
|
275
|
+
if (!toolId) return;
|
|
276
|
+
const detail = document.getElementById(`detail-${toolId}`);
|
|
277
|
+
S.toolMap.set(toolId, {
|
|
278
|
+
item,
|
|
279
|
+
detail,
|
|
280
|
+
name: item.dataset.toolName || '',
|
|
281
|
+
group: item.closest('.step-group'),
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const lastChild = $msgs.lastElementChild;
|
|
286
|
+
if (lastChild && lastChild.classList.contains('step-group')) {
|
|
287
|
+
S.currentGroup = lastChild;
|
|
288
|
+
S.currentGroupCount = lastChild.querySelectorAll('.step-item').length;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---- Clear conversation UI ----
|
|
293
|
+
export function clearConversationUi() {
|
|
294
|
+
debugLog('clear_conversation_ui', {
|
|
295
|
+
waiting: S.waiting,
|
|
296
|
+
sessionId: S.sessionId || null,
|
|
297
|
+
lastSeq: S.lastSeq,
|
|
298
|
+
replaying: S.replaying,
|
|
299
|
+
});
|
|
300
|
+
if (S.cacheSaveTimer) {
|
|
301
|
+
clearTimeout(S.cacheSaveTimer);
|
|
302
|
+
S.cacheSaveTimer = null;
|
|
303
|
+
}
|
|
304
|
+
for (const [uploadId, waiter] of S.uploadWaiters) {
|
|
305
|
+
waiter.reject(new Error('Upload reset'));
|
|
306
|
+
S.uploadWaiters.delete(uploadId);
|
|
307
|
+
}
|
|
308
|
+
S.seenUuids.clear();
|
|
309
|
+
S.messageMap.clear();
|
|
310
|
+
S.toolMap.clear();
|
|
311
|
+
S.currentGroup = null;
|
|
312
|
+
S.currentGroupCount = 0;
|
|
313
|
+
S.isAtBottom = true;
|
|
314
|
+
S.waiting = false;
|
|
315
|
+
S.workingEl = null;
|
|
316
|
+
S.waitStartedAt = 0;
|
|
317
|
+
S.lastSeq = 0;
|
|
318
|
+
S.pendingPerms = [];
|
|
319
|
+
S.pendingPlanContent = '';
|
|
320
|
+
resetInteractionState();
|
|
321
|
+
resetTodoState();
|
|
322
|
+
clearPendingImage({ abortUpload: false });
|
|
323
|
+
$msgs.innerHTML = getWelcomeMarkup();
|
|
324
|
+
$('input-area').classList.remove('waiting');
|
|
325
|
+
$input.disabled = false;
|
|
326
|
+
$input.placeholder = INPUT_PLACEHOLDER_DEFAULT;
|
|
327
|
+
setSendButtonMode('send');
|
|
328
|
+
updateSendBtn();
|
|
329
|
+
updateScrollBtn();
|
|
330
|
+
setConnBanner(false);
|
|
331
|
+
$('perm-overlay').classList.remove('visible');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ---- Session cache integration ----
|
|
335
|
+
export async function restoreSessionCache(sessionId) {
|
|
336
|
+
debugLog('restore_session_cache_start', { sessionId });
|
|
337
|
+
if (!serverCacheAddr || !sessionId) return false;
|
|
338
|
+
|
|
339
|
+
let record;
|
|
340
|
+
try {
|
|
341
|
+
record = await chatCacheRead(buildCacheKey(serverCacheAddr, sessionId));
|
|
342
|
+
} catch {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
if (!record || !record.html) {
|
|
346
|
+
debugLog('restore_session_cache_miss', { sessionId });
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
$msgs.innerHTML = record.html;
|
|
351
|
+
$msgs.querySelectorAll('[data-optimistic], .working-indicator').forEach(el => el.remove());
|
|
352
|
+
S.seenUuids = new Set(Array.isArray(record.seenUuids) ? record.seenUuids : []);
|
|
353
|
+
S.lastSeq = Number.isInteger(record.lastSeq) ? record.lastSeq : 0;
|
|
354
|
+
S.cwd = record.cwd || S.cwd;
|
|
355
|
+
S.model = record.model || '';
|
|
356
|
+
restoreTodoSnapshot({
|
|
357
|
+
tasks: Array.isArray(record.todoTasks) ? record.todoTasks : [],
|
|
358
|
+
panelOpen: !!record.todoPanelOpen,
|
|
359
|
+
});
|
|
360
|
+
rebuildRuntimeStateFromDom();
|
|
361
|
+
removeWorkingIndicator();
|
|
362
|
+
$input.disabled = false;
|
|
363
|
+
$('btn-send').disabled = false;
|
|
364
|
+
$('input-area').classList.remove('waiting');
|
|
365
|
+
$input.placeholder = INPUT_PLACEHOLDER_DEFAULT;
|
|
366
|
+
updateHeaderInfo();
|
|
367
|
+
updateSendBtn();
|
|
368
|
+
updateScrollBtn();
|
|
369
|
+
requestAnimationFrame(() => { $chat.scrollTop = $chat.scrollHeight; });
|
|
370
|
+
debugLog('restore_session_cache_done', {
|
|
371
|
+
sessionId,
|
|
372
|
+
lastSeq: S.lastSeq,
|
|
373
|
+
waiting: S.waiting,
|
|
374
|
+
});
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function flushSessionCacheSave() {
|
|
379
|
+
if (S.cacheSaveTimer) {
|
|
380
|
+
clearTimeout(S.cacheSaveTimer);
|
|
381
|
+
S.cacheSaveTimer = null;
|
|
382
|
+
}
|
|
383
|
+
await persistSessionCache();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function persistSessionCache() {
|
|
387
|
+
if (!serverCacheAddr || !S.sessionId) return;
|
|
388
|
+
const todoSnapshot = getTodoSnapshot();
|
|
389
|
+
const snapshotRoot = $msgs.cloneNode(true);
|
|
390
|
+
snapshotRoot.querySelectorAll('[data-optimistic], .working-indicator').forEach(el => el.remove());
|
|
391
|
+
const record = {
|
|
392
|
+
cacheKey: buildCacheKey(serverCacheAddr, S.sessionId),
|
|
393
|
+
serverAddr,
|
|
394
|
+
sessionId: S.sessionId,
|
|
395
|
+
html: snapshotRoot.innerHTML,
|
|
396
|
+
seenUuids: Array.from(S.seenUuids),
|
|
397
|
+
todoTasks: todoSnapshot.tasks,
|
|
398
|
+
todoPanelOpen: todoSnapshot.panelOpen,
|
|
399
|
+
cwd: S.cwd,
|
|
400
|
+
model: S.model,
|
|
401
|
+
lastSeq: S.lastSeq,
|
|
402
|
+
updatedAt: Date.now(),
|
|
403
|
+
};
|
|
404
|
+
record.sizeBytes = estimateCacheBytes(record);
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
if (record.sizeBytes > CHAT_CACHE_MAX_SESSION_BYTES) {
|
|
408
|
+
await chatCacheDelete(record.cacheKey).catch(() => {});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await chatCacheWrite(record);
|
|
412
|
+
await pruneChatCache();
|
|
413
|
+
} catch (err) {
|
|
414
|
+
console.warn('[chat-cache]', err);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function scheduleSessionCacheSave() {
|
|
419
|
+
if (!serverCacheAddr || !S.sessionId) return;
|
|
420
|
+
if (S.cacheSaveTimer) clearTimeout(S.cacheSaveTimer);
|
|
421
|
+
S.cacheSaveTimer = setTimeout(async () => {
|
|
422
|
+
S.cacheSaveTimer = null;
|
|
423
|
+
await persistSessionCache();
|
|
424
|
+
}, 250);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ---- Plan card ----
|
|
428
|
+
export function renderPlanCard(planContent) {
|
|
429
|
+
if ($msgs.querySelector('.plan-inline-card')) return;
|
|
430
|
+
closeGroup();
|
|
431
|
+
const el = document.createElement('div');
|
|
432
|
+
el.className = 'plan-inline-card';
|
|
433
|
+
el.innerHTML = `
|
|
434
|
+
<div class="plan-inline-header">
|
|
435
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 12l2 2 4-4"/></svg>
|
|
436
|
+
<span>执行计划</span>
|
|
437
|
+
</div>
|
|
438
|
+
<div class="plan-inline-body">${renderMd(planContent)}</div>
|
|
439
|
+
`;
|
|
440
|
+
$msgs.appendChild(el);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function consumePendingPlanCard() {
|
|
444
|
+
const plan = normalizePlanContent(S.pendingPlanContent);
|
|
445
|
+
S.pendingPlanContent = '';
|
|
446
|
+
if (!plan) return;
|
|
447
|
+
renderPlanCard(plan);
|
|
448
|
+
scrollEnd();
|
|
449
|
+
scheduleSessionCacheSave();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ---- Image overlay ----
|
|
453
|
+
function showImageOverlay(src) {
|
|
454
|
+
let overlay = document.getElementById('image-overlay');
|
|
455
|
+
if (!overlay) {
|
|
456
|
+
overlay = document.createElement('div');
|
|
457
|
+
overlay.id = 'image-overlay';
|
|
458
|
+
overlay.className = 'image-overlay';
|
|
459
|
+
overlay.addEventListener('click', () => overlay.classList.remove('visible'));
|
|
460
|
+
overlay.innerHTML = '<img>';
|
|
461
|
+
document.body.appendChild(overlay);
|
|
462
|
+
}
|
|
463
|
+
overlay.querySelector('img').src = src;
|
|
464
|
+
overlay.classList.add('visible');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ---- Event Processing ----
|
|
468
|
+
export function processEvent(evt, seq) {
|
|
469
|
+
try {
|
|
470
|
+
if (!evt) return;
|
|
471
|
+
if (Number.isInteger(seq) && seq > S.lastSeq) S.lastSeq = seq;
|
|
472
|
+
if (S.seenUuids.has(evt.uuid)) return;
|
|
473
|
+
if (evt.uuid) S.seenUuids.add(evt.uuid);
|
|
474
|
+
removeWelcome();
|
|
475
|
+
|
|
476
|
+
if (evt.isCompactSummary) {
|
|
477
|
+
renderCompactSummary(evt);
|
|
478
|
+
scrollEnd();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (evt.type === 'system' && evt.subtype === 'local_command') {
|
|
483
|
+
const raw = (evt.content || '').replace(/<\/?local-command-stdout>/g, '').replace(/\x1B\[[0-9;]*m/g, '').trim();
|
|
484
|
+
if (raw.includes('Total cost:')) {
|
|
485
|
+
renderCostCard(raw);
|
|
486
|
+
scrollEnd();
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (evt.type === 'interrupt') {
|
|
492
|
+
finalizeOptimisticBubble();
|
|
493
|
+
renderInterruptBanner(evt);
|
|
494
|
+
scrollEnd();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (evt.type === 'user' && evt.message) {
|
|
499
|
+
const c = evt.message.content;
|
|
500
|
+
if (typeof c === 'string' && isJunkContent(c)) return;
|
|
501
|
+
if (typeof c === 'string' && /^\[Request interrupted by user/.test(c.trim())) return;
|
|
502
|
+
if (Array.isArray(c) && c.length === 1 && c[0].type === 'text' &&
|
|
503
|
+
/^\[Request interrupted by user/.test(c[0].text)) return;
|
|
504
|
+
const planPrefix = 'Implement the following plan:';
|
|
505
|
+
const rawText = typeof c === 'string' ? c
|
|
506
|
+
: (Array.isArray(c) && c.length >= 1 && c[0].type === 'text' ? c[0].text : '');
|
|
507
|
+
if (rawText.trimStart().startsWith(planPrefix)) {
|
|
508
|
+
let planBody = rawText.trimStart().slice(planPrefix.length).trim();
|
|
509
|
+
const boilerplateIdx = planBody.indexOf('\nIf you need specific details from before exiting plan mode');
|
|
510
|
+
if (boilerplateIdx !== -1) planBody = planBody.slice(0, boilerplateIdx).trim();
|
|
511
|
+
if (planBody) {
|
|
512
|
+
renderPlanCard(planBody);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (evt.type === 'assistant' && evt.message) {
|
|
518
|
+
const blocks = evt.message.content;
|
|
519
|
+
if (Array.isArray(blocks) && blocks.length === 1 && blocks[0].type === 'text') {
|
|
520
|
+
const txt = blocks[0].text;
|
|
521
|
+
if (isJunkContent(txt)) return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (evt.type === 'user' && evt.message) renderUser(evt);
|
|
526
|
+
else if (evt.type === 'assistant' && evt.message) {
|
|
527
|
+
if (S.waiting) switchToWorking();
|
|
528
|
+
renderAssistant(evt);
|
|
529
|
+
}
|
|
530
|
+
scrollEnd();
|
|
531
|
+
scheduleSessionCacheSave();
|
|
532
|
+
} catch (e) {
|
|
533
|
+
console.error('[processEvent]', e);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---- User ----
|
|
538
|
+
function renderUser(evt) {
|
|
539
|
+
const c = evt.message.content;
|
|
540
|
+
if (Array.isArray(c)) {
|
|
541
|
+
const imageBlocks = c.filter(b => b && b.type === 'image');
|
|
542
|
+
const textBlocks = c.filter(b => b && b.type === 'text' && b.text);
|
|
543
|
+
if (imageBlocks.length > 0 || textBlocks.length > 0) {
|
|
544
|
+
closeGroup();
|
|
545
|
+
if (finalizeOptimisticBubble()) return;
|
|
546
|
+
|
|
547
|
+
const el = document.createElement('div');
|
|
548
|
+
el.className = 'user-msg';
|
|
549
|
+
let html = '';
|
|
550
|
+
if (imageBlocks.length > 0) {
|
|
551
|
+
html += imageBlocks.map(block => {
|
|
552
|
+
const source = block.source && block.source.type === 'base64' ? block.source : null;
|
|
553
|
+
if (!source || !source.data) return '';
|
|
554
|
+
const mediaType = source.media_type || 'image/png';
|
|
555
|
+
return `<img src="data:${mediaType};base64,${source.data}" style="max-width:200px;max-height:120px;border-radius:8px;display:block;margin-bottom:${textBlocks.length ? '6px' : '0'}">`;
|
|
556
|
+
}).join('');
|
|
557
|
+
}
|
|
558
|
+
if (textBlocks.length > 0) {
|
|
559
|
+
const cleaned = textBlocks.map(block => stripImageTags(block.text)).filter(Boolean);
|
|
560
|
+
if (cleaned.length > 0) {
|
|
561
|
+
html += cleaned.map(t => esc(t).replace(/\n/g, '<br>')).join('<br>');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (html) {
|
|
565
|
+
el.innerHTML = html;
|
|
566
|
+
$msgs.appendChild(el);
|
|
567
|
+
}
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
for (const b of c) {
|
|
571
|
+
if (b.type === 'tool_result') {
|
|
572
|
+
resolveInteractiveToolResult(b);
|
|
573
|
+
handleTodoToolResult(b, evt);
|
|
574
|
+
attachResult(b);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (typeof c === 'string') {
|
|
580
|
+
const cleaned = stripImageTags(c);
|
|
581
|
+
if (cleaned.trim()) {
|
|
582
|
+
closeGroup();
|
|
583
|
+
if (finalizeOptimisticBubble()) return;
|
|
584
|
+
const el = document.createElement('div');
|
|
585
|
+
el.className = 'user-msg';
|
|
586
|
+
el.innerHTML = esc(cleaned).replace(/\n/g, '<br>');
|
|
587
|
+
$msgs.appendChild(el);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ---- Interrupt Banner ----
|
|
593
|
+
function renderInterruptBanner(evt) {
|
|
594
|
+
closeGroup();
|
|
595
|
+
const wrapper = document.createElement('div');
|
|
596
|
+
wrapper.style.cssText = 'position:relative;display:flex;align-items:center;margin:12px 0;overflow:hidden;';
|
|
597
|
+
const line = document.createElement('div');
|
|
598
|
+
line.style.cssText = 'position:absolute;left:0;right:0;top:50%;height:1px;background:var(--border);';
|
|
599
|
+
wrapper.appendChild(line);
|
|
600
|
+
const el = document.createElement('div');
|
|
601
|
+
const isTerminal = evt.source === 'terminal';
|
|
602
|
+
el.className = 'interrupt-banner' + (isTerminal ? ' terminal-interrupt' : ' user-interrupt');
|
|
603
|
+
const label = isTerminal ? '终端中断' : '用户中断';
|
|
604
|
+
const icon = isTerminal
|
|
605
|
+
? '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M7 15l3-3-3-3"/><line x1="13" y1="15" x2="17" y2="15"/></svg>'
|
|
606
|
+
: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>';
|
|
607
|
+
el.innerHTML = icon + '<span>' + label + '</span>';
|
|
608
|
+
wrapper.appendChild(el);
|
|
609
|
+
$msgs.appendChild(wrapper);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ---- Cost Card ----
|
|
613
|
+
function renderCostCard(raw) {
|
|
614
|
+
closeGroup();
|
|
615
|
+
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
|
|
616
|
+
let totalCost = '', apiDuration = '', wallDuration = '', codeChanges = '';
|
|
617
|
+
const models = [];
|
|
618
|
+
let inModels = false;
|
|
619
|
+
|
|
620
|
+
for (const line of lines) {
|
|
621
|
+
if (line.startsWith('Total cost:')) totalCost = line.replace('Total cost:', '').trim();
|
|
622
|
+
else if (line.startsWith('Total duration (API):')) apiDuration = line.replace('Total duration (API):', '').trim();
|
|
623
|
+
else if (line.startsWith('Total duration (wall):')) wallDuration = line.replace('Total duration (wall):', '').trim();
|
|
624
|
+
else if (line.startsWith('Total code changes:')) codeChanges = line.replace('Total code changes:', '').trim();
|
|
625
|
+
else if (line.startsWith('Usage by model:')) inModels = true;
|
|
626
|
+
else if (inModels) {
|
|
627
|
+
const m = line.match(/^(.+?):\s*(.+)\((\$[\d.]+)\)\s*$/);
|
|
628
|
+
if (m) {
|
|
629
|
+
models.push({ name: m[1].trim(), detail: m[2].trim().replace(/,\s*$/, ''), cost: m[3] });
|
|
630
|
+
} else {
|
|
631
|
+
const m2 = line.match(/^(.+?):\s*(.+)$/);
|
|
632
|
+
if (m2) models.push({ name: m2[1].trim(), detail: m2[2].trim(), cost: '' });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const el = document.createElement('div');
|
|
638
|
+
el.className = 'cost-card';
|
|
639
|
+
el.innerHTML = `
|
|
640
|
+
<div class="cost-header">
|
|
641
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
|
642
|
+
<span>费用概览</span>
|
|
643
|
+
</div>
|
|
644
|
+
<div class="cost-grid">
|
|
645
|
+
<div class="cost-item"><div class="cost-label">总费用</div><div class="cost-value cost-highlight">${esc(totalCost)}</div></div>
|
|
646
|
+
<div class="cost-item"><div class="cost-label">API 耗时</div><div class="cost-value">${esc(apiDuration)}</div></div>
|
|
647
|
+
<div class="cost-item"><div class="cost-label">实际耗时</div><div class="cost-value">${esc(wallDuration)}</div></div>
|
|
648
|
+
<div class="cost-item"><div class="cost-label">代码变更</div><div class="cost-value">${esc(codeChanges)}</div></div>
|
|
649
|
+
</div>
|
|
650
|
+
${models.length ? `
|
|
651
|
+
<div class="cost-models">
|
|
652
|
+
<div class="cost-models-title">模型用量</div>
|
|
653
|
+
${models.map(m => `
|
|
654
|
+
<div class="cost-model-row">
|
|
655
|
+
<div class="cost-model-name">${esc(m.name)}</div>
|
|
656
|
+
<div class="cost-model-detail">${esc(m.detail)}</div>
|
|
657
|
+
${m.cost ? `<div class="cost-model-cost">${esc(m.cost)}</div>` : ''}
|
|
658
|
+
</div>
|
|
659
|
+
`).join('')}
|
|
660
|
+
</div>` : ''}
|
|
661
|
+
`;
|
|
662
|
+
$msgs.appendChild(el);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ---- Compact Summary ----
|
|
666
|
+
function renderCompactSummary(evt) {
|
|
667
|
+
hideCmdOverlay();
|
|
668
|
+
$('input-area').classList.remove('waiting');
|
|
669
|
+
if (S.waiting) setWaiting(false, 'compact_summary');
|
|
670
|
+
|
|
671
|
+
closeGroup();
|
|
672
|
+
S.messageMap.clear();
|
|
673
|
+
S.toolMap.clear();
|
|
674
|
+
S.currentGroup = null;
|
|
675
|
+
S.currentGroupCount = 0;
|
|
676
|
+
$msgs.innerHTML = '';
|
|
677
|
+
|
|
678
|
+
const el = document.createElement('div');
|
|
679
|
+
el.className = 'compact-divider';
|
|
680
|
+
el.innerHTML = `
|
|
681
|
+
<div class="compact-line"></div>
|
|
682
|
+
<div class="compact-badge">
|
|
683
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.66 0 3-4.03 3-9s-1.34-9-3-9m0 18c-1.66 0-3-4.03-3-9s1.34-9 3-9"/></svg>
|
|
684
|
+
<span>历史对话已压缩</span>
|
|
685
|
+
</div>
|
|
686
|
+
<div class="compact-line"></div>
|
|
687
|
+
`;
|
|
688
|
+
$msgs.appendChild(el);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ---- Assistant ----
|
|
692
|
+
function renderAssistant(evt) {
|
|
693
|
+
const blocks = evt.message.content || [];
|
|
694
|
+
const msgId = evt.message.id;
|
|
695
|
+
const usage = evt.message.usage;
|
|
696
|
+
|
|
697
|
+
syncConfirmedModel(evt.message.model, { allowToast: true });
|
|
698
|
+
|
|
699
|
+
for (const b of blocks) {
|
|
700
|
+
try {
|
|
701
|
+
if (b.type === 'thinking' && b.thinking) renderThinking(b);
|
|
702
|
+
else if (b.type === 'text' && b.text) { closeGroup(); renderText(b.text, msgId); }
|
|
703
|
+
else if (b.type === 'tool_use') {
|
|
704
|
+
const toolName = b.name || '';
|
|
705
|
+
if (isTodoTool(toolName)) {
|
|
706
|
+
handleTodoToolUse(b);
|
|
707
|
+
}
|
|
708
|
+
if (registerInteractiveToolUse(b)) {
|
|
709
|
+
// handled by interaction state machine
|
|
710
|
+
} else if (!HIDDEN_STEP_TOOLS.has(toolName)) {
|
|
711
|
+
renderTool(b);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} catch (e) {
|
|
715
|
+
console.error('[renderBlock]', e);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (usage && evt.message.stop_reason) {
|
|
720
|
+
const total = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) +
|
|
721
|
+
(usage.cache_creation_input_tokens || 0) + (usage.output_tokens || 0);
|
|
722
|
+
if (total > 0 && S.currentGroup) {
|
|
723
|
+
let tc = S.currentGroup.querySelector('.token-count');
|
|
724
|
+
if (!tc) {
|
|
725
|
+
tc = document.createElement('div');
|
|
726
|
+
tc.className = 'token-count';
|
|
727
|
+
S.currentGroup.appendChild(tc);
|
|
728
|
+
}
|
|
729
|
+
tc.textContent = formatTokens(total);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function renderText(text, msgId) {
|
|
735
|
+
let el = S.messageMap.get(msgId);
|
|
736
|
+
if (!el) {
|
|
737
|
+
el = document.createElement('div');
|
|
738
|
+
el.className = 'assistant-text';
|
|
739
|
+
if (msgId) el.dataset.messageId = msgId;
|
|
740
|
+
S.messageMap.set(msgId, el);
|
|
741
|
+
$msgs.appendChild(el);
|
|
742
|
+
}
|
|
743
|
+
const d = document.createElement('div');
|
|
744
|
+
d.innerHTML = renderMd(text);
|
|
745
|
+
el.appendChild(d);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function renderThinking(b) {
|
|
749
|
+
closeGroup();
|
|
750
|
+
const el = document.createElement('div');
|
|
751
|
+
el.className = 'thinking';
|
|
752
|
+
const preview = trunc(b.thinking.replace(/\n/g, ' '), 60);
|
|
753
|
+
el.innerHTML = `
|
|
754
|
+
<div class="thinking-toggle">
|
|
755
|
+
<span class="thinking-chevron"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 18l6-6-6-6"/></svg></span>
|
|
756
|
+
<span>Thinking</span>
|
|
757
|
+
<span style="color:var(--text-muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0">${esc(preview)}</span>
|
|
758
|
+
</div>
|
|
759
|
+
<div class="thinking-content">${esc(b.thinking)}</div>
|
|
760
|
+
`;
|
|
761
|
+
$msgs.appendChild(el);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function renderTool(b) {
|
|
765
|
+
const list = ensureGroup();
|
|
766
|
+
S.currentGroupCount++;
|
|
767
|
+
S.currentGroup.querySelector('.step-count').textContent = S.currentGroupCount + ' steps';
|
|
768
|
+
|
|
769
|
+
const desc = toolDesc(b.name, b.input);
|
|
770
|
+
const item = document.createElement('div');
|
|
771
|
+
item.className = 'step-item loading';
|
|
772
|
+
item.dataset.toolId = b.id || '';
|
|
773
|
+
item.dataset.toolName = b.name || '';
|
|
774
|
+
item.innerHTML = `
|
|
775
|
+
<div class="step-icon">${iconFor(b.name)}</div>
|
|
776
|
+
<span class="step-name">${esc(b.name)}</span>
|
|
777
|
+
<span class="step-desc">${esc(trunc(desc, 40))}</span>
|
|
778
|
+
<span class="step-duration" id="dur-${b.id}"></span>
|
|
779
|
+
<span class="step-arrow"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg></span>
|
|
780
|
+
`;
|
|
781
|
+
|
|
782
|
+
const detail = document.createElement('div');
|
|
783
|
+
detail.className = 'step-detail';
|
|
784
|
+
detail.id = `detail-${b.id}`;
|
|
785
|
+
const inputFull = toolInputFull(b.name, b.input);
|
|
786
|
+
const isEdit = b.name === 'Edit' && b.input && b.input.old_string !== undefined;
|
|
787
|
+
if (isEdit) {
|
|
788
|
+
detail.innerHTML = buildDiffHtml(b.input.old_string, b.input.new_string, b.input.file_path, b.input._startLine);
|
|
789
|
+
} else {
|
|
790
|
+
detail.innerHTML = `
|
|
791
|
+
<div class="detail-input">${esc(inputFull)}</div>
|
|
792
|
+
<div class="detail-result" id="result-${b.id}"></div>
|
|
793
|
+
`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
list.appendChild(item);
|
|
797
|
+
list.appendChild(detail);
|
|
798
|
+
S.toolMap.set(b.id, { item, detail, name: b.name, group: S.currentGroup });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function attachResult(b) {
|
|
802
|
+
const info = S.toolMap.get(b.tool_use_id);
|
|
803
|
+
if (!info) return;
|
|
804
|
+
info.item.classList.remove('loading');
|
|
805
|
+
const isErr = b.is_error === true;
|
|
806
|
+
if (isErr) info.detail.classList.add('error');
|
|
807
|
+
|
|
808
|
+
let text = '';
|
|
809
|
+
let images = [];
|
|
810
|
+
if (typeof b.content === 'string') {
|
|
811
|
+
text = b.content;
|
|
812
|
+
} else if (Array.isArray(b.content)) {
|
|
813
|
+
const textParts = [];
|
|
814
|
+
for (const c of b.content) {
|
|
815
|
+
if (c.type === 'image' && c.source && c.source.data) {
|
|
816
|
+
const mediaType = c.source.media_type || 'image/png';
|
|
817
|
+
images.push({ data: c.source.data, mediaType });
|
|
818
|
+
} else if (c.text) {
|
|
819
|
+
textParts.push(c.text);
|
|
820
|
+
} else {
|
|
821
|
+
textParts.push(JSON.stringify(c));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
text = textParts.join('\n');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const resultEl = info.detail.querySelector('.detail-result');
|
|
828
|
+
if (resultEl) {
|
|
829
|
+
resultEl.textContent = trunc(text, 3000);
|
|
830
|
+
if (isErr) resultEl.style.color = 'var(--error)';
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (images.length > 0) {
|
|
834
|
+
closeGroup();
|
|
835
|
+
for (const img of images) {
|
|
836
|
+
const wrapper = document.createElement('div');
|
|
837
|
+
wrapper.className = 'result-image-block';
|
|
838
|
+
const imgEl = document.createElement('img');
|
|
839
|
+
imgEl.src = `data:${img.mediaType};base64,${img.data}`;
|
|
840
|
+
imgEl.addEventListener('click', () => showImageOverlay(imgEl.src));
|
|
841
|
+
wrapper.appendChild(imgEl);
|
|
842
|
+
$msgs.appendChild(wrapper);
|
|
843
|
+
}
|
|
844
|
+
scrollEnd();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// ---- Init delegated click handlers ----
|
|
849
|
+
export function initRenderer() {
|
|
850
|
+
$msgs.addEventListener('click', (e) => {
|
|
851
|
+
const toggle = e.target.closest('.thinking-toggle');
|
|
852
|
+
if (toggle) { toggle.parentElement.classList.toggle('open'); return; }
|
|
853
|
+
const header = e.target.closest('.step-group-header');
|
|
854
|
+
if (header) { header.parentElement.classList.toggle('open'); return; }
|
|
855
|
+
const item = e.target.closest('.step-item');
|
|
856
|
+
if (item) {
|
|
857
|
+
const toolId = item.dataset.toolId;
|
|
858
|
+
const detail = toolId && document.getElementById(`detail-${toolId}`);
|
|
859
|
+
if (detail) detail.classList.toggle('open');
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|