micro-models-agent 2.12.0 → 2.13.1
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/CHANGELOG.md +29 -0
- package/dist/assets/app.js +22 -2
- package/dist/assets/index.html +39 -9
- package/dist/assets/js/dom.js +15 -0
- package/dist/assets/js/feed.js +129 -10
- package/dist/assets/js/sidebar.js +512 -0
- package/dist/assets/js/tabs.js +8 -2
- package/dist/assets/setup.js +267 -8
- package/dist/assets/styles.css +168 -3
- package/dist/i18n/en.json +89 -3
- package/dist/i18n/ru.json +89 -3
- package/dist/main.js +615 -92
- package/package.json +1 -1
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// Sidebar tabs (Session / Map / Trace) for `mma web`.
|
|
2
|
+
//
|
|
3
|
+
// Owns: active-tab state (localStorage), the live per-session Trace buffer with
|
|
4
|
+
// master-detail expansion, and the Map tab rendering. Leaf module: imports only
|
|
5
|
+
// dom/i18n/format/store/api — the frontend DAG stays acyclic
|
|
6
|
+
// (app → sidebar → {dom,i18n,format,store,api}).
|
|
7
|
+
// Design: docs/specs/2026-09-30-webui-sidebar-tabs-design.md.
|
|
8
|
+
|
|
9
|
+
import { el, icon } from "./dom.js";
|
|
10
|
+
import { api } from "./api.js";
|
|
11
|
+
import { S } from "./store.js";
|
|
12
|
+
import { t } from "./i18n.js";
|
|
13
|
+
import { labelOf, categoryOf, argText, formatMs, fmtTokens } from "./format.js";
|
|
14
|
+
|
|
15
|
+
const TAB_KEY = "mma-sidebar-tab";
|
|
16
|
+
const TABS = ["session", "map", "trace"];
|
|
17
|
+
const ICON_NAME = { session: "list-checks", map: "map", trace: "activity" };
|
|
18
|
+
const TRACE_CAP = 500;
|
|
19
|
+
const MAX_OUTPUT_CHARS = 20000;
|
|
20
|
+
|
|
21
|
+
let sbEl = null, nameEl = null, nameTextEl = null, mapEl = null, listEl = null, detailEl = null;
|
|
22
|
+
let detailGlyph = null, detailTitle = null, detailBody = null, detailClose = null;
|
|
23
|
+
let active = "session";
|
|
24
|
+
let trace = []; // { id, kind, ts, name, args, output, success, durationMs, phase, text, steps, currentIndex }
|
|
25
|
+
let selected = null; // trace id
|
|
26
|
+
let turn = 0;
|
|
27
|
+
let seq = 0;
|
|
28
|
+
const nodeById = new Map(); // entry id → row node
|
|
29
|
+
const callIndex = new Map(); // toolCallId → entry
|
|
30
|
+
const headerByTurn = new Map(); // turn → header node
|
|
31
|
+
let textEntry = null; // current text row (updated in place)
|
|
32
|
+
|
|
33
|
+
function paneEl(id) { return document.getElementById("pane-" + id); }
|
|
34
|
+
|
|
35
|
+
function mqMobile() {
|
|
36
|
+
return typeof window !== "undefined" && window.matchMedia
|
|
37
|
+
? window.matchMedia("(max-width: 760px)").matches : false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readStored() {
|
|
41
|
+
try { return localStorage.getItem(TAB_KEY); } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
function storeTab(id) {
|
|
44
|
+
try { localStorage.setItem(TAB_KEY, id); } catch { /* private mode */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setSidebarTab(id, persist = true) {
|
|
48
|
+
if (!TABS.includes(id)) id = "session";
|
|
49
|
+
if (persist) storeTab(id);
|
|
50
|
+
const effective = id === "trace" && mqMobile() ? "session" : id;
|
|
51
|
+
active = effective;
|
|
52
|
+
TABS.forEach((name) => {
|
|
53
|
+
const pane = paneEl(name);
|
|
54
|
+
if (pane) pane.hidden = name !== effective;
|
|
55
|
+
const tab = sbEl && sbEl.querySelector('.sb-tab[data-tab="' + name + '"]');
|
|
56
|
+
if (tab) {
|
|
57
|
+
const on = name === effective;
|
|
58
|
+
tab.classList.toggle("on", on);
|
|
59
|
+
tab.setAttribute("aria-selected", on ? "true" : "false");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (nameTextEl) nameTextEl.textContent = t("web.sidebar.tab_" + effective);
|
|
63
|
+
const nic = nameEl && nameEl.querySelector(".sb-ico");
|
|
64
|
+
if (nic) { nic.textContent = ""; nic.appendChild(icon(ICON_NAME[effective])); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function collapseDetail() {
|
|
68
|
+
selected = null;
|
|
69
|
+
if (sbEl) sbEl.classList.remove("sb-expanded");
|
|
70
|
+
if (detailEl) detailEl.hidden = true;
|
|
71
|
+
if (listEl) listEl.querySelectorAll(".trc.sel").forEach((n) => n.classList.remove("sel"));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function phaseLabel(phase) {
|
|
75
|
+
if (phase === "composing") return t("web.rail.composing");
|
|
76
|
+
if (phase === "tool") return t("web.rail.working");
|
|
77
|
+
if (phase === "thinking") return t("web.action.thinking");
|
|
78
|
+
return String(phase || "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function catClass(kind, name) {
|
|
82
|
+
if (kind === "tool" || kind === "tool_fail") return categoryOf(name);
|
|
83
|
+
if (kind === "phase") return "phase";
|
|
84
|
+
return "meta";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rowLabel(entry) {
|
|
88
|
+
switch (entry.kind) {
|
|
89
|
+
case "phase": return phaseLabel(entry.phase);
|
|
90
|
+
case "plan": return t("web.sidebar.trace_plan");
|
|
91
|
+
case "question": return t("web.sidebar.trace_question");
|
|
92
|
+
case "text": return t("web.sidebar.trace_text", { count: entry.chars || 0 });
|
|
93
|
+
default: return labelOf(entry.name);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function rowArgs(entry) {
|
|
98
|
+
if (entry.kind === "text" || entry.kind === "phase") return "";
|
|
99
|
+
if (entry.kind === "plan") {
|
|
100
|
+
const idx = typeof entry.currentIndex === "number" ? entry.currentIndex : 0;
|
|
101
|
+
const total = Array.isArray(entry.steps) ? entry.steps.length : 0;
|
|
102
|
+
if (!total) return "";
|
|
103
|
+
return t("web.rail.step_of", { cur: Math.min(idx + 1, total), total });
|
|
104
|
+
}
|
|
105
|
+
if (entry.kind === "question") return entry.text || "";
|
|
106
|
+
return argText(entry.args);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isWarn(entry) {
|
|
110
|
+
return entry.kind === "tool_fail" || entry.kind === "error";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderRow(entry) {
|
|
114
|
+
const cls = ["trc", catClass(entry.kind, entry.name)];
|
|
115
|
+
if (isWarn(entry)) cls.push("warn");
|
|
116
|
+
if (selected === entry.id) cls.push("sel");
|
|
117
|
+
const row = el("div", cls.join(" "));
|
|
118
|
+
row.dataset.id = String(entry.id);
|
|
119
|
+
row.appendChild(el("span", "g", isWarn(entry) ? "⚠" : entry.kind === "question" ? "?" : "◇"));
|
|
120
|
+
row.appendChild(el("span", "lab", rowLabel(entry)));
|
|
121
|
+
const args = rowArgs(entry);
|
|
122
|
+
if (args) row.appendChild(el("span", "args", args));
|
|
123
|
+
if (entry.durationMs) row.appendChild(el("span", "t", formatMs(entry.durationMs)));
|
|
124
|
+
row.addEventListener("click", () => {
|
|
125
|
+
if (selected === entry.id) { collapseDetail(); return; }
|
|
126
|
+
if (listEl) listEl.querySelectorAll(".trc.sel").forEach((n) => n.classList.remove("sel"));
|
|
127
|
+
selected = entry.id;
|
|
128
|
+
row.classList.add("sel");
|
|
129
|
+
renderDetail(entry);
|
|
130
|
+
if (sbEl) sbEl.classList.add("sb-expanded");
|
|
131
|
+
});
|
|
132
|
+
return row;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderList() {
|
|
136
|
+
if (!listEl) return;
|
|
137
|
+
listEl.textContent = "";
|
|
138
|
+
listEl.dataset.lastTurn = "-1";
|
|
139
|
+
nodeById.clear();
|
|
140
|
+
headerByTurn.clear();
|
|
141
|
+
if (selected && !trace.some((e) => e.id === selected)) selected = null;
|
|
142
|
+
if (trace.length === 0) {
|
|
143
|
+
listEl.appendChild(el("div", "trace-turn", t("web.sidebar.trace_empty")));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
let shownTurn = -1;
|
|
147
|
+
for (const entry of trace) {
|
|
148
|
+
if (entry.turn !== shownTurn) {
|
|
149
|
+
shownTurn = entry.turn;
|
|
150
|
+
const h = el("div", "trace-turn", t("web.sidebar.trace_turn", { n: entry.turn }));
|
|
151
|
+
headerByTurn.set(entry.turn, h);
|
|
152
|
+
listEl.appendChild(h);
|
|
153
|
+
}
|
|
154
|
+
const row = renderRow(entry);
|
|
155
|
+
nodeById.set(entry.id, row);
|
|
156
|
+
listEl.appendChild(row);
|
|
157
|
+
}
|
|
158
|
+
listEl.dataset.lastTurn = String(shownTurn);
|
|
159
|
+
listEl.scrollTop = listEl.scrollHeight;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function append(entry) {
|
|
163
|
+
entry.id = ++seq;
|
|
164
|
+
entry.turn = turn;
|
|
165
|
+
entry.ts = Date.now();
|
|
166
|
+
trace.push(entry);
|
|
167
|
+
while (trace.length > TRACE_CAP) {
|
|
168
|
+
const old = trace.shift();
|
|
169
|
+
const node = nodeById.get(old.id);
|
|
170
|
+
if (node) { node.remove(); nodeById.delete(old.id); }
|
|
171
|
+
if (old.toolCallId != null) callIndex.delete(old.toolCallId);
|
|
172
|
+
if (!trace.some((e) => e.turn === old.turn)) {
|
|
173
|
+
const h = headerByTurn.get(old.turn);
|
|
174
|
+
if (h) { h.remove(); headerByTurn.delete(old.turn); }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return entry;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function pushRow(entry) {
|
|
181
|
+
if (!listEl) return;
|
|
182
|
+
const last = listEl.dataset.lastTurn ? Number(listEl.dataset.lastTurn) : -1;
|
|
183
|
+
if (entry.turn !== last) {
|
|
184
|
+
const h = el("div", "trace-turn", t("web.sidebar.trace_turn", { n: entry.turn }));
|
|
185
|
+
headerByTurn.set(entry.turn, h);
|
|
186
|
+
listEl.appendChild(h);
|
|
187
|
+
listEl.dataset.lastTurn = String(entry.turn);
|
|
188
|
+
}
|
|
189
|
+
const row = renderRow(entry);
|
|
190
|
+
nodeById.set(entry.id, row);
|
|
191
|
+
listEl.appendChild(row);
|
|
192
|
+
listEl.scrollTop = listEl.scrollHeight;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function replaceRow(entry) {
|
|
196
|
+
const old = nodeById.get(entry.id);
|
|
197
|
+
const next = renderRow(entry);
|
|
198
|
+
nodeById.set(entry.id, next);
|
|
199
|
+
if (old && old.parentNode) old.replaceWith(next);
|
|
200
|
+
else if (listEl) listEl.appendChild(next);
|
|
201
|
+
if (listEl) listEl.scrollTop = listEl.scrollHeight;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateTextRow(entry) {
|
|
205
|
+
const row = nodeById.get(entry.id);
|
|
206
|
+
if (!row) return;
|
|
207
|
+
row.textContent = "";
|
|
208
|
+
row.appendChild(el("span", "g", "✦"));
|
|
209
|
+
row.appendChild(el("span", "lab", rowLabel(entry)));
|
|
210
|
+
if (listEl) listEl.scrollTop = listEl.scrollHeight;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- public API ----
|
|
214
|
+
|
|
215
|
+
export function sidebarSessionChanged(sid) {
|
|
216
|
+
const prev = sbEl && sbEl.dataset.sid;
|
|
217
|
+
if (prev === (sid || "")) return;
|
|
218
|
+
if (sbEl) sbEl.dataset.sid = sid || "";
|
|
219
|
+
trace = [];
|
|
220
|
+
callIndex.clear();
|
|
221
|
+
textEntry = null;
|
|
222
|
+
turn = 0;
|
|
223
|
+
seq = 0;
|
|
224
|
+
collapseDetail();
|
|
225
|
+
renderList();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function sidebarTurnEnd() {
|
|
229
|
+
turn += 1;
|
|
230
|
+
textEntry = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function sidebarTraceEvent(kind, data) {
|
|
234
|
+
if (!data) return;
|
|
235
|
+
switch (kind) {
|
|
236
|
+
case "tool_start": {
|
|
237
|
+
const entry = append({ kind: "tool", name: data.name, args: data.args || {}, toolCallId: data.toolCallId });
|
|
238
|
+
if (data.toolCallId != null) callIndex.set(data.toolCallId, entry);
|
|
239
|
+
pushRow(entry);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "tool_end": {
|
|
243
|
+
const entry = data.toolCallId != null ? callIndex.get(data.toolCallId) : undefined;
|
|
244
|
+
if (data.toolCallId != null) callIndex.delete(data.toolCallId);
|
|
245
|
+
if (entry) {
|
|
246
|
+
entry.kind = data.success === false ? "tool_fail" : "tool";
|
|
247
|
+
entry.success = data.success !== false;
|
|
248
|
+
entry.durationMs = data.durationMs;
|
|
249
|
+
entry.output = data.output || "";
|
|
250
|
+
replaceRow(entry);
|
|
251
|
+
} else {
|
|
252
|
+
const e2 = append({
|
|
253
|
+
kind: data.success === false ? "tool_fail" : "tool",
|
|
254
|
+
name: data.name, args: data.args || {}, toolCallId: data.toolCallId,
|
|
255
|
+
success: data.success !== false, durationMs: data.durationMs, output: data.output || "",
|
|
256
|
+
});
|
|
257
|
+
pushRow(e2);
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case "phase":
|
|
262
|
+
if (data.phase !== "done") pushRow(append({ kind: "phase", phase: data.phase }));
|
|
263
|
+
break;
|
|
264
|
+
case "plan":
|
|
265
|
+
pushRow(append({
|
|
266
|
+
kind: "plan",
|
|
267
|
+
steps: Array.isArray(data.steps) ? data.steps : [],
|
|
268
|
+
currentIndex: typeof data.currentIndex === "number" ? data.currentIndex : 0,
|
|
269
|
+
}));
|
|
270
|
+
break;
|
|
271
|
+
case "question":
|
|
272
|
+
pushRow(append({ kind: "question", text: data.question || data.text || "" }));
|
|
273
|
+
break;
|
|
274
|
+
case "text": {
|
|
275
|
+
if (!textEntry) { textEntry = append({ kind: "text", text: "", chars: 0 }); pushRow(textEntry); }
|
|
276
|
+
const chunk = data.chunk || "";
|
|
277
|
+
textEntry.chars += chunk.length;
|
|
278
|
+
textEntry.text += chunk;
|
|
279
|
+
if (textEntry.text.length > 4000) textEntry.text = textEntry.text.slice(-4000);
|
|
280
|
+
updateTextRow(textEntry);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "error":
|
|
284
|
+
pushRow(append({ kind: "error", message: data.message || data.error || "" }));
|
|
285
|
+
break;
|
|
286
|
+
default:
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function detailGroup(title, bodyNode) {
|
|
292
|
+
const g = el("div");
|
|
293
|
+
g.appendChild(el("h5", "", title));
|
|
294
|
+
g.appendChild(bodyNode);
|
|
295
|
+
return g;
|
|
296
|
+
}
|
|
297
|
+
function kv(label, value) {
|
|
298
|
+
const row = el("div", "kv");
|
|
299
|
+
row.appendChild(el("span", "dim", label));
|
|
300
|
+
const b = el("b");
|
|
301
|
+
b.textContent = value;
|
|
302
|
+
row.appendChild(b);
|
|
303
|
+
return row;
|
|
304
|
+
}
|
|
305
|
+
function codeBlock(text) {
|
|
306
|
+
const c = el("div", "code");
|
|
307
|
+
c.textContent = text;
|
|
308
|
+
return c;
|
|
309
|
+
}
|
|
310
|
+
function capOutput(text) {
|
|
311
|
+
if (typeof text !== "string") return "";
|
|
312
|
+
return text.length <= MAX_OUTPUT_CHARS ? text : text.slice(0, MAX_OUTPUT_CHARS) + "\n… [truncated from " + text.length + " chars]";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function renderDetail(entry) {
|
|
316
|
+
if (!detailEl) return;
|
|
317
|
+
detailEl.hidden = false;
|
|
318
|
+
const warn = isWarn(entry);
|
|
319
|
+
detailGlyph.textContent = warn ? "⚠" : entry.kind === "question" ? "?" : "◇";
|
|
320
|
+
detailGlyph.style.color = warn ? "var(--yellow)" : "var(--accent)";
|
|
321
|
+
detailTitle.textContent = rowLabel(entry);
|
|
322
|
+
detailBody.textContent = "";
|
|
323
|
+
|
|
324
|
+
const meta = el("div");
|
|
325
|
+
meta.appendChild(el("h5", "", t("web.sidebar.detail_metadata")));
|
|
326
|
+
const d = entry.ts ? new Date(entry.ts) : null;
|
|
327
|
+
if (entry.kind === "tool" || entry.kind === "tool_fail") {
|
|
328
|
+
meta.appendChild(kv(t("web.sidebar.detail_status"), entry.success === false ? t("web.sidebar.detail_fail") : t("web.sidebar.detail_ok")));
|
|
329
|
+
if (entry.durationMs) meta.appendChild(kv(t("web.sidebar.detail_duration"), formatMs(entry.durationMs)));
|
|
330
|
+
}
|
|
331
|
+
if (d) meta.appendChild(kv(t("web.sidebar.detail_time"), d.toLocaleTimeString()));
|
|
332
|
+
if (entry.toolCallId) meta.appendChild(kv(t("web.sidebar.detail_tool_call"), String(entry.toolCallId)));
|
|
333
|
+
if (sbEl && sbEl.dataset.sid) meta.appendChild(kv(t("web.sidebar.detail_session"), sbEl.dataset.sid));
|
|
334
|
+
detailBody.appendChild(meta);
|
|
335
|
+
|
|
336
|
+
if (entry.kind === "tool" || entry.kind === "tool_fail") {
|
|
337
|
+
detailBody.appendChild(detailGroup(t("web.sidebar.detail_arguments"), codeBlock(JSON.stringify(entry.args || {}, null, 2))));
|
|
338
|
+
detailBody.appendChild(detailGroup(t("web.sidebar.detail_output"), codeBlock(capOutput(entry.output))));
|
|
339
|
+
} else if (entry.kind === "text") {
|
|
340
|
+
detailBody.appendChild(detailGroup(t("web.sidebar.detail_text"), codeBlock(entry.text || "")));
|
|
341
|
+
} else if (entry.kind === "plan") {
|
|
342
|
+
const list = el("div");
|
|
343
|
+
(entry.steps || []).forEach((s) => {
|
|
344
|
+
const mark = s.status === "done" ? "✓" : s.status === "failed" ? "✗" : s.status === "in_progress" ? "▸" : "○";
|
|
345
|
+
const cls = s.status === "failed" ? "fail" : s.status === "in_progress" ? "cur" : s.status === "done" ? "done" : "";
|
|
346
|
+
list.appendChild(el("div", ("plan-step " + cls).trim(), mark + " " + s.text));
|
|
347
|
+
});
|
|
348
|
+
detailBody.appendChild(detailGroup(t("web.sidebar.detail_plan"), list));
|
|
349
|
+
} else if (entry.kind === "question") {
|
|
350
|
+
detailBody.appendChild(detailGroup(t("web.sidebar.trace_question"), codeBlock(entry.text || "")));
|
|
351
|
+
} else if (entry.kind === "error") {
|
|
352
|
+
detailBody.appendChild(detailGroup(t("web.sidebar.detail_error"), codeBlock(entry.message || "")));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---- Map tab ----
|
|
357
|
+
|
|
358
|
+
function fmtUpdated(updatedAt) {
|
|
359
|
+
if (typeof updatedAt !== "number") return t("web.sidebar.map_never");
|
|
360
|
+
const d = new Date(updatedAt);
|
|
361
|
+
const now = new Date();
|
|
362
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
363
|
+
const time = pad(d.getHours()) + ":" + pad(d.getMinutes());
|
|
364
|
+
if (d.toDateString() === now.toDateString()) return t("web.sidebar.map_today", { time });
|
|
365
|
+
return pad(d.getDate()) + "." + pad(d.getMonth() + 1) + " " + time;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function mapKv(label, value, cls) {
|
|
369
|
+
const row = el("div", ("kv" + (cls ? " " + cls : "")).trim());
|
|
370
|
+
row.appendChild(el("span", "dim", label));
|
|
371
|
+
const b = el("b");
|
|
372
|
+
b.textContent = value;
|
|
373
|
+
row.appendChild(b);
|
|
374
|
+
return row;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let reindexing = false; // клиентский спиннер до свежего снапшота
|
|
378
|
+
let postResolved = false; // POST вернулся; следующий снапшот снимет спиннер
|
|
379
|
+
let reindexError = false; // последний POST не удался
|
|
380
|
+
|
|
381
|
+
export function sidebarMapState(host) {
|
|
382
|
+
if (!mapEl) return;
|
|
383
|
+
const map = (host && host.projectMap) || { indexed: false, files: 0, symbols: 0, tokens: 0, languages: [] };
|
|
384
|
+
const serverIndexing = !!(map && map.indexing);
|
|
385
|
+
if (postResolved && !serverIndexing) { reindexing = false; postResolved = false; }
|
|
386
|
+
const indexing = serverIndexing || reindexing;
|
|
387
|
+
const num = (v) => (map.indexed ? String(v || 0) : "—");
|
|
388
|
+
mapEl.textContent = "";
|
|
389
|
+
const status = indexing ? t("web.sidebar.map_indexing")
|
|
390
|
+
: !map.indexed ? t("web.sidebar.map_never")
|
|
391
|
+
: (map.watcher ? t("web.sidebar.map_ready") : t("web.sidebar.map_stale"));
|
|
392
|
+
mapEl.appendChild(mapKv(t("web.sidebar.map_status"), status));
|
|
393
|
+
mapEl.appendChild(mapKv(t("web.sidebar.map_updated"), fmtUpdated(map.updatedAt)));
|
|
394
|
+
mapEl.appendChild(mapKv(t("web.sidebar.map_watcher"), map.watcher ? t("web.sidebar.map_on") : t("web.sidebar.map_off")));
|
|
395
|
+
mapEl.appendChild(mapKv(t("web.sidebar.map_files"), num(map.files)));
|
|
396
|
+
mapEl.appendChild(mapKv(t("web.sidebar.map_symbols"), num(map.symbols)));
|
|
397
|
+
mapEl.appendChild(mapKv(t("web.sidebar.map_tokens"), map.indexed ? fmtTokens(map.tokens || 0) : "—"));
|
|
398
|
+
if (host && host.workspace) mapEl.appendChild(mapKv(t("web.sidebar.map_root"), host.workspace));
|
|
399
|
+
|
|
400
|
+
if (Array.isArray(map.languages) && map.languages.length > 0) {
|
|
401
|
+
const head = el("div", "kv");
|
|
402
|
+
head.appendChild(el("span", "dim", t("web.sidebar.map_languages")));
|
|
403
|
+
mapEl.appendChild(head);
|
|
404
|
+
map.languages.forEach((l) => mapEl.appendChild(mapKv(l.language, l.files + " · " + l.symbols)));
|
|
405
|
+
}
|
|
406
|
+
if (reindexError) mapEl.appendChild(mapKv(t("web.sidebar.map_reindex_failed"), "—", "wn"));
|
|
407
|
+
|
|
408
|
+
const btn = el("button", "map-reindex", indexing ? t("web.sidebar.map_reindexing") : t("web.sidebar.map_reindex"));
|
|
409
|
+
btn.type = "button";
|
|
410
|
+
btn.disabled = indexing || !map.indexed;
|
|
411
|
+
btn.addEventListener("click", async () => {
|
|
412
|
+
reindexing = true;
|
|
413
|
+
postResolved = false;
|
|
414
|
+
reindexError = false;
|
|
415
|
+
sidebarMapState(host);
|
|
416
|
+
sidebarMapIndicator(host);
|
|
417
|
+
const r = await api("/api/index", { method: "POST" });
|
|
418
|
+
if (!r.ok) {
|
|
419
|
+
reindexing = false;
|
|
420
|
+
postResolved = false;
|
|
421
|
+
reindexError = true;
|
|
422
|
+
sidebarMapState(host);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Свежий снапшот (поллинг / `state`-broadcast по завершении) снимет спиннер.
|
|
426
|
+
postResolved = true;
|
|
427
|
+
});
|
|
428
|
+
mapEl.appendChild(btn);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---- indicators ----
|
|
432
|
+
|
|
433
|
+
export function sidebarBusy(on) {
|
|
434
|
+
const tab = sbEl && sbEl.querySelector('.sb-tab[data-tab="trace"]');
|
|
435
|
+
if (tab) tab.dataset.indicator = on ? "live" : "";
|
|
436
|
+
}
|
|
437
|
+
export function sidebarMapIndicator(host) {
|
|
438
|
+
const tab = sbEl && sbEl.querySelector('.sb-tab[data-tab="map"]');
|
|
439
|
+
if (!tab) return;
|
|
440
|
+
const map = (host && host.projectMap) || null;
|
|
441
|
+
const busy = (map && map.indexing) || reindexing;
|
|
442
|
+
const next = busy ? "busy" : map && map.indexed && !map.watcher ? "stale" : "";
|
|
443
|
+
tab.dataset.indicator = next;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---- init / i18n ----
|
|
447
|
+
|
|
448
|
+
export function applySidebarI18n() {
|
|
449
|
+
if (!sbEl) return;
|
|
450
|
+
TABS.forEach((id) => {
|
|
451
|
+
const tab = sbEl.querySelector('.sb-tab[data-tab="' + id + '"]');
|
|
452
|
+
if (tab) { const label = t("web.sidebar.tab_" + id); tab.title = label; tab.setAttribute("aria-label", label); }
|
|
453
|
+
});
|
|
454
|
+
setSidebarTab(active, false);
|
|
455
|
+
renderList();
|
|
456
|
+
if (selected) {
|
|
457
|
+
const entry = trace.find((x) => x.id === selected);
|
|
458
|
+
if (entry) renderDetail(entry);
|
|
459
|
+
}
|
|
460
|
+
sidebarMapState(S.state.host);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function initSidebar() {
|
|
464
|
+
if (sbEl) return;
|
|
465
|
+
sbEl = document.querySelector(".sb");
|
|
466
|
+
if (!sbEl) return;
|
|
467
|
+
nameEl = document.getElementById("sbName");
|
|
468
|
+
nameTextEl = document.getElementById("sbNameText");
|
|
469
|
+
mapEl = document.getElementById("mapBody");
|
|
470
|
+
listEl = document.getElementById("traceList");
|
|
471
|
+
detailEl = document.getElementById("traceDetail");
|
|
472
|
+
detailGlyph = document.getElementById("detailGlyph");
|
|
473
|
+
detailTitle = document.getElementById("detailTitle");
|
|
474
|
+
detailBody = document.getElementById("detailBody");
|
|
475
|
+
detailClose = document.getElementById("detailClose");
|
|
476
|
+
if (detailClose) detailClose.addEventListener("click", collapseDetail);
|
|
477
|
+
|
|
478
|
+
sbEl.querySelectorAll(".sb-tab .sb-ico").forEach((span) => {
|
|
479
|
+
const id = span.closest(".sb-tab").dataset.tab;
|
|
480
|
+
span.textContent = "";
|
|
481
|
+
span.appendChild(icon(ICON_NAME[id]));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
sbEl.querySelectorAll(".sb-tab").forEach((tab) => {
|
|
485
|
+
tab.addEventListener("click", () => setSidebarTab(tab.dataset.tab));
|
|
486
|
+
tab.addEventListener("keydown", (e) => {
|
|
487
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
const i = TABS.indexOf(tab.dataset.tab);
|
|
490
|
+
const next = TABS[(i + (e.key === "ArrowRight" ? 1 : TABS.length - 1)) % TABS.length];
|
|
491
|
+
setSidebarTab(next);
|
|
492
|
+
const focus = sbEl.querySelector('.sb-tab[data-tab="' + next + '"]');
|
|
493
|
+
if (focus) focus.focus();
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
document.addEventListener("keydown", (e) => {
|
|
498
|
+
if (!e.altKey || !["1", "2", "3"].includes(e.key)) return;
|
|
499
|
+
const tag = document.activeElement ? String(document.activeElement.tagName) : "";
|
|
500
|
+
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
501
|
+
e.preventDefault();
|
|
502
|
+
setSidebarTab(TABS[Number(e.key) - 1]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
document.addEventListener("keydown", (e) => {
|
|
506
|
+
if (e.key === "Escape" && selected) collapseDetail();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const stored = readStored();
|
|
510
|
+
setSidebarTab(stored && TABS.includes(stored) ? stored : "session");
|
|
511
|
+
renderList();
|
|
512
|
+
}
|
package/dist/assets/js/tabs.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
makeFeed, showEmptyFeed, activeFeed, liveFeed, appendTo,
|
|
14
14
|
clearEmpty, showBusy, clearFeed, finishMd, resetTurnText,
|
|
15
15
|
finishThink, feedHasContent, loadHistory, syncUrl, renderPlan,
|
|
16
|
+
scrollToBottom,
|
|
16
17
|
} from "./feed.js";
|
|
17
18
|
import { openConfirm, setIndicator } from "./dialog.js";
|
|
18
19
|
import { setBusy, setPlan as railSetPlan } from "./rail.js";
|
|
@@ -122,7 +123,7 @@ export function openTab(id, name, kind, server) {
|
|
|
122
123
|
msgs.replaceChildren(tab.feed);
|
|
123
124
|
renderTabstrip();
|
|
124
125
|
renderSessionsDropdown();
|
|
125
|
-
|
|
126
|
+
scrollToBottom();
|
|
126
127
|
if (tab.kind === "session" && !server) {
|
|
127
128
|
// Auto-opened live session: SSE fills the feed, no transcript fetch.
|
|
128
129
|
tab.historyLoaded = true;
|
|
@@ -159,6 +160,7 @@ function closeTab(id) {
|
|
|
159
160
|
S.activeTabId = next.id;
|
|
160
161
|
S.liveTabId = next.id;
|
|
161
162
|
msgs.replaceChildren(next.feed);
|
|
163
|
+
scrollToBottom();
|
|
162
164
|
} else {
|
|
163
165
|
S.activeTabId = null;
|
|
164
166
|
S.liveTabId = null;
|
|
@@ -636,6 +638,7 @@ async function send() {
|
|
|
636
638
|
finishThink();
|
|
637
639
|
if (text.startsWith("/")) {
|
|
638
640
|
appendTo(feed, el("div", "u", text));
|
|
641
|
+
scrollToBottom();
|
|
639
642
|
clearInput();
|
|
640
643
|
const name = text.slice(1).split(/\s+/)[0].toLowerCase();
|
|
641
644
|
// `/clear` and `/new` never reach the server: both are pure browser-side
|
|
@@ -663,6 +666,7 @@ async function send() {
|
|
|
663
666
|
feed.insertBefore(banner, feed.firstChild);
|
|
664
667
|
}
|
|
665
668
|
appendTo(feed, userMessageNode(text, images));
|
|
669
|
+
scrollToBottom();
|
|
666
670
|
clearInput();
|
|
667
671
|
const body = { text };
|
|
668
672
|
if (images.length) body.images = images;
|
|
@@ -672,7 +676,9 @@ async function send() {
|
|
|
672
676
|
appendTo(feed, el("div", "warnline", "⚠ " + msg));
|
|
673
677
|
} else {
|
|
674
678
|
if (images.length) clearAttachments();
|
|
675
|
-
showBusy
|
|
679
|
+
// Delay the spinner so fast first tokens never flash it: `showBusy`
|
|
680
|
+
// itself skips when a stream/think block already started.
|
|
681
|
+
window.setTimeout(() => showBusy(feed), 180);
|
|
676
682
|
}
|
|
677
683
|
}
|
|
678
684
|
|