mimetic-cli 0.1.3 → 0.1.5

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.
Files changed (50) hide show
  1. package/README.md +67 -12
  2. package/dist/env-file.d.ts +14 -0
  3. package/dist/env-file.js +108 -0
  4. package/dist/env-file.js.map +1 -0
  5. package/dist/feedback.d.ts +7 -5
  6. package/dist/feedback.js +61 -4
  7. package/dist/feedback.js.map +1 -1
  8. package/dist/init-templates.js +29 -0
  9. package/dist/init-templates.js.map +1 -1
  10. package/dist/lab-app-runner.d.ts +78 -0
  11. package/dist/lab-app-runner.js +403 -0
  12. package/dist/lab-app-runner.js.map +1 -0
  13. package/dist/labs.d.ts +67 -0
  14. package/dist/labs.js +257 -0
  15. package/dist/labs.js.map +1 -0
  16. package/dist/observer-assets.js +473 -25
  17. package/dist/observer-assets.js.map +1 -1
  18. package/dist/observer.d.ts +6 -0
  19. package/dist/observer.js +49 -8
  20. package/dist/observer.js.map +1 -1
  21. package/dist/oss-lab.d.ts +1 -1
  22. package/dist/oss-lab.js +6 -6
  23. package/dist/oss-lab.js.map +1 -1
  24. package/dist/oss-meta-lab.d.ts +113 -1
  25. package/dist/oss-meta-lab.js +2753 -200
  26. package/dist/oss-meta-lab.js.map +1 -1
  27. package/dist/oss-remote-telemetry.d.ts +77 -0
  28. package/dist/oss-remote-telemetry.js +393 -0
  29. package/dist/oss-remote-telemetry.js.map +1 -0
  30. package/dist/program.d.ts +8 -0
  31. package/dist/program.js +668 -70
  32. package/dist/program.js.map +1 -1
  33. package/dist/run.d.ts +105 -3
  34. package/dist/run.js +684 -22
  35. package/dist/run.js.map +1 -1
  36. package/docs/architecture/local-codex-tui-actor.md +9 -6
  37. package/docs/architecture/oss-lab-poc.md +119 -47
  38. package/docs/architecture/project-layout.md +40 -6
  39. package/docs/assets/mimetic-oss-lab-observer.png +0 -0
  40. package/docs/contracts/feedback.md +15 -12
  41. package/docs/contracts/policy.md +9 -2
  42. package/docs/contracts/run-bundle.md +62 -0
  43. package/docs/contracts/schemas.md +21 -0
  44. package/docs/goals/current.md +50 -17
  45. package/docs/product/open-source-install-experience.md +63 -8
  46. package/docs/ramp/README.md +26 -8
  47. package/docs/roadmap/world-class-open-source-v0.md +41 -20
  48. package/package.json +9 -6
  49. package/skills/mimetic-cli/SKILL.md +89 -4
  50. package/skills/mimetic-cli/agents/openai.yaml +1 -1
@@ -416,6 +416,10 @@ a { color: inherit; text-decoration: none; }
416
416
 
417
417
  /* ============================================================ STREAM SURFACES */
418
418
  .surface-fill { position: absolute; inset: 0; width: 100%; height: 100%; }
