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.
@@ -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 (!activePlaced) {
230
- activeList.appendChild(el("div", "si", t("web.sessions.none_active")));
231
- }
232
- if (!recentList.childNodes.length) {
233
- recentList.appendChild(el("div", "si", t("web.sessions.none")));
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
- setSessOpen(!sessDD.classList.contains("open"));
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
- setSessOpen(!sessDD.classList.contains("open"));
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
- if (S.state.busy) {
631
- // Don't drop the input silently: tell the user a turn is still running.
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 msg = (r.data && r.data.error) || r.status;
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
- // Delay the spinner so fast first tokens never flash it: `showBusy`
680
- // itself skips when a stream/think block already started.
681
- window.setTimeout(() => showBusy(feed), 180);
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
 
@@ -244,11 +244,14 @@ body[data-font="large"] { font-size: 17px; }
244
244
 
245
245
  /* chat */
246
246
  .chat { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; }
247
- .msgs { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 18px 22px; display: flex; flex-direction: column; gap: 11px; }
247
+ .msgs { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 0 22px; display: flex; flex-direction: column; gap: 11px; }
248
248
  .msgs > * { flex-shrink: 0; min-height: fit-content; }
249
249
  /* messages live inside a per-tab `.feed` wrapper; it must mirror the `.msgs`
250
- column gap so turns are separated instead of running together */
251
- .feed { display: flex; flex-direction: column; gap: 11px; }
250
+ column gap so turns are separated instead of running together.
251
+ Vertical breathing lives on `.feed`, NOT on the scroller: sticky edge fades
252
+ are constrained to the content box, so scroller padding would offset them
253
+ from the header/composer edges. */
254
+ .feed { display: flex; flex-direction: column; gap: 11px; padding: 18px 0; }
252
255
  .feed > * { flex-shrink: 0; min-height: fit-content; }
253
256
  .u {
254
257
  color: var(--u-fg); background: var(--u-bg); border-left: 2px solid var(--u-border);
@@ -270,7 +273,7 @@ body[data-font="large"] { font-size: 17px; }
270
273
  font-size: 12.5px; overflow-wrap: anywhere;
271
274
  }
272
275
  .metaln.warn { color: var(--yellow); }
273
- .act { display: flex; gap: 8px; min-width: 0; }
276
+ .act { display: flex; gap: 8px; min-width: 0; line-height: 1.35; }
274
277
  .act .lab { white-space: nowrap; }
275
278
  .act .args { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
276
279
  .act.recon .g, .act.recon .lab { color: var(--cyan); }
@@ -311,7 +314,15 @@ body.light .code { border: 1px solid var(--border); }
311
314
  .toolout .more { color: var(--dim-b); cursor: pointer; font-size: 12px; }
312
315
  .toolout .more:hover { color: var(--dim-a); }
313
316
 
314
- /* markdown answers (streamed by md.js) — lines and glyphs, no boxes */
317
+ /* markdown answers (streamed by md.js) — lines and glyphs, no boxes.
318
+ Top-level answers carry a quiet accent rail (mirrors the user's `> ` block),
319
+ so the finished message reads instantly apart from the dim work process
320
+ (tool/think rows stay plain). Scoped to `.feed >` — code blocks inside keep
321
+ their own styling. */
322
+ .feed > .md, .feed > .ans {
323
+ border-left: 2px solid var(--accent);
324
+ padding-left: 12px;
325
+ }
315
326
  .md { color: var(--ans); overflow-wrap: anywhere; }
316
327
  .md > * { flex-shrink: 0; }
317
328
  .md p { margin: 4px 0; white-space: pre-wrap; }
@@ -391,7 +402,14 @@ body.light .codeblock { border: 1px solid var(--border); }
391
402
  }
392
403
  body.light .imgcard .ph { background: var(--ph-light); }
393
404
  .imgcard .cap { padding: 6px 10px; color: var(--dim-a); font-size: 12px; }
394
- .filerow { margin-left: 26px; display: flex; gap: 8px; align-items: baseline; min-width: 0; font-size: 12.5px; color: var(--muted); }
405
+ .filerow { margin-left: 26px; display: flex; gap: 8px; align-items: baseline; min-width: 0; font-size: 12.5px; line-height: 1.35; color: var(--muted); }
406
+ /* Compact tool lists: consecutive action/file rows sit tight (~4px visual),
407
+ // while turns keep the airy 11px feed gap. Negative margin compensates the
408
+ // `.feed { gap: 11px }` only between siblings in a run. */
409
+ .feed > .filerow + .filerow,
410
+ .feed > .act + .act,
411
+ .feed > .filerow + .act,
412
+ .feed > .act + .filerow { margin-top: -7px; }
395
413
  .filerow .g { flex: 0 0 auto; }
396
414
  .filerow.recon .g, .filerow.recon .lab { color: var(--cyan); }
397
415
  .filerow.mutate .g, .filerow.mutate .lab { color: var(--magenta); }
@@ -404,10 +422,6 @@ body.light .imgcard .ph { background: var(--ph-light); }
404
422
  @media (pointer: coarse) { .filerow .copy-btn { opacity: 1; } }
405
423
  .filerow .copy-btn:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
406
424
  .filerow .copy-btn .svg-icon { width: 14px; height: 14px; }
407
- .filegroup { margin-left: 26px; font-size: 12.5px; color: var(--muted); }
408
- .filegroup .head { background: transparent; border: 0; color: inherit; font: inherit; cursor: pointer; display: flex; gap: 6px; align-items: baseline; padding: 0; }
409
- .filegroup .items.hidden { display: none; }
410
- .filegroup .items > .filerow { margin-left: 0; }
411
425
  .linkline { margin-left: 26px; min-width: 0; overflow-wrap: anywhere; }
412
426
  .linkline a { color: var(--accent); text-decoration: none; overflow-wrap: anywhere; word-break: break-word; }
413
427
  .linkline a:hover { text-decoration: underline; }
@@ -522,6 +536,16 @@ body.light .sel > button { border: 1px solid var(--border); }
522
536
  color: var(--fg); font: inherit; font-size: 12.5px; padding: 4px 8px;
523
537
  }
524
538
  .menu .ctx-custom .linkbtn { flex: 0 0 auto; }
539
+ /* Dropdown search fields (composer model menu, sessions menu): sticky on top
540
+ so the query stays visible while the list scrolls underneath. */
541
+ .menu .searchbox {
542
+ margin-bottom: 4px; position: sticky; top: 0; z-index: 1;
543
+ }
544
+ .model-menu-list { display: contents; }
545
+ .dd-menu .searchbox {
546
+ margin: 2px 2px 6px; width: calc(100% - 4px);
547
+ position: sticky; top: 0; z-index: 1;
548
+ }
525
549
 
526
550
  .inp {
527
551
  width: 100%; padding: 12px 14px; resize: none; display: block;
@@ -619,7 +643,7 @@ body.light .sel > button { border: 1px solid var(--border); }
619
643
 
620
644
  /* new rows rise in softly (a bulk history render animates once as a whole —
621
645
  one shared fade, still cheap: opacity/transform only) */
622
- .feed > :is(.u, .md, .act, .think, .q, .filerow, .filegroup, .planblock, .imgcard, .linkline,
646
+ .feed > :is(.u, .md, .act, .think, .q, .filerow, .planblock, .imgcard, .linkline,
623
647
  .toolout, .metric, .detail, .metaln, .warnline, .errline, .cmdout, .sysline,
624
648
  .startbanner, .grouped, .msg-actions, .busy) {
625
649
  animation: rise .18s ease-out;
@@ -646,20 +670,25 @@ body.light .sel > button { border: 1px solid var(--border); }
646
670
  .overlay.open .modal { animation: rise .18s ease-out; }
647
671
 
648
672
  /* hover/active blends */
649
- .sel > button, .menu .mi, .stab, .q .qopt, .q .qsend, .filerow, .filegroup, .icon-btn,
673
+ .sel > button, .menu .mi, .stab, .q .qopt, .q .qsend, .filerow, .icon-btn,
650
674
  .btn, .linkbtn, .chip, .cb-copy, .act-btn, .copy-btn, .composer .attach-btn,
651
675
  .composer .send-btn, .dd-menu .si, .dd-menu .act, .cmdk-item, .toggle {
652
676
  transition: background-color .12s, color .12s, border-color .12s;
653
677
  }
654
678
  .composer { transition: border-color .15s, box-shadow .15s; }
655
679
 
656
- /* scroll edge fades on the transcript (classes from the feed scroll hook) */
680
+ /* scroll edge fades on the transcript (classes from the feed scroll hook).
681
+ Sticky in-flow pseudos glued exactly to the scrollport edges: they sit in
682
+ the scroller's padding-free content box (breathing lives on `.feed`), so
683
+ `top: 0` / `bottom: 0` meet the header above and the composer below with
684
+ no visible offset. Negative margins neutralize their flow height plus the
685
+ flex gap, so hidden fades take no space. */
657
686
  .msgs::before, .msgs::after {
658
687
  content: ""; position: sticky; display: block; height: 22px; flex: 0 0 auto;
659
688
  pointer-events: none; z-index: 5; opacity: 0; transition: opacity .25s;
660
689
  }
661
- .msgs::before { top: 0; margin-bottom: -22px; background: linear-gradient(var(--bg), transparent); }
662
- .msgs::after { bottom: 0; margin-top: -22px; background: linear-gradient(transparent, var(--bg)); }
690
+ .msgs::before { top: 0; margin-bottom: -33px; background: linear-gradient(var(--bg), transparent); }
691
+ .msgs::after { bottom: 0; margin-top: -33px; background: linear-gradient(transparent, var(--bg)); }
663
692
  .msgs.scrolled::before { opacity: 1; }
664
693
  .msgs.unfollowed::after { opacity: 1; }
665
694
 
@@ -734,12 +763,16 @@ body.light .sel > button { border: 1px solid var(--border); }
734
763
  @keyframes rlFlash { 0% { filter: brightness(1.9); } 100% { filter: brightness(1); } }
735
764
 
736
765
  /* ---- sidebar — plan / usage / environment sections ---- */
766
+ /* Sidebar never scrolls horizontally, in any tab: long paths / model names /
767
+ ids wrap or ellipsize inside their rows, never widening the pane. */
737
768
  .sb {
738
769
  width: 340px; min-width: 340px; display: flex; flex-direction: column;
739
770
  border-left: 1px solid var(--border); background: var(--sb-bg); min-height: 0;
771
+ overflow-x: hidden;
740
772
  }
741
773
  .sec {
742
- display: flex; flex-direction: column; min-height: 0;
774
+ display: flex; flex-direction: column; min-height: 0; min-width: 0;
775
+ max-width: 100%; overflow-x: hidden;
743
776
  border-bottom: 1px solid var(--border);
744
777
  }
745
778
  .sec:last-child { border-bottom: none; }
@@ -751,8 +784,8 @@ body.light .sel > button { border: 1px solid var(--border); }
751
784
  .sec-h:hover { color: var(--sec-h-hover); }
752
785
  .sec-h .n { margin-left: auto; color: var(--dim-b); }
753
786
  .sec-body {
754
- overflow-y: auto; padding: 0 16px 12px;
755
- min-height: 0;
787
+ overflow-y: auto; overflow-x: hidden; padding: 0 16px 12px;
788
+ min-height: 0; min-width: 0; max-width: 100%;
756
789
  }
757
790
  .sec-body.collapsed { display: none; }
758
791
  /* plan section takes the lion's share and scrolls */
@@ -786,7 +819,7 @@ body.light .sel > button { border: 1px solid var(--border); }
786
819
  }
787
820
  .sb-name .sb-ico svg { width: 14px; height: 14px; color: var(--accent); }
788
821
 
789
- .sb-pane { display: flex; flex-direction: column; flex: 1; min-height: 0; }
822
+ .sb-pane { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; max-width: 100%; overflow-x: hidden; }
790
823
  .sb-pane[hidden] { display: none; }
791
824
 
792
825
  /* master-detail expansion */
@@ -794,25 +827,26 @@ body.light .sel > button { border: 1px solid var(--border); }
794
827
  .sb.sb-expanded { width: 680px; min-width: 680px; }
795
828
  #pane-trace { flex-direction: row; }
796
829
  .sb-expanded #pane-trace { position: relative; }
797
- .trace-list { flex: 1; min-width: 0; overflow-y: auto; padding: 6px 10px 12px; display: flex; flex-direction: column; gap: 1px; }
798
- .trace-turn { color: var(--dim-c); font-size: 11px; margin: 8px 6px 2px; }
799
- .trc { display: flex; gap: 8px; align-items: baseline; padding: 4px 6px; font-size: 12px; color: var(--muted); border-radius: 4px; cursor: pointer; }
830
+ .trace-list { flex: 1; min-width: 0; max-width: 100%; overflow-y: auto; overflow-x: hidden; padding: 6px 10px 12px; display: flex; flex-direction: column; gap: 1px; }
831
+ .trace-turn { color: var(--dim-c); font-size: 11px; margin: 8px 6px 2px; overflow-wrap: anywhere; }
832
+ .trc { display: flex; gap: 8px; align-items: baseline; padding: 4px 6px; font-size: 12px; color: var(--muted); border-radius: 4px; cursor: pointer; min-width: 0; max-width: 100%; overflow: hidden; }
800
833
  .trc:hover { background: var(--hover-bg); }
801
834
  .trc.sel { background: var(--accent-bg); }
802
835
  .trc .g { flex: 0 0 auto; }
803
836
  .trc.recon .g { color: var(--cyan); } .trc.mutate .g { color: var(--purple); }
804
837
  .trc.execute .g { color: var(--accent); } .trc.meta .g { color: var(--meta); } .trc.phase .g { color: var(--think); }
805
- .trc .lab { color: var(--fg); white-space: nowrap; }
838
+ .trc .lab { color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 0 1 auto; }
806
839
  .trc .args { color: var(--args); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1; }
807
840
  .trc .t { color: var(--dim-b); margin-left: auto; flex: 0 0 auto; }
808
841
  .trc.warn .g, .trc.warn .lab { color: var(--yellow); }
809
842
  .trc.phase .lab { color: var(--think); }
810
- .trc-detail { display: flex; flex-direction: column; flex: 0 0 340px; min-width: 0; border-left: 1px solid var(--border); }
843
+ .trc-detail { display: flex; flex-direction: column; flex: 0 0 340px; min-width: 0; max-width: 100%; overflow-x: hidden; border-left: 1px solid var(--border); }
811
844
  .trc-detail[hidden] { display: none; }
812
- .detail-h { display: flex; gap: 8px; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); color: var(--fg); font-size: 12.5px; flex-shrink: 0; }
845
+ .detail-h { display: flex; gap: 8px; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); color: var(--fg); font-size: 12.5px; flex-shrink: 0; min-width: 0; max-width: 100%; overflow: hidden; }
813
846
  .detail-h .g { color: var(--accent); }
814
847
  .detail-h .x { margin-left: auto; color: var(--dim-b); cursor: pointer; }
815
- .detail-b { padding: 10px 14px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }
848
+ .detail-b { padding: 10px 14px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; overflow-x: hidden; min-width: 0; max-width: 100%; }
849
+ .detail-b .code { max-width: 100%; }
816
850
  .detail-b h5 { margin: 0 0 4px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--sec-h); font-weight: 600; }
