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.
- package/package.json +2 -1
- package/tui/src/node-sdk-transport.js +22 -5
- package/ui/core/src/controller.js +1 -1
- package/ui/core/src/reducer.js +3 -2
- package/ui/core/src/selectors.js +142 -2
- package/ui/core/src/state.js +1 -1
- package/ui/react/src/web-app.js +541 -22
- package/web/dist/assets/abnfDiagram-VRR7QNED-BHkTpRkx.js +1 -0
- package/web/dist/assets/arc-BOgZyEWA.js +1 -0
- package/web/dist/assets/architectureDiagram-ZJ3FMSHR-DeE8fDMy.js +36 -0
- package/web/dist/assets/blockDiagram-677ZJIJ3-CzhdxVEu.js +132 -0
- package/web/dist/assets/c4Diagram-LMCZKHZV-Cc6p_IKP.js +10 -0
- package/web/dist/assets/channel-DhhWyKRW.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-BCo94LTM.js +1 -0
- package/web/dist/assets/chunk-32BRIVSS-DaklJ-6i.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-BN_wg2pr.js +15 -0
- package/web/dist/assets/chunk-EX3LRPZG-DSFFLUoL.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-DB4dAjza.js +1 -0
- package/web/dist/assets/chunk-MOJQB5TN-CRM5eGQS.js +88 -0
- package/web/dist/assets/chunk-RYQCIY6F-BFlkjazU.js +1 -0
- package/web/dist/assets/chunk-V7JOEXUC-Bnfh4AAv.js +206 -0
- package/web/dist/assets/chunk-VR4S4FIN-CQc4_qVM.js +1 -0
- package/web/dist/assets/chunk-XXDRQBXY-CZivyoDm.js +1 -0
- package/web/dist/assets/classDiagram-OUVF2IWQ-CQBxGpio.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CQBxGpio.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-BUibhTD-.js +1 -0
- package/web/dist/assets/cynefin-VYW2F7L2-CRkjg5zF.js +166 -0
- package/web/dist/assets/cynefinDiagram-TSTJHNR4-BoeQn4Um.js +62 -0
- package/web/dist/assets/cytoscape.esm-D3_iZ_3b.js +321 -0
- package/web/dist/assets/dagre-VKFMJZFB-DGo7-PY5.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-FQU43EPY-DMXd2bdO.js +3 -0
- package/web/dist/assets/diagram-G47NLZAW-CkYHblMZ.js +24 -0
- package/web/dist/assets/diagram-NH7WQ7WH-C9iwEe4T.js +24 -0
- package/web/dist/assets/diagram-OA4YK3LP-DpYLrfwb.js +30 -0
- package/web/dist/assets/diagram-WEI45ONY-BvrBukHb.js +41 -0
- package/web/dist/assets/ebnfDiagram-CCIWWBDH-DGl1r3zR.js +1 -0
- package/web/dist/assets/erDiagram-Q63AITRT-CoP5DBSb.js +85 -0
- package/web/dist/assets/flowDiagram-23GEKE2U-DVHtyYyr.js +156 -0
- package/web/dist/assets/ganttDiagram-NO4QXBWP-_JyR78yC.js +292 -0
- package/web/dist/assets/gitGraphDiagram-IHSO6WYX-Bj7sh0Db.js +106 -0
- package/web/dist/assets/graph-DOmOIIwC.js +1 -0
- package/web/dist/assets/{index-BDMVG8w6.js → index-CB6NfSZb.js} +3 -3
- package/web/dist/assets/{index-CxkLM8_a.css → index-SmXlZ1eu.css} +1 -1
- package/web/dist/assets/infoDiagram-FWYZ7A6U-BrvMeRmA.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-FXEZZL3T-lNDujpEC.js +70 -0
- package/web/dist/assets/journeyDiagram-5HDEW3XC-yoD2QRni.js +139 -0
- package/web/dist/assets/kanban-definition-HUTT4EX6-DtyMlypg.js +89 -0
- package/web/dist/assets/katex-HP8lGamR.js +257 -0
- package/web/dist/assets/layout-D-LzfAck.js +1 -0
- package/web/dist/assets/linear-DXPbXF9v.js +1 -0
- package/web/dist/assets/map-DxJ2ADlA.js +1 -0
- package/web/dist/assets/mermaid.core-DCEXqVUL.js +308 -0
- package/web/dist/assets/mindmap-definition-LN4V7U3C-DpKHDJFg.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-2B236MQR-mxFI_8qh.js +1 -0
- package/web/dist/assets/pieDiagram-ENE6RG2P-sUWLrwMG.js +39 -0
- package/web/dist/assets/pilotswarm-CdEkcq2V.js +91 -0
- package/web/dist/assets/quadrantDiagram-ABIIQ3AL-Cph1z7ye.js +7 -0
- package/web/dist/assets/railroadDiagram-RFXS5EU6-BPKkt2va.js +1 -0
- package/web/dist/assets/requirementDiagram-TGXJPOKE-BB6DNHCy.js +84 -0
- package/web/dist/assets/sankeyDiagram-HTMAVEWB-BASvPQk2.js +40 -0
- package/web/dist/assets/sequenceDiagram-DBY2YBRQ-DMrNas01.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-Ddq5yeZZ.js +1 -0
- package/web/dist/assets/stateDiagram-2N3HPSRC-DUb_IFST.js +1 -0
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C3nsHf5u.js +1 -0
- package/web/dist/assets/swimlanes-5IMT3BWC-Cs9JsGaf.js +2 -0
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-C2xZA5V6.js +8 -0
- package/web/dist/assets/timeline-definition-FHXFAJF6-xuNF5fTm.js +120 -0
- package/web/dist/assets/vennDiagram-L72KCM5P-fsI1GFLy.js +34 -0
- package/web/dist/assets/wardleyDiagram-EHGQE667-DEc-kumC.js +78 -0
- package/web/dist/assets/xychartDiagram-FW5EYKEG-BCo6Qrfu.js +7 -0
- package/web/dist/index.html +3 -3
- package/web/dist/assets/pilotswarm-CjMx44so.js +0 -90
package/ui/react/src/web-app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
|
2835
|
-
|
|
2836
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|