419
+ .live-stream-mount { position: absolute; inset: 0; overflow: hidden; background: #000; }
420
+ .live-stream-overlay { position: absolute; overflow: hidden; pointer-events: none; z-index: 2; }
421
+ .live-stream-overlay[data-focus="true"] .bw-lab-dock { max-height: 86px; }
422
+ .live-stream-overlay[data-focus="true"] .bw-chip-v { max-width: min(420px, 38vw); }
419
423
 
420
424
  /* browser/ui mock */
421
425
  .bw { position: absolute; inset: 0; display: flex; flex-direction: column; background: #0b0d10; }
@@ -435,6 +439,35 @@ a { color: inherit; text-decoration: none; }
435
439
  .bw-viewport { flex: 1; position: relative; overflow: hidden; background: #fbfcfd; }
436
440
  .bw-app-wait { position: absolute; inset: 0; display: grid; place-items: center; align-content: center; gap: 8px; background: #fbfcfd; color: #5a626c; text-align: center; padding: 16px; }
437
441
  .bw-app-wait .wait-spinner { border-color: rgba(20,28,40,.12); border-top-color: var(--accent); }
442
+ .bw-lab-dock {
443
+ position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 8;
444
+ display: flex; align-items: flex-end; gap: 5px; flex-wrap: wrap;
445
+ max-height: 54px; overflow: hidden; pointer-events: none;
446
+ }
447
+ .bw-lab-chip {
448
+ min-width: 0; max-width: 100%; pointer-events: auto;
449
+ display: inline-flex; align-items: center; gap: 5px;
450
+ padding: 3px 7px; border-radius: 6px;
451
+ background: rgba(8,10,13,.78); backdrop-filter: blur(8px);
452
+ border: 1px solid rgba(255,255,255,.12);
453
+ color: var(--text-2); box-shadow: var(--shadow-1);
454
+ }
455
+ a.bw-lab-chip:hover { border-color: rgba(255,255,255,.22); color: var(--text-1); }
456
+ .bw-lab-chip[data-tone="live"] { color: var(--accent-2); border-color: color-mix(in oklab, var(--accent) 35%, transparent); }
457
+ .bw-lab-chip[data-tone="ok"] { color: var(--green); border-color: color-mix(in oklab, var(--green) 34%, transparent); }
458
+ .bw-lab-chip[data-tone="warn"] { color: var(--amber); border-color: color-mix(in oklab, var(--amber) 34%, transparent); }
459
+ .bw-lab-chip[data-tone="err"] { color: var(--red); border-color: color-mix(in oklab, var(--red) 34%, transparent); }
460
+ .bw-chip-k {
461
+ flex: none; font-family: var(--mono); font-size: 8.5px; letter-spacing: .08em;
462
+ text-transform: uppercase; color: currentColor;
463
+ }
464
+ .bw-chip-v {
465
+ min-width: 0; max-width: min(240px, 42vw);
466
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
467
+ font-family: var(--mono); font-size: 9px; color: var(--text-2);
468
+ }
469
+ .focus-stage-area .bw-lab-dock { max-height: 86px; }
470
+ .focus-stage-area .bw-chip-v { max-width: min(420px, 38vw); }
438
471
  .bw-cursor {
439
472
  position: absolute; width: 16px; height: 16px; z-index: 5; pointer-events: none;
440
473
  transition: left 1.1s var(--ease), top 1.1s var(--ease);
@@ -611,12 +644,47 @@ a { color: inherit; text-decoration: none; }
611
644
 
612
645
  .file-row { display: flex; align-items: center; gap: 11px; padding: 11px 16px; border-bottom: 1px solid var(--line); transition: background .14s; }
613
646
  .file-row:hover { background: var(--surface-1); }
647
+ .file-row[data-selected="true"] { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent); }
648
+ .file-row > button, .file-row > a:not(.file-open) { display: flex; align-items: center; gap: 11px; min-width: 0; flex: 1; text-align: left; }
614
649
  .file-ic { width: 28px; height: 28px; border-radius: 7px; display: grid; place-items: center; background: var(--surface-2); border: 1px solid var(--line); color: var(--text-2); flex: none; }
615
650
  .file-ic svg { width: 14px; height: 14px; }
616
651
  .file-meta { min-width: 0; flex: 1; }
617
652
  .file-name { font-size: 12.5px; color: var(--text-1); }
618
653
  .file-path { font-family: var(--mono); font-size: 9.5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
619
654
  .file-kind { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); padding: 2px 7px; border-radius: 5px; background: var(--surface-2); flex: none; }
655
+ .file-open { font-family: var(--mono); font-size: 9px; color: var(--accent-2); padding: 3px 6px; border-radius: 5px; border: 1px solid var(--line); flex: none; }
656
+ .file-open:hover { background: var(--surface-3); }
657
+ .file-inspector { border-bottom: 1px solid var(--line); background: var(--surface-1); }
658
+ .fi-head { padding: 14px 16px 12px; border-bottom: 1px solid var(--line); }
659
+ .fi-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
660
+ .fi-title h3 { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: -0.01em; }
661
+ .fi-status { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: .08em; padding: 2px 7px; border-radius: 999px; background: var(--surface-3); color: var(--text-2); }
662
+ .fi-status[data-status="passed"] { background: var(--green-soft); color: var(--green); }
663
+ .fi-status[data-status="needs_review"], .fi-status[data-status="blocked"] { background: var(--amber-soft); color: var(--amber); }
664
+ .fi-summary { margin-top: 7px; color: var(--text-2); font-size: 11.5px; line-height: 1.45; }
665
+ .fi-section { padding: 12px 16px; border-bottom: 1px solid var(--line); }
666
+ .fi-section:last-child { border-bottom: 0; }
667
+ .fi-section-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
668
+ .fi-section-title span:first-child { font-family: var(--mono); font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3); }
669
+ .fi-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
670
+ .fi-stat { padding: 8px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-0); min-width: 0; }
671
+ .fi-stat-k { font-family: var(--mono); font-size: 8.5px; color: var(--text-3); text-transform: uppercase; letter-spacing: .08em; }
672
+ .fi-stat-v { margin-top: 2px; font-size: 13px; font-weight: 600; color: var(--text-1); overflow-wrap: anywhere; }
673
+ .fi-check { display: grid; grid-template-columns: auto 1fr; gap: 8px; padding: 8px 0; border-top: 1px solid var(--line); }
674
+ .fi-check:first-child { border-top: 0; padding-top: 0; }
675
+ .fi-dot { width: 18px; height: 18px; border-radius: 6px; display: grid; place-items: center; background: var(--surface-2); border: 1px solid var(--line); color: var(--text-3); }
676
+ .fi-check[data-ok="true"] .fi-dot { color: var(--green); border-color: color-mix(in oklab, var(--green) 30%, transparent); background: var(--green-soft); }
677
+ .fi-check[data-ok="false"] .fi-dot { color: var(--amber); border-color: color-mix(in oklab, var(--amber) 30%, transparent); background: var(--amber-soft); }
678
+ .fi-check-name { font-size: 12px; color: var(--text-1); }
679
+ .fi-check-detail { margin-top: 2px; font-size: 11px; color: var(--text-3); line-height: 1.4; }
680
+ .fi-tree { max-height: 260px; overflow: auto; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-0); padding: 7px 0; }
681
+ .fi-tree-row { display: grid; grid-template-columns: auto 1fr auto; gap: 7px; padding: 3px 9px; font-family: var(--mono); font-size: 9.5px; color: var(--text-2); }
682
+ .fi-tree-row[data-type="directory"] { color: var(--accent-2); }
683
+ .fi-tree-size { color: var(--text-4); }
684
+ .fi-pre { margin: 0; max-height: 320px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--mono); font-size: 10px; line-height: 1.45; background: var(--surface-0); border: 1px solid var(--line); border-radius: 7px; padding: 10px; color: var(--text-2); }
685
+ .fi-preview + .fi-preview { margin-top: 10px; }
686
+ .fi-preview-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px; font-family: var(--mono); font-size: 9.5px; color: var(--text-3); }
687
+ .fi-load { padding: 24px 16px; color: var(--text-3); text-align: center; }
620
688
 
621
689
  .tab-empty { padding: 36px 24px; text-align: center; color: var(--text-3); font-size: 12px; }
622
690
 
@@ -679,16 +747,25 @@ html[data-theme="light"] .scrim { background: rgba(20,28,40,.32); }
679
747
  .lane-count .lc-label { display: none; }
680
748
  }
