micro-models-agent 2.12.0 → 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.
@@ -103,6 +103,7 @@
103
103
  securityBashBlock: false,
104
104
  securityFlagsBlock: false,
105
105
  securityPathsRestrict: true,
106
+ securityDetail: { bash: {}, paths: {}, flags: {}, network: {}, advanced: {} },
106
107
  },
107
108
  };
108
109
 
@@ -118,6 +119,12 @@
118
119
  state.answers.contextWindow = r.data.defaults.contextWindow || state.answers.contextWindow;
119
120
  state.answers.maxToolIterations =
120
121
  r.data.defaults.maxToolIterations || state.answers.maxToolIterations;
122
+ if (r.data.defaults.security) {
123
+ state.answers.securityDetail = Object.assign(
124
+ state.answers.securityDetail,
125
+ r.data.defaults.security,
126
+ );
127
+ }
121
128
  }
122
129
  const first = (r.data.providers || [])[0];
123
130
  if (first) {
@@ -137,6 +144,7 @@
137
144
  // --- rendering -------------------------------------------------------------
138
145
 
139
146
  function render() {
147
+ closePopup();
140
148
  const wrap = h("div", "setup-wrap");
141
149
  const title = h("h1", "setup-h");
142
150
  title.appendChild(h("span", "glyph", "◇"));
@@ -258,6 +266,27 @@
258
266
  });
259
267
  box.appendChild(field(tr("web.setup.provider.base_url"), input));
260
268
 
269
+ const actions = h("div", "setup-row-inline");
270
+ const test = h("button", "setup-btn", tr("web.setup.provider.test"));
271
+ test.type = "button";
272
+ test.addEventListener("click", async () => {
273
+ if (!state.answers.apiBase) {
274
+ setMsg(box, "err", tr("web.setup.provider.required"));
275
+ return;
276
+ }
277
+ test.disabled = true;
278
+ setMsg(box, "", tr("web.setup.provider.testing"));
279
+ const r = await api("/api/setup/models", {
280
+ method: "POST",
281
+ body: { baseUrl: state.answers.apiBase, apiKey: state.answers.apiKey },
282
+ });
283
+ test.disabled = false;
284
+ const models = (r.ok && r.data && r.data.models) || [];
285
+ if (r.ok && models.length) setMsg(box, "ok", tr("web.setup.provider.test_ok"));
286
+ else setMsg(box, "warn", tr("web.setup.provider.test_fail"));
287
+ });
288
+ actions.appendChild(test);
289
+
261
290
  if (state.answers.provider === "openai-compat" || state.answers.provider === "lmstudio") {
262
291
  const scan = h("button", "setup-btn", tr("web.setup.provider.scan"));
263
292
  scan.type = "button";
@@ -291,8 +320,11 @@
291
320
  results.appendChild(b);
292
321
  });
293
322
  });
294
- box.appendChild(scan);
323
+ actions.appendChild(scan);
324
+ box.appendChild(actions);
295
325
  box.appendChild(results);
326
+ } else {
327
+ box.appendChild(actions);
296
328
  }
297
329
  box.appendChild(messageEl());
298
330
  return box;
@@ -393,6 +425,38 @@
393
425
  });
394
426
  box.appendChild(field(tr("web.setup.context.contextWindow"), cw));
395
427
 