817
851
  .map-sec { flex: 1 1 auto; }
818
852
  .map-reindex { margin-top: 10px; font: inherit; font-size: 12px; color: var(--accent); background: transparent; border: 1px solid var(--border-2); border-radius: 5px; padding: 4px 10px; cursor: pointer; width: fit-content; }
@@ -832,8 +866,15 @@ body.light .sel > button { border: 1px solid var(--border); }
832
866
  .plan-step::before { content: ""; position: absolute; left: 4px; top: 0; bottom: 0; width: 1px; background: var(--plan-line); }
833
867
  .plan-step .sub { display: block; color: var(--dim-b); font-size: 11.5px; padding-left: 0; }
834
868
 
835
- .kv { display: flex; justify-content: space-between; padding: 2px 0; color: var(--muted); gap: 10px; }
836
- .kv b { font-weight: 400; color: var(--fg); text-align: right; }
869
+ .kv { display: flex; justify-content: space-between; padding: 2px 0; color: var(--muted); gap: 10px; min-width: 0; max-width: 100%; }
870
+ .kv > * { min-width: 0; }
871
+ /* Подпись строки никогда не сжимается — иначе её текст наезжает на значение. */
872
+ .kv > span:first-child { flex-shrink: 0; }
873
+ .kv b { font-weight: 400; color: var(--fg); text-align: right; overflow-wrap: anywhere; word-break: break-word; min-width: 0; max-width: 100%; }
874
+ /* Корень карты: подпись сверху, длинный путь снизу на всю ширину. */
875
+ .map-path { display: flex; flex-direction: column; gap: 2px; padding: 2px 0; min-width: 0; max-width: 100%; color: var(--muted); }
876
+ .map-path .dim { color: var(--dim-a); }
877
+ .map-path b { font-weight: 400; color: var(--fg); overflow-wrap: anywhere; word-break: break-word; min-width: 0; max-width: 100%; }
837
878
  body.light .kv b { color: var(--fg); }