681
749
  @media (max-width: 860px) {
682
- .focus { grid-template-columns: 1fr; grid-template-rows: 1fr; }
750
+ .focus { display: block; height: 100%; min-height: 0; overflow-y: auto; }
683
751
  .focus-rail { display: none; }
752
+ .focus-stage { min-height: auto; }
753
+ .focus-bar { position: sticky; top: 0; z-index: 25; }
754
+ .focus-stage-area {
755
+ display: block; min-height: min(60vh, 560px); height: auto;
756
+ padding: 14px; overflow: visible;
757
+ }
758
+ .focus-frame {
759
+ width: 100%; height: auto; max-height: none;
760
+ min-height: 280px;
761
+ }
684
762
  .focus-side {
685
- position: fixed; left: 0; right: 0; bottom: 0; top: auto; z-index: 72;
686
- max-height: 62vh; border-left: none; border-top: 1px solid var(--line-2);
687
- border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: var(--shadow-pop);
688
- transform: translateY(calc(100% - 52px)); transition: transform .3s var(--ease);
763
+ position: relative; left: auto; right: auto; bottom: auto; top: auto; z-index: auto;
764
+ min-height: 360px; max-height: none; border-left: none; border-top: 1px solid var(--line-2);
765
+ border-radius: 0; box-shadow: none; transform: none;
689
766
  }
690
- .focus-side[data-sheet="open"] { transform: translateY(0); }
691
- .sheet-grip { display: flex; }
767
+ .focus-side[data-sheet="open"], .focus-side[data-sheet="closed"] { transform: none; }
768
+ .sheet-grip { display: none; }
692
769
  }
693
770
  .sheet-grip { display: none; align-items: center; justify-content: center; gap: 8px; height: 40px; flex: none; border-bottom: 1px solid var(--line); cursor: grab; }
694
771
  .sheet-grip::before { content: ""; width: 36px; height: 4px; border-radius: 2px; background: var(--line-3); }
@@ -892,6 +969,7 @@ export function observerClientJs() {
892
969
 
893
970
  // ---------------------------------------------------------------- hydrate
894
971
  var DATA_FILE = "observer-data.json";
972
+ var REFRESH_MS = 5000;
895
973
  var dataEl = document.getElementById("observer-data");
896
974
  var currentData = null;
897
975
  try { currentData = JSON.parse((dataEl && dataEl.textContent) || "null"); } catch (e) { currentData = null; }
@@ -924,9 +1002,10 @@ export function observerClientJs() {
924
1002
  }
925
1003
 
926
1004
  // ---------------------------------------------------------------- state
1005
+ var initialFocusId = focusFromHash();
927
1006
  var S = {
928
- view: "grid",
929
- focusedId: focusFromHash(),
1007
+ view: initialFocusId ? "focus" : "grid",
1008
+ focusedId: initialFocusId,
930
1009
  statusSel: [],
931
1010
  kindSel: [],
932
1011
  query: "",
@@ -935,6 +1014,7 @@ export function observerClientJs() {
935
1014
  detailsOpen: false,
936
1015
  tweaksOpen: false,
937
1016
  consoleOpen: false,
1017
+ filePath: null,
938
1018
  railCollapsed: !!readPref("railCollapsed", false),
939
1019
  sideCollapsed: !!readPref("sideCollapsed", false),
940
1020
  sheetOpen: false,
@@ -945,6 +1025,10 @@ export function observerClientJs() {
945
1025
  statusViz: readPref("statusViz", "badges"),
946
1026
  motion: readPref("motion", "full")
947
1027
  };
1028
+ var artifactCache = {};
1029
+ var liveStreamFrames = {};
1030
+ var liveStreamHost = null;
1031
+ var liveStreamLayoutRaf = null;
948
1032
 
949
1033
  // ---------------------------------------------------------------- tone / labels
950
1034
  var TONE = {
@@ -1009,7 +1093,7 @@ export function observerClientJs() {
1009
1093
 
1010
1094
  // ---------------------------------------------------------------- derive (observer-data.v1 -> display)
1011
1095
  function laneName(s) { return s.label || (s.sim && s.sim.summary) || s.id || "lane"; }
1012
- function laneRoute(s) { return (s.ui && s.ui.route) || s.url || (s.terminal && s.terminal.title) || ""; }
1096
+ function laneRoute(s) { return (s.ui && (s.ui.route || s.ui.appUrl || s.ui.nestedObserverUrl)) || s.url || (s.terminal && s.terminal.title) || ""; }
1013
1097
  function laneProgress(s) {
1014
1098
  if (s.sim && typeof s.sim.progress === "number") return Math.max(0, Math.min(100, Math.round(s.sim.progress)));
1015
1099
  var t = tone(s.status);
@@ -1086,6 +1170,107 @@ export function observerClientJs() {
1086
1170
  while (arr.length && arr[arr.length - 1].trim() === "") arr.pop();
1087
1171
  return arr.map(function (line) { return { text: line, cls: classify(line) }; });
1088
1172
  }
1173
+ function clip(v, n) {
1174
+ var s = String(v == null ? "" : v).trim();
1175
+ if (!s) return "";
1176
+ return s.length > n ? (s.slice(0, Math.max(0, n - 1)) + "…") : s;
1177
+ }
1178
+ function shortSurfaceValue(v) {
1179
+ var s = String(v == null ? "" : v).trim();
1180
+ if (!s) return "";
1181
+ var cut = s.split("?")[0].split("#")[0];
1182
+ var parts = cut.split("/");
1183
+ var tail = parts.filter(function (p) { return !!p; }).slice(-2).join("/");
1184
+ return clip(tail || cut || s, 48);
1185
+ }
1186
+ function linkHref(v, artifactPath) {
1187
+ var raw = String(v == null ? "" : v).trim();
1188
+ var low = raw.toLowerCase();
1189
+ if (!raw) return "";
1190
+ if (low.indexOf("http://") === 0 || low.indexOf("https://") === 0 || low.indexOf("file:") === 0) return raw;
1191
+ if (raw.indexOf("://") >= 0) return "";
1192
+ if (raw.charAt(0) === "/" || raw.indexOf("..") >= 0) return "";
1193
+ return artifactPath ? ("../" + raw) : raw;
1194
+ }
1195
+ function firstArtifactKind(s, kind) {
1196
+ var arts = laneArtifacts(s);
1197
+ for (var i = 0; i < arts.length; i += 1) {
1198
+ if (arts[i] && arts[i].kind === kind) return arts[i];
1199
+ }
1200
+ return null;
1201
+ }
1202
+ function artifactKinds(arts) {
1203
+ var seen = {}, out = [];
1204
+ arts.forEach(function (a) {
1205
+ var k = (a && a.kind) || "file";
1206
+ if (!seen[k]) { seen[k] = true; out.push(k); }
1207
+ });
1208
+ var shown = out.slice(0, 3).join("+");
1209
+ return shown + (out.length > 3 ? "+" : "");
1210
+ }
1211
+ function terminalExcerpt(s) {
1212
+ var lines = termLines(s).filter(function (ln) { return !!String(ln.text || "").trim(); });
1213
+ if (!lines.length) return "";
1214
+ return clip(lines[lines.length - 1].text, 88);
1215
+ }
1216
+ function completionTone(c) {
1217
+ var t = tone(c && c.status);
1218
+ return t === "running" ? "live" : t === "complete" ? "ok" : t === "blocked" ? "warn" : c && c.status === "failed" ? "err" : "info";
1219
+ }
1220
+ function completionText(c) {
1221
+ if (!c) return "";
1222
+ var bits = [statusLabel(c.status)];
1223
+ if (typeof c.exitCode === "number") bits.push("exit " + c.exitCode);
1224
+ if (typeof c.nestedObserverPresent === "boolean") bits.push("observer " + (c.nestedObserverPresent ? "present" : "missing"));
1225
+ if (typeof c.nestedVerifyPassed === "boolean") bits.push("verify " + (c.nestedVerifyPassed ? "passed" : "failed"));
1226
+ if (c.reason) bits.push(c.reason);
1227
+ return clip(bits.join(" · "), 110);
1228
+ }
1229
+ function labChip(kind, label, detail, href, toneName) {
1230
+ var title = label + (detail ? ": " + detail : "");
1231
+ var inner = '<span class="bw-chip-k">' + esc(label) + '</span>' + (detail ? '<span class="bw-chip-v">' + esc(detail) + '</span>' : "");
1232
+ var attrs = ' class="bw-lab-chip" data-kind="' + esc(kind) + '" data-tone="' + esc(toneName || "info") + '" title="' + esc(title) + '"';
1233
+ if (href) return '<a' + attrs + ' href="' + esc(href) + '" target="_blank" rel="noopener noreferrer" data-action="external">' + inner + '</a>';
1234
+ return '<span' + attrs + '>' + inner + '</span>';
1235
+ }
1236
+ function browserLiveUrl(s) {
1237
+ return (s.embed && s.embed.kind === "iframe" && s.embed.url) || s.url || "";
1238
+ }
1239
+ function liveStreamMount(s, liveUrl) {
1240
+ return '<div class="live-stream-mount surface-fill" data-live-stream-id="' + esc(s.id) + '" data-live-stream-url="' + esc(liveUrl) + '" data-live-stream-title="' + esc(laneName(s) || "live stream") + '">'
1241
+ + '<div class="bw-app-wait"><div class="wait-spinner" style="width:24px;height:24px"></div>'
1242
+ + '<div class="mono" style="font-size:9px">connecting live stream</div></div></div>';
1243
+ }
1244
+ function browserShot(s) {
1245
+ var art = firstArtifactKind(s, "screenshot");
1246
+ return (s.ui && s.ui.screenshotUrl) || (s.embed && s.embed.kind === "screenshot" && s.embed.url) || (art && linkHref(art.path, true)) || "";
1247
+ }
1248
+ function browserHasLabSignals(s) {
1249
+ var ui = s.ui || {};
1250
+ return !!(browserLiveUrl(s) || browserShot(s) || ui.appUrl || ui.nestedObserverUrl || ui.nestedObserverPath || ui.state || s.completion || terminalExcerpt(s) || laneArtifacts(s).length);
1251
+ }
1252
+ function browserLabDock(s, shot) {
1253
+ var ui = s.ui || {};
1254
+ var chips = [];
1255
+ var appUrl = ui.appUrl || "";
1256
+ var nestedArt = firstArtifactKind(s, "observer");
1257
+ var nested = ui.nestedObserverUrl || ui.nestedObserverPath || (nestedArt && nestedArt.path) || "";
1258
+ var completion = s.completion || null;
1259
+ var shotArt = firstArtifactKind(s, "screenshot");
1260
+ var arts = laneArtifacts(s);
1261
+ var tail = terminalExcerpt(s);
1262
+
1263
+ if (appUrl) chips.push(labChip("app", "app", shortSurfaceValue(appUrl), linkHref(appUrl, false), "live"));
1264
+ if (nested) chips.push(labChip("observer", "observer", shortSurfaceValue(nested), linkHref(nested, !!(nestedArt && nested === nestedArt.path)), "info"));
1265
+ else if (completion && typeof completion.nestedObserverPresent === "boolean") chips.push(labChip("observer", "observer", completion.nestedObserverPresent ? "present" : "missing", "", completion.nestedObserverPresent ? "ok" : "warn"));
1266
+ if (completion) chips.push(labChip("completion", "status", completionText(completion), "", completionTone(completion)));
1267
+ if (tail) chips.push(labChip("terminal", "terminal", tail, "", "info"));
1268
+ if (shot || shotArt) chips.push(labChip("screenshot", "shot", shot ? (S.media === "screenshot" ? "viewing fallback" : "fallback ready") : "artifact", shot ? linkHref(shot, false) : linkHref(shotArt.path, true), "info"));
1269
+ if (arts.length) chips.push(labChip("artifact", "files", arts.length + " " + artifactKinds(arts), "", "info"));
1270
+ if (ui.state && !completion) chips.push(labChip("state", "state", clip(ui.state, 72), "", "info"));
1271
+ if (!chips.length) return "";
1272
+ return '<div class="bw-lab-dock" aria-label="Browser lane lab surfaces">' + chips.slice(0, 6).join("") + '</div>';
1273
+ }
1089
1274
 
1090
1275
  function consoleLines() {
1091
1276
  var rows = [];
@@ -1159,16 +1344,19 @@ export function observerClientJs() {
1159
1344
  function browserSurface(s) {
1160
1345
  var live = tone(s.status) === "running";
1161
1346
  var route = laneRoute(s) || "(local)";
1162
- var shot = (s.ui && s.ui.screenshotUrl) || (s.embed && s.embed.kind === "screenshot" && s.embed.url);
1347
+ var liveUrl = browserLiveUrl(s);
1348
+ var shot = browserShot(s);
1349
+ var dock = browserLabDock(s, shot);
1163
1350
  var body;
1164
- if (shot) body = '<img class="surface-fill" style="object-fit:cover;object-position:top" src="' + esc(shot) + '" alt="viewport screenshot"/>';
1351
+ if (liveUrl && S.media === "live") body = liveStreamMount(s, liveUrl);
1352
+ else if (shot) body = '<img class="surface-fill" style="object-fit:cover;object-position:top" src="' + esc(shot) + '" alt="viewport screenshot"/>';
1165
1353
  else body = '<div class="bw-app-wait"><div class="wait-spinner" style="width:24px;height:24px"></div>'
1166
1354
  + '<div class="mono" style="font-size:9px">' + esc(route) + '</div>'
1167
1355
  + '<div style="font-size:10px">' + esc(laneStep(s)) + '</div></div>';
1168
1356
  return '<div class="bw">'
1169
1357
  + '<div class="bw-chrome"><div class="bw-dots"><i></i><i></i><i></i></div>'
1170
1358
  + '<div class="bw-url">' + icon("lock", 8) + '<span>' + esc(route) + '</span></div></div>'
1171
- + '<div class="bw-viewport">' + body + '</div>'
1359
+ + '<div class="bw-viewport">' + body + (liveUrl && S.media === "live" ? "" : dock) + '</div>'
1172
1360
  + (live ? liveTag() : "")
1173
1361
  + '</div>';
1174
1362
  }
@@ -1210,9 +1398,9 @@ export function observerClientJs() {
1210
1398
  function streamSurface(s, focus) {
1211
1399
  var k = s.kind, st = s.status;
1212
1400
  var hasTail = !!(s.terminal && s.terminal.tail && String(s.terminal.tail).trim());
1213
- if ((st === "blocked" || st === "timed_out") && !hasTail) return waitSurface(s);
1401
+ if ((st === "blocked" || st === "timed_out") && !hasTail && !((k === "ui" || k === "browser") && browserHasLabSignals(s))) return waitSurface(s);
1214
1402
  if (k === "ui" || k === "browser") {
1215
- if (st === "blocked" || st === "timed_out" || st === "failed" || st === "contract_proof_only") return waitSurface(s);
1403
+ if ((st === "blocked" || st === "timed_out" || st === "failed" || st === "contract_proof_only") && !browserHasLabSignals(s)) return waitSurface(s);
1216
1404
  return browserSurface(s);
1217
1405
  }
1218
1406
  if (k === "terminal" || k === "tui") return hasTail ? terminalSurface(s, focus) : waitSurface(s);
@@ -1393,6 +1581,113 @@ export function observerClientJs() {
1393
1581
  if (kind === "codex-ui") return "spark";
1394
1582
  return "globe";
1395
1583
  }
1584
+ function artifactHref(a) {
1585
+ return a && a.path ? linkHref(a.path, true) : "";
1586
+ }
1587
+ function ensureArtifactLoaded(a) {
1588
+ if (!a || !a.path || a.kind !== "filesystem") return;
1589
+ var key = a.path;
1590
+ if (artifactCache[key]) return;
1591
+ if (location.protocol === "file:") {
1592
+ artifactCache[key] = { status: "offline", text: "", json: null };
1593
+ return;
1594
+ }
1595
+ var href = artifactHref(a);
1596
+ if (!href) {
1597
+ artifactCache[key] = { status: "error", text: "", json: null, error: "Artifact path is not fetchable." };
1598
+ return;
1599
+ }
1600
+ artifactCache[key] = { status: "loading", text: "", json: null };
1601
+ fetch(href, { cache: "no-store" }).then(function (r) {
1602
+ if (!r.ok) throw new Error("HTTP " + r.status);
1603
+ return r.text();
1604
+ }).then(function (text) {
1605
+ var json = null;
1606
+ try { json = JSON.parse(text); } catch (e) {}
1607
+ artifactCache[key] = { status: "loaded", text: text, json: json };
1608
+ render();
1609
+ }).catch(function (e) {
1610
+ artifactCache[key] = { status: "error", text: "", json: null, error: e && e.message ? e.message : "Fetch failed." };
1611
+ render();
1612
+ });
1613
+ }
1614
+ function formatBytes(n) {
1615
+ if (typeof n !== "number" || !isFinite(n)) return "";
1616
+ if (n < 1024) return n + "b";
1617
+ if (n < 1024 * 1024) return Math.round(n / 102.4) / 10 + "kb";
1618
+ return Math.round(n / 1024 / 102.4) / 10 + "mb";
1619
+ }
1620
+ function renderGenericArtifact(a, state) {
1621
+ if (!state || state.status === "loading") return '<div class="fi-load"><span class="wait-spinner" style="width:18px;height:18px;display:inline-block;vertical-align:middle;margin-right:8px"></span>Loading artifact…</div>';
1622
+ if (state.status === "offline") return '<div class="fi-load">Static file view cannot hydrate artifacts inline. Open the artifact link directly.</div>';
1623
+ if (state.status === "error") return '<div class="fi-load">Could not load artifact: ' + esc(state.error || "unknown error") + '</div>';
1624
+ return '<div class="file-inspector"><div class="fi-head"><div class="fi-title"><h3>' + esc(a.label || a.path) + '</h3><span class="fi-status">artifact</span></div>'
1625
+ + '<div class="fi-summary mono">' + esc(a.path) + '</div></div>'
1626
+ + '<div class="fi-section"><pre class="fi-pre">' + esc(state.text || "") + '</pre></div></div>';
1627
+ }
1628
+ function renderSetupQualityArtifact(a, state) {
1629
+ if (!state || state.status !== "loaded" || !state.json || state.json.schema !== "mimetic.setup-quality.v1") {
1630
+ return renderGenericArtifact(a, state);
1631
+ }
1632
+ var q = state.json;
1633
+ var checks = q.checks || [];
1634
+ var mim = q.mimetic || {};
1635
+ var scripts = q.packageScripts || {};
1636
+ var tree = q.tree || [];
1637
+ var previews = q.previews || [];
1638
+ var scriptRows = Object.keys(scripts).sort().slice(0, 18).map(function (key) {
1639
+ return '<div class="fi-tree-row"><span>$</span><span>' + esc(key) + '</span><span class="fi-tree-size">' + esc(scripts[key]) + '</span></div>';
1640
+ }).join("");
1641
+ var treeRows = tree.slice(0, 160).map(function (entry) {
1642
+ return '<div class="fi-tree-row" data-type="' + esc(entry.type || "file") + '"><span>' + (entry.type === "directory" ? "dir" : "file") + '</span><span>' + esc(entry.path) + '</span><span class="fi-tree-size">' + esc(formatBytes(entry.sizeBytes)) + '</span></div>';
1643
+ }).join("");
1644
+ var previewRows = previews.length ? previews.map(function (preview) {
1645
+ return '<div class="fi-preview"><div class="fi-preview-head"><span>' + esc(preview.path) + '</span><span>' + esc(preview.language || "text") + (preview.truncated ? " · truncated" : "") + '</span></div><pre class="fi-pre">' + esc(preview.text || "") + '</pre></div>';
1646
+ }).join("") : '<div class="tab-empty">No raw file previews persisted for this run.</div>';
1647
+ return '<div class="file-inspector"><div class="fi-head"><div class="fi-title"><h3>Setup quality</h3><span class="fi-status" data-status="' + esc(q.status || "unknown") + '">' + esc(q.status || "unknown") + '</span></div>'
1648
+ + '<div class="fi-summary">' + esc(q.summary || "") + '</div><div class="fi-summary mono">' + esc(a.path) + '</div></div>'
1649
+ + '<div class="fi-section"><div class="fi-grid">'
1650
+ + '<div class="fi-stat"><div class="fi-stat-k">personas</div><div class="fi-stat-v">' + esc(mim.personaCount || 0) + '</div></div>'
1651
+ + '<div class="fi-stat"><div class="fi-stat-k">scenarios</div><div class="fi-stat-v">' + esc(mim.scenarioCount || 0) + '</div></div>'
1652
+ + '<div class="fi-stat"><div class="fi-stat-k">config</div><div class="fi-stat-v">' + (mim.configPresent ? "present" : "missing") + '</div></div>'
1653
+ + '<div class="fi-stat"><div class="fi-stat-k">runtime ignore</div><div class="fi-stat-v">' + (mim.gitignoreContainsRuntimeIgnore ? "present" : "missing") + '</div></div>'
1654
+ + '</div></div>'
1655
+ + '<div class="fi-section"><div class="fi-section-title"><span>Checks</span><span class="mono">' + checks.filter(function (c) { return c.ok; }).length + ' / ' + checks.length + '</span></div>'
1656
+ + checks.map(function (check) { return '<div class="fi-check" data-ok="' + (check.ok ? "true" : "false") + '"><span class="fi-dot">' + icon(check.ok ? "check" : "alert", 11) + '</span><div><div class="fi-check-name">' + esc(check.label) + '</div><div class="fi-check-detail">' + esc(check.detail) + '</div></div></div>'; }).join("") + '</div>'
1657
+ + '<div class="fi-section"><div class="fi-section-title"><span>Package scripts</span><span class="mono">' + Object.keys(scripts).length + '</span></div><div class="fi-tree">' + (scriptRows || '<div class="tab-empty">No package scripts captured.</div>') + '</div></div>'
1658
+ + '<div class="fi-section"><div class="fi-section-title"><span>Tree</span><span class="mono">' + tree.length + '</span></div><div class="fi-tree">' + (treeRows || '<div class="tab-empty">No tree entries captured.</div>') + '</div></div>'
1659
+ + '<div class="fi-section"><div class="fi-section-title"><span>Previews</span><span class="mono">' + previews.length + '</span></div>' + previewRows + '</div></div>';
1660
+ }
1661
+ function buildFilesTab(s) {
1662
+ var arts = laneArtifacts(s);
1663
+ if (!arts.length) return '<div class="tab-empty">No evidence artifacts linked.</div>';
1664
+ var selected = null;
1665
+ for (var i = 0; i < arts.length; i += 1) {
1666
+ if (arts[i].path === S.filePath) { selected = arts[i]; break; }
1667
+ }
1668
+ if (!selected) {
1669
+ for (var j = 0; j < arts.length; j += 1) {
1670
+ if (arts[j].kind === "filesystem") { selected = arts[j]; break; }
1671
+ }
1672
+ }
1673
+ var inspector = "";
1674
+ if (selected && selected.kind === "filesystem") {
1675
+ ensureArtifactLoaded(selected);
1676
+ inspector = renderSetupQualityArtifact(selected, artifactCache[selected.path]);
1677
+ }
1678
+ var rows = arts.map(function (a) {
1679
+ var href = artifactHref(a);
1680
+ var isSelected = selected && selected.path === a.path;
1681
+ var inspectable = a.kind === "filesystem";
1682
+ var main = inspectable
1683
+ ? '<button data-action="inspect-file:' + esc(a.path) + '"><span class="file-ic">' + icon("file", 14) + '</span><span class="file-meta"><div class="file-name">' + esc(a.label) + '</div><div class="file-path mono">' + esc(a.path) + '</div></span><span class="file-kind">' + esc(a.kind) + '</span></button>'
1684
+ : '<a href="' + esc(href) + '" target="_blank" rel="noopener noreferrer" data-action="external"><span class="file-ic">' + icon("file", 14) + '</span><span class="file-meta"><div class="file-name">' + esc(a.label) + '</div><div class="file-path mono">' + esc(a.path) + '</div></span><span class="file-kind">' + esc(a.kind) + '</span></a>';
1685
+ return '<div class="file-row" data-selected="' + (isSelected ? "true" : "false") + '">' + main
1686
+ + (href ? '<a class="file-open" href="' + esc(href) + '" target="_blank" rel="noopener noreferrer" data-action="external">open</a>' : "")
1687
+ + '</div>';
1688
+ }).join("");
1689
+ return inspector + rows;
1690
+ }
1396
1691
  function buildSideBody(s) {
1397
1692
  if (S.tab === "events") {
1398
1693
  var evs = laneEvents(s);
@@ -1404,13 +1699,7 @@ export function observerClientJs() {
1404
1699
  }).join("");
1405
1700
  }
1406
1701
  if (S.tab === "files") {
1407
- var arts = laneArtifacts(s);
1408
- if (!arts.length) return '<div class="tab-empty">No evidence artifacts linked.</div>';
1409
- return arts.map(function (a) {
1410
- return '<a class="file-row" href="../' + esc(a.path) + '"><span class="file-ic">' + icon("file", 14) + '</span>'
1411
- + '<span class="file-meta"><div class="file-name">' + esc(a.label) + '</div><div class="file-path mono">' + esc(a.path) + '</div></span>'
1412
- + '<span class="file-kind">' + esc(a.kind) + '</span></a>';
1413
- }).join("");
1702
+ return buildFilesTab(s);
1414
1703
  }
1415
1704
  var lines = termLines(s);
1416
1705
  if (!lines.length) lines = [{ text: "No log text recorded for this lane.", cls: "dim" }];
@@ -1596,6 +1885,141 @@ export function observerClientJs() {
1596
1885
  menu.style.left = left + "px";
1597
1886
  menu.style.visibility = "visible";
1598
1887
  }
1888
+ function ensureLiveStreamHost() {
1889
+ if (liveStreamHost) return liveStreamHost;
1890
+ if (!document.createElement) return null;
1891
+ var host = document.body || document.documentElement;
1892
+ if (!host || !host.appendChild) return null;
1893
+ liveStreamHost = document.createElement("div");
1894
+ liveStreamHost.setAttribute("data-live-stream-host", "true");
1895
+ liveStreamHost.style.position = "fixed";
1896
+ liveStreamHost.style.inset = "0";
1897
+ liveStreamHost.style.overflow = "visible";
1898
+ liveStreamHost.style.pointerEvents = "none";
1899
+ liveStreamHost.style.zIndex = "20";
1900
+ host.appendChild(liveStreamHost);
1901
+ return liveStreamHost;
1902
+ }
1903
+ function createLiveStreamRecord(id, url, title) {
1904
+ var host = ensureLiveStreamHost();
1905
+ if (!host) return null;
1906
+ var wrapper = document.createElement("div");
1907
+ wrapper.setAttribute("data-live-stream-wrapper", id);
1908
+ wrapper.style.position = "fixed";
1909
+ wrapper.style.overflow = "hidden";
1910
+ wrapper.style.background = "#000";
1911
+ wrapper.style.pointerEvents = "none";
1912
+ wrapper.style.display = "none";
1913
+ var frame = document.createElement("iframe");
1914
+ frame.style.position = "absolute";
1915
+ frame.style.border = "0";
1916
+ frame.style.margin = "0";
1917
+ frame.style.display = "block";
1918
+ frame.setAttribute("allow", "clipboard-read; clipboard-write; fullscreen");
1919
+ frame.setAttribute("referrerpolicy", "no-referrer");
1920
+ frame.setAttribute("title", title || "live stream");
1921
+ frame.setAttribute("data-live-stream-frame", id);
1922
+ frame.src = url;
1923
+ var overlay = document.createElement("div");
1924
+ overlay.className = "live-stream-overlay";
1925
+ overlay.style.position = "absolute";
1926
+ overlay.style.overflow = "hidden";
1927
+ overlay.style.pointerEvents = "none";
1928
+ overlay.style.zIndex = "2";
1929
+ wrapper.appendChild(frame);
1930
+ wrapper.appendChild(overlay);
1931
+ host.appendChild(wrapper);
1932
+ return { url: url, wrapper: wrapper, frame: frame, overlay: overlay, overlayHtml: "" };
1933
+ }
1934
+ function removeLiveStreamRecord(id) {
1935
+ var rec = liveStreamFrames[id];
1936
+ if (rec && rec.wrapper && rec.wrapper.parentNode) rec.wrapper.parentNode.removeChild(rec.wrapper);
1937
+ delete liveStreamFrames[id];
1938
+ }
1939
+ function hideLiveStreamRecord(rec) {
1940
+ if (!rec || !rec.wrapper) return;
1941
+ rec.wrapper.style.display = "none";
1942
+ rec.wrapper.style.pointerEvents = "none";
1943
+ }
1944
+ function clipRect(rect) {
1945
+ var stage = app.querySelector && app.querySelector(".stage");
1946
+ var s = stage && stage.getBoundingClientRect ? stage.getBoundingClientRect() : null;
1947
+ var left = Math.max(rect.left, 0, s ? s.left : 0);
1948
+ var top = Math.max(rect.top, 0, s ? s.top : 0);
1949
+ var right = Math.min(rect.right, window.innerWidth || rect.right, s ? s.right : rect.right);
1950
+ var bottom = Math.min(rect.bottom, window.innerHeight || rect.bottom, s ? s.bottom : rect.bottom);
1951
+ return { left: left, top: top, right: right, bottom: bottom, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
1952
+ }
1953
+ function layoutLiveStreamRecord(rec, mount, overlayHtml) {
1954
+ if (!rec || !mount || !mount.getBoundingClientRect) return hideLiveStreamRecord(rec);
1955
+ var rect = mount.getBoundingClientRect();
1956
+ var clipped = clipRect(rect);
1957
+ if (rect.width < 2 || rect.height < 2 || clipped.width < 2 || clipped.height < 2) return hideLiveStreamRecord(rec);
1958
+ rec.wrapper.style.display = "block";
1959
+ rec.wrapper.style.left = clipped.left + "px";
1960
+ rec.wrapper.style.top = clipped.top + "px";
1961
+ rec.wrapper.style.width = clipped.width + "px";
1962
+ rec.wrapper.style.height = clipped.height + "px";
1963
+ rec.wrapper.style.pointerEvents = S.view === "focus" ? "auto" : "none";
1964
+ rec.frame.style.left = (rect.left - clipped.left) + "px";
1965
+ rec.frame.style.top = (rect.top - clipped.top) + "px";
1966
+ rec.frame.style.width = rect.width + "px";
1967
+ rec.frame.style.height = rect.height + "px";
1968
+ rec.frame.style.pointerEvents = S.view === "focus" ? "auto" : "none";
1969
+ if (rec.overlay) {
1970
+ rec.overlay.style.left = rec.frame.style.left;
1971
+ rec.overlay.style.top = rec.frame.style.top;
1972
+ rec.overlay.style.width = rec.frame.style.width;
1973
+ rec.overlay.style.height = rec.frame.style.height;
1974
+ rec.overlay.setAttribute("data-focus", S.view === "focus" ? "true" : "false");
1975
+ if (rec.overlayHtml !== overlayHtml) {
1976
+ rec.overlay.innerHTML = overlayHtml || "";
1977
+ rec.overlayHtml = overlayHtml || "";
1978
+ }
1979
+ }
1980
+ }
1981
+ function reconcileLiveStreams() {
1982
+ if (!app.querySelectorAll || !document.createElement) return;
1983
+ var known = {};
1984
+ currentData.streams.forEach(function (s) {
1985
+ var url = browserLiveUrl(s);
1986
+ if (url) known[s.id] = { url: url, stream: s };
1987
+ });
1988
+ var mounts = app.querySelectorAll("[data-live-stream-id]");
1989
+ var visible = {};
1990
+ Array.prototype.forEach.call(mounts, function (mount) {
1991
+ var id = mount.getAttribute("data-live-stream-id");
1992
+ var url = mount.getAttribute("data-live-stream-url") || "";
1993
+ var title = mount.getAttribute("data-live-stream-title") || "live stream";
1994
+ if (!id || !url) return;
1995
+ var knownStream = known[id] && known[id].stream;
1996
+ var overlayHtml = knownStream ? browserLabDock(knownStream, browserShot(knownStream)) : "";
1997
+ var rec = liveStreamFrames[id];
1998
+ if (!rec || rec.url !== url) {
1999
+ removeLiveStreamRecord(id);
2000
+ rec = createLiveStreamRecord(id, url, title);
2001
+ if (!rec) return;
2002
+ liveStreamFrames[id] = rec;
2003
+ } else {
2004
+ rec.frame.setAttribute("title", title);
2005
+ }
2006
+ visible[id] = true;
2007
+ layoutLiveStreamRecord(rec, mount, overlayHtml);
2008
+ });
2009
+ Object.keys(liveStreamFrames).forEach(function (id) {
2010
+ var rec = liveStreamFrames[id];
2011
+ if (!known[id] || (rec && rec.url !== known[id].url)) removeLiveStreamRecord(id);
2012
+ else if (!visible[id]) hideLiveStreamRecord(rec);
2013
+ });
2014
+ }
2015
+ function scheduleLiveStreamLayout() {
2016
+ if (liveStreamLayoutRaf != null) return;
2017
+ var raf = window.requestAnimationFrame || function (fn) { return window.setTimeout(fn, 16); };
2018
+ liveStreamLayoutRaf = raf(function () {
2019
+ liveStreamLayoutRaf = null;
2020
+ reconcileLiveStreams();
2021
+ });
2022
+ }
1599
2023
 
1600
2024
  function render() {
1601
2025
  var docEl = document.documentElement;
@@ -1631,6 +2055,7 @@ export function observerClientJs() {
1631
2055
  if (inp) { inp.focus(); try { inp.setSelectionRange(caret, caret); } catch (e) {} }
1632
2056
  }
1633
2057
  placeMenus();
2058
+ reconcileLiveStreams();
1634
2059
  var cb = document.getElementById("console-body");
1635
2060
  if (cb && scrolls[".console-body"] == null) cb.scrollTop = cb.scrollHeight;
1636
2061
  }
@@ -1689,6 +2114,7 @@ export function observerClientJs() {
1689
2114
  if (arg === "status") toggleArr(S.statusSel, arg2); else toggleArr(S.kindSel, arg2);
1690
2115
  render(); break;
1691
2116
  case "toggle-console": S.consoleOpen = !S.consoleOpen; render(); break;
2117
+ case "inspect-file": S.filePath = action.slice("inspect-file:".length); S.tab = "files"; render(); break;
1692
2118
  case "toggle-details": S.detailsOpen = !S.detailsOpen; S.tweaksOpen = false; render(); break;
1693
2119
  case "toggle-tweaks": S.tweaksOpen = !S.tweaksOpen; S.detailsOpen = false; render(); break;
1694
2120
  case "toggle-theme": S.theme = (S.theme === "light" ? "dark" : "light"); writePref("theme", S.theme); render(); break;
@@ -1758,11 +2184,33 @@ export function observerClientJs() {
1758
2184
  S.focusedId = next; if (S.view !== "focus") S.view = "focus"; render();
1759
2185
  }
1760
2186
  });
1761
- window.addEventListener("resize", function () { if (openDd) placeMenus(); });
2187
+ window.addEventListener("resize", function () { if (openDd) placeMenus(); scheduleLiveStreamLayout(); });
2188
+ app.addEventListener("scroll", scheduleLiveStreamLayout, true);
1762
2189
 
1763
2190
  // ================================================================ POLLING
1764
2191
  function dataKey(d) {
1765
- return (d.generatedAt || "") + "|" + (d.streams || []).map(function (s) { return s.id + s.status + laneProgress(s); }).join(",");
2192
+ var streams = (d.streams || []).map(function (s) {
2193
+ return {
2194
+ id: s.id,
2195
+ status: s.status,
2196
+ progress: laneProgress(s),
2197
+ step: laneStep(s),
2198
+ updatedAt: s.updatedAt,
2199
+ url: browserLiveUrl(s),
2200
+ route: laneRoute(s),
2201
+ tail: (s.terminalPlain || (s.terminal && s.terminal.tail) || "").slice(-1200),
2202
+ artifacts: (s.artifacts || []).map(function (a) { return [a.kind, a.path, a.label]; }),
2203
+ completion: s.completion || null
2204
+ };
2205
+ });
2206
+ var events = (d.events || []).slice(-200).map(function (e) { return [e.at, e.level, e.type, e.simId, e.streamId, e.message]; });
2207
+ var run = d.run || {};
2208
+ return JSON.stringify({
2209
+ run: [run.runId, run.status, (run.lifecycle || []).length, (run.knownGaps || []).length],
2210
+ summary: d.summary || {},
2211
+ streams: streams,
2212
+ events: events
2213
+ });
1766
2214
  }
1767
2215
  function refresh() {
1768
2216
  if (location.protocol === "file:") return Promise.resolve();
@@ -1794,7 +2242,7 @@ export function observerClientJs() {
1794
2242
  render();
1795
2243
  refresh().then(function () {
1796
2244
  if (location.protocol !== "file:") {
1797
- refreshTimer = setInterval(refresh, 2500);
2245
+ refreshTimer = setInterval(refresh, REFRESH_MS);
1798
2246
  refreshHistoryIndex();
1799
2247
  historyTimer = setInterval(refreshHistoryIndex, 30000);
1800
2248
  }