pilotswarm 0.5.33 → 0.5.35
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 -2
- package/tui/src/app.js +10 -0
- package/tui/src/node-sdk-transport.js +11 -0
- package/ui/core/src/controller.js +533 -24
- package/ui/core/src/formatting.js +13 -1
- package/ui/core/src/history.js +8 -0
- package/ui/core/src/reducer.js +172 -28
- package/ui/core/src/selectors.js +160 -272
- package/ui/core/src/state.js +34 -1
- package/ui/core/src/themes/catppuccin-mocha.js +2 -1
- package/ui/core/src/themes/cobalt2.js +2 -1
- package/ui/core/src/themes/doom.js +2 -1
- package/ui/core/src/themes/dracula.js +2 -1
- package/ui/core/src/themes/duroxide.js +136 -0
- package/ui/core/src/themes/github-dark.js +2 -1
- package/ui/core/src/themes/github-light.js +2 -1
- package/ui/core/src/themes/gruvbox-dark.js +2 -1
- package/ui/core/src/themes/hacker-x-matrix.js +2 -1
- package/ui/core/src/themes/hacker-x-orion-prime.js +2 -1
- package/ui/core/src/themes/helpers.js +22 -4
- package/ui/core/src/themes/high-contrast-mono.js +2 -1
- package/ui/core/src/themes/index.js +26 -5
- package/ui/core/src/themes/ms-dos.js +2 -1
- package/ui/core/src/themes/noctis-obscuro.js +2 -1
- package/ui/core/src/themes/noctis.js +2 -1
- package/ui/core/src/themes/rust.js +125 -0
- package/ui/core/src/themes/spongebob.js +110 -0
- package/ui/core/src/themes/terminal-green.js +2 -1
- package/ui/core/src/themes/win95.js +2 -1
- package/ui/core/src/themes/winamp.js +2 -1
- package/ui/core/src/themes/workspace-dark.js +2 -1
- package/ui/react/src/web-app.js +1153 -307
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DlP1x0QR.js → abnfDiagram-VRR7QNED-BwJEohLG.js} +1 -1
- package/web/dist/assets/{arc-Zo_8acjl.js → arc-CWUJ_S5s.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Bc18aajT.js → architectureDiagram-ZJ3FMSHR-i6mrLxuE.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-DU76JwlX.js → blockDiagram-677ZJIJ3-C_KG-qPQ.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-COqs9Vhg.js → c4Diagram-LMCZKHZV-BBJH7hbB.js} +1 -1
- package/web/dist/assets/channel-5Ddqi9ib.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-DLg4HU8R.js → chunk-2Q5K7J3B-CXwaFcaM.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-CU0Q4Vsz.js → chunk-32BRIVSS-C8ujjkK-.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BqRSeab5.js → chunk-5VM5RSS4-M9rz7YYk.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DenHmuGS.js → chunk-EX3LRPZG-bk6tRxOA.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BUpE28jH.js → chunk-JWPE2WC7-DZ4WSHGH.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-D4WdKP4p.js → chunk-MOJQB5TN-B5-y6tMF.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DJgnfeyv.js → chunk-RYQCIY6F-yKcnHxm0.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DII2R5xT.js → chunk-V7JOEXUC-CpoNcATD.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-BJ_02Sft.js → chunk-VR4S4FIN-Bf1yD2fn.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DTi_vPws.js → chunk-XXDRQBXY-EnPvDVhP.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B8u1fZiB.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B8u1fZiB.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-DzdVN7Sx.js → cose-bilkent-JH36ORCC-qhKoA2qj.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CLw-4-yw.js → cynefin-VYW2F7L2-jcyYelJS.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-P8h9O8wh.js → cynefinDiagram-TSTJHNR4-D4RLufLP.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-CDArowlh.js → dagre-VKFMJZFB-C9BA3GTO.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-GiMWEGCC.js → diagram-FQU43EPY-DbSSrVUJ.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CpiRJOW2.js → diagram-G47NLZAW-3egISQXQ.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-BOdTWSov.js → diagram-NH7WQ7WH-8IKEMGRg.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BPKB7KD5.js → diagram-OA4YK3LP-VnaogqtK.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-CPS66m-d.js → diagram-WEI45ONY-DxigXH0T.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-nbpYqaxu.js → ebnfDiagram-CCIWWBDH-BSkNwJ9x.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-BF547i7I.js → erDiagram-Q63AITRT-CKN8JXl6.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-CWIuM8_R.js → flowDiagram-23GEKE2U-DDaD5ExP.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-DZRmOOgZ.js → ganttDiagram-NO4QXBWP-tOrBOuoI.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-DQASL6yJ.js → gitGraphDiagram-IHSO6WYX-Cdp867ON.js} +1 -1
- package/web/dist/assets/{index-MikpTOty.js → index-BmwU43_u.js} +6 -6
- package/web/dist/assets/index-D-nA7gUe.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-DlK6reuU.js → infoDiagram-FWYZ7A6U-DwcKHD2h.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-DOiJ-xI_.js → ishikawaDiagram-FXEZZL3T-CWffK5kz.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-B4EVeK0i.js → journeyDiagram-5HDEW3XC-BEAgS-EZ.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-eBT_l0PV.js → kanban-definition-HUTT4EX6-xDm-mRRj.js} +1 -1
- package/web/dist/assets/{linear-CGPMpgYy.js → linear-oP9MA0as.js} +1 -1
- package/web/dist/assets/{mermaid.core-CMg-D4qI.js → mermaid.core-DgXXt1OS.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-COIkL1R_.js → mindmap-definition-LN4V7U3C-BBQgVrYC.js} +1 -1
- package/web/dist/assets/{papaparse.min-BkzE5bQr.js → papaparse.min-CR6JmBWm.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-nXLPVk3y.js → pegDiagram-2B236MQR-C2O0cf3j.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-CLFUyga1.js → pieDiagram-ENE6RG2P-D08pUtYU.js} +1 -1
- package/web/dist/assets/pilotswarm-D7NLCbuc.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-BHSt68p5.js → quadrantDiagram-ABIIQ3AL-D-Z7YoEK.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-CyohRm8J.js → railroadDiagram-RFXS5EU6-ClkdveLI.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-CQTikEcB.js → requirementDiagram-TGXJPOKE-IY4xbn-Q.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-CxDlhil0.js → sankeyDiagram-HTMAVEWB-Cpt5i7KM.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-n4ZRJPY0.js → sequenceDiagram-DBY2YBRQ-c07lALcz.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-J_LGaCmN.js → sizeCapture-X5ZJPWSS-CmCDiNbI.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-CWavHbpD.js → stateDiagram-2N3HPSRC-Dueu17zX.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-AGpIV7wW.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-l-bxBeeJ.js → swimlanes-5IMT3BWC-Csc2Fup8.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTGZSV0N.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-ChA5iURW.js → timeline-definition-FHXFAJF6-4-lf702e.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-DVkk3iKL.js → vennDiagram-L72KCM5P-CmlaKpLI.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CrN8c7Sv.js → wardleyDiagram-EHGQE667-B9Qe44pi.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-Hym-Hdkg.js → xychartDiagram-FW5EYKEG-BFcc0CLn.js} +1 -1
- package/web/dist/index.html +3 -3
- package/ui/core/src/themes/dark-high-contrast.js +0 -56
- package/ui/core/src/themes/workspace-dark-rich.js +0 -65
- package/web/dist/assets/channel-DA38xcdK.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-1k2XCPxl.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-1k2XCPxl.js +0 -1
- package/web/dist/assets/index-Do0UrHeI.css +0 -1
- package/web/dist/assets/pilotswarm-C17s5NOb.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-ClqTpkzd.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Bfar1rKu.js +0 -8
package/ui/core/src/selectors.js
CHANGED
|
@@ -80,19 +80,66 @@ function shouldCompactPaneTitleMetadata(width) {
|
|
|
80
80
|
function getSessionVisualStatus(session) {
|
|
81
81
|
if (!session) return "unknown";
|
|
82
82
|
const status = session.status || "unknown";
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
&& (status === "waiting" || status === "idle" || status === "unknown")
|
|
86
|
-
) {
|
|
83
|
+
const dormant = status === "waiting" || status === "idle" || status === "unknown";
|
|
84
|
+
if (session.cronActive === true && dormant) {
|
|
87
85
|
return "cron_waiting";
|
|
88
86
|
}
|
|
87
|
+
// `activeChildCount` is stamped by the reducer (applyActiveDescendantCounts)
|
|
88
|
+
// and counts only running/input_required descendants — a sub-agent that has
|
|
89
|
+
// finished stays alive and idle, so a plain child count would leave every
|
|
90
|
+
// parent stuck in this state forever. Checked after cron so a scheduled
|
|
91
|
+
// session keeps reading as scheduled.
|
|
92
|
+
if (dormant && (Number(session.activeChildCount) || 0) > 0) {
|
|
93
|
+
return "awaiting_children";
|
|
94
|
+
}
|
|
95
|
+
return status;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sessionStatusLabelForVisualStatus(session, status) {
|
|
99
|
+
if (status === "cron_waiting") return "waiting";
|
|
100
|
+
if (status === "awaiting_children") {
|
|
101
|
+
// The row status is debounced (see mergeSessionRowVisualStatus), so it
|
|
102
|
+
// can outlive the count that produced it by a few seconds.
|
|
103
|
+
const active = Number(session?.activeChildCount) || 0;
|
|
104
|
+
return active > 0 ? `waiting on ${active}` : "waiting on children";
|
|
105
|
+
}
|
|
89
106
|
return status;
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
function getSessionSummaryStatusLabel(session) {
|
|
93
110
|
const status = getSessionVisualStatus(session);
|
|
94
111
|
if (!status || status === "unknown") return "";
|
|
95
|
-
return
|
|
112
|
+
return sessionStatusLabelForVisualStatus(session, status);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The label + timestamp the session-detail box shows, as ONE derivation shared
|
|
117
|
+
* with the list row.
|
|
118
|
+
*
|
|
119
|
+
* The detail box used to print `session.status` raw, and that is why it flipped
|
|
120
|
+
* between "idle" and "waiting" while a turn sat parked: two writers disagree
|
|
121
|
+
* mid-turn — the 4s catalog poll carries the CMS row state, the post-event
|
|
122
|
+
* detail sync carries the live orchestration customStatus — and whichever
|
|
123
|
+
* landed last won. The list has never had that problem because it reads the
|
|
124
|
+
* DEBOUNCED status (mergeSessionRowVisualStatus holds a change for 5s for
|
|
125
|
+
* exactly this reason) and because the cron-aware derivation folds
|
|
126
|
+
* idle/waiting/unknown into one steady state for a scheduled session.
|
|
127
|
+
*
|
|
128
|
+
* Reading raw meant the box opted out of both. This is the same accessor the
|
|
129
|
+
* row uses, so the two can no longer disagree with each other either.
|
|
130
|
+
*/
|
|
131
|
+
export function selectSessionStatusSummary(session) {
|
|
132
|
+
if (!session) return null;
|
|
133
|
+
const timestampMs = session.updatedAt
|
|
134
|
+
|| session.summaryUpdatedAt
|
|
135
|
+
|| session.latestSummaryUpdatedAt
|
|
136
|
+
|| session.createdAt
|
|
137
|
+
|| 0;
|
|
138
|
+
return {
|
|
139
|
+
status: getSessionRowStatusLabel(session) || "unknown",
|
|
140
|
+
timestampMs: timestampMs || null,
|
|
141
|
+
relative: timestampMs ? formatRelativeTime(timestampMs) : "—",
|
|
142
|
+
};
|
|
96
143
|
}
|
|
97
144
|
|
|
98
145
|
function getSessionRowStatusLabel(session) {
|
|
@@ -104,7 +151,7 @@ function getSessionRowStatusLabel(session) {
|
|
|
104
151
|
// already smoothed. The summary view keeps the immediate raw label above.
|
|
105
152
|
const status = getSessionRowVisualStatus(session);
|
|
106
153
|
if (!status || status === "unknown") return "";
|
|
107
|
-
return
|
|
154
|
+
return sessionStatusLabelForVisualStatus(session, status);
|
|
108
155
|
}
|
|
109
156
|
|
|
110
157
|
function getSessionRowVisualStatus(session) {
|
|
@@ -132,6 +179,7 @@ function getSessionVisualKind(session, mode = "local") {
|
|
|
132
179
|
mode === "remote"
|
|
133
180
|
&& isTerminalOrchestrationStatus(session?.orchestrationStatus)
|
|
134
181
|
&& status !== "cron_waiting"
|
|
182
|
+
&& status !== "awaiting_children"
|
|
135
183
|
&& status !== "waiting"
|
|
136
184
|
&& status !== "input_required"
|
|
137
185
|
) {
|
|
@@ -151,6 +199,7 @@ function getSessionRowVisualKind(session, mode = "local") {
|
|
|
151
199
|
mode === "remote"
|
|
152
200
|
&& isTerminalOrchestrationStatus(session?.orchestrationStatus)
|
|
153
201
|
&& status !== "cron_waiting"
|
|
202
|
+
&& status !== "awaiting_children"
|
|
154
203
|
&& status !== "waiting"
|
|
155
204
|
&& status !== "input_required"
|
|
156
205
|
) {
|
|
@@ -165,6 +214,9 @@ function sessionStatusColor(session, mode = "local") {
|
|
|
165
214
|
switch (getSessionRowVisualKind(session, mode)) {
|
|
166
215
|
case "running": return "green";
|
|
167
216
|
case "cron_waiting": return "yellow";
|
|
217
|
+
// Distinct from both running (this session is not doing the work) and
|
|
218
|
+
// waiting (nothing is happening) — something IS running, one level down.
|
|
219
|
+
case "awaiting_children": return "magenta";
|
|
168
220
|
case "waiting": return "yellow";
|
|
169
221
|
case "input_required": return "cyan";
|
|
170
222
|
case "cancelled": return "gray";
|
|
@@ -181,6 +233,7 @@ function sessionStatusIcon(session, mode = "local") {
|
|
|
181
233
|
switch (getSessionRowVisualKind(session, mode)) {
|
|
182
234
|
case "running": return "*";
|
|
183
235
|
case "cron_waiting": return "~";
|
|
236
|
+
case "awaiting_children": return ">";
|
|
184
237
|
case "waiting": return "~";
|
|
185
238
|
case "input_required": return "?";
|
|
186
239
|
case "cancelled": return "x";
|
|
@@ -242,7 +295,11 @@ function ownerInitials(owner) {
|
|
|
242
295
|
*/
|
|
243
296
|
export function manualOrderContainerKey(session, pinned = false) {
|
|
244
297
|
if (!isManuallyOrderableSession(session)) return null;
|
|
245
|
-
|
|
298
|
+
// Pinned folders and pinned sessions are SEPARATE bands (rankSessionBand
|
|
299
|
+
// 1 and 2), so they cannot share a container: the drag would offer drop
|
|
300
|
+
// positions among the pinned sessions that the sort then refuses, and the
|
|
301
|
+
// row landed somewhere other than the insertion line the user aimed at.
|
|
302
|
+
if (pinned) return session.isGroup ? "pinned:folders" : "pinned:sessions";
|
|
246
303
|
if (session.isGroup) return "folders";
|
|
247
304
|
if (session.groupId) return `g:${session.groupId}`;
|
|
248
305
|
return "";
|
|
@@ -591,10 +648,18 @@ function buildTotalDescendantCounts(byId) {
|
|
|
591
648
|
const { childMap } = buildChildMaps(byId);
|
|
592
649
|
const counts = new Map();
|
|
593
650
|
|
|
651
|
+
// `visiting` bounds a malformed parent chain (self-parent, cycle). Without
|
|
652
|
+
// it the recursion below never terminates and the whole UI hangs on a
|
|
653
|
+
// RangeError rather than rendering a slightly wrong count.
|
|
654
|
+
const visiting = new Set();
|
|
655
|
+
|
|
594
656
|
function countFor(sessionId) {
|
|
595
657
|
if (counts.has(sessionId)) return counts.get(sessionId);
|
|
658
|
+
if (visiting.has(sessionId)) return 0;
|
|
659
|
+
visiting.add(sessionId);
|
|
596
660
|
const children = childMap.get(sessionId) || [];
|
|
597
661
|
const total = children.reduce((sum, childId) => sum + 1 + countFor(childId), 0);
|
|
662
|
+
visiting.delete(sessionId);
|
|
598
663
|
counts.set(sessionId, total);
|
|
599
664
|
return total;
|
|
600
665
|
}
|
|
@@ -2705,238 +2770,6 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
|
2705
2770
|
return memoize(lines.length > 0 ? lines : [{ text: "No messages yet.", color: "gray" }]);
|
|
2706
2771
|
}
|
|
2707
2772
|
|
|
2708
|
-
// True when a chat message is plain user/assistant prose the rich (block)
|
|
2709
|
-
// renderer can format as markdown directly. Everything with terminal-era
|
|
2710
|
-
// special handling — splash art, thinking cards, chrome-less summaries,
|
|
2711
|
-
// system/PilotSwarm cards, asked-and-answered exchanges, embedded
|
|
2712
|
-
// [SYSTEM: …] notice segments — stays on the line renderer, which already
|
|
2713
|
-
// knows every edge case.
|
|
2714
|
-
function isRichRenderableChatMessage(message) {
|
|
2715
|
-
if (!message) return false;
|
|
2716
|
-
if (message.kind === "epoch-divider" || message.kind === "regen-refused" || message.kind === "regen-failed") return false;
|
|
2717
|
-
if (message.splash || message.thinking || message.noChrome) return false;
|
|
2718
|
-
if (message.role !== "user" && message.role !== "assistant") return false;
|
|
2719
|
-
if (message.role === "user" && parseAskedAndAnsweredExchange(message?.text || "")) return false;
|
|
2720
|
-
const segments = splitSystemNoticeSegments(message?.text || "");
|
|
2721
|
-
if (segments.some((segment) => segment.kind === "system")) return false;
|
|
2722
|
-
return true;
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
/**
|
|
2726
|
-
* Message-level view of the active chat for the rich (desktop-style) web
|
|
2727
|
-
* renderer. Returns an ordered array of blocks:
|
|
2728
|
-
*
|
|
2729
|
-
* { kind: "message", id, role, header, text, pendingPhase, attachments }
|
|
2730
|
-
* — plain user/assistant prose. `header` is describeChatMessageHeader()
|
|
2731
|
-
* output; `text` is the artifact-link-decorated markdown source the
|
|
2732
|
-
* renderer formats itself; `attachments` is { sessionId, attachments }
|
|
2733
|
-
* (the sentinel shape) or null.
|
|
2734
|
-
*
|
|
2735
|
-
* { kind: "lines", variant, lines }
|
|
2736
|
-
* — everything else (splash, thinking cards, system cards, dividers),
|
|
2737
|
-
* pre-rendered through the exact same line builders the terminal
|
|
2738
|
-
* transcript uses, so no edge case has two implementations.
|
|
2739
|
-
*
|
|
2740
|
-
* The TUI never calls this; selectChatLines remains the terminal path.
|
|
2741
|
-
*/
|
|
2742
|
-
// Rich message blocks, cached on the message object. Keyed weakly so entries
|
|
2743
|
-
// die with the transcript they came from.
|
|
2744
|
-
const chatMessageBlockCache = new WeakMap();
|
|
2745
|
-
|
|
2746
|
-
export function selectChatBlocks(state, maxWidth = 80, options = {}) {
|
|
2747
|
-
const messages = selectActiveChat(state);
|
|
2748
|
-
if (!messages || messages.length === 0) return [];
|
|
2749
|
-
|
|
2750
|
-
const viewerKey = ownerKeyForOwner(state?.auth?.principal);
|
|
2751
|
-
const activeSessionId = state?.sessions?.activeSessionId;
|
|
2752
|
-
const activeSession = activeSessionId ? state?.sessions?.byId?.[activeSessionId] : null;
|
|
2753
|
-
const ownerKey = ownerKeyForOwner(activeSession?.owner);
|
|
2754
|
-
const sharedContext = Boolean(activeSession && (
|
|
2755
|
-
(activeSession.visibility && activeSession.visibility !== "private")
|
|
2756
|
-
|| (ownerKey && viewerKey && ownerKey !== viewerKey)
|
|
2757
|
-
));
|
|
2758
|
-
const buildOptions = {
|
|
2759
|
-
...(options?.tableMode ? { tableMode: options.tableMode } : {}),
|
|
2760
|
-
...(viewerKey ? { viewerKey } : {}),
|
|
2761
|
-
...(ownerKey ? { ownerKey } : {}),
|
|
2762
|
-
...(sharedContext ? { sharedContext: true } : {}),
|
|
2763
|
-
};
|
|
2764
|
-
|
|
2765
|
-
const blocks = [];
|
|
2766
|
-
for (const message of messages) {
|
|
2767
|
-
// Fast path FIRST. Classifying a message as rich-renderable scans its
|
|
2768
|
-
// text (asked-and-answered parsing, system-notice splitting), and that
|
|
2769
|
-
// ran for every message on every call — so a pane-splitter drag paid
|
|
2770
|
-
// it across the whole transcript per width step. A cache entry only
|
|
2771
|
-
// exists for a message already classified rich, and `kind` is part of
|
|
2772
|
-
// what is compared, so a hit is safe to take before classifying.
|
|
2773
|
-
// Object.is, NOT ===: `createdAt` is NaN whenever the source event had
|
|
2774
|
-
// no parseable timestamp, and NaN === NaN is false, so a plain equality
|
|
2775
|
-
// check could never hit for those messages — the cache would silently
|
|
2776
|
-
// do nothing while still costing a lookup. Same trap as comparing
|
|
2777
|
-
// Infinity by subtraction.
|
|
2778
|
-
const cached = message ? chatMessageBlockCache.get(message) : null;
|
|
2779
|
-
if (cached
|
|
2780
|
-
&& Object.is(cached.text, message.text)
|
|
2781
|
-
&& Object.is(cached.kind, message.kind)
|
|
2782
|
-
&& Object.is(cached.role, message.role)
|
|
2783
|
-
&& Object.is(cached.id, message.id)
|
|
2784
|
-
&& Object.is(cached.pendingPhase, message.pendingPhase)
|
|
2785
|
-
&& Object.is(cached.stopped, message.stopped)
|
|
2786
|
-
// The header is derived from these, and the chips from attachments.
|
|
2787
|
-
// Messages are mutated in place (pendingPhase provably is), so every
|
|
2788
|
-
// field the block is built from has to be compared — not just the
|
|
2789
|
-
// obvious ones.
|
|
2790
|
-
&& Object.is(cached.createdAt, message.createdAt)
|
|
2791
|
-
&& Object.is(cached.time, message.time)
|
|
2792
|
-
&& Object.is(cached.sender, message.sender)
|
|
2793
|
-
&& Object.is(cached.attachments, message.attachments)
|
|
2794
|
-
&& Object.is(cached.tableMode, buildOptions.tableMode)
|
|
2795
|
-
&& Object.is(cached.viewerKey, buildOptions.viewerKey)
|
|
2796
|
-
&& Object.is(cached.ownerKey, buildOptions.ownerKey)
|
|
2797
|
-
&& Object.is(cached.sharedContext, buildOptions.sharedContext)) {
|
|
2798
|
-
blocks.push(cached.block);
|
|
2799
|
-
continue;
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
if (message?.kind === "epoch-divider") {
|
|
2803
|
-
blocks.push({ kind: "lines", variant: "divider", lines: [buildEpochDividerLine(message, maxWidth)] });
|
|
2804
|
-
continue;
|
|
2805
|
-
}
|
|
2806
|
-
if (message?.kind === "regen-failed") {
|
|
2807
|
-
blocks.push({ kind: "lines", variant: "divider", lines: [buildRegenFailedLine(message, maxWidth)] });
|
|
2808
|
-
continue;
|
|
2809
|
-
}
|
|
2810
|
-
if (message?.kind === "regen-refused") {
|
|
2811
|
-
blocks.push({ kind: "lines", variant: "divider", lines: [buildRegenRefusedLine(message, maxWidth)] });
|
|
2812
|
-
continue;
|
|
2813
|
-
}
|
|
2814
|
-
if (isRichRenderableChatMessage(message)) {
|
|
2815
|
-
// A rich message block does NOT depend on maxWidth — the browser
|
|
2816
|
-
// wraps this text with CSS; only the terminal needs a column count.
|
|
2817
|
-
// Rebuilding it on every width step meant dragging a pane splitter
|
|
2818
|
-
// re-parsed artifact links and re-derived headers for the entire
|
|
2819
|
-
// loaded transcript to produce byte-identical output. Cache on the
|
|
2820
|
-
// message itself, keyed by the inputs that CAN change it.
|
|
2821
|
-
const attachmentLines = buildAttachmentChipLines(message, { tableMode: "sentinel" });
|
|
2822
|
-
const attachmentSentinel = attachmentLines.find((line) => line?.kind === "imageAttachments") || null;
|
|
2823
|
-
const block = {
|
|
2824
|
-
kind: "message",
|
|
2825
|
-
id: message?.id ?? null,
|
|
2826
|
-
role: message.role,
|
|
2827
|
-
header: describeChatMessageHeader(message, buildOptions),
|
|
2828
|
-
text: decorateArtifactLinksForChat(message?.text || ""),
|
|
2829
|
-
pendingPhase: message?.pendingPhase || null,
|
|
2830
|
-
attachments: attachmentSentinel
|
|
2831
|
-
? { sessionId: attachmentSentinel.sessionId, attachments: attachmentSentinel.attachments }
|
|
2832
|
-
: null,
|
|
2833
|
-
};
|
|
2834
|
-
chatMessageBlockCache.set(message, {
|
|
2835
|
-
block,
|
|
2836
|
-
text: message.text,
|
|
2837
|
-
kind: message.kind,
|
|
2838
|
-
createdAt: message.createdAt,
|
|
2839
|
-
time: message.time,
|
|
2840
|
-
sender: message.sender,
|
|
2841
|
-
attachments: message.attachments,
|
|
2842
|
-
role: message.role,
|
|
2843
|
-
id: message.id,
|
|
2844
|
-
pendingPhase: message.pendingPhase,
|
|
2845
|
-
stopped: message.stopped,
|
|
2846
|
-
tableMode: buildOptions.tableMode,
|
|
2847
|
-
viewerKey: buildOptions.viewerKey,
|
|
2848
|
-
ownerKey: buildOptions.ownerKey,
|
|
2849
|
-
sharedContext: buildOptions.sharedContext,
|
|
2850
|
-
});
|
|
2851
|
-
blocks.push(block);
|
|
2852
|
-
continue;
|
|
2853
|
-
}
|
|
2854
|
-
const lineBlock = cachedLineBlock(message, maxWidth, buildOptions);
|
|
2855
|
-
if (lineBlock) blocks.push(lineBlock);
|
|
2856
|
-
}
|
|
2857
|
-
return blocks;
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
/**
|
|
2861
|
-
* Non-message blocks (thinking cards, system cards, splash, chrome-less
|
|
2862
|
-
* summaries) go through the terminal line builders, which DO depend on width —
|
|
2863
|
-
* so unlike message blocks these cannot be made width-independent. What they
|
|
2864
|
-
* can avoid is being rebuilt when the width has not moved, which is every poll:
|
|
2865
|
-
* the poll replaces `session`, the portal's useMemo sees a new selectorState,
|
|
2866
|
-
* and the whole transcript is re-derived. Measured at 1500 messages with 10%
|
|
2867
|
-
* line blocks that was 1.17ms a time, and 5.26ms at 50%.
|
|
2868
|
-
*
|
|
2869
|
-
* Only the LAST width is kept per message. A drag sweeps widths and will miss
|
|
2870
|
-
* on every step — that work is genuinely required, since these lines are
|
|
2871
|
-
* wrapped to a column count.
|
|
2872
|
-
*
|
|
2873
|
-
* The compared fields were enumerated from buildChatMessageLines and the
|
|
2874
|
-
* builders it delegates to (message/thinking/system-notice cards). `stopped` is
|
|
2875
|
-
* included because history.js sets it IN PLACE on an existing message.
|
|
2876
|
-
*/
|
|
2877
|
-
const chatLineBlockCache = new WeakMap();
|
|
2878
|
-
|
|
2879
|
-
function cachedLineBlock(message, maxWidth, buildOptions) {
|
|
2880
|
-
if (!message || typeof message !== "object") {
|
|
2881
|
-
const lines = buildChatMessageLines(message, maxWidth, buildOptions);
|
|
2882
|
-
return lines.length > 0 ? { kind: "lines", variant: "event", lines } : null;
|
|
2883
|
-
}
|
|
2884
|
-
const cached = chatLineBlockCache.get(message);
|
|
2885
|
-
if (cached
|
|
2886
|
-
&& Object.is(cached.maxWidth, maxWidth)
|
|
2887
|
-
&& Object.is(cached.text, message.text)
|
|
2888
|
-
&& Object.is(cached.mobileText, message.mobileText)
|
|
2889
|
-
&& Object.is(cached.role, message.role)
|
|
2890
|
-
&& Object.is(cached.time, message.time)
|
|
2891
|
-
&& Object.is(cached.createdAt, message.createdAt)
|
|
2892
|
-
&& Object.is(cached.splash, message.splash)
|
|
2893
|
-
&& Object.is(cached.thinking, message.thinking)
|
|
2894
|
-
&& Object.is(cached.thinkingLabel, message.thinkingLabel)
|
|
2895
|
-
&& Object.is(cached.progressKind, message.progressKind)
|
|
2896
|
-
&& Object.is(cached.noChrome, message.noChrome)
|
|
2897
|
-
&& Object.is(cached.cardTitle, message.cardTitle)
|
|
2898
|
-
&& Object.is(cached.cardTitleColor, message.cardTitleColor)
|
|
2899
|
-
&& Object.is(cached.cardBorderColor, message.cardBorderColor)
|
|
2900
|
-
&& Object.is(cached.stopped, message.stopped)
|
|
2901
|
-
&& Object.is(cached.tableMode, buildOptions.tableMode)
|
|
2902
|
-
&& Object.is(cached.viewerKey, buildOptions.viewerKey)
|
|
2903
|
-
&& Object.is(cached.ownerKey, buildOptions.ownerKey)
|
|
2904
|
-
&& Object.is(cached.sharedContext, buildOptions.sharedContext)) {
|
|
2905
|
-
return cached.block;
|
|
2906
|
-
}
|
|
2907
|
-
const lines = buildChatMessageLines(message, maxWidth, buildOptions);
|
|
2908
|
-
const block = lines.length > 0 ? { kind: "lines", variant: "event", lines } : null;
|
|
2909
|
-
chatLineBlockCache.set(message, {
|
|
2910
|
-
block,
|
|
2911
|
-
maxWidth,
|
|
2912
|
-
text: message.text,
|
|
2913
|
-
mobileText: message.mobileText,
|
|
2914
|
-
role: message.role,
|
|
2915
|
-
time: message.time,
|
|
2916
|
-
createdAt: message.createdAt,
|
|
2917
|
-
splash: message.splash,
|
|
2918
|
-
thinking: message.thinking,
|
|
2919
|
-
thinkingLabel: message.thinkingLabel,
|
|
2920
|
-
progressKind: message.progressKind,
|
|
2921
|
-
noChrome: message.noChrome,
|
|
2922
|
-
cardTitle: message.cardTitle,
|
|
2923
|
-
cardTitleColor: message.cardTitleColor,
|
|
2924
|
-
cardBorderColor: message.cardBorderColor,
|
|
2925
|
-
stopped: message.stopped,
|
|
2926
|
-
tableMode: buildOptions.tableMode,
|
|
2927
|
-
viewerKey: buildOptions.viewerKey,
|
|
2928
|
-
ownerKey: buildOptions.ownerKey,
|
|
2929
|
-
sharedContext: buildOptions.sharedContext,
|
|
2930
|
-
});
|
|
2931
|
-
return block;
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
// A centered inline rule ("──── label ────") for transcript markers. The dash
|
|
2935
|
-
// runs are CAPPED (not stretched to maxWidth): the web portal wraps by pixel
|
|
2936
|
-
// width, and a maxWidth-long "─" run overflows a narrower pane and wraps the
|
|
2937
|
-
// rule mid-label. A short symmetric rule reads as a divider on every width, and
|
|
2938
|
-
// when the label alone will not fit it is rendered bare (wraps as plain text,
|
|
2939
|
-
// never as dangling dash fragments).
|
|
2940
2773
|
function buildRuleLine(label, color, maxWidth) {
|
|
2941
2774
|
const safeWidth = Math.max(24, Number(maxWidth) || 80);
|
|
2942
2775
|
const room = safeWidth - label.length;
|
|
@@ -4390,7 +4223,9 @@ export function selectStatusBar(state) {
|
|
|
4390
4223
|
if (state.ui.modal?.type === "sessionAgentPicker") {
|
|
4391
4224
|
return {
|
|
4392
4225
|
left: "Select an agent for the new session",
|
|
4393
|
-
|
|
4226
|
+
// Enter no longer always creates — on a section header it opens or
|
|
4227
|
+
// closes — and the disclosure keys have to be named somewhere.
|
|
4228
|
+
right: "up/down move · ←/→ open/close · enter start · esc cancel",
|
|
4394
4229
|
};
|
|
4395
4230
|
}
|
|
4396
4231
|
if (state.ui.modal?.type === "sessionGroupPicker") {
|
|
@@ -5615,36 +5450,52 @@ export function selectSessionAgentPickerModal(state, maxWidth = 76) {
|
|
|
5615
5450
|
const items = Array.isArray(modal.items) ? modal.items : [];
|
|
5616
5451
|
const selectedIndex = Math.max(0, Number(modal.selectedIndex) || 0);
|
|
5617
5452
|
const contentWidth = Math.max(24, maxWidth - 4);
|
|
5618
|
-
// Items
|
|
5619
|
-
//
|
|
5620
|
-
//
|
|
5621
|
-
// mechanism the model picker uses.
|
|
5622
|
-
const GROUP_HEADINGS = { shared: "Shared", mine: "My agents", generic: "" };
|
|
5623
|
-
// Package-scope vocabulary only makes sense when packages exist: a pure
|
|
5624
|
-
// baked deployment keeps the classic flat list, no headings.
|
|
5625
|
-
const hasPackageItems = items.some((item) => item?.packageName);
|
|
5453
|
+
// Items ARE rows, headings included: navigation walks onto a heading and
|
|
5454
|
+
// Enter toggles it, so every section is reachable from the keyboard.
|
|
5455
|
+
// rowItemIndexes stays 1:1 for the renderers that map a click back to an
|
|
5456
|
+
// item (the same mechanism the model picker uses).
|
|
5626
5457
|
const rows = [];
|
|
5627
5458
|
const rowItemIndexes = [];
|
|
5628
|
-
let lastGroup = null;
|
|
5629
5459
|
items.forEach((item, index) => {
|
|
5630
|
-
const group = item?.group || "shared";
|
|
5631
|
-
if (hasPackageItems && group !== lastGroup) {
|
|
5632
|
-
const heading = GROUP_HEADINGS[group] ?? "";
|
|
5633
|
-
rows.push([{ text: heading ? `── ${heading} ──` : "──", color: "gray" }]);
|
|
5634
|
-
rowItemIndexes.push(null);
|
|
5635
|
-
lastGroup = group;
|
|
5636
|
-
}
|
|
5637
5460
|
const isSelected = index === selectedIndex;
|
|
5638
|
-
const
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5461
|
+
const indent = " ".repeat(Math.max(0, Number(item?.depth) || 0) * 2);
|
|
5462
|
+
let labelRuns;
|
|
5463
|
+
|
|
5464
|
+
if (item?.kind === "section") {
|
|
5465
|
+
const arrow = item.collapsed ? "▸" : "▾";
|
|
5466
|
+
// Only when the row's scope disagrees with the shelf it is on:
|
|
5467
|
+
// an admin sees other users' user-scoped packages, and those land
|
|
5468
|
+
// under Shared while not being shared at all. Otherwise the
|
|
5469
|
+
// section heading already carries it.
|
|
5470
|
+
const scopeBadge = item.sectionKind === "package" && item.packageScope === "user" && !item.mine
|
|
5471
|
+
? " [private]"
|
|
5472
|
+
: "";
|
|
5473
|
+
// The count runs INLINE after the title, not right-aligned with
|
|
5474
|
+
// padding. `contentWidth` is a character count, and the portal's
|
|
5475
|
+
// row is a fluid-width button — padding a row out to a guessed
|
|
5476
|
+
// column width simply overflowed it, and the run wrapped mid-phrase
|
|
5477
|
+
// ("2 / entries · 4 agents"). Inline can only ever break at a
|
|
5478
|
+
// space.
|
|
5479
|
+
labelRuns = fitRuns([
|
|
5480
|
+
{ text: `${indent}${arrow} `, color: "cyan" },
|
|
5481
|
+
{ text: item.title || "Section", color: "white", bold: true },
|
|
5482
|
+
...(scopeBadge ? [{ text: scopeBadge, color: "cyan" }] : []),
|
|
5483
|
+
...(item.meta ? [{ text: ` · ${item.meta}`, color: "gray" }] : []),
|
|
5484
|
+
], contentWidth);
|
|
5485
|
+
} else {
|
|
5486
|
+
const callable = item?.kind === "generic" || item?.supportsDirectStart !== false;
|
|
5487
|
+
// A sub-agent that cannot be started is shown, not hidden — seeing
|
|
5488
|
+
// what a package is made of is most of the value of grouping it.
|
|
5489
|
+
// Dimming plus the └ connector carries it; the detail pane spells
|
|
5490
|
+
// out why. An extra "called only" tag on the row was redundant with
|
|
5491
|
+
// both, and read as a sentence fragment.
|
|
5492
|
+
const glyph = item?.kind === "generic" ? "○ " : (item?.depth || 0) > 0 && item?.parentAgentName ? "└ " : "★ ";
|
|
5493
|
+
labelRuns = fitRuns([
|
|
5494
|
+
{ text: `${indent}${glyph}`, color: callable ? "yellow" : "gray" },
|
|
5495
|
+
{ text: item?.title || item?.agentName || item?.id || "Agent", color: callable ? "white" : "gray", bold: callable },
|
|
5496
|
+
], contentWidth);
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5648
5499
|
rows.push(isSelected
|
|
5649
5500
|
? buildActiveHighlightLine(labelRuns.map((run) => run.text).join("").padEnd(contentWidth, " "))
|
|
5650
5501
|
: labelRuns);
|
|
@@ -5654,7 +5505,29 @@ export function selectSessionAgentPickerModal(state, maxWidth = 76) {
|
|
|
5654
5505
|
const selectedItem = items[selectedIndex] || null;
|
|
5655
5506
|
const selectedModel = modal.sessionOptions?.model || null;
|
|
5656
5507
|
const selectedReasoningEffort = modal.sessionOptions?.reasoningEffort || null;
|
|
5657
|
-
const detailsLines = selectedItem
|
|
5508
|
+
const detailsLines = selectedItem?.kind === "section"
|
|
5509
|
+
? [
|
|
5510
|
+
[{ text: selectedItem.title || "Section", color: "white", bold: true }],
|
|
5511
|
+
...(selectedItem.sectionKind === "package"
|
|
5512
|
+
? [
|
|
5513
|
+
[{ text: `${selectedItem.packageName || "package"}@${selectedItem.packageSemver || "?"}`, color: "gray" }],
|
|
5514
|
+
...(selectedItem.ownerLabel ? [[{ text: `Published by ${selectedItem.ownerLabel}`, color: "gray" }]] : []),
|
|
5515
|
+
]
|
|
5516
|
+
: [[{
|
|
5517
|
+
text: selectedItem.sectionKind === "builtin"
|
|
5518
|
+
? "Agents baked into this deployment."
|
|
5519
|
+
: "Agent packages installed on this deployment.",
|
|
5520
|
+
color: "gray",
|
|
5521
|
+
}]]),
|
|
5522
|
+
[{ text: "", color: "gray" }],
|
|
5523
|
+
[{ text: selectedItem.meta || "", color: "gray" }],
|
|
5524
|
+
[{ text: "", color: "gray" }],
|
|
5525
|
+
[{
|
|
5526
|
+
text: selectedItem.collapsed ? "Enter or → to open." : "Enter or ← to close.",
|
|
5527
|
+
color: "gray",
|
|
5528
|
+
}],
|
|
5529
|
+
]
|
|
5530
|
+
: selectedItem
|
|
5658
5531
|
? [
|
|
5659
5532
|
[{
|
|
5660
5533
|
text: selectedItem.title || selectedItem.agentName || selectedItem.id || "Agent",
|
|
@@ -5667,6 +5540,12 @@ export function selectSessionAgentPickerModal(state, maxWidth = 76) {
|
|
|
5667
5540
|
...(selectedItem.packageName
|
|
5668
5541
|
? [[{ text: `Package: ${selectedItem.packageName}@${selectedItem.packageSemver || "?"} · ${selectedItem.packageScope || "shared"}`, color: "gray" }]]
|
|
5669
5542
|
: (selectedItem.kind === "agent" ? [[{ text: "Built-in agent", color: "gray" }]] : [])),
|
|
5543
|
+
...(selectedItem.startedBy?.length
|
|
5544
|
+
? [[{ text: `Started by: ${selectedItem.startedBy.join(", ")}`, color: "gray" }]]
|
|
5545
|
+
: []),
|
|
5546
|
+
...(selectedItem.kind !== "generic" && selectedItem.supportsDirectStart === false
|
|
5547
|
+
? [[{ text: "Cannot be started on its own", color: "yellow" }]]
|
|
5548
|
+
: []),
|
|
5670
5549
|
...(selectedModel ? [[{ text: `Model: ${selectedModel}`, color: "gray" }]] : []),
|
|
5671
5550
|
...(selectedReasoningEffort ? [[{ text: `Reasoning: ${selectedReasoningEffort}`, color: "gray" }]] : []),
|
|
5672
5551
|
[{ text: "", color: "gray" }],
|
|
@@ -5922,11 +5801,16 @@ export function selectRenameSessionModal(state, maxWidth = 76) {
|
|
|
5922
5801
|
text: " cancel",
|
|
5923
5802
|
color: "gray",
|
|
5924
5803
|
}],
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5804
|
+
// A folder has no agent writing titles for it, so the auto-title
|
|
5805
|
+
// caveat is noise there — and worse, it implies a folder is a
|
|
5806
|
+
// session.
|
|
5807
|
+
...(modal.isGroup ? [] : [
|
|
5808
|
+
[{ text: "", color: "gray" }],
|
|
5809
|
+
[{
|
|
5810
|
+
text: "Manual titles stop future automatic LLM title changes for this session.",
|
|
5811
|
+
color: "gray",
|
|
5812
|
+
}],
|
|
5813
|
+
]),
|
|
5930
5814
|
],
|
|
5931
5815
|
detailsLines,
|
|
5932
5816
|
idealWidth: Math.min(
|
|
@@ -7632,6 +7516,10 @@ export function selectThemePickerModal(state, maxWidth = 80) {
|
|
|
7632
7516
|
const detailsLines = selectedItem
|
|
7633
7517
|
? [
|
|
7634
7518
|
[{ text: `theme: ${selectedItem.description || "Shared theme for the portal and native TUI."}`, color: "white" }],
|
|
7519
|
+
// The portal draws group headings between rows; the TUI list is a
|
|
7520
|
+
// flat scroller with no room for them, so the group is surfaced
|
|
7521
|
+
// here instead. The ORDER still clusters by group in both.
|
|
7522
|
+
...(selectedItem.group ? [[{ text: `group: ${selectedItem.group}`, color: "gray" }]] : []),
|
|
7635
7523
|
{ text: "", color: "gray" },
|
|
7636
7524
|
[{
|
|
7637
7525
|
text: previewingTheme
|
package/ui/core/src/state.js
CHANGED
|
@@ -162,7 +162,8 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
162
162
|
nodeMapSelectedNode: null,
|
|
163
163
|
sequenceExpandedTurns: [],
|
|
164
164
|
sequenceSelectedTurn: null,
|
|
165
|
-
// "rich"
|
|
165
|
+
// "rich" was retired as a view mode; a persisted "rich" falls
|
|
166
|
+
// through to the transcript.
|
|
166
167
|
chatViewMode: chatViewMode === "summary" ? "summary" : "transcript",
|
|
167
168
|
statsViewMode: "session",
|
|
168
169
|
prompt: "",
|
|
@@ -174,6 +175,10 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
174
175
|
// A persisted theme id may name a theme that no longer ships —
|
|
175
176
|
// fall back to the default instead of crashing theme consumers.
|
|
176
177
|
themeId: getTheme(themeId)?.id || DEFAULT_THEME_ID,
|
|
178
|
+
// Touch-friendly scale: bumps type and control sizes one step. A phone in
|
|
179
|
+
// portrait is not the only case — a wall display or a shared screen wants
|
|
180
|
+
// the same thing — so it is a preference, not a viewport query.
|
|
181
|
+
touchScale: false,
|
|
177
182
|
modal: null,
|
|
178
183
|
fullscreenPane: null,
|
|
179
184
|
layout: {
|
|
@@ -263,6 +268,34 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
263
268
|
// Selection to restore when leaving a chat-opened preview, so
|
|
264
269
|
// following a chat link never disturbs the list.
|
|
265
270
|
restoreArtifactId: null,
|
|
271
|
+
// The portal's artifact takeover pane: a full-height reader that
|
|
272
|
+
// replaces the inspector AND activity panes in the right column.
|
|
273
|
+
// Opened by chat artifact cards and artifact deep links; the Files
|
|
274
|
+
// list keeps its own stacked list-above-preview view, because
|
|
275
|
+
// taking over the column would hide the list being browsed.
|
|
276
|
+
//
|
|
277
|
+
// The TUI ignores this entirely — it has no such pane.
|
|
278
|
+
paneOpen: false,
|
|
279
|
+
// What the right column looked like BEFORE the pane opened, so ✕
|
|
280
|
+
// can put it back. When the user had already resized the column
|
|
281
|
+
// out of existence, closing must return to that — not conjure an
|
|
282
|
+
// inspector they had deliberately dismissed.
|
|
283
|
+
paneRestoresToHidden: false,
|
|
284
|
+
// Rendered page vs. HTML source. Lifted out of the preview
|
|
285
|
+
// component so the reader pane's single header can own the toggle
|
|
286
|
+
// — the alternative was a second header row stacked under the
|
|
287
|
+
// first, which is exactly the box-in-a-box the pane exists to undo.
|
|
288
|
+
htmlViewMode: "rendered",
|
|
289
|
+
// Zoom for the rendered HTML frame ONLY. Browser zoom scales the
|
|
290
|
+
// whole workspace — the session list and transcript included —
|
|
291
|
+
// when all you wanted was to read a dense chart. This scales the
|
|
292
|
+
// artifact and nothing else.
|
|
293
|
+
htmlZoom: 1,
|
|
294
|
+
// Fit-to-width for the rendered HTML frame. Lifted here for the
|
|
295
|
+
// same reason htmlViewMode is: on mobile the panel is chromeless
|
|
296
|
+
// and the artifact bar owns the controls, so the two surfaces have
|
|
297
|
+
// to read one value.
|
|
298
|
+
htmlFitWidth: false,
|
|
266
299
|
filter: {
|
|
267
300
|
scope: "selectedSession",
|
|
268
301
|
query: "",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createTheme } from "./helpers.js";
|
|
1
|
+
import { createTheme, THEME_GROUP_VSCODE } from "./helpers.js";
|
|
2
2
|
|
|
3
3
|
const catppuccinMochaTheme = createTheme({
|
|
4
4
|
id: "catppuccin-mocha",
|
|
5
5
|
label: "Catppuccin Mocha",
|
|
6
6
|
description: "Creamy dark palette with soft pastels and high legibility.",
|
|
7
|
+
group: THEME_GROUP_VSCODE,
|
|
7
8
|
page: {
|
|
8
9
|
background: "#1e1e2e",
|
|
9
10
|
foreground: "#cdd6f4",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createTheme } from "./helpers.js";
|
|
1
|
+
import { createTheme, THEME_GROUP_VSCODE } from "./helpers.js";
|
|
2
2
|
|
|
3
3
|
const cobalt2Theme = createTheme({
|
|
4
4
|
id: "cobalt2",
|
|
5
5
|
label: "Cobalt2",
|
|
6
6
|
description: "Electric blue shell with bright yellow accents and saturated syntax colors.",
|
|
7
|
+
group: THEME_GROUP_VSCODE,
|
|
7
8
|
page: {
|
|
8
9
|
background: "#193549",
|
|
9
10
|
foreground: "#ffffff",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTheme } from "./helpers.js";
|
|
1
|
+
import { createTheme, THEME_GROUP_RETRO } from "./helpers.js";
|
|
2
2
|
|
|
3
3
|
// DOOM (id Software, 1993) — the status bar, not the shooter.
|
|
4
4
|
//
|
|
@@ -19,6 +19,7 @@ const doomTheme = createTheme({
|
|
|
19
19
|
id: "doom",
|
|
20
20
|
label: "DOOM",
|
|
21
21
|
description: "STBAR brown metal, bone type, and the three keycards as the status palette.",
|
|
22
|
+
group: THEME_GROUP_RETRO,
|
|
22
23
|
// The face, drawn as data rather than markup so ui/core stays free of any
|
|
23
24
|
// framework and nothing has to inject raw HTML. The portal renders it in
|
|
24
25
|
// place of the deployment's logo; the TUI ignores it.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createTheme } from "./helpers.js";
|
|
1
|
+
import { createTheme, THEME_GROUP_VSCODE } from "./helpers.js";
|
|
2
2
|
|
|
3
3
|
const draculaTheme = createTheme({
|
|
4
4
|
id: "dracula",
|
|
5
5
|
label: "Dracula",
|
|
6
6
|
description: "Classic Dracula contrast with neon green, pink, and violet accents.",
|
|
7
|
+
group: THEME_GROUP_VSCODE,
|
|
7
8
|
page: {
|
|
8
9
|
background: "#282a36",
|
|
9
10
|
foreground: "#f8f8f2",
|