428
+ const detect = h("button", "setup-btn", tr("web.setup.context.detect"));
429
+ detect.type = "button";
430
+ detect.addEventListener("click", async () => {
431
+ if (!state.answers.apiBase || !state.answers.model) {
432
+ setMsg(box, "err", tr("web.setup.context.detect_fail"));
433
+ return;
434
+ }
435
+ detect.disabled = true;
436
+ setMsg(box, "", tr("web.setup.context.detecting"));
437
+ const r = await api("/api/setup/context", {
438
+ method: "POST",
439
+ body: {
440
+ baseUrl: state.answers.apiBase,
441
+ apiKey: state.answers.apiKey,
442
+ model: state.answers.model,
443
+ providerType: state.answers.provider,
444
+ },
445
+ });
446
+ detect.disabled = false;
447
+ const n = r.ok && r.data && r.data.contextWindow;
448
+ if (r.ok && r.data && r.data.ok && typeof n === "number" && n > 0) {
449
+ state.answers.contextWindow = n;
450
+ cw.value = String(n);
451
+ setMsg(box, "ok", tr("web.setup.context.detect_ok", { count: n }));
452
+ } else {
453
+ setMsg(box, "warn", tr("web.setup.context.detect_fail"));
454
+ }
455
+ });
456
+ const row = h("div", "setup-row-inline");
457
+ row.appendChild(detect);
458
+ box.appendChild(row);
459
+
396
460
  const it = h("input", "setup-input");
397
461
  it.type = "number";
398
462
  it.min = "1";
