micro-models-agent 2.15.0 → 2.16.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 +27 -0
- package/dist/assets/app.js +6 -2
- package/dist/assets/index.html +1 -0
- package/dist/assets/js/feed.js +67 -51
- package/dist/assets/js/format.js +8 -0
- package/dist/assets/js/settings.js +74 -8
- package/dist/assets/js/sidebar.js +198 -5
- package/dist/assets/js/tabs.js +105 -18
- package/dist/assets/md.js +14 -0
- package/dist/assets/styles.css +79 -30
- package/dist/i18n/en.json +20 -1
- package/dist/i18n/ru.json +20 -1
- package/dist/main.js +355 -134
- package/package.json +1 -1
|
@@ -25,6 +25,8 @@ let trace = []; // { id, kind, ts, name, args, output, success, duratio
|
|
|
25
25
|
let selected = null; // trace id
|
|
26
26
|
let turn = 0;
|
|
27
27
|
let seq = 0;
|
|
28
|
+
let logCursor = null; // max session-log ts already pulled (supplement fetch)
|
|
29
|
+
let logPulling = false;
|
|
28
30
|
const nodeById = new Map(); // entry id → row node
|
|
29
31
|
const callIndex = new Map(); // toolCallId → entry
|
|
30
32
|
const headerByTurn = new Map(); // turn → header node
|
|
@@ -48,6 +50,7 @@ export function setSidebarTab(id, persist = true) {
|
|
|
48
50
|
if (!TABS.includes(id)) id = "session";
|
|
49
51
|
if (persist) storeTab(id);
|
|
50
52
|
const effective = id === "trace" && mqMobile() ? "session" : id;
|
|
53
|
+
if (effective !== active) collapseDetail();
|
|
51
54
|
active = effective;
|
|
52
55
|
TABS.forEach((name) => {
|
|
53
56
|
const pane = paneEl(name);
|
|
@@ -90,6 +93,18 @@ function rowLabel(entry) {
|
|
|
90
93
|
case "plan": return t("web.sidebar.trace_plan");
|
|
91
94
|
case "question": return t("web.sidebar.trace_question");
|
|
92
95
|
case "text": return t("web.sidebar.trace_text", { count: entry.chars || 0 });
|
|
96
|
+
case "status": return t("web.sidebar.trace_status");
|
|
97
|
+
case "log": return t("web.sidebar.trace_log", { level: entry.level || "" });
|
|
98
|
+
case "retract": return t("web.sidebar.trace_retract");
|
|
99
|
+
case "index": return t("web.sidebar.trace_index", { phase: entry.phase || "" });
|
|
100
|
+
case "critic": return t("web.sidebar.trace_critic", { status: entry.status || "" });
|
|
101
|
+
case "moe": return t("web.sidebar.trace_moe");
|
|
102
|
+
case "compaction": return t("web.sidebar.trace_compaction");
|
|
103
|
+
case "probe": return t("web.sidebar.trace_probe");
|
|
104
|
+
case "usage": return t("web.sidebar.trace_usage");
|
|
105
|
+
case "audit": return t("web.sidebar.trace_audit");
|
|
106
|
+
case "claim": return t("web.sidebar.trace_claim", { kind: entry.subkind || "" });
|
|
107
|
+
case "output": return t("web.sidebar.trace_output");
|
|
93
108
|
default: return labelOf(entry.name);
|
|
94
109
|
}
|
|
95
110
|
}
|
|
@@ -103,11 +118,19 @@ function rowArgs(entry) {
|
|
|
103
118
|
return t("web.rail.step_of", { cur: Math.min(idx + 1, total), total });
|
|
104
119
|
}
|
|
105
120
|
if (entry.kind === "question") return entry.text || "";
|
|
121
|
+
// Диагностические строки несут смысл в одной строке (статус/лог/ретракт/
|
|
122
|
+
// индекс/критик/MoE/компакция/проба/usage/аудит/claim/вывод).
|
|
123
|
+
if (entry.text && typeof entry.text === "string" && entry.kind !== "tool" && entry.kind !== "tool_fail") {
|
|
124
|
+
const line = entry.text.split("\n").map((l) => l.trim()).find((l) => l.length > 0) || "";
|
|
125
|
+
return line.slice(0, 160);
|
|
126
|
+
}
|
|
106
127
|
return argText(entry.args);
|
|
107
128
|
}
|
|
108
129
|
|
|
109
130
|
function isWarn(entry) {
|
|
110
|
-
return entry.kind === "tool_fail" || entry.kind === "error"
|
|
131
|
+
return entry.kind === "tool_fail" || entry.kind === "error" || entry.kind === "claim" ||
|
|
132
|
+
(entry.kind === "critic" && entry.status === "critical") ||
|
|
133
|
+
(entry.kind === "log" && (entry.level === "warn" || entry.level === "error"));
|
|
111
134
|
}
|
|
112
135
|
|
|
113
136
|
function renderRow(entry) {
|
|
@@ -221,12 +244,16 @@ export function sidebarSessionChanged(sid) {
|
|
|
221
244
|
textEntry = null;
|
|
222
245
|
turn = 0;
|
|
223
246
|
seq = 0;
|
|
247
|
+
// Trace живой: старые ходы чужой сессии не докачиваем, только новые события.
|
|
248
|
+
logCursor = new Date().toISOString();
|
|
249
|
+
logPulling = false;
|
|
224
250
|
collapseDetail();
|
|
225
251
|
renderList();
|
|
226
252
|
}
|
|
227
253
|
|
|
228
|
-
export function sidebarTurnEnd() {
|
|
229
|
-
|
|
254
|
+
export function sidebarTurnEnd(turnSeq) {
|
|
255
|
+
// Серверный номер хода (реконнекты локальный счётчик не сбивают).
|
|
256
|
+
turn = typeof turnSeq === "number" && turnSeq > turn ? turnSeq : turn + 1;
|
|
230
257
|
textEntry = null;
|
|
231
258
|
}
|
|
232
259
|
|
|
@@ -276,13 +303,118 @@ export function sidebarTraceEvent(kind, data) {
|
|
|
276
303
|
const chunk = data.chunk || "";
|
|
277
304
|
textEntry.chars += chunk.length;
|
|
278
305
|
textEntry.text += chunk;
|
|
279
|
-
|
|
306
|
+
// Полный текст хода — в память (деталь покажет целиком); обрезка честная,
|
|
307
|
+
// тем же капом и маркером, что у выводов тулов.
|
|
308
|
+
if (textEntry.text.length > MAX_OUTPUT_CHARS) {
|
|
309
|
+
textEntry.text = textEntry.text.slice(0, MAX_OUTPUT_CHARS) +
|
|
310
|
+
"\n… [truncated from " + textEntry.chars + " chars]";
|
|
311
|
+
}
|
|
280
312
|
updateTextRow(textEntry);
|
|
281
313
|
break;
|
|
282
314
|
}
|
|
283
315
|
case "error":
|
|
284
316
|
pushRow(append({ kind: "error", message: data.message || data.error || "" }));
|
|
285
317
|
break;
|
|
318
|
+
case "meta": {
|
|
319
|
+
// Тела тулов (`kind: "tool"`) уже идут строками tool_start/tool_end —
|
|
320
|
+
// в Trace несём только статусы (stuck/escalation/honest-stop и т.п.).
|
|
321
|
+
if (data.kind !== undefined && data.kind !== "status") break;
|
|
322
|
+
const text = data.chunk || "";
|
|
323
|
+
if (!text.trim()) break;
|
|
324
|
+
pushRow(append({ kind: "status", text }));
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case "log": {
|
|
328
|
+
// Внутренний шёпот петли (`diagnostic` + info) в Trace не несём —
|
|
329
|
+
// только варнинги/ошибки и пользовательские info (probe/overflow).
|
|
330
|
+
if (data.diagnostic && data.level === "info") break;
|
|
331
|
+
const text = data.text || "";
|
|
332
|
+
if (!text.trim()) break;
|
|
333
|
+
pushRow(append({ kind: "log", text, level: data.level || "", source: data.source || "" }));
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
case "retract":
|
|
337
|
+
pushRow(append({ kind: "retract", text: data.reason || "" }));
|
|
338
|
+
break;
|
|
339
|
+
case "index": {
|
|
340
|
+
const stats = data.stats && typeof data.stats === "object"
|
|
341
|
+
? Object.entries(data.stats).map(([k, v]) => k + ": " + v).join(", ")
|
|
342
|
+
: "";
|
|
343
|
+
pushRow(append({ kind: "index", phase: data.phase || "", text: stats }));
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
default:
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Докачка диагностических событий из session-лога (критик, MoE, компакция,
|
|
353
|
+
* проба контекста, usage, аудит, claim-retry, вывод). Отдельных SSE-продюсеров
|
|
354
|
+
* нет — данные уже лежат в логе с `iteration`; тянем инкремент по курсору на
|
|
355
|
+
* каждый `turn_end`. Живые tool/text-строки не дублируются (другие kinds).
|
|
356
|
+
*/
|
|
357
|
+
export async function sidebarPullLog() {
|
|
358
|
+
const sid = sbEl && sbEl.dataset.sid;
|
|
359
|
+
if (!sid || logPulling) return;
|
|
360
|
+
logPulling = true;
|
|
361
|
+
try {
|
|
362
|
+
let url = "/api/sessions/" + encodeURIComponent(sid) + "/log?limit=100";
|
|
363
|
+
if (logCursor) url += "&since=" + encodeURIComponent(logCursor);
|
|
364
|
+
const r = await api(url);
|
|
365
|
+
if (!r.ok || !r.data || !Array.isArray(r.data.log)) return;
|
|
366
|
+
for (const e of r.data.log) {
|
|
367
|
+
if (typeof e.ts === "string" && (logCursor === null || e.ts > logCursor)) logCursor = e.ts;
|
|
368
|
+
appendLogEntry(e);
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
/* best-effort supplement: Trace живёт и без докачки */
|
|
372
|
+
} finally {
|
|
373
|
+
logPulling = false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function appendLogEntry(e) {
|
|
378
|
+
if (!e || typeof e.type !== "string") return;
|
|
379
|
+
switch (e.type) {
|
|
380
|
+
case "critic_check":
|
|
381
|
+
pushRow(append({ kind: "critic", status: "check", text: "", iteration: e.iteration }));
|
|
382
|
+
break;
|
|
383
|
+
case "critic_verdict":
|
|
384
|
+
pushRow(append({ kind: "critic", status: e.status || "", text: e.content || "", iteration: e.iteration }));
|
|
385
|
+
break;
|
|
386
|
+
case "critic_escalation":
|
|
387
|
+
pushRow(append({ kind: "critic", status: "escalation", text: e.content || "", iteration: e.iteration }));
|
|
388
|
+
break;
|
|
389
|
+
case "moe_plan":
|
|
390
|
+
case "moe_subtask":
|
|
391
|
+
case "moe_replan":
|
|
392
|
+
case "moe_verify": {
|
|
393
|
+
const bits = [e.title, e.subtaskId, e.expertTag, e.status].filter((x) => typeof x === "string" && x).join(" · ");
|
|
394
|
+
pushRow(append({ kind: "moe", subtype: e.type, text: bits, iteration: e.iteration }));
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case "compaction":
|
|
398
|
+
pushRow(append({ kind: "compaction", text: e.content || "", iteration: e.iteration }));
|
|
399
|
+
break;
|
|
400
|
+
case "context_probe":
|
|
401
|
+
pushRow(append({ kind: "probe", text: e.content || "" }));
|
|
402
|
+
break;
|
|
403
|
+
case "llm_usage": {
|
|
404
|
+
const toks = typeof e.totalTokens === "number" ? e.totalTokens : (e.promptTokens || 0) + (e.completionTokens || 0);
|
|
405
|
+
const who = [e.model || e.provider, toks ? toks + " tok" : ""].filter(Boolean).join(" · ");
|
|
406
|
+
pushRow(append({ kind: "usage", text: who, iteration: e.iteration }));
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case "audit":
|
|
410
|
+
pushRow(append({ kind: "audit", text: e.content || "", iteration: e.iteration }));
|
|
411
|
+
break;
|
|
412
|
+
case "claim_retry":
|
|
413
|
+
pushRow(append({ kind: "claim", subkind: e.kind || "", text: e.content || "", iteration: e.iteration }));
|
|
414
|
+
break;
|
|
415
|
+
case "output":
|
|
416
|
+
pushRow(append({ kind: "output", text: e.content || "", iteration: e.iteration }));
|
|
417
|
+
break;
|
|
286
418
|
default:
|
|
287
419
|
break;
|
|
288
420
|
}
|
|
@@ -331,6 +463,8 @@ function renderDetail(entry) {
|
|
|
331
463
|
if (d) meta.appendChild(kv(t("web.sidebar.detail_time"), d.toLocaleTimeString()));
|
|
332
464
|
if (entry.toolCallId) meta.appendChild(kv(t("web.sidebar.detail_tool_call"), String(entry.toolCallId)));
|
|
333
465
|
if (sbEl && sbEl.dataset.sid) meta.appendChild(kv(t("web.sidebar.detail_session"), sbEl.dataset.sid));
|
|
466
|
+
meta.appendChild(kv(t("web.sidebar.detail_turn"), String(entry.turn)));
|
|
467
|
+
if (typeof entry.iteration === "number") meta.appendChild(kv(t("web.sidebar.detail_iteration"), String(entry.iteration)));
|
|
334
468
|
detailBody.appendChild(meta);
|
|
335
469
|
|
|
336
470
|
if (entry.kind === "tool" || entry.kind === "tool_fail") {
|
|
@@ -338,6 +472,7 @@ function renderDetail(entry) {
|
|
|
338
472
|
detailBody.appendChild(detailGroup(t("web.sidebar.detail_output"), codeBlock(capOutput(entry.output))));
|
|
339
473
|
} else if (entry.kind === "text") {
|
|
340
474
|
detailBody.appendChild(detailGroup(t("web.sidebar.detail_text"), codeBlock(entry.text || "")));
|
|
475
|
+
loadTextProvenance(entry);
|
|
341
476
|
} else if (entry.kind === "plan") {
|
|
342
477
|
const list = el("div");
|
|
343
478
|
(entry.steps || []).forEach((s) => {
|
|
@@ -350,6 +485,52 @@ function renderDetail(entry) {
|
|
|
350
485
|
detailBody.appendChild(detailGroup(t("web.sidebar.trace_question"), codeBlock(entry.text || "")));
|
|
351
486
|
} else if (entry.kind === "error") {
|
|
352
487
|
detailBody.appendChild(detailGroup(t("web.sidebar.detail_error"), codeBlock(entry.message || "")));
|
|
488
|
+
} else if (
|
|
489
|
+
entry.kind === "status" || entry.kind === "log" || entry.kind === "retract" ||
|
|
490
|
+
entry.kind === "index" || entry.kind === "critic" || entry.kind === "moe" ||
|
|
491
|
+
entry.kind === "compaction" || entry.kind === "probe" || entry.kind === "usage" ||
|
|
492
|
+
entry.kind === "audit" || entry.kind === "claim" || entry.kind === "output"
|
|
493
|
+
) {
|
|
494
|
+
const title =
|
|
495
|
+
entry.kind === "moe" && entry.subtype ? entry.subtype
|
|
496
|
+
: entry.kind === "log" && entry.level ? "log · " + entry.level
|
|
497
|
+
: entry.kind === "index" && entry.phase ? "index · " + entry.phase
|
|
498
|
+
: entry.kind === "critic" && entry.status ? "critic · " + entry.status
|
|
499
|
+
: entry.kind === "claim" && entry.subkind ? "claim_retry · " + entry.subkind
|
|
500
|
+
: rowLabel(entry);
|
|
501
|
+
detailBody.appendChild(detailGroup(title, codeBlock(entry.text || "")));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Привязка текста хода к session-логу: сколько assistant-сообщений в сессии,
|
|
507
|
+
* их итерации и провенанс последнего LLM-вызова (модель/провайдер/токены).
|
|
508
|
+
* Живой текст хода уже показан выше целиком; это — сверка с логом.
|
|
509
|
+
*/
|
|
510
|
+
async function loadTextProvenance(entry) {
|
|
511
|
+
const sid = sbEl && sbEl.dataset.sid;
|
|
512
|
+
if (!sid || !detailBody || selected !== entry.id) return;
|
|
513
|
+
try {
|
|
514
|
+
const r = await api(
|
|
515
|
+
"/api/sessions/" + encodeURIComponent(sid) + "/log?types=assistant,llm_usage&limit=20",
|
|
516
|
+
);
|
|
517
|
+
if (!r.ok || !r.data || !Array.isArray(r.data.log) || selected !== entry.id) return;
|
|
518
|
+
const assistants = r.data.log.filter((e) => e.type === "assistant");
|
|
519
|
+
const usages = r.data.log.filter((e) => e.type === "llm_usage");
|
|
520
|
+
const iters = assistants.map((e) => e.iteration).filter((n) => typeof n === "number");
|
|
521
|
+
const last = usages.length > 0 ? usages[usages.length - 1] : null;
|
|
522
|
+
const bits = [
|
|
523
|
+
t("web.sidebar.detail_provenance_count", { n: assistants.length }) +
|
|
524
|
+
(iters.length > 0 ? " · iter " + Math.min(...iters) + "–" + Math.max(...iters) : ""),
|
|
525
|
+
last
|
|
526
|
+
? [last.model || last.provider, last.totalTokens ?? (last.promptTokens || 0) + (last.completionTokens || 0)]
|
|
527
|
+
.filter(Boolean)
|
|
528
|
+
.join(" · ")
|
|
529
|
+
: "",
|
|
530
|
+
].filter(Boolean).join("\n");
|
|
531
|
+
if (bits) detailBody.appendChild(detailGroup(t("web.sidebar.detail_provenance"), codeBlock(bits)));
|
|
532
|
+
} catch {
|
|
533
|
+
/* best-effort: живой текст уже на месте */
|
|
353
534
|
}
|
|
354
535
|
}
|
|
355
536
|
|
|
@@ -374,6 +555,18 @@ function mapKv(label, value, cls) {
|
|
|
374
555
|
return row;
|
|
375
556
|
}
|
|
376
557
|
|
|
558
|
+
// Корень — всегда длинный путь: подпись сверху, путь снизу на всю ширину.
|
|
559
|
+
// В одну flex-строку с подписью он не влезает и склеивается с ней.
|
|
560
|
+
function mapPathRow(label, value) {
|
|
561
|
+
const wrap = el("div", "map-path");
|
|
562
|
+
wrap.appendChild(el("span", "dim", label));
|
|
563
|
+
const b = el("b");
|
|
564
|
+
b.textContent = value;
|
|
565
|
+
b.title = value;
|
|
566
|
+
wrap.appendChild(b);
|
|
567
|
+
return wrap;
|
|
568
|
+
}
|
|
569
|
+
|
|
377
570
|
let reindexing = false; // клиентский спиннер до свежего снапшота
|
|
378
571
|
let postResolved = false; // POST вернулся; следующий снапшот снимет спиннер
|
|
379
572
|
let reindexError = false; // последний POST не удался
|
|
@@ -395,7 +588,7 @@ export function sidebarMapState(host) {
|
|
|
395
588
|
mapEl.appendChild(mapKv(t("web.sidebar.map_files"), num(map.files)));
|
|
396
589
|
mapEl.appendChild(mapKv(t("web.sidebar.map_symbols"), num(map.symbols)));
|
|
397
590
|
mapEl.appendChild(mapKv(t("web.sidebar.map_tokens"), map.indexed ? fmtTokens(map.tokens || 0) : "—"));
|
|
398
|
-
if (host && host.workspace) mapEl.appendChild(
|
|
591
|
+
if (host && host.workspace) mapEl.appendChild(mapPathRow(t("web.sidebar.map_root"), host.workspace));
|
|
399
592
|
|
|
400
593
|
if (Array.isArray(map.languages) && map.languages.length > 0) {
|
|
401
594
|
const head = el("div", "kv");
|
package/dist/assets/js/tabs.js
CHANGED
|
@@ -8,7 +8,7 @@ el, icon,
|
|
|
8
8
|
import { api } from "./api.js";
|
|
9
9
|
import { t } from "./i18n.js";
|
|
10
10
|
import { S, MAX_IMAGES, MAX_IMAGE_BYTES } from "./store.js";
|
|
11
|
-
import { relTime, contextLabel } from "./format.js";
|
|
11
|
+
import { relTime, contextLabel, matchesQuery } from "./format.js";
|
|
12
12
|
import {
|
|
13
13
|
makeFeed, showEmptyFeed, activeFeed, liveFeed, appendTo,
|
|
14
14
|
clearEmpty, showBusy, clearFeed, finishMd, resetTurnText,
|
|
@@ -204,18 +204,70 @@ document.getElementById("sessRefresh").addEventListener("click", () => {
|
|
|
204
204
|
loadSessions();
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
// Live filter text of the sessions dropdown. Module-level so the 2s
|
|
208
|
+
// `renderState` poll and the 15s `loadSessions` poll (both re-render the lists)
|
|
209
|
+
// never wipe an in-progress query: the `<input id="sessSearch">` itself lives
|
|
210
|
+
// statically in `index.html` and only the row lists are redrawn.
|
|
211
|
+
let sessQuery = "";
|
|
212
|
+
let sessSearchWired = false;
|
|
213
|
+
|
|
214
|
+
// One-time wiring for the static `#sessSearch` input: placeholder follows the
|
|
215
|
+
// locale on every render, listeners attach once. Returns the input or null.
|
|
216
|
+
function ensureSessSearch() {
|
|
217
|
+
const search = document.getElementById("sessSearch");
|
|
218
|
+
if (!search) return null;
|
|
219
|
+
search.placeholder = t("web.sessions.search");
|
|
220
|
+
search.setAttribute("aria-label", t("web.sessions.search"));
|
|
221
|
+
if (!sessSearchWired) {
|
|
222
|
+
sessSearchWired = true;
|
|
223
|
+
search.addEventListener("click", (e) => e.stopPropagation());
|
|
224
|
+
search.addEventListener("input", () => {
|
|
225
|
+
sessQuery = search.value;
|
|
226
|
+
renderSessionsDropdown();
|
|
227
|
+
// Re-render must not steal the caret: the input node itself survives
|
|
228
|
+
// (only the lists redraw), but re-query it to be safe.
|
|
229
|
+
const again = document.getElementById("sessSearch");
|
|
230
|
+
if (again && document.activeElement !== again) again.focus();
|
|
231
|
+
});
|
|
232
|
+
search.addEventListener("keydown", (e) => {
|
|
233
|
+
e.stopPropagation();
|
|
234
|
+
if (e.key === "Escape") {
|
|
235
|
+
if (search.value) {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
search.value = "";
|
|
238
|
+
sessQuery = "";
|
|
239
|
+
renderSessionsDropdown();
|
|
240
|
+
search.focus();
|
|
241
|
+
} else {
|
|
242
|
+
setSessOpen(false);
|
|
243
|
+
}
|
|
244
|
+
} else if (e.key === "ArrowDown") {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
const first = document.querySelector("#sessActiveList .si-row .dd-row-main, #sessRecentList .si-row .dd-row-main");
|
|
247
|
+
if (first) first.focus();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return search;
|
|
252
|
+
}
|
|
253
|
+
|
|
207
254
|
export function renderSessionsDropdown() {
|
|
255
|
+
ensureSessSearch();
|
|
208
256
|
const activeList = document.getElementById("sessActiveList");
|
|
209
257
|
const recentList = document.getElementById("sessRecentList");
|
|
258
|
+
const activeHeader = document.getElementById("sessActiveHeader");
|
|
259
|
+
const recentHeader = document.getElementById("sessRecentHeader");
|
|
210
260
|
activeList.textContent = "";
|
|
211
261
|
recentList.textContent = "";
|
|
262
|
+
const q = sessQuery.trim();
|
|
212
263
|
const activeId = S.state.activeSessionId;
|
|
213
264
|
const sorted = S.state.sessions.slice().sort((a, b) => {
|
|
214
265
|
const at = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
215
266
|
const bt = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
216
267
|
return bt - at;
|
|
217
|
-
});
|
|
268
|
+
}).filter((s) => matchesQuery((s.name || "") + " " + String(s.id || ""), q));
|
|
218
269
|
let activePlaced = false;
|
|
270
|
+
let recentPlaced = false;
|
|
219
271
|
sorted.forEach((s) => {
|
|
220
272
|
const isActive = s.id === activeId;
|
|
221
273
|
const row = sessionRow(s, isActive);
|
|
@@ -224,13 +276,27 @@ export function renderSessionsDropdown() {
|
|
|
224
276
|
activePlaced = true;
|
|
225
277
|
} else {
|
|
226
278
|
recentList.appendChild(row);
|
|
279
|
+
recentPlaced = true;
|
|
227
280
|
}
|
|
228
281
|
});
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
282
|
+
if (q) {
|
|
283
|
+
// Filtered view: empty sections hide with their headers; zero hits total
|
|
284
|
+
// show a single no-matches row instead of the unfiltered placeholders.
|
|
285
|
+
if (activeHeader) activeHeader.hidden = !activePlaced;
|
|
286
|
+
if (recentHeader) recentHeader.hidden = !recentPlaced;
|
|
287
|
+
if (!activePlaced && !recentPlaced) {
|
|
288
|
+
if (recentHeader) recentHeader.hidden = true;
|
|
289
|
+
recentList.appendChild(el("div", "si dim", t("web.settings.no_matches")));
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
if (activeHeader) activeHeader.hidden = false;
|
|
293
|
+
if (recentHeader) recentHeader.hidden = false;
|
|
294
|
+
if (!activePlaced) {
|
|
295
|
+
activeList.appendChild(el("div", "si", t("web.sessions.none_active")));
|
|
296
|
+
}
|
|
297
|
+
if (!recentPlaced) {
|
|
298
|
+
recentList.appendChild(el("div", "si", t("web.sessions.none")));
|
|
299
|
+
}
|
|
234
300
|
}
|
|
235
301
|
if (S.state.sessionsError) {
|
|
236
302
|
recentList.appendChild(el("div", "dd-err", S.state.sessionsError));
|
|
@@ -304,11 +370,28 @@ const sessBtn = document.getElementById("sessBtn");
|
|
|
304
370
|
export function setSessOpen(open) {
|
|
305
371
|
sessDD.classList.toggle("open", !!open);
|
|
306
372
|
sessBtn.setAttribute("aria-expanded", open ? "true" : "false");
|
|
373
|
+
if (!open && sessQuery) {
|
|
374
|
+
// Closing wipes the filter so the next opening starts from the full list.
|
|
375
|
+
sessQuery = "";
|
|
376
|
+
const search = document.getElementById("sessSearch");
|
|
377
|
+
if (search && search.value) {
|
|
378
|
+
search.value = "";
|
|
379
|
+
renderSessionsDropdown();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function focusSessSearch() {
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
const search = document.getElementById("sessSearch");
|
|
386
|
+
if (search) search.focus();
|
|
387
|
+
}, 0);
|
|
307
388
|
}
|
|
308
389
|
sessBtn.addEventListener("click", (e) => {
|
|
309
390
|
e.stopPropagation();
|
|
310
391
|
document.querySelectorAll("[data-sel]").forEach((s) => s.classList.remove("open"));
|
|
311
|
-
|
|
392
|
+
const willOpen = !sessDD.classList.contains("open");
|
|
393
|
+
setSessOpen(willOpen);
|
|
394
|
+
if (willOpen) focusSessSearch();
|
|
312
395
|
});
|
|
313
396
|
|
|
314
397
|
// --- mobile session sub-header -------------------------------------------
|
|
@@ -370,7 +453,9 @@ if (sessCloseBtn) sessCloseBtn.addEventListener("click", () => {
|
|
|
370
453
|
if (sessNameBtn) sessNameBtn.addEventListener("click", (e) => {
|
|
371
454
|
e.stopPropagation();
|
|
372
455
|
document.querySelectorAll("[data-sel]").forEach((s) => s.classList.remove("open"));
|
|
373
|
-
|
|
456
|
+
const willOpen = !sessDD.classList.contains("open");
|
|
457
|
+
setSessOpen(willOpen);
|
|
458
|
+
if (willOpen) focusSessSearch();
|
|
374
459
|
});
|
|
375
460
|
if (typeof window.matchMedia === "function") {
|
|
376
461
|
const narrowMq = window.matchMedia("(max-width: 760px)");
|
|
@@ -627,11 +712,8 @@ async function send() {
|
|
|
627
712
|
const images = S.attachments.slice();
|
|
628
713
|
if (!text && !images.length) return;
|
|
629
714
|
const feed = liveFeed() || activeFeed();
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (feed) appendTo(feed, el("div", "warnline", "⚠ " + t("web.input.busy")));
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
715
|
+
// No client-side busy gate: a message sent mid-turn is queued server-side
|
|
716
|
+
// (`202 {queued: n}`) and runs after the current turn, in order.
|
|
635
717
|
clearEmpty(feed);
|
|
636
718
|
finishMd();
|
|
637
719
|
resetTurnText();
|
|
@@ -672,13 +754,18 @@ async function send() {
|
|
|
672
754
|
if (images.length) body.images = images;
|
|
673
755
|
const r = await api("/api/message", { method: "POST", body });
|
|
674
756
|
if (!r.ok) {
|
|
675
|
-
const
|
|
757
|
+
const err = (r.data && r.data.error) || r.status;
|
|
758
|
+
const msg = err === "queue_full" ? t("web.input.queue_full") : err;
|
|
676
759
|
appendTo(feed, el("div", "warnline", "⚠ " + msg));
|
|
677
760
|
} else {
|
|
678
761
|
if (images.length) clearAttachments();
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
762
|
+
if (r.data && r.data.queued) {
|
|
763
|
+
appendTo(feed, el("div", "detail", "↳ " + t("web.input.queued", { n: r.data.queued })));
|
|
764
|
+
} else {
|
|
765
|
+
// Delay the spinner so fast first tokens never flash it: `showBusy`
|
|
766
|
+
// itself skips when a stream/think block already started.
|
|
767
|
+
window.setTimeout(() => showBusy(feed), 180);
|
|
768
|
+
}
|
|
682
769
|
}
|
|
683
770
|
}
|
|
684
771
|
|
package/dist/assets/md.js
CHANGED
|
@@ -198,6 +198,20 @@ function addText(data, text) {
|
|
|
198
198
|
data.code.codeEl.appendChild(document.createTextNode(text));
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
|
+
if (data.opts.animate) {
|
|
202
|
+
// Stagger cascade within a flush: each span starts before the previous
|
|
203
|
+
// one finishes ((n%4)*20ms delay, .15s duration). Modulo keeps long
|
|
204
|
+
// outputs cascading instead of settling on a permanent max delay.
|
|
205
|
+
const n = data.animN || 0;
|
|
206
|
+
data.animN = n + 1;
|
|
207
|
+
const span = document.createElement("span");
|
|
208
|
+
span.className = "tk";
|
|
209
|
+
const delay = (n % 4) * 20;
|
|
210
|
+
if (delay) span.style.animationDelay = delay + "ms";
|
|
211
|
+
span.appendChild(document.createTextNode(text));
|
|
212
|
+
data.nodes[data.index].appendChild(span);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
201
215
|
smd.default_add_text(data, text);
|
|
202
216
|
}
|
|
203
217
|
|