pilotswarm 0.5.36 → 0.5.38
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 +3 -3
- package/tui/src/app.js +0 -4
- package/tui/src/index.js +0 -9
- package/ui/core/src/canvas-actions.js +148 -0
- package/ui/core/src/commands.js +0 -1
- package/ui/core/src/controller.js +296 -21
- package/ui/core/src/history.js +134 -1
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/reducer.js +407 -19
- package/ui/core/src/selectors.js +223 -154
- package/ui/core/src/state.js +139 -2
- package/ui/core/src/themes/index.js +1 -1
- package/ui/react/src/components.js +1 -7
- package/ui/react/src/web-app.js +1719 -366
- package/web/auth/config.js +3 -0
- package/web/auth/index.js +8 -1
- package/web/auth/normalize/proxy.js +48 -0
- package/web/auth/providers/proxy.js +141 -0
- package/web/dist/assets/{abnfDiagram-VRR7QNED-LOLCtsns.js → abnfDiagram-VRR7QNED-DrJhXhSQ.js} +1 -1
- package/web/dist/assets/{arc-BE-jVDEi.js → arc-BEaG9DWd.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CdmUjwtj.js → architectureDiagram-ZJ3FMSHR-D9fc1Imx.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-NZ4TOa8L.js → blockDiagram-677ZJIJ3-rS5Eikvt.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-9ubX8uGA.js → c4Diagram-LMCZKHZV-HKLlX5cR.js} +1 -1
- package/web/dist/assets/channel-DRy7FvyF.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-DNDnjbP6.js → chunk-2Q5K7J3B-kq0TyLSB.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-iilfiffr.js → chunk-32BRIVSS-O-LMarT5.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BakOMFmf.js → chunk-5VM5RSS4-Di4h0CAu.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-BhluhGyZ.js → chunk-EX3LRPZG-C8rgqUOt.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-B6GKLlNb.js → chunk-JWPE2WC7-BSH41q4T.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-CXmt8msF.js → chunk-MOJQB5TN-BAFuQHAC.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-Bu58-SDr.js → chunk-RYQCIY6F-DAg6JOs7.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-AQdyoYHL.js → chunk-V7JOEXUC-DbVrT8BG.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DeM5iMsE.js → chunk-VR4S4FIN-C6f5_Grp.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DjGjajy1.js → chunk-XXDRQBXY-CqcCYpqA.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-C35RhB8X.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-C35RhB8X.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-BGi4_egJ.js → cose-bilkent-JH36ORCC-DJw04eFh.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CcgV1ve4.js → cynefin-VYW2F7L2-CpjwdEIU.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BRDEsbBG.js → cynefinDiagram-TSTJHNR4-RqEIWoT0.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-OjuC8dJB.js → dagre-VKFMJZFB-C1eGQNK2.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-CGdROWYh.js → diagram-FQU43EPY-BODLQ7dz.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-B5J0EUfS.js → diagram-G47NLZAW-DlhmukMC.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-zmb4-cP2.js → diagram-NH7WQ7WH-Ba_jZoCN.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-j514bgaH.js → diagram-OA4YK3LP-DnSiAJBG.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-h5G459mZ.js → diagram-WEI45ONY-B3KoQ7_W.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-GnMgbuFk.js → ebnfDiagram-CCIWWBDH-ChnFc5gf.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-BFgH3UQp.js → erDiagram-Q63AITRT-D2KkJXLZ.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-BsbQ_VXV.js → flowDiagram-23GEKE2U-CmybZufj.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-C_fjo2sf.js → ganttDiagram-NO4QXBWP-BzPurW_v.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BUoXrAI9.js → gitGraphDiagram-IHSO6WYX-DAbXl4gO.js} +1 -1
- package/web/dist/assets/{index-ONJimzcx.js → index-CRY94nFp.js} +6 -6
- package/web/dist/assets/index-DtL0lbis.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CJiSANyk.js → infoDiagram-FWYZ7A6U-C6qGjNA1.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-BKoQ-a2c.js → ishikawaDiagram-FXEZZL3T-D2d1tO-9.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-Db6Crcd3.js → journeyDiagram-5HDEW3XC-fP13w1b7.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-B9GaB0UJ.js → kanban-definition-HUTT4EX6-BJfWAXZ0.js} +1 -1
- package/web/dist/assets/{linear-BXwRb8MN.js → linear-CltApkHf.js} +1 -1
- package/web/dist/assets/{mermaid.core-DwzXN6up.js → mermaid.core-B1Dwetk3.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-DKC_C_OY.js → mindmap-definition-LN4V7U3C-CdRUD7LF.js} +1 -1
- package/web/dist/assets/{papaparse.min-Cm1Pr54l.js → papaparse.min-C1y3wBH1.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-IeawbZPt.js → pegDiagram-2B236MQR-CeQkqN6Q.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-BOdsYBt8.js → pieDiagram-ENE6RG2P-Dqgc87jA.js} +1 -1
- package/web/dist/assets/pilotswarm-DayYdi39.js +96 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-Cr5yoN7C.js → quadrantDiagram-ABIIQ3AL-CD7f-w6m.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-E7wuFBuk.js → railroadDiagram-RFXS5EU6-CcsAijSC.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-sZCKiuDs.js → requirementDiagram-TGXJPOKE-BkzF80pL.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-B_EWziUR.js → sankeyDiagram-HTMAVEWB-D9XOFw7Y.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-D2mN7onX.js → sequenceDiagram-DBY2YBRQ-BFfaS_Sz.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CEL9Ly2T.js → sizeCapture-X5ZJPWSS-CDJV16tP.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-C7ejJoeN.js → stateDiagram-2N3HPSRC-C0E-MPXO.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BMqXeMJu.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-Ck3pbFxk.js → swimlanes-5IMT3BWC-n5Vsv7n9.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CGl4gRnH.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-K3H8Yuyr.js → timeline-definition-FHXFAJF6-DW2gV8-n.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-C3NKjSv9.js → vennDiagram-L72KCM5P-D9dbDm08.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CeeVuFiO.js → wardleyDiagram-EHGQE667-CIcG7fiC.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-CF-vCvEq.js → xychartDiagram-FW5EYKEG-elEIFsc9.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/runtime.js +20 -1
- package/web/dist/assets/channel-BhHZnKdH.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-CM_bKBKz.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CM_bKBKz.js +0 -1
- package/web/dist/assets/index-CnH1Yv_d.css +0 -1
- package/web/dist/assets/pilotswarm-kCQFVjJm.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-UaVDt_gU.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CqO_fF5t.js +0 -8
package/ui/core/src/selectors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { INSPECTOR_TABS, FOCUS_REGIONS } from "./commands.js";
|
|
2
|
+
import { canvasKey as canvasSlotKey, parseCanvasKey } from "./state.js";
|
|
2
3
|
import { isManuallyOrderableSession } from "./session-tree.js";
|
|
3
4
|
import {
|
|
4
5
|
createSplashCard,
|
|
@@ -106,11 +107,6 @@ function sessionStatusLabelForVisualStatus(session, status) {
|
|
|
106
107
|
return status;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
function getSessionSummaryStatusLabel(session) {
|
|
110
|
-
const status = getSessionVisualStatus(session);
|
|
111
|
-
if (!status || status === "unknown") return "";
|
|
112
|
-
return sessionStatusLabelForVisualStatus(session, status);
|
|
113
|
-
}
|
|
114
110
|
|
|
115
111
|
/**
|
|
116
112
|
* The label + timestamp the session-detail box shows, as ONE derivation shared
|
|
@@ -1130,6 +1126,10 @@ export function selectSessionRows(state) {
|
|
|
1130
1126
|
// built under one branding to a caller using another, and the root
|
|
1131
1127
|
// session's name would flip between them.
|
|
1132
1128
|
state.branding?.title,
|
|
1129
|
+
// The canvas marker below reads these. Without them the memo hands
|
|
1130
|
+
// back a row built before the draw and the marker never appears.
|
|
1131
|
+
state.canvas?.bySessionId?.[entry.sessionId],
|
|
1132
|
+
state.canvas?.prefs?.[entry.sessionId],
|
|
1133
1133
|
];
|
|
1134
1134
|
const cached = memo.get(entry.sessionId);
|
|
1135
1135
|
if (cached && sameRowDeps(cached.deps, deps)) return cached.row;
|
|
@@ -1165,6 +1165,15 @@ export function selectSessionRows(state) {
|
|
|
1165
1165
|
: null,
|
|
1166
1166
|
orderable: isManuallyOrderableSession(session),
|
|
1167
1167
|
orderContainer: manualOrderContainerKey(session, pinned),
|
|
1168
|
+
// Canvas state for THIS row. The toolbar badge answers the same
|
|
1169
|
+
// question for the active session only; the list has to answer it
|
|
1170
|
+
// for every session at once, which is the point — a canvas drawn
|
|
1171
|
+
// in a session you are not looking at is otherwise invisible.
|
|
1172
|
+
//
|
|
1173
|
+
// null no canvas, or one that was cleared
|
|
1174
|
+
// "canvas" a canvas exists and nothing is new
|
|
1175
|
+
// "unseen" it changed since this user last looked at it
|
|
1176
|
+
canvasMark: sessionCanvasMark(state, entry.sessionId),
|
|
1168
1177
|
};
|
|
1169
1178
|
memo.set(entry.sessionId, { deps, row });
|
|
1170
1179
|
return row;
|
|
@@ -1176,7 +1185,7 @@ export function selectSessionRows(state) {
|
|
|
1176
1185
|
const ownerSearchText = effectiveOwner
|
|
1177
1186
|
? `${ownerDisplayName(effectiveOwner, "")} ${effectiveOwner.email || ""}`
|
|
1178
1187
|
: "";
|
|
1179
|
-
const summarySearchText =
|
|
1188
|
+
const summarySearchText = "";
|
|
1180
1189
|
return matchesSearchQuery(row.text, query)
|
|
1181
1190
|
|| matchesSearchQuery(row.sessionId, query)
|
|
1182
1191
|
|| matchesSearchQuery(ownerSearchText, query)
|
|
@@ -1557,7 +1566,7 @@ export function selectActiveChat(state) {
|
|
|
1557
1566
|
.replace(/[\r\n]+/g, " ")
|
|
1558
1567
|
.trim() || " ";
|
|
1559
1568
|
const memberRows = memberSessions.map((candidate) => (
|
|
1560
|
-
`| ${markdownCell(candidate.title || candidate.sessionId)} | ${markdownCell(candidate.status || "unknown")}
|
|
1569
|
+
`| ${markdownCell(candidate.title || candidate.sessionId)} | ${markdownCell(candidate.status || "unknown")} |`
|
|
1561
1570
|
));
|
|
1562
1571
|
const lines = [
|
|
1563
1572
|
`# ${session.title || session.groupId}`,
|
|
@@ -1575,8 +1584,8 @@ export function selectActiveChat(state) {
|
|
|
1575
1584
|
"## Members",
|
|
1576
1585
|
...(memberRows.length > 0
|
|
1577
1586
|
? [
|
|
1578
|
-
"| Session | Status |
|
|
1579
|
-
"
|
|
1587
|
+
"| Session | Status |",
|
|
1588
|
+
"|---|---|",
|
|
1580
1589
|
...memberRows,
|
|
1581
1590
|
]
|
|
1582
1591
|
: ["_No top-level members._"]),
|
|
@@ -1589,9 +1598,6 @@ export function selectActiveChat(state) {
|
|
|
1589
1598
|
createdAt: session.updatedAt || Date.now(),
|
|
1590
1599
|
}];
|
|
1591
1600
|
}
|
|
1592
|
-
if (state.ui?.chatViewMode === "summary") {
|
|
1593
|
-
return buildSessionSummaryCards(session);
|
|
1594
|
-
}
|
|
1595
1601
|
const history = state.history.bySessionId.get(sessionId);
|
|
1596
1602
|
const chat = history?.chat || [];
|
|
1597
1603
|
const pendingQuestionMessage = session?.pendingQuestion?.question
|
|
@@ -1618,123 +1624,6 @@ export function selectActiveChat(state) {
|
|
|
1618
1624
|
return messages;
|
|
1619
1625
|
}
|
|
1620
1626
|
|
|
1621
|
-
function buildSessionSummaryCards(session) {
|
|
1622
|
-
const normalizeSummaryMarkdownText = (value) => {
|
|
1623
|
-
const source = String(value || "");
|
|
1624
|
-
if (!source.includes("\\n")) return source;
|
|
1625
|
-
return source
|
|
1626
|
-
.replace(/\\\\n/g, "\n")
|
|
1627
|
-
.replace(/\\n/g, "\n");
|
|
1628
|
-
};
|
|
1629
|
-
|
|
1630
|
-
const summaryState = session?.summaryState && typeof session.summaryState === "object" && !Array.isArray(session.summaryState)
|
|
1631
|
-
? session.summaryState
|
|
1632
|
-
: null;
|
|
1633
|
-
const stateJson = summaryState?.state && typeof summaryState.state === "object" && !Array.isArray(summaryState.state)
|
|
1634
|
-
? summaryState.state
|
|
1635
|
-
: null;
|
|
1636
|
-
const title = buildSessionDisplayTitle(session) || session?.title || session?.sessionId || "Session";
|
|
1637
|
-
const lines = [];
|
|
1638
|
-
lines.push(`# ${title}`);
|
|
1639
|
-
lines.push("");
|
|
1640
|
-
|
|
1641
|
-
// Compact metadata header — status / intent / updated on their own
|
|
1642
|
-
// lines, each as a bold-labeled value. Skipped when empty.
|
|
1643
|
-
const metaPairs = [];
|
|
1644
|
-
const statusLabel = getSessionSummaryStatusLabel(session);
|
|
1645
|
-
if (statusLabel) metaPairs.push(["Status", statusLabel]);
|
|
1646
|
-
if (summaryState?.intent) metaPairs.push(["Intent", String(summaryState.intent)]);
|
|
1647
|
-
if (session?.summaryUpdatedAt) {
|
|
1648
|
-
const ts = formatTimestamp(session.summaryUpdatedAt);
|
|
1649
|
-
if (ts) metaPairs.push(["Updated", ts]);
|
|
1650
|
-
}
|
|
1651
|
-
for (const [label, value] of metaPairs) {
|
|
1652
|
-
lines.push(`**${label}:** ${value}`);
|
|
1653
|
-
}
|
|
1654
|
-
if (metaPairs.length > 0) lines.push("");
|
|
1655
|
-
|
|
1656
|
-
// Free-form summary paragraph (uses the LLM-authored summary if present,
|
|
1657
|
-
// otherwise the short summary the CLI rolls up).
|
|
1658
|
-
const narrative = normalizeSummaryMarkdownText(summaryState?.summary || session?.shortSummary || "").trim();
|
|
1659
|
-
if (narrative) {
|
|
1660
|
-
lines.push(narrative);
|
|
1661
|
-
lines.push("");
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
// Structured state — each key:value renders as a bold-labeled bullet.
|
|
1665
|
-
// Lists/objects are rendered as nested bullets so arrays of "progress"
|
|
1666
|
-
// steps don't appear as JSON blobs.
|
|
1667
|
-
if (stateJson) {
|
|
1668
|
-
const entries = Object.entries(stateJson)
|
|
1669
|
-
.filter(([, value]) => value !== null && value !== undefined && !(typeof value === "string" && !value.trim()));
|
|
1670
|
-
if (entries.length > 0) {
|
|
1671
|
-
lines.push("## State");
|
|
1672
|
-
for (const [key, value] of entries) {
|
|
1673
|
-
if (Array.isArray(value)) {
|
|
1674
|
-
if (value.length === 0) {
|
|
1675
|
-
lines.push(`- **${key}:** _(empty)_`);
|
|
1676
|
-
} else {
|
|
1677
|
-
lines.push(`- **${key}:**`);
|
|
1678
|
-
for (const item of value.slice(0, 16)) {
|
|
1679
|
-
const text = typeof item === "string"
|
|
1680
|
-
? normalizeSummaryMarkdownText(item)
|
|
1681
|
-
: JSON.stringify(item);
|
|
1682
|
-
lines.push(` - ${text}`);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
} else if (value && typeof value === "object") {
|
|
1686
|
-
lines.push(`- **${key}:**`);
|
|
1687
|
-
for (const [subKey, subValue] of Object.entries(value).slice(0, 16)) {
|
|
1688
|
-
const text = typeof subValue === "string"
|
|
1689
|
-
? normalizeSummaryMarkdownText(subValue)
|
|
1690
|
-
: JSON.stringify(subValue);
|
|
1691
|
-
lines.push(` - **${subKey}:** ${text}`);
|
|
1692
|
-
}
|
|
1693
|
-
} else {
|
|
1694
|
-
lines.push(`- **${key}:** ${normalizeSummaryMarkdownText(String(value))}`);
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
lines.push("");
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
// Structured tail sections. Each section renders only if it actually
|
|
1702
|
-
// has items; skipped sections do not occupy vertical space.
|
|
1703
|
-
for (const [heading, field] of [["Blockers", "blockers"], ["Open Questions", "openQuestions"], ["Next Actions", "nextActions"]]) {
|
|
1704
|
-
const items = Array.isArray(summaryState?.[field]) ? summaryState[field].filter(Boolean).slice(0, 12) : [];
|
|
1705
|
-
if (items.length === 0) continue;
|
|
1706
|
-
lines.push(`## ${heading}`);
|
|
1707
|
-
for (const item of items) {
|
|
1708
|
-
lines.push(`- ${typeof item === "string" ? normalizeSummaryMarkdownText(item) : JSON.stringify(item)}`);
|
|
1709
|
-
}
|
|
1710
|
-
lines.push("");
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
if (!summaryState && !narrative) {
|
|
1714
|
-
lines.push("_No summary has been written yet for this session._");
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
// Collapse trailing blank lines so the pane doesn't render empty rows
|
|
1718
|
-
// at the bottom.
|
|
1719
|
-
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1720
|
-
|
|
1721
|
-
return [{
|
|
1722
|
-
id: `summary:${session?.sessionId || "none"}`,
|
|
1723
|
-
role: "system",
|
|
1724
|
-
noChrome: true,
|
|
1725
|
-
text: lines.join("\n"),
|
|
1726
|
-
createdAt: session?.summaryUpdatedAt || session?.updatedAt || Date.now(),
|
|
1727
|
-
}];
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
export function selectActiveOutboxMessages(state) {
|
|
1731
|
-
const sessionId = state.sessions.activeSessionId;
|
|
1732
|
-
if (!sessionId) return [];
|
|
1733
|
-
return Array.isArray(state.outbox?.bySessionId?.[sessionId])
|
|
1734
|
-
? state.outbox.bySessionId[sessionId].map((item) => buildPendingOutboxMessage(sessionId, item)).filter(Boolean)
|
|
1735
|
-
: [];
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
1627
|
function prefixRuns(text, color = "gray", options = {}) {
|
|
1739
1628
|
return [{
|
|
1740
1629
|
text,
|
|
@@ -2593,7 +2482,6 @@ function liveActivityPhaseText(item) {
|
|
|
2593
2482
|
// so visibility is governed by session status rather than transcript role.
|
|
2594
2483
|
// `options.spinnerFrame` animates the marker.
|
|
2595
2484
|
export function selectLiveActivityLines(state, options = {}) {
|
|
2596
|
-
if (state?.ui?.chatViewMode === "summary") return [];
|
|
2597
2485
|
const session = selectActiveSession(state);
|
|
2598
2486
|
if (!session || session.isGroup) return [];
|
|
2599
2487
|
const status = String(session?.status || "").toLowerCase();
|
|
@@ -2665,7 +2553,7 @@ export function selectLiveActivityLines(state, options = {}) {
|
|
|
2665
2553
|
// Keyed on identities rather than fields on purpose: selectActiveChat is NOT a
|
|
2666
2554
|
// pure function of the chat array — it also appends a pending-question message,
|
|
2667
2555
|
// an answered-question message and a session-error message, and branches on
|
|
2668
|
-
//
|
|
2556
|
+
// branding. Anything derived from `session` therefore has
|
|
2669
2557
|
// to invalidate when `session` does, which identity comparison gives for free.
|
|
2670
2558
|
//
|
|
2671
2559
|
// `byId` is in the key for two reasons found by adversarial review, both of
|
|
@@ -2695,7 +2583,6 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
|
2695
2583
|
session: memoSessionId ? state?.sessions?.byId?.[memoSessionId] ?? null : null,
|
|
2696
2584
|
byId: state?.sessions?.byId ?? null,
|
|
2697
2585
|
chat: state?.history?.bySessionId?.get?.(memoSessionId)?.chat ?? null,
|
|
2698
|
-
viewMode: state?.ui?.chatViewMode ?? null,
|
|
2699
2586
|
branding: state?.branding ?? null,
|
|
2700
2587
|
principal: state?.auth?.principal ?? null,
|
|
2701
2588
|
maxWidth,
|
|
@@ -2754,6 +2641,10 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
|
2754
2641
|
lines.push(buildRegenRefusedLine(message, maxWidth));
|
|
2755
2642
|
} else if (message?.kind === "regen-failed") {
|
|
2756
2643
|
lines.push(buildRegenFailedLine(message, maxWidth));
|
|
2644
|
+
} else if (message?.kind === "canvas-update") {
|
|
2645
|
+
lines.push(buildCanvasUpdateLine(message, message.sessionId || memoSessionId));
|
|
2646
|
+
} else if (message?.kind === "canvas-action") {
|
|
2647
|
+
lines.push(buildCanvasActionLine(message));
|
|
2757
2648
|
} else {
|
|
2758
2649
|
const messageLines = buildChatMessageLines(message, maxWidth, buildOptions);
|
|
2759
2650
|
appendChatBlockLines(lines, messageLines);
|
|
@@ -2770,6 +2661,158 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
|
2770
2661
|
return memoize(lines.length > 0 ? lines : [{ text: "No messages yet.", color: "gray" }]);
|
|
2771
2662
|
}
|
|
2772
2663
|
|
|
2664
|
+
/**
|
|
2665
|
+
* The canvas-update transcript line — carried by BOTH hosts, rendered by one.
|
|
2666
|
+
*
|
|
2667
|
+
* The line is flagged `canvasUpdate: true`; the portal's chat renderer skips
|
|
2668
|
+
* flagged lines outright (there, the canvas pane updating is the whole
|
|
2669
|
+
* signal), while the TUI renders it as an ordinary artifact link, press-a
|
|
2670
|
+
* affordance and all — a host that cannot render the canvas needs the link.
|
|
2671
|
+
*/
|
|
2672
|
+
/**
|
|
2673
|
+
* Everything the Canvas toggle and pane need about the ACTIVE session's
|
|
2674
|
+
* canvas, in one read: whether one exists, its revision and caption, and
|
|
2675
|
+
* whether the yellow unseen-changes badge should light
|
|
2676
|
+
* (latestRev > lastViewedRev, and only while the canvas is not on screen).
|
|
2677
|
+
*/
|
|
2678
|
+
/**
|
|
2679
|
+
* The canvas marker for one session row.
|
|
2680
|
+
*
|
|
2681
|
+
* Same rules selectCanvasView applies to the active session, minus the
|
|
2682
|
+
* on-screen check: a row's marker describes the SESSION, not the layout, so it
|
|
2683
|
+
* does not care which column happens to be open.
|
|
2684
|
+
*
|
|
2685
|
+
* A cleared canvas (draw_canvas("") -> sizeBytes 0) no longer exists. Unknown
|
|
2686
|
+
* size (null, from a degraded snapshot) is presumed drawn — a marker that is
|
|
2687
|
+
* occasionally early beats one that silently never shows.
|
|
2688
|
+
*/
|
|
2689
|
+
export function sessionCanvasMark(state, sessionId) {
|
|
2690
|
+
if (!sessionId) return null;
|
|
2691
|
+
// Aggregate across the session's slots: ANY unseen slot lights the dot;
|
|
2692
|
+
// any drawn slot at all marks the row. Slot 1 is the bare id key.
|
|
2693
|
+
let mark = null;
|
|
2694
|
+
for (let slot = 1; slot <= 5; slot += 1) {
|
|
2695
|
+
const key = canvasSlotKey(sessionId, slot);
|
|
2696
|
+
const entry = state?.canvas?.bySessionId?.[key];
|
|
2697
|
+
const prefs = state?.canvas?.prefs?.[key];
|
|
2698
|
+
if (!entry) {
|
|
2699
|
+
// No live state loaded (cold page). canvasPrefs persist per user,
|
|
2700
|
+
// so a slot this user has ever VIEWED still marks — plain, never
|
|
2701
|
+
// "unseen": whether it changed since is unknowable client-side.
|
|
2702
|
+
if ((prefs?.lastViewedRev || 0) > 0) mark = mark || "canvas";
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
const latestRev = entry.latestRev || 0;
|
|
2706
|
+
if (latestRev <= 0 || entry.sizeBytes === 0) continue;
|
|
2707
|
+
const unseen = latestRev > (prefs?.lastViewedRev || 0)
|
|
2708
|
+
|| (entry.latestDataRev || 0) > (prefs?.lastViewedDataRev || 0);
|
|
2709
|
+
if (unseen) return "unseen";
|
|
2710
|
+
mark = "canvas";
|
|
2711
|
+
}
|
|
2712
|
+
return mark;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
export function selectCanvasView(state) {
|
|
2716
|
+
const sessionId = state?.sessions?.activeSessionId || null;
|
|
2717
|
+
// The pane shows ONE slot at a time. An out-of-range or never-drawn slot
|
|
2718
|
+
// falls back to 1 so a stale selection can never strand the pane.
|
|
2719
|
+
const wantedSlot = Number(state?.ui?.canvasSlot) || 1;
|
|
2720
|
+
const bySessionId = state?.canvas?.bySessionId || {};
|
|
2721
|
+
// Every drawn slot of the active session, for the selector and arrows.
|
|
2722
|
+
const slots = [];
|
|
2723
|
+
if (sessionId) {
|
|
2724
|
+
for (let slot = 1; slot <= 5; slot += 1) {
|
|
2725
|
+
const e = bySessionId[canvasSlotKey(sessionId, slot)];
|
|
2726
|
+
if (!e || (e.latestRev || 0) <= 0 || e.sizeBytes === 0) continue;
|
|
2727
|
+
const p = state?.canvas?.prefs?.[canvasSlotKey(sessionId, slot)];
|
|
2728
|
+
slots.push({
|
|
2729
|
+
slot,
|
|
2730
|
+
name: typeof e.name === "string" ? e.name : "",
|
|
2731
|
+
latestRev: e.latestRev || 0,
|
|
2732
|
+
unseen: (e.latestRev || 0) > (p?.lastViewedRev || 0)
|
|
2733
|
+
|| (e.latestDataRev || 0) > (p?.lastViewedDataRev || 0),
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
const slot = slots.some((s2) => s2.slot === wantedSlot) ? wantedSlot : 1;
|
|
2738
|
+
const key = sessionId ? canvasSlotKey(sessionId, slot) : null;
|
|
2739
|
+
const entry = key ? bySessionId[key] : null;
|
|
2740
|
+
const prefs = key ? state?.canvas?.prefs?.[key] : null;
|
|
2741
|
+
const latestRev = entry?.latestRev || 0;
|
|
2742
|
+
const lastViewedRev = prefs?.lastViewedRev || 0;
|
|
2743
|
+
const latestDataRev = entry?.latestDataRev || 0;
|
|
2744
|
+
const lastViewedDataRev = prefs?.lastViewedDataRev || 0;
|
|
2745
|
+
// Read from canvasOpen, not the retired rightPaneMode enum. Diagnostics
|
|
2746
|
+
// moving to its own column means "not canvas" no longer implies "panes",
|
|
2747
|
+
// and this drives the unseen badge, which must light whenever the canvas
|
|
2748
|
+
// is off screen.
|
|
2749
|
+
const mode = state?.ui?.canvasOpen ? "canvas" : "panes";
|
|
2750
|
+
// A cleared canvas (draw_canvas("") → sizeBytes 0) no longer exists for
|
|
2751
|
+
// affordance purposes: no phone tab, no badge, blank teaching state.
|
|
2752
|
+
// Unknown size (null — snapshot degrade) is presumed drawn.
|
|
2753
|
+
const exists = latestRev > 0 && entry?.sizeBytes !== 0;
|
|
2754
|
+
return {
|
|
2755
|
+
sessionId,
|
|
2756
|
+
slot,
|
|
2757
|
+
slots,
|
|
2758
|
+
name: typeof entry?.name === "string" ? entry.name : "",
|
|
2759
|
+
mode,
|
|
2760
|
+
exists,
|
|
2761
|
+
latestRev,
|
|
2762
|
+
note: entry?.note || "",
|
|
2763
|
+
sizeBytes: entry?.sizeBytes ?? null,
|
|
2764
|
+
snapshotLoaded: Boolean(entry?.snapshotLoaded),
|
|
2765
|
+
optedOut: Boolean(prefs?.optedOut),
|
|
2766
|
+
lastViewedRev,
|
|
2767
|
+
flipSeq: state?.canvas?.flipSeq || 0,
|
|
2768
|
+
responseContract: entry?.responseContract || null,
|
|
2769
|
+
latestDataRev,
|
|
2770
|
+
lastViewedDataRev,
|
|
2771
|
+
dataPayload: entry?.dataPayload || null,
|
|
2772
|
+
unseen: exists && (latestRev > lastViewedRev || latestDataRev > lastViewedDataRev) && mode !== "canvas",
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
/**
|
|
2777
|
+
* A validated canvas response on its way to the agent. Flagged so the PORTAL
|
|
2778
|
+
* chat skips it (the canvas redraw is the visible half of the loop) while the
|
|
2779
|
+
* TUI — which renders no canvas and would otherwise show a silent gap in the
|
|
2780
|
+
* conversation — gets a compact dim line.
|
|
2781
|
+
*/
|
|
2782
|
+
function buildCanvasActionLine(message) {
|
|
2783
|
+
const dataKeys = message?.data && typeof message.data === "object" ? Object.keys(message.data) : [];
|
|
2784
|
+
const detail = dataKeys.length === 1 && typeof message.data[dataKeys[0]] === "string"
|
|
2785
|
+
? message.data[dataKeys[0]].slice(0, 120)
|
|
2786
|
+
: (dataKeys.length ? JSON.stringify(message.data).slice(0, 120) : "");
|
|
2787
|
+
return {
|
|
2788
|
+
runs: [
|
|
2789
|
+
{ text: "[canvas] ", color: "cyan", bold: true },
|
|
2790
|
+
{ text: `${message.action || "action"}`, color: "white" },
|
|
2791
|
+
...(detail ? [{ text: ` — ${detail}`, color: "gray" }] : []),
|
|
2792
|
+
],
|
|
2793
|
+
canvasAction: true,
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
function buildCanvasUpdateLine(message, sessionId) {
|
|
2798
|
+
const rev = Number(message?.rev) || 0;
|
|
2799
|
+
const note = typeof message?.note === "string" && message.note.trim() ? ` — ${message.note.trim()}` : "";
|
|
2800
|
+
const runs = [
|
|
2801
|
+
{ text: "[canvas] ", color: "cyan", bold: true },
|
|
2802
|
+
{ text: `rev ${rev}${note} `, color: "white" },
|
|
2803
|
+
];
|
|
2804
|
+
if (sessionId) {
|
|
2805
|
+
runs.push({
|
|
2806
|
+
text: "[artifact: canvas.html]",
|
|
2807
|
+
color: "cyan",
|
|
2808
|
+
underline: true,
|
|
2809
|
+
href: `artifact://${sessionId}/canvas.html`,
|
|
2810
|
+
});
|
|
2811
|
+
runs.push({ text: " (press a to download)", color: "gray" });
|
|
2812
|
+
}
|
|
2813
|
+
return { runs, canvasUpdate: true };
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2773
2816
|
function buildRuleLine(label, color, maxWidth) {
|
|
2774
2817
|
const safeWidth = Math.max(24, Number(maxWidth) || 80);
|
|
2775
2818
|
const room = safeWidth - label.length;
|
|
@@ -2825,6 +2868,14 @@ function buildRegenFailedLine(message, maxWidth) {
|
|
|
2825
2868
|
return buildRuleLine(label, "red", maxWidth);
|
|
2826
2869
|
}
|
|
2827
2870
|
|
|
2871
|
+
export function selectActiveOutboxMessages(state) {
|
|
2872
|
+
const sessionId = state.sessions.activeSessionId;
|
|
2873
|
+
if (!sessionId) return [];
|
|
2874
|
+
return Array.isArray(state.outbox?.bySessionId?.[sessionId])
|
|
2875
|
+
? state.outbox.bySessionId[sessionId].map((item) => buildPendingOutboxMessage(sessionId, item)).filter(Boolean)
|
|
2876
|
+
: [];
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2828
2879
|
export function selectOutboxOverlayLines(state, maxWidth = 80, options = {}) {
|
|
2829
2880
|
const messages = selectActiveOutboxMessages(state);
|
|
2830
2881
|
if (!messages || messages.length === 0) return [];
|
|
@@ -2956,9 +3007,7 @@ export function selectChatPaneChrome(state, options = {}) {
|
|
|
2956
3007
|
const outboxItems = Array.isArray(state.outbox?.bySessionId?.[session.sessionId])
|
|
2957
3008
|
? state.outbox.bySessionId[session.sessionId]
|
|
2958
3009
|
: [];
|
|
2959
|
-
const progress =
|
|
2960
|
-
? null
|
|
2961
|
-
: buildLiveProgressState(session, history, history?.chat || [], outboxItems);
|
|
3010
|
+
const progress = buildLiveProgressState(session, history, history?.chat || [], outboxItems);
|
|
2962
3011
|
|
|
2963
3012
|
// Border top-right: status · model · context (mirrors the session-row meta),
|
|
2964
3013
|
// with the live turn-progress label appended while a turn is running.
|
|
@@ -4266,11 +4315,9 @@ export function selectStatusBar(state) {
|
|
|
4266
4315
|
right: "up/down move · space toggle · ctrl-g group · c cancel · d complete · D hard delete · V/esc exit",
|
|
4267
4316
|
};
|
|
4268
4317
|
}
|
|
4269
|
-
const chatViewMode = state.ui?.chatViewMode === "summary" ? "summary" : "transcript";
|
|
4270
|
-
const chatViewHint = chatViewMode === "summary" ? "s transcript" : "s summary";
|
|
4271
4318
|
const hints = {
|
|
4272
4319
|
[FOCUS_REGIONS.SESSIONS]: `up/down switch · ctrl-u/ctrl-d page · ctrl-g move group · f filter · P pin · V select · d done · D delete · r refresh · t title · v visibility · S share · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane · p prompt`,
|
|
4273
|
-
[FOCUS_REGIONS.CHAT]:
|
|
4320
|
+
[FOCUS_REGIONS.CHAT]: `j/k scroll · ctrl-u/ctrl-d page · e older history · g/G top/bottom · d done · ${fullscreenHint} · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane · p prompt`,
|
|
4274
4321
|
[FOCUS_REGIONS.INSPECTOR]: state.ui.inspectorTab === "logs"
|
|
4275
4322
|
? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · t tail · f filter · ${fullscreenHint} · left/right tab · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane`
|
|
4276
4323
|
: state.ui.inspectorTab === "stats"
|
|
@@ -4855,13 +4902,8 @@ function buildSequenceStatsLines(state, session, maxWidth) {
|
|
|
4855
4902
|
if (!body) body = "orchestration stats unavailable";
|
|
4856
4903
|
}
|
|
4857
4904
|
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
{ text: "Stats ", color: "cyan", bold: true },
|
|
4861
|
-
{ text: body, color: "white" },
|
|
4862
|
-
], Math.max(18, maxWidth))];
|
|
4863
|
-
}
|
|
4864
|
-
|
|
4905
|
+
// Narrow panes used to degrade to a single truncated line; a box whose
|
|
4906
|
+
// content wraps reads better on a phone and costs two rows of chrome.
|
|
4865
4907
|
return buildMessageCardLines({
|
|
4866
4908
|
title: "Stats",
|
|
4867
4909
|
body,
|
|
@@ -5884,8 +5926,10 @@ export function selectSessionGroupNameModal(state, maxWidth = 76) {
|
|
|
5884
5926
|
const value = String(modal.value || "");
|
|
5885
5927
|
const sessionCount = Array.isArray(modal.sessionIds) ? modal.sessionIds.length : 0;
|
|
5886
5928
|
const previewTitle = value.trim() || "…";
|
|
5929
|
+
const renaming = modal.mode === "rename";
|
|
5887
5930
|
return {
|
|
5888
|
-
title: modal.title || "New Group",
|
|
5931
|
+
title: modal.title || (renaming ? "Rename Group" : "New Group"),
|
|
5932
|
+
mode: renaming ? "rename" : "create",
|
|
5889
5933
|
value,
|
|
5890
5934
|
cursorIndex: Math.max(0, Math.min(Number(modal.cursorIndex) || 0, value.length)),
|
|
5891
5935
|
placeholder: "Type a group name",
|
|
@@ -5893,15 +5937,17 @@ export function selectSessionGroupNameModal(state, maxWidth = 76) {
|
|
|
5893
5937
|
helpLines: [
|
|
5894
5938
|
[
|
|
5895
5939
|
{ text: "Enter", color: "cyan", bold: true },
|
|
5896
|
-
{ text: " create and move ", color: "gray" },
|
|
5940
|
+
{ text: renaming ? " rename " : " create and move ", color: "gray" },
|
|
5897
5941
|
{ text: "Esc", color: "cyan", bold: true },
|
|
5898
5942
|
{ text: " cancel", color: "gray" },
|
|
5899
5943
|
],
|
|
5900
5944
|
[{ text: "", color: "gray" }],
|
|
5901
|
-
|
|
5945
|
+
renaming
|
|
5946
|
+
? [{ text: "Only the group's name changes; its sessions stay put.", color: "gray" }]
|
|
5947
|
+
: [{ text: `The new group will contain ${sessionCount} selected session${sessionCount === 1 ? "" : "s"}.`, color: "gray" }],
|
|
5902
5948
|
],
|
|
5903
5949
|
detailsLines: [
|
|
5904
|
-
[{ text: "New group: ", color: "gray" }, { text: previewTitle, color: "white", bold: true }],
|
|
5950
|
+
[{ text: renaming ? "Rename to: " : "New group: ", color: "gray" }, { text: previewTitle, color: "white", bold: true }],
|
|
5905
5951
|
[{ text: "Groups are containers only; session actions remain per-session.", color: "gray" }],
|
|
5906
5952
|
],
|
|
5907
5953
|
idealWidth: Math.min(Math.max(56, displayLength(previewTitle) + 18), maxWidth),
|
|
@@ -6328,6 +6374,20 @@ function formatSessionAge(ts) {
|
|
|
6328
6374
|
return `${Math.floor(days / 7)}w`;
|
|
6329
6375
|
}
|
|
6330
6376
|
|
|
6377
|
+
// Card-row timestamp: month, day, clock — no year, no "at", no timezone.
|
|
6378
|
+
// Cards live inside fitted boxes on a ~44-column phone; the full form is 29
|
|
6379
|
+
// characters and its padStart alignment dragged EVERY row in the table past
|
|
6380
|
+
// the card interior, wrapping all of them. Recency is what these rows convey;
|
|
6381
|
+
// the year and zone are noise there (the full form remains on Created/Updated).
|
|
6382
|
+
function formatCardTimestamp(ts) {
|
|
6383
|
+
if (!ts) return "—";
|
|
6384
|
+
const date = ts instanceof Date ? ts : new Date(ts);
|
|
6385
|
+
if (Number.isNaN(date.getTime())) return "—";
|
|
6386
|
+
const day = date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
6387
|
+
const clock = date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit" });
|
|
6388
|
+
return `${day}, ${clock}`;
|
|
6389
|
+
}
|
|
6390
|
+
|
|
6331
6391
|
function formatLocalTimestamp(ts) {
|
|
6332
6392
|
if (!ts) return "—";
|
|
6333
6393
|
const date = ts instanceof Date ? ts : new Date(ts);
|
|
@@ -6470,7 +6530,7 @@ function buildSessionStatsLines(state, session, maxWidth) {
|
|
|
6470
6530
|
["Cache Read", formatCompactNumber(summary.tokensCacheRead)],
|
|
6471
6531
|
["Cache Write", formatCompactNumber(summary.tokensCacheWrite)],
|
|
6472
6532
|
["Hit Ratio", hitRatioLabel],
|
|
6473
|
-
]),
|
|
6533
|
+
], { maxWidth: Math.max(12, w - 4) }),
|
|
6474
6534
|
width: w,
|
|
6475
6535
|
titleColor: "green",
|
|
6476
6536
|
borderColor: "gray",
|
|
@@ -6506,9 +6566,9 @@ function buildSessionStatsLines(state, session, maxWidth) {
|
|
|
6506
6566
|
["Dehydrations", String(summary.dehydrationCount)],
|
|
6507
6567
|
["Hydrations", String(summary.hydrationCount)],
|
|
6508
6568
|
["Lossy Handoffs", String(summary.lossyHandoffCount)],
|
|
6509
|
-
["Last Dehydrated", summary.lastDehydratedAt ?
|
|
6510
|
-
["Last Hydrated", summary.lastHydratedAt ?
|
|
6511
|
-
]),
|
|
6569
|
+
["Last Dehydrated", summary.lastDehydratedAt ? formatCardTimestamp(summary.lastDehydratedAt) : null],
|
|
6570
|
+
["Last Hydrated", summary.lastHydratedAt ? formatCardTimestamp(summary.lastHydratedAt) : null],
|
|
6571
|
+
], { maxWidth: Math.max(12, w - 4) }),
|
|
6512
6572
|
width: w,
|
|
6513
6573
|
titleColor: "yellow",
|
|
6514
6574
|
borderColor: "gray",
|
|
@@ -6636,8 +6696,17 @@ function formatKeyValueTable(pairs, options = {}) {
|
|
|
6636
6696
|
const labelWidth = rows.reduce((max, [label]) => Math.max(max, String(label).length), 0);
|
|
6637
6697
|
const valueWidth = rows.reduce((max, [, value]) => Math.max(max, String(value).length), 0);
|
|
6638
6698
|
const filler = " ".repeat(gap);
|
|
6699
|
+
// One oversized value must not wrap every row: the aligned grid pads each
|
|
6700
|
+
// row to labelWidth+gap+valueWidth, so a single 29-char timestamp used to
|
|
6701
|
+
// push ALL rows past a narrow card's interior. When the grid cannot fit,
|
|
6702
|
+
// keep label alignment but let each value sit flush after its label —
|
|
6703
|
+
// short rows stay whole and only the long row wraps.
|
|
6704
|
+
const maxWidth = Number(options.maxWidth);
|
|
6705
|
+
const aligned = !Number.isFinite(maxWidth) || labelWidth + gap + valueWidth <= maxWidth;
|
|
6639
6706
|
return rows
|
|
6640
|
-
.map(([label, value]) =>
|
|
6707
|
+
.map(([label, value]) => (aligned
|
|
6708
|
+
? `${String(label).padEnd(labelWidth)}${filler}${String(value).padStart(valueWidth)}`
|
|
6709
|
+
: `${String(label).padEnd(labelWidth)}${filler}${String(value)}`))
|
|
6641
6710
|
.join("\n");
|
|
6642
6711
|
}
|
|
6643
6712
|
|