pilotswarm 0.5.21 → 0.5.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +2 -1
  2. package/tui/src/node-sdk-transport.js +22 -5
  3. package/ui/core/src/controller.js +1 -1
  4. package/ui/core/src/reducer.js +3 -2
  5. package/ui/core/src/selectors.js +142 -2
  6. package/ui/core/src/state.js +1 -1
  7. package/ui/react/src/web-app.js +541 -22
  8. package/web/dist/assets/abnfDiagram-VRR7QNED-BHkTpRkx.js +1 -0
  9. package/web/dist/assets/arc-BOgZyEWA.js +1 -0
  10. package/web/dist/assets/architectureDiagram-ZJ3FMSHR-DeE8fDMy.js +36 -0
  11. package/web/dist/assets/blockDiagram-677ZJIJ3-CzhdxVEu.js +132 -0
  12. package/web/dist/assets/c4Diagram-LMCZKHZV-Cc6p_IKP.js +10 -0
  13. package/web/dist/assets/channel-DhhWyKRW.js +1 -0
  14. package/web/dist/assets/chunk-2Q5K7J3B-BCo94LTM.js +1 -0
  15. package/web/dist/assets/chunk-32BRIVSS-DaklJ-6i.js +1 -0
  16. package/web/dist/assets/chunk-5VM5RSS4-BN_wg2pr.js +15 -0
  17. package/web/dist/assets/chunk-EX3LRPZG-DSFFLUoL.js +231 -0
  18. package/web/dist/assets/chunk-JWPE2WC7-DB4dAjza.js +1 -0
  19. package/web/dist/assets/chunk-MOJQB5TN-CRM5eGQS.js +88 -0
  20. package/web/dist/assets/chunk-RYQCIY6F-BFlkjazU.js +1 -0
  21. package/web/dist/assets/chunk-V7JOEXUC-Bnfh4AAv.js +206 -0
  22. package/web/dist/assets/chunk-VR4S4FIN-CQc4_qVM.js +1 -0
  23. package/web/dist/assets/chunk-XXDRQBXY-CZivyoDm.js +1 -0
  24. package/web/dist/assets/classDiagram-OUVF2IWQ-CQBxGpio.js +1 -0
  25. package/web/dist/assets/classDiagram-v2-EOCWNBFH-CQBxGpio.js +1 -0
  26. package/web/dist/assets/cose-bilkent-JH36ORCC-BUibhTD-.js +1 -0
  27. package/web/dist/assets/cynefin-VYW2F7L2-CRkjg5zF.js +166 -0
  28. package/web/dist/assets/cynefinDiagram-TSTJHNR4-BoeQn4Um.js +62 -0
  29. package/web/dist/assets/cytoscape.esm-D3_iZ_3b.js +321 -0
  30. package/web/dist/assets/dagre-VKFMJZFB-DGo7-PY5.js +4 -0
  31. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  32. package/web/dist/assets/diagram-FQU43EPY-DMXd2bdO.js +3 -0
  33. package/web/dist/assets/diagram-G47NLZAW-CkYHblMZ.js +24 -0
  34. package/web/dist/assets/diagram-NH7WQ7WH-C9iwEe4T.js +24 -0
  35. package/web/dist/assets/diagram-OA4YK3LP-DpYLrfwb.js +30 -0
  36. package/web/dist/assets/diagram-WEI45ONY-BvrBukHb.js +41 -0
  37. package/web/dist/assets/ebnfDiagram-CCIWWBDH-DGl1r3zR.js +1 -0
  38. package/web/dist/assets/erDiagram-Q63AITRT-CoP5DBSb.js +85 -0
  39. package/web/dist/assets/flowDiagram-23GEKE2U-DVHtyYyr.js +156 -0
  40. package/web/dist/assets/ganttDiagram-NO4QXBWP-_JyR78yC.js +292 -0
  41. package/web/dist/assets/gitGraphDiagram-IHSO6WYX-Bj7sh0Db.js +106 -0
  42. package/web/dist/assets/graph-DOmOIIwC.js +1 -0
  43. package/web/dist/assets/{index-BDMVG8w6.js → index-CB6NfSZb.js} +3 -3
  44. package/web/dist/assets/{index-CxkLM8_a.css → index-SmXlZ1eu.css} +1 -1
  45. package/web/dist/assets/infoDiagram-FWYZ7A6U-BrvMeRmA.js +2 -0
  46. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  47. package/web/dist/assets/ishikawaDiagram-FXEZZL3T-lNDujpEC.js +70 -0
  48. package/web/dist/assets/journeyDiagram-5HDEW3XC-yoD2QRni.js +139 -0
  49. package/web/dist/assets/kanban-definition-HUTT4EX6-DtyMlypg.js +89 -0
  50. package/web/dist/assets/katex-HP8lGamR.js +257 -0
  51. package/web/dist/assets/layout-D-LzfAck.js +1 -0
  52. package/web/dist/assets/linear-DXPbXF9v.js +1 -0
  53. package/web/dist/assets/map-DxJ2ADlA.js +1 -0
  54. package/web/dist/assets/mermaid.core-DCEXqVUL.js +308 -0
  55. package/web/dist/assets/mindmap-definition-LN4V7U3C-DpKHDJFg.js +96 -0
  56. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  57. package/web/dist/assets/pegDiagram-2B236MQR-mxFI_8qh.js +1 -0
  58. package/web/dist/assets/pieDiagram-ENE6RG2P-sUWLrwMG.js +39 -0
  59. package/web/dist/assets/pilotswarm-CdEkcq2V.js +91 -0
  60. package/web/dist/assets/quadrantDiagram-ABIIQ3AL-Cph1z7ye.js +7 -0
  61. package/web/dist/assets/railroadDiagram-RFXS5EU6-BPKkt2va.js +1 -0
  62. package/web/dist/assets/requirementDiagram-TGXJPOKE-BB6DNHCy.js +84 -0
  63. package/web/dist/assets/sankeyDiagram-HTMAVEWB-BASvPQk2.js +40 -0
  64. package/web/dist/assets/sequenceDiagram-DBY2YBRQ-DMrNas01.js +162 -0
  65. package/web/dist/assets/sizeCapture-X5ZJPWSS-Ddq5yeZZ.js +1 -0
  66. package/web/dist/assets/stateDiagram-2N3HPSRC-DUb_IFST.js +1 -0
  67. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C3nsHf5u.js +1 -0
  68. package/web/dist/assets/swimlanes-5IMT3BWC-Cs9JsGaf.js +2 -0
  69. package/web/dist/assets/swimlanesDiagram-G3AALYLV-C2xZA5V6.js +8 -0
  70. package/web/dist/assets/timeline-definition-FHXFAJF6-xuNF5fTm.js +120 -0
  71. package/web/dist/assets/vennDiagram-L72KCM5P-fsI1GFLy.js +34 -0
  72. package/web/dist/assets/wardleyDiagram-EHGQE667-DEc-kumC.js +78 -0
  73. package/web/dist/assets/xychartDiagram-FW5EYKEG-BCo6Qrfu.js +7 -0
  74. package/web/dist/index.html +3 -3
  75. package/web/dist/assets/pilotswarm-CjMx44so.js +0 -90
