@oversk7/pi-web-dsh-style 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +90 -0
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/abort-session.ts +150 -0
- package/attachment-store.ts +161 -0
- package/file-preview.ts +94 -0
- package/index.ts +177 -0
- package/local-session.ts +190 -0
- package/package.json +70 -0
- package/pi-rpc.ts +253 -0
- package/server.ts +3060 -0
- package/sse-channel.ts +72 -0
- package/state-store.ts +64 -0
- package/transcript.ts +328 -0
- package/web/app.css +535 -0
- package/web/app.js +4228 -0
- package/web/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/favicon.svg +4 -0
- package/web/index.html +25 -0
- package/web/manifest.webmanifest +9 -0
- package/windows-clipboard.ts +81 -0
package/sse-channel.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type SseEvent = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export interface SseWritable {
|
|
4
|
+
write(payload: string): boolean;
|
|
5
|
+
once(event: "drain", listener: () => void): unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SseClient {
|
|
9
|
+
res: SseWritable;
|
|
10
|
+
waitingForDrain: boolean;
|
|
11
|
+
pendingEvents: Map<string, SseEvent>;
|
|
12
|
+
closed: boolean;
|
|
13
|
+
onError: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function eventKey(event: SseEvent): string {
|
|
17
|
+
const type = typeof event.type === "string" ? event.type : "event";
|
|
18
|
+
const sessionId = typeof event.sessionId === "string" ? event.sessionId : "";
|
|
19
|
+
return sessionId ? `${type}:${sessionId}` : type;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createSseClient(res: SseWritable, onError: () => void): SseClient {
|
|
23
|
+
return {
|
|
24
|
+
res,
|
|
25
|
+
waitingForDrain: false,
|
|
26
|
+
pendingEvents: new Map(),
|
|
27
|
+
closed: false,
|
|
28
|
+
onError,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeEvent(client: SseClient, event: SseEvent): boolean {
|
|
33
|
+
try {
|
|
34
|
+
const accepted = client.res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
35
|
+
if (accepted) return true;
|
|
36
|
+
client.waitingForDrain = true;
|
|
37
|
+
client.res.once("drain", () => flushPendingEvents(client));
|
|
38
|
+
return false;
|
|
39
|
+
} catch {
|
|
40
|
+
closeSseClient(client);
|
|
41
|
+
client.onError();
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function flushPendingEvents(client: SseClient): void {
|
|
47
|
+
if (client.closed) return;
|
|
48
|
+
client.waitingForDrain = false;
|
|
49
|
+
while (!client.waitingForDrain && client.pendingEvents.size > 0) {
|
|
50
|
+
const next = client.pendingEvents.entries().next().value as [string, SseEvent] | undefined;
|
|
51
|
+
if (!next) return;
|
|
52
|
+
client.pendingEvents.delete(next[0]);
|
|
53
|
+
writeEvent(client, next[1]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sendSseEvent(client: SseClient, event: SseEvent): void {
|
|
58
|
+
if (client.closed) return;
|
|
59
|
+
if (client.waitingForDrain) {
|
|
60
|
+
// Keep only the newest event of each kind while the browser catches up.
|
|
61
|
+
// This bounds memory even when snapshots contain a long conversation.
|
|
62
|
+
client.pendingEvents.set(eventKey(event), event);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
writeEvent(client, event);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function closeSseClient(client: SseClient): void {
|
|
69
|
+
client.closed = true;
|
|
70
|
+
client.waitingForDrain = false;
|
|
71
|
+
client.pendingEvents.clear();
|
|
72
|
+
}
|
package/state-store.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
interface ReadResult {
|
|
6
|
+
kind: "missing" | "present";
|
|
7
|
+
raw?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function readOptional(path: string): Promise<ReadResult> {
|
|
11
|
+
try {
|
|
12
|
+
return { kind: "present", raw: await readFile(path, "utf8") };
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { kind: "missing" };
|
|
15
|
+
throw new Error(`Unable to read pi-web state at ${path}`, { cause: error });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseState<T>(path: string, raw: string, validate: (value: unknown) => value is T): T {
|
|
20
|
+
let parsed: unknown;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(raw);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(`Invalid JSON in pi-web state at ${path}`, { cause: error });
|
|
25
|
+
}
|
|
26
|
+
if (!validate(parsed)) throw new Error(`Invalid pi-web state structure at ${path}`);
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeJsonAtomically(path: string, value: unknown): Promise<void> {
|
|
31
|
+
await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function writeTextAtomically(path: string, content: string): Promise<void> {
|
|
35
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
36
|
+
await mkdir(dirname(path), { recursive: true });
|
|
37
|
+
try {
|
|
38
|
+
await writeFile(tmp, content, "utf8");
|
|
39
|
+
await rename(tmp, path);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
await unlink(tmp).catch(() => {});
|
|
42
|
+
throw new Error(`Unable to write pi-web state at ${path}`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function loadJsonWithLegacyMigration<T>(options: {
|
|
47
|
+
stateFile: string;
|
|
48
|
+
legacyStateFile?: string;
|
|
49
|
+
validate: (value: unknown) => value is T;
|
|
50
|
+
}): Promise<T | undefined> {
|
|
51
|
+
const current = await readOptional(options.stateFile);
|
|
52
|
+
if (current.kind === "present") {
|
|
53
|
+
return parseState(options.stateFile, current.raw!, options.validate);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const legacyPath = options.legacyStateFile;
|
|
57
|
+
if (!legacyPath || resolve(legacyPath) === resolve(options.stateFile)) return undefined;
|
|
58
|
+
const legacy = await readOptional(legacyPath);
|
|
59
|
+
if (legacy.kind === "missing") return undefined;
|
|
60
|
+
|
|
61
|
+
const parsed = parseState(legacyPath, legacy.raw!, options.validate);
|
|
62
|
+
await writeTextAtomically(options.stateFile, legacy.raw!);
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
package/transcript.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
export type Json = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export interface RenderedBlock {
|
|
4
|
+
type: "text" | "thinking" | "toolCall";
|
|
5
|
+
text?: string;
|
|
6
|
+
thinking?: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
arguments?: unknown;
|
|
10
|
+
argumentsText?: string;
|
|
11
|
+
result?: { content: string; isError: boolean; details?: unknown };
|
|
12
|
+
running?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RenderedMessage {
|
|
16
|
+
kind: "user" | "assistant" | "toolResult" | "bashExecution" | "compaction" | "notice";
|
|
17
|
+
id?: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
blocks?: RenderedBlock[];
|
|
20
|
+
toolCallId?: string;
|
|
21
|
+
toolName?: string;
|
|
22
|
+
isError?: boolean;
|
|
23
|
+
timestamp?: string | number;
|
|
24
|
+
usage?: unknown;
|
|
25
|
+
model?: string;
|
|
26
|
+
provider?: string;
|
|
27
|
+
stopReason?: string;
|
|
28
|
+
command?: string;
|
|
29
|
+
output?: string;
|
|
30
|
+
streaming?: boolean;
|
|
31
|
+
status?: "running" | "complete" | "error" | "aborted";
|
|
32
|
+
reason?: string;
|
|
33
|
+
tokensBefore?: number;
|
|
34
|
+
estimatedTokensAfter?: number;
|
|
35
|
+
errorMessage?: string;
|
|
36
|
+
willRetry?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function timestampValue(value: unknown): string | number | undefined {
|
|
40
|
+
return typeof value === "string" || typeof value === "number" ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function contentText(content: unknown): string {
|
|
44
|
+
if (typeof content === "string") return stripAnsi(content);
|
|
45
|
+
if (Array.isArray(content)) {
|
|
46
|
+
return content.map((part) => {
|
|
47
|
+
if (!part || typeof part !== "object") return "";
|
|
48
|
+
const p = part as Json;
|
|
49
|
+
if (p.type === "text" && typeof p.text === "string") return stripAnsi(p.text);
|
|
50
|
+
if (p.type === "thinking" && typeof p.thinking === "string") return stripAnsi(p.thinking);
|
|
51
|
+
return "";
|
|
52
|
+
}).filter(Boolean).join("\n");
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Strip ANSI SGR/escape sequences (pi appends colored turn summaries to text). */
|
|
58
|
+
export function stripAnsi(value: string): string {
|
|
59
|
+
return value.replace(/\u001b\[[0-9;:<>?]*[ -/]*[@-~]/g, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function summarizeArgs(args: unknown): string {
|
|
63
|
+
if (!args || typeof args !== "object") return String(args ?? "");
|
|
64
|
+
const record = args as Json;
|
|
65
|
+
if (typeof record.command === "string") return record.command;
|
|
66
|
+
if (typeof record.pattern === "string") return record.pattern;
|
|
67
|
+
if (typeof record.path === "string") return record.path;
|
|
68
|
+
try {
|
|
69
|
+
const text = JSON.stringify(args);
|
|
70
|
+
return text.length > 200 ? `${text.slice(0, 200)}…` : text;
|
|
71
|
+
} catch {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function renderAssistantMessage(message: Json): RenderedMessage {
|
|
77
|
+
const blocks: RenderedBlock[] = [];
|
|
78
|
+
if (Array.isArray(message.content)) {
|
|
79
|
+
for (const part of message.content as Json[]) {
|
|
80
|
+
if (!part || typeof part !== "object") continue;
|
|
81
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
82
|
+
blocks.push({ type: "text", text: stripAnsi(part.text) });
|
|
83
|
+
} else if (part.type === "thinking" && typeof part.thinking === "string") {
|
|
84
|
+
blocks.push({ type: "thinking", thinking: stripAnsi(part.thinking) });
|
|
85
|
+
} else if (part.type === "toolCall") {
|
|
86
|
+
const args = part.arguments;
|
|
87
|
+
blocks.push({
|
|
88
|
+
type: "toolCall",
|
|
89
|
+
id: typeof part.id === "string" ? part.id : undefined,
|
|
90
|
+
name: typeof part.name === "string" ? part.name : "tool",
|
|
91
|
+
arguments: args,
|
|
92
|
+
argumentsText: summarizeArgs(args),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const stopReason = typeof message.stopReason === "string" ? message.stopReason : undefined;
|
|
98
|
+
const errorMessage = typeof message.errorMessage === "string" ? stripAnsi(message.errorMessage) : undefined;
|
|
99
|
+
return {
|
|
100
|
+
kind: "assistant",
|
|
101
|
+
blocks,
|
|
102
|
+
timestamp: timestampValue(message.timestamp),
|
|
103
|
+
usage: message.usage ?? undefined,
|
|
104
|
+
model: typeof message.model === "string" ? message.model : undefined,
|
|
105
|
+
provider: typeof message.provider === "string" ? message.provider : undefined,
|
|
106
|
+
stopReason,
|
|
107
|
+
errorMessage,
|
|
108
|
+
isError: stopReason === "error" || Boolean(errorMessage),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function renderMessage(message: Json): RenderedMessage | undefined {
|
|
113
|
+
if (!message || typeof message !== "object") return undefined;
|
|
114
|
+
const role = message.role;
|
|
115
|
+
if (role === "user") {
|
|
116
|
+
return { kind: "user", text: contentText(message.content), timestamp: timestampValue(message.timestamp) };
|
|
117
|
+
}
|
|
118
|
+
if (role === "assistant") return renderAssistantMessage(message);
|
|
119
|
+
if (role === "toolResult") {
|
|
120
|
+
return {
|
|
121
|
+
kind: "toolResult",
|
|
122
|
+
toolCallId: typeof message.toolCallId === "string" ? message.toolCallId : undefined,
|
|
123
|
+
toolName: typeof message.toolName === "string" ? message.toolName : undefined,
|
|
124
|
+
text: contentText(message.content),
|
|
125
|
+
isError: Boolean(message.isError),
|
|
126
|
+
timestamp: timestampValue(message.timestamp),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (role === "bashExecution") {
|
|
130
|
+
return {
|
|
131
|
+
kind: "bashExecution",
|
|
132
|
+
command: typeof message.command === "string" ? stripAnsi(message.command) : undefined,
|
|
133
|
+
output: typeof message.output === "string" ? stripAnsi(message.output) : undefined,
|
|
134
|
+
timestamp: timestampValue(message.timestamp),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function renderCompaction(entry: Json): RenderedMessage {
|
|
141
|
+
const numberField = (key: string): number | undefined => {
|
|
142
|
+
const value = entry[key];
|
|
143
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
kind: "compaction",
|
|
147
|
+
id: typeof entry.id === "string" ? entry.id : undefined,
|
|
148
|
+
text: typeof entry.summary === "string" ? stripAnsi(entry.summary) : "",
|
|
149
|
+
timestamp: timestampValue(entry.timestamp),
|
|
150
|
+
status: "complete",
|
|
151
|
+
tokensBefore: numberField("tokensBefore"),
|
|
152
|
+
estimatedTokensAfter: numberField("estimatedTokensAfter"),
|
|
153
|
+
usage: entry.usage ?? undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function renderTranscript(rawMessages: unknown): RenderedMessage[] {
|
|
158
|
+
if (!Array.isArray(rawMessages)) return [];
|
|
159
|
+
const out: RenderedMessage[] = [];
|
|
160
|
+
for (const raw of rawMessages) {
|
|
161
|
+
const rendered = renderMessage(raw as Json);
|
|
162
|
+
if (rendered) out.push(rendered);
|
|
163
|
+
}
|
|
164
|
+
return attachToolResults(out);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function renderSessionEntries(rawEntries: unknown, leafValue: unknown): RenderedMessage[] {
|
|
168
|
+
if (!Array.isArray(rawEntries)) return [];
|
|
169
|
+
const entries = rawEntries.filter((entry): entry is Json => Boolean(entry && typeof entry === "object"));
|
|
170
|
+
const byId = new Map<string, Json>();
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (typeof entry.id === "string") byId.set(entry.id, entry);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let current: Json | undefined = typeof leafValue === "string" ? byId.get(leafValue) : undefined;
|
|
176
|
+
if (!current) current = entries[entries.length - 1];
|
|
177
|
+
const path: Json[] = [];
|
|
178
|
+
const seen = new Set<string>();
|
|
179
|
+
while (current) {
|
|
180
|
+
const id: string | undefined = typeof current.id === "string" ? current.id : undefined;
|
|
181
|
+
if (id && seen.has(id)) break;
|
|
182
|
+
if (id) seen.add(id);
|
|
183
|
+
path.push(current);
|
|
184
|
+
const parentId: string | undefined = typeof current.parentId === "string" ? current.parentId : undefined;
|
|
185
|
+
current = parentId && parentId !== id ? byId.get(parentId) : undefined;
|
|
186
|
+
}
|
|
187
|
+
path.reverse();
|
|
188
|
+
|
|
189
|
+
const rendered: RenderedMessage[] = [];
|
|
190
|
+
for (const entry of path) {
|
|
191
|
+
if (entry.type === "message" && entry.message && typeof entry.message === "object") {
|
|
192
|
+
const message = renderMessage(entry.message as Json);
|
|
193
|
+
if (message) rendered.push(message);
|
|
194
|
+
} else if (entry.type === "compaction") {
|
|
195
|
+
rendered.push(renderCompaction(entry));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return attachToolResults(rendered);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function attachToolResults(messages: RenderedMessage[]): RenderedMessage[] {
|
|
202
|
+
const byCallId = new Map<string, RenderedBlock>();
|
|
203
|
+
for (const message of messages) {
|
|
204
|
+
if (message.kind !== "assistant" || !message.blocks) continue;
|
|
205
|
+
for (const block of message.blocks) {
|
|
206
|
+
if (block.type === "toolCall" && block.id) byCallId.set(block.id, block);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const attached = new Set<RenderedMessage>();
|
|
211
|
+
for (const message of messages) {
|
|
212
|
+
if (message.kind !== "toolResult" || !message.toolCallId) continue;
|
|
213
|
+
const block = byCallId.get(message.toolCallId);
|
|
214
|
+
if (!block) continue;
|
|
215
|
+
block.result = { content: message.text ?? "", isError: Boolean(message.isError) };
|
|
216
|
+
block.running = false;
|
|
217
|
+
attached.add(message);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return attached.size === 0 ? messages : messages.filter((message) => !attached.has(message));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function applyAssistantDelta(messages: RenderedMessage[], event: Json): void {
|
|
224
|
+
const delta = (event.assistantMessageEvent ?? {}) as Json;
|
|
225
|
+
const message = (event.message ?? {}) as Json;
|
|
226
|
+
let assistant = messages.find((m) => m.kind === "assistant" && m.streaming === true);
|
|
227
|
+
if (!assistant) {
|
|
228
|
+
const created: RenderedMessage = {
|
|
229
|
+
kind: "assistant",
|
|
230
|
+
blocks: [],
|
|
231
|
+
streaming: true,
|
|
232
|
+
timestamp: timestampValue(message.timestamp) ?? Date.now(),
|
|
233
|
+
};
|
|
234
|
+
messages.push(created);
|
|
235
|
+
assistant = created;
|
|
236
|
+
}
|
|
237
|
+
const blocks = assistant.blocks ?? (assistant.blocks = []);
|
|
238
|
+
const index = typeof delta.contentIndex === "number" ? delta.contentIndex : 0;
|
|
239
|
+
const dType = delta.type;
|
|
240
|
+
if (dType === "text_start") {
|
|
241
|
+
blocks[index] = { type: "text", text: "" };
|
|
242
|
+
} else if (dType === "text_delta" && typeof delta.delta === "string") {
|
|
243
|
+
if (blocks[index]?.type !== "text") blocks[index] = { type: "text", text: "" };
|
|
244
|
+
blocks[index].text = `${blocks[index].text ?? ""}${stripAnsi(delta.delta)}`;
|
|
245
|
+
} else if (dType === "text_end" && typeof delta.content === "string") {
|
|
246
|
+
if (blocks[index]?.type !== "text") blocks[index] = { type: "text", text: "" };
|
|
247
|
+
blocks[index].text = stripAnsi(delta.content);
|
|
248
|
+
} else if (dType === "thinking_start") {
|
|
249
|
+
blocks[index] = { type: "thinking", thinking: "" };
|
|
250
|
+
} else if (dType === "thinking_delta" && typeof delta.delta === "string") {
|
|
251
|
+
if (blocks[index]?.type !== "thinking") blocks[index] = { type: "thinking", thinking: "" };
|
|
252
|
+
blocks[index].thinking = `${blocks[index].thinking ?? ""}${stripAnsi(delta.delta)}`;
|
|
253
|
+
} else if (dType === "thinking_end" && typeof delta.content === "string") {
|
|
254
|
+
if (blocks[index]?.type !== "thinking") blocks[index] = { type: "thinking", thinking: "" };
|
|
255
|
+
blocks[index].thinking = stripAnsi(delta.content);
|
|
256
|
+
} else if (dType === "toolcall_start") {
|
|
257
|
+
blocks[index] = { type: "toolCall", id: "", name: "", argumentsText: "", running: true };
|
|
258
|
+
} else if (dType === "toolcall_delta") {
|
|
259
|
+
if (blocks[index]?.type !== "toolCall") blocks[index] = { type: "toolCall", id: "", name: "", argumentsText: "", running: true };
|
|
260
|
+
if (typeof delta.delta === "string") blocks[index].argumentsText = `${blocks[index].argumentsText ?? ""}${delta.delta}`;
|
|
261
|
+
} else if (dType === "toolcall_end") {
|
|
262
|
+
const call = (delta.toolCall ?? {}) as Json;
|
|
263
|
+
const args = call.arguments;
|
|
264
|
+
blocks[index] = {
|
|
265
|
+
type: "toolCall",
|
|
266
|
+
id: typeof call.id === "string" ? call.id : blocks[index]?.id,
|
|
267
|
+
name: typeof call.name === "string" ? call.name : blocks[index]?.name ?? "tool",
|
|
268
|
+
arguments: args,
|
|
269
|
+
argumentsText: summarizeArgs(args),
|
|
270
|
+
running: true,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function applyToolExecution(messages: RenderedMessage[], event: Json): void {
|
|
276
|
+
const callId = typeof event.toolCallId === "string" ? event.toolCallId : "";
|
|
277
|
+
if (!callId) return;
|
|
278
|
+
let block: RenderedBlock | undefined;
|
|
279
|
+
let assistant: RenderedMessage | undefined;
|
|
280
|
+
for (const m of messages) {
|
|
281
|
+
if (m.kind === "assistant" && m.blocks) {
|
|
282
|
+
const found = m.blocks.find((b) => b.type === "toolCall" && b.id === callId);
|
|
283
|
+
if (found) {
|
|
284
|
+
block = found;
|
|
285
|
+
assistant = m;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!block) {
|
|
291
|
+
assistant = { kind: "assistant", blocks: [], streaming: true, timestamp: Date.now() };
|
|
292
|
+
messages.push(assistant);
|
|
293
|
+
block = {
|
|
294
|
+
type: "toolCall",
|
|
295
|
+
id: callId,
|
|
296
|
+
name: typeof event.toolName === "string" ? event.toolName : "tool",
|
|
297
|
+
arguments: event.args ?? {},
|
|
298
|
+
argumentsText: summarizeArgs(event.args),
|
|
299
|
+
running: true,
|
|
300
|
+
};
|
|
301
|
+
assistant.blocks = [...(assistant.blocks ?? []), block];
|
|
302
|
+
}
|
|
303
|
+
if (event.type === "tool_execution_start") {
|
|
304
|
+
block.name = typeof event.toolName === "string" ? event.toolName : block.name;
|
|
305
|
+
block.arguments = event.args ?? block.arguments;
|
|
306
|
+
block.argumentsText = summarizeArgs(event.args) || block.argumentsText;
|
|
307
|
+
block.running = true;
|
|
308
|
+
} else if (event.type === "tool_execution_update") {
|
|
309
|
+
block.running = true;
|
|
310
|
+
const partial = event.partialResult as Json | undefined;
|
|
311
|
+
if (partial) block.result = { content: contentText(partial.content), isError: false, details: partial.details };
|
|
312
|
+
} else if (event.type === "tool_execution_end") {
|
|
313
|
+
block.running = false;
|
|
314
|
+
const result = event.result as Json | undefined;
|
|
315
|
+
block.result = {
|
|
316
|
+
content: contentText(result?.content),
|
|
317
|
+
isError: Boolean(event.isError),
|
|
318
|
+
details: result?.details,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function findLast<T>(items: T[], predicate: (item: T) => boolean): number {
|
|
324
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
325
|
+
if (predicate(items[i])) return i;
|
|
326
|
+
}
|
|
327
|
+
return -1;
|
|
328
|
+
}
|