pilotswarm 0.5.23 → 0.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/ui/core/src/controller.js +328 -5
- package/ui/core/src/formatting.js +46 -1
- package/ui/core/src/history.js +29 -0
- package/ui/core/src/reducer.js +49 -0
- package/ui/core/src/selectors.js +147 -10
- package/ui/core/src/state.js +11 -0
- package/ui/core/src/themes/github-light.js +5 -1
- package/ui/core/src/themes/index.js +4 -0
- package/ui/core/src/themes/light-high-contrast.js +4 -1
- package/ui/core/src/themes/ms-dos.js +84 -0
- package/ui/core/src/themes/win95.js +81 -0
- package/ui/react/src/web-app.js +806 -67
- package/web/dist/assets/{abnfDiagram-VRR7QNED-BHkTpRkx.js → abnfDiagram-VRR7QNED-Bjz_3uNq.js} +1 -1
- package/web/dist/assets/{arc-BOgZyEWA.js → arc-a3km8Cfi.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DeE8fDMy.js → architectureDiagram-ZJ3FMSHR-4twPAKdk.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-CzhdxVEu.js → blockDiagram-677ZJIJ3-FnFRjWxr.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-Cc6p_IKP.js → c4Diagram-LMCZKHZV-B8R4iXqh.js} +1 -1
- package/web/dist/assets/channel-BQwFhqIW.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-BCo94LTM.js → chunk-2Q5K7J3B-C7dcVQSj.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-DaklJ-6i.js → chunk-32BRIVSS-DQQ_NOog.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BN_wg2pr.js → chunk-5VM5RSS4-BRR5_oWJ.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DSFFLUoL.js → chunk-EX3LRPZG-C0Jg_k6w.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-DB4dAjza.js → chunk-JWPE2WC7-CoHkckoP.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-CRM5eGQS.js → chunk-MOJQB5TN-CTIj5Opw.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-BFlkjazU.js → chunk-RYQCIY6F-BsP4OCo4.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-Bnfh4AAv.js → chunk-V7JOEXUC-B5KS36_O.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-CQc4_qVM.js → chunk-VR4S4FIN-Dm92_7ut.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-CZivyoDm.js → chunk-XXDRQBXY-BGivbXmv.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C12RKvcF.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C12RKvcF.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-BUibhTD-.js → cose-bilkent-JH36ORCC-DVjdL84y.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CRkjg5zF.js → cynefin-VYW2F7L2-pcKhk42O.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BoeQn4Um.js → cynefinDiagram-TSTJHNR4-CdHQXyCH.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DGo7-PY5.js → dagre-VKFMJZFB-Okt50Noy.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-DMXd2bdO.js → diagram-FQU43EPY-D52R5ow3.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CkYHblMZ.js → diagram-G47NLZAW-KiJTa2OF.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-C9iwEe4T.js → diagram-NH7WQ7WH-DZ-NFYLH.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-DpYLrfwb.js → diagram-OA4YK3LP-et-Lf7yH.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-BvrBukHb.js → diagram-WEI45ONY-eXhZ7Ksl.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DGl1r3zR.js → ebnfDiagram-CCIWWBDH-CTWYusI0.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-CoP5DBSb.js → erDiagram-Q63AITRT-DwTN2EqF.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-DVHtyYyr.js → flowDiagram-23GEKE2U-CmxOIpNk.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-_JyR78yC.js → ganttDiagram-NO4QXBWP-TsPnDFPN.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-Bj7sh0Db.js → gitGraphDiagram-IHSO6WYX-B_IVM0u8.js} +1 -1
- package/web/dist/assets/{index-CB6NfSZb.js → index-CB16o5aO.js} +2 -2
- package/web/dist/assets/index-CN_7NnbR.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-BrvMeRmA.js → infoDiagram-FWYZ7A6U-BFjk6921.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-lNDujpEC.js → ishikawaDiagram-FXEZZL3T-BiTzJYKW.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-yoD2QRni.js → journeyDiagram-5HDEW3XC-eeyGUQSj.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-DtyMlypg.js → kanban-definition-HUTT4EX6-DPYZ3frK.js} +1 -1
- package/web/dist/assets/{linear-DXPbXF9v.js → linear-Buu9IOJQ.js} +1 -1
- package/web/dist/assets/{mermaid.core-DCEXqVUL.js → mermaid.core-xO1oSQS3.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-DpKHDJFg.js → mindmap-definition-LN4V7U3C-TasB4KZ4.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-mxFI_8qh.js → pegDiagram-2B236MQR-Btr2u_Xy.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-sUWLrwMG.js → pieDiagram-ENE6RG2P-0iBpCJcu.js} +1 -1
- package/web/dist/assets/pilotswarm-v4IdVngK.js +93 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-Cph1z7ye.js → quadrantDiagram-ABIIQ3AL-CbVQOzUs.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-BPKkt2va.js → railroadDiagram-RFXS5EU6-BkwzDhMx.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BB6DNHCy.js → requirementDiagram-TGXJPOKE-DpF9yvmD.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-BASvPQk2.js → sankeyDiagram-HTMAVEWB-CjvbElms.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DMrNas01.js → sequenceDiagram-DBY2YBRQ-DRj2K28H.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-Ddq5yeZZ.js → sizeCapture-X5ZJPWSS-Bi3gxz8F.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-DUb_IFST.js → stateDiagram-2N3HPSRC-_INdmyGU.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-A5HcuE43.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-Cs9JsGaf.js → swimlanes-5IMT3BWC-D1pFtL4Y.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Bf_Rn9Zk.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-xuNF5fTm.js → timeline-definition-FHXFAJF6-PYQ9uRSp.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-fsI1GFLy.js → vennDiagram-L72KCM5P-xCF6u31z.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-DEc-kumC.js → wardleyDiagram-EHGQE667-DPcm9Msn.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-BCo6Qrfu.js → xychartDiagram-FW5EYKEG-D9ZNU0hG.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/channel-DhhWyKRW.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-CQBxGpio.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CQBxGpio.js +0 -1
- package/web/dist/assets/index-SmXlZ1eu.css +0 -1
- package/web/dist/assets/pilotswarm-CdEkcq2V.js +0 -91
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C3nsHf5u.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-C2xZA5V6.js +0 -8
package/ui/core/src/selectors.js
CHANGED
|
@@ -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
|
+
[/</g, "<"],
|
|
335
|
+
[/>/g, ">"],
|
|
336
|
+
[/"/g, '"'],
|
|
337
|
+
[/�?39;/g, "'"],
|
|
338
|
+
[/'/g, "'"],
|
|
339
|
+
[/ /g, " "],
|
|
340
|
+
// Ampersand LAST: decoding it first would let "&lt;" collapse to "<".
|
|
341
|
+
[/&/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 [];
|
|
@@ -2904,6 +2954,40 @@ export function selectFileSessionIdsForScope(state, scope = selectFilesScope(sta
|
|
|
2904
2954
|
return ordered;
|
|
2905
2955
|
}
|
|
2906
2956
|
|
|
2957
|
+
/**
|
|
2958
|
+
* Artifacts in the order they appear in the CONVERSATION.
|
|
2959
|
+
*
|
|
2960
|
+
* The Files list is sorted for browsing (alphabetical); the transcript order is
|
|
2961
|
+
* the order the work actually happened in. Stepping through a preview opened
|
|
2962
|
+
* from a chat link should follow the conversation, not the filing cabinet — so
|
|
2963
|
+
* these are two distinct sequences and neither can be derived from the other.
|
|
2964
|
+
*
|
|
2965
|
+
* Ids are `<sessionId>/<filename>`, matching files.selectedArtifactId.
|
|
2966
|
+
*/
|
|
2967
|
+
export function selectChatOrderedArtifactIds(state) {
|
|
2968
|
+
const session = selectActiveSession(state);
|
|
2969
|
+
const sessionId = session?.sessionId;
|
|
2970
|
+
if (!sessionId) return [];
|
|
2971
|
+
const history = state.history?.bySessionId?.get?.(sessionId);
|
|
2972
|
+
const events = history?.events || [];
|
|
2973
|
+
const seen = new Set();
|
|
2974
|
+
const ordered = [];
|
|
2975
|
+
const re = /artifact:\/\/([a-f0-9-]+)\/([^\s"'{})\]]+)/g;
|
|
2976
|
+
for (const event of events) {
|
|
2977
|
+
const text = typeof event?.data?.content === "string" ? event.data.content : "";
|
|
2978
|
+
if (!text) continue;
|
|
2979
|
+
re.lastIndex = 0;
|
|
2980
|
+
let m;
|
|
2981
|
+
while ((m = re.exec(text)) !== null) {
|
|
2982
|
+
const id = `${m[1]}/${m[2]}`;
|
|
2983
|
+
if (seen.has(id)) continue;
|
|
2984
|
+
seen.add(id);
|
|
2985
|
+
ordered.push(id);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return ordered;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2907
2991
|
export function selectFileBrowserItems(state) {
|
|
2908
2992
|
const scope = selectFilesScope(state);
|
|
2909
2993
|
const activeSessionId = selectActiveSession(state)?.sessionId || null;
|
|
@@ -3656,7 +3740,9 @@ function isSequenceOrchestratorType(type) {
|
|
|
3656
3740
|
}
|
|
3657
3741
|
|
|
3658
3742
|
function mapEventToSequenceEntry(event) {
|
|
3659
|
-
|
|
3743
|
+
// Compact: the sequence TIME column is fixed-width, and the full form
|
|
3744
|
+
// truncates any dated row to "26 Jul …", losing the time entirely.
|
|
3745
|
+
const time = formatTimestampCompact(event?.createdAt);
|
|
3660
3746
|
const nodeLabel = shortNodeLabel(event?.workerNodeId);
|
|
3661
3747
|
const detailText = eventMessageText(event);
|
|
3662
3748
|
const preview = summarizeEventPreview(detailText, 20);
|
|
@@ -3849,7 +3935,7 @@ function buildSessionStatusSequenceEntry(session) {
|
|
|
3849
3935
|
const safeCreatedAtMs = Number.isFinite(createdAtMs) ? createdAtMs : Date.now();
|
|
3850
3936
|
|
|
3851
3937
|
return {
|
|
3852
|
-
time:
|
|
3938
|
+
time: formatTimestampCompact(safeCreatedAtMs),
|
|
3853
3939
|
createdAtMs: safeCreatedAtMs,
|
|
3854
3940
|
nodeLabel: "orch",
|
|
3855
3941
|
color: errorKind === "failed" ? "red" : "yellow",
|
|
@@ -3908,6 +3994,39 @@ function buildNodeMapNodeUnionForWindow(state, window) {
|
|
|
3908
3994
|
return Array.from(labels).sort((left, right) => left.localeCompare(right));
|
|
3909
3995
|
}
|
|
3910
3996
|
|
|
3997
|
+
/**
|
|
3998
|
+
* Strip trailing pad from the last run of a sequence line.
|
|
3999
|
+
*
|
|
4000
|
+
* Every column is padded to colWidth for alignment, but the FINAL column's
|
|
4001
|
+
* trailing spaces align nothing — there is no column after them. With
|
|
4002
|
+
* `white-space: pre` those spaces occupy real width, so every line rendered at
|
|
4003
|
+
* exactly the computed width and the pane had zero tolerance for rounding
|
|
4004
|
+
* between "columns x char width" and its actual pixel width. The result was a
|
|
4005
|
+
* horizontal scrollbar that never went away, on content that visibly fit.
|
|
4006
|
+
*/
|
|
4007
|
+
function trimTrailingRunPad(runs) {
|
|
4008
|
+
const out = runs.slice();
|
|
4009
|
+
while (out.length > 0) {
|
|
4010
|
+
const last = out[out.length - 1];
|
|
4011
|
+
const trimmed = String(last.text ?? "").replace(/\s+$/, "");
|
|
4012
|
+
if (trimmed === "") { out.pop(); continue; }
|
|
4013
|
+
out[out.length - 1] = { ...last, text: trimmed };
|
|
4014
|
+
break;
|
|
4015
|
+
}
|
|
4016
|
+
return out;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
/** Widest rendered line, so decoration never outruns real content. */
|
|
4020
|
+
function sequenceLineWidth(line) {
|
|
4021
|
+
if (Array.isArray(line?.runs)) {
|
|
4022
|
+
return line.runs.reduce((n, r) => n + displayLength(String(r.text ?? "")), 0);
|
|
4023
|
+
}
|
|
4024
|
+
if (Array.isArray(line)) {
|
|
4025
|
+
return line.reduce((n, r) => n + displayLength(String(r.text ?? "")), 0);
|
|
4026
|
+
}
|
|
4027
|
+
return displayLength(String(line?.text ?? ""));
|
|
4028
|
+
}
|
|
4029
|
+
|
|
3911
4030
|
function buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth) {
|
|
3912
4031
|
const runs = [
|
|
3913
4032
|
{ text: padDisplayText("TIME", timeWidth), color: "white", bold: true },
|
|
@@ -3917,12 +4036,15 @@ function buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth) {
|
|
|
3917
4036
|
if (index > 0) runs.push({ text: " ", color: null });
|
|
3918
4037
|
runs.push({ text: padDisplayText(nodeLabel, colWidth), color: "white", bold: true });
|
|
3919
4038
|
});
|
|
3920
|
-
return runs;
|
|
4039
|
+
return trimTrailingRunPad(runs);
|
|
3921
4040
|
}
|
|
3922
4041
|
|
|
3923
|
-
function buildSequenceDividerLine(nodeLabels, timeWidth, colWidth) {
|
|
4042
|
+
function buildSequenceDividerLine(nodeLabels, timeWidth, colWidth, contentWidth = Infinity) {
|
|
4043
|
+
const full = `${"-".repeat(timeWidth)} ${nodeLabels.map(() => "─".repeat(colWidth)).join(" ")}`;
|
|
4044
|
+
// Clip to the widest real line: a full-width rule under short content is
|
|
4045
|
+
// itself enough to force the scrollbar this trimming exists to remove.
|
|
3924
4046
|
return plainInspectorLine(
|
|
3925
|
-
|
|
4047
|
+
Number.isFinite(contentWidth) ? full.slice(0, Math.max(timeWidth, contentWidth)) : full,
|
|
3926
4048
|
"gray",
|
|
3927
4049
|
);
|
|
3928
4050
|
}
|
|
@@ -4011,7 +4133,7 @@ function buildSequenceEventLine(entry, nodeLabels, timeWidth, colWidth) {
|
|
|
4011
4133
|
}
|
|
4012
4134
|
});
|
|
4013
4135
|
|
|
4014
|
-
return runs;
|
|
4136
|
+
return trimTrailingRunPad(runs);
|
|
4015
4137
|
}
|
|
4016
4138
|
|
|
4017
4139
|
function buildNodeMapHeaderLine(nodeLabels, colWidth) {
|
|
@@ -4117,7 +4239,13 @@ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
|
|
|
4117
4239
|
.map((entry) => entry.nodeLabel)
|
|
4118
4240
|
.filter((nodeLabel) => nodeLabel && nodeLabel !== "orch"))).sort((left, right) => left.localeCompare(right));
|
|
4119
4241
|
const uniqueNodes = unionNodes.length > 0 ? unionNodes : activeSessionNodes;
|
|
4120
|
-
|
|
4242
|
+
// Size the TIME column to what is actually in it. A fixed 8 fits "20:10:19"
|
|
4243
|
+
// but silently truncates any dated row, which is precisely the row where
|
|
4244
|
+
// the date matters. Capped so one odd stamp cannot starve the lanes.
|
|
4245
|
+
const timeWidth = Math.min(13, Math.max(
|
|
4246
|
+
8,
|
|
4247
|
+
...visibleEntries.map((entry) => displayLength(String(entry.time || ""))),
|
|
4248
|
+
));
|
|
4121
4249
|
const availableWidth = Math.max(18, maxWidth);
|
|
4122
4250
|
const maxNodes = Math.max(1, Math.floor((availableWidth - timeWidth - 1) / 6));
|
|
4123
4251
|
let nodeLabels = ["orch", ...uniqueNodes];
|
|
@@ -4158,12 +4286,21 @@ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
|
|
|
4158
4286
|
}
|
|
4159
4287
|
}
|
|
4160
4288
|
|
|
4289
|
+
// Decoration is sized to real content, never to the pane's nominal width:
|
|
4290
|
+
// a full-width rule under short content forces the very scrollbar the
|
|
4291
|
+
// trimming above exists to remove.
|
|
4292
|
+
const headerLine = buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth);
|
|
4293
|
+
const contentWidth = lines.reduce(
|
|
4294
|
+
(widest, line) => Math.max(widest, sequenceLineWidth(line)),
|
|
4295
|
+
sequenceLineWidth(headerLine),
|
|
4296
|
+
);
|
|
4297
|
+
|
|
4161
4298
|
return {
|
|
4162
4299
|
stickyLines: [
|
|
4163
4300
|
...statsLines,
|
|
4164
4301
|
plainInspectorLine(`Window: ${recentWindow.label}`, "gray"),
|
|
4165
|
-
|
|
4166
|
-
buildSequenceDividerLine(nodeLabels, timeWidth, colWidth),
|
|
4302
|
+
headerLine,
|
|
4303
|
+
buildSequenceDividerLine(nodeLabels, timeWidth, colWidth, contentWidth),
|
|
4167
4304
|
],
|
|
4168
4305
|
lines,
|
|
4169
4306
|
};
|
package/ui/core/src/state.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|