@@ -14,6 +14,7 @@ import {
14
14
  createStore,
15
15
  getPromptInputRows,
16
16
  getTheme,
17
+ isThemeLight,
17
18
  parseTerminalMarkupRuns,
18
19
  PilotSwarmUiController,
19
20
  tokenizeInlineMarkdown,
@@ -22,6 +23,7 @@ import {
22
23
  selectArtifactPickerModal,
23
24
  selectArtifactUploadModal,
24
25
  selectLiveActivityLines,
26
+ selectChatBlocks,
25
27
  selectChatLines,
26
28
  selectChatPaneChrome,
27
29
  selectOutboxOverlayLines,
@@ -208,7 +210,7 @@ function normalizeProfileSettings(settings) {
208
210
  const id = candidate.activeSessionId == null ? null : String(candidate.activeSessionId).trim();
209
211
  normalized.activeSessionId = id || null;
210
212
  }
211
- if (candidate.chatViewMode === "summary" || candidate.chatViewMode === "transcript") {
213
+ if (candidate.chatViewMode === "summary" || candidate.chatViewMode === "transcript" || candidate.chatViewMode === "rich") {
212
214
  normalized.chatViewMode = candidate.chatViewMode;
213
215
  }
214
216
  return normalized;
@@ -890,6 +892,275 @@ function usePanePixelScroll(ref, scrollOffset, paneKey, controller) {
890
892
  }, [controller, paneKey, ref]);
891
893
  }
892
894
 
895
+ // ── Code syntax highlighting ──────────────────────────────────────────────
896
+ // A dependency-free tokenizer. Vendoring a real highlighter (highlight.js,
897
+ // prism) is not worth the offline-vendor cost for what fenced blocks in a
898
+ // chat transcript need: comments, strings, numbers, keywords, and call
899
+ // targets. Token colors resolve through the active THEME (same path as
900
+ // markdown links), so every theme — light or dark — stays coherent.
901
+
902
+ const CODE_LANGUAGE_ALIASES = {
903
+ js: "js", jsx: "js", mjs: "js", cjs: "js", javascript: "js",
904
+ ts: "js", tsx: "js", typescript: "js",
905
+ json: "json", jsonc: "json",
906
+ py: "python", python: "python",
907
+ sh: "shell", bash: "shell", zsh: "shell", shell: "shell", console: "shell",
908
+ sql: "sql", postgres: "sql", postgresql: "sql", psql: "sql",
909
+ rs: "rust", rust: "rust",
910
+ go: "go", golang: "go",
911
+ yaml: "yaml", yml: "yaml",
912
+ css: "css", scss: "css",
913
+ };
914
+
915
+ function isMermaidLanguage(language) {
916
+ return String(language || "").trim().toLowerCase() === "mermaid";
917
+ }
918
+
919
+ const CODE_KEYWORDS = {
920
+ js: new Set(["async", "await", "break", "case", "catch", "class", "const", "continue", "default", "delete", "do", "else", "export", "extends", "finally", "for", "from", "function", "if", "import", "in", "instanceof", "let", "new", "of", "return", "static", "super", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "yield", "true", "false", "null", "undefined", "interface", "type", "enum", "implements", "readonly", "public", "private", "protected"]),
921
+ python: new Set(["and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "None", "nonlocal", "not", "or", "pass", "raise", "return", "True", "False", "try", "while", "with", "yield", "self"]),
922
+ shell: new Set(["if", "then", "else", "elif", "fi", "for", "while", "do", "done", "case", "esac", "in", "function", "return", "export", "local", "set", "echo", "cd", "sudo", "source"]),
923
+ sql: new Set(["select", "from", "where", "insert", "into", "values", "update", "set", "delete", "create", "table", "alter", "drop", "index", "join", "left", "right", "inner", "outer", "on", "group", "order", "by", "having", "limit", "offset", "and", "or", "not", "null", "as", "distinct", "union", "all", "case", "when", "then", "else", "end", "with", "returning", "primary", "key", "foreign", "references", "default", "constraint", "begin", "commit", "rollback"]),
924
+ rust: new Set(["as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while"]),
925
+ go: new Set(["break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func", "go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct", "switch", "type", "var", "nil", "true", "false"]),
926
+ json: new Set(["true", "false", "null"]),
927
+ yaml: new Set(["true", "false", "null", "yes", "no"]),
928
+ css: new Set(["important", "media", "keyframes", "import", "supports"]),
929
+ };
930
+
931
+ // Scanner rules per family. Order matters: comments and strings first so a
932
+ // `#` or quote inside them never re-enters another rule. Sticky (`y`) so a
933
+ // rule only ever matches at the current cursor.
934
+ function codeScannerRules(lang) {
935
+ const hashComment = { type: "comment", re: /#[^\n]*/y };
936
+ const slashComment = { type: "comment", re: /\/\/[^\n]*|\/\*[\s\S]*?\*\//y };
937
+ const dashComment = { type: "comment", re: /--[^\n]*|\/\*[\s\S]*?\*\//y };
938
+ const number = { type: "number", re: /\b\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?\b|\b0[xX][0-9a-fA-F]+\b/y };
939
+ const identifier = { type: "identifier", re: /[A-Za-z_$][\w$]*/y };
940
+ const dq = { type: "string", re: /"(?:\\.|[^"\\])*"?/y };
941
+ const sq = { type: "string", re: /'(?:\\.|[^'\\])*'?/y };
942
+ const backtick = { type: "string", re: /`(?:\\.|[^`\\])*`?/y };
943
+ const pyTriple = { type: "string", re: /"""[\s\S]*?"""|'''[\s\S]*?'''/y };
944
+
945
+ switch (lang) {
946
+ case "python":
947
+ return [hashComment, pyTriple, dq, sq, number, identifier];
948
+ case "shell":
949
+ return [hashComment, dq, sq, number, identifier];
950
+ case "sql":
951
+ return [dashComment, sq, dq, number, identifier];
952
+ case "yaml":
953
+ return [hashComment, dq, sq, number, identifier];
954
+ case "json":
955
+ return [dq, number, identifier];
956
+ case "css":
957
+ return [{ type: "comment", re: /\/\*[\s\S]*?\*\//y }, dq, sq, number, identifier];
958
+ default:
959
+ return [slashComment, dq, sq, backtick, number, identifier];
960
+ }
961
+ }
962
+
963
+ const CODE_TOKEN_COLORS = {
964
+ comment: "gray",
965
+ string: "green",
966
+ number: "yellow",
967
+ keyword: "magenta",
968
+ fn: "cyan",
969
+ };
970
+
971
+ // Blocks past this size skip highlighting — a chat transcript should never
972
+ // pay tokenizer cost for a dumped file.
973
+ const CODE_HIGHLIGHT_MAX_CHARS = 20000;
974
+
975
+ // Tokenizing runs inside the render path, so an unmemoized transcript
976
+ // re-highlights EVERY code block on every render (a keystroke in the
977
+ // composer re-renders the pane). Results are content-addressed and cached;
978
+ // the cap keeps a long-lived session from growing this without bound.
979
+ const CODE_TOKEN_CACHE = new Map();
980
+ const CODE_TOKEN_CACHE_MAX = 120;
981
+
982
+ function tokenizeCodeCached(source, language) {
983
+ const key = `${language || ""}\u0000${source}`;
984
+ const hit = CODE_TOKEN_CACHE.get(key);
985
+ if (hit) {
986
+ // Refresh LRU position.
987
+ CODE_TOKEN_CACHE.delete(key);
988
+ CODE_TOKEN_CACHE.set(key, hit);
989
+ return hit;
990
+ }
991
+ const tokens = tokenizeCode(source, language);
992
+ CODE_TOKEN_CACHE.set(key, tokens);
993
+ if (CODE_TOKEN_CACHE.size > CODE_TOKEN_CACHE_MAX) {
994
+ CODE_TOKEN_CACHE.delete(CODE_TOKEN_CACHE.keys().next().value);
995
+ }
996
+ return tokens;
997
+ }
998
+
999
+ function tokenizeCode(source, language) {
1000
+ const lang = CODE_LANGUAGE_ALIASES[String(language || "").trim().toLowerCase()] || "default";
1001
+ const keywords = CODE_KEYWORDS[lang] || CODE_KEYWORDS.js;
1002
+ const rules = codeScannerRules(lang);
1003
+ const tokens = [];
1004
+ let cursor = 0;
1005
+ let plainStart = 0;
1006
+
1007
+ const flushPlain = (end) => {
1008
+ if (end > plainStart) tokens.push({ type: "plain", text: source.slice(plainStart, end) });
1009
+ };
1010
+
1011
+ while (cursor < source.length) {
1012
+ let hit = null;
1013
+ for (const rule of rules) {
1014
+ rule.re.lastIndex = cursor;
1015
+ const match = rule.re.exec(source);
1016
+ if (match && match.index === cursor && match[0].length > 0) {
1017
+ hit = { type: rule.type, text: match[0] };
1018
+ break;
1019
+ }
1020
+ }
1021
+ if (!hit) {
1022
+ cursor += 1;
1023
+ continue;
1024
+ }
1025
+ let type = hit.type;
1026
+ if (type === "identifier") {
1027
+ if (keywords.has(hit.text)) {
1028
+ type = "keyword";
1029
+ } else {
1030
+ // A call target reads as a function; anything else is plain.
1031
+ const after = source.slice(cursor + hit.text.length);
1032
+ type = /^\s*\(/.test(after) ? "fn" : "plain";
1033
+ }
1034
+ }
1035
+ if (type === "plain") {
1036
+ cursor += hit.text.length;
1037
+ continue;
1038
+ }
1039
+ flushPlain(cursor);
1040
+ tokens.push({ type, text: hit.text });
1041
+ cursor += hit.text.length;
1042
+ plainStart = cursor;
1043
+ }
1044
+ flushPlain(source.length);
1045
+ return tokens;
1046
+ }
1047
+
1048
+ function renderHighlightedCode(content, language, theme) {
1049
+ const source = String(content || "");
1050
+ if (!source || source.length > CODE_HIGHLIGHT_MAX_CHARS) return source;
1051
+ let tokens;
1052
+ try {
1053
+ tokens = tokenizeCodeCached(source, language);
1054
+ } catch {
1055
+ return source;
1056
+ }
1057
+ return tokens.map((token, index) => {
1058
+ const color = CODE_TOKEN_COLORS[token.type];
1059
+ if (!color) return React.createElement(React.Fragment, { key: `t:${index}` }, token.text);
1060
+ return React.createElement("span", {
1061
+ key: `t:${index}`,
1062
+ className: `ps-code-token is-${token.type}`,
1063
+ style: { color: resolveColor(theme, color) || undefined },
1064
+ }, token.text);
1065
+ });
1066
+ }
1067
+
1068
+ // ── Mermaid diagrams ──────────────────────────────────────────────────────
1069
+ // mermaid is ~2MB, so it is imported DYNAMICALLY: vite splits it into its own
1070
+ // chunk that is fetched only when a transcript actually contains a ```mermaid
1071
+ // fence. A failed parse (very common while a diagram is still streaming in)
1072
+ // falls back to the highlighted source rather than an error box.
1073
+
1074
+ let mermaidModulePromise = null;
1075
+
1076
+ function loadMermaid() {
1077
+ if (!mermaidModulePromise) {
1078
+ mermaidModulePromise = import("mermaid")
1079
+ .then((module) => module.default || module)
1080
+ .catch((error) => {
1081
+ mermaidModulePromise = null;
1082
+ throw error;
1083
+ });
1084
+ }
1085
+ return mermaidModulePromise;
1086
+ }
1087
+
1088
+ // initialize() must run per render, not once at import: the module promise is
1089
+ // cached, so configuring the palette only on first load left every diagram
1090
+ // frozen on whichever theme happened to be active when mermaid loaded (a
1091
+ // dark->light switch kept rendering dark diagrams until a reload).
1092
+ function configureMermaid(mermaid, light) {
1093
+ mermaid.initialize({
1094
+ startOnLoad: false,
1095
+ securityLevel: "strict",
1096
+ theme: light ? "default" : "dark",
1097
+ fontFamily: "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",
1098
+ });
1099
+ return mermaid;
1100
+ }
1101
+
1102
+ let mermaidRenderSeq = 0;
1103
+
1104
+ // mermaid.render() is NOT re-entrant: it stages each diagram through shared
1105
+ // internal state, so two blocks in one message racing each other leaves one
1106
+ // of them unrendered (whichever lost the race falls back to source). Renders
1107
+ // are therefore serialized through a promise chain.
1108
+ let mermaidRenderQueue = Promise.resolve();
1109
+
1110
+ function queueMermaidRender(task) {
1111
+ const run = mermaidRenderQueue.then(task, task);
1112
+ mermaidRenderQueue = run.then(() => undefined, () => undefined);
1113
+ return run;
1114
+ }
1115
+
1116
+ function MermaidDiagram({ code, theme, fallback }) {
1117
+ const [svg, setSvg] = React.useState(null);
1118
+ const [failed, setFailed] = React.useState(false);
1119
+ const [errorText, setErrorText] = React.useState("");
1120
+ const light = React.useMemo(() => isThemeLight(theme), [theme]);
1121
+
1122
+ React.useEffect(() => {
1123
+ let active = true;
1124
+ const source = String(code || "").trim();
1125
+ if (!source) return undefined;
1126
+ setFailed(false);
1127
+ loadMermaid()
1128
+ .then((mermaid) => queueMermaidRender(async () => {
1129
+ if (!active) return;
1130
+ configureMermaid(mermaid, light);
1131
+ mermaidRenderSeq += 1;
1132
+ const id = `ps-mermaid-${mermaidRenderSeq}`;
1133
+ // parse() first so a half-streamed diagram fails cheaply and
1134
+ // without mermaid injecting an error graphic into the DOM.
1135
+ await mermaid.parse(source);
1136
+ const { svg: rendered } = await mermaid.render(id, source);
1137
+ if (active) setSvg(rendered);
1138
+ }))
1139
+ .catch((error) => {
1140
+ if (active) { setSvg(null); setFailed(true); setErrorText(String(error?.message || error)); }
1141
+ });
1142
+ return () => { active = false; };
1143
+ }, [code, light]);
1144
+
1145
+ if (svg && !failed) {
1146
+ return React.createElement("div", {
1147
+ className: "ps-mermaid",
1148
+ // mermaid output is generated from the diagram source with
1149
+ // securityLevel "strict" (HTML labels off, scripts stripped).
1150
+ dangerouslySetInnerHTML: { __html: svg },
1151
+ });
1152
+ }
1153
+ // A diagram mermaid cannot parse falls back to its source WITH the
1154
+ // reason: silently showing code left "why didn't this render?" a mystery.
1155
+ if (failed && errorText) {
1156
+ return React.createElement("div", { className: "ps-mermaid-failed" },
1157
+ React.createElement("div", { className: "ps-mermaid-error" },
1158
+ `Diagram could not be rendered — ${String(errorText).split("\n")[0]}`),
1159
+ fallback);
1160
+ }
1161
+ return fallback;
1162
+ }
1163
+
893
1164
  function renderInlineMarkdown(source, theme, keyPrefix = "md") {
894
1165
  return tokenizeInlineMarkdown(source).map((token, index) => {
895
1166
  const key = `${keyPrefix}:${index}`;
@@ -1186,10 +1457,19 @@ function MarkdownPreviewContent({ content, theme }) {
1186
1457
  }, renderInlineMarkdown(block.text, theme, `heading:${index}`));
1187
1458
  }
1188
1459
  if (block.type === "code") {
1189
- return React.createElement("section", { key: `block:${index}`, className: "ps-md-code-block" },
1460
+ const codeSection = React.createElement("section", { key: `block:${index}`, className: "ps-md-code-block" },
1190
1461
  React.createElement("div", { className: "ps-md-code-header" }, block.language || "text"),
1191
1462
  React.createElement("pre", { className: "ps-md-code-pre" },
1192
- React.createElement("code", null, block.content)));
1463
+ React.createElement("code", null, renderHighlightedCode(block.content, block.language, theme))));
1464
+ if (isMermaidLanguage(block.language)) {
1465
+ return React.createElement(MermaidDiagram, {
1466
+ key: `block:${index}`,
1467
+ code: block.content,
1468
+ theme,
1469
+ fallback: codeSection,
1470
+ });
1471
+ }
1472
+ return codeSection;
1193
1473
  }
1194
1474
  if (block.type === "blockquote") {
1195
1475
  return React.createElement("blockquote", { key: `block:${index}`, className: "ps-md-quote" },
@@ -1661,6 +1941,78 @@ function StructuredChatBlocks({ lines, theme, controller = null }) {
1661
1941
  return React.createElement(StructuredBlockList, { blocks, theme, controller });
1662
1942
  }
1663
1943
 
1944
+ // Rich (desktop-style) chat transcript. Consumes selectChatBlocks: plain
1945
+ // user/assistant prose renders as proportional-type markdown messages;
1946
+ // every other block (splash, thinking cards, system cards, epoch dividers)
1947
+ // reuses the structured line renderer so the terminal-era special cases
1948
+ // keep exactly one implementation.
1949
+ function RichChatBlocks({ blocks, theme, controller = null }) {
1950
+ return React.createElement("div", { className: "ps-rich-chat" },
1951
+ (blocks || []).map((block, index) => {
1952
+ if (block.kind === "message") {
1953
+ const header = block.header || {};
1954
+ const roleClass = block.role === "user" ? " is-user" : " is-assistant";
1955
+ const pendingClass = block.pendingPhase ? ` is-${block.pendingPhase}` : "";
1956
+ const text = String(block.text || "");
1957
+ // A speaker label only earns its place when it disambiguates:
1958
+ // another human in a shared session. The viewer's own turns
1959
+ // and the agent's are obvious from side and styling.
1960
+ const showLabel = block.role === "user" && header.fromOtherPerson;
1961
+ // Consecutive turns from the same speaker read as one
1962
+ // continuous passage — no repeated header, tighter spacing.
1963
+ const previous = blocks[index - 1];
1964
+ const continued = previous
1965
+ && previous.kind === "message"
1966
+ && previous.role === block.role
1967
+ && !(previous.header && previous.header.fromOtherPerson)
1968
+ && !showLabel;
1969
+ return React.createElement("article", {
1970
+ key: block.id != null ? `msg:${block.id}` : `msg:${index}`,
1971
+ className: `ps-rich-msg${roleClass}${pendingClass}${continued ? " is-continued" : ""}`,
1972
+ },
1973
+ React.createElement("header", { className: "ps-rich-msg-head" },
1974
+ showLabel
1975
+ ? React.createElement("span", {
1976
+ className: "ps-rich-msg-label",
1977
+ style: { color: resolveColor(theme, header.roleColor) || undefined },
1978
+ }, header.roleLabel)
1979
+ : null,
1980
+ // Delivery glyph + timestamp are on-demand detail:
1981
+ // revealed on hover so a quiet transcript stays quiet.
1982
+ React.createElement("span", { className: "ps-rich-msg-meta" },
1983
+ header.glyph
1984
+ ? React.createElement("span", {
1985
+ className: "ps-rich-msg-glyph",
1986
+ title: block.pendingPhase || undefined,
1987
+ style: { color: resolveColor(theme, header.glyphColor) || undefined },
1988
+ }, header.glyph)
1989
+ : null,
1990
+ header.time
1991
+ ? React.createElement("span", { className: "ps-rich-msg-time" }, header.time)
1992
+ : null)),
1993
+ text.trim()
1994
+ ? React.createElement("div", { className: "ps-rich-msg-body" },
1995
+ React.createElement(MarkdownPreviewContent, { content: text, theme }))
1996
+ : null,
1997
+ block.attachments
1998
+ ? React.createElement(ArtifactImageStrip, {
1999
+ controller,
2000
+ sessionId: block.attachments.sessionId,
2001
+ attachments: block.attachments.attachments,
2002
+ })
2003
+ : null);
2004
+ }
2005
+ return React.createElement("div", {
2006
+ key: `lines:${index}`,
2007
+ className: `ps-rich-lineblock is-${block.variant || "event"}`,
2008
+ }, React.createElement(StructuredChatBlocks, {
2009
+ lines: normalizeLines(block.lines || []),
2010
+ theme,
2011
+ controller,
2012
+ }));
2013
+ }));
2014
+ }
2015
+
1664
2016
  // Authenticated artifact thumbnails: bytes are fetched through the transport
1665
2017
  // (Bearer token) and served to <img> as object URLs. Module-level cache so
1666
2018
  // scrolling the transcript doesn't refetch; oldest entries are revoked at cap.
@@ -1767,10 +2119,19 @@ function StructuredBlockList({ blocks, theme, controller = null }) {
1767
2119
  }
1768
2120
 
1769
2121
  if (block.type === "code") {
1770
- return React.createElement("section", { key: `code:${index}`, className: "ps-md-code-block ps-chat-code-block" },
2122
+ const chatCodeSection = React.createElement("section", { key: `code:${index}`, className: "ps-md-code-block ps-chat-code-block" },
1771
2123
  React.createElement("div", { className: "ps-md-code-header" }, block.language || "text"),
1772
2124
  React.createElement("pre", { className: "ps-md-code-pre" },
1773
- React.createElement("code", null, block.content || "")));
2125
+ React.createElement("code", null, renderHighlightedCode(block.content || "", block.language, theme))));
2126
+ if (isMermaidLanguage(block.language)) {
2127
+ return React.createElement(MermaidDiagram, {
2128
+ key: `code:${index}`,
2129
+ code: block.content || "",
2130
+ theme,
2131
+ fallback: chatCodeSection,
2132
+ });
2133
+ }
2134
+ return chatCodeSection;
1774
2135
  }
1775
2136
 
1776
2137
  if (block.type === "card") {
@@ -1856,6 +2217,19 @@ function StructuredBlockList({ blocks, theme, controller = null }) {
1856
2217
  }));
1857
2218
  }
1858
2219
 
2220
+ // Pane titles carry terminal-style bracketed runs (" [+3]", " [5f086c7c]").
2221
+ // The rich view keeps the information but drops the brackets — a web title
2222
+ // bar shows a count and an id, it does not draw them in ASCII delimiters.
2223
+ function unbracketTitleRuns(runs) {
2224
+ if (!Array.isArray(runs)) return runs;
2225
+ return runs.map((run) => {
2226
+ const text = String(run?.text ?? "");
2227
+ const match = /^(\s*)\[(.+)\](\s*)$/.exec(text);
2228
+ if (!match) return run;
2229
+ return { ...run, text: `${match[1]}${match[2]}${match[3]}` };
2230
+ });
2231
+ }
2232
+
1859
2233
  function Panel({ title, titleRight = null, color = "gray", focused = false, actions = null, children, theme, className = "" }) {
1860
2234
  const accent = resolveColor(theme, color);
1861
2235
  return React.createElement("section", {
@@ -2056,6 +2430,68 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
2056
2430
  bottomContent);
2057
2431
  }
2058
2432
 
2433
+ // Rich (desktop-style) session row: a status dot instead of a glyph, the
2434
+ // title in proportional type, and the id/age/model metadata demoted to a
2435
+ // muted second line under the selected row. Depth becomes real indentation
2436
+ // with a guide rail rather than an ASCII "└" prefix.
2437
+ const SESSION_KIND_GLYPHS = { group: "🗂", system: "⚙", service: "⚗" };
2438
+
2439
+ function RichSessionRow({ row, theme }) {
2440
+ const chrome = row.chrome;
2441
+ if (!chrome) return React.createElement(SessionRowContent, { row, theme, structured: true });
2442
+
2443
+ const kindGlyph = SESSION_KIND_GLYPHS[chrome.kind] || null;
2444
+ const accent = resolveColor(theme, chrome.accentColor) || undefined;
2445
+ const detailRuns = Array.isArray(row.detailRuns) ? row.detailRuns : [];
2446
+
2447
+ return React.createElement("div", { className: "ps-rich-session-row" },
2448
+ React.createElement("div", { className: "ps-rich-session-main" },
2449
+ chrome.selectMode
2450
+ ? React.createElement("span", {
2451
+ className: `ps-rich-session-check${chrome.checked ? " is-checked" : ""}`,
2452
+ }, chrome.checked ? "✓" : "")
2453
+ : null,
2454
+ kindGlyph
2455
+ ? React.createElement("span", { className: "ps-rich-session-kind" }, kindGlyph)
2456
+ : React.createElement("span", {
2457
+ className: "ps-rich-session-dot",
2458
+ style: { background: resolveColor(theme, chrome.statusColor) || undefined },
2459
+ title: row.status || undefined,
2460
+ }),
2461
+ chrome.owner
2462
+ ? React.createElement("span", {
2463
+ className: `ps-rich-session-owner${chrome.owner.isMine ? " is-mine" : ""}`,
2464
+ title: chrome.owner.initials,
2465
+ }, chrome.owner.initials)
2466
+ : null,
2467
+ React.createElement("span", {
2468
+ className: `ps-rich-session-title${chrome.untitled ? " is-untitled" : ""}`,
2469
+ style: chrome.kind === "session" ? undefined : { color: accent },
2470
+ title: chrome.title,
2471
+ }, chrome.title),
2472
+ chrome.pinned ? React.createElement("span", { className: "ps-rich-session-pin", title: "Pinned" }, "📌") : null,
2473
+ chrome.cron ? React.createElement("span", { className: "ps-rich-session-cron", title: "Scheduled" }, "⏱") : null,
2474
+ chrome.childBadge
2475
+ ? React.createElement("span", {
2476
+ className: "ps-rich-session-count",
2477
+ style: { color: resolveColor(theme, chrome.childBadge.color) || undefined },
2478
+ }, chrome.childBadge.text)
2479
+ : null,
2480
+ chrome.ctx
2481
+ ? React.createElement("span", {
2482
+ className: "ps-rich-session-ctx",
2483
+ style: { color: resolveColor(theme, chrome.ctx.color) || undefined },
2484
+ }, chrome.ctx.text)
2485
+ : null),
2486
+ // The detail line is ALWAYS rendered, selected or not. Swapping row
2487
+ // content on selection made the whole list jump; now the only things
2488
+ // that reflow are expand/collapse and the highlight moving.
2489
+ detailRuns.length > 0
2490
+ ? React.createElement("div", { className: "ps-rich-session-detail" },
2491
+ React.createElement(Runs, { runs: detailRuns, theme }))
2492
+ : null);
2493
+ }
2494
+
2059
2495
  function SessionRowContent({ row, theme, structured = false }) {
2060
2496
  const hasStructuredRuns = structured && Array.isArray(row.titleRuns);
2061
2497
  if (!hasStructuredRuns) {
@@ -2105,6 +2541,11 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
2105
2541
  connectionMode: state.connection?.mode || "local",
2106
2542
  modalOpen: Boolean(state.ui.modal),
2107
2543
  focused: state.ui.focusRegion === "sessions",
2544
+ // The rich chat view also restyles the session list, so the portal
2545
+ // reads as one product rather than half terminal / half desktop.
2546
+ // TODO: promote this to its own `ui.richUi` setting if the two are
2547
+ // ever wanted independently.
2548
+ rich: state.ui.chatViewMode === "rich",
2108
2549
  }), shallowEqualObject);
2109
2550
  const rows = React.useMemo(() => selectSessionRows({
2110
2551
  sessions: {
@@ -2207,7 +2648,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
2207
2648
  })
2208
2649
  : (activeSession && !activeSession.isSystem && !activeSession.isGroup && !activeSession.parentSessionId ? [activeSession.sessionId] : []);
2209
2650
  const canMoveToGroup = groupableIds.length > 0;
2210
- const combinedPanelClassName = `ps-session-pane${panelClassName ? ` ${panelClassName}` : ""}`;
2651
+ const combinedPanelClassName = `ps-session-pane${viewState.rich ? " is-rich" : ""}${panelClassName ? ` ${panelClassName}` : ""}`;
2211
2652
  const setSessionButtonRef = React.useCallback((sessionId, node) => {
2212
2653
  if (!sessionId) return;
2213
2654
  if (node) {
@@ -2376,10 +2817,16 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
2376
2817
  },
2377
2818
  },
2378
2819
  React.createElement("div", {
2379
- className: "ps-line ps-session-row-content",
2380
- style: { paddingInlineStart: `${Math.max(0, row.depth) * 4}px` },
2820
+ className: viewState.rich ? "ps-session-row-content is-rich" : "ps-line ps-session-row-content",
2821
+ style: {
2822
+ paddingInlineStart: viewState.rich
2823
+ ? `${Math.max(0, row.depth) * 14}px`
2824
+ : `${Math.max(0, row.depth) * 4}px`,
2825
+ },
2381
2826
  },
2382
- React.createElement(SessionRowContent, { row, theme, structured: structuredRows })),
2827
+ viewState.rich
2828
+ ? React.createElement(RichSessionRow, { row, theme })
2829
+ : React.createElement(SessionRowContent, { row, theme, structured: structuredRows })),
2383
2830
  )),
2384
2831
  )),
2385
2832
  (manageOpen && activeSession && !activeSession.isGroup)
@@ -2831,10 +3278,24 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
2831
3278
  () => appendAnimatedDotsToRuns(chrome.titleRight, chrome.animateTitleRight ? animatedDots : ""),
2832
3279
  [animatedDots, chrome.animateTitleRight, chrome.titleRight],
2833
3280
  );