@@ -408,25 +472,219 @@
408
472
  function renderSecurity() {
409
473
  const box = stepBody("security");
410
474
  const toggles = [
411
- ["securityPathsRestrict", "web.setup.security.paths"],
412
- ["securityBashBlock", "web.setup.security.bash"],
413
- ["securityFlagsBlock", "web.setup.security.flags"],
475
+ ["securityPathsRestrict", "web.setup.security.paths", "web.setup.security.paths_desc", "paths"],
476
+ ["securityBashBlock", "web.setup.security.bash", "web.setup.security.bash_desc", "bash"],
477
+ ["securityFlagsBlock", "web.setup.security.flags", "web.setup.security.flags_desc", "flags"],
414
478
  ];
415
- toggles.forEach(([key, labelKey]) => {
416
- const row = h("label", "setup-toggle");
479
+ toggles.forEach(([key, labelKey, descKey, group]) => {
480
+ const row = h("div", "setup-sec-row");
481
+ const label = h("label", "setup-toggle");
417
482
  const cb = h("input");
418
483
  cb.type = "checkbox";
419
484
  cb.checked = Boolean(state.answers[key]);
420
485
  cb.addEventListener("change", () => {
421
486
  state.answers[key] = cb.checked;
422
487
  });
423
- row.appendChild(cb);
424
- row.appendChild(h("span", null, tr(labelKey)));
488
+ label.appendChild(cb);
489
+ const texts = h("span", "setup-toggle-texts");
490
+ texts.appendChild(h("span", "t", tr(labelKey)));
491
+ const desc = tr(descKey);
492
+ if (desc !== descKey) texts.appendChild(h("span", "d", desc));
493
+ label.appendChild(texts);
494
+ row.appendChild(label);
495
+ row.appendChild(detailButton(group, tr(labelKey)));
425
496
  box.appendChild(row);
426
497
  });
498
+
499
+ const advRow = h("div", "setup-sec-row");
500
+ const advTexts = h("span", "setup-toggle setup-adv-label");
501
+ advTexts.appendChild(h("span", "t", tr("web.setup.security.advanced")));
502
+ advRow.appendChild(advTexts);
503
+ advRow.appendChild(detailButton("advanced", tr("web.setup.security.advanced")));
504
+ box.appendChild(advRow);
427
505
  return box;
428
506
  }
429
507
 
508
+ // Описание поп-апов тонкой настройки: путь в securityDetail + тип поля.
509
+ const SECURITY_POPUPS = {
510
+ paths: [
511
+ ["paths.denied", "list", "web.setup.security.denied"],
512
+ ["paths.allowed", "list", "web.setup.security.allowed"],
513
+ ],
514
+ bash: [
515
+ ["bash.blacklist", "list", "web.setup.security.blacklist"],
516
+ ["bash.whitelist", "list", "web.setup.security.whitelist"],
517
+ ["bash.logCommands", "bool", "web.setup.security.log_commands"],
518
+ ],
519
+ flags: [
520
+ ["flags.dangerousFlags", "list", "web.setup.security.dangerous_flags"],
521
+ ["flags.dangerousOperators", "list", "web.setup.security.dangerous_operators"],
522
+ ],
523
+ advanced: [
524
+ ["network.enabled", "bool", "web.setup.security.network_enabled"],
525
+ ["network.deniedDomains", "list", "web.setup.security.denied_domains"],
526
+ ["network.allowedDomains", "list", "web.setup.security.allowed_domains"],
527
+ ["network.requestTimeout", "int", "web.setup.security.request_timeout"],
528
+ ["network.maxResponseSize", "int", "web.setup.security.max_response_size"],
529
+ ["advanced.maxRecursionDepth", "int", "web.setup.security.max_recursion"],
530
+ ["advanced.maxFileOperations", "int", "web.setup.security.max_file_ops"],
531
+ ["advanced.maxRequestsPerMinute", "int", "web.setup.security.rate_rpm"],
532
+ ["advanced.maxParallelTasks", "int", "web.setup.security.rate_parallel"],
533
+ ["advanced.contentScanEnabled", "bool", "web.setup.security.content_scan"],
534
+ ["advanced.sessionEncryptionEnabled", "bool", "web.setup.security.session_encryption"],
535
+ ["advanced.auditNotifierEnabled", "bool", "web.setup.security.audit_enabled"],
536
+ ["advanced.auditNotifierMinSeverity", "enum", "web.setup.security.audit_severity"],
537
+ ["advanced.auditNotifierEventTypes", "list", "web.setup.security.audit_events"],
538
+ ["advanced.auditNotifierMaxRetries", "int", "web.setup.security.audit_retries"],
539
+ ["advanced.auditNotifierWebhookTimeout", "int", "web.setup.security.audit_timeout"],
540
+ ],
541
+ };
542
+ const SEVERITIES = ["low", "medium", "high", "critical"];
543
+ const GROUP_KEYS = {
544
+ paths: ["paths"],
545
+ bash: ["bash"],
546
+ flags: ["flags"],
547
+ advanced: ["network", "advanced"],
548
+ };
549
+
550
+ function getPath(obj, path) {
551
+ return path.split(".").reduce((o, k) => (o == null ? undefined : o[k]), obj);
552
+ }
553
+ function setPath(obj, path, value) {
554
+ const keys = path.split(".");
555
+ let cur = obj;
556
+ for (let i = 0; i < keys.length - 1; i++) {
557
+ if (typeof cur[keys[i]] !== "object" || cur[keys[i]] === null) cur[keys[i]] = {};
558
+ cur = cur[keys[i]];
559
+ }
560
+ cur[keys[keys.length - 1]] = value;
561
+ }
562
+
563
+ function detailButton(group, title) {
564
+ const btn = h("button", "setup-detail-btn");
565
+ btn.type = "button";
566
+ btn.appendChild(h("span", "g", "⚙"));
567
+ btn.appendChild(h("span", "t", tr("web.setup.security.configure")));
568
+ btn.title = title;
569
+ btn.addEventListener("click", (e) => {
570
+ e.preventDefault();
571
+ openSecurityPopup(group, title);
572
+ });
573
+ return btn;
574
+ }
575
+
576
+ let popup = null;
577
+
578
+ function openSecurityPopup(group, title) {
579
+ const overlay = h("div", "setup-overlay");
580
+ const modal = h("div", "setup-modal");
581
+ modal.setAttribute("role", "dialog");
582
+ modal.setAttribute("aria-modal", "true");
583
+
584
+ const head = h("div", "setup-modal-head");
585
+ const hIcon = h("span", "glyph", "⚙");
586
+ head.appendChild(hIcon);
587
+ head.appendChild(h("span", "setup-modal-title", title));
588
+ const close = h("button", "setup-modal-x", "✕");
589
+ close.type = "button";
590
+ close.addEventListener("click", closePopup);
591
+ head.appendChild(close);
592
+ modal.appendChild(head);
593
+
594
+ const body = h("div", "setup-modal-body");
595
+ body.appendChild(h("p", "setup-hint", tr("web.setup.security.popup_hint")));
596
+ SECURITY_POPUPS[group].forEach(([path, type, labelKey]) => {
597
+ body.appendChild(popupField(path, type, tr(labelKey)));
598
+ });
599
+ modal.appendChild(body);
600
+
601
+ const foot = h("div", "setup-modal-foot");
602
+ const reset = h("button", "setup-btn link", tr("web.setup.security.reset"));
603
+ reset.type = "button";
604
+ reset.addEventListener("click", () => {
605
+ const defaults = (state.display && state.display.defaults && state.display.defaults.security) || {};
606
+ (GROUP_KEYS[group] || [group]).forEach((src) => {
607
+ const fresh = defaults[src] || {};
608
+ Object.keys(fresh).forEach((k) => setPath(state.answers.securityDetail, src + "." + k, fresh[k]));
609
+ });
610
+ closePopup();
611
+ openSecurityPopup(group, title);
612
+ });
613
+ foot.appendChild(reset);
614
+ foot.appendChild(h("span", "spacer"));
615
+ const done = h("button", "setup-btn primary", tr("web.setup.security.popup_done"));
616
+ done.type = "button";
617
+ done.addEventListener("click", closePopup);
618
+ foot.appendChild(done);
619
+ modal.appendChild(foot);
620
+
621
+ overlay.appendChild(modal);
622
+ overlay.addEventListener("click", (e) => {
623
+ if (e.target === overlay) closePopup();
624
+ });
625
+ popup = overlay;
626
+ gate.appendChild(overlay);
627
+ }
628
+
629
+ function popupField(path, type, label) {
630
+ const wrap = h("div", "setup-field");
631
+ if (type === "list") {
632
+ wrap.appendChild(h("label", "setup-label", label));
633
+ const ta = h("textarea", "setup-input setup-textarea");
634
+ const cur = getPath(state.answers.securityDetail, path);
635
+ ta.value = Array.isArray(cur) ? cur.join("\n") : "";
636
+ ta.rows = 5;
637
+ ta.spellcheck = false;
638
+ ta.addEventListener("input", () => {
639
+ const items = ta.value
640
+ .split("\n")
641
+ .map((s) => s.trim())
642
+ .filter(Boolean);
643
+ setPath(state.answers.securityDetail, path, items);
644
+ });
645
+ wrap.appendChild(ta);
646
+ } else if (type === "bool") {
647
+ const row = h("label", "setup-toggle");
648
+ const cb = h("input");
649
+ cb.type = "checkbox";
650
+ cb.checked = Boolean(getPath(state.answers.securityDetail, path));
651
+ cb.addEventListener("change", () => setPath(state.answers.securityDetail, path, cb.checked));
652
+ row.appendChild(cb);
653
+ row.appendChild(h("span", "t", label));
654
+ wrap.appendChild(row);
655
+ } else if (type === "int") {
656
+ wrap.appendChild(h("label", "setup-label", label));
657
+ const inp = h("input", "setup-input");
658
+ inp.type = "number";
659
+ inp.min = "1";
660
+ const cur = getPath(state.answers.securityDetail, path);
661
+ inp.value = typeof cur === "number" ? String(cur) : "";
662
+ inp.addEventListener("input", () => {
663
+ const n = parseInt(inp.value, 10);
664
+ setPath(state.answers.securityDetail, path, Number.isFinite(n) && n > 0 ? n : undefined);
665
+ });
666
+ wrap.appendChild(inp);
667
+ } else if (type === "enum") {
668
+ wrap.appendChild(h("label", "setup-label", label));
669
+ const sel = h("select", "setup-input");
670
+ const cur = getPath(state.answers.securityDetail, path);
671
+ SEVERITIES.forEach((sev) => {
672
+ const opt = h("option", null, tr("web.setup.security.severity." + sev));
673
+ opt.value = sev;
674
+ if (cur === sev) opt.selected = true;
675
+ sel.appendChild(opt);
676
+ });
677
+ sel.addEventListener("change", () => setPath(state.answers.securityDetail, path, sel.value));
678
+ wrap.appendChild(sel);
679
+ }
680
+ return wrap;
681
+ }
682
+
683
+ function closePopup() {
684
+ if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
685
+ popup = null;
686
+ }
687
+
430
688
  function securitySummary() {
431
689
  const on = [];
432
690
  if (state.answers.securityPathsRestrict) on.push(tr("web.setup.security.paths"));
@@ -534,6 +792,7 @@
534
792
  securityBashBlock: state.answers.securityBashBlock,
535
793
  securityFlagsBlock: state.answers.securityFlagsBlock,
536
794
  securityPathsRestrict: state.answers.securityPathsRestrict,
795
+ securityDetail: state.answers.securityDetail,
537
796
  },
538
797
  });