838
879
  .kv .dim { color: var(--dim-a); }
839
880
  .kv .wn { color: var(--yellow); }
@@ -1372,7 +1413,8 @@ button.toggle:disabled { opacity: .5; cursor: default; }
1372
1413
  border-left: 1px solid var(--border); border-top: none;
1373
1414
  box-shadow: var(--shadow-dd);
1374
1415
  }
1375
- .msgs { padding: 14px; }
1416
+ .msgs { padding: 0 14px; }
1417
+ .feed { padding: 14px 0; }
1376
1418
  .input-area { margin: 10px 14px 12px; }
1377
1419
  .inp-meta { flex-wrap: wrap; gap: 8px; }
1378
1420
  }
@@ -1395,7 +1437,8 @@ button.toggle:disabled { opacity: .5; cursor: default; }
1395
1437
  .content-col { padding: 14px; }
1396
1438
  .row { font-size: 12.5px; }
1397
1439
  .input-area { margin: 8px 10px 10px; }
1398
- .msgs { padding: 12px 10px; }
1440
+ .msgs { padding: 0 10px; }
1441
+ .feed { padding: 12px 0; }
1399
1442
  /* narrow phones: rail stacks vertically, track takes the full row */
1400
1443
  .rail-wrap.l-line, .rail-wrap.l-meta {
1401
1444
  flex-direction: column; align-items: stretch; gap: 6px;
package/dist/i18n/en.json CHANGED
@@ -1087,6 +1087,18 @@
1087
1087
  "web.sidebar.trace_text": "composing… {count} chars",
1088
1088
  "web.sidebar.trace_plan": "plan update",
1089
1089
  "web.sidebar.trace_question": "question",
1090
+ "web.sidebar.trace_status": "status",
1091
+ "web.sidebar.trace_log": "log · {level}",
1092
+ "web.sidebar.trace_retract": "draft retracted",
1093
+ "web.sidebar.trace_index": "index · {phase}",
1094
+ "web.sidebar.trace_critic": "critic · {status}",
1095
+ "web.sidebar.trace_moe": "moe",
1096
+ "web.sidebar.trace_compaction": "compaction",
1097
+ "web.sidebar.trace_probe": "context probe",
1098
+ "web.sidebar.trace_usage": "llm usage",
1099
+ "web.sidebar.trace_audit": "audit",
1100
+ "web.sidebar.trace_claim": "claim retry · {kind}",
1101
+ "web.sidebar.trace_output": "output",
1090
1102
  "web.sidebar.detail_metadata": "metadata",
1091
1103
  "web.sidebar.detail_arguments": "arguments",
1092
1104
  "web.sidebar.detail_output": "output",
@@ -1101,6 +1113,10 @@
1101
1113
  "web.sidebar.detail_time": "time",
1102
1114
  "web.sidebar.detail_tool_call": "tool call",
1103
1115
  "web.sidebar.detail_session": "session",
1116
+ "web.sidebar.detail_turn": "turn",
1117
+ "web.sidebar.detail_iteration": "iteration",
1118
+ "web.sidebar.detail_provenance": "session-log provenance",
1119
+ "web.sidebar.detail_provenance_count": "{n} assistant messages in log",
1104
1120
  "web.env.dir": "dir",
1105
1121
  "web.env.agents": "AGENTS.md",
1106
1122
  "web.env.map": "map",
@@ -1420,9 +1436,10 @@
1420
1436
  "web.input.shift_enter": "Shift+Enter — newline",
1421
1437
  "web.input.esc": "Esc — stop",
1422
1438
  "web.input.attach": "Ctrl+V — attach",
1423
- "web.input.busy": "still working — Esc to stop",
1424
1439
  "web.input.send": "send",
1425
1440
  "web.input.stop": "stop",
1441
+ "web.input.queued": "queued · position {n}",
1442
+ "web.input.queue_full": "message queue is full — wait for the turn to end",
1426
1443
  "web.empty.title": "new chat",
1427
1444
  "web.empty.hint": "type a message below · / — commands · model and context are chosen above the input",
1428
1445
  "web.attach.title": "attach image",
@@ -1439,6 +1456,7 @@
1439
1456
  "web.sessions.none_active": "— no active session —",
1440
1457
  "web.sessions.none": "— none —",
1441
1458
  "web.sessions.delete": "delete",
1459
+ "web.sessions.search": "search sessions…",
1442
1460
  "web.sessions.create_failed": "session create failed",
1443
1461
  "web.sessions.delete_failed": "delete failed",
1444
1462
  "web.sessions.confirm_title": "Delete session?",
@@ -1451,6 +1469,7 @@
1451
1469
  "web.selectors.switch_busy": "switch in progress, please wait",
1452
1470
  "web.selectors.reasoning_off": "off (backend ignores it)",
1453
1471
  "web.selectors.no_models": "no models",
1472
+ "web.selectors.search_models": "search models…",
1454
1473
  "web.selectors.refresh_list": "↻ refresh list",
1455
1474
  "web.selectors.custom": "custom…",
1456
1475
  "web.selectors.current": "current",
package/dist/i18n/ru.json CHANGED
@@ -1090,6 +1090,18 @@
1090
1090
  "web.sidebar.trace_text": "печатает… {count} симв.",
1091
1091
  "web.sidebar.trace_plan": "обновление плана",
1092
1092
  "web.sidebar.trace_question": "вопрос",
1093
+ "web.sidebar.trace_status": "статус",
1094
+ "web.sidebar.trace_log": "лог · {level}",
1095
+ "web.sidebar.trace_retract": "черновик отозван",
1096
+ "web.sidebar.trace_index": "индекс · {phase}",
1097
+ "web.sidebar.trace_critic": "критик · {status}",
1098
+ "web.sidebar.trace_moe": "moe",
1099
+ "web.sidebar.trace_compaction": "компакция",
1100
+ "web.sidebar.trace_probe": "проба контекста",
1101
+ "web.sidebar.trace_usage": "расход llm",
1102
+ "web.sidebar.trace_audit": "аудит",
1103
+ "web.sidebar.trace_claim": "перепроверка · {kind}",
1104
+ "web.sidebar.trace_output": "вывод",
1093
1105
  "web.sidebar.detail_metadata": "метаданные",
1094
1106
  "web.sidebar.detail_arguments": "аргументы",
1095
1107
  "web.sidebar.detail_output": "вывод",
@@ -1104,6 +1116,10 @@
1104
1116
  "web.sidebar.detail_time": "время",
1105
1117
  "web.sidebar.detail_tool_call": "вызов тула",
1106
1118
  "web.sidebar.detail_session": "сессия",
1119
+ "web.sidebar.detail_turn": "ход",
1120
+ "web.sidebar.detail_iteration": "итерация",
1121
+ "web.sidebar.detail_provenance": "сверка с логом",
1122
+ "web.sidebar.detail_provenance_count": "assistant-сообщений в логе: {n}",
1107
1123
  "web.env.dir": "директория",
1108
1124
  "web.env.agents": "AGENTS.md",
1109
1125
  "web.env.map": "карта",
@@ -1396,9 +1412,10 @@
1396
1412
  "web.input.shift_enter": "Shift+Enter — новая строка",
1397
1413
  "web.input.esc": "Esc — стоп",
1398
1414
  "web.input.attach": "Ctrl+V — вложить",
1399
- "web.input.busy": "ещё работаю — Esc для остановки",
1400
1415
  "web.input.send": "отправить",
1401
1416
  "web.input.stop": "остановить",
1417
+ "web.input.queued": "в очереди · позиция {n}",
1418
+ "web.input.queue_full": "очередь сообщений полна — дождись конца хода",
1402
1419
  "web.empty.title": "новый чат",
1403
1420
  "web.empty.hint": "введи сообщение ниже · / — команды · модель и контекст выбираются над полем",
1404
1421
  "web.attach.title": "прикрепить изображение",
@@ -1415,6 +1432,7 @@
1415
1432
  "web.sessions.none_active": "— нет активной сессии —",
1416
1433
  "web.sessions.none": "— нет —",
1417
1434
  "web.sessions.delete": "удалить",
1435
+ "web.sessions.search": "поиск сессий…",
1418
1436
  "web.sessions.create_failed": "не удалось создать сессию",
1419
1437
  "web.sessions.delete_failed": "не удалось удалить",
1420
1438
  "web.sessions.confirm_title": "Удалить сессию?",
@@ -1427,6 +1445,7 @@
1427
1445
  "web.selectors.switch_busy": "переключение уже идёт, подождите",
1428
1446
  "web.selectors.reasoning_off": "выкл (бэкенд игнорирует)",
1429
1447
  "web.selectors.no_models": "нет моделей",
1448
+ "web.selectors.search_models": "поиск моделей…",
1430
1449
  "web.selectors.refresh_list": "↻ обновить список",
1431
1450
  "web.selectors.custom": "другое…",
1432
1451
  "web.selectors.current": "текущее",