pilotswarm 0.5.22 → 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 +2 -1
- package/tui/src/node-sdk-transport.js +22 -5
- package/ui/core/src/controller.js +329 -6
- package/ui/core/src/formatting.js +46 -1
- package/ui/core/src/history.js +29 -0
- package/ui/core/src/reducer.js +52 -2
- package/ui/core/src/selectors.js +288 -11
- package/ui/core/src/state.js +12 -1
- 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 +1342 -84
- package/web/dist/assets/abnfDiagram-VRR7QNED-Bjz_3uNq.js +1 -0
- package/web/dist/assets/arc-a3km8Cfi.js +1 -0
- package/web/dist/assets/architectureDiagram-ZJ3FMSHR-4twPAKdk.js +36 -0
- package/web/dist/assets/blockDiagram-677ZJIJ3-FnFRjWxr.js +132 -0
- package/web/dist/assets/c4Diagram-LMCZKHZV-B8R4iXqh.js +10 -0
- package/web/dist/assets/channel-BQwFhqIW.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-C7dcVQSj.js +1 -0
- package/web/dist/assets/chunk-32BRIVSS-DQQ_NOog.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-BRR5_oWJ.js +15 -0
- package/web/dist/assets/chunk-EX3LRPZG-C0Jg_k6w.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-CoHkckoP.js +1 -0
- package/web/dist/assets/chunk-MOJQB5TN-CTIj5Opw.js +88 -0
- package/web/dist/assets/chunk-RYQCIY6F-BsP4OCo4.js +1 -0
- package/web/dist/assets/chunk-V7JOEXUC-B5KS36_O.js +206 -0
- package/web/dist/assets/chunk-VR4S4FIN-Dm92_7ut.js +1 -0
- package/web/dist/assets/chunk-XXDRQBXY-BGivbXmv.js +1 -0
- 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-DVjdL84y.js +1 -0
- package/web/dist/assets/cynefin-VYW2F7L2-pcKhk42O.js +166 -0
- package/web/dist/assets/cynefinDiagram-TSTJHNR4-CdHQXyCH.js +62 -0
- package/web/dist/assets/cytoscape.esm-D3_iZ_3b.js +321 -0
- package/web/dist/assets/dagre-VKFMJZFB-Okt50Noy.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-FQU43EPY-D52R5ow3.js +3 -0
- package/web/dist/assets/diagram-G47NLZAW-KiJTa2OF.js +24 -0
- package/web/dist/assets/diagram-NH7WQ7WH-DZ-NFYLH.js +24 -0
- package/web/dist/assets/diagram-OA4YK3LP-et-Lf7yH.js +30 -0
- package/web/dist/assets/diagram-WEI45ONY-eXhZ7Ksl.js +41 -0
- package/web/dist/assets/ebnfDiagram-CCIWWBDH-CTWYusI0.js +1 -0
- package/web/dist/assets/erDiagram-Q63AITRT-DwTN2EqF.js +85 -0
- package/web/dist/assets/flowDiagram-23GEKE2U-CmxOIpNk.js +156 -0
- package/web/dist/assets/ganttDiagram-NO4QXBWP-TsPnDFPN.js +292 -0
- package/web/dist/assets/gitGraphDiagram-IHSO6WYX-B_IVM0u8.js +106 -0
- package/web/dist/assets/graph-DOmOIIwC.js +1 -0
- package/web/dist/assets/{index-CuxEGti7.js → index-CB16o5aO.js} +3 -3
- package/web/dist/assets/index-CN_7NnbR.css +1 -0
- package/web/dist/assets/infoDiagram-FWYZ7A6U-BFjk6921.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-FXEZZL3T-BiTzJYKW.js +70 -0
- package/web/dist/assets/journeyDiagram-5HDEW3XC-eeyGUQSj.js +139 -0
- package/web/dist/assets/kanban-definition-HUTT4EX6-DPYZ3frK.js +89 -0
- package/web/dist/assets/katex-HP8lGamR.js +257 -0
- package/web/dist/assets/layout-D-LzfAck.js +1 -0
- package/web/dist/assets/linear-Buu9IOJQ.js +1 -0
- package/web/dist/assets/map-DxJ2ADlA.js +1 -0
- package/web/dist/assets/mermaid.core-xO1oSQS3.js +308 -0
- package/web/dist/assets/mindmap-definition-LN4V7U3C-TasB4KZ4.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-2B236MQR-Btr2u_Xy.js +1 -0
- package/web/dist/assets/pieDiagram-ENE6RG2P-0iBpCJcu.js +39 -0
- package/web/dist/assets/pilotswarm-v4IdVngK.js +93 -0
- package/web/dist/assets/quadrantDiagram-ABIIQ3AL-CbVQOzUs.js +7 -0
- package/web/dist/assets/railroadDiagram-RFXS5EU6-BkwzDhMx.js +1 -0
- package/web/dist/assets/requirementDiagram-TGXJPOKE-DpF9yvmD.js +84 -0
- package/web/dist/assets/sankeyDiagram-HTMAVEWB-CjvbElms.js +40 -0
- package/web/dist/assets/sequenceDiagram-DBY2YBRQ-DRj2K28H.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-Bi3gxz8F.js +1 -0
- package/web/dist/assets/stateDiagram-2N3HPSRC-_INdmyGU.js +1 -0
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-A5HcuE43.js +1 -0
- package/web/dist/assets/swimlanes-5IMT3BWC-D1pFtL4Y.js +2 -0
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Bf_Rn9Zk.js +8 -0
- package/web/dist/assets/timeline-definition-FHXFAJF6-PYQ9uRSp.js +120 -0
- package/web/dist/assets/vennDiagram-L72KCM5P-xCF6u31z.js +34 -0
- package/web/dist/assets/wardleyDiagram-EHGQE667-DPcm9Msn.js +78 -0
- package/web/dist/assets/xychartDiagram-FW5EYKEG-D9ZNU0hG.js +7 -0
- package/web/dist/index.html +3 -3
- package/web/dist/assets/index-CxkLM8_a.css +0 -1
- package/web/dist/assets/pilotswarm-CjMx44so.js +0 -90
package/ui/core/src/reducer.js
CHANGED
|
@@ -637,7 +637,8 @@ export function appReducer(state, action) {
|
|
|
637
637
|
const hasOwnerFilter = Object.prototype.hasOwnProperty.call(settings, "sessionOwnerFilter");
|
|
638
638
|
const hasLayout = Object.prototype.hasOwnProperty.call(settings, "layoutAdjustments");
|
|
639
639
|
const hasChatViewMode = Object.prototype.hasOwnProperty.call(settings, "chatViewMode")
|
|
640
|
-
&& (settings.chatViewMode === "summary" || settings.chatViewMode === "transcript"
|
|
640
|
+
&& (settings.chatViewMode === "summary" || settings.chatViewMode === "transcript"
|
|
641
|
+
|| settings.chatViewMode === "rich");
|
|
641
642
|
const hasPins = Object.prototype.hasOwnProperty.call(settings, "pinnedSessionIds");
|
|
642
643
|
const hasCollapsed = Object.prototype.hasOwnProperty.call(settings, "collapsedSessionIds");
|
|
643
644
|
// A pending/resolved deep-link intent outranks the profile's
|
|
@@ -701,7 +702,7 @@ export function appReducer(state, action) {
|
|
|
701
702
|
}
|
|
702
703
|
|
|
703
704
|
case "ui/chatViewMode":
|
|
704
|
-
if (action.mode !== "summary" && action.mode !== "transcript") {
|
|
705
|
+
if (action.mode !== "summary" && action.mode !== "transcript" && action.mode !== "rich") {
|
|
705
706
|
return state;
|
|
706
707
|
}
|
|
707
708
|
return {
|
|
@@ -1921,6 +1922,8 @@ export function appReducer(state, action) {
|
|
|
1921
1922
|
downloads: {},
|
|
1922
1923
|
};
|
|
1923
1924
|
const entries = normalizeArtifactEntries(action.entries);
|
|
1925
|
+
// Stamped by the caller, never Date.now() here — reducers stay pure.
|
|
1926
|
+
const fetchedAt = Number(action.fetchedAt) || 0;
|
|
1924
1927
|
const hasFilename = (filename) => entries.some((entry) => entry.filename === filename);
|
|
1925
1928
|
const selectedFilename = current.selectedFilename && hasFilename(current.selectedFilename)
|
|
1926
1929
|
? current.selectedFilename
|
|
@@ -1933,6 +1936,10 @@ export function appReducer(state, action) {
|
|
|
1933
1936
|
selectedFilename,
|
|
1934
1937
|
loading: false,
|
|
1935
1938
|
loaded: true,
|
|
1939
|
+
// Stored, not just computed — without this the TTL guard read
|
|
1940
|
+
// undefined, treated every list as ancient, and refetched on
|
|
1941
|
+
// every poll cycle. That was the ~3s flicker.
|
|
1942
|
+
fetchedAt,
|
|
1936
1943
|
error: null,
|
|
1937
1944
|
};
|
|
1938
1945
|
return {
|
|
@@ -1998,6 +2005,39 @@ export function appReducer(state, action) {
|
|
|
1998
2005
|
};
|
|
1999
2006
|
}
|
|
2000
2007
|
|
|
2008
|
+
case "files/toggleMark": {
|
|
2009
|
+
const id = String(action.artifactId || "");
|
|
2010
|
+
if (!id) return state;
|
|
2011
|
+
const marked = new Set(state.files.markedIds || []);
|
|
2012
|
+
if (marked.has(id)) marked.delete(id);
|
|
2013
|
+
else marked.add(id);
|
|
2014
|
+
return { ...state, files: { ...state.files, markedIds: [...marked] } };
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
case "files/setMarks":
|
|
2018
|
+
return {
|
|
2019
|
+
...state,
|
|
2020
|
+
files: {
|
|
2021
|
+
...state.files,
|
|
2022
|
+
markedIds: Array.isArray(action.artifactIds) ? [...new Set(action.artifactIds.map(String))] : [],
|
|
2023
|
+
},
|
|
2024
|
+
};
|
|
2025
|
+
|
|
2026
|
+
case "files/previewOrigin":
|
|
2027
|
+
return {
|
|
2028
|
+
...state,
|
|
2029
|
+
files: {
|
|
2030
|
+
...state.files,
|
|
2031
|
+
previewOrigin: action.origin || null,
|
|
2032
|
+
restoreArtifactId: action.origin === "chat"
|
|
2033
|
+
? (action.restoreArtifactId ?? state.files.restoreArtifactId ?? null)
|
|
2034
|
+
: null,
|
|
2035
|
+
},
|
|
2036
|
+
};
|
|
2037
|
+
|
|
2038
|
+
case "files/clearMarks":
|
|
2039
|
+
return { ...state, files: { ...state.files, markedIds: [] } };
|
|
2040
|
+
|
|
2001
2041
|
case "files/selectGlobal":
|
|
2002
2042
|
return {
|
|
2003
2043
|
...state,
|
|
@@ -2136,6 +2176,16 @@ export function appReducer(state, action) {
|
|
|
2136
2176
|
}
|
|
2137
2177
|
|
|
2138
2178
|
case "files/deleted": {
|
|
2179
|
+
// Fall through to the existing handling, but drop the deleted id
|
|
2180
|
+
// from the marks first so a bulk delete cannot leave phantoms.
|
|
2181
|
+
state = {
|
|
2182
|
+
...state,
|
|
2183
|
+
files: {
|
|
2184
|
+
...state.files,
|
|
2185
|
+
markedIds: (state.files.markedIds || [])
|
|
2186
|
+
.filter((id) => id !== `${action.sessionId}/${action.filename}`),
|
|
2187
|
+
},
|
|
2188
|
+
};
|
|
2139
2189
|
const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
|
|
2140
2190
|
const current = bySessionId[action.sessionId] || {
|
|
2141
2191
|
entries: [],
|
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,
|
|
@@ -229,7 +230,15 @@ function shouldDecorateSessionOwners(state) {
|
|
|
229
230
|
// distinct human owner — otherwise the owner chip is pure noise on every
|
|
230
231
|
// row (you are always "you"). A narrowed owner filter is an explicit
|
|
231
232
|
// multi-user context, so honor that too.
|
|
232
|
-
|
|
233
|
+
// A narrowed filter counts as an explicit multi-user context ONLY when it
|
|
234
|
+
// names specific owners. The DEFAULT signed-in filter already sets
|
|
235
|
+
// `all: false` AND `includeShared: true` (see
|
|
236
|
+
// defaultOwnerFilterForPrincipal), so testing either of those tagged every
|
|
237
|
+
// row in a single-user deployment with a pointless "you are you" chip.
|
|
238
|
+
// Everything else falls through to counting DISTINCT owners below, which
|
|
239
|
+
// is what the chip actually communicates.
|
|
240
|
+
const ownerFilter = state.sessions?.ownerFilter;
|
|
241
|
+
if (Array.isArray(ownerFilter?.ownerKeys) && ownerFilter.ownerKeys.length > 0) return true;
|
|
233
242
|
const owners = new Set();
|
|
234
243
|
for (const session of Object.values(state.sessions?.byId || {})) {
|
|
235
244
|
if (!session || session.isSystem || session.isGroup) continue;
|
|
@@ -316,7 +325,38 @@ function splitTypedSessionTitle(displayTitle) {
|
|
|
316
325
|
return { userTitle, typeTitle };
|
|
317
326
|
}
|
|
318
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
|
+
|
|
319
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) {
|
|
320
360
|
const title = String(session?.title || "").trim();
|
|
321
361
|
const splitTitle = splitAgentPrefixedSessionTitle(session);
|
|
322
362
|
if (!splitTitle) return title;
|
|
@@ -731,6 +771,37 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
731
771
|
badgeRuns: [],
|
|
732
772
|
selectedMetaRuns: detailRuns,
|
|
733
773
|
detailRuns,
|
|
774
|
+
// Structured row description for renderers that draw real chrome
|
|
775
|
+
// instead of terminal runs (the portal's rich session list): status
|
|
776
|
+
// becomes a dot, depth becomes indentation, the owner/child/cron
|
|
777
|
+
// badges become chips. Purely additive — `runs`/`titleRuns` above
|
|
778
|
+
// stay the terminal's source of truth and the TUI ignores this.
|
|
779
|
+
chrome: {
|
|
780
|
+
kind: session?.isGroup ? "group"
|
|
781
|
+
: session?.isSystem ? "system"
|
|
782
|
+
: session?.serviceKind ? "service"
|
|
783
|
+
: "session",
|
|
784
|
+
title: hasRealTitle ? rawTitle : shortId,
|
|
785
|
+
untitled: !hasRealTitle,
|
|
786
|
+
statusColor: sessionStatusColor(session, mode),
|
|
787
|
+
accentColor: mainColor,
|
|
788
|
+
pinned: isPinned,
|
|
789
|
+
selectMode,
|
|
790
|
+
checked: isSelected,
|
|
791
|
+
owner: shouldDecorateSessionOwners(state) && !session?.isSystem && !session?.isGroup
|
|
792
|
+
? {
|
|
793
|
+
initials: effectiveOwner ? ownerInitials(effectiveOwner) : "?",
|
|
794
|
+
isMine: Boolean(ownerKeyForOwner(state?.auth?.principal)
|
|
795
|
+
&& ownerKeyForOwner(effectiveOwner) === ownerKeyForOwner(state?.auth?.principal)),
|
|
796
|
+
}
|
|
797
|
+
: null,
|
|
798
|
+
childBadge: collapseBadge ? { text: collapseBadge.text, color: collapseBadge.color } : null,
|
|
799
|
+
cron: Boolean(cronBadge),
|
|
800
|
+
age: relTime || null,
|
|
801
|
+
model: modelLabel || null,
|
|
802
|
+
shortId,
|
|
803
|
+
ctx: ctxRuns.length > 0 ? { text: ctxRuns[0].text, color: ctxRuns[0].color } : null,
|
|
804
|
+
},
|
|
734
805
|
};
|
|
735
806
|
}
|
|
736
807
|
|
|
@@ -1374,7 +1445,12 @@ function prefixRuns(text, color = "gray", options = {}) {
|
|
|
1374
1445
|
}];
|
|
1375
1446
|
}
|
|
1376
1447
|
|
|
1377
|
-
|
|
1448
|
+
// Header facts for one chat message — speaker label, label color, delivery
|
|
1449
|
+
// glyph, and timestamp — extracted from buildChatMessagePrefix so the rich
|
|
1450
|
+
// (block) chat renderer can consume the same labeling rules without the
|
|
1451
|
+
// terminal run/prefix shape. Returns { time, glyph, glyphColor, roleLabel,
|
|
1452
|
+
// roleColor }.
|
|
1453
|
+
function describeChatMessageHeader(message, options = {}) {
|
|
1378
1454
|
const time = formatTimestamp(message?.createdAt || message?.time);
|
|
1379
1455
|
// A user.message is labeled from the CURRENT VIEWER's perspective: "You"
|
|
1380
1456
|
// for the viewer's own messages, the sender's name for anyone else. When
|
|
@@ -1459,6 +1535,15 @@ function buildChatMessagePrefix(message, options = {}) {
|
|
|
1459
1535
|
: message?.role === "system"
|
|
1460
1536
|
? "yellow"
|
|
1461
1537
|
: "white";
|
|
1538
|
+
// `fromOtherPerson` lets renderers drop the speaker label for the
|
|
1539
|
+
// viewer's own messages and the agent's (a 1:1 chat needs no "You:" /
|
|
1540
|
+
// "Agent:" on every turn) while still naming OTHER humans in a shared
|
|
1541
|
+
// session, where attribution is the whole point.
|
|
1542
|
+
return { time, glyph, glyphColor, roleLabel, roleColor, fromOtherPerson };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function buildChatMessagePrefix(message, options = {}) {
|
|
1546
|
+
const { time, glyph, glyphColor, roleLabel, roleColor } = describeChatMessageHeader(message, options);
|
|
1462
1547
|
const prefix = time ? `[${time}] ` : "";
|
|
1463
1548
|
const runs = [...prefixRuns(prefix, "gray")];
|
|
1464
1549
|
if (glyph) {
|
|
@@ -2303,6 +2388,8 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
|
2303
2388
|
lines.push(buildEpochDividerLine(message, maxWidth));
|
|
2304
2389
|
} else if (message?.kind === "regen-refused") {
|
|
2305
2390
|
lines.push(buildRegenRefusedLine(message, maxWidth));
|
|
2391
|
+
} else if (message?.kind === "regen-failed") {
|
|
2392
|
+
lines.push(buildRegenFailedLine(message, maxWidth));
|
|
2306
2393
|
} else {
|
|
2307
2394
|
const messageLines = buildChatMessageLines(message, maxWidth, buildOptions);
|
|
2308
2395
|
appendChatBlockLines(lines, messageLines);
|
|
@@ -2319,6 +2406,97 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
|
2319
2406
|
return lines.length > 0 ? lines : [{ text: "No messages yet.", color: "gray" }];
|
|
2320
2407
|
}
|
|
2321
2408
|
|
|
2409
|
+
// True when a chat message is plain user/assistant prose the rich (block)
|
|
2410
|
+
// renderer can format as markdown directly. Everything with terminal-era
|
|
2411
|
+
// special handling — splash art, thinking cards, chrome-less summaries,
|
|
2412
|
+
// system/PilotSwarm cards, asked-and-answered exchanges, embedded
|
|
2413
|
+
// [SYSTEM: …] notice segments — stays on the line renderer, which already
|
|
2414
|
+
// knows every edge case.
|
|
2415
|
+
function isRichRenderableChatMessage(message) {
|
|
2416
|
+
if (!message) return false;
|
|
2417
|
+
if (message.kind === "epoch-divider" || message.kind === "regen-refused" || message.kind === "regen-failed") return false;
|
|
2418
|
+
if (message.splash || message.thinking || message.noChrome) return false;
|
|
2419
|
+
if (message.role !== "user" && message.role !== "assistant") return false;
|
|
2420
|
+
if (message.role === "user" && parseAskedAndAnsweredExchange(message?.text || "")) return false;
|
|
2421
|
+
const segments = splitSystemNoticeSegments(message?.text || "");
|
|
2422
|
+
if (segments.some((segment) => segment.kind === "system")) return false;
|
|
2423
|
+
return true;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/**
|
|
2427
|
+
* Message-level view of the active chat for the rich (desktop-style) web
|
|
2428
|
+
* renderer. Returns an ordered array of blocks:
|
|
2429
|
+
*
|
|
2430
|
+
* { kind: "message", id, role, header, text, pendingPhase, attachments }
|
|
2431
|
+
* — plain user/assistant prose. `header` is describeChatMessageHeader()
|
|
2432
|
+
* output; `text` is the artifact-link-decorated markdown source the
|
|
2433
|
+
* renderer formats itself; `attachments` is { sessionId, attachments }
|
|
2434
|
+
* (the sentinel shape) or null.
|
|
2435
|
+
*
|
|
2436
|
+
* { kind: "lines", variant, lines }
|
|
2437
|
+
* — everything else (splash, thinking cards, system cards, dividers),
|
|
2438
|
+
* pre-rendered through the exact same line builders the terminal
|
|
2439
|
+
* transcript uses, so no edge case has two implementations.
|
|
2440
|
+
*
|
|
2441
|
+
* The TUI never calls this; selectChatLines remains the terminal path.
|
|
2442
|
+
*/
|
|
2443
|
+
export function selectChatBlocks(state, maxWidth = 80, options = {}) {
|
|
2444
|
+
const messages = selectActiveChat(state);
|
|
2445
|
+
if (!messages || messages.length === 0) return [];
|
|
2446
|
+
|
|
2447
|
+
const viewerKey = ownerKeyForOwner(state?.auth?.principal);
|
|
2448
|
+
const activeSessionId = state?.sessions?.activeSessionId;
|
|
2449
|
+
const activeSession = activeSessionId ? state?.sessions?.byId?.[activeSessionId] : null;
|
|
2450
|
+
const ownerKey = ownerKeyForOwner(activeSession?.owner);
|
|
2451
|
+
const sharedContext = Boolean(activeSession && (
|
|
2452
|
+
(activeSession.visibility && activeSession.visibility !== "private")
|
|
2453
|
+
|| (ownerKey && viewerKey && ownerKey !== viewerKey)
|
|
2454
|
+
));
|
|
2455
|
+
const buildOptions = {
|
|
2456
|
+
...(options?.tableMode ? { tableMode: options.tableMode } : {}),
|
|
2457
|
+
...(viewerKey ? { viewerKey } : {}),
|
|
2458
|
+
...(ownerKey ? { ownerKey } : {}),
|
|
2459
|
+
...(sharedContext ? { sharedContext: true } : {}),
|
|
2460
|
+
};
|
|
2461
|
+
|
|
2462
|
+
const blocks = [];
|
|
2463
|
+
for (const message of messages) {
|
|
2464
|
+
if (message?.kind === "epoch-divider") {
|
|
2465
|
+
blocks.push({ kind: "lines", variant: "divider", lines: [buildEpochDividerLine(message, maxWidth)] });
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (message?.kind === "regen-failed") {
|
|
2469
|
+
blocks.push({ kind: "lines", variant: "divider", lines: [buildRegenFailedLine(message, maxWidth)] });
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
2472
|
+
if (message?.kind === "regen-refused") {
|
|
2473
|
+
blocks.push({ kind: "lines", variant: "divider", lines: [buildRegenRefusedLine(message, maxWidth)] });
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
if (isRichRenderableChatMessage(message)) {
|
|
2477
|
+
const attachmentLines = buildAttachmentChipLines(message, { tableMode: "sentinel" });
|
|
2478
|
+
const attachmentSentinel = attachmentLines.find((line) => line?.kind === "imageAttachments") || null;
|
|
2479
|
+
blocks.push({
|
|
2480
|
+
kind: "message",
|
|
2481
|
+
id: message?.id ?? null,
|
|
2482
|
+
role: message.role,
|
|
2483
|
+
header: describeChatMessageHeader(message, buildOptions),
|
|
2484
|
+
text: decorateArtifactLinksForChat(message?.text || ""),
|
|
2485
|
+
pendingPhase: message?.pendingPhase || null,
|
|
2486
|
+
attachments: attachmentSentinel
|
|
2487
|
+
? { sessionId: attachmentSentinel.sessionId, attachments: attachmentSentinel.attachments }
|
|
2488
|
+
: null,
|
|
2489
|
+
});
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
const lines = buildChatMessageLines(message, maxWidth, buildOptions);
|
|
2493
|
+
if (lines.length > 0) {
|
|
2494
|
+
blocks.push({ kind: "lines", variant: "event", lines });
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return blocks;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2322
2500
|
// A centered inline rule ("──── label ────") for transcript markers. The dash
|
|
2323
2501
|
// runs are CAPPED (not stretched to maxWidth): the web portal wraps by pixel
|
|
2324
2502
|
// width, and a maxWidth-long "─" run overflows a narrower pane and wraps the
|
|
@@ -2368,6 +2546,18 @@ function buildRegenRefusedLine(message, maxWidth) {
|
|
|
2368
2546
|
return buildRuleLine(label, "yellow", maxWidth);
|
|
2369
2547
|
}
|
|
2370
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
|
+
|
|
2371
2561
|
export function selectOutboxOverlayLines(state, maxWidth = 80, options = {}) {
|
|
2372
2562
|
const messages = selectActiveOutboxMessages(state);
|
|
2373
2563
|
if (!messages || messages.length === 0) return [];
|
|
@@ -2764,6 +2954,40 @@ export function selectFileSessionIdsForScope(state, scope = selectFilesScope(sta
|
|
|
2764
2954
|
return ordered;
|
|
2765
2955
|
}
|
|
2766
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
|
+
|
|
2767
2991
|
export function selectFileBrowserItems(state) {
|
|
2768
2992
|
const scope = selectFilesScope(state);
|
|
2769
2993
|
const activeSessionId = selectActiveSession(state)?.sessionId || null;
|
|
@@ -3516,7 +3740,9 @@ function isSequenceOrchestratorType(type) {
|
|
|
3516
3740
|
}
|
|
3517
3741
|
|
|
3518
3742
|
function mapEventToSequenceEntry(event) {
|
|
3519
|
-
|
|
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);
|
|
3520
3746
|
const nodeLabel = shortNodeLabel(event?.workerNodeId);
|
|
3521
3747
|
const detailText = eventMessageText(event);
|
|
3522
3748
|
const preview = summarizeEventPreview(detailText, 20);
|
|
@@ -3709,7 +3935,7 @@ function buildSessionStatusSequenceEntry(session) {
|
|
|
3709
3935
|
const safeCreatedAtMs = Number.isFinite(createdAtMs) ? createdAtMs : Date.now();
|
|
3710
3936
|
|
|
3711
3937
|
return {
|
|
3712
|
-
time:
|
|
3938
|
+
time: formatTimestampCompact(safeCreatedAtMs),
|
|
3713
3939
|
createdAtMs: safeCreatedAtMs,
|
|
3714
3940
|
nodeLabel: "orch",
|
|
3715
3941
|
color: errorKind === "failed" ? "red" : "yellow",
|
|
@@ -3768,6 +3994,39 @@ function buildNodeMapNodeUnionForWindow(state, window) {
|
|
|
3768
3994
|
return Array.from(labels).sort((left, right) => left.localeCompare(right));
|
|
3769
3995
|
}
|
|
3770
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
|
+
|
|
3771
4030
|
function buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth) {
|
|
3772
4031
|
const runs = [
|
|
3773
4032
|
{ text: padDisplayText("TIME", timeWidth), color: "white", bold: true },
|
|
@@ -3777,12 +4036,15 @@ function buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth) {
|
|
|
3777
4036
|
if (index > 0) runs.push({ text: " ", color: null });
|
|
3778
4037
|
runs.push({ text: padDisplayText(nodeLabel, colWidth), color: "white", bold: true });
|
|
3779
4038
|
});
|
|
3780
|
-
return runs;
|
|
4039
|
+
return trimTrailingRunPad(runs);
|
|
3781
4040
|
}
|
|
3782
4041
|
|
|
3783
|
-
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.
|
|
3784
4046
|
return plainInspectorLine(
|
|
3785
|
-
|
|
4047
|
+
Number.isFinite(contentWidth) ? full.slice(0, Math.max(timeWidth, contentWidth)) : full,
|
|
3786
4048
|
"gray",
|
|
3787
4049
|
);
|
|
3788
4050
|
}
|
|
@@ -3871,7 +4133,7 @@ function buildSequenceEventLine(entry, nodeLabels, timeWidth, colWidth) {
|
|
|
3871
4133
|
}
|
|
3872
4134
|
});
|
|
3873
4135
|
|
|
3874
|
-
return runs;
|
|
4136
|
+
return trimTrailingRunPad(runs);
|
|
3875
4137
|
}
|
|
3876
4138
|
|
|
3877
4139
|
function buildNodeMapHeaderLine(nodeLabels, colWidth) {
|
|
@@ -3977,7 +4239,13 @@ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
|
|
|
3977
4239
|
.map((entry) => entry.nodeLabel)
|
|
3978
4240
|
.filter((nodeLabel) => nodeLabel && nodeLabel !== "orch"))).sort((left, right) => left.localeCompare(right));
|
|
3979
4241
|
const uniqueNodes = unionNodes.length > 0 ? unionNodes : activeSessionNodes;
|
|
3980
|
-
|
|
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
|
+
));
|
|
3981
4249
|
const availableWidth = Math.max(18, maxWidth);
|
|
3982
4250
|
const maxNodes = Math.max(1, Math.floor((availableWidth - timeWidth - 1) / 6));
|
|
3983
4251
|
let nodeLabels = ["orch", ...uniqueNodes];
|
|
@@ -4018,12 +4286,21 @@ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
|
|
|
4018
4286
|
}
|
|
4019
4287
|
}
|
|
4020
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
|
+
|
|
4021
4298
|
return {
|
|
4022
4299
|
stickyLines: [
|
|
4023
4300
|
...statsLines,
|
|
4024
4301
|
plainInspectorLine(`Window: ${recentWindow.label}`, "gray"),
|
|
4025
|
-
|
|
4026
|
-
buildSequenceDividerLine(nodeLabels, timeWidth, colWidth),
|
|
4302
|
+
headerLine,
|
|
4303
|
+
buildSequenceDividerLine(nodeLabels, timeWidth, colWidth, contentWidth),
|
|
4027
4304
|
],
|
|
4028
4305
|
lines,
|
|
4029
4306
|
};
|
package/ui/core/src/state.js
CHANGED
|
@@ -129,7 +129,7 @@ export function createInitialState({ mode = "local", branding = null, themeId =
|
|
|
129
129
|
inspectorTab: INSPECTOR_TABS[0],
|
|
130
130
|
sequenceExpandedTurns: [],
|
|
131
131
|
sequenceSelectedTurn: null,
|
|
132
|
-
chatViewMode: chatViewMode === "summary"
|
|
132
|
+
chatViewMode: (chatViewMode === "summary" || chatViewMode === "rich") ? chatViewMode : "transcript",
|
|
133
133
|
statsViewMode: "session",
|
|
134
134
|
prompt: "",
|
|
135
135
|
promptCursor: 0,
|
|
@@ -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;
|