micro-models-agent 2.15.0 → 2.16.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/CHANGELOG.md +21 -0
- package/dist/assets/app.js +6 -2
- package/dist/assets/index.html +1 -0
- package/dist/assets/js/feed.js +24 -47
- 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/styles.css +73 -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
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Web UI incoming message queue** (`src/modules/webui/state.ts`, `util.ts`, `forward.ts`, `module.ts`, `server.ts`, `run-agent.ts`, `assets/js/tabs.js`): a message sent mid-turn no longer dies on `409 busy` — it queues (`202 {queued: n}`, cap `MAX_QUEUE_LENGTH = 5`, overflow `429 queue_full`) and runs as the next turn on `turn_end`, in order. Drain reuses the single-flight submit path; the queue clears on server stop; `GET /api/state` and SSE `state` carry `queueLen`. The composer stays editable during a turn with a `↳ queued · position N` receipt (`web.input.queued/queue_full`, en+ru; dead `web.input.busy` removed). Docs: `docs/agents/modules.md`. Tests: `tests/webui-server.test.ts`, `tests/webui-auth.test.ts`.
|
|
12
|
+
|
|
13
|
+
- **Web Trace: diagnostics coverage + full model text** (`src/modules/webui/log-api.ts`, `sessions-api.ts`, `forward.ts`, `state.ts`, `module.ts`, `assets/app.js`, `assets/js/sidebar.js`): Trace shows status rows (SSE `meta`), warn/error logs (SSE `log`, minus info-diagnostic chatter), draft retractions, and index start/done/error — plus critic/MoE/compaction/context-probe/usage/audit/claim-retry/output pulled incrementally from `session.jsonl` on every `turn_end` via the new `GET /api/sessions/:id/log` (`?types=&since=&limit=`, field whitelist, secret scrub, no new producers). `turn_end` carries a server `turnSeq`; text details show the full turn text (honest 20K cap instead of a 4K tail slice) with session/turn and a session-log provenance footer (assistant count, iterations, model/tokens). Git ops stay inside `bash` rows (prefix heuristics too fragile). i18n `web.sidebar.trace_*/detail_*` (en+ru). Tests: `tests/webui-log.test.ts`, e2e `web-boot` (Trace panel + index lifecycle).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Web file list is always expanded with full root-relative links** (`src/modules/webui/assets/js/feed.js`, `styles.css`): consecutive file rows no longer collapse into an expandable `│ N files` group (its chevron toggle failed to open) — every file tool renders its own `◇ <label> · <full-path-link> [copy] <status>` line, and the link text is the full workspace-relative path (`displayPathFromRoot`, absolute paths stripped by the workspace prefix) instead of basename + dim tail. Consecutive file/action rows are compact (`~4px` via `margin-top: -7px` compensating the `11px` feed gap, `line-height: 1.35`). Group CSS/JS removed. Tests: `tests/webui-assets.test.ts`, `tests/e2e/web-filecard.e2e-test.ts`.
|
|
18
|
+
- **Web transcript edges and answer separation** (`src/modules/webui/assets/styles.css`): scroll edge fades are glued exactly to the transcript edges — vertical breathing moved from the scroller to `.feed` (sticky pseudos are constrained to the padding-free content box, previously leaving a gap under the header and above the composer) and hidden fades take zero flow space (`-33px` margins neutralizing height + flex gap). Finished assistant answers carry a quiet accent rail (`.feed > .md/.ans`, `border-left: 2px solid var(--accent)`, mirroring the user block) so the result reads instantly apart from the dim tool/think process. Tests: `tests/webui-assets.test.ts`.
|
|
19
|
+
- **Success-claim no longer vetoed by plan-bookkeeping failures** (`src/modules/hallucination/success-claim.ts`): `checkSuccessClaim` ignores `plan`/`plan_step`/`todo` evidences — their failures police the agent's own notebook (covered by the final audit), never task outcome. Lived-in case (ses_mupgenhq_75f66l, qwen3.5-9b): an honest "tests passed" was retracted over a stale `re-plan` error ("Длина checks должна совпадать"), forcing a same-substance re-answer after the task was done. Tests: `tests/hallucination-success-claim.test.ts`.
|
|
20
|
+
|
|
21
|
+
- **Web UI boot was dead on 2.15.0** (`src/modules/webui/assets/js/settings.js`): a stray `}` in `setStaleBar` pushed `return` to top level (`Uncaught SyntaxError: Illegal return statement`), killing the whole `app.js` import graph — blank page, no feed, selectors stuck at `–`. One-line brace fix.
|
|
22
|
+
- **Web e2e boot smoke** (`tests/e2e/web-boot.e2e-test.ts`): `mma web` boots in a real Chromium with zero console/page errors (live session tab, chrome skeleton, settings modal) and every served JS module in the live `/app.js` import graph answers 200; `web-helpers.launch` now names collected console errors when the feed never boots, so the next broken module fails with its `SyntaxError` in the message instead of a bare timeout.
|
|
23
|
+
|
|
24
|
+
## [2.16.0] - 2026-10-01
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Web dropdown search for models and sessions** (`src/modules/webui/assets/js/settings.js`, `tabs.js`, `format.js`, `index.html`, `styles.css`, `src/i18n/en.json`, `ru.json`): the composer model menu and the sessions dropdown (`☰`) gain a sticky search field — case-insensitive substring (`matchesQuery` in `format.js`; sessions match name + id) with keyboard support (`↓` into the list, `Enter` picks the first hit, `Esc` clears/closes, `↑` back to search). The sessions input lives statically in the markup so the 2s state poll never steals the caret; the model filter survives list re-reads and clears on pick. Empty sections hide with their headers, zero hits show `no matches`; `↻ refresh` / `+ new session` stay outside the filter. New keys `web.selectors.search_models` / `web.sessions.search` (en+ru). Tests: `tests/webui-assets.test.ts`.
|
|
29
|
+
|
|
9
30
|
## [2.15.0] - 2026-10-01
|
|
10
31
|
|
|
11
32
|
### Added
|
package/dist/assets/app.js
CHANGED
|
@@ -23,6 +23,7 @@ initRail, setPlan as railPlan, railEvent, applyRailI18n,
|
|
|
23
23
|
import {
|
|
24
24
|
initSidebar, sidebarSessionChanged, sidebarMapState,
|
|
25
25
|
sidebarTraceEvent, sidebarTurnEnd, sidebarBusy, sidebarMapIndicator,
|
|
26
|
+
sidebarPullLog,
|
|
26
27
|
applySidebarI18n,
|
|
27
28
|
} from "./js/sidebar.js";
|
|
28
29
|
|
|
@@ -141,7 +142,7 @@ const handlers = [
|
|
|
141
142
|
["tool_end", onToolEnd],
|
|
142
143
|
["plan", onPlan],
|
|
143
144
|
["question", onQuestion],
|
|
144
|
-
["turn_end", (d) => { onTurnEnd(d); sidebarTurnEnd(); sidebarBusy(false); }],
|
|
145
|
+
["turn_end", (d) => { onTurnEnd(d); sidebarTurnEnd(d.turnSeq); sidebarBusy(false); sidebarPullLog(); }],
|
|
145
146
|
["log", onLog],
|
|
146
147
|
["retract", (d) => onRetract(d.reason || "", d.sessionId)],
|
|
147
148
|
];
|
|
@@ -161,7 +162,10 @@ for (const ev of ["tool_start", "tool_end", "text", "plan"]) {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
// Sidebar Trace: live per-session event log. Feed handlers stay untouched.
|
|
164
|
-
|
|
165
|
+
// Tool/text/phase/plan/question/error stream live; status/log/retract/index
|
|
166
|
+
// ride the same SSE bus; critic/MoE/compaction/probe/usage/audit/claim rows
|
|
167
|
+
// arrive via the session-log supplement Trace pulls on every turn_end.
|
|
168
|
+
for (const ev of ["tool_start", "tool_end", "phase", "plan", "question", "meta", "log", "retract", "index"]) {
|
|
165
169
|
es.addEventListener(ev, (e) => { const d = parse(e); if (d) sidebarTraceEvent(ev, d); });
|
|
166
170
|
}
|
|
167
171
|
es.addEventListener("text", (e) => { const d = parse(e); if (d) sidebarTraceEvent("text", d); });
|
package/dist/assets/index.html
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<div class="dd" id="sessDD">
|
|
17
17
|
<button class="icon-btn" id="sessBtn" title="all sessions" aria-label="all sessions" aria-haspopup="menu" aria-expanded="false">☰</button>
|
|
18
18
|
<div class="dd-menu">
|
|
19
|
+
<input class="searchbox" id="sessSearch" type="search" autocomplete="off" spellcheck="false" placeholder="search sessions…" aria-label="search sessions">
|
|
19
20
|
<div class="h" id="sessActiveHeader">active</div>
|
|
20
21
|
<div id="sessActiveList"></div>
|
|
21
22
|
<div class="h" id="sessRecentHeader">recent</div>
|
package/dist/assets/js/feed.js
CHANGED
|
@@ -444,44 +444,23 @@ function safeUrl(url) {
|
|
|
444
444
|
return null;
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
head.setAttribute("aria-expanded", "false");
|
|
463
|
-
head.appendChild(el("span", "g", "│ "));
|
|
464
|
-
head.appendChild(el("span", "count", pluralCount("web.card.files", 2)));
|
|
465
|
-
const fail = prevRow.dataset.fail === "1" || row.dataset.fail === "1";
|
|
466
|
-
if (fail) wrap.dataset.fail = "1";
|
|
467
|
-
head.appendChild(el("span", "ok", fail ? " ✗" : " ✓"));
|
|
468
|
-
const chev = icon("chevron-down");
|
|
469
|
-
head.appendChild(chev);
|
|
470
|
-
const items = el("div", "items hidden");
|
|
471
|
-
feed.replaceChild(wrap, prevRow);
|
|
472
|
-
items.appendChild(prevRow);
|
|
473
|
-
items.appendChild(row);
|
|
474
|
-
wrap.appendChild(head);
|
|
475
|
-
wrap.appendChild(items);
|
|
476
|
-
head.addEventListener("click", () => {
|
|
477
|
-
const open = wrap.dataset.open === "1";
|
|
478
|
-
wrap.dataset.open = open ? "" : "1";
|
|
479
|
-
head.setAttribute("aria-expanded", open ? "false" : "true");
|
|
480
|
-
items.classList.toggle("hidden", open);
|
|
481
|
-
});
|
|
482
|
-
return wrap;
|
|
447
|
+
// File rows are always expanded: one `filerow` per file-tool call, no
|
|
448
|
+
// collapsing group. Consecutive rows stay a plain expanded list (the old
|
|
449
|
+
// E grouping with a chevron toggle hid the list and failed to open).
|
|
450
|
+
function displayPathFromRoot(path) {
|
|
451
|
+
const raw = String(path == null ? "" : path).replace(/\\/g, "/");
|
|
452
|
+
try {
|
|
453
|
+
const ws = S && S.state && S.state.host && S.state.host.workspace
|
|
454
|
+
? String(S.state.host.workspace).replace(/\\/g, "/").replace(/\/+$/, "")
|
|
455
|
+
: "";
|
|
456
|
+
if (ws && (raw === ws || raw.indexOf(ws + "/") === 0)) {
|
|
457
|
+
const rel = raw === ws ? "" : raw.slice(ws.length + 1);
|
|
458
|
+
if (rel) return rel;
|
|
459
|
+
}
|
|
460
|
+
} catch (err) {
|
|
461
|
+
/* fail-open: show raw path */
|
|
483
462
|
}
|
|
484
|
-
return
|
|
463
|
+
return raw;
|
|
485
464
|
}
|
|
486
465
|
|
|
487
466
|
function cardFor(name, args, sid, ok) {
|
|
@@ -490,22 +469,21 @@ function cardFor(name, args, sid, ok) {
|
|
|
490
469
|
if (FILE_TOOLS.indexOf(name) !== -1) {
|
|
491
470
|
const path = args.path || args.file || args.dir;
|
|
492
471
|
if (!path) return null;
|
|
472
|
+
const shown = displayPathFromRoot(path);
|
|
493
473
|
const row = el("div", "filerow " + categoryOf(name));
|
|
494
474
|
row.appendChild(el("span", "g", "◇"));
|
|
495
475
|
row.appendChild(el("span", "lab", labelOf(name)));
|
|
496
476
|
row.appendChild(el("span", "sep", " · "));
|
|
497
477
|
if (OPENABLE_TOOLS.indexOf(name) !== -1) {
|
|
498
|
-
const openLink = el("a", "path", String(path)
|
|
499
|
-
openLink.href = "/api/file?path=" + encodeURIComponent(path);
|
|
478
|
+
const openLink = el("a", "path", shown || String(path));
|
|
479
|
+
openLink.href = "/api/file?path=" + encodeURIComponent(String(path));
|
|
500
480
|
openLink.target = "_blank";
|
|
501
481
|
openLink.rel = "noopener noreferrer";
|
|
502
|
-
openLink.title = t("web.card.open_title") + " · " + path;
|
|
482
|
+
openLink.title = t("web.card.open_title") + " · " + String(path);
|
|
503
483
|
row.appendChild(openLink);
|
|
504
|
-
const tail = String(path).split(/[\\/]/).slice(0, -1).join("/");
|
|
505
|
-
if (tail) row.appendChild(el("span", "tail", " · " + tail));
|
|
506
484
|
} else {
|
|
507
|
-
const s = el("span", "tail", path);
|
|
508
|
-
s.title = path;
|
|
485
|
+
const s = el("span", "tail", shown || String(path));
|
|
486
|
+
s.title = String(path);
|
|
509
487
|
row.appendChild(s);
|
|
510
488
|
}
|
|
511
489
|
const copyBtn = el("button", "copy-btn");
|
|
@@ -514,7 +492,7 @@ function cardFor(name, args, sid, ok) {
|
|
|
514
492
|
copyBtn.setAttribute("aria-label", t("web.card.copy_title"));
|
|
515
493
|
copyBtn.appendChild(icon("copy"));
|
|
516
494
|
copyBtn.addEventListener("click", () => {
|
|
517
|
-
copyText(path).then((ok) => {
|
|
495
|
+
copyText(String(path)).then((ok) => {
|
|
518
496
|
copyBtn.replaceChildren(icon(ok ? "check" : "copy"));
|
|
519
497
|
if (!ok) { const st = row.querySelector(".st"); if (st) st.textContent = " ⚠"; }
|
|
520
498
|
window.setTimeout(() => copyBtn.replaceChildren(icon("copy")), 1400);
|
|
@@ -523,11 +501,10 @@ function cardFor(name, args, sid, ok) {
|
|
|
523
501
|
row.appendChild(copyBtn);
|
|
524
502
|
row.appendChild(el("span", "st", ok === false ? " ✗" : ok === true ? " ✓" : ""));
|
|
525
503
|
if (ok === false) row.dataset.fail = "1";
|
|
526
|
-
if (/\.drawio(\.xml)?$/i.test(path) && verbosity() === "verbose") {
|
|
504
|
+
if (/\.drawio(\.xml)?$/i.test(String(path)) && verbosity() === "verbose") {
|
|
527
505
|
row.appendChild(el("span", "detail", "↳ " + t("web.card.drawio_hint")));
|
|
528
506
|
}
|
|
529
507
|
appendTo(feed, row);
|
|
530
|
-
try { maybeGroupFilerow(feed, row); } catch (err) { /* fail-open: plain row stays */ }
|
|
531
508
|
return row;
|
|
532
509
|
}
|
|
533
510
|
if (LINK_TOOLS.indexOf(name) !== -1 && args.url) {
|
package/dist/assets/js/format.js
CHANGED
|
@@ -90,6 +90,14 @@ for (const raw of text.split("\n")) {
|
|
|
90
90
|
return "";
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// Case-insensitive substring match for dropdown filters (model / session
|
|
94
|
+
// search). Empty query matches everything; non-strings stringify safely.
|
|
95
|
+
export function matchesQuery(haystack, q) {
|
|
96
|
+
const needle = String(q == null ? "" : q).trim().toLowerCase();
|
|
97
|
+
if (!needle) return true;
|
|
98
|
+
return String(haystack == null ? "" : haystack).toLowerCase().indexOf(needle) !== -1;
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
export function relTime(ts) {
|
|
94
102
|
if (!ts) return "";
|
|
95
103
|
const diff = Date.now() - new Date(ts).getTime();
|
|
@@ -8,7 +8,7 @@ el, icon, link, inlineTextNodes, connStatus, staleBar, makeToggle,
|
|
|
8
8
|
import { api } from "./api.js";
|
|
9
9
|
import { t, loadI18n } from "./i18n.js";
|
|
10
10
|
import { S, setPath, getPath, THEME_KEY, hooks } from "./store.js";
|
|
11
|
-
import { contextLabel } from "./format.js";
|
|
11
|
+
import { contextLabel, matchesQuery } from "./format.js";
|
|
12
12
|
import { closeAllMenus, setSessOpen } from "./tabs.js";
|
|
13
13
|
import { setIndicator, closeConfirm, confirmOverlay, trapTab } from "./dialog.js";
|
|
14
14
|
import { controlFor, PANEL_SECTIONS, keyToPanel } from "./settings-controls.js";
|
|
@@ -48,6 +48,11 @@ function menuAction(label, iconName, onClick) {
|
|
|
48
48
|
return mi;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Live filter text of the composer model dropdown. Kept module-level so a
|
|
52
|
+
// `renderSelectors()` re-read (config/models reload) preserves the query and
|
|
53
|
+
// the input is rebuilt with it; cleared once a model is actually picked.
|
|
54
|
+
let modelFilter = "";
|
|
55
|
+
|
|
51
56
|
export function renderSelectors() {
|
|
52
57
|
const cfg = S.state.config || {};
|
|
53
58
|
const provider = cfg.provider || {};
|
|
@@ -55,13 +60,70 @@ export function renderSelectors() {
|
|
|
55
60
|
setSelButton(document.getElementById("selModelBtn"), t("web.selectors.model"), cfg.model || "—");
|
|
56
61
|
const modelMenu = document.getElementById("selModelMenu");
|
|
57
62
|
modelMenu.textContent = "";
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
// Search row stays outside the filtered list: `drawModelList` only touches
|
|
64
|
+
// `listBox`, so typing never rebuilds (or unfocuses) the input itself.
|
|
65
|
+
const search = document.createElement("input");
|
|
66
|
+
search.id = "selModelSearch";
|
|
67
|
+
search.className = "searchbox";
|
|
68
|
+
search.type = "search";
|
|
69
|
+
search.autocomplete = "off";
|
|
70
|
+
search.spellcheck = false;
|
|
71
|
+
search.placeholder = t("web.selectors.search_models");
|
|
72
|
+
search.setAttribute("aria-label", t("web.selectors.search_models"));
|
|
73
|
+
search.value = modelFilter;
|
|
74
|
+
search.addEventListener("click", (e) => e.stopPropagation());
|
|
75
|
+
modelMenu.appendChild(search);
|
|
76
|
+
const listBox = el("div", "model-menu-list");
|
|
77
|
+
modelMenu.appendChild(listBox);
|
|
78
|
+
const drawModelList = () => {
|
|
79
|
+
listBox.textContent = "";
|
|
80
|
+
const models = S.state.models.filter((m) => matchesQuery(m, modelFilter));
|
|
81
|
+
if (!models.length) {
|
|
82
|
+
listBox.appendChild(el("div", "mi dim", S.state.models.length ? t("web.settings.no_matches") : t("web.selectors.no_models")));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
models.forEach((m) => {
|
|
86
|
+
const item = menuItem(m, "", m === cfg.model, () => {
|
|
87
|
+
modelFilter = "";
|
|
88
|
+
postConfig("model", m, true);
|
|
89
|
+
});
|
|
90
|
+
// ArrowUp on the first row returns focus to the search field.
|
|
91
|
+
item.addEventListener("keydown", (e) => {
|
|
92
|
+
if (e.key === "ArrowUp" && item === listBox.querySelector(".mi:not(.dim)")) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
e.stopPropagation();
|
|
95
|
+
search.focus();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
listBox.appendChild(item);
|
|
63
99
|
});
|
|
64
|
-
}
|
|
100
|
+
};
|
|
101
|
+
search.addEventListener("input", () => {
|
|
102
|
+
modelFilter = search.value;
|
|
103
|
+
drawModelList();
|
|
104
|
+
});
|
|
105
|
+
search.addEventListener("keydown", (e) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
if (e.key === "ArrowDown") {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
const first = listBox.querySelector(".mi:not(.dim)");
|
|
110
|
+
if (first) first.focus();
|
|
111
|
+
} else if (e.key === "Enter") {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
const first = listBox.querySelector(".mi:not(.dim)");
|
|
114
|
+
if (first) first.click();
|
|
115
|
+
} else if (e.key === "Escape") {
|
|
116
|
+
if (search.value) {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
search.value = "";
|
|
119
|
+
modelFilter = "";
|
|
120
|
+
drawModelList();
|
|
121
|
+
} else {
|
|
122
|
+
closeAllMenus();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
drawModelList();
|
|
65
127
|
modelMenu.appendChild(el("div", "sep"));
|
|
66
128
|
modelMenu.appendChild(
|
|
67
129
|
menuAction(t("web.selectors.refresh_list"), "refresh", () => {
|
|
@@ -196,6 +258,11 @@ document.querySelectorAll("[data-sel]").forEach((sel) => {
|
|
|
196
258
|
});
|
|
197
259
|
const open = sel.classList.toggle("open");
|
|
198
260
|
btn.setAttribute("aria-expanded", open ? "true" : "false");
|
|
261
|
+
// Opening the model menu lands focus in its search field.
|
|
262
|
+
if (open && sel.dataset && sel.dataset.sel === "model") {
|
|
263
|
+
const search = document.getElementById("selModelSearch");
|
|
264
|
+
if (search) setTimeout(() => search.focus(), 0);
|
|
265
|
+
}
|
|
199
266
|
});
|
|
200
267
|
});
|
|
201
268
|
|
|
@@ -340,7 +407,6 @@ export function setStaleBar(stale, force) {
|
|
|
340
407
|
if (!on) return;
|
|
341
408
|
staleBar.appendChild(el("span", "", "⚠ " + t("web.i18n.stale") + " "));
|
|
342
409
|
staleBar.appendChild(link(t("web.i18n.reload"), "", () => location.reload()));
|
|
343
|
-
}
|
|
344
410
|
return lastStale;
|
|
345
411
|
}
|
|
346
412
|
|
|
@@ -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");
|