pilotswarm 0.0.1 → 0.4.0
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/README.md +37 -1
- package/mcp/README.md +484 -0
- package/mcp/dist/bin/pilotswarm-mcp.d.ts +3 -0
- package/mcp/dist/bin/pilotswarm-mcp.d.ts.map +1 -0
- package/mcp/dist/bin/pilotswarm-mcp.js +367 -0
- package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -0
- package/mcp/dist/src/auth.d.ts +7 -0
- package/mcp/dist/src/auth.d.ts.map +1 -0
- package/mcp/dist/src/auth.js +99 -0
- package/mcp/dist/src/auth.js.map +1 -0
- package/mcp/dist/src/context.d.ts +48 -0
- package/mcp/dist/src/context.d.ts.map +1 -0
- package/mcp/dist/src/context.js +83 -0
- package/mcp/dist/src/context.js.map +1 -0
- package/mcp/dist/src/index.d.ts +4 -0
- package/mcp/dist/src/index.d.ts.map +1 -0
- package/mcp/dist/src/index.js +3 -0
- package/mcp/dist/src/index.js.map +1 -0
- package/mcp/dist/src/prompts/skills.d.ts +4 -0
- package/mcp/dist/src/prompts/skills.d.ts.map +1 -0
- package/mcp/dist/src/prompts/skills.js +11 -0
- package/mcp/dist/src/prompts/skills.js.map +1 -0
- package/mcp/dist/src/resources/agents.d.ts +4 -0
- package/mcp/dist/src/resources/agents.d.ts.map +1 -0
- package/mcp/dist/src/resources/agents.js +64 -0
- package/mcp/dist/src/resources/agents.js.map +1 -0
- package/mcp/dist/src/resources/facts.d.ts +4 -0
- package/mcp/dist/src/resources/facts.d.ts.map +1 -0
- package/mcp/dist/src/resources/facts.js +125 -0
- package/mcp/dist/src/resources/facts.js.map +1 -0
- package/mcp/dist/src/resources/models.d.ts +4 -0
- package/mcp/dist/src/resources/models.d.ts.map +1 -0
- package/mcp/dist/src/resources/models.js +43 -0
- package/mcp/dist/src/resources/models.js.map +1 -0
- package/mcp/dist/src/resources/sessions.d.ts +4 -0
- package/mcp/dist/src/resources/sessions.d.ts.map +1 -0
- package/mcp/dist/src/resources/sessions.js +190 -0
- package/mcp/dist/src/resources/sessions.js.map +1 -0
- package/mcp/dist/src/resources/subscriptions.d.ts +9 -0
- package/mcp/dist/src/resources/subscriptions.d.ts.map +1 -0
- package/mcp/dist/src/resources/subscriptions.js +157 -0
- package/mcp/dist/src/resources/subscriptions.js.map +1 -0
- package/mcp/dist/src/server.d.ts +4 -0
- package/mcp/dist/src/server.d.ts.map +1 -0
- package/mcp/dist/src/server.js +59 -0
- package/mcp/dist/src/server.js.map +1 -0
- package/mcp/dist/src/tools/agents.d.ts +4 -0
- package/mcp/dist/src/tools/agents.d.ts.map +1 -0
- package/mcp/dist/src/tools/agents.js +317 -0
- package/mcp/dist/src/tools/agents.js.map +1 -0
- package/mcp/dist/src/tools/facts.d.ts +4 -0
- package/mcp/dist/src/tools/facts.d.ts.map +1 -0
- package/mcp/dist/src/tools/facts.js +151 -0
- package/mcp/dist/src/tools/facts.js.map +1 -0
- package/mcp/dist/src/tools/models.d.ts +4 -0
- package/mcp/dist/src/tools/models.d.ts.map +1 -0
- package/mcp/dist/src/tools/models.js +256 -0
- package/mcp/dist/src/tools/models.js.map +1 -0
- package/mcp/dist/src/tools/sessions.d.ts +4 -0
- package/mcp/dist/src/tools/sessions.d.ts.map +1 -0
- package/mcp/dist/src/tools/sessions.js +606 -0
- package/mcp/dist/src/tools/sessions.js.map +1 -0
- package/mcp/dist/src/util/command.d.ts +52 -0
- package/mcp/dist/src/util/command.d.ts.map +1 -0
- package/mcp/dist/src/util/command.js +78 -0
- package/mcp/dist/src/util/command.js.map +1 -0
- package/package.json +81 -6
- package/tui/README.md +35 -0
- package/tui/bin/tui.js +30 -0
- package/tui/plugins/.mcp.json +7 -0
- package/tui/plugins/plugin.json +13 -0
- package/tui/plugins/session-policy.json +8 -0
- package/tui/src/app.js +850 -0
- package/tui/src/auth/cli.js +111 -0
- package/tui/src/auth/entra-auth.js +218 -0
- package/tui/src/bootstrap-env.js +176 -0
- package/tui/src/embedded-workers.js +79 -0
- package/tui/src/http-transport-host.js +106 -0
- package/tui/src/index.js +340 -0
- package/tui/src/node-sdk-transport.js +1793 -0
- package/tui/src/platform.js +984 -0
- package/tui/src/plugin-config.js +239 -0
- package/tui/src/portal.js +7 -0
- package/tui/src/version.js +7 -0
- package/tui/tui-splash.txt +11 -0
- package/ui/core/README.md +6 -0
- package/ui/core/src/commands.js +93 -0
- package/ui/core/src/context-usage.js +212 -0
- package/ui/core/src/controller.js +6104 -0
- package/ui/core/src/formatting.js +1036 -0
- package/ui/core/src/history.js +932 -0
- package/ui/core/src/index.js +13 -0
- package/ui/core/src/layout.js +332 -0
- package/ui/core/src/reducer.js +1935 -0
- package/ui/core/src/selectors.js +5409 -0
- package/ui/core/src/session-errors.js +14 -0
- package/ui/core/src/session-tree.js +151 -0
- package/ui/core/src/state.js +248 -0
- package/ui/core/src/store.js +23 -0
- package/ui/core/src/system-titles.js +24 -0
- package/ui/core/src/themes/catppuccin-mocha.js +56 -0
- package/ui/core/src/themes/cobalt2.js +56 -0
- package/ui/core/src/themes/dark-high-contrast.js +56 -0
- package/ui/core/src/themes/daylight.js +62 -0
- package/ui/core/src/themes/dracula.js +56 -0
- package/ui/core/src/themes/github-dark.js +56 -0
- package/ui/core/src/themes/github-light.js +59 -0
- package/ui/core/src/themes/gruvbox-dark.js +56 -0
- package/ui/core/src/themes/hacker-x-matrix.js +56 -0
- package/ui/core/src/themes/hacker-x-orion-prime.js +56 -0
- package/ui/core/src/themes/helpers.js +79 -0
- package/ui/core/src/themes/high-contrast-mono.js +59 -0
- package/ui/core/src/themes/index.js +52 -0
- package/ui/core/src/themes/light-high-contrast.js +62 -0
- package/ui/core/src/themes/noctis-obscuro.js +56 -0
- package/ui/core/src/themes/noctis.js +56 -0
- package/ui/core/src/themes/paper-ink.js +62 -0
- package/ui/core/src/themes/solarized-ops.js +59 -0
- package/ui/core/src/themes/terminal-green.js +59 -0
- package/ui/core/src/themes/tokyo-night.js +56 -0
- package/ui/react/README.md +5 -0
- package/ui/react/src/chat-status.js +39 -0
- package/ui/react/src/components.js +1989 -0
- package/ui/react/src/index.js +4 -0
- package/ui/react/src/platform.js +15 -0
- package/ui/react/src/use-controller-state.js +38 -0
- package/ui/react/src/web-app.js +4390 -0
- package/web/README.md +198 -0
- package/web/api/router.js +196 -0
- package/web/api/ws.js +152 -0
- package/web/auth/authz/engine.js +204 -0
- package/web/auth/config.js +115 -0
- package/web/auth/index.js +175 -0
- package/web/auth/normalize/entra.js +22 -0
- package/web/auth/providers/entra.js +76 -0
- package/web/auth/providers/none.js +24 -0
- package/web/auth.js +10 -0
- package/web/bin/serve.js +53 -0
- package/web/config.js +20 -0
- package/web/dist/app.js +469 -0
- package/web/dist/assets/index-CBgQQk-j.css +1 -0
- package/web/dist/assets/index-DMefB7Wb.js +24 -0
- package/web/dist/assets/msal-CytV9RFv.js +7 -0
- package/web/dist/assets/pilotswarm-DyBlqVTY.js +87 -0
- package/web/dist/assets/react-BZwsW8pe.js +1 -0
- package/web/dist/index.html +16 -0
- package/web/runtime.js +454 -0
- package/web/server.js +276 -0
- package/index.js +0 -1
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
import { formatHumanDurationSeconds, formatTimestamp, shortSessionId, stripTerminalMarkupTags, summarizeJson } from "./formatting.js";
|
|
2
|
+
import { formatCompactionActivityRuns } from "./context-usage.js";
|
|
3
|
+
import { canonicalSystemTitle } from "./system-titles.js";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_HISTORY_EVENT_LIMIT = 300;
|
|
6
|
+
export const HISTORY_EVENT_LIMIT_STEPS = [
|
|
7
|
+
DEFAULT_HISTORY_EVENT_LIMIT,
|
|
8
|
+
1_000,
|
|
9
|
+
3_000,
|
|
10
|
+
10_000,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function clampHistoryItems(items, maxItems) {
|
|
14
|
+
const list = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
15
|
+
const safeMax = Math.max(DEFAULT_HISTORY_EVENT_LIMIT, Number(maxItems) || DEFAULT_HISTORY_EVENT_LIMIT);
|
|
16
|
+
return list.length > safeMax ? list.slice(-safeMax) : list;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeMessageText(text) {
|
|
20
|
+
return String(text || "").replace(/\r\n/g, "\n").trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatCronTimestamp(value) {
|
|
24
|
+
if (!value) return "";
|
|
25
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
26
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
27
|
+
try {
|
|
28
|
+
return date.toLocaleString(undefined, {
|
|
29
|
+
month: "short",
|
|
30
|
+
day: "numeric",
|
|
31
|
+
hour: "2-digit",
|
|
32
|
+
minute: "2-digit",
|
|
33
|
+
hour12: false,
|
|
34
|
+
timeZoneName: "short",
|
|
35
|
+
}).replace(/,\s*/g, " ").replace(/\s+/g, " ").trim();
|
|
36
|
+
} catch {
|
|
37
|
+
return date.toISOString().replace(/\.000Z$/, "Z");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const REHYDRATION_NOTICE_PREFIX_RE = /^The session was dehydrated and has been rehydrated on a new worker(?: \([^)]+\))?\./i;
|
|
42
|
+
const REHYDRATION_NOTICE_FULL_RE = /^The session was dehydrated and has been rehydrated on a new worker(?: \([^)]+\))?\.\s*The LLM conversation history is preserved\.?/i;
|
|
43
|
+
|
|
44
|
+
export function isRehydrationNoticeText(text) {
|
|
45
|
+
return REHYDRATION_NOTICE_PREFIX_RE.test(normalizeMessageText(text));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function stripLeadingRehydrationNoticeText(text) {
|
|
49
|
+
const normalized = String(text || "").replace(/\r\n/g, "\n").trim();
|
|
50
|
+
if (!normalized) return "";
|
|
51
|
+
|
|
52
|
+
const stripped = normalized.replace(REHYDRATION_NOTICE_FULL_RE, "").trimStart();
|
|
53
|
+
if (stripped !== normalized) return stripped.trim();
|
|
54
|
+
if (isRehydrationNoticeText(normalized)) {
|
|
55
|
+
return normalized.replace(REHYDRATION_NOTICE_PREFIX_RE, "").trim();
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function splitSystemNoticeSegments(text) {
|
|
61
|
+
const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
|
|
62
|
+
const segments = [];
|
|
63
|
+
let textLines = [];
|
|
64
|
+
|
|
65
|
+
function flushText() {
|
|
66
|
+
if (textLines.length === 0) return;
|
|
67
|
+
segments.push({
|
|
68
|
+
kind: "text",
|
|
69
|
+
text: textLines.join("\n"),
|
|
70
|
+
});
|
|
71
|
+
textLines = [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let index = 0; index < lines.length;) {
|
|
75
|
+
const line = lines[index];
|
|
76
|
+
if (!/^\s*\[SYSTEM:/i.test(line)) {
|
|
77
|
+
textLines.push(line);
|
|
78
|
+
index += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const singleLineMatch = /^\s*\[SYSTEM:\s*(.*?)\]\s*$/i.exec(line);
|
|
83
|
+
if (singleLineMatch) {
|
|
84
|
+
flushText();
|
|
85
|
+
segments.push({
|
|
86
|
+
kind: "system",
|
|
87
|
+
text: singleLineMatch[1].trim(),
|
|
88
|
+
});
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const noticeLines = [line.replace(/^\s*\[SYSTEM:\s*/i, "")];
|
|
94
|
+
let closingIndex = -1;
|
|
95
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
96
|
+
const closingLine = lines[cursor];
|
|
97
|
+
if (closingLine.trim() === "]") {
|
|
98
|
+
closingIndex = cursor;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (/\]\s*$/.test(closingLine.trim())) {
|
|
103
|
+
const closingContent = closingLine.replace(/\]\s*$/, "");
|
|
104
|
+
if (closingContent.trim()) {
|
|
105
|
+
noticeLines.push(closingContent);
|
|
106
|
+
}
|
|
107
|
+
closingIndex = cursor;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
noticeLines.push(closingLine);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (closingIndex === -1) {
|
|
115
|
+
textLines.push(line);
|
|
116
|
+
index += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
flushText();
|
|
121
|
+
segments.push({
|
|
122
|
+
kind: "system",
|
|
123
|
+
text: noticeLines.join("\n").trim(),
|
|
124
|
+
});
|
|
125
|
+
index = closingIndex + 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
flushText();
|
|
129
|
+
return segments;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractVisibleChatText(text, role) {
|
|
133
|
+
const normalized = String(text || "").replace(/\r\n/g, "\n");
|
|
134
|
+
if (role === "user" || role === "assistant") {
|
|
135
|
+
return splitSystemNoticeSegments(normalized)
|
|
136
|
+
.filter((segment) => segment.kind === "text")
|
|
137
|
+
.map((segment) => segment.text)
|
|
138
|
+
.join("\n")
|
|
139
|
+
.trim();
|
|
140
|
+
}
|
|
141
|
+
return normalized.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractEmbeddedSystemNoticeTexts(text, role) {
|
|
145
|
+
if (role !== "user" && role !== "assistant") return [];
|
|
146
|
+
return splitSystemNoticeSegments(text)
|
|
147
|
+
.filter((segment) => segment.kind === "system")
|
|
148
|
+
.map((segment) => segment.text)
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function comparableMessageText(message) {
|
|
153
|
+
const normalized = normalizeMessageText(message?.text || "");
|
|
154
|
+
if (!normalized) return "";
|
|
155
|
+
if (message?.role === "user" || message?.role === "assistant") {
|
|
156
|
+
const visibleText = extractVisibleChatText(normalized, message.role);
|
|
157
|
+
if (visibleText) return visibleText;
|
|
158
|
+
}
|
|
159
|
+
return stripLeadingRehydrationNoticeText(normalized);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function parseAskedAndAnsweredExchange(text) {
|
|
163
|
+
const source = String(text || "").replace(/\r\n/g, "\n").trim();
|
|
164
|
+
const prefix = 'The user was asked: "';
|
|
165
|
+
const marker = '"\nThe user responded: "';
|
|
166
|
+
|
|
167
|
+
if (!source.startsWith(prefix) || !source.endsWith('"')) return null;
|
|
168
|
+
const markerIndex = source.indexOf(marker, prefix.length);
|
|
169
|
+
if (markerIndex === -1) return null;
|
|
170
|
+
|
|
171
|
+
const question = source.slice(prefix.length, markerIndex);
|
|
172
|
+
const answer = source.slice(markerIndex + marker.length, -1);
|
|
173
|
+
if (!question.trim() || !answer.trim()) return null;
|
|
174
|
+
return { question, answer };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function hasVisibleMessageText(text) {
|
|
178
|
+
return normalizeMessageText(stripTerminalMarkupTags(text)).length > 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isInternalSystemLikeText(text) {
|
|
182
|
+
const normalized = normalizeMessageText(text);
|
|
183
|
+
if (!normalized) return false;
|
|
184
|
+
|
|
185
|
+
return /^\[SYSTEM:/i.test(normalized)
|
|
186
|
+
|| /^\[CHILD_UPDATE\b/i.test(normalized)
|
|
187
|
+
|| /^\[SESSION_MESSAGE(?:_RESPONSE)?\b/i.test(normalized)
|
|
188
|
+
|| /^Buffered child updates arrived /i.test(normalized)
|
|
189
|
+
|| /^There is an active recurring schedule every /i.test(normalized)
|
|
190
|
+
|| /^It remains active automatically after this turn completes/i.test(normalized)
|
|
191
|
+
|| /^Sub-agent spawned successfully\./i.test(normalized)
|
|
192
|
+
|| /^Message sent to sub-agent /i.test(normalized)
|
|
193
|
+
|| /^No sub-agents have been spawned yet\./i.test(normalized)
|
|
194
|
+
|| /^Sub-agent status report \(/i.test(normalized)
|
|
195
|
+
|| /^Active sessions \(/i.test(normalized)
|
|
196
|
+
|| /^Sub-agents completed:/i.test(normalized)
|
|
197
|
+
|| /^Sub-agent .* has been (completed gracefully|cancelled|deleted)\./i.test(normalized)
|
|
198
|
+
|| /^The runtime recovered this session after the worker lost the live Copilot session\./i.test(normalized)
|
|
199
|
+
|| /^The runtime recovered this session after the live Copilot session was lost on a worker\./i.test(normalized)
|
|
200
|
+
|| /^The runtime is replaying this turn after a worker restart/i.test(normalized)
|
|
201
|
+
|| /^The runtime detected missing Copilot session state for /i.test(normalized)
|
|
202
|
+
|| /^(spawn_agent|message_agent|check_agents|wait_for_agents|complete_agent|cancel_agent|delete_agent) failed/i.test(normalized);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isSessionMessageText(text) {
|
|
206
|
+
return /^\[SESSION_MESSAGE(?:_RESPONSE)?\b/i.test(normalizeMessageText(text));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseSessionMessageHeaderField(header, key) {
|
|
210
|
+
const source = String(header || "");
|
|
211
|
+
const marker = `${key}=`;
|
|
212
|
+
const start = source.indexOf(marker);
|
|
213
|
+
if (start < 0) return "";
|
|
214
|
+
const rest = source.slice(start + marker.length);
|
|
215
|
+
const nextMarkers = [
|
|
216
|
+
" request_id=",
|
|
217
|
+
" from=",
|
|
218
|
+
" subject=",
|
|
219
|
+
" reason=",
|
|
220
|
+
" expects_response=",
|
|
221
|
+
" expires_at=",
|
|
222
|
+
" verdict=",
|
|
223
|
+
];
|
|
224
|
+
let end = rest.length;
|
|
225
|
+
for (const nextMarker of nextMarkers) {
|
|
226
|
+
const index = rest.indexOf(nextMarker);
|
|
227
|
+
if (index >= 0) end = Math.min(end, index);
|
|
228
|
+
}
|
|
229
|
+
return rest.slice(0, end).trim();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildSessionMessageChatCard(event, text) {
|
|
233
|
+
const source = normalizeMessageText(text);
|
|
234
|
+
if (!source) return null;
|
|
235
|
+
|
|
236
|
+
const requestMatch = /^\[SESSION_MESSAGE\s+([^\]]+)\]\n?/i.exec(source);
|
|
237
|
+
if (requestMatch) {
|
|
238
|
+
const header = requestMatch[1];
|
|
239
|
+
const bodyMarker = "\nRequest body:\n";
|
|
240
|
+
const bodyIndex = source.indexOf(bodyMarker);
|
|
241
|
+
const body = bodyIndex >= 0
|
|
242
|
+
? source.slice(bodyIndex + bodyMarker.length).trim()
|
|
243
|
+
: source.slice(requestMatch[0].length).trim();
|
|
244
|
+
const requestId = parseSessionMessageHeaderField(header, "request_id");
|
|
245
|
+
const from = parseSessionMessageHeaderField(header, "from");
|
|
246
|
+
const subject = parseSessionMessageHeaderField(header, "subject");
|
|
247
|
+
const expectsResponse = /\bexpects_response=true\b/i.test(header);
|
|
248
|
+
const lines = [
|
|
249
|
+
from ? `From: session-${shortSessionId(from)}` : "",
|
|
250
|
+
requestId ? `Request ID: ${requestId}` : "",
|
|
251
|
+
subject ? `Subject: ${subject}` : "",
|
|
252
|
+
expectsResponse ? "Response: required" : "Response: not required",
|
|
253
|
+
"",
|
|
254
|
+
body,
|
|
255
|
+
].filter((line, index) => index === 4 || String(line || "").trim());
|
|
256
|
+
return {
|
|
257
|
+
id: `${event.sessionId}:${event.seq}`,
|
|
258
|
+
role: "system",
|
|
259
|
+
text: lines.join("\n"),
|
|
260
|
+
time: formatTimestamp(event.createdAt),
|
|
261
|
+
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
262
|
+
cardTitle: expectsResponse ? "Session Request" : "Session Message",
|
|
263
|
+
cardTitleColor: "cyan",
|
|
264
|
+
cardBorderColor: "cyan",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const responseMatch = /^\[SESSION_MESSAGE_RESPONSE\s+([^\]]+)\]\n?/i.exec(source);
|
|
269
|
+
if (responseMatch) {
|
|
270
|
+
const header = responseMatch[1];
|
|
271
|
+
const replyIntroRe = /^This is the requested cross-session response\.[^\n]*\n\n?/i;
|
|
272
|
+
const body = source.slice(responseMatch[0].length).trim().replace(replyIntroRe, "").trim();
|
|
273
|
+
const requestId = parseSessionMessageHeaderField(header, "request_id");
|
|
274
|
+
const from = parseSessionMessageHeaderField(header, "from");
|
|
275
|
+
const verdict = parseSessionMessageHeaderField(header, "verdict") || "answered";
|
|
276
|
+
const lines = [
|
|
277
|
+
from ? `From: session-${shortSessionId(from)}` : "",
|
|
278
|
+
requestId ? `Request ID: ${requestId}` : "",
|
|
279
|
+
verdict ? `Verdict: ${verdict}` : "",
|
|
280
|
+
"",
|
|
281
|
+
body,
|
|
282
|
+
].filter((line, index) => index === 3 || String(line || "").trim());
|
|
283
|
+
return {
|
|
284
|
+
id: `${event.sessionId}:${event.seq}`,
|
|
285
|
+
role: "system",
|
|
286
|
+
text: lines.join("\n"),
|
|
287
|
+
time: formatTimestamp(event.createdAt),
|
|
288
|
+
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
289
|
+
cardTitle: "Session Reply",
|
|
290
|
+
cardTitleColor: "green",
|
|
291
|
+
cardBorderColor: "green",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function deriveChatRole(event, fallbackRole, text) {
|
|
299
|
+
if (event?.eventType === "system.message") return "system";
|
|
300
|
+
if ((fallbackRole === "user" || fallbackRole === "assistant") && isInternalSystemLikeText(text)) return "system";
|
|
301
|
+
return fallbackRole;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function areMessagesEquivalent(left, right) {
|
|
305
|
+
if (!left || !right) return false;
|
|
306
|
+
if (left.role !== right.role) return false;
|
|
307
|
+
|
|
308
|
+
const leftText = comparableMessageText(left);
|
|
309
|
+
const rightText = comparableMessageText(right);
|
|
310
|
+
if (!leftText || !rightText || leftText !== rightText) return false;
|
|
311
|
+
|
|
312
|
+
const leftTime = Number(left.createdAt || 0);
|
|
313
|
+
const rightTime = Number(right.createdAt || 0);
|
|
314
|
+
if (left.optimistic || right.optimistic) return true;
|
|
315
|
+
if (!leftTime || !rightTime) return false;
|
|
316
|
+
return Math.abs(leftTime - rightTime) <= 10_000;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function dedupeChatMessages(chat = []) {
|
|
320
|
+
const deduped = [];
|
|
321
|
+
|
|
322
|
+
for (const message of chat) {
|
|
323
|
+
if (!message) continue;
|
|
324
|
+
const previous = deduped[deduped.length - 1];
|
|
325
|
+
if (!areMessagesEquivalent(previous, message)) {
|
|
326
|
+
deduped.push(message);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (previous?.optimistic && !message?.optimistic) {
|
|
331
|
+
deduped[deduped.length - 1] = message;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (!previous?.optimistic && message?.optimistic) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const previousTime = Number(previous?.createdAt || 0);
|
|
339
|
+
const currentTime = Number(message?.createdAt || 0);
|
|
340
|
+
deduped[deduped.length - 1] = currentTime >= previousTime ? message : previous;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return deduped;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildChatMessage(event, role) {
|
|
347
|
+
const rawText = messageTextFromEvent(event);
|
|
348
|
+
const sessionMessageCard = buildSessionMessageChatCard(event, rawText);
|
|
349
|
+
if (sessionMessageCard) return sessionMessageCard;
|
|
350
|
+
|
|
351
|
+
const text = extractVisibleChatText(rawText, role);
|
|
352
|
+
if (!hasVisibleMessageText(text)) return null;
|
|
353
|
+
return {
|
|
354
|
+
id: `${event.sessionId}:${event.seq}`,
|
|
355
|
+
role: deriveChatRole(event, role, text),
|
|
356
|
+
text,
|
|
357
|
+
time: formatTimestamp(event.createdAt),
|
|
358
|
+
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function shouldRenderSystemMessageAsActivity(event) {
|
|
363
|
+
// Most system.message events stay out of the chat pane. They include the
|
|
364
|
+
// per-turn system prompt sent to the LLM plus internal rehydration notices,
|
|
365
|
+
// and the full content remains in CMS for diagnostics. Cross-session
|
|
366
|
+
// request/reply protocol prompts are the exception because they are the
|
|
367
|
+
// durable user-visible communication channel between sessions.
|
|
368
|
+
if (event?.eventType !== "system.message") return false;
|
|
369
|
+
return !isSessionMessageText(messageTextFromEvent(event));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function buildEmbeddedSystemNoticeActivityItems(event, role) {
|
|
373
|
+
return extractEmbeddedSystemNoticeTexts(messageTextFromEvent(event), role)
|
|
374
|
+
.map((text, index) => formatActivity({
|
|
375
|
+
...event,
|
|
376
|
+
seq: `${event?.seq ?? "?"}:system:${index + 1}`,
|
|
377
|
+
eventType: "system.message",
|
|
378
|
+
data: { content: text },
|
|
379
|
+
}))
|
|
380
|
+
.filter(Boolean);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function reconcileOptimisticMessage(chat, incomingMessage) {
|
|
384
|
+
if (!Array.isArray(chat) || incomingMessage?.role !== "user") {
|
|
385
|
+
return [...(chat || [])];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const normalizedIncoming = normalizeMessageText(incomingMessage.text);
|
|
389
|
+
const parsedExchange = parseAskedAndAnsweredExchange(incomingMessage.text);
|
|
390
|
+
const normalizedAnsweredText = parsedExchange ? normalizeMessageText(parsedExchange.answer) : "";
|
|
391
|
+
let removed = false;
|
|
392
|
+
|
|
393
|
+
return chat.filter((message) => {
|
|
394
|
+
if (removed) return true;
|
|
395
|
+
if (!message?.optimistic || message.role !== incomingMessage.role) return true;
|
|
396
|
+
const normalizedMessageText = normalizeMessageText(message.text);
|
|
397
|
+
if (normalizedMessageText !== normalizedIncoming && normalizedMessageText !== normalizedAnsweredText) return true;
|
|
398
|
+
removed = true;
|
|
399
|
+
return false;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function messageTextFromEvent(event) {
|
|
404
|
+
const data = event?.data;
|
|
405
|
+
if (typeof data === "string") return data;
|
|
406
|
+
if (data && typeof data === "object") {
|
|
407
|
+
if (typeof data.content === "string") return data.content;
|
|
408
|
+
if (typeof data.text === "string") return data.text;
|
|
409
|
+
if (typeof data.message === "string") return data.message;
|
|
410
|
+
if (typeof data.question === "string") return data.question;
|
|
411
|
+
}
|
|
412
|
+
return summarizeJson(data);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function flattenRunsText(runs) {
|
|
416
|
+
return (runs || []).map((run) => run?.text || "").join("");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function summarizeActivityPreview(text, maxLen = 120) {
|
|
420
|
+
const compact = String(text || "")
|
|
421
|
+
.replace(/\s+/g, " ")
|
|
422
|
+
.trim();
|
|
423
|
+
if (!compact) return "";
|
|
424
|
+
return compact.length > maxLen
|
|
425
|
+
? `${compact.slice(0, maxLen - 3)}...`
|
|
426
|
+
: compact;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function joinUniqueActivityDetail(parts = []) {
|
|
430
|
+
const seen = new Set();
|
|
431
|
+
const normalized = [];
|
|
432
|
+
for (const part of parts) {
|
|
433
|
+
const text = String(part || "").trim();
|
|
434
|
+
if (!text) continue;
|
|
435
|
+
const key = text.toLowerCase();
|
|
436
|
+
if (seen.has(key)) continue;
|
|
437
|
+
seen.add(key);
|
|
438
|
+
normalized.push(text);
|
|
439
|
+
}
|
|
440
|
+
return normalized.join(" | ");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function formatDehydrationActivityDetail(event, fallbackBody = "") {
|
|
444
|
+
return joinUniqueActivityDetail([
|
|
445
|
+
event?.data?.reason,
|
|
446
|
+
event?.data?.detail,
|
|
447
|
+
event?.data?.message,
|
|
448
|
+
event?.data?.error,
|
|
449
|
+
fallbackBody,
|
|
450
|
+
]);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function formatLossyHandoffActivityDetail(event, fallbackBody = "") {
|
|
454
|
+
return joinUniqueActivityDetail([
|
|
455
|
+
event?.data?.message,
|
|
456
|
+
event?.data?.detail,
|
|
457
|
+
event?.data?.error,
|
|
458
|
+
fallbackBody,
|
|
459
|
+
]);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function formatRehydrationActivityDetail(event, fallbackBody = "") {
|
|
463
|
+
const rawBody = messageTextFromEvent(event);
|
|
464
|
+
const stripped = stripLeadingRehydrationNoticeText(fallbackBody);
|
|
465
|
+
return joinUniqueActivityDetail([
|
|
466
|
+
stripLeadingRehydrationNoticeText(rawBody),
|
|
467
|
+
stripped,
|
|
468
|
+
event?.data?.detail,
|
|
469
|
+
event?.data?.message,
|
|
470
|
+
]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function formatToolArgValue(value) {
|
|
474
|
+
if (value == null) return "null";
|
|
475
|
+
if (typeof value === "string") {
|
|
476
|
+
return JSON.stringify(value.length > 32 ? `${value.slice(0, 29)}...` : value);
|
|
477
|
+
}
|
|
478
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
479
|
+
if (Array.isArray(value)) return `[${value.length}]`;
|
|
480
|
+
if (typeof value === "object") return "{...}";
|
|
481
|
+
return String(value);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function formatToolArgsSummary(toolName, args) {
|
|
485
|
+
if (!args || typeof args !== "object") return "";
|
|
486
|
+
|
|
487
|
+
if (toolName === "wait") {
|
|
488
|
+
const seconds = args.seconds != null ? `${args.seconds}s` : "?";
|
|
489
|
+
const preserve = args.preserveWorkerAffinity === true ? " preserve=true" : "";
|
|
490
|
+
const reason = typeof args.reason === "string" && args.reason
|
|
491
|
+
? ` reason=${JSON.stringify(args.reason)}`
|
|
492
|
+
: "";
|
|
493
|
+
return ` ${seconds}${preserve}${reason}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (toolName === "cron") {
|
|
497
|
+
if (args.action === "cancel") return " cancel";
|
|
498
|
+
const seconds = args.seconds != null ? `${args.seconds}s` : "?";
|
|
499
|
+
const reason = typeof args.reason === "string" && args.reason
|
|
500
|
+
? ` reason=${JSON.stringify(args.reason)}`
|
|
501
|
+
: "";
|
|
502
|
+
return ` ${seconds}${reason}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const entries = Object.entries(args)
|
|
506
|
+
.slice(0, 4)
|
|
507
|
+
.map(([key, value]) => `${key}=${formatToolArgValue(value)}`);
|
|
508
|
+
if (entries.length === 0) return "";
|
|
509
|
+
const suffix = Object.keys(args).length > entries.length ? ", ..." : "";
|
|
510
|
+
return ` ${entries.join(", ")}${suffix}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function buildActivityPrefix(time) {
|
|
514
|
+
return time ? [{ text: `[${time}] `, color: "white" }] : [];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function buildLabeledActivityRuns(time, label, labelColor, detail = "", detailColor = "white") {
|
|
518
|
+
return [
|
|
519
|
+
...buildActivityPrefix(time),
|
|
520
|
+
{ text: label, color: labelColor },
|
|
521
|
+
...(detail ? [{ text: ` ${detail}`, color: detailColor }] : []),
|
|
522
|
+
];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function formatEventSnippet(event, maxLen = 96) {
|
|
526
|
+
const body = summarizeActivityPreview(stripTerminalMarkupTags(messageTextFromEvent(event)), maxLen);
|
|
527
|
+
return body || "";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function formatToolActivityRuns(time, event, phase = "start") {
|
|
531
|
+
const toolCallId = typeof event?.data?.toolCallId === "string" ? event.data.toolCallId : "";
|
|
532
|
+
const requestId = typeof event?.data?.requestId === "string" ? event.data.requestId : "";
|
|
533
|
+
const toolName = event?.data?.toolName
|
|
534
|
+
|| event?.data?.name
|
|
535
|
+
|| (toolCallId ? `tool call ${toolCallId.slice(0, 8)}` : requestId ? `tool request ${requestId.slice(0, 8)}` : "tool");
|
|
536
|
+
const args = event?.data?.arguments || event?.data?.args;
|
|
537
|
+
const durableSessionId = event?.data?.durableSessionId;
|
|
538
|
+
const summary = formatToolArgsSummary(toolName, args);
|
|
539
|
+
const phasePrefix = phase === "start"
|
|
540
|
+
? "▶"
|
|
541
|
+
: phase === "partial"
|
|
542
|
+
? "…"
|
|
543
|
+
: "✓";
|
|
544
|
+
const phaseColor = phase === "start"
|
|
545
|
+
? "yellow"
|
|
546
|
+
: phase === "partial"
|
|
547
|
+
? "cyan"
|
|
548
|
+
: "green";
|
|
549
|
+
|
|
550
|
+
return [
|
|
551
|
+
...buildActivityPrefix(time),
|
|
552
|
+
{
|
|
553
|
+
text: `${phasePrefix} ${toolName}${summary}`,
|
|
554
|
+
color: phaseColor,
|
|
555
|
+
},
|
|
556
|
+
...(durableSessionId
|
|
557
|
+
? [{ text: ` [sess ${shortSessionId(durableSessionId)}]`, color: "gray" }]
|
|
558
|
+
: []),
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function formatActivity(event) {
|
|
563
|
+
const time = formatTimestamp(event.createdAt);
|
|
564
|
+
const body = formatEventSnippet(event);
|
|
565
|
+
let runs = null;
|
|
566
|
+
|
|
567
|
+
switch (event.eventType) {
|
|
568
|
+
case "assistant.usage":
|
|
569
|
+
case "session.info":
|
|
570
|
+
case "session.idle":
|
|
571
|
+
case "session.usage_info":
|
|
572
|
+
case "pending_messages.modified":
|
|
573
|
+
case "pending_messages.cancelled":
|
|
574
|
+
case "abort":
|
|
575
|
+
case "assistant.turn_end":
|
|
576
|
+
case "assistant.streaming_progress":
|
|
577
|
+
return null;
|
|
578
|
+
|
|
579
|
+
case "tool.execution_start":
|
|
580
|
+
runs = formatToolActivityRuns(time, event, "start");
|
|
581
|
+
break;
|
|
582
|
+
|
|
583
|
+
case "tool.execution_complete":
|
|
584
|
+
runs = formatToolActivityRuns(time, event, "complete");
|
|
585
|
+
break;
|
|
586
|
+
|
|
587
|
+
case "tool.execution_partial_result":
|
|
588
|
+
runs = formatToolActivityRuns(time, event, "partial");
|
|
589
|
+
break;
|
|
590
|
+
|
|
591
|
+
case "tool.execution_progress": {
|
|
592
|
+
const data = (event?.data ?? {}) || {};
|
|
593
|
+
const toolName = data.toolName || data.name || "tool";
|
|
594
|
+
const detail = typeof data.message === "string" && data.message
|
|
595
|
+
? data.message
|
|
596
|
+
: typeof data.detail === "string" && data.detail
|
|
597
|
+
? data.detail
|
|
598
|
+
: (data.percent != null ? `${Math.round(Number(data.percent))}%` : "running…");
|
|
599
|
+
runs = buildLabeledActivityRuns(time, "[tool]", "cyan", `${toolName} ${detail}`, "white");
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
case "assistant.reasoning":
|
|
604
|
+
runs = buildLabeledActivityRuns(time, "[reasoning]", "gray", body || "…", "white");
|
|
605
|
+
break;
|
|
606
|
+
|
|
607
|
+
case "assistant.intent": {
|
|
608
|
+
const data = (event?.data ?? {}) || {};
|
|
609
|
+
const text = typeof data.summary === "string" && data.summary
|
|
610
|
+
? data.summary
|
|
611
|
+
: typeof data.intent === "string" && data.intent
|
|
612
|
+
? data.intent
|
|
613
|
+
: (body || "");
|
|
614
|
+
runs = buildLabeledActivityRuns(time, "[intent]", "gray", text || "…", "white");
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
case "assistant.turn_start":
|
|
619
|
+
runs = buildLabeledActivityRuns(time, "[turn start]", "gray", body);
|
|
620
|
+
break;
|
|
621
|
+
|
|
622
|
+
case "session.turn_completed":
|
|
623
|
+
runs = buildLabeledActivityRuns(
|
|
624
|
+
time,
|
|
625
|
+
"[turn completed]",
|
|
626
|
+
"gray",
|
|
627
|
+
event?.data?.iteration != null ? `iter ${event.data.iteration}` : body,
|
|
628
|
+
);
|
|
629
|
+
break;
|
|
630
|
+
|
|
631
|
+
case "session.lossy_handoff":
|
|
632
|
+
runs = buildLabeledActivityRuns(
|
|
633
|
+
time,
|
|
634
|
+
"[lossy handoff]",
|
|
635
|
+
"yellow",
|
|
636
|
+
formatLossyHandoffActivityDetail(event, body) || "handoff to a new worker",
|
|
637
|
+
);
|
|
638
|
+
break;
|
|
639
|
+
|
|
640
|
+
case "session.dehydrated":
|
|
641
|
+
runs = buildLabeledActivityRuns(
|
|
642
|
+
time,
|
|
643
|
+
"[dehydrated]",
|
|
644
|
+
"cyan",
|
|
645
|
+
formatDehydrationActivityDetail(event, body),
|
|
646
|
+
);
|
|
647
|
+
break;
|
|
648
|
+
|
|
649
|
+
case "session.hydrated":
|
|
650
|
+
case "session.rehydrated":
|
|
651
|
+
runs = buildLabeledActivityRuns(time, "[rehydrated]", "green", body);
|
|
652
|
+
break;
|
|
653
|
+
|
|
654
|
+
case "session.wait_started": {
|
|
655
|
+
const seconds = event?.data?.seconds != null ? `${event.data.seconds}s` : "?";
|
|
656
|
+
const reason = typeof event?.data?.reason === "string" && event.data.reason
|
|
657
|
+
? ` reason=${JSON.stringify(event.data.reason)}`
|
|
658
|
+
: "";
|
|
659
|
+
const preserve = event?.data?.preserveAffinity ? " preserve=true" : "";
|
|
660
|
+
runs = buildLabeledActivityRuns(time, "[wait]", "yellow", `${seconds}${preserve}${reason}`.trim());
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
case "session.input_required_started":
|
|
665
|
+
runs = buildLabeledActivityRuns(time, "[input]", "yellow", body);
|
|
666
|
+
break;
|
|
667
|
+
|
|
668
|
+
case "session.agent_spawned":
|
|
669
|
+
runs = buildLabeledActivityRuns(
|
|
670
|
+
time,
|
|
671
|
+
"[spawn]",
|
|
672
|
+
"cyan",
|
|
673
|
+
event?.data?.agentId || shortSessionId(event?.data?.childSessionId),
|
|
674
|
+
"white",
|
|
675
|
+
);
|
|
676
|
+
break;
|
|
677
|
+
|
|
678
|
+
case "session.cron_started":
|
|
679
|
+
runs = buildLabeledActivityRuns(
|
|
680
|
+
time,
|
|
681
|
+
"[cron]",
|
|
682
|
+
"magenta",
|
|
683
|
+
`started${event?.data?.seconds != null ? ` ${event.data.seconds}s` : ""}${event?.data?.reason ? ` reason=${JSON.stringify(event.data.reason)}` : ""}`,
|
|
684
|
+
);
|
|
685
|
+
break;
|
|
686
|
+
|
|
687
|
+
case "session.cron_at_scheduled":
|
|
688
|
+
case "session.cron_at_started":
|
|
689
|
+
runs = buildLabeledActivityRuns(
|
|
690
|
+
time,
|
|
691
|
+
"[cron]",
|
|
692
|
+
"magenta",
|
|
693
|
+
`scheduled${event?.data?.nextFireAt ? ` ${formatCronTimestamp(event.data.nextFireAt)}` : ""}${event?.data?.reason ? ` reason=${JSON.stringify(event.data.reason)}` : ""}`,
|
|
694
|
+
);
|
|
695
|
+
break;
|
|
696
|
+
|
|
697
|
+
case "session.cron_fired":
|
|
698
|
+
runs = buildLabeledActivityRuns(
|
|
699
|
+
time,
|
|
700
|
+
"[cron]",
|
|
701
|
+
"magenta",
|
|
702
|
+
`fired${event?.data?.reason ? ` reason=${JSON.stringify(event.data.reason)}` : ""}`,
|
|
703
|
+
);
|
|
704
|
+
break;
|
|
705
|
+
|
|
706
|
+
case "session.cron_at_fired":
|
|
707
|
+
runs = buildLabeledActivityRuns(
|
|
708
|
+
time,
|
|
709
|
+
"[cron]",
|
|
710
|
+
"magenta",
|
|
711
|
+
`fired${event?.data?.scheduledAt ? ` ${formatCronTimestamp(event.data.scheduledAt)}` : ""}${event?.data?.reason ? ` reason=${JSON.stringify(event.data.reason)}` : ""}`,
|
|
712
|
+
);
|
|
713
|
+
break;
|
|
714
|
+
|
|
715
|
+
case "session.cron_cancelled":
|
|
716
|
+
case "session.cron_at_cancelled":
|
|
717
|
+
runs = buildLabeledActivityRuns(
|
|
718
|
+
time,
|
|
719
|
+
"[cron]",
|
|
720
|
+
"magenta",
|
|
721
|
+
`cancelled${event?.data?.reason ? ` reason=${JSON.stringify(event.data.reason)}` : ""}`,
|
|
722
|
+
);
|
|
723
|
+
break;
|
|
724
|
+
|
|
725
|
+
case "session.cron_at_completed":
|
|
726
|
+
runs = buildLabeledActivityRuns(
|
|
727
|
+
time,
|
|
728
|
+
"[cron]",
|
|
729
|
+
"magenta",
|
|
730
|
+
`completed${event?.data?.reason ? ` reason=${JSON.stringify(event.data.reason)}` : ""}`,
|
|
731
|
+
);
|
|
732
|
+
break;
|
|
733
|
+
|
|
734
|
+
case "session.command_received":
|
|
735
|
+
runs = buildLabeledActivityRuns(
|
|
736
|
+
time,
|
|
737
|
+
"[command]",
|
|
738
|
+
"magenta",
|
|
739
|
+
`/${event?.data?.cmd || "?"}${body ? ` ${body}` : ""}`,
|
|
740
|
+
);
|
|
741
|
+
break;
|
|
742
|
+
|
|
743
|
+
case "session.command_completed":
|
|
744
|
+
runs = buildLabeledActivityRuns(
|
|
745
|
+
time,
|
|
746
|
+
"[command]",
|
|
747
|
+
"magenta",
|
|
748
|
+
`/${event?.data?.cmd || "?"} ok${body ? ` ${body}` : ""}`,
|
|
749
|
+
);
|
|
750
|
+
break;
|
|
751
|
+
|
|
752
|
+
case "session.compaction_start":
|
|
753
|
+
case "session.compaction_complete":
|
|
754
|
+
runs = formatCompactionActivityRuns(time, event.eventType, event.data || {});
|
|
755
|
+
break;
|
|
756
|
+
|
|
757
|
+
case "session.error":
|
|
758
|
+
runs = buildLabeledActivityRuns(time, "[error]", "red", body || "session error", "white");
|
|
759
|
+
break;
|
|
760
|
+
|
|
761
|
+
case "system.message":
|
|
762
|
+
if (isRehydrationNoticeText(messageTextFromEvent(event))) {
|
|
763
|
+
runs = buildLabeledActivityRuns(
|
|
764
|
+
time,
|
|
765
|
+
"[rehydrated]",
|
|
766
|
+
"green",
|
|
767
|
+
formatRehydrationActivityDetail(event, body) || "conversation preserved",
|
|
768
|
+
);
|
|
769
|
+
} else {
|
|
770
|
+
runs = buildLabeledActivityRuns(time, "[system]", "gray", body || "system message");
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
773
|
+
|
|
774
|
+
default:
|
|
775
|
+
runs = [
|
|
776
|
+
...buildActivityPrefix(time),
|
|
777
|
+
{ text: `[${event.eventType}]`, color: "gray" },
|
|
778
|
+
...(body ? [{ text: ` ${body}`, color: "white" }] : []),
|
|
779
|
+
];
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
id: `${event.sessionId}:${event.seq}`,
|
|
785
|
+
eventType: event.eventType,
|
|
786
|
+
time,
|
|
787
|
+
seq: Number.isFinite(Number(event?.seq)) ? Number(event.seq) : 0,
|
|
788
|
+
createdAt: event?.createdAt instanceof Date
|
|
789
|
+
? event.createdAt.getTime()
|
|
790
|
+
: (event?.createdAt ? new Date(event.createdAt).getTime() : 0),
|
|
791
|
+
text: flattenRunsText(runs),
|
|
792
|
+
line: runs,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function getNextHistoryEventLimit(currentLimit = DEFAULT_HISTORY_EVENT_LIMIT) {
|
|
797
|
+
const safeCurrent = Math.max(DEFAULT_HISTORY_EVENT_LIMIT, Number(currentLimit) || DEFAULT_HISTORY_EVENT_LIMIT);
|
|
798
|
+
const nextLimit = HISTORY_EVENT_LIMIT_STEPS.find((limit) => limit > safeCurrent);
|
|
799
|
+
return nextLimit || Math.min(100_000, safeCurrent * 2);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function buildHistoryModel(events = [], options = {}) {
|
|
803
|
+
const requestedLimit = Math.max(
|
|
804
|
+
DEFAULT_HISTORY_EVENT_LIMIT,
|
|
805
|
+
Number(options.requestedLimit ?? options.eventLimit ?? DEFAULT_HISTORY_EVENT_LIMIT) || DEFAULT_HISTORY_EVENT_LIMIT,
|
|
806
|
+
);
|
|
807
|
+
const chat = [];
|
|
808
|
+
const activity = [];
|
|
809
|
+
const storedEvents = [];
|
|
810
|
+
|
|
811
|
+
for (const event of events) {
|
|
812
|
+
storedEvents.push(event);
|
|
813
|
+
if (event.eventType === "user.message") {
|
|
814
|
+
activity.push(...buildEmbeddedSystemNoticeActivityItems(event, "user"));
|
|
815
|
+
const message = buildChatMessage(event, "user");
|
|
816
|
+
if (message) chat.push(message);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (event.eventType === "assistant.message") {
|
|
820
|
+
activity.push(...buildEmbeddedSystemNoticeActivityItems(event, "assistant"));
|
|
821
|
+
const message = buildChatMessage(event, "assistant");
|
|
822
|
+
if (message) chat.push(message);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (event.eventType === "system.message") {
|
|
826
|
+
if (shouldRenderSystemMessageAsActivity(event)) {
|
|
827
|
+
const activityItem = formatActivity(event);
|
|
828
|
+
if (activityItem) activity.push(activityItem);
|
|
829
|
+
} else {
|
|
830
|
+
const message = buildChatMessage(event, "system");
|
|
831
|
+
if (message) chat.push(message);
|
|
832
|
+
}
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const activityItem = formatActivity(event);
|
|
836
|
+
if (activityItem) activity.push(activityItem);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
chat: dedupeChatMessages(chat),
|
|
841
|
+
activity,
|
|
842
|
+
events: storedEvents.slice(-requestedLimit),
|
|
843
|
+
loadedEventLimit: requestedLimit,
|
|
844
|
+
loadedEventCount: storedEvents.length,
|
|
845
|
+
hasOlderEvents: storedEvents.length >= requestedLimit,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function appendEventToHistory(history, event) {
|
|
850
|
+
const existingEvents = Array.isArray(history?.events) ? history.events : [];
|
|
851
|
+
const loadedEventLimit = Math.max(
|
|
852
|
+
DEFAULT_HISTORY_EVENT_LIMIT,
|
|
853
|
+
Number(history?.loadedEventLimit ?? DEFAULT_HISTORY_EVENT_LIMIT) || DEFAULT_HISTORY_EVENT_LIMIT,
|
|
854
|
+
);
|
|
855
|
+
const nextEvents = existingEvents.length > 0 && existingEvents[existingEvents.length - 1]?.seq === event?.seq
|
|
856
|
+
? existingEvents
|
|
857
|
+
: [...existingEvents, event].slice(-loadedEventLimit);
|
|
858
|
+
const next = {
|
|
859
|
+
chat: clampHistoryItems(history?.chat || [], loadedEventLimit),
|
|
860
|
+
activity: clampHistoryItems(history?.activity || [], loadedEventLimit),
|
|
861
|
+
events: nextEvents,
|
|
862
|
+
lastSeq: event.seq,
|
|
863
|
+
loadedEventLimit,
|
|
864
|
+
loadedEventCount: Math.max(Number(history?.loadedEventCount || 0), nextEvents.length),
|
|
865
|
+
hasOlderEvents: Boolean(history?.hasOlderEvents),
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
if (event.eventType === "user.message") {
|
|
869
|
+
next.activity.push(...buildEmbeddedSystemNoticeActivityItems(event, "user"));
|
|
870
|
+
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
|
871
|
+
const message = buildChatMessage(event, "user");
|
|
872
|
+
if (!message) return next;
|
|
873
|
+
next.chat = reconcileOptimisticMessage(next.chat, message);
|
|
874
|
+
next.chat.push(message);
|
|
875
|
+
next.chat = clampHistoryItems(dedupeChatMessages(next.chat), loadedEventLimit);
|
|
876
|
+
return next;
|
|
877
|
+
}
|
|
878
|
+
if (event.eventType === "assistant.message") {
|
|
879
|
+
next.activity.push(...buildEmbeddedSystemNoticeActivityItems(event, "assistant"));
|
|
880
|
+
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
|
881
|
+
const message = buildChatMessage(event, "assistant");
|
|
882
|
+
if (!message) return next;
|
|
883
|
+
next.chat.push(message);
|
|
884
|
+
next.chat = clampHistoryItems(dedupeChatMessages(next.chat), loadedEventLimit);
|
|
885
|
+
return next;
|
|
886
|
+
}
|
|
887
|
+
if (event.eventType === "system.message") {
|
|
888
|
+
if (shouldRenderSystemMessageAsActivity(event)) {
|
|
889
|
+
const activityItem = formatActivity(event);
|
|
890
|
+
if (activityItem) {
|
|
891
|
+
next.activity.push(activityItem);
|
|
892
|
+
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
|
893
|
+
}
|
|
894
|
+
} else {
|
|
895
|
+
const message = buildChatMessage(event, "system");
|
|
896
|
+
if (!message) return next;
|
|
897
|
+
next.chat.push(message);
|
|
898
|
+
next.chat = clampHistoryItems(dedupeChatMessages(next.chat), loadedEventLimit);
|
|
899
|
+
}
|
|
900
|
+
return next;
|
|
901
|
+
}
|
|
902
|
+
const activityItem = formatActivity(event);
|
|
903
|
+
if (activityItem) {
|
|
904
|
+
next.activity.push(activityItem);
|
|
905
|
+
next.activity = clampHistoryItems(next.activity, loadedEventLimit);
|
|
906
|
+
}
|
|
907
|
+
return next;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export function createSplashCard(branding, session = null) {
|
|
911
|
+
const splash = typeof session?.splash === "string" && session.splash.trim()
|
|
912
|
+
? session.splash
|
|
913
|
+
: branding?.splash;
|
|
914
|
+
if (!splash) return [];
|
|
915
|
+
const title = session?.isSystem
|
|
916
|
+
? canonicalSystemTitle(session, branding?.title || "PilotSwarm")
|
|
917
|
+
: (session?.title || branding?.title || "PilotSwarm");
|
|
918
|
+
const hint = "{gray-fg}Start interacting with this session to replace the splash screen.{/gray-fg}";
|
|
919
|
+
return [{
|
|
920
|
+
id: `splash:${title}`,
|
|
921
|
+
role: "system",
|
|
922
|
+
text: `${splash}\n\n${hint}`,
|
|
923
|
+
time: "",
|
|
924
|
+
splash: true,
|
|
925
|
+
}];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export function buildSessionLabel(session) {
|
|
929
|
+
const title = session.title || shortSessionId(session.sessionId);
|
|
930
|
+
const shortId = shortSessionId(session.sessionId);
|
|
931
|
+
return title.includes(shortId) ? title : `${title} (${shortId})`;
|
|
932
|
+
}
|