micro-models-agent 2.14.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 +38 -0
- package/dist/assets/app.js +9 -3
- package/dist/assets/index.html +2 -0
- package/dist/assets/js/dom.js +1 -0
- package/dist/assets/js/feed.js +103 -53
- package/dist/assets/js/format.js +8 -0
- package/dist/assets/js/settings.js +94 -8
- package/dist/assets/js/sidebar.js +198 -5
- package/dist/assets/js/tabs.js +105 -18
- package/dist/assets/styles.css +107 -30
- package/dist/i18n/en.json +30 -1
- package/dist/i18n/ru.json +30 -1
- package/dist/main.js +615 -166
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,44 @@ 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
|
+
|
|
30
|
+
## [2.15.0] - 2026-10-01
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Web UI: stale-translations badge** (`src/modules/webui/i18n-stale.ts`, `i18n-api.ts`, `host-state.ts`, `core/host.ts`, `assets/index.html`, `js/dom.js`, `js/settings.js`, `app.js`, `styles.css`): `GET /api/i18n` and the state snapshot carry `i18nStale` (dict files on disk newer than the server process); the topbar shows a yellow badge with a reload action instead of silently rendering raw keys. New keys `web.i18n.stale`/`web.i18n.reload` (en+ru). Tests: `tests/webui-i18n.test.ts`, `tests/webui-usage.test.ts`, `tests/webui-assets.test.ts`.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **Plan auto-advance no longer completes modify-steps on stale files** (`src/modules/execution/module.ts`): `advancePlanIfStepComplete` and final-audit `autoCloseEvidenceSteps` now require `provablyStale()` to be false — every named file must be created/modified after `plan.createdAt` (fail-open on bad dates and unreadable mtimes, delete-kind unchanged). Previously a step naming a long-existing file auto-completed before a line of code was written (observed: 6 plan steps auto-closed while the agent was still on step 1). Tests: `tests/execution-plan-deliverables.test.ts`.
|
|
39
|
+
- **Critic evidence includes git-tracked changes** (`src/modules/critic/evidence.ts`): `buildEvidencePack` unions tool-arg file lists with `git status --porcelain` (staged + unstaged + untracked, capped, fail-open outside repos), so edits made outside file tools (helper scripts via bash, heredocs) no longer produce false `concern` verdicts about missing changes. Tests: `tests/critic-evidence.test.ts`.
|
|
40
|
+
- **Repeated-failure memory writes are diagnosed, model misses skipped** (`src/core/agent/tool-batch.ts`, `loop-state.ts`): `diagnoseRepeatedFailures()` records distinct error headlines with signature-based cause/solution (edit_file anchor miss → CRLF/stale-read hypothesis) and skips global-memory writes for model errors (invented test paths, exit-code-only grep misses). Previously boilerplate causes and the model's own wrong guesses landed in `errors.md`. Tests: `tests/tool-failure-diagnosis.test.ts`.
|
|
41
|
+
- **Factual check resolves path suffixes** (`src/modules/hallucination/factual.ts`): `webui/state.ts` now matches `src/modules/webui/state.ts` via the cached basename index (no new FS walks), fixing false "mentioned files not found" warnings on short paths. Tests: `tests/hallucination-factual.test.ts`.
|
|
42
|
+
- **`edit_file` tolerates LF anchors in CRLF files** (`src/tools/edit-file.ts`): `spliceNormalized()` matches on normalized line endings and splices the original, keeping the file's ending style (no more "string not found" loops on Windows checkouts). Tests: `tests/edit-file.test.ts`.
|
|
43
|
+
- **Web usage re-render covers the context breakdown** (`src/modules/webui/assets/js/feed.js`): `h.contextBreakdown` added to the `renderUsage` signature so part shifts at constant used/limit repaint the segmented bar and legend. Guard: `tests/webui-assets.test.ts`.
|
|
44
|
+
- **Terminal context-budget part words are localized** (`src/cli/repl.ts`, `src/i18n/en.json`, `ru.json`): the startup banner showed untranslated latin `sys/res/hist` in both locales; now `repl.ctx_part_system/reserve/history` (`system/reserve/history` + `система/резерв/история`). Web legend (usage parts) intentionally unchanged. Tests: `tests/i18n.test.ts`.
|
|
45
|
+
- **Verification hygiene is model-facing** (`src/core/system-prompt.ts`, `src/tools/browser.ts`): verification `mma web` servers must run isolated (`MMA_CONFIG_DIR`), never against the user's live server; without vision capabilities the browser description steers to snapshot text and DOM/API assertions. Tests: `tests/core/bootstrap.test.ts`, `tests/tools/browser.test.ts`.
|
|
46
|
+
|
|
9
47
|
## [2.14.0] - 2026-10-01
|
|
10
48
|
|
|
11
49
|
### Changed
|
package/dist/assets/app.js
CHANGED
|
@@ -15,7 +15,7 @@ loadCommands,
|
|
|
15
15
|
} from "./js/tabs.js";
|
|
16
16
|
import {
|
|
17
17
|
renderSelectors, renderSettings, loadConfig, loadModels, loadProviders,
|
|
18
|
-
loadMemory, loadSkills, loadPlugins, applyTheme, currentTheme, applyFontSize, setConnStatus,
|
|
18
|
+
loadMemory, loadSkills, loadPlugins, applyTheme, currentTheme, applyFontSize, setConnStatus, setStaleBar,
|
|
19
19
|
} from "./js/settings.js";
|
|
20
20
|
import {
|
|
21
21
|
initRail, setPlan as railPlan, railEvent, applyRailI18n,
|
|
@@ -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
|
|
|
@@ -88,6 +89,7 @@ function renderState(snapshot, busy) {
|
|
|
88
89
|
sidebarSessionChanged(sessionId);
|
|
89
90
|
renderWorkspace(S.state.host);
|
|
90
91
|
renderSessionsDropdown();
|
|
92
|
+
setStaleBar(!!(snapshot && snapshot.i18nStale));
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
async function refresh() {
|
|
@@ -140,7 +142,7 @@ const handlers = [
|
|
|
140
142
|
["tool_end", onToolEnd],
|
|
141
143
|
["plan", onPlan],
|
|
142
144
|
["question", onQuestion],
|
|
143
|
-
["turn_end", (d) => { onTurnEnd(d); sidebarTurnEnd(); sidebarBusy(false); }],
|
|
145
|
+
["turn_end", (d) => { onTurnEnd(d); sidebarTurnEnd(d.turnSeq); sidebarBusy(false); sidebarPullLog(); }],
|
|
144
146
|
["log", onLog],
|
|
145
147
|
["retract", (d) => onRetract(d.reason || "", d.sessionId)],
|
|
146
148
|
];
|
|
@@ -160,7 +162,10 @@ for (const ev of ["tool_start", "tool_end", "text", "plan"]) {
|
|
|
160
162
|
});
|
|
161
163
|
}
|
|
162
164
|
// Sidebar Trace: live per-session event log. Feed handlers stay untouched.
|
|
163
|
-
|
|
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"]) {
|
|
164
169
|
es.addEventListener(ev, (e) => { const d = parse(e); if (d) sidebarTraceEvent(ev, d); });
|
|
165
170
|
}
|
|
166
171
|
es.addEventListener("text", (e) => { const d = parse(e); if (d) sidebarTraceEvent("text", d); });
|
|
@@ -259,6 +264,7 @@ function applyStaticI18n() {
|
|
|
259
264
|
applySidebarI18n();
|
|
260
265
|
if (msgs.querySelector(".empty")) showEmptyFeed();
|
|
261
266
|
if (connStatus && !connStatus.hidden) setConnStatus(true);
|
|
267
|
+
setStaleBar(undefined, true);
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
async function boot() {
|
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>
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
<button class="icon-btn" id="toggleSidebar" title="sidebar" aria-label="sidebar" aria-expanded="false">▤</button>
|
|
30
31
|
<button class="icon-btn" id="openSettings" title="settings" aria-label="settings" aria-haspopup="dialog" aria-expanded="false">⚙</button>
|
|
31
32
|
<span class="conn" id="connStatus" role="status" aria-live="polite" hidden></span>
|
|
33
|
+
<span class="conn stale" id="staleBar" role="status" hidden></span>
|
|
32
34
|
</div>
|
|
33
35
|
|
|
34
36
|
<div class="bar-sub" id="sessBar">
|
package/dist/assets/js/dom.js
CHANGED
|
@@ -14,6 +14,7 @@ export const attachStrip = document.getElementById("attachStrip");
|
|
|
14
14
|
export const attachErr = document.getElementById("attachErr");
|
|
15
15
|
export const imgPicker = document.getElementById("imgPicker");
|
|
16
16
|
export const connStatus = document.getElementById("connStatus");
|
|
17
|
+
export const staleBar = document.getElementById("staleBar");
|
|
17
18
|
export const liveEl = document.getElementById("live");
|
|
18
19
|
export const sendBtn = document.getElementById("sendBtn");
|
|
19
20
|
|
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) {
|
|
@@ -578,6 +555,30 @@ export function onToolEnd(data) {
|
|
|
578
555
|
S.toolNodes.delete(data.toolCallId);
|
|
579
556
|
const args = S.pendingArgs.get(data.toolCallId) || {};
|
|
580
557
|
S.pendingArgs.delete(data.toolCallId);
|
|
558
|
+
// File tools render as a single filerow (viewer link + copy): the `.act`
|
|
559
|
+
// line from `onToolStart` is replaced, otherwise the feed shows the same
|
|
560
|
+
// action twice (act + filerow). Without a path `cardFor` renders nothing,
|
|
561
|
+
// so keep the generic act line in that case.
|
|
562
|
+
const filePath = args.path || args.file || args.dir;
|
|
563
|
+
if (FILE_TOOLS.indexOf(data.name) !== -1 && filePath) {
|
|
564
|
+
if (line && line.parentNode) line.remove();
|
|
565
|
+
if (verbosity() === "quiet") {
|
|
566
|
+
if (data.success) {
|
|
567
|
+
const card = cardFor(data.name, args, data.sessionId, data.success);
|
|
568
|
+
if (card) S.runNodes.push(card);
|
|
569
|
+
S.runDuration += typeof data.durationMs === "number" ? data.durationMs : 0;
|
|
570
|
+
} else {
|
|
571
|
+
const card = cardFor(data.name, args, data.sessionId, data.success);
|
|
572
|
+
const cause = firstOutputLine(data.output);
|
|
573
|
+
if (cause) insertAfter(card || line, el("div", "detail", "↳ " + cause));
|
|
574
|
+
if (card) S.runNodes.push(card);
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const card = cardFor(data.name, args, data.sessionId, data.success);
|
|
579
|
+
renderToolOutput(card || line, data.output);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
581
582
|
if (verbosity() === "quiet") {
|
|
582
583
|
// Quiet hides tool bodies. Track successful nodes so a run of read-only
|
|
583
584
|
// actions can fold at the next segment; a failure keeps its line and adds
|
|
@@ -1015,6 +1016,58 @@ function usageRow(label, value, cls) {
|
|
|
1015
1016
|
return row;
|
|
1016
1017
|
}
|
|
1017
1018
|
|
|
1019
|
+
// Segmented context bar: system · tools · history · reserve · free, widths
|
|
1020
|
+
// relative to the full context window so the parts always sum to one bar.
|
|
1021
|
+
function contextSegBar(h, used, limit) {
|
|
1022
|
+
const bar = el("div", "bar segbar");
|
|
1023
|
+
const bd = h.contextBreakdown;
|
|
1024
|
+
if (!bd || typeof bd.window !== "number" || bd.window <= 0) {
|
|
1025
|
+
// Legacy fallback (pre-breakdown payloads): single used/limit fill.
|
|
1026
|
+
const fill = el("i");
|
|
1027
|
+
fill.style.width = Math.min(100, Math.round((used / limit) * 100)) + "%";
|
|
1028
|
+
bar.appendChild(fill);
|
|
1029
|
+
return bar;
|
|
1030
|
+
}
|
|
1031
|
+
const denom = Math.max(bd.window, bd.used + bd.reserve);
|
|
1032
|
+
const parts = [
|
|
1033
|
+
["seg-system", bd.system],
|
|
1034
|
+
["seg-tools", bd.tools],
|
|
1035
|
+
["seg-history", bd.history],
|
|
1036
|
+
["seg-reserve", bd.reserve],
|
|
1037
|
+
["seg-free", bd.free],
|
|
1038
|
+
];
|
|
1039
|
+
for (const [cls, n] of parts) {
|
|
1040
|
+
if (!n || n <= 0) continue;
|
|
1041
|
+
const seg = el("i", cls);
|
|
1042
|
+
seg.style.width = (n / denom) * 100 + "%";
|
|
1043
|
+
bar.appendChild(seg);
|
|
1044
|
+
}
|
|
1045
|
+
return bar;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Dim legend under the segmented bar: one dot+label+value per context part.
|
|
1049
|
+
function contextLegend(h) {
|
|
1050
|
+
const box = el("div", "seglegend");
|
|
1051
|
+
const bd = h.contextBreakdown;
|
|
1052
|
+
if (!bd) return box;
|
|
1053
|
+
const items = [
|
|
1054
|
+
["seg-system", "web.usage.part_system", bd.system],
|
|
1055
|
+
["seg-tools", "web.usage.part_tools", bd.tools],
|
|
1056
|
+
["seg-history", "web.usage.part_history", bd.history],
|
|
1057
|
+
["seg-reserve", "web.usage.part_reserve", bd.reserve],
|
|
1058
|
+
["seg-free", "web.usage.part_free", bd.free],
|
|
1059
|
+
];
|
|
1060
|
+
for (const [cls, labelKey, n] of items) {
|
|
1061
|
+
if (!n || n <= 0) continue;
|
|
1062
|
+
const item = el("span", "segl-item");
|
|
1063
|
+
item.appendChild(el("i", "segl-dot " + cls));
|
|
1064
|
+
item.appendChild(el("span", "segl-label", t(labelKey)));
|
|
1065
|
+
item.appendChild(el("b", "", fmtTokens(n)));
|
|
1066
|
+
box.appendChild(item);
|
|
1067
|
+
}
|
|
1068
|
+
return box;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1018
1071
|
// Session row with a copy button next to the id (icon-only, ✓ on success).
|
|
1019
1072
|
function usageSessionRow(sessionId) {
|
|
1020
1073
|
const row = el("div", "kv");
|
|
@@ -1045,7 +1098,7 @@ export function renderUsage(h) {
|
|
|
1045
1098
|
const cfg = S.state.config || {};
|
|
1046
1099
|
const sig = JSON.stringify([
|
|
1047
1100
|
h.sessionId, h.model, h.contextUsed, h.contextLimit, h.cacheHitRate, h.costUsd,
|
|
1048
|
-
h.compactionCount, h.contextQuality, h.contextProbe, cfg.model, cfg.contextWindow,
|
|
1101
|
+
h.compactionCount, h.contextQuality, h.contextProbe, h.contextBreakdown, cfg.model, cfg.contextWindow,
|
|
1049
1102
|
I18N.locale,
|
|
1050
1103
|
]);
|
|
1051
1104
|
if (sig === S.lastUsageSig) return;
|
|
@@ -1058,11 +1111,8 @@ export function renderUsage(h) {
|
|
|
1058
1111
|
const limit = typeof h.contextLimit === "number" ? h.contextLimit : cfg.contextWindow || null;
|
|
1059
1112
|
usageEl.appendChild(usageRow(t("web.usage.context"), used != null && limit ? fmtTokens(used) + " / " + fmtTokens(limit) : "—"));
|
|
1060
1113
|
if (used != null && limit) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
fill.style.width = Math.min(100, Math.round((used / limit) * 100)) + "%";
|
|
1064
|
-
bar.appendChild(fill);
|
|
1065
|
-
usageEl.appendChild(bar);
|
|
1114
|
+
usageEl.appendChild(contextSegBar(h, used, limit));
|
|
1115
|
+
usageEl.appendChild(contextLegend(h));
|
|
1066
1116
|
}
|
|
1067
1117
|
const hit = typeof h.cacheHitRate === "number" ? h.cacheHitRate : null;
|
|
1068
1118
|
usageEl.appendChild(usageRow(t("web.usage.cache"), hit != null ? Math.round(hit * 100) + "%" : "—", hit != null ? "" : "dim"));
|
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();
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// share from tabs; confirm/toast from dialog. Never imports app (the locale
|
|
4
4
|
// refresh reaches the app shell through the store hooks registry).
|
|
5
5
|
import {
|
|
6
|
-
el, icon, link, inlineTextNodes, connStatus, makeToggle,
|
|
6
|
+
el, icon, link, inlineTextNodes, connStatus, staleBar, makeToggle,
|
|
7
7
|
} from "./dom.js";
|
|
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
|
|
|
@@ -324,6 +391,25 @@ export function setConnStatus(disconnected) {
|
|
|
324
391
|
connStatus.textContent = disconnected ? "⋯ " + t("web.conn.reconnecting") : "";
|
|
325
392
|
}
|
|
326
393
|
|
|
394
|
+
// Stale-translations banner: dict files on disk are newer than the running
|
|
395
|
+
// server process, so the chrome may show raw keys. Rebuilt only on S.state
|
|
396
|
+
// change (not on every 2s poll) to keep the reload-button focus stable;
|
|
397
|
+
// `force` re-renders the label after a locale switch.
|
|
398
|
+
let lastStale = null;
|
|
399
|
+
export function setStaleBar(stale, force) {
|
|
400
|
+
if (!staleBar) return;
|
|
401
|
+
// undefined = перерисовать последнее состояние (смена локали).
|
|
402
|
+
const on = stale === undefined ? !!lastStale : !!stale;
|
|
403
|
+
if (!force && on === lastStale) return;
|
|
404
|
+
lastStale = on;
|
|
405
|
+
staleBar.hidden = !on;
|
|
406
|
+
staleBar.textContent = "";
|
|
407
|
+
if (!on) return;
|
|
408
|
+
staleBar.appendChild(el("span", "", "⚠ " + t("web.i18n.stale") + " "));
|
|
409
|
+
staleBar.appendChild(link(t("web.i18n.reload"), "", () => location.reload()));
|
|
410
|
+
return lastStale;
|
|
411
|
+
}
|
|
412
|
+
|
|
327
413
|
// Panel → named sections lives in `settings-controls.js` (PANEL_SECTIONS).
|
|
328
414
|
// Custom panels (model/ui/browser…) render a bespoke body first and then their
|
|
329
415
|
// sections; purely generic panels (general/context/…) render sections only.
|