539
798
  if (!r.ok) {
@@ -229,7 +229,7 @@ body[data-font="large"] { font-size: 17px; }
229
229
  .layout { flex: 1; display: flex; min-height: 0; }
230
230
 
231
231
  /* chat */
232
- .chat { flex: 1; display: flex; flex-direction: column; min-width: 0; }
232
+ .chat { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; }
233
233
  .msgs { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 18px 22px; display: flex; flex-direction: column; gap: 11px; }
234
234
  .msgs > * { flex-shrink: 0; min-height: fit-content; }
235
235
  /* messages live inside a per-tab `.feed` wrapper; it must mirror the `.msgs`
@@ -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; }
@@ -452,6 +444,18 @@ body.light .imgcard .ph { background: var(--ph-light); }
452
444
  }
453
445
  .empty .g { color: var(--green); }
454
446
 
447
+ /* jump-to-latest pill: only visible when the reader scrolled up */
448
+ .to-latest {
449
+ position: absolute; left: 50%; bottom: 148px; transform: translateX(-50%);
450
+ z-index: 20; font: inherit; font-size: 12.5px; cursor: pointer;
451
+ color: var(--accent); background: var(--panel);
452
+ border: 1px solid var(--border-2); border-radius: 999px;
453
+ padding: 5px 14px; box-shadow: var(--shadow-menu);
454
+ white-space: nowrap;
455
+ }
456
+ .to-latest:hover { border-color: var(--accent); }
457
+ .to-latest[hidden] { display: none; }
458
+
455
459
  /* ---- composer: selectors, attachments, input, autocomplete ---- */
456
460
  .input-area { margin: 14px 22px 16px; position: relative; }
457
461
  .selectors { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; min-width: 0; }
@@ -590,6 +594,61 @@ body.light .sel > button { border: 1px solid var(--border); }
590
594
  .input-area.dragover .composer { border-color: var(--accent); box-shadow: 0 0 0 2px var(--input-shadow); }
591
595
  @keyframes blink { 50% { opacity: 0; } }
592
596
 
597
+ /* ---- chat motion ("magic"): entrances, caret, pulses, pops -----------------
598
+ Rules: only opacity/transform animations (never height/margin), 100–200ms,
599
+ calm single pulses. Killed globally by the `prefers-reduced-motion` block
600
+ below. Glyphs stay textual per STYLE.md — motion, not decoration. */
601
+ @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
602
+ @keyframes pop { from { opacity: 0; transform: scale(.98) translateY(2px); } to { opacity: 1; transform: none; } }
603
+ @keyframes softPulse { 50% { opacity: .45; } }
604
+ @keyframes fadeIn { from { opacity: 0; } }
605
+
606
+ /* new rows rise in softly (a bulk history render animates once as a whole —
607
+ one shared fade, still cheap: opacity/transform only) */
608
+ .feed > :is(.u, .md, .act, .think, .q, .filerow, .filegroup, .planblock, .imgcard, .linkline,
609
+ .toolout, .metric, .detail, .metaln, .warnline, .errline, .cmdout, .sysline,
610
+ .startbanner, .grouped, .msg-actions, .busy) {
611
+ animation: rise .18s ease-out;
612
+ }
613
+ /* quiet-mode run folding fades instead of snapping away */
614
+ .fold-out { opacity: 0; transition: opacity .15s ease-out; }
615
+
616
+ /* streaming caret at the tail of live markdown */
617
+ .md.streaming::after {
618
+ content: "▌"; color: var(--accent); margin-left: 2px;
619
+ animation: blink 1s steps(2, start) infinite;
620
+ }
621
+ /* alive labels: in-feed spinner, the *live* think block, rail phase,
622
+ a running tool. Finished blocks stay static — a pulsing "думаю" after
623
+ the turn reads as hung. */
624
+ .busy .lab, .think.live .think-h .lab, .rail-l1 .lab, .act.running .lab {
625
+ animation: softPulse 1.6s ease-in-out infinite;
626
+ }
627
+
628
+ /* menus, autocomplete and dialogs pop instead of snapping */
629
+ .dd.open .dd-menu, .sel.open .menu, .cmdk:not([hidden]) { animation: pop .12s ease-out; }
630
+ .to-latest:not([hidden]) { animation: pop .15s ease-out; }
631
+ .overlay.open { animation: fadeIn .15s ease-out; }
632
+ .overlay.open .modal { animation: rise .18s ease-out; }
633
+
634
+ /* hover/active blends */
635
+ .sel > button, .menu .mi, .stab, .q .qopt, .q .qsend, .filerow, .filegroup, .icon-btn,
636
+ .btn, .linkbtn, .chip, .cb-copy, .act-btn, .copy-btn, .composer .attach-btn,
637
+ .composer .send-btn, .dd-menu .si, .dd-menu .act, .cmdk-item, .toggle {
638
+ transition: background-color .12s, color .12s, border-color .12s;
639
+ }
640
+ .composer { transition: border-color .15s, box-shadow .15s; }
641
+
642
+ /* scroll edge fades on the transcript (classes from the feed scroll hook) */
643
+ .msgs::before, .msgs::after {
644
+ content: ""; position: sticky; display: block; height: 22px; flex: 0 0 auto;
645
+ pointer-events: none; z-index: 5; opacity: 0; transition: opacity .25s;
646
+ }
647
+ .msgs::before { top: 0; margin-bottom: -22px; background: linear-gradient(var(--bg), transparent); }
648
+ .msgs::after { bottom: 0; margin-top: -22px; background: linear-gradient(transparent, var(--bg)); }
649
+ .msgs.scrolled::before { opacity: 1; }
650
+ .msgs.unfollowed::after { opacity: 1; }
651
+
593
652
  /* ---- activity rail — indicator above the composer while a turn runs ----
594
653
  Reproduces docs/design/ui/demo-activity-rail-chat.html (variant №12).
595
654
  `l-line` (default) = compact one-liner, `l-meta` = inside the hint row,
@@ -686,6 +745,66 @@ body.light .sel > button { border: 1px solid var(--border); }
686
745
  .sec.plan-sec { flex: 1 1 auto; }
687
746
  .sec.usage-sec { flex: 0 1 auto; max-height: 45%; }
688
747
 
748
+ /* ---- sidebar tabs ---- */
749
+ .sb-tabs { display: flex; gap: 6px; padding: 10px 14px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
750
+ .sb-tab {
751
+ position: relative; width: 34px; height: 34px; flex: 0 0 auto;
752
+ display: inline-flex; align-items: center; justify-content: center; padding: 0;
753
+ border: 1px solid var(--border-2); border-radius: 6px;
754
+ background: transparent; color: var(--stab-fg); cursor: pointer; font: inherit;
755
+ }
756
+ .sb-tab:hover { color: var(--fg); background: var(--hover-bg); }
757
+ .sb-tab.on { color: var(--accent); background: var(--accent-bg); border-color: transparent; }
758
+ .sb-tab .sb-ico svg { width: 17px; height: 17px; }
759
+ .sb-tab .sb-dot {
760
+ position: absolute; top: 4px; right: 4px; width: 6px; height: 6px; border-radius: 50%;
761
+ background: var(--accent); opacity: 0; transition: opacity .2s;
762
+ }
763
+ .sb-tab[data-indicator="live"] .sb-dot { opacity: 1; animation: sbPulse 1s ease-in-out infinite; }
764
+ .sb-tab[data-indicator="busy"] .sb-dot { opacity: 1; background: var(--accent); }
765
+ .sb-tab[data-indicator="stale"] .sb-dot { opacity: 1; background: var(--yellow); }
766
+ @keyframes sbPulse { 0%,100% { opacity: 1 } 50% { opacity: .25 } }
767
+
768
+ .sb-name {
769
+ display: flex; align-items: center; gap: 8px; flex-shrink: 0;
770
+ padding: 10px 16px; color: var(--fg); font-size: 12.5px; font-weight: 600;
771
+ letter-spacing: .03em; border-bottom: 1px solid var(--border);
772
+ }
773
+ .sb-name .sb-ico svg { width: 14px; height: 14px; color: var(--accent); }
774
+
775
+ .sb-pane { display: flex; flex-direction: column; flex: 1; min-height: 0; }
776
+ .sb-pane[hidden] { display: none; }
777
+
778
+ /* master-detail expansion */
779
+ .sb { transition: width .22s ease, min-width .22s ease; }
780
+ .sb.sb-expanded { width: 680px; min-width: 680px; }
781
+ #pane-trace { flex-direction: row; }
782
+ .sb-expanded #pane-trace { position: relative; }
783
+ .trace-list { flex: 1; min-width: 0; overflow-y: auto; padding: 6px 10px 12px; display: flex; flex-direction: column; gap: 1px; }
784
+ .trace-turn { color: var(--dim-c); font-size: 11px; margin: 8px 6px 2px; }
785
+ .trc { display: flex; gap: 8px; align-items: baseline; padding: 4px 6px; font-size: 12px; color: var(--muted); border-radius: 4px; cursor: pointer; }
786
+ .trc:hover { background: var(--hover-bg); }
787
+ .trc.sel { background: var(--accent-bg); }
788
+ .trc .g { flex: 0 0 auto; }
789
+ .trc.recon .g { color: var(--cyan); } .trc.mutate .g { color: var(--purple); }
790
+ .trc.execute .g { color: var(--accent); } .trc.meta .g { color: var(--meta); } .trc.phase .g { color: var(--think); }
791
+ .trc .lab { color: var(--fg); white-space: nowrap; }
792
+ .trc .args { color: var(--args); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1; }
793
+ .trc .t { color: var(--dim-b); margin-left: auto; flex: 0 0 auto; }
794
+ .trc.warn .g, .trc.warn .lab { color: var(--yellow); }
795
+ .trc.phase .lab { color: var(--think); }
796
+ .trc-detail { display: flex; flex-direction: column; flex: 0 0 340px; min-width: 0; border-left: 1px solid var(--border); }
797
+ .trc-detail[hidden] { display: none; }
798
+ .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; }
799
+ .detail-h .g { color: var(--accent); }
800
+ .detail-h .x { margin-left: auto; color: var(--dim-b); cursor: pointer; }
801
+ .detail-b { padding: 10px 14px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }
802
+ .detail-b h5 { margin: 0 0 4px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--sec-h); font-weight: 600; }
803
+ .map-sec { flex: 1 1 auto; }
804
+ .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; }
805
+ .map-reindex:disabled { color: var(--dim-b); cursor: default; }
806
+ .kv.wn b { color: var(--yellow); }
807
+
689
808
  ::-webkit-scrollbar { width: 8px; }
690
809
  ::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 4px; }
691
810
 
@@ -1212,6 +1331,7 @@ button.toggle:disabled { opacity: .5; cursor: default; }
1212
1331
  .chat { min-height: 0; }
1213
1332
  /* Phones: sidebar hidden, opens as an overlay drawer via body.sb-open. */
1214
1333
  .sb { display: none; }
1334
+ .sb-tab[data-tab="trace"], #pane-trace { display: none; }
1215
1335
  body.sb-open .sb {
1216
1336
  display: flex; position: fixed; top: 41px; right: 0; bottom: 0; z-index: 45;
1217
1337
  width: min(360px, 85vw); min-width: 0; max-height: none;
@@ -1349,8 +1469,45 @@ body.setup-active .layout { display: none; }
1349
1469
  .setup-summary .k { color: var(--dim-a); width: 150px; flex: 0 0 auto; }
1350
1470
  .setup-summary .v { color: var(--fg); word-break: break-all; }
1351
1471
 
1352
- .setup-toggle { display: flex; align-items: center; gap: 9px; padding: 7px 0; font-size: 13px; cursor: pointer; }
1353
- .setup-toggle input { accent-color: var(--green-fill); width: 15px; height: 15px; }
1472
+ .setup-toggle { display: flex; align-items: flex-start; gap: 9px; padding: 7px 0; font-size: 13px; cursor: pointer; }
1473
+ .setup-toggle input { accent-color: var(--green-fill); width: 15px; height: 15px; margin-top: 2px; flex: 0 0 auto; }
1474
+ .setup-toggle-texts { display: flex; flex-direction: column; gap: 2px; }
1475
+ .setup-toggle-texts .t { font-weight: 500; }
1476
+ .setup-toggle-texts .d { color: var(--dim-a); font-size: 12px; line-height: 1.5; }
1477
+
1478
+ .setup-sec-row { display: flex; align-items: center; gap: 8px; }
1479
+ .setup-sec-row .setup-toggle { flex: 1; }
1480
+ .setup-adv-label { padding: 7px 0; }
1481
+ .setup-detail-btn {
1482
+ display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
1483
+ background: transparent; color: var(--dim-a);
1484
+ border: 1px solid var(--border); border-radius: 7px;
1485
+ padding: 5px 9px; font-size: 12px; font-family: inherit; cursor: pointer;
1486
+ }
1487
+ .setup-detail-btn:hover { background: var(--hover-bg); color: var(--fg); }
1488
+ .setup-detail-btn .g { color: var(--accent); }
1489
+
1490
+ .setup-textarea { resize: vertical; min-height: 84px; font-family: var(--mono, ui-monospace, monospace); font-size: 12.5px; line-height: 1.5; }
1491
+
1492
+ .setup-overlay {
1493
+ position: fixed; inset: 0; z-index: 60;
1494
+ background: rgba(0, 0, 0, .55);
1495
+ display: flex; align-items: center; justify-content: center; padding: 20px;
1496
+ }
1497
+ .setup-modal {
1498
+ width: 100%; max-width: 560px; max-height: 86vh; display: flex; flex-direction: column;
1499
+ background: var(--surface); border: 1px solid var(--border-2); border-radius: 10px;
1500
+ box-shadow: 0 18px 50px rgba(0, 0, 0, .45);
1501
+ }
1502
+ .setup-modal-head { display: flex; align-items: center; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--border-3); }
1503
+ .setup-modal-head .glyph { color: var(--accent); }
1504
+ .setup-modal-title { font-weight: 600; font-size: 14px; flex: 1; }
1505
+ .setup-modal-x { background: none; border: none; color: var(--dim-a); font-size: 15px; cursor: pointer; padding: 2px 6px; }
1506
+ .setup-modal-x:hover { color: var(--fg); }
1507
+ .setup-modal-body { padding: 14px 16px; overflow-y: auto; }
1508
+ .setup-modal-body .setup-field:last-child { margin-bottom: 0; }
1509
+ .setup-modal-foot { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-top: 1px solid var(--border-3); }
1510
+ .setup-modal-foot .spacer { flex: 1; }
1354
1511
 
1355
1512
  @media (max-width: 640px) {
1356
1513
  .setup-gate { padding: 24px 14px; }