2834
- const lines = React.useMemo(
2835
- () => selectChatLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" }),
2836
- [selectorState, viewState.contentWidth],
3281
+ const richMode = viewState.chatViewMode === "rich" && !viewState.activeSessionIsGroup;
3282
+ const richBlocks = React.useMemo(
3283
+ () => (richMode ? selectChatBlocks(selectorState, viewState.contentWidth, { tableMode: "sentinel" }) : null),
3284
+ [richMode, selectorState, viewState.contentWidth],
2837
3285
  );
3286
+ const lines = React.useMemo(() => {
3287
+ if (richBlocks) {
3288
+ // Rich mode paints from `richBlocks` via renderBody; these
3289
+ // pseudo-lines exist only as the scroll-sync change signal
3290
+ // (autoscroll-to-bottom fires when a block grows or arrives).
3291
+ return richBlocks.map((block, index) => ({
3292
+ text: block.kind === "message"
3293
+ ? `m:${block.id ?? index}:${(block.text || "").length}:${block.pendingPhase || ""}`
3294
+ : `l:${index}:${(block.lines || []).length}`,
3295
+ }));
3296
+ }
3297
+ return selectChatLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" });
3298
+ }, [richBlocks, selectorState, viewState.contentWidth]);
2838
3299
  const spinnerFrame = useSpinnerFrame(viewState.activeSessionStatus === "running");
2839
3300
  const liveActivityLines = React.useMemo(
2840
3301
  () => selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth: viewState.contentWidth }),
@@ -2903,7 +3364,10 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
2903
3364
 
2904
3365
  return React.createElement(ScrollLinesPanel, {
2905
3366
  controller,
2906
- title: mobile ? compactTitleRuns(chrome.title, 28) : chrome.title,
3367
+ title: (() => {
3368
+ const baseTitle = mobile ? compactTitleRuns(chrome.title, 28) : chrome.title;
3369
+ return richMode ? unbracketTitleRuns(baseTitle) : baseTitle;
3370
+ })(),
2907
3371
  titleRight: mobile && titleRight ? compactTitleRuns(titleRight, 18) : titleRight,
2908
3372
  // Chat/Summary toggling lives on the top toolbar so the pane chrome
2909
3373
  // stays clean. The toolbar button is disabled for group sessions.
@@ -2915,10 +3379,17 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
2915
3379
  scrollOffset: viewState.scroll,
2916
3380
  scrollMode: "bottom",
2917
3381
  paneKey: "chat",
2918
- className: "is-wrapped",
2919
- panelClassName: "ps-chat-panel",
3382
+ className: richMode ? "is-wrapped is-rich" : "is-wrapped",
3383
+ panelClassName: richMode ? "ps-chat-panel is-rich" : "ps-chat-panel",
2920
3384
  bottomContent: composer,
2921
3385
  structuredBlocks: true,
3386
+ renderBody: richBlocks
3387
+ ? (_bodyLines, bodyTheme) => React.createElement(RichChatBlocks, {
3388
+ blocks: richBlocks,
3389
+ theme: bodyTheme,
3390
+ controller,
3391
+ })
3392
+ : null,
2922
3393
  });
2923
3394
  }
2924
3395
 
@@ -3325,7 +3796,7 @@ function InspectorPane({ controller, mobile = false, panelClassName = "", extraA
3325
3796
  stickyBottom: inspector.activeTab === "logs",
3326
3797
  paneKey: "inspector",
3327
3798
  className: inspector.activeTab === "history" || inspector.activeTab === "logs" ? "is-wrapped" : "is-preserve",
3328
- panelClassName: `${inspector.activeTab === "sequence" ? "has-preserved-sticky" : ""}${panelClassName ? ` ${panelClassName}` : ""}`.trim(),
3799
+ panelClassName: `ps-inspector-pane${inspector.activeTab === "sequence" ? " has-preserved-sticky" : ""}${panelClassName ? ` ${panelClassName}` : ""}`.trim(),
3329
3800
  });
3330
3801
  }
3331
3802
 
@@ -3374,7 +3845,9 @@ function ActivityPane({ controller, panelClassName = "", extraActions = null })
3374
3845
  stickyBottom: true,
3375
3846
  paneKey: "activity",
3376
3847
  className: "is-wrapped",
3377
- panelClassName,
3848
+ // Stable identity class so styling can target the activity surface
3849
+ // without depending on which slot rendered it.
3850
+ panelClassName: `ps-activity-pane${panelClassName ? ` ${panelClassName}` : ""}`,
3378
3851
  });
3379
3852
  }
3380
3853
 
@@ -3646,7 +4119,19 @@ function PromptComposer({ controller, mobile, active = true, onAfterSend = null
3646
4119
  }, "✕"));
3647
4120
  }))
3648
4121
  : null,
3649
- React.createElement("label", { className: "ps-prompt-label" }, promptState.answerMode ? "answer" : selectedCancelling ? "cancelling" : selectedQueued ? "queued" : promptState.editingPending ? "pending" : "you"),
4122
+ // The label carries real mode signal (answer / queued / pending /
4123
+ // cancelling); only the idle "you" is a terminal-prompt affordance,
4124
+ // so it is tagged for the rich view to hide.
4125
+ (() => {
4126
+ const promptLabelText = promptState.answerMode ? "answer"
4127
+ : selectedCancelling ? "cancelling"
4128
+ : selectedQueued ? "queued"
4129
+ : promptState.editingPending ? "pending"
4130
+ : "you";
4131
+ return React.createElement("label", {
4132
+ className: `ps-prompt-label${promptLabelText === "you" ? " is-default" : ""}`,
4133
+ }, promptLabelText);
4134
+ })(),
3650
4135
  React.createElement("textarea", {
3651
4136
  ref: inputRef,
3652
4137
  className: "ps-prompt-input",
@@ -3933,6 +4418,12 @@ function IconButton({ icon, label, onClick, disabled = false, active = false, cl
3933
4418
  }
3934
4419
 
3935
4420
  function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus = null, chatFocusDisabled = false }) {
4421
+ const richUi = useControllerSelector(controller, (state) => state.ui.chatViewMode === "rich");
4422
+ const [headerSlot, setHeaderSlot] = React.useState(null);
4423
+ React.useEffect(() => {
4424
+ if (typeof document === "undefined") return;
4425
+ setHeaderSlot(richUi && !mobile ? document.getElementById("ps-header-toolbar-slot") : null);
4426
+ }, [richUi, mobile]);
3936
4427
  const adminVisible = useControllerSelector(controller, (state) => Boolean(state.admin?.visible));
3937
4428
  const chatView = useControllerSelector(controller, (state) => ({
3938
4429
  mode: state.ui.chatViewMode || "transcript",
@@ -3966,6 +4457,16 @@ function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus
3966
4457
  label: "Theme",
3967
4458
  onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {}),
3968
4459
  },
4460
+ {
4461
+ key: "rich",
4462
+ icon: "Aa",
4463
+ label: chatView.activeSessionIsGroup
4464
+ ? "Groups show group details"
4465
+ : (chatView.mode === "rich" ? "Show terminal transcript" : "Rich chat view (experimental)"),
4466
+ onClick: () => controller.setChatViewMode(chatView.mode === "rich" ? "transcript" : "rich"),
4467
+ disabled: chatView.activeSessionIsGroup,
4468
+ active: chatView.mode === "rich",
4469
+ },
3969
4470
  {
3970
4471
  key: "summary",
3971
4472
  icon: chatView.mode === "summary" ? "💬" : "≣",
@@ -4012,9 +4513,17 @@ function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus
4012
4513
  );
4013
4514
  }
4014
4515
 
4015
- return React.createElement("div", { className: "ps-toolbar" },
4516
+ const toolbar = React.createElement("div", { className: "ps-toolbar" },
4016
4517
  React.createElement("div", { className: "ps-toolbar-actions" }, buttonDefs.map(renderButton)),
4017
4518
  );
4519
+
4520
+ // Rich desktop UI: the toolbar lives IN the portal header (one top bar)
4521
+ // rather than as its own strip, handing that row back to the panes. The
4522
+ // slot is rendered by PortalHeader in App.jsx, outside this tree, so the
4523
+ // handoff is a portal. Falls back to inline rendering when the slot is
4524
+ // absent (mobile layouts, embeddings that use their own header).
4525
+ if (headerSlot) return createPortal(toolbar, headerSlot);
4526
+ return toolbar;
4018
4527
  }
4019
4528
 
4020
4529
  function ColumnResizeHandle({ controller, paneAdjust = 0 }) {
@@ -5255,6 +5764,16 @@ export function createWebPilotSwarmController({ transport, mode = "remote", bran
5255
5764
  }
5256
5765
 
5257
5766
  export function PilotSwarmWebApp({ controller }) {
5767
+ // The rich UI restyles chrome that lives OUTSIDE this tree (the portal
5768
+ // header in App.jsx), so the flag rides on <body> and every surface
5769
+ // keys off `body.ps-rich-ui` in CSS rather than threading a prop.
5770
+ const richUi = useControllerSelector(controller, (state) => state.ui.chatViewMode === "rich");
5771
+ React.useEffect(() => {
5772
+ if (typeof document === "undefined") return undefined;
5773
+ document.body.classList.toggle("ps-rich-ui", richUi);
5774
+ return () => document.body.classList.remove("ps-rich-ui");
5775
+ }, [richUi]);
5776
+
5258
5777
  const viewportRef = React.useRef(null);
5259
5778
  const mainGridRef = React.useRef(null);
5260
5779
  const viewport = useMeasuredViewport(viewportRef);
@@ -5507,14 +6026,14 @@ export function PilotSwarmWebApp({ controller }) {
5507
6026
  const desktopWorkspace = React.createElement("div", {
5508
6027
  className: "ps-workspace-grid",
5509
6028
  style: {
5510
- gridTemplateColumns: `minmax(0, ${layout.leftWidth}fr) 16px minmax(0, ${layout.rightWidth}fr)`,
6029
+ gridTemplateColumns: `minmax(0, ${layout.leftWidth}fr) var(--ps-resizer-track, 16px) minmax(0, ${layout.rightWidth}fr)`,
5511
6030
  },
5512
6031
  },
5513
6032
  React.createElement("div", {
5514
6033
  ref: mainGridRef,
5515
6034
  className: `ps-workspace-main-grid is-session-${sessionColumnMode}`,
5516
6035
  style: {
5517
- gridTemplateColumns: `${sessionColumnTrack} 16px minmax(14rem, 1fr)`,
6036
+ gridTemplateColumns: `${sessionColumnTrack} var(--ps-resizer-track, 16px) minmax(14rem, 1fr)`,
5518
6037
  },
5519
6038
  },
5520
6039
  sessionColumnMode !== "hidden" ? React.createElement("div", {
@@ -5538,7 +6057,7 @@ export function PilotSwarmWebApp({ controller }) {
5538
6057
  React.createElement(ColumnResizeHandle, { controller, paneAdjust: state.paneAdjust }),
5539
6058
  React.createElement("div", {
5540
6059
  className: "ps-workspace-column",
5541
- style: { gridTemplateRows: `${layout.inspectorPaneHeight}fr 16px ${layout.activityPaneHeight}fr` },
6060
+ style: { gridTemplateRows: `${layout.inspectorPaneHeight}fr var(--ps-resizer-track, 16px) ${layout.activityPaneHeight}fr` },
5542
6061
  },
5543
6062
  React.createElement("div", {
5544
6063
  className: "ps-workspace-pane-slot",