micro-models-agent 2.13.1 → 2.14.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 CHANGED
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.14.0] - 2026-10-01
10
+
11
+ ### Changed
12
+
13
+ - **Web UI**: file actions now render as quiet terminal-style lines instead of boxed cards (`src/modules/webui/assets/js/feed.js`, `styles.css`). Each file tool shows `◇ <label> · <path-link> [copy-icon] <status>` in the tool-category color — no more Open/Download/Path pill buttons (path click opens the viewer, copy is an icon-only hover action). Consecutive file rows collapse into an expandable `│ N files` group with a chevron toggle. File-type badges are gone (format detection stays silent for highlighting only; draw.io hint is verbose-only). Dead strings `web.card.open/download/copy_path/download_title` removed, count via new `web.card.files_*` (en+ru). Tests: `tests/webui-assets.test.ts`, `tests/webui-smoke.test.ts`, new `tests/e2e/web-filecard.e2e-test.ts`.
14
+
9
15
  ## [2.13.1] - 2026-09-30
10
16
 
11
17
  ### Fixed
@@ -444,56 +444,91 @@ function safeUrl(url) {
444
444
  return null;
445
445
  }
446
446
 
447
- function cardFor(name, args, sid) {
447
+ function maybeGroupFilerow(feed, row) {
448
+ const prev = row.previousElementSibling;
449
+ const group = prev && prev.classList && prev.classList.contains("filegroup") ? prev : null;
450
+ const prevRow = prev && prev.classList && prev.classList.contains("filerow") ? prev : null;
451
+ if (group && group.dataset.open !== "1") {
452
+ group.querySelector(".items").appendChild(row);
453
+ if (row.dataset.fail === "1") group.dataset.fail = "1";
454
+ const n = group.querySelectorAll(".items > .filerow").length;
455
+ group.querySelector(".count").textContent = pluralCount("web.card.files", n);
456
+ group.querySelector(".ok").textContent = group.dataset.fail === "1" ? " ✗" : " ✓";
457
+ return group;
458
+ }
459
+ if (prevRow) {
460
+ const wrap = el("div", "filegroup");
461
+ const head = el("button", "head");
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;
483
+ }
484
+ return null;
485
+ }
486
+
487
+ function cardFor(name, args, sid, ok) {
448
488
  const feed = feedForEvent(sid);
449
489
  if (!feed) return null;
450
490
  if (FILE_TOOLS.indexOf(name) !== -1) {
451
491
  const path = args.path || args.file || args.dir;
452
492
  if (!path) return null;
453
- const card = el("div", "filecard");
454
- const top = el("div", "fc-top");
455
- top.appendChild(el("span", "g", "◇"));
456
- const nameEl = el("span", "name", path);
457
- nameEl.title = path;
458
- top.appendChild(nameEl);
459
- card.appendChild(top);
460
- const bottom = el("div", "fc-bottom");
461
- bottom.appendChild(el("span", "lab", labelOf(name)));
462
- const actions = el("span", "fc-actions");
463
- const baseName = String(path.split(/[\\/]/).pop() || path);
493
+ const row = el("div", "filerow " + categoryOf(name));
494
+ row.appendChild(el("span", "g", "◇"));
495
+ row.appendChild(el("span", "lab", labelOf(name)));
496
+ row.appendChild(el("span", "sep", " · "));
464
497
  if (OPENABLE_TOOLS.indexOf(name) !== -1) {
465
- const openLink = el("a", "fc-open", t("web.card.open"));
498
+ const openLink = el("a", "path", String(path).split(/[\\/]/).pop() || path);
466
499
  openLink.href = "/api/file?path=" + encodeURIComponent(path);
467
500
  openLink.target = "_blank";
468
501
  openLink.rel = "noopener noreferrer";
469
- openLink.title = t("web.card.open_title");
470
- actions.appendChild(openLink);
471
- const dlLink = el("a", "fc-open", t("web.card.download"));
472
- dlLink.href = "/api/file?path=" + encodeURIComponent(path) + "&download=1";
473
- dlLink.title = t("web.card.download_title");
474
- dlLink.setAttribute("download", baseName);
475
- actions.appendChild(dlLink);
502
+ openLink.title = t("web.card.open_title") + " · " + path;
503
+ row.appendChild(openLink);
504
+ const tail = String(path).split(/[\\/]/).slice(0, -1).join("/");
505
+ if (tail) row.appendChild(el("span", "tail", " · " + tail));
506
+ } else {
507
+ const s = el("span", "tail", path);
508
+ s.title = path;
509
+ row.appendChild(s);
476
510
  }
477
- const copyBtn = el("button", "fc-open", t("web.card.copy_path"));
511
+ const copyBtn = el("button", "copy-btn");
478
512
  copyBtn.type = "button";
479
513
  copyBtn.title = t("web.card.copy_title");
514
+ copyBtn.setAttribute("aria-label", t("web.card.copy_title"));
515
+ copyBtn.appendChild(icon("copy"));
480
516
  copyBtn.addEventListener("click", () => {
481
517
  copyText(path).then((ok) => {
482
- copyBtn.textContent = ok ? "✓ " + t("web.code.copied") : "⚠ " + t("web.code.copy_failed");
483
- window.setTimeout(() => {
484
- copyBtn.textContent = t("web.card.copy_path");
485
- }, 1400);
518
+ copyBtn.replaceChildren(icon(ok ? "check" : "copy"));
519
+ if (!ok) { const st = row.querySelector(".st"); if (st) st.textContent = " ⚠"; }
520
+ window.setTimeout(() => copyBtn.replaceChildren(icon("copy")), 1400);
486
521
  });
487
522
  });
488
- actions.appendChild(copyBtn);
489
- bottom.appendChild(actions);
490
- card.appendChild(bottom);
491
- if (/\.drawio(\.xml)?$/i.test(path)) {
492
- const hint = el("div", "fc-hint", t("web.card.drawio_hint"));
493
- card.appendChild(hint);
523
+ row.appendChild(copyBtn);
524
+ row.appendChild(el("span", "st", ok === false ? " ✗" : ok === true ? " ✓" : ""));
525
+ if (ok === false) row.dataset.fail = "1";
526
+ if (/\.drawio(\.xml)?$/i.test(path) && verbosity() === "verbose") {
527
+ row.appendChild(el("span", "detail", "↳ " + t("web.card.drawio_hint")));
494
528
  }
495
- appendTo(feed, card);
496
- return card;
529
+ appendTo(feed, row);
530
+ try { maybeGroupFilerow(feed, row); } catch (err) { /* fail-open: plain row stays */ }
531
+ return row;
497
532
  }
498
533
  if (LINK_TOOLS.indexOf(name) !== -1 && args.url) {
499
534
  const line = el("div", "linkline");
@@ -549,7 +584,7 @@ export function onToolEnd(data) {
549
584
  // a single `↳` cause instead of the whole model-facing body.
550
585
  if (data.success) {
551
586
  if (line) S.runNodes.push(line);
552
- const card = cardFor(data.name, args, data.sessionId);
587
+ const card = cardFor(data.name, args, data.sessionId, data.success);
553
588
  if (card) S.runNodes.push(card);
554
589
  S.runDuration += typeof data.durationMs === "number" ? data.durationMs : 0;
555
590
  } else {
@@ -558,7 +593,7 @@ export function onToolEnd(data) {
558
593
  }
559
594
  return;
560
595
  }
561
- if (data.success) cardFor(data.name, args, data.sessionId);
596
+ if (data.success) cardFor(data.name, args, data.sessionId, data.success);
562
597
  renderToolOutput(line, data.output);
563
598
  }
564
599
 
@@ -377,31 +377,23 @@ body.light .codeblock { border: 1px solid var(--border); }
377
377
  }
378
378
  body.light .imgcard .ph { background: var(--ph-light); }
379
379
  .imgcard .cap { padding: 6px 10px; color: var(--dim-a); font-size: 12px; }
380
- .filecard {
381
- margin-left: 26px; padding: 8px 12px; border-radius: 6px;
382
- background: var(--surface); border: 1px solid var(--border-2); color: var(--muted);
383
- display: flex; flex-direction: column; gap: 6px; align-items: stretch;
384
- font-size: 12.5px; max-width: 480px; min-width: 0;
385
- }
386
- .filecard .fc-top { display: flex; gap: 8px; align-items: baseline; min-width: 0; }
387
- .filecard .g { color: var(--cyan); flex: 0 0 auto; }
388
- .filecard .name {
389
- color: var(--fg); min-width: 0; overflow-wrap: anywhere; word-break: break-word;
390
- }
391
- .filecard .fc-bottom {
392
- display: flex; gap: 10px; align-items: center; justify-content: space-between;
393
- }
394
- .filecard .lab { color: var(--dim-a); min-width: 0; overflow-wrap: anywhere; }
395
- .filecard .fc-open {
396
- flex: 0 0 auto; font: inherit; font-size: 12px; line-height: 1.4;
397
- color: var(--accent); background: transparent; border: 1px solid var(--border-2);
398
- border-radius: 4px; padding: 2px 10px; cursor: pointer; text-decoration: none;
399
- }
400
- .filecard .fc-open:hover { border-color: var(--accent); text-decoration: none; }
401
- .filecard .fc-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
402
- .filecard button.fc-open { font: inherit; }
403
- .filecard .fc-actions { display: flex; gap: 6px; align-items: center; margin-left: auto; flex: 0 0 auto; }
404
- .filecard .fc-hint { color: var(--dim-a); font-size: 12px; }
380
+ .filerow { margin-left: 26px; display: flex; gap: 8px; align-items: baseline; min-width: 0; font-size: 12.5px; color: var(--muted); }
381
+ .filerow .g { flex: 0 0 auto; }
382
+ .filerow.recon .g, .filerow.recon .lab { color: var(--cyan); }
383
+ .filerow.mutate .g, .filerow.mutate .lab { color: var(--magenta); }
384
+ .filerow .sep { color: var(--dim-b); }
385
+ .filerow a.path { color: var(--accent); text-decoration: none; overflow-wrap: anywhere; word-break: break-word; }
386
+ .filerow a.path:hover { text-decoration: underline; }
387
+ .filerow .tail { color: var(--dim-a); overflow-wrap: anywhere; word-break: break-word; }
388
+ .filerow .copy-btn { background: transparent; border: 0; padding: 2px; color: var(--dim-a); cursor: pointer; opacity: 0; }
389
+ .filerow:hover .copy-btn, .filerow:focus-within .copy-btn { opacity: 1; }
390
+ @media (pointer: coarse) { .filerow .copy-btn { opacity: 1; } }
391
+ .filerow .copy-btn:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
392
+ .filerow .copy-btn .svg-icon { width: 14px; height: 14px; }
393
+ .filegroup { margin-left: 26px; font-size: 12.5px; color: var(--muted); }
394
+ .filegroup .head { background: transparent; border: 0; color: inherit; font: inherit; cursor: pointer; display: flex; gap: 6px; align-items: baseline; padding: 0; }
395
+ .filegroup .items.hidden { display: none; }
396
+ .filegroup .items > .filerow { margin-left: 0; }
405
397
  .linkline { margin-left: 26px; min-width: 0; overflow-wrap: anywhere; }
406
398
  .linkline a { color: var(--accent); text-decoration: none; overflow-wrap: anywhere; word-break: break-word; }
407
399
  .linkline a:hover { text-decoration: underline; }
@@ -613,7 +605,7 @@ body.light .sel > button { border: 1px solid var(--border); }
613
605
 
614
606
  /* new rows rise in softly (a bulk history render animates once as a whole —
615
607
  one shared fade, still cheap: opacity/transform only) */
616
- .feed > :is(.u, .md, .act, .think, .q, .filecard, .planblock, .imgcard, .linkline,
608
+ .feed > :is(.u, .md, .act, .think, .q, .filerow, .filegroup, .planblock, .imgcard, .linkline,
617
609
  .toolout, .metric, .detail, .metaln, .warnline, .errline, .cmdout, .sysline,
618
610
  .startbanner, .grouped, .msg-actions, .busy) {
619
611
  animation: rise .18s ease-out;
@@ -640,7 +632,7 @@ body.light .sel > button { border: 1px solid var(--border); }
640
632
  .overlay.open .modal { animation: rise .18s ease-out; }
641
633
 
642
634
  /* hover/active blends */
643
- .sel > button, .menu .mi, .stab, .q .qopt, .q .qsend, .filecard, .icon-btn,
635
+ .sel > button, .menu .mi, .stab, .q .qopt, .q .qsend, .filerow, .filegroup, .icon-btn,
644
636
  .btn, .linkbtn, .chip, .cb-copy, .act-btn, .copy-btn, .composer .attach-btn,
645
637
  .composer .send-btn, .dd-menu .si, .dd-menu .act, .cmdk-item, .toggle {
646
638
  transition: background-color .12s, color .12s, border-color .12s;
package/dist/i18n/en.json CHANGED
@@ -1500,13 +1500,12 @@
1500
1500
  "web.code.copy_failed": "copy failed",
1501
1501
  "web.msg.copy": "copy",
1502
1502
  "web.msg.copy_md": "copy as markdown",
1503
- "web.card.open": "open ↗",
1504
- "web.card.download": "download ⤓",
1505
- "web.card.copy_path": "copy path",
1506
1503
  "web.card.open_title": "Preview in a browser tab",
1507
- "web.card.download_title": "Download the file",
1508
1504
  "web.card.copy_title": "Copy the workspace path",
1509
1505
  "web.card.drawio_hint": "Draw.io diagram is XML — preview shows code; open it in draw.io / VS Code to see the scheme.",
1506
+ "web.card.files_one": "{count} file",
1507
+ "web.card.files_few": "{count} files",
1508
+ "web.card.files_many": "{count} files",
1510
1509
  "web.card.screenshot": "screenshot",
1511
1510
  "web.card.attached_image": "attached image",
1512
1511
  "web.card.browser_viewport": "browser · viewport",
package/dist/i18n/ru.json CHANGED
@@ -1476,13 +1476,12 @@
1476
1476
  "web.code.copy_failed": "не скопировано",
1477
1477
  "web.msg.copy": "копировать",
1478
1478
  "web.msg.copy_md": "копировать как markdown",
1479
- "web.card.open": "открыть ↗",
1480
- "web.card.download": "скачать ⤓",
1481
- "web.card.copy_path": "путь",
1482
1479
  "web.card.open_title": "Предпросмотр во вкладке браузера",
1483
- "web.card.download_title": "Скачать файл",
1484
1480
  "web.card.copy_title": "Скопировать путь в рабочей папке",
1485
1481
  "web.card.drawio_hint": "Диаграмма draw.io — это XML: предпросмотр покажет код, схему смотри в draw.io / VS Code.",
1482
+ "web.card.files_one": "{count} файл",
1483
+ "web.card.files_few": "{count} файла",
1484
+ "web.card.files_many": "{count} файлов",
1486
1485
  "web.card.screenshot": "снимок экрана",
1487
1486
  "web.card.attached_image": "прикреплённое изображение",
1488
1487
  "web.card.browser_viewport": "браузер · область просмотра",
package/dist/main.js CHANGED
@@ -4025,13 +4025,12 @@ Updated: {updatedAt}`,
4025
4025
  "web.code.copy_failed": "copy failed",
4026
4026
  "web.msg.copy": "copy",
4027
4027
  "web.msg.copy_md": "copy as markdown",
4028
- "web.card.open": "open ↗",
4029
- "web.card.download": "download ⤓",
4030
- "web.card.copy_path": "copy path",
4031
4028
  "web.card.open_title": "Preview in a browser tab",
4032
- "web.card.download_title": "Download the file",
4033
4029
  "web.card.copy_title": "Copy the workspace path",
4034
4030
  "web.card.drawio_hint": "Draw.io diagram is XML — preview shows code; open it in draw.io / VS Code to see the scheme.",
4031
+ "web.card.files_one": "{count} file",
4032
+ "web.card.files_few": "{count} files",
4033
+ "web.card.files_many": "{count} files",
4035
4034
  "web.card.screenshot": "screenshot",
4036
4035
  "web.card.attached_image": "attached image",
4037
4036
  "web.card.browser_viewport": "browser · viewport",
@@ -5905,13 +5904,12 @@ var init_ru = __esm(() => {
5905
5904
  "web.code.copy_failed": "не скопировано",
5906
5905
  "web.msg.copy": "копировать",
5907
5906
  "web.msg.copy_md": "копировать как markdown",
5908
- "web.card.open": "открыть ↗",
5909
- "web.card.download": "скачать ⤓",
5910
- "web.card.copy_path": "путь",
5911
5907
  "web.card.open_title": "Предпросмотр во вкладке браузера",
5912
- "web.card.download_title": "Скачать файл",
5913
5908
  "web.card.copy_title": "Скопировать путь в рабочей папке",
5914
5909
  "web.card.drawio_hint": "Диаграмма draw.io — это XML: предпросмотр покажет код, схему смотри в draw.io / VS Code.",
5910
+ "web.card.files_one": "{count} файл",
5911
+ "web.card.files_few": "{count} файла",
5912
+ "web.card.files_many": "{count} файлов",
5915
5913
  "web.card.screenshot": "снимок экрана",
5916
5914
  "web.card.attached_image": "прикреплённое изображение",
5917
5915
  "web.card.browser_viewport": "браузер · область просмотра",
@@ -11908,6 +11906,18 @@ var init_walker = __esm(() => {
11908
11906
  // src/modules/indexer/cache.ts
11909
11907
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync18, mkdirSync as mkdirSync11, rmSync } from "fs";
11910
11908
  import { join as join20 } from "path";
11909
+ function isIndexedFile(v) {
11910
+ if (typeof v !== "object" || v === null)
11911
+ return false;
11912
+ const f = v;
11913
+ return typeof f.path === "string" && typeof f.language === "string" && Array.isArray(f.exports) && typeof f.size === "number";
11914
+ }
11915
+ function isIndexResult(v) {
11916
+ if (typeof v !== "object" || v === null)
11917
+ return false;
11918
+ const r = v;
11919
+ return Array.isArray(r.files) && r.files.every(isIndexedFile) && typeof r.totalSize === "number";
11920
+ }
11911
11921
 
11912
11922
  class IndexCache {
11913
11923
  cachePath;
@@ -11927,7 +11937,7 @@ class IndexCache {
11927
11937
  return null;
11928
11938
  try {
11929
11939
  const parsed = JSON.parse(readFileSync14(this.cachePath, "utf-8"));
11930
- if (parsed.version !== CACHE_VERSION || !parsed.result)
11940
+ if (parsed.version !== CACHE_VERSION || !isIndexResult(parsed.result))
11931
11941
  return null;
11932
11942
  if (this.root !== undefined && parsed.root !== this.root)
11933
11943
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "2.13.1",
3
+ "version": "2.14.0",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {