pilotswarm 0.5.23 → 0.5.25

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 (79) hide show
  1. package/package.json +5 -3
  2. package/ui/core/src/controller.js +328 -5
  3. package/ui/core/src/formatting.js +46 -1
  4. package/ui/core/src/history.js +29 -0
  5. package/ui/core/src/reducer.js +49 -0
  6. package/ui/core/src/selectors.js +154 -13
  7. package/ui/core/src/state.js +11 -0
  8. package/ui/core/src/themes/github-light.js +5 -1
  9. package/ui/core/src/themes/index.js +4 -0
  10. package/ui/core/src/themes/light-high-contrast.js +4 -1
  11. package/ui/core/src/themes/ms-dos.js +84 -0
  12. package/ui/core/src/themes/win95.js +81 -0
  13. package/ui/react/src/web-app.js +886 -78
  14. package/web/dist/assets/{abnfDiagram-VRR7QNED-BHkTpRkx.js → abnfDiagram-VRR7QNED-C4qRR7va.js} +1 -1
  15. package/web/dist/assets/{arc-BOgZyEWA.js → arc-BQw2V2pL.js} +1 -1
  16. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DeE8fDMy.js → architectureDiagram-ZJ3FMSHR-DMdWnLGG.js} +1 -1
  17. package/web/dist/assets/{blockDiagram-677ZJIJ3-CzhdxVEu.js → blockDiagram-677ZJIJ3-DZnALw8j.js} +1 -1
  18. package/web/dist/assets/{c4Diagram-LMCZKHZV-Cc6p_IKP.js → c4Diagram-LMCZKHZV-BGGZ42iX.js} +1 -1
  19. package/web/dist/assets/channel-CB157dQ5.js +1 -0
  20. package/web/dist/assets/{chunk-2Q5K7J3B-BCo94LTM.js → chunk-2Q5K7J3B-Dastiegd.js} +1 -1
  21. package/web/dist/assets/{chunk-32BRIVSS-DaklJ-6i.js → chunk-32BRIVSS-B4XexRzf.js} +1 -1
  22. package/web/dist/assets/{chunk-5VM5RSS4-BN_wg2pr.js → chunk-5VM5RSS4-D66MNGCN.js} +1 -1
  23. package/web/dist/assets/{chunk-EX3LRPZG-DSFFLUoL.js → chunk-EX3LRPZG-DeqPS4Xm.js} +1 -1
  24. package/web/dist/assets/{chunk-JWPE2WC7-DB4dAjza.js → chunk-JWPE2WC7-DIMWiXpM.js} +1 -1
  25. package/web/dist/assets/{chunk-MOJQB5TN-CRM5eGQS.js → chunk-MOJQB5TN-vTyL0qgx.js} +1 -1
  26. package/web/dist/assets/{chunk-RYQCIY6F-BFlkjazU.js → chunk-RYQCIY6F-C7U9MKGw.js} +1 -1
  27. package/web/dist/assets/{chunk-V7JOEXUC-Bnfh4AAv.js → chunk-V7JOEXUC-BzPwQ7n3.js} +1 -1
  28. package/web/dist/assets/{chunk-VR4S4FIN-CQc4_qVM.js → chunk-VR4S4FIN-CmDsyK3b.js} +1 -1
  29. package/web/dist/assets/{chunk-XXDRQBXY-CZivyoDm.js → chunk-XXDRQBXY-ZJn6BaGL.js} +1 -1
  30. package/web/dist/assets/classDiagram-OUVF2IWQ-B6JwBYlv.js +1 -0
  31. package/web/dist/assets/classDiagram-v2-EOCWNBFH-B6JwBYlv.js +1 -0
  32. package/web/dist/assets/{cose-bilkent-JH36ORCC-BUibhTD-.js → cose-bilkent-JH36ORCC-CAA8JZZM.js} +1 -1
  33. package/web/dist/assets/{cynefin-VYW2F7L2-CRkjg5zF.js → cynefin-VYW2F7L2-B6pX-lUD.js} +1 -1
  34. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BoeQn4Um.js → cynefinDiagram-TSTJHNR4-sKTHU_UH.js} +1 -1
  35. package/web/dist/assets/{dagre-VKFMJZFB-DGo7-PY5.js → dagre-VKFMJZFB-EuakJ578.js} +1 -1
  36. package/web/dist/assets/{diagram-FQU43EPY-DMXd2bdO.js → diagram-FQU43EPY-DR3M_pic.js} +1 -1
  37. package/web/dist/assets/{diagram-G47NLZAW-CkYHblMZ.js → diagram-G47NLZAW-BlS6yM0u.js} +1 -1
  38. package/web/dist/assets/{diagram-NH7WQ7WH-C9iwEe4T.js → diagram-NH7WQ7WH-CQNXe6dc.js} +1 -1
  39. package/web/dist/assets/{diagram-OA4YK3LP-DpYLrfwb.js → diagram-OA4YK3LP-BizBeSQo.js} +1 -1
  40. package/web/dist/assets/{diagram-WEI45ONY-BvrBukHb.js → diagram-WEI45ONY-C-_KM6Gg.js} +1 -1
  41. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DGl1r3zR.js → ebnfDiagram-CCIWWBDH-BapWEdic.js} +1 -1
  42. package/web/dist/assets/{erDiagram-Q63AITRT-CoP5DBSb.js → erDiagram-Q63AITRT-Btijn1Xj.js} +1 -1
  43. package/web/dist/assets/{flowDiagram-23GEKE2U-DVHtyYyr.js → flowDiagram-23GEKE2U-D8zK8HkH.js} +1 -1
  44. package/web/dist/assets/{ganttDiagram-NO4QXBWP-_JyR78yC.js → ganttDiagram-NO4QXBWP-DyjkOIMW.js} +1 -1
  45. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-Bj7sh0Db.js → gitGraphDiagram-IHSO6WYX-3r9_QeYN.js} +1 -1
  46. package/web/dist/assets/index-BEhdfIki.css +1 -0
  47. package/web/dist/assets/{index-CB6NfSZb.js → index-m0kBr7aj.js} +2 -2
  48. package/web/dist/assets/{infoDiagram-FWYZ7A6U-BrvMeRmA.js → infoDiagram-FWYZ7A6U-Ccd8ACUz.js} +1 -1
  49. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-lNDujpEC.js → ishikawaDiagram-FXEZZL3T-DPRUC8V2.js} +1 -1
  50. package/web/dist/assets/{journeyDiagram-5HDEW3XC-yoD2QRni.js → journeyDiagram-5HDEW3XC-CkvayMsL.js} +1 -1
  51. package/web/dist/assets/{kanban-definition-HUTT4EX6-DtyMlypg.js → kanban-definition-HUTT4EX6-CAK9Z-ha.js} +1 -1
  52. package/web/dist/assets/{linear-DXPbXF9v.js → linear-B-_ZjN2w.js} +1 -1
  53. package/web/dist/assets/{mermaid.core-DCEXqVUL.js → mermaid.core-BN9c--2r.js} +4 -4
  54. package/web/dist/assets/{mindmap-definition-LN4V7U3C-DpKHDJFg.js → mindmap-definition-LN4V7U3C-DuD_AHRU.js} +1 -1
  55. package/web/dist/assets/{pegDiagram-2B236MQR-mxFI_8qh.js → pegDiagram-2B236MQR-Cc1v58KA.js} +1 -1
  56. package/web/dist/assets/{pieDiagram-ENE6RG2P-sUWLrwMG.js → pieDiagram-ENE6RG2P-Bc32--ur.js} +1 -1
  57. package/web/dist/assets/pilotswarm-BgzdDy09.js +93 -0
  58. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-Cph1z7ye.js → quadrantDiagram-ABIIQ3AL-DpPTc2EP.js} +1 -1
  59. package/web/dist/assets/{railroadDiagram-RFXS5EU6-BPKkt2va.js → railroadDiagram-RFXS5EU6-DXMzwejw.js} +1 -1
  60. package/web/dist/assets/{requirementDiagram-TGXJPOKE-BB6DNHCy.js → requirementDiagram-TGXJPOKE-B3ImAxw-.js} +1 -1
  61. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-BASvPQk2.js → sankeyDiagram-HTMAVEWB-hmVMffE2.js} +1 -1
  62. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DMrNas01.js → sequenceDiagram-DBY2YBRQ-D53fk6uF.js} +1 -1
  63. package/web/dist/assets/{sizeCapture-X5ZJPWSS-Ddq5yeZZ.js → sizeCapture-X5ZJPWSS-4U-I5d2g.js} +1 -1
  64. package/web/dist/assets/{stateDiagram-2N3HPSRC-DUb_IFST.js → stateDiagram-2N3HPSRC-0Y5TgWhW.js} +1 -1
  65. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DqIcGfAY.js +1 -0
  66. package/web/dist/assets/{swimlanes-5IMT3BWC-Cs9JsGaf.js → swimlanes-5IMT3BWC-Umhy4qUu.js} +2 -2
  67. package/web/dist/assets/swimlanesDiagram-G3AALYLV-DA9txLi6.js +8 -0
  68. package/web/dist/assets/{timeline-definition-FHXFAJF6-xuNF5fTm.js → timeline-definition-FHXFAJF6-D5hOg3Qu.js} +1 -1
  69. package/web/dist/assets/{vennDiagram-L72KCM5P-fsI1GFLy.js → vennDiagram-L72KCM5P-QVIcE3UD.js} +1 -1
  70. package/web/dist/assets/{wardleyDiagram-EHGQE667-DEc-kumC.js → wardleyDiagram-EHGQE667-BEj1M2cB.js} +1 -1
  71. package/web/dist/assets/{xychartDiagram-FW5EYKEG-BCo6Qrfu.js → xychartDiagram-FW5EYKEG-xojRIXIf.js} +1 -1
  72. package/web/dist/index.html +3 -3
  73. package/web/dist/assets/channel-DhhWyKRW.js +0 -1
  74. package/web/dist/assets/classDiagram-OUVF2IWQ-CQBxGpio.js +0 -1
  75. package/web/dist/assets/classDiagram-v2-EOCWNBFH-CQBxGpio.js +0 -1
  76. package/web/dist/assets/index-SmXlZ1eu.css +0 -1
  77. package/web/dist/assets/pilotswarm-CdEkcq2V.js +0 -91
  78. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C3nsHf5u.js +0 -1
  79. package/web/dist/assets/swimlanesDiagram-G3AALYLV-C2xZA5V6.js +0 -8
