web: fix args-object React crash; e2e plugin-contract scenario
This commit is contained in:
@@ -54,7 +54,9 @@ function toolEnd(id: string, isError?: boolean, preview?: string): EventFrame {
|
||||
// render as-is (React #31 crash) — deriveChat must normalize to text.
|
||||
describe("object tool args from the real plugin", () => {
|
||||
it("argsText normalizes every shape the plugin emits", () => {
|
||||
expect(argsText({ path: "src/main.ts" })).toBe('{\n "path": "src/main.ts"\n}');
|
||||
expect(argsText({ path: "src/main.ts" })).toBe(
|
||||
'{\n "path": "src/main.ts"\n}',
|
||||
);
|
||||
expect(argsText('"plain"')).toBe('"plain"');
|
||||
expect(argsText(42)).toBe("42");
|
||||
expect(argsText(null)).toBe("");
|
||||
|
||||
+271
-243
@@ -2,137 +2,143 @@ import type { EventFrame } from "./protocol";
|
||||
|
||||
// ---------- event list merge ----------
|
||||
|
||||
export function mergeEvents(existing: EventFrame[], incoming: EventFrame[]): EventFrame[] {
|
||||
const bySeq = new Map<number, EventFrame>();
|
||||
for (const e of existing) bySeq.set(e.seq, e);
|
||||
for (const e of incoming) bySeq.set(e.seq, e);
|
||||
return Array.from(bySeq.values()).sort((a, b) => a.seq - b.seq);
|
||||
export function mergeEvents(
|
||||
existing: EventFrame[],
|
||||
incoming: EventFrame[],
|
||||
): EventFrame[] {
|
||||
const bySeq = new Map<number, EventFrame>();
|
||||
for (const e of existing) bySeq.set(e.seq, e);
|
||||
for (const e of incoming) bySeq.set(e.seq, e);
|
||||
return Array.from(bySeq.values()).sort((a, b) => a.seq - b.seq);
|
||||
}
|
||||
|
||||
/** Highest persisted seq — deltas (`message_update`) are not persisted, so they
|
||||
* must not advance the refetch cursor. */
|
||||
export function lastPersistedSeq(events: EventFrame[]): number {
|
||||
let max = 0;
|
||||
for (const e of events) if (e.type !== "message_update" && e.seq > max) max = e.seq;
|
||||
return max;
|
||||
let max = 0;
|
||||
for (const e of events)
|
||||
if (e.type !== "message_update" && e.seq > max) max = e.seq;
|
||||
return max;
|
||||
}
|
||||
|
||||
// ---------- chat view derivation ----------
|
||||
|
||||
export interface ToolState {
|
||||
id: string;
|
||||
name: string;
|
||||
args: string;
|
||||
running: boolean;
|
||||
isError: boolean;
|
||||
preview: string;
|
||||
id: string;
|
||||
name: string;
|
||||
args: string;
|
||||
running: boolean;
|
||||
isError: boolean;
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
key: string;
|
||||
role: "user" | "assistant" | "system" | "toolResult";
|
||||
text: string;
|
||||
thinking: string | null;
|
||||
toolCalls: { id: string; name: string; argsJson: string }[];
|
||||
toolCallId: string | null;
|
||||
streaming: boolean;
|
||||
key: string;
|
||||
role: "user" | "assistant" | "system" | "toolResult";
|
||||
text: string;
|
||||
thinking: string | null;
|
||||
toolCalls: { id: string; name: string; argsJson: string }[];
|
||||
toolCallId: string | null;
|
||||
streaming: boolean;
|
||||
}
|
||||
|
||||
export interface ChatDerivation {
|
||||
messages: ChatMessage[];
|
||||
tools: Map<string, ToolState>;
|
||||
busy: boolean;
|
||||
messages: ChatMessage[];
|
||||
tools: Map<string, ToolState>;
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
// The plugin mirrors pi extension events verbatim: tool args arrive as a raw
|
||||
// JSON object (or string in older frames). Normalizing at this boundary keeps
|
||||
// the render layer string-only; regression: object args crashed React (#31).
|
||||
export function argsText(args: unknown): string {
|
||||
if (typeof args === "string") return args;
|
||||
if (args === null || args === undefined) return "";
|
||||
try {
|
||||
return JSON.stringify(args, null, 2) ?? "";
|
||||
} catch {
|
||||
return String(args);
|
||||
}
|
||||
if (typeof args === "string") return args;
|
||||
if (args === null || args === undefined) return "";
|
||||
try {
|
||||
return JSON.stringify(args, null, 2) ?? "";
|
||||
} catch {
|
||||
return String(args);
|
||||
}
|
||||
}
|
||||
|
||||
export function deriveChat(events: EventFrame[]): ChatDerivation {
|
||||
const messages: ChatMessage[] = [];
|
||||
const tools = new Map<string, ToolState>();
|
||||
let stream: { id: string; text: string } | null = null;
|
||||
let busy = false;
|
||||
const messages: ChatMessage[] = [];
|
||||
const tools = new Map<string, ToolState>();
|
||||
let stream: { id: string; text: string } | null = null;
|
||||
let busy = false;
|
||||
|
||||
for (const e of events) {
|
||||
switch (e.type) {
|
||||
case "tool_execution_start": {
|
||||
if (e.toolCallId !== undefined) {
|
||||
tools.set(e.toolCallId, {
|
||||
id: e.toolCallId,
|
||||
name: e.toolName ?? "tool",
|
||||
args: argsText(e.args),
|
||||
running: true,
|
||||
isError: false,
|
||||
preview: "",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "tool_execution_end": {
|
||||
const t = e.toolCallId !== undefined ? tools.get(e.toolCallId) : undefined;
|
||||
if (t !== undefined) {
|
||||
t.running = false;
|
||||
t.isError = e.isError ?? false;
|
||||
t.preview = e.resultPreview ?? "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "agent_start":
|
||||
busy = true;
|
||||
break;
|
||||
case "agent_settled":
|
||||
busy = false;
|
||||
break;
|
||||
case "message_start":
|
||||
if (e.message?.role === "assistant") stream = { id: e.message.id, text: "" };
|
||||
break;
|
||||
case "message_update":
|
||||
if (stream !== null) stream.text += e.delta ?? "";
|
||||
break;
|
||||
case "message_end": {
|
||||
const m = e.message;
|
||||
if (m !== undefined) {
|
||||
if (stream !== null && stream.id === m.id) stream = null;
|
||||
messages.push({
|
||||
key: `msg-${e.seq}`,
|
||||
role: m.role,
|
||||
text: m.text,
|
||||
thinking: m.thinking,
|
||||
toolCalls: m.toolCalls ?? [],
|
||||
toolCallId: m.toolCallId,
|
||||
streaming: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const e of events) {
|
||||
switch (e.type) {
|
||||
case "tool_execution_start": {
|
||||
if (e.toolCallId !== undefined) {
|
||||
tools.set(e.toolCallId, {
|
||||
id: e.toolCallId,
|
||||
name: e.toolName ?? "tool",
|
||||
args: argsText(e.args),
|
||||
running: true,
|
||||
isError: false,
|
||||
preview: "",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "tool_execution_end": {
|
||||
const t =
|
||||
e.toolCallId !== undefined ? tools.get(e.toolCallId) : undefined;
|
||||
if (t !== undefined) {
|
||||
t.running = false;
|
||||
t.isError = e.isError ?? false;
|
||||
t.preview = e.resultPreview ?? "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "agent_start":
|
||||
busy = true;
|
||||
break;
|
||||
case "agent_settled":
|
||||
busy = false;
|
||||
break;
|
||||
case "message_start":
|
||||
if (e.message?.role === "assistant")
|
||||
stream = { id: e.message.id, text: "" };
|
||||
break;
|
||||
case "message_update":
|
||||
if (stream !== null) stream.text += e.delta ?? "";
|
||||
break;
|
||||
case "message_end": {
|
||||
const m = e.message;
|
||||
if (m !== undefined) {
|
||||
if (stream !== null && stream.id === m.id) stream = null;
|
||||
messages.push({
|
||||
key: `msg-${e.seq}`,
|
||||
role: m.role,
|
||||
text: m.text,
|
||||
thinking: m.thinking,
|
||||
toolCalls: m.toolCalls ?? [],
|
||||
toolCallId: m.toolCallId,
|
||||
streaming: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream !== null) {
|
||||
messages.push({
|
||||
key: `stream-${stream.id}`,
|
||||
role: "assistant",
|
||||
text: stream.text,
|
||||
thinking: null,
|
||||
toolCalls: [],
|
||||
toolCallId: null,
|
||||
streaming: true,
|
||||
});
|
||||
}
|
||||
if (stream !== null) {
|
||||
messages.push({
|
||||
key: `stream-${stream.id}`,
|
||||
role: "assistant",
|
||||
text: stream.text,
|
||||
thinking: null,
|
||||
toolCalls: [],
|
||||
toolCallId: null,
|
||||
streaming: true,
|
||||
});
|
||||
}
|
||||
|
||||
return { messages, tools, busy: busy || stream !== null };
|
||||
return { messages, tools, busy: busy || stream !== null };
|
||||
}
|
||||
|
||||
// ---------- todo / subagent derivation (client-side, per PROTOCOL.md) ----------
|
||||
@@ -140,168 +146,190 @@ export function deriveChat(events: EventFrame[]): ChatDerivation {
|
||||
export type TodoStatus = "pending" | "in-progress" | "completed";
|
||||
|
||||
export interface TodoItem {
|
||||
content: string;
|
||||
status: TodoStatus;
|
||||
deleted: boolean;
|
||||
content: string;
|
||||
status: TodoStatus;
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface SubagentRun {
|
||||
key: string;
|
||||
name: string;
|
||||
running: boolean;
|
||||
isError: boolean;
|
||||
key: string;
|
||||
name: string;
|
||||
running: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export interface TaskDerivation {
|
||||
todos: TodoItem[];
|
||||
subagents: SubagentRun[];
|
||||
workingTools: ToolState[];
|
||||
todos: TodoItem[];
|
||||
subagents: SubagentRun[];
|
||||
workingTools: ToolState[];
|
||||
}
|
||||
|
||||
const TODO_TOOL = "todo";
|
||||
const SUBAGENT_TOOL = "subagent";
|
||||
|
||||
function parseJson(raw: unknown): unknown {
|
||||
if (typeof raw === "object" && raw !== null) return raw; // already-decoded args
|
||||
if (typeof raw !== "string" || raw.length === 0) return undefined;
|
||||
try {
|
||||
return JSON.parse(raw) as unknown;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof raw === "object" && raw !== null) return raw; // already-decoded args
|
||||
if (typeof raw !== "string" || raw.length === 0) return undefined;
|
||||
try {
|
||||
return JSON.parse(raw) as unknown;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStatus(raw: unknown): TodoStatus {
|
||||
if (typeof raw !== "string") return "pending";
|
||||
switch (raw.toLowerCase()) {
|
||||
case "in_progress":
|
||||
case "in-progress":
|
||||
case "inprogress":
|
||||
case "in progress":
|
||||
case "doing":
|
||||
case "started":
|
||||
return "in-progress";
|
||||
case "completed":
|
||||
case "complete":
|
||||
case "done":
|
||||
return "completed";
|
||||
default:
|
||||
return "pending";
|
||||
}
|
||||
if (typeof raw !== "string") return "pending";
|
||||
switch (raw.toLowerCase()) {
|
||||
case "in_progress":
|
||||
case "in-progress":
|
||||
case "inprogress":
|
||||
case "in progress":
|
||||
case "doing":
|
||||
case "started":
|
||||
return "in-progress";
|
||||
case "completed":
|
||||
case "complete":
|
||||
case "done":
|
||||
return "completed";
|
||||
default:
|
||||
return "pending";
|
||||
}
|
||||
}
|
||||
|
||||
function extractSnapshot(raw: unknown): { content: string; status: TodoStatus }[] | null {
|
||||
let arr: unknown = raw;
|
||||
if (Array.isArray(raw) === false && raw !== null && typeof raw === "object") {
|
||||
const o = raw as Record<string, unknown>;
|
||||
const nested = o.todos ?? o.items ?? o.tasks ?? o.list;
|
||||
if (Array.isArray(nested)) arr = nested;
|
||||
}
|
||||
if (Array.isArray(arr) === false) return null;
|
||||
const items: { content: string; status: TodoStatus }[] = [];
|
||||
for (const entry of arr) {
|
||||
if (typeof entry === "string") {
|
||||
items.push({ content: entry, status: "pending" });
|
||||
continue;
|
||||
}
|
||||
if (entry !== null && typeof entry === "object") {
|
||||
const o = entry as Record<string, unknown>;
|
||||
const content = o.content ?? o.title ?? o.text ?? o.subject ?? o.summary;
|
||||
if (typeof content === "string" && content.length > 0) {
|
||||
items.push({ content, status: normalizeStatus(o.status) });
|
||||
}
|
||||
}
|
||||
}
|
||||
return items.length > 0 ? items : null;
|
||||
function extractSnapshot(
|
||||
raw: unknown,
|
||||
): { content: string; status: TodoStatus }[] | null {
|
||||
let arr: unknown = raw;
|
||||
if (Array.isArray(raw) === false && raw !== null && typeof raw === "object") {
|
||||
const o = raw as Record<string, unknown>;
|
||||
const nested = o.todos ?? o.items ?? o.tasks ?? o.list;
|
||||
if (Array.isArray(nested)) arr = nested;
|
||||
}
|
||||
if (Array.isArray(arr) === false) return null;
|
||||
const items: { content: string; status: TodoStatus }[] = [];
|
||||
for (const entry of arr) {
|
||||
if (typeof entry === "string") {
|
||||
items.push({ content: entry, status: "pending" });
|
||||
continue;
|
||||
}
|
||||
if (entry !== null && typeof entry === "object") {
|
||||
const o = entry as Record<string, unknown>;
|
||||
const content = o.content ?? o.title ?? o.text ?? o.subject ?? o.summary;
|
||||
if (typeof content === "string" && content.length > 0) {
|
||||
items.push({ content, status: normalizeStatus(o.status) });
|
||||
}
|
||||
}
|
||||
}
|
||||
return items.length > 0 ? items : null;
|
||||
}
|
||||
|
||||
export function deriveTasks(events: EventFrame[]): TaskDerivation {
|
||||
const toolNames = new Map<string, string>(); // toolCallId -> toolName
|
||||
for (const e of events) {
|
||||
if (e.type === "tool_execution_start" && e.toolCallId !== undefined) {
|
||||
toolNames.set(e.toolCallId, e.toolName ?? "");
|
||||
}
|
||||
}
|
||||
const toolNames = new Map<string, string>(); // toolCallId -> toolName
|
||||
for (const e of events) {
|
||||
if (e.type === "tool_execution_start" && e.toolCallId !== undefined) {
|
||||
toolNames.set(e.toolCallId, e.toolName ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
// todo snapshots, seq-ordered: args from execution start, result from execution end + toolResult message
|
||||
const snapshots: { seq: number; items: { content: string; status: TodoStatus }[] }[] = [];
|
||||
const subagents: SubagentRun[] = [];
|
||||
const runningTools: ToolState[] = [];
|
||||
// todo snapshots, seq-ordered: args from execution start, result from execution end + toolResult message
|
||||
const snapshots: {
|
||||
seq: number;
|
||||
items: { content: string; status: TodoStatus }[];
|
||||
}[] = [];
|
||||
const subagents: SubagentRun[] = [];
|
||||
const runningTools: ToolState[] = [];
|
||||
|
||||
for (const e of events) {
|
||||
if (e.type === "tool_execution_start" && e.toolCallId !== undefined) {
|
||||
const name = e.toolName ?? "";
|
||||
if (name === TODO_TOOL) {
|
||||
const snap = extractSnapshot(parseJson(e.args));
|
||||
if (snap !== null) snapshots.push({ seq: e.seq, items: snap });
|
||||
} else if (name === SUBAGENT_TOOL) {
|
||||
const parsed = parseJson(e.args);
|
||||
const o = parsed !== null && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
|
||||
const nameField = o.agentName ?? o.agent ?? o.name ?? o.agentType ?? o.role;
|
||||
subagents.push({
|
||||
key: e.toolCallId,
|
||||
name: typeof nameField === "string" && nameField.length > 0 ? nameField : "subagent",
|
||||
running: true,
|
||||
isError: false,
|
||||
});
|
||||
} else {
|
||||
runningTools.push({
|
||||
id: e.toolCallId,
|
||||
name,
|
||||
args: argsText(e.args),
|
||||
running: true,
|
||||
isError: false,
|
||||
preview: "",
|
||||
});
|
||||
}
|
||||
} else if (e.type === "tool_execution_end" && e.toolCallId !== undefined) {
|
||||
const name = toolNames.get(e.toolCallId) ?? "";
|
||||
if (name === SUBAGENT_TOOL) {
|
||||
const run = subagents.find((s) => s.key === e.toolCallId);
|
||||
if (run !== undefined) {
|
||||
run.running = false;
|
||||
run.isError = e.isError ?? false;
|
||||
}
|
||||
} else if (name !== TODO_TOOL) {
|
||||
const t = runningTools.find((w) => w.id === e.toolCallId);
|
||||
if (t !== undefined) {
|
||||
t.running = false;
|
||||
t.isError = e.isError ?? false;
|
||||
t.preview = e.resultPreview ?? "";
|
||||
}
|
||||
}
|
||||
if (name === TODO_TOOL) {
|
||||
const snap = extractSnapshot(parseJson(e.resultPreview));
|
||||
if (snap !== null) snapshots.push({ seq: e.seq, items: snap });
|
||||
}
|
||||
} else if (e.type === "message_end" && e.message?.role === "toolResult" && e.message.toolCallId !== null) {
|
||||
const name = toolNames.get(e.message.toolCallId) ?? "";
|
||||
if (name === TODO_TOOL) {
|
||||
const snap = extractSnapshot(parseJson(e.message.text));
|
||||
if (snap !== null) snapshots.push({ seq: e.seq, items: snap });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const e of events) {
|
||||
if (e.type === "tool_execution_start" && e.toolCallId !== undefined) {
|
||||
const name = e.toolName ?? "";
|
||||
if (name === TODO_TOOL) {
|
||||
const snap = extractSnapshot(parseJson(e.args));
|
||||
if (snap !== null) snapshots.push({ seq: e.seq, items: snap });
|
||||
} else if (name === SUBAGENT_TOOL) {
|
||||
const parsed = parseJson(e.args);
|
||||
const o =
|
||||
parsed !== null && typeof parsed === "object"
|
||||
? (parsed as Record<string, unknown>)
|
||||
: {};
|
||||
const nameField =
|
||||
o.agentName ?? o.agent ?? o.name ?? o.agentType ?? o.role;
|
||||
subagents.push({
|
||||
key: e.toolCallId,
|
||||
name:
|
||||
typeof nameField === "string" && nameField.length > 0
|
||||
? nameField
|
||||
: "subagent",
|
||||
running: true,
|
||||
isError: false,
|
||||
});
|
||||
} else {
|
||||
runningTools.push({
|
||||
id: e.toolCallId,
|
||||
name,
|
||||
args: argsText(e.args),
|
||||
running: true,
|
||||
isError: false,
|
||||
preview: "",
|
||||
});
|
||||
}
|
||||
} else if (e.type === "tool_execution_end" && e.toolCallId !== undefined) {
|
||||
const name = toolNames.get(e.toolCallId) ?? "";
|
||||
if (name === SUBAGENT_TOOL) {
|
||||
const run = subagents.find((s) => s.key === e.toolCallId);
|
||||
if (run !== undefined) {
|
||||
run.running = false;
|
||||
run.isError = e.isError ?? false;
|
||||
}
|
||||
} else if (name !== TODO_TOOL) {
|
||||
const t = runningTools.find((w) => w.id === e.toolCallId);
|
||||
if (t !== undefined) {
|
||||
t.running = false;
|
||||
t.isError = e.isError ?? false;
|
||||
t.preview = e.resultPreview ?? "";
|
||||
}
|
||||
}
|
||||
if (name === TODO_TOOL) {
|
||||
const snap = extractSnapshot(parseJson(e.resultPreview));
|
||||
if (snap !== null) snapshots.push({ seq: e.seq, items: snap });
|
||||
}
|
||||
} else if (
|
||||
e.type === "message_end" &&
|
||||
e.message?.role === "toolResult" &&
|
||||
e.message.toolCallId !== null
|
||||
) {
|
||||
const name = toolNames.get(e.message.toolCallId) ?? "";
|
||||
if (name === TODO_TOOL) {
|
||||
const snap = extractSnapshot(parseJson(e.message.text));
|
||||
if (snap !== null) snapshots.push({ seq: e.seq, items: snap });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todos: latest snapshot wins; earlier items missing from it are deleted
|
||||
const todos: TodoItem[] = [];
|
||||
if (snapshots.length > 0) {
|
||||
snapshots.sort((a, b) => a.seq - b.seq);
|
||||
const latest = snapshots[snapshots.length - 1]?.items ?? [];
|
||||
const seen = new Map<string, TodoStatus>();
|
||||
for (const snap of snapshots) {
|
||||
for (const item of snap.items) if (!seen.has(item.content)) seen.set(item.content, item.status);
|
||||
}
|
||||
for (const item of latest) {
|
||||
todos.push({ content: item.content, status: item.status, deleted: false });
|
||||
seen.delete(item.content);
|
||||
}
|
||||
for (const [content] of seen) todos.push({ content, status: "pending", deleted: true });
|
||||
}
|
||||
// todos: latest snapshot wins; earlier items missing from it are deleted
|
||||
const todos: TodoItem[] = [];
|
||||
if (snapshots.length > 0) {
|
||||
snapshots.sort((a, b) => a.seq - b.seq);
|
||||
const latest = snapshots[snapshots.length - 1]?.items ?? [];
|
||||
const seen = new Map<string, TodoStatus>();
|
||||
for (const snap of snapshots) {
|
||||
for (const item of snap.items)
|
||||
if (!seen.has(item.content)) seen.set(item.content, item.status);
|
||||
}
|
||||
for (const item of latest) {
|
||||
todos.push({
|
||||
content: item.content,
|
||||
status: item.status,
|
||||
deleted: false,
|
||||
});
|
||||
seen.delete(item.content);
|
||||
}
|
||||
for (const [content] of seen)
|
||||
todos.push({ content, status: "pending", deleted: true });
|
||||
}
|
||||
|
||||
return {
|
||||
todos,
|
||||
subagents,
|
||||
workingTools: runningTools.filter((t) => t.running),
|
||||
};
|
||||
return {
|
||||
todos,
|
||||
subagents,
|
||||
workingTools: runningTools.filter((t) => t.running),
|
||||
};
|
||||
}
|
||||
|
||||
+93
-89
@@ -4,155 +4,159 @@ export const PROTOCOL_VERSION: 1 = 1;
|
||||
|
||||
/** Event types emitted by the plugin, carried in the envelope `type` field. */
|
||||
export const EventType = {
|
||||
Hello: "hello",
|
||||
MessageStart: "message_start",
|
||||
MessageUpdate: "message_update",
|
||||
MessageEnd: "message_end",
|
||||
ToolExecutionStart: "tool_execution_start",
|
||||
ToolExecutionUpdate: "tool_execution_update",
|
||||
ToolExecutionEnd: "tool_execution_end",
|
||||
AgentStart: "agent_start",
|
||||
AgentEnd: "agent_end",
|
||||
AgentSettled: "agent_settled",
|
||||
SessionInfo: "session_info",
|
||||
Bye: "bye",
|
||||
Hello: "hello",
|
||||
MessageStart: "message_start",
|
||||
MessageUpdate: "message_update",
|
||||
MessageEnd: "message_end",
|
||||
ToolExecutionStart: "tool_execution_start",
|
||||
ToolExecutionUpdate: "tool_execution_update",
|
||||
ToolExecutionEnd: "tool_execution_end",
|
||||
AgentStart: "agent_start",
|
||||
AgentEnd: "agent_end",
|
||||
AgentSettled: "agent_settled",
|
||||
SessionInfo: "session_info",
|
||||
Bye: "bye",
|
||||
} as const;
|
||||
export type EventType = (typeof EventType)[keyof typeof EventType];
|
||||
|
||||
/** REST routes (base `/api`, bearer auth). */
|
||||
export const Route = {
|
||||
Sessions: "/api/sessions",
|
||||
SessionEvents: (id: string): string => `/api/sessions/${encodeURIComponent(id)}/events`,
|
||||
SessionPrompt: (id: string): string => `/api/sessions/${encodeURIComponent(id)}/prompt`,
|
||||
SessionAbort: (id: string): string => `/api/sessions/${encodeURIComponent(id)}/abort`,
|
||||
SessionContainer: (id: string): string => `/api/sessions/${encodeURIComponent(id)}/container`,
|
||||
Spawn: "/api/spawn",
|
||||
SpawnStatus: "/api/spawn/status",
|
||||
GitlabStatus: "/api/gitlab/status",
|
||||
GitlabConnect: "/api/gitlab/connect",
|
||||
GitlabRepos: "/api/gitlab/repos",
|
||||
Sessions: "/api/sessions",
|
||||
SessionEvents: (id: string): string =>
|
||||
`/api/sessions/${encodeURIComponent(id)}/events`,
|
||||
SessionPrompt: (id: string): string =>
|
||||
`/api/sessions/${encodeURIComponent(id)}/prompt`,
|
||||
SessionAbort: (id: string): string =>
|
||||
`/api/sessions/${encodeURIComponent(id)}/abort`,
|
||||
SessionContainer: (id: string): string =>
|
||||
`/api/sessions/${encodeURIComponent(id)}/container`,
|
||||
Spawn: "/api/spawn",
|
||||
SpawnStatus: "/api/spawn/status",
|
||||
GitlabStatus: "/api/gitlab/status",
|
||||
GitlabConnect: "/api/gitlab/connect",
|
||||
GitlabRepos: "/api/gitlab/repos",
|
||||
} as const;
|
||||
|
||||
/** Session snapshot, carried by `hello`/`session_info` and REST session list. */
|
||||
export interface SessionInfo {
|
||||
id: string;
|
||||
name: string | null;
|
||||
cwd: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
/** true if spawned-by-daemon container session */
|
||||
agent: boolean;
|
||||
repo: string | null;
|
||||
startedAt: number;
|
||||
id: string;
|
||||
name: string | null;
|
||||
cwd: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
/** true if spawned-by-daemon container session */
|
||||
agent: boolean;
|
||||
repo: string | null;
|
||||
startedAt: number;
|
||||
}
|
||||
|
||||
/** Session list row (REST `GET /api/sessions` + WS `session_list`). */
|
||||
export interface SessionListItem extends SessionInfo {
|
||||
online: boolean;
|
||||
lastEventAt: number | null;
|
||||
online: boolean;
|
||||
lastEventAt: number | null;
|
||||
}
|
||||
|
||||
export type MessageRole = "user" | "assistant" | "toolResult" | "system";
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
argsJson: string;
|
||||
id: string;
|
||||
name: string;
|
||||
argsJson: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: MessageRole;
|
||||
id: string;
|
||||
text: string;
|
||||
thinking: string | null;
|
||||
toolCalls: ToolCall[];
|
||||
/** toolResult messages: which call this answers */
|
||||
toolCallId: string | null;
|
||||
role: MessageRole;
|
||||
id: string;
|
||||
text: string;
|
||||
thinking: string | null;
|
||||
toolCalls: ToolCall[];
|
||||
/** toolResult messages: which call this answers */
|
||||
toolCallId: string | null;
|
||||
}
|
||||
|
||||
/** Persisted event envelope + flattened payload. */
|
||||
export interface EventFrame {
|
||||
v: 1;
|
||||
sessionId: string;
|
||||
/** monotonic per-session, plugin-assigned, starts at 1 */
|
||||
seq: number;
|
||||
/** unix ms */
|
||||
ts: number;
|
||||
type: EventType | string;
|
||||
// ---- payload fields (union, present depending on `type`) ----
|
||||
session?: SessionInfo;
|
||||
message?: Message;
|
||||
delta?: string;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
args?: unknown;
|
||||
partial?: string;
|
||||
isError?: boolean;
|
||||
resultPreview?: string;
|
||||
usage?: { inputTokens?: number; outputTokens?: number; totalCost?: number };
|
||||
reason?: string;
|
||||
v: 1;
|
||||
sessionId: string;
|
||||
/** monotonic per-session, plugin-assigned, starts at 1 */
|
||||
seq: number;
|
||||
/** unix ms */
|
||||
ts: number;
|
||||
type: EventType | string;
|
||||
// ---- payload fields (union, present depending on `type`) ----
|
||||
session?: SessionInfo;
|
||||
message?: Message;
|
||||
delta?: string;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
args?: unknown;
|
||||
partial?: string;
|
||||
isError?: boolean;
|
||||
resultPreview?: string;
|
||||
usage?: { inputTokens?: number; outputTokens?: number; totalCost?: number };
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** Query params + response shapes for REST routes. */
|
||||
export interface PromptBody {
|
||||
message: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PromptResponse {
|
||||
ok: boolean;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export interface AbortResponse {
|
||||
ok: boolean;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export interface ContainerResponse {
|
||||
ok: boolean;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export interface SpawnBody {
|
||||
repo: string;
|
||||
branch?: string;
|
||||
repo: string;
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
export interface SpawnResponse {
|
||||
sessionId: string;
|
||||
containerId: string;
|
||||
sessionId: string;
|
||||
containerId: string;
|
||||
}
|
||||
|
||||
export interface SpawnJob {
|
||||
repo: string;
|
||||
state: string;
|
||||
containerId?: string;
|
||||
sessionId?: string;
|
||||
repo: string;
|
||||
state: string;
|
||||
containerId?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface GitlabStatus {
|
||||
connected: boolean;
|
||||
baseUrl: string;
|
||||
username?: string;
|
||||
connected: boolean;
|
||||
baseUrl: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface GitlabConnectResponse {
|
||||
username: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface Repo {
|
||||
path: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
lastActivityAt: string;
|
||||
webUrl: string;
|
||||
defaultBranch: string;
|
||||
path: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
lastActivityAt: string;
|
||||
webUrl: string;
|
||||
defaultBranch: string;
|
||||
}
|
||||
|
||||
/** ---- WS transport 3 (browser → daemon) ---- */
|
||||
|
||||
export type ServerFrame =
|
||||
| { type: "session_list"; sessions: SessionListItem[] }
|
||||
| { type: "events"; sessionId: string; after: number; events: EventFrame[] }
|
||||
| { type: "spawn_status"; jobs: SpawnJob[] };
|
||||
| { type: "session_list"; sessions: SessionListItem[] }
|
||||
| { type: "events"; sessionId: string; after: number; events: EventFrame[] }
|
||||
| { type: "spawn_status"; jobs: SpawnJob[] };
|
||||
|
||||
export type ClientFrame =
|
||||
| { type: "subscribe"; sessionId: string }
|
||||
| { type: "unsubscribe"; sessionId: string };
|
||||
| { type: "subscribe"; sessionId: string }
|
||||
| { type: "unsubscribe"; sessionId: string };
|
||||
|
||||
Reference in New Issue
Block a user