Skip to content

Commit 187bd3c

Browse files
authored
feat(cloud): add polling for cloud runs (#200)
1 parent 216cc8d commit 187bd3c

File tree

12 files changed

+748
-170
lines changed

12 files changed

+748
-170
lines changed

apps/array/src/api/fetcher.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const buildApiFetcher: (config: {
1010
): Promise<Response> => {
1111
const headers = new Headers();
1212
headers.set("Authorization", `Bearer ${token}`);
13+
headers.set("Content-Type", "application/json");
1314

1415
if (input.urlSearchParams) {
1516
input.url.search = input.urlSearchParams.toString();
@@ -21,10 +22,6 @@ export const buildApiFetcher: (config: {
2122
? JSON.stringify(input.parameters?.body)
2223
: undefined;
2324

24-
if (body) {
25-
headers.set("Content-Type", "application/json");
26-
}
27-
2825
if (input.parameters?.header) {
2926
for (const [key, value] of Object.entries(input.parameters.header)) {
3027
if (value != null) {

apps/array/src/api/posthogClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { StoredLogEntry } from "@features/sessions/utils/parseSessionLogs";
12
import type { AgentEvent } from "@posthog/agent";
23
import { logger } from "@renderer/lib/logger";
34
import type { Task, TaskRun } from "@shared/types";
@@ -236,7 +237,7 @@ export class PostHogAPIClient {
236237
async appendTaskRunLog(
237238
taskId: string,
238239
runId: string,
239-
entries: AgentEvent[],
240+
entries: StoredLogEntry[],
240241
): Promise<void> {
241242
const teamId = await this.getTeamId();
242243
const url = `${this.api.baseUrl}/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`;
@@ -246,7 +247,6 @@ export class PostHogAPIClient {
246247
path: url,
247248
overrides: {
248249
body: JSON.stringify({ entries }),
249-
headers: { "Content-Type": "application/json" },
250250
},
251251
});
252252
if (!response.ok) {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Bug, Info, Warning, XCircle } from "@phosphor-icons/react";
2+
import { Badge, Flex, Text } from "@radix-ui/themes";
3+
4+
interface ConsoleMessageProps {
5+
level: "info" | "debug" | "warn" | "error";
6+
message: string;
7+
timestamp?: string;
8+
}
9+
10+
export function ConsoleMessage({ level, message }: ConsoleMessageProps) {
11+
const getIcon = () => {
12+
switch (level) {
13+
case "error":
14+
return <XCircle size={12} weight="fill" />;
15+
case "warn":
16+
return <Warning size={12} weight="fill" />;
17+
case "debug":
18+
return <Bug size={12} weight="fill" />;
19+
default:
20+
return <Info size={12} weight="fill" />;
21+
}
22+
};
23+
24+
const getBadgeColor = (): "gray" | "yellow" | "red" | "purple" => {
25+
switch (level) {
26+
case "error":
27+
return "red";
28+
case "warn":
29+
return "yellow";
30+
case "debug":
31+
return "purple";
32+
default:
33+
return "gray";
34+
}
35+
};
36+
37+
const getLabel = () => {
38+
switch (level) {
39+
case "error":
40+
return "error";
41+
case "warn":
42+
return "warn";
43+
case "debug":
44+
return "debug";
45+
default:
46+
return "info";
47+
}
48+
};
49+
50+
return (
51+
<Flex align="center" gap="3" className="py-1">
52+
<Badge color={getBadgeColor()} variant="soft" size="1">
53+
<Flex align="center" gap="1">
54+
{getIcon()}
55+
<Text size="1">{getLabel()}</Text>
56+
</Flex>
57+
</Badge>
58+
<Text size="2" className="text-gray-12">
59+
{message}
60+
</Text>
61+
</Flex>
62+
);
63+
}

apps/array/src/renderer/features/sessions/components/SessionView.tsx

Lines changed: 139 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
1414
import type { SessionEvent } from "../stores/sessionStore";
1515
import { useSessionViewStore } from "../stores/sessionViewStore";
1616
import { AgentMessage } from "./AgentMessage";
17+
import { ConsoleMessage } from "./ConsoleMessage";
1718
import { formatDuration, GeneratingIndicator } from "./GeneratingIndicator";
1819
import { MessageEditor } from "./MessageEditor";
1920
import { ToolCallBlock } from "./ToolCallBlock";
@@ -79,11 +80,18 @@ interface ToolData {
7980
result?: unknown;
8081
}
8182

83+
interface ConsoleData {
84+
level: "info" | "debug" | "warn" | "error";
85+
message: string;
86+
timestamp?: string;
87+
}
88+
8289
interface ParsedMessage {
8390
id: string;
84-
type: "user" | "agent" | "tool";
91+
type: "user" | "agent" | "tool" | "console";
8592
content: string;
8693
toolData?: ToolData;
94+
consoleData?: ConsoleData;
8795
eventIndex?: number;
8896
}
8997

@@ -98,6 +106,9 @@ function parseSessionNotification(
98106
notification: SessionNotification,
99107
): ParseResult {
100108
const { update } = notification;
109+
if (!update?.sessionUpdate) {
110+
return null;
111+
}
101112

102113
switch (update.sessionUpdate) {
103114
case "user_message_chunk":
@@ -161,86 +172,132 @@ function mapToolStatus(
161172
}
162173
}
163174

164-
function processEvents(events: SessionEvent[]): ParsedMessage[] {
165-
const messages: ParsedMessage[] = [];
166-
let currentAgentText = "";
167-
let agentMessageKey = 0;
168-
let agentStartEventIndex = 0;
169-
const toolCalls = new Map<
170-
string,
171-
{ toolData: ParsedMessage["toolData"]; eventIndex: number }
172-
>();
173-
174-
const flushAgentMessage = (_eventIndex: number) => {
175-
if (currentAgentText) {
176-
messages.push({
177-
id: `agent-${agentMessageKey}`,
178-
type: "agent",
179-
content: currentAgentText,
180-
eventIndex: agentStartEventIndex,
181-
});
182-
currentAgentText = "";
183-
agentMessageKey++;
175+
class MessageBuilder {
176+
private messages: ParsedMessage[] = [];
177+
private pendingAgentText = "";
178+
private agentStartIndex = 0;
179+
private agentMessageCount = 0;
180+
private toolMessages = new Map<string, ParsedMessage>();
181+
182+
flushAgentText(): void {
183+
if (!this.pendingAgentText) return;
184+
this.messages.push({
185+
id: `agent-${this.agentMessageCount++}`,
186+
type: "agent",
187+
content: this.pendingAgentText,
188+
eventIndex: this.agentStartIndex,
189+
});
190+
this.pendingAgentText = "";
191+
}
192+
193+
addUser(content: string, ts: number, eventIndex: number): void {
194+
this.flushAgentText();
195+
this.messages.push({
196+
id: `user-${ts}`,
197+
type: "user",
198+
content,
199+
eventIndex,
200+
});
201+
}
202+
203+
addAgentChunk(content: string, eventIndex: number): void {
204+
if (!this.pendingAgentText) {
205+
this.agentStartIndex = eventIndex;
184206
}
207+
this.pendingAgentText += content;
208+
}
209+
210+
addTool(toolData: ToolData, eventIndex: number): void {
211+
this.flushAgentText();
212+
const msg: ParsedMessage = {
213+
id: `tool-${toolData.toolCallId}`,
214+
type: "tool",
215+
content: "",
216+
toolData,
217+
eventIndex,
218+
};
219+
this.toolMessages.set(toolData.toolCallId, msg);
220+
this.messages.push(msg);
221+
}
222+
223+
updateTool(toolData: ToolData): void {
224+
const existing = this.toolMessages.get(toolData.toolCallId);
225+
if (!existing?.toolData) return;
226+
existing.toolData.status = toolData.status;
227+
existing.toolData.result = toolData.result;
228+
if (toolData.kind) existing.toolData.kind = toolData.kind;
229+
}
230+
231+
addConsole(consoleData: ConsoleData, _ts: number, eventIndex: number): void {
232+
this.flushAgentText();
233+
this.messages.push({
234+
id: `console-${eventIndex}`,
235+
type: "console",
236+
content: consoleData.message,
237+
consoleData,
238+
eventIndex,
239+
});
240+
}
241+
242+
build(): ParsedMessage[] {
243+
this.flushAgentText();
244+
return this.messages;
245+
}
246+
}
247+
248+
function tryParseConsoleMessage(
249+
event: SessionEvent,
250+
): { level: ConsoleData["level"]; message: string } | null {
251+
if (event.type !== "acp_message") return null;
252+
const msg = event.message as {
253+
method?: string;
254+
params?: { level?: string; message?: string };
185255
};
256+
if (msg?.method !== "_posthog/console" || !msg.params?.message) return null;
257+
return {
258+
level: (msg.params.level ?? "info") as ConsoleData["level"],
259+
message: msg.params.message,
260+
};
261+
}
262+
263+
function processEvents(events: SessionEvent[]): ParsedMessage[] {
264+
const builder = new MessageBuilder();
186265

187266
for (let i = 0; i < events.length; i++) {
188267
const event = events[i];
189-
if (event.type === "session_update") {
190-
const parsed = parseSessionNotification(event.notification);
191-
if (!parsed) continue;
192-
193-
switch (parsed.type) {
194-
case "user": {
195-
flushAgentMessage(i);
196-
messages.push({
197-
id: `user-${event.ts}`,
198-
type: "user",
199-
content: parsed.content,
200-
eventIndex: i,
201-
});
202-
break;
203-
}
204-
case "agent": {
205-
if (!currentAgentText) {
206-
agentStartEventIndex = i;
207-
}
208-
currentAgentText += parsed.content;
209-
break;
210-
}
211-
case "tool": {
212-
flushAgentMessage(i);
213-
toolCalls.set(parsed.toolData.toolCallId, {
214-
toolData: parsed.toolData,
215-
eventIndex: i,
216-
});
217-
messages.push({
218-
id: `tool-${parsed.toolData.toolCallId}`,
219-
type: "tool",
220-
content: "",
221-
toolData: parsed.toolData,
222-
eventIndex: i,
223-
});
224-
break;
225-
}
226-
case "tool_update": {
227-
const existing = toolCalls.get(parsed.toolData.toolCallId);
228-
if (existing) {
229-
existing.toolData!.status = parsed.toolData.status;
230-
existing.toolData!.result = parsed.toolData.result;
231-
if (parsed.toolData.kind) {
232-
existing.toolData!.kind = parsed.toolData.kind;
233-
}
234-
}
235-
break;
236-
}
237-
}
268+
269+
const consoleMsg = tryParseConsoleMessage(event);
270+
if (consoleMsg) {
271+
builder.addConsole(
272+
{ ...consoleMsg, timestamp: new Date(event.ts).toISOString() },
273+
event.ts,
274+
i,
275+
);
276+
continue;
238277
}
239-
}
240278

241-
flushAgentMessage(events.length - 1);
279+
if (event.type !== "session_update") continue;
280+
281+
const parsed = parseSessionNotification(event.notification);
282+
if (!parsed) continue;
283+
284+
switch (parsed.type) {
285+
case "user":
286+
builder.addUser(parsed.content, event.ts, i);
287+
break;
288+
case "agent":
289+
builder.addAgentChunk(parsed.content, i);
290+
break;
291+
case "tool":
292+
builder.addTool(parsed.toolData, i);
293+
break;
294+
case "tool_update":
295+
builder.updateTool(parsed.toolData);
296+
break;
297+
}
298+
}
242299

243-
return messages;
300+
return builder.build();
244301
}
245302

246303
interface ConversationTurn {
@@ -374,6 +431,15 @@ export function SessionView({
374431
switch (message.type) {
375432
case "agent":
376433
return <AgentMessage key={message.id} content={message.content} />;
434+
case "console":
435+
return message.consoleData ? (
436+
<ConsoleMessage
437+
key={message.id}
438+
level={message.consoleData.level}
439+
message={message.consoleData.message}
440+
timestamp={message.consoleData.timestamp}
441+
/>
442+
) : null;
377443
case "tool":
378444
return message.toolData ? (
379445
<ToolCallBlock

apps/array/src/renderer/features/sessions/components/TurnCollapsible.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface ToolData {
1515

1616
interface ParsedMessage {
1717
id: string;
18-
type: "user" | "agent" | "tool";
18+
type: "user" | "agent" | "tool" | "console";
1919
content: string;
2020
toolData?: ToolData;
2121
}

0 commit comments

Comments
 (0)