@@ -12,6 +12,7 @@ import {
12
12
  extractHttpLinks,
13
13
  formatHumanDurationSeconds,
14
14
  formatTimestamp,
15
+ formatTimestampCompact,
15
16
  padRunsToDisplayWidth,
16
17
  parseMarkdownLines,
17
18
  shortModelName,
@@ -324,7 +325,38 @@ function splitTypedSessionTitle(displayTitle) {
324
325
  return { userTitle, typeTitle };
325
326
  }
326
327
 
328
+ // Titles are plain text everywhere they are rendered (terminal runs, DOM
329
+ // text nodes) — never interpreted as HTML. A title that arrived already
330
+ // HTML-escaped therefore shows its entities literally ("PostgreSQL &
331
+ // MySQL"). Decoding the handful of entities an escaper emits repairs those
332
+ // rows on read; the write path also normalizes now, so this covers history.
333
+ const HTML_ENTITY_REPLACEMENTS = [
334
+ [/&lt;/g, "<"],
335
+ [/&gt;/g, ">"],
336
+ [/&quot;/g, '"'],
337
+ [/&#0?39;/g, "'"],
338
+ [/&apos;/g, "'"],
339
+ [/&nbsp;/g, " "],
340
+ // Ampersand LAST: decoding it first would let "&amp;lt;" collapse to "<".
341
+ [/&amp;/g, "&"],
342
+ ];
343
+
344
+ export function decodeHtmlEntitiesForDisplay(value) {
345
+ let text = String(value ?? "");
346
+ if (!text.includes("&")) return text;
347
+ for (const [pattern, replacement] of HTML_ENTITY_REPLACEMENTS) {
348
+ text = text.replace(pattern, replacement);
349
+ }
350
+ return text;
351
+ }
352
+
327
353
  function buildSessionDisplayTitle(session) {
354
+ // Decode at the single exit so the agent-prefixed and typed-title paths
355
+ // are covered too, not just the bare title.
356
+ return decodeHtmlEntitiesForDisplay(buildRawSessionDisplayTitle(session));
357
+ }
358
+
359
+ function buildRawSessionDisplayTitle(session) {
328
360
  const title = String(session?.title || "").trim();
329
361
  const splitTitle = splitAgentPrefixedSessionTitle(session);
330
362
  if (!splitTitle) return title;
@@ -2356,6 +2388,8 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
2356
2388
  lines.push(buildEpochDividerLine(message, maxWidth));
2357
2389
  } else if (message?.kind === "regen-refused") {
2358
2390
  lines.push(buildRegenRefusedLine(message, maxWidth));
2391
+ } else if (message?.kind === "regen-failed") {
2392
+ lines.push(buildRegenFailedLine(message, maxWidth));
2359
2393
  } else {
2360
2394
  const messageLines = buildChatMessageLines(message, maxWidth, buildOptions);
2361
2395
  appendChatBlockLines(lines, messageLines);
@@ -2380,7 +2414,7 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
2380
2414
  // knows every edge case.
2381
2415
  function isRichRenderableChatMessage(message) {
2382
2416
  if (!message) return false;
2383
- if (message.kind === "epoch-divider" || message.kind === "regen-refused") return false;
2417
+ if (message.kind === "epoch-divider" || message.kind === "regen-refused" || message.kind === "regen-failed") return false;
2384
2418
  if (message.splash || message.thinking || message.noChrome) return false;
2385
2419
  if (message.role !== "user" && message.role !== "assistant") return false;
2386
2420
  if (message.role === "user" && parseAskedAndAnsweredExchange(message?.text || "")) return false;
@@ -2431,6 +2465,10 @@ export function selectChatBlocks(state, maxWidth = 80, options = {}) {
2431
2465
  blocks.push({ kind: "lines", variant: "divider", lines: [buildEpochDividerLine(message, maxWidth)] });
2432
2466
  continue;
2433
2467
  }
2468
+ if (message?.kind === "regen-failed") {
2469
+ blocks.push({ kind: "lines", variant: "divider", lines: [buildRegenFailedLine(message, maxWidth)] });
2470
+ continue;
2471
+ }
2434
2472
  if (message?.kind === "regen-refused") {
2435
2473
  blocks.push({ kind: "lines", variant: "divider", lines: [buildRegenRefusedLine(message, maxWidth)] });
2436
2474
  continue;
@@ -2508,6 +2546,18 @@ function buildRegenRefusedLine(message, maxWidth) {
2508
2546
  return buildRuleLine(label, "yellow", maxWidth);
2509
2547
  }
2510
2548
 
2549
+ // A regeneration that was accepted then failed downstream. RED (vs yellow for
2550
+ // a refusal): a refusal is the system declining on purpose, a failure is the
2551
+ // pipeline breaking. Names the stage and the underlying cause, because the
2552
+ // agent's own turn will have already claimed success from the optimistic ack.
2553
+ function buildRegenFailedLine(message, maxWidth) {
2554
+ const stage = String(message?.stage || "unknown");
2555
+ const raw = String(message?.error || "").split("\n")[0];
2556
+ const cause = raw.length > 120 ? `${raw.slice(0, 117)}…` : raw;
2557
+ const label = ` ✕ regeneration failed at ${stage}${cause ? ` · ${cause}` : ""} `;
2558
+ return buildRuleLine(label, "red", maxWidth);
2559
+ }
2560
+
2511
2561
  export function selectOutboxOverlayLines(state, maxWidth = 80, options = {}) {
2512
2562
  const messages = selectActiveOutboxMessages(state);
2513
2563
  if (!messages || messages.length === 0) return [];
@@ -2524,11 +2574,15 @@ export function selectOutboxOverlayLines(state, maxWidth = 80, options = {}) {
2524
2574
  if (rejectedCount > 0) parts.push(`${rejectedCount} rejected`);
2525
2575
  const label = parts.length > 0 ? parts.join(" · ") : "queued prompts";
2526
2576
  const labelText = ` queued prompts: ${label} `;
2527
- const rightRule = Math.max(1, safeWidth - labelText.length);
2577
+ // The trailing rule is drawn by CSS, not by repeating "─" to a computed
2578
+ // character count. That count is derived from a pane width measured in
2579
+ // CHARACTERS, and when the estimate exceeds what actually fits — easily
2580
+ // triggered by a font-size change, or by probing a pane whose font differs
2581
+ // from this one — label + rule overflow and the rule wraps onto its own
2582
+ // line. A flex rule fills whatever space is really left, at any font size.
2528
2583
  const lines = [
2529
2584
  [
2530
- { text: labelText, color: "gray" },
2531
- { text: "─".repeat(rightRule), color: "gray" },
2585
+ { text: labelText, color: "gray", trailingRule: true },
2532
2586
  ],
2533
2587
  ];
2534
2588
 
@@ -2904,6 +2958,40 @@ export function selectFileSessionIdsForScope(state, scope = selectFilesScope(sta
2904
2958
  return ordered;
2905
2959
  }
2906
2960
 
2961
+ /**
2962
+ * Artifacts in the order they appear in the CONVERSATION.
2963
+ *
2964
+ * The Files list is sorted for browsing (alphabetical); the transcript order is
2965
+ * the order the work actually happened in. Stepping through a preview opened
2966
+ * from a chat link should follow the conversation, not the filing cabinet — so
2967
+ * these are two distinct sequences and neither can be derived from the other.
2968
+ *
2969
+ * Ids are `<sessionId>/<filename>`, matching files.selectedArtifactId.
2970
+ */
2971
+ export function selectChatOrderedArtifactIds(state) {
2972
+ const session = selectActiveSession(state);
2973
+ const sessionId = session?.sessionId;
2974
+ if (!sessionId) return [];
2975
+ const history = state.history?.bySessionId?.get?.(sessionId);
2976
+ const events = history?.events || [];
2977
+ const seen = new Set();
2978
+ const ordered = [];
2979
+ const re = /artifact:\/\/([a-f0-9-]+)\/([^\s"'{})\]]+)/g;
2980
+ for (const event of events) {
2981
+ const text = typeof event?.data?.content === "string" ? event.data.content : "";
2982
+ if (!text) continue;
2983
+ re.lastIndex = 0;
2984
+ let m;
2985
+ while ((m = re.exec(text)) !== null) {
2986
+ const id = `${m[1]}/${m[2]}`;
2987
+ if (seen.has(id)) continue;
2988
+ seen.add(id);
2989
+ ordered.push(id);
2990
+ }
2991
+ }
2992
+ return ordered;
2993
+ }
2994
+
2907
2995
  export function selectFileBrowserItems(state) {
2908
2996
  const scope = selectFilesScope(state);
2909
2997
  const activeSessionId = selectActiveSession(state)?.sessionId || null;
@@ -3656,7 +3744,9 @@ function isSequenceOrchestratorType(type) {
3656
3744
  }
3657
3745
 
3658
3746
  function mapEventToSequenceEntry(event) {
3659
- const time = formatTimestamp(event?.createdAt);
3747
+ // Compact: the sequence TIME column is fixed-width, and the full form
3748
+ // truncates any dated row to "26 Jul …", losing the time entirely.
3749
+ const time = formatTimestampCompact(event?.createdAt);
3660
3750
  const nodeLabel = shortNodeLabel(event?.workerNodeId);
3661
3751
  const detailText = eventMessageText(event);
3662
3752
  const preview = summarizeEventPreview(detailText, 20);
@@ -3849,7 +3939,7 @@ function buildSessionStatusSequenceEntry(session) {
3849
3939
  const safeCreatedAtMs = Number.isFinite(createdAtMs) ? createdAtMs : Date.now();
3850
3940
 
3851
3941
  return {
3852
- time: formatTimestamp(safeCreatedAtMs),
3942
+ time: formatTimestampCompact(safeCreatedAtMs),
3853
3943
  createdAtMs: safeCreatedAtMs,
3854
3944
  nodeLabel: "orch",
3855
3945
  color: errorKind === "failed" ? "red" : "yellow",
@@ -3908,6 +3998,39 @@ function buildNodeMapNodeUnionForWindow(state, window) {
3908
3998
  return Array.from(labels).sort((left, right) => left.localeCompare(right));
3909
3999
  }
3910
4000
 
4001
+ /**
4002
+ * Strip trailing pad from the last run of a sequence line.
4003
+ *
4004
+ * Every column is padded to colWidth for alignment, but the FINAL column's
4005
+ * trailing spaces align nothing — there is no column after them. With
4006
+ * `white-space: pre` those spaces occupy real width, so every line rendered at
4007
+ * exactly the computed width and the pane had zero tolerance for rounding
4008
+ * between "columns x char width" and its actual pixel width. The result was a
4009
+ * horizontal scrollbar that never went away, on content that visibly fit.
4010
+ */
4011
+ function trimTrailingRunPad(runs) {
4012
+ const out = runs.slice();
4013
+ while (out.length > 0) {
4014
+ const last = out[out.length - 1];
4015
+ const trimmed = String(last.text ?? "").replace(/\s+$/, "");
4016
+ if (trimmed === "") { out.pop(); continue; }
4017
+ out[out.length - 1] = { ...last, text: trimmed };
4018
+ break;
4019
+ }
4020
+ return out;
4021
+ }
4022
+
4023
+ /** Widest rendered line, so decoration never outruns real content. */
4024
+ function sequenceLineWidth(line) {
4025
+ if (Array.isArray(line?.runs)) {
4026
+ return line.runs.reduce((n, r) => n + displayLength(String(r.text ?? "")), 0);
4027
+ }
4028
+ if (Array.isArray(line)) {
4029
+ return line.reduce((n, r) => n + displayLength(String(r.text ?? "")), 0);
4030
+ }
4031
+ return displayLength(String(line?.text ?? ""));
4032
+ }
4033
+
3911
4034
  function buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth) {
3912
4035
  const runs = [
3913
4036
  { text: padDisplayText("TIME", timeWidth), color: "white", bold: true },
@@ -3917,12 +4040,15 @@ function buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth) {
3917
4040
  if (index > 0) runs.push({ text: " ", color: null });
3918
4041
  runs.push({ text: padDisplayText(nodeLabel, colWidth), color: "white", bold: true });
3919
4042
  });
3920
- return runs;
4043
+ return trimTrailingRunPad(runs);
3921
4044
  }
3922
4045
 
3923
- function buildSequenceDividerLine(nodeLabels, timeWidth, colWidth) {
4046
+ function buildSequenceDividerLine(nodeLabels, timeWidth, colWidth, contentWidth = Infinity) {
4047
+ const full = `${"-".repeat(timeWidth)} ${nodeLabels.map(() => "─".repeat(colWidth)).join(" ")}`;
4048
+ // Clip to the widest real line: a full-width rule under short content is
4049
+ // itself enough to force the scrollbar this trimming exists to remove.
3924
4050
  return plainInspectorLine(
3925
- `${"-".repeat(timeWidth)} ${nodeLabels.map(() => "─".repeat(colWidth)).join(" ")}`,
4051
+ Number.isFinite(contentWidth) ? full.slice(0, Math.max(timeWidth, contentWidth)) : full,
3926
4052
  "gray",
3927
4053
  );
3928
4054
  }
@@ -4011,7 +4137,7 @@ function buildSequenceEventLine(entry, nodeLabels, timeWidth, colWidth) {
4011
4137
  }
4012
4138
  });
4013
4139
 
4014
- return runs;
4140
+ return trimTrailingRunPad(runs);
4015
4141
  }
4016
4142
 
4017
4143
  function buildNodeMapHeaderLine(nodeLabels, colWidth) {
@@ -4117,7 +4243,13 @@ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
4117
4243
  .map((entry) => entry.nodeLabel)
4118
4244
  .filter((nodeLabel) => nodeLabel && nodeLabel !== "orch"))).sort((left, right) => left.localeCompare(right));
4119
4245
  const uniqueNodes = unionNodes.length > 0 ? unionNodes : activeSessionNodes;
4120
- const timeWidth = 8;
4246
+ // Size the TIME column to what is actually in it. A fixed 8 fits "20:10:19"
4247
+ // but silently truncates any dated row, which is precisely the row where
4248
+ // the date matters. Capped so one odd stamp cannot starve the lanes.
4249
+ const timeWidth = Math.min(13, Math.max(
4250
+ 8,
4251
+ ...visibleEntries.map((entry) => displayLength(String(entry.time || ""))),
4252
+ ));
4121
4253
  const availableWidth = Math.max(18, maxWidth);
4122
4254
  const maxNodes = Math.max(1, Math.floor((availableWidth - timeWidth - 1) / 6));
4123
4255
  let nodeLabels = ["orch", ...uniqueNodes];
@@ -4158,12 +4290,21 @@ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
4158
4290
  }
4159
4291
  }
4160
4292
 
4293
+ // Decoration is sized to real content, never to the pane's nominal width:
4294
+ // a full-width rule under short content forces the very scrollbar the
4295
+ // trimming above exists to remove.
4296
+ const headerLine = buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth);
4297
+ const contentWidth = lines.reduce(
4298
+ (widest, line) => Math.max(widest, sequenceLineWidth(line)),
4299
+ sequenceLineWidth(headerLine),
4300
+ );
4301
+
4161
4302
  return {
4162
4303
  stickyLines: [
4163
4304
  ...statsLines,
4164
4305
  plainInspectorLine(`Window: ${recentWindow.label}`, "gray"),
4165
- buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth),
4166
- buildSequenceDividerLine(nodeLabels, timeWidth, colWidth),
4306
+ headerLine,
4307
+ buildSequenceDividerLine(nodeLabels, timeWidth, colWidth, contentWidth),
4167
4308
  ],
4168
4309
  lines,
4169
4310
  };
@@ -201,6 +201,17 @@ export function createInitialState({ mode = "local", branding = null, themeId =
201
201
  bySessionId: {},
202
202
  fullscreen: false,
203
203
  selectedArtifactId: null,
204
+ // Bulk selection. Deliberately separate from selectedArtifactId:
205
+ // marking rows must not move the preview, which keeps showing the
206
+ // originally selected artifact.
207
+ markedIds: [],
208
+ // How the preview was opened. "chat" must back out to the chat
209
+ // pane and leave the artifact list exactly as it was; "list" backs
210
+ // out to the list. One back button cannot serve both without this.
211
+ previewOrigin: null,
212
+ // Selection to restore when leaving a chat-opened preview, so
213
+ // following a chat link never disturbs the list.
214
+ restoreArtifactId: null,
204
215
  filter: {
205
216
  scope: "selectedSession",
206
217
  query: "",
@@ -5,7 +5,11 @@ const githubLightTheme = createTheme({
5
5
  label: "GitHub Light",
6
6
  description: "GitHub light palette with crisp neutrals, blue actions, and high-contrast status colors.",
7
7
  page: {
8
- background: "#ffffff",
8
+ // Canvas, deliberately NOT the same as tui.surface (#ffffff). When the
9
+ // page and the panes are both pure white there is nothing for a pane to
10
+ // sit on, and every surface-vs-transparent color-mix in the stylesheet
11
+ // collapses to white. GitHub's own light UI uses exactly this pairing.
12
+ background: "#f6f8fa",
9
13
  foreground: "#1f2328",
10
14
  overlayBackground: "#ffffff",
11
15
  overlayForeground: "#1f2328",
@@ -16,6 +16,8 @@ import darkHighContrastTheme from "./dark-high-contrast.js";
16
16
  import terminalGreenTheme from "./terminal-green.js";
17
17
  import solarizedOpsTheme from "./solarized-ops.js";
18
18
  import highContrastMonoTheme from "./high-contrast-mono.js";
19
+ import win95Theme from "./win95.js";
20
+ import msDosTheme from "./ms-dos.js";
19
21
 
20
22
  const THEMES = Object.freeze([
21
23
  draculaTheme,
@@ -36,6 +38,8 @@ const THEMES = Object.freeze([
36
38
  terminalGreenTheme,
37
39
  solarizedOpsTheme,
38
40
  highContrastMonoTheme,
41
+ win95Theme,
42
+ msDosTheme,
39
43
  ].sort((left, right) => left.label.localeCompare(right.label, undefined, { sensitivity: "base" })));
40
44
 
41
45
  const THEME_MAP = new Map(THEMES.map((theme) => [theme.id, theme]));
@@ -8,7 +8,10 @@ const lightHighContrastTheme = createTheme({
8
8
  label: "Light High Contrast",
9
9
  description: "Pure black on white with maximum-contrast accents — nothing washes out, ever.",
10
10
  page: {
11
- background: "#ffffff",
11
+ // Canvas only — panes stay pure #ffffff (tui.surface), so "black on
12
+ // white" still holds for every piece of text. Without this the panes,
13
+ // headers and composer were all the same white as the page.
14
+ background: "#eef0f2",
12
15
  foreground: "#000000",
13
16
  overlayBackground: "#ffffff",
14
17
  overlayForeground: "#000000",
@@ -0,0 +1,84 @@
1
+ import { createTheme } from "./helpers.js";
2
+
3
+ // MS-DOS 2.0 (1983) — MDA/CGA, text only, no windows.
4
+ //
5
+ // The detail that makes or breaks this: DOS's default attribute was 0x07,
6
+ // which is LIGHT GREY on black, not white. White (#ffffff) was attribute 15,
7
+ // used sparingly for emphasis. A pure white-on-black theme reads as "modern
8
+ // terminal"; #aaaaaa reads as DOS instantly.
9
+ //
10
+ // The palette is the CGA 16, including the quirk everyone remembers — dark
11
+ // yellow rendered as BROWN (#aa5500), never as olive.
12
+ //
13
+ // DOS 2.0 itself drew no boxes; the shell was an 80x25 scrolling field. The
14
+ // code-page-437 framing that index.css adds is how DOS APPLICATIONS looked,
15
+ // and CP437 shipped with the original PC, so it is era-correct even though the
16
+ // 2.0 command line never used it. A multi-pane portal needs some framing, and
17
+ // this is the honest period answer.
18
+ const msDosTheme = createTheme({
19
+ id: "ms-dos",
20
+ label: "MS-DOS 2.0",
21
+ description: "CGA light-grey on black, the 16-colour CGA palette with brown, and CP437 box framing.",
22
+ page: {
23
+ background: "#000000",
24
+ foreground: "#aaaaaa",
25
+ overlayBackground: "#000000",
26
+ overlayForeground: "#aaaaaa",
27
+ // DEVIATION, deliberate: authentic CGA dark grey is #555555, which is
28
+ // roughly 3:1 on black and fails the muted-contrast bar the theme suite
29
+ // enforces. This is the lightest grey that still reads as secondary
30
+ // against #aaaaaa body text while staying legible.
31
+ hintColor: "#8f8f8f",
32
+ modalBackdrop: "rgba(0, 0, 0, 0.78)",
33
+ modalBackground: "#000000",
34
+ modalBorder: "#aaaaaa",
35
+ modalForeground: "#aaaaaa",
36
+ modalMuted: "#8f8f8f",
37
+ // Reverse video — exactly how CGA highlighted a selection.
38
+ modalSelectedBackground: "#aaaaaa",
39
+ modalSelectedBorder: "#ffffff",
40
+ modalSelectedForeground: "#000000",
41
+ },
42
+ terminal: {
43
+ background: "#000000",
44
+ foreground: "#aaaaaa",
45
+ cursor: "#aaaaaa",
46
+ cursorAccent: "#000000",
47
+ selectionBackground: "#aaaaaa",
48
+ // The CGA 16, unmodified.
49
+ black: "#000000",
50
+ red: "#aa0000",
51
+ green: "#00aa00",
52
+ yellow: "#aa5500", // brown — the CGA quirk, not olive
53
+ blue: "#0000aa",
54
+ magenta: "#aa00aa",
55
+ cyan: "#00aaaa",
56
+ white: "#aaaaaa", // attribute 7, the DOS default
57
+ brightBlack: "#8f8f8f",
58
+ brightRed: "#ff5555",
59
+ brightGreen: "#55ff55",
60
+ brightYellow: "#ffff55",
61
+ brightBlue: "#5555ff",
62
+ brightMagenta: "#ff55ff",
63
+ brightCyan: "#55ffff",
64
+ brightWhite: "#ffffff", // attribute 15, emphasis only
65
+ },
66
+ tui: {
67
+ surface: "#000000",
68
+ // Frames are drawn in body grey so they read as CP437 line characters
69
+ // rather than as chrome borrowed from a modern UI.
70
+ border: "#aaaaaa",
71
+ // Bright white is reserved for emphasis, so the operator's own turns
72
+ // get it — the one place attribute 15 is worth spending.
73
+ userChat: "#ffffff",
74
+ userChatLabel: "#ffffff",
75
+ activeHighlightBackground: "#aaaaaa",
76
+ activeHighlightForeground: "#000000",
77
+ selectionBackground: "#aaaaaa",
78
+ selectionForeground: "#000000",
79
+ promptCursorBackground: "#aaaaaa",
80
+ promptCursorForeground: "#000000",
81
+ },
82
+ });
83
+
84
+ export default msDosTheme;
@@ -0,0 +1,81 @@
1
+ import { createTheme } from "./helpers.js";
2
+
3
+ // Windows 95, as it actually was — not a pastel homage.
4
+ //
5
+ // The palette is the real one: #C0C0C0 button face, #000080 active title bar,
6
+ // #008080 desktop teal, and the 16-colour VGA set for terminal output. Those
7
+ // exact values are what make it read as Win95 rather than "grey theme"; the
8
+ // bevels that complete the look are in index.css, keyed on the theme id, since
9
+ // this file can only carry colours.
10
+ //
11
+ // One deliberate departure: the chat pane keeps readable accent colours rather
12
+ // than pure VGA on grey, because a transcript is prose, not a DOS screen.
13
+ const win95Theme = createTheme({
14
+ id: "win95",
15
+ label: "Windows 95",
16
+ description: "Authentic 3D-bevelled grey, navy title bars, teal desktop, and the 16-colour VGA terminal set.",
17
+ page: {
18
+ // Desktop teal behind the panes — the single most recognizable colour
19
+ // of the era, and it makes the grey panes read as raised windows.
20
+ background: "#008080",
21
+ foreground: "#000000",
22
+ overlayBackground: "#c0c0c0",
23
+ overlayForeground: "#000000",
24
+ // The classic disabled/secondary grey; darker than the face so it
25
+ // stays legible on #c0c0c0 rather than vanishing into it.
26
+ hintColor: "#5a5a5a",
27
+ modalBackdrop: "rgba(0, 0, 0, 0.45)",
28
+ modalBackground: "#c0c0c0",
29
+ modalBorder: "#808080",
30
+ modalForeground: "#000000",
31
+ modalMuted: "#5a5a5a",
32
+ // Selection is the title-bar navy, exactly as Explorer did it.
33
+ modalSelectedBackground: "#000080",
34
+ modalSelectedBorder: "#000080",
35
+ modalSelectedForeground: "#ffffff",
36
+ },
37
+ terminal: {
38
+ // Button face, not white: this is a Win95 window, and pure white would
39
+ // break the bevel illusion the whole theme depends on.
40
+ background: "#c0c0c0",
41
+ foreground: "#000000",
42
+ cursor: "#000080",
43
+ cursorAccent: "#c0c0c0",
44
+ selectionBackground: "#000080",
45
+ // The 16-colour VGA palette, unmodified.
46
+ black: "#000000",
47
+ red: "#800000",
48
+ green: "#008000",
49
+ yellow: "#808000",
50
+ blue: "#000080",
51
+ magenta: "#800080",
52
+ cyan: "#008080",
53
+ white: "#c0c0c0",
54
+ brightBlack: "#5a5a5a",
55
+ brightRed: "#c00000",
56
+ brightGreen: "#006000",
57
+ brightYellow: "#7a5c00",
58
+ brightBlue: "#0000c0",
59
+ brightMagenta: "#a000a0",
60
+ brightCyan: "#006a6a",
61
+ brightWhite: "#000000",
62
+ },
63
+ tui: {
64
+ surface: "#c0c0c0",
65
+ // Mid-grey is the Win95 shadow edge; it separates panes from the teal
66
+ // desktop without the black hairline a modern theme would use.
67
+ border: "#808080",
68
+ // Navy on grey for the operator's own turns — the highest-contrast
69
+ // pairing available in this palette.
70
+ userChat: "#000080",
71
+ userChatLabel: "#000080",
72
+ activeHighlightBackground: "#000080",
73
+ activeHighlightForeground: "#ffffff",
74
+ selectionBackground: "#000080",
75
+ selectionForeground: "#ffffff",
76
+ promptCursorBackground: "#000080",
77
+ promptCursorForeground: "#ffffff",
78
+ },
79
+ });
80
+
81
+ export default win95Theme;