@xgspz/cli-ui 0.6.14 → 0.6.16
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/CHANGELOG.md +37 -4
- package/README.md +29 -4
- package/dist/{cli-ui-embed-D-3DvyTN.js → cli-ui-embed-D9xgur2y.js} +18376 -18648
- package/dist/{cli-ui-index-C34qFb4u.js → cli-ui-index-WBtxmw23.js} +2 -2
- package/dist/{cli-ui-jszip.min-DojbZPiE.js → cli-ui-jszip.min-CU3ZzoXb.js} +1 -1
- package/dist/{cli-ui-jszip.min-BObcE2nn.js → cli-ui-jszip.min-DdU0X3VJ.js} +2 -2
- package/dist/{cli-ui-xlsx-JVLtx0qR.js → cli-ui-xlsx-Dx6T7qhC.js} +1 -1
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +1 -1
- package/src/board/ChangesPane.tsx +10 -4
- package/src/board/CliBoard.tsx +48 -16
- package/src/board/FileViewer.tsx +34 -1
- package/src/board/diffRefresh.ts +63 -0
- package/src/board/linkage.ts +68 -0
- package/src/board/pathResolve.ts +108 -0
- package/src/chat/CliChat.tsx +105 -182
- package/src/chat/Composer.tsx +4 -3
- package/src/core/conversation.ts +16 -4
- package/src/core/store.ts +120 -40
- package/src/core/types.ts +2 -0
- package/src/msglist/CliMsgList.tsx +3 -2
- package/src/operator/node/AgentConnect.tsx +19 -15
- package/src/operator/node/AppearancePanel.tsx +1 -1
- package/src/operator/node/BranchNavigator.tsx +1 -1
- package/src/operator/node/BrowserPane.tsx +1 -1
- package/src/operator/node/Chat.tsx +16 -261
- package/src/operator/node/CommandPalette.tsx +1 -1
- package/src/operator/node/Composer.tsx +1 -1
- package/src/operator/node/Layout.tsx +1 -1
- package/src/operator/node/Markdown.tsx +6 -36
- package/src/operator/node/Modal.tsx +1 -1
- package/src/operator/node/OnboardingWizard.tsx +9 -5
- package/src/operator/node/Orchestrator.tsx +1 -1
- package/src/operator/node/ProjectLanding.tsx +1 -1
- package/src/operator/node/ProjectsColumn.tsx +1 -1
- package/src/operator/node/ProviderAuth.tsx +12 -10
- package/src/operator/node/RunSwitch.tsx +1 -1
- package/src/operator/node/Services.tsx +1 -1
- package/src/operator/node/SessionRail.tsx +27 -8
- package/src/operator/node/SessionView.tsx +1 -1
- package/src/operator/node/SettingsView.tsx +1 -1
- package/src/operator/node/TaskBoard.tsx +1 -1
- package/src/operator/node/TaskChanges.tsx +34 -5
- package/src/operator/node/TaskHero.tsx +1 -1
- package/src/operator/node/TasksColumn.tsx +1 -1
- package/src/operator/node/Transcript.tsx +10 -237
- package/src/operator/node/Welcome.tsx +1 -1
- package/src/operator/node/WorktreePrune.tsx +1 -1
- package/src/operator/node/boardInteraction.ts +6 -3
- package/src/operator/node/boardState.ts +12 -13
- package/src/operator/node/fileLinks.ts +4 -38
- package/src/operator/node/format.ts +12 -104
- package/src/operator/node/github.tsx +10 -8
- package/src/operator/node/modals.tsx +15 -13
- package/src/operator/node/recheckOnReturn.ts +28 -0
- package/src/operator/node/shared.tsx +10 -50
- package/src/operator/node/useTaskStream.ts +2 -2
- package/src/operator.base.css +3 -0
- package/src/operator.css +3 -0
- package/src/styles.css +5 -0
- package/src/ui/Markdown.tsx +36 -21
- package/src/ui/Transcript.tsx +8 -5
- package/src/ui/fileLinks.ts +6 -3
- package/src/ui/format.ts +11 -5
- package/src/ui/shared.tsx +11 -7
- package/src/ui/transcriptModel.ts +40 -9
- package/src/ui/useTranscriptWindow.ts +253 -0
- package/src/operator/node/boundedPoll.ts +0 -47
- package/src/operator/node/icons.tsx +0 -88
- package/src/operator/node/transcriptModel.ts +0 -129
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/* Orchestrator — icon set (ported from the design's icons.js) */
|
|
2
|
-
import type { SVGProps } from "react";
|
|
3
|
-
|
|
4
|
-
type P = SVGProps<SVGSVGElement>;
|
|
5
|
-
|
|
6
|
-
function S(children: React.ReactNode, props?: P) {
|
|
7
|
-
return (
|
|
8
|
-
<svg
|
|
9
|
-
viewBox="0 0 24 24"
|
|
10
|
-
fill="none"
|
|
11
|
-
stroke="currentColor"
|
|
12
|
-
strokeWidth={1.7}
|
|
13
|
-
strokeLinecap="round"
|
|
14
|
-
strokeLinejoin="round"
|
|
15
|
-
width="1em"
|
|
16
|
-
height="1em"
|
|
17
|
-
style={{ flex: "none" }}
|
|
18
|
-
{...props}
|
|
19
|
-
>
|
|
20
|
-
{children}
|
|
21
|
-
</svg>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const Icon = {
|
|
26
|
-
plus: (p?: P) => S(<><line x1={12} y1={5} x2={12} y2={19} /><line x1={5} y1={12} x2={19} y2={12} /></>, p),
|
|
27
|
-
list: (p?: P) => S(<><line x1={9} y1={6} x2={20} y2={6} /><line x1={9} y1={12} x2={20} y2={12} /><line x1={9} y1={18} x2={20} y2={18} /><circle cx={5} cy={6} r={0.5} /><circle cx={5} cy={12} r={0.5} /><circle cx={5} cy={18} r={0.5} /></>, p),
|
|
28
|
-
board: (p?: P) => S(<><rect x={3} y={4} width={5} height={16} rx={1.5} /><rect x={10} y={4} width={5} height={11} rx={1.5} /><rect x={17} y={4} width={4} height={8} rx={1.5} /></>, p),
|
|
29
|
-
chart: (p?: P) => S(<><line x1={5} y1={20} x2={5} y2={13} /><line x1={12} y1={20} x2={12} y2={5} /><line x1={19} y1={20} x2={19} y2={9} /></>, p),
|
|
30
|
-
folder: (p?: P) => S(<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />, p),
|
|
31
|
-
gear: (p?: P) => S(<><circle cx={12} cy={12} r={3} /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></>, p),
|
|
32
|
-
send: (p?: P) => S(<><path d="M7 11l5-5 5 5" /><line x1={12} y1={6} x2={12} y2={18} /></>, p),
|
|
33
|
-
spark: (p?: P) => S(<path d="M12 3l1.6 5.4L19 10l-5.4 1.6L12 17l-1.6-5.4L5 10l5.4-1.6z" />, p),
|
|
34
|
-
clock: (p?: P) => S(<><circle cx={12} cy={12} r={9} /><path d="M12 7v5l3 2" /></>, p),
|
|
35
|
-
clear: (p?: P) => S(<><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v4h4" /></>, p),
|
|
36
|
-
dots: (p?: P) => S(<><circle cx={5} cy={12} r={1} /><circle cx={12} cy={12} r={1} /><circle cx={19} cy={12} r={1} /></>, p),
|
|
37
|
-
chevDown: (p?: P) => S(<path d="M6 9l6 6 6-6" />, p),
|
|
38
|
-
chevUp: (p?: P) => S(<path d="M6 15l6-6 6 6" />, p),
|
|
39
|
-
chevRight: (p?: P) => S(<path d="M9 6l6 6-6 6" />, p),
|
|
40
|
-
toBottom: (p?: P) => S(<><path d="M7 7l5 5 5-5" /><line x1={5} y1={17} x2={19} y2={17} /></>, p),
|
|
41
|
-
x: (p?: P) => S(<><line x1={6} y1={6} x2={18} y2={18} /><line x1={6} y1={18} x2={18} y2={6} /></>, p),
|
|
42
|
-
clip: (p?: P) => S(<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />, p),
|
|
43
|
-
check: (p?: P) => S(<path d="M5 12l5 5 9-11" />, p),
|
|
44
|
-
lock: (p?: P) => S(<><rect x={4} y={11} width={16} height={9} rx={2} /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></>, p),
|
|
45
|
-
edit: (p?: P) => S(<><path d="M12 20h9" /><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z" /></>, p),
|
|
46
|
-
doc: (p?: P) => S(<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6" /></>, p),
|
|
47
|
-
github: (p?: P) => S(<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />, p),
|
|
48
|
-
git: (p?: P) => S(<><circle cx={6} cy={6} r={2} /><circle cx={6} cy={18} r={2} /><circle cx={18} cy={9} r={2} /><path d="M18 11a8 8 0 0 1-8 8" /><line x1={6} y1={8} x2={6} y2={16} /></>, p),
|
|
49
|
-
flag: (p?: P) => S(<path d="M5 21V4h12l-2 4 2 4H5" />, p),
|
|
50
|
-
bolt: (p?: P) => S(<path d="M13 2L4 14h7l-1 8 9-12h-7z" />, p),
|
|
51
|
-
moon: (p?: P) => S(<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />, p),
|
|
52
|
-
sun: (p?: P) => S(<><circle cx={12} cy={12} r={4} /><line x1={12} y1={2} x2={12} y2={5} /><line x1={12} y1={19} x2={12} y2={22} /><line x1={2} y1={12} x2={5} y2={12} /><line x1={19} y1={12} x2={22} y2={12} /><line x1={4.9} y1={4.9} x2={6.8} y2={6.8} /><line x1={17.2} y1={17.2} x2={19.1} y2={19.1} /><line x1={4.9} y1={19.1} x2={6.8} y2={17.2} /><line x1={17.2} y1={6.8} x2={19.1} y2={4.9} /></>, p),
|
|
53
|
-
external: (p?: P) => S(<><path d="M14 4h6v6" /><path d="M20 4l-9 9" /><path d="M19 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5" /></>, p),
|
|
54
|
-
copy: (p?: P) => S(<><rect x={9} y={9} width={12} height={12} rx={2} /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></>, p),
|
|
55
|
-
play: (p?: P) => S(<path d="M7 5l11 7-11 7z" />, p),
|
|
56
|
-
stop: (p?: P) => S(<rect x={6} y={6} width={12} height={12} rx={2} />, p),
|
|
57
|
-
sliders: (p?: P) => S(<><line x1={4} y1={21} x2={4} y2={14} /><line x1={4} y1={10} x2={4} y2={3} /><line x1={12} y1={21} x2={12} y2={12} /><line x1={12} y1={8} x2={12} y2={3} /><line x1={20} y1={21} x2={20} y2={16} /><line x1={20} y1={12} x2={20} y2={3} /><line x1={1} y1={14} x2={7} y2={14} /><line x1={9} y1={8} x2={15} y2={8} /><line x1={17} y1={16} x2={23} y2={16} /></>, p),
|
|
58
|
-
search: (p?: P) => S(<><circle cx={11} cy={11} r={7} /><line x1={21} y1={21} x2={16.65} y2={16.65} /></>, p),
|
|
59
|
-
terminal: (p?: P) => S(<><rect x={3} y={4} width={18} height={16} rx={2} /><path d="M7 9l3 3-3 3" /><line x1={13} y1={15} x2={17} y2={15} /></>, p),
|
|
60
|
-
archive: (p?: P) => S(<><rect x={3} y={4} width={18} height={4} rx={1} /><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8" /><line x1={10} y1={12} x2={14} y2={12} /></>, p),
|
|
61
|
-
restore: (p?: P) => S(<><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v4h4" /><path d="M12 8v4l3 2" /></>, p),
|
|
62
|
-
download: (p?: P) => S(<><path d="M12 4v11" /><path d="M7 11l5 5 5-5" /><line x1={5} y1={20} x2={19} y2={20} /></>, p),
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// ---- agent brand marks ----
|
|
66
|
-
// Logos, not line icons: filled paths in `currentColor` (the stroked `S()` set
|
|
67
|
-
// above would smear these at avatar size). Keyed by agent driver id so any UI
|
|
68
|
-
// that knows a task's `agent` can stamp the right mark; unknown/legacy ids get
|
|
69
|
-
// no entry and callers fall back to a generic glyph.
|
|
70
|
-
|
|
71
|
-
function Brand(children: React.ReactNode, props?: P) {
|
|
72
|
-
return (
|
|
73
|
-
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none" width="1em" height="1em" style={{ flex: "none" }} aria-hidden {...props}>
|
|
74
|
-
{children}
|
|
75
|
-
</svg>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export const AgentMark: Record<string, (p?: P) => React.ReactElement> = {
|
|
80
|
-
claude: (p?: P) => Brand(
|
|
81
|
-
<path d="M4.709 15.955l4.72-2.647.079-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 0 1-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.984-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.322.455-.34-.049-2.204-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />,
|
|
82
|
-
p,
|
|
83
|
-
),
|
|
84
|
-
codex: (p?: P) => Brand(
|
|
85
|
-
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.911 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.182a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.998-2.9 6.056 6.056 0 0 0-.748-7.073zm-9.022 12.608a4.476 4.476 0 0 1-2.876-1.04l.142-.081 4.778-2.758a.795.795 0 0 0 .393-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.495 4.494zM3.6 18.304a4.471 4.471 0 0 1-.535-3.014l.142.085 4.783 2.758a.771.771 0 0 0 .78 0l5.843-3.368v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.499 4.499 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.677l5.814 3.354-2.02 1.169a.076.076 0 0 1-.071 0l-4.83-2.787A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.856L13.104 8.364 15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.024l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.499 4.499 0 0 1 6.681 4.66zM8.307 12.863l-2.02-1.164a.08.08 0 0 1-.038-.057V6.074a4.499 4.499 0 0 1 7.376-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.682zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />,
|
|
86
|
-
p,
|
|
87
|
-
),
|
|
88
|
-
};
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// Pure helpers behind the live transcript (useTaskStream + SessionView), kept
|
|
2
|
-
// free of React so they can be unit-tested. The rules they encode exist for
|
|
3
|
-
// long tasks: thousands of messages, megabytes of tool output.
|
|
4
|
-
|
|
5
|
-
import type { ToolData } from "./protocol/types";
|
|
6
|
-
import type { Msg } from "./types";
|
|
7
|
-
import { buildSessions } from "./format";
|
|
8
|
-
|
|
9
|
-
/** Messages the live view's snapshot carries (GET /messages?tail=N). */
|
|
10
|
-
export const SNAPSHOT_TAIL = 300;
|
|
11
|
-
/** Older messages fetched per page from /history. */
|
|
12
|
-
export const HISTORY_PAGE = 300;
|
|
13
|
-
/** Transcript rows kept in the DOM while following the newest output. */
|
|
14
|
-
export const RENDER_WINDOW = 120;
|
|
15
|
-
/** Rows revealed per step when scrolling back (or forward again). */
|
|
16
|
-
export const RENDER_STEP = 80;
|
|
17
|
-
/** Most rows ever in the DOM at once; the far end is dropped beyond this. */
|
|
18
|
-
export const RENDER_MAX = 360;
|
|
19
|
-
/**
|
|
20
|
-
* Transcripts one tab keeps in memory: the open task plus the most recently
|
|
21
|
-
* left ones (switching back to those shows at once). Each kept one is trimmed
|
|
22
|
-
* to a snapshot's size when left (trimToTail), so on real node data this is
|
|
23
|
-
* ~10 MB typical, well under 100 MB at worst. Per tab: several tabs on the
|
|
24
|
-
* same task each keep their own.
|
|
25
|
-
*/
|
|
26
|
-
export const KEEP_TRANSCRIPTS = 30;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* A transcript the tab is leaving, brought back to what a fresh snapshot holds
|
|
30
|
-
* (the newest `tail` messages; `older` grows by what was dropped), so a task
|
|
31
|
-
* read far back doesn't keep thousands of messages around in the background.
|
|
32
|
-
* Returns the input untouched when it is already that small.
|
|
33
|
-
*/
|
|
34
|
-
export function trimToTail(msgs: Msg[], older: number, tail = SNAPSHOT_TAIL): { msgs: Msg[]; older: number } {
|
|
35
|
-
const queued = msgs.filter((m) => m.role === "queued");
|
|
36
|
-
const rest = msgs.filter((m) => m.role !== "queued");
|
|
37
|
-
if (rest.length <= tail) return { msgs, older };
|
|
38
|
-
return { msgs: [...rest.slice(rest.length - tail), ...queued], older: older + rest.length - tail };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** The tab's kept-transcript order after opening `id`, and which tasks fall out. */
|
|
42
|
-
export function touchRecent(recent: string[], id: string, keep = KEEP_TRANSCRIPTS): { recent: string[]; evicted: string[] } {
|
|
43
|
-
const next = [id, ...recent.filter((t) => t !== id)];
|
|
44
|
-
return { recent: next.slice(0, keep), evicted: next.slice(keep) };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Parsed tool payloads, cached per message object. Message objects are replaced
|
|
48
|
-
// whenever their content changes, so a cached entry can never go stale, and the
|
|
49
|
-
// transcript stops re-parsing every tool row's JSON on each live event.
|
|
50
|
-
const toolCache = new WeakMap<Msg, ToolData>();
|
|
51
|
-
/** The tool card behind a "tool" message. Read-only: never mutate the result. */
|
|
52
|
-
export function toolDataOf(m: Msg): ToolData {
|
|
53
|
-
let data = toolCache.get(m);
|
|
54
|
-
if (!data) {
|
|
55
|
-
try { data = JSON.parse(m.content) as ToolData; } catch { data = { title: m.content }; }
|
|
56
|
-
toolCache.set(m, data);
|
|
57
|
-
}
|
|
58
|
-
return data;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Index of the message with this id, searching from the newest end (where live updates land). */
|
|
62
|
-
export function indexFromEnd(arr: Msg[], id: string): number {
|
|
63
|
-
for (let i = arr.length - 1; i >= 0; i--) if (arr[i].id === id) return i;
|
|
64
|
-
return -1;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** True when an AskUserQuestion card near the end is still unanswered. */
|
|
68
|
-
export function hasOpenAsk(messages: Msg[], scan = 400): boolean {
|
|
69
|
-
for (let i = messages.length - 1, n = 0; i >= 0 && n < scan; i--, n++) {
|
|
70
|
-
const m = messages[i];
|
|
71
|
-
if (m.role !== "tool") continue;
|
|
72
|
-
const d = toolDataOf(m);
|
|
73
|
-
if (d.ask && !d.ask.answers) return true;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Apply a (re)connect snapshot to what the tab already holds.
|
|
80
|
-
*
|
|
81
|
-
* - Unchanged messages keep their object identity, so memoized rows don't
|
|
82
|
-
* re-render (and re-parse their markdown) on every reconnect.
|
|
83
|
-
* - Older pages the user already scrolled in are kept when the snapshot's tail
|
|
84
|
-
* still overlaps them; `older` is reduced by what was kept.
|
|
85
|
-
*/
|
|
86
|
-
export function mergeSnapshot(prev: Msg[] | undefined, committed: Msg[], queued: Msg[], older: number): { msgs: Msg[]; older: number } {
|
|
87
|
-
const byId = new Map<string, Msg>();
|
|
88
|
-
for (const m of prev ?? []) byId.set(m.id, m);
|
|
89
|
-
const reuse = (m: Msg): Msg => {
|
|
90
|
-
const p = byId.get(m.id);
|
|
91
|
-
if (!p || p.role !== m.role || p.content !== m.content || p.generation !== m.generation) return m;
|
|
92
|
-
return p.ts === m.ts ? p : { ...p, ts: m.ts };
|
|
93
|
-
};
|
|
94
|
-
const fresh = committed.map(reuse);
|
|
95
|
-
let kept: Msg[] = [];
|
|
96
|
-
if (prev && fresh.length && older > 0) {
|
|
97
|
-
const at = prev.findIndex((m) => m.id === fresh[0].id);
|
|
98
|
-
// Only persisted rows (they carry ts) — local notices died with the old view.
|
|
99
|
-
if (at > 0) kept = prev.slice(0, at).filter((m) => m.role !== "queued" && m.ts != null);
|
|
100
|
-
}
|
|
101
|
-
return { msgs: [...kept, ...fresh, ...queued.map(reuse)], older: Math.max(0, older - kept.length) };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export type TranscriptItem =
|
|
105
|
-
| { kind: "label"; key: string; n: number; current: boolean }
|
|
106
|
-
| { kind: "break"; key: string; summary: string }
|
|
107
|
-
| { kind: "msg"; key: string; m: Msg; initial: boolean; hideWho: boolean };
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* The transcript as a flat row list (session labels, /clear breaks, messages),
|
|
111
|
-
* so the view can render any window of it. `hasOlder`: earlier messages aren't
|
|
112
|
-
* loaded yet, so the first loaded one is not necessarily its session's opener.
|
|
113
|
-
*/
|
|
114
|
-
export function buildTranscriptItems(messages: Msg[], hasOlder: boolean): TranscriptItem[] {
|
|
115
|
-
const sessions = buildSessions(messages);
|
|
116
|
-
const items: TranscriptItem[] = [];
|
|
117
|
-
sessions.forEach((s, si) => {
|
|
118
|
-
if (si > 0 && s.summaryBefore) items.push({ kind: "break", key: `b-${s.n}`, summary: s.summaryBefore });
|
|
119
|
-
items.push({ kind: "label", key: `s-${s.n}`, n: s.n, current: si === sessions.length - 1 });
|
|
120
|
-
s.messages.forEach((m, mi) => {
|
|
121
|
-
const prev = s.messages[mi - 1];
|
|
122
|
-
// collapse the repeated agent header across an assistant run (text → tool → text)
|
|
123
|
-
const hideWho = m.role === "assistant" && !!prev && (prev.role === "assistant" || prev.role === "tool");
|
|
124
|
-
const initial = mi === 0 && m.role === "user" && !(si === 0 && hasOlder);
|
|
125
|
-
items.push({ kind: "msg", key: m.id, m, initial, hideWho });
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
return items;
|
|
129
|
-
}
|