iterate 0.2.5 → 0.2.7
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 +661 -0
- package/README.md +40 -77
- package/bin/iterate.js +12 -5
- package/dist/cli-DMS4kJph.mjs +868 -0
- package/dist/cli-DMS4kJph.mjs.map +1 -0
- package/dist/config-DtnR7Lv7.mjs +170 -0
- package/dist/config-DtnR7Lv7.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +8 -0
- package/dist/index.mjs.map +1 -0
- package/dist/stream-tui/agent-chat-terminal.d.mts +1 -0
- package/dist/stream-tui/agent-chat-terminal.mjs +933 -0
- package/dist/stream-tui/agent-chat-terminal.mjs.map +1 -0
- package/dist/worker.d.mts +33 -0
- package/dist/worker.mjs +18 -0
- package/dist/worker.mjs.map +1 -0
- package/package.json +31 -23
- package/dist/cli.d.ts +0 -7
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -430
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { c as connectItx, r as readConfig } from "../config-DtnR7Lv7.mjs";
|
|
3
|
+
import { StyledText, bg, createCliRenderer, fg } from "@opentui/core";
|
|
4
|
+
import { createRoot, useKeyboard } from "@opentui/react";
|
|
5
|
+
import { useCallback, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { jsx, jsxs } from "@opentui/react/jsx-runtime";
|
|
7
|
+
//#region ../ui/src/components/events/agent-ui-reducer.ts
|
|
8
|
+
function initialAgentUiState() {
|
|
9
|
+
return {
|
|
10
|
+
live: null,
|
|
11
|
+
queuedUserMessages: [],
|
|
12
|
+
eventCount: 0,
|
|
13
|
+
presence: [],
|
|
14
|
+
nextLocalIndex: 0
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fold a batch of events into settled-item ops + the resulting state.
|
|
19
|
+
* Idempotent by construction: replaying the same events from the same entry
|
|
20
|
+
* state yields the same ops, and the processor upserts by local_index.
|
|
21
|
+
*/
|
|
22
|
+
function planAgentUiOps(start, events) {
|
|
23
|
+
const ops = [];
|
|
24
|
+
let state = start;
|
|
25
|
+
for (const event of events) state = reduceAgentUiEvent(state, event, ops);
|
|
26
|
+
return {
|
|
27
|
+
endState: state,
|
|
28
|
+
ops
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Append a settled item op at the next dense position. */
|
|
32
|
+
function emitItem(state, ops, item) {
|
|
33
|
+
ops.push({
|
|
34
|
+
localIndex: state.nextLocalIndex,
|
|
35
|
+
item
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
nextLocalIndex: state.nextLocalIndex + 1
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const AGENT_LLM_REQUEST_REQUESTED = "events.iterate.com/agent/llm-request-requested";
|
|
43
|
+
const AGENT_LLM_REQUEST_COMPLETED = "events.iterate.com/agent/llm-request-completed";
|
|
44
|
+
const AGENT_LLM_REQUEST_CANCELLED = "events.iterate.com/agent/llm-request-cancelled";
|
|
45
|
+
const AGENT_OUTPUT_ADDED = "events.iterate.com/agent/output-added";
|
|
46
|
+
const AGENT_STATUS_UPDATED = "events.iterate.com/agent/status-updated";
|
|
47
|
+
const OPENAI_WS_REQUEST_STARTED = "events.iterate.com/openai-ws/llm-request-started";
|
|
48
|
+
const OPENAI_WS_RESPONSE_CHUNK = "events.iterate.com/openai-ws/llm-response-chunk";
|
|
49
|
+
const CLOUDFLARE_AI_REQUEST_STARTED = "events.iterate.com/cloudflare-ai/llm-request-started";
|
|
50
|
+
const CLOUDFLARE_AI_RESPONSE_CHUNK = "events.iterate.com/cloudflare-ai/llm-response-chunk";
|
|
51
|
+
const SCRIPT_EXECUTION_REQUESTED = "events.iterate.com/capability-host/script-execution-requested";
|
|
52
|
+
const SCRIPT_EXECUTION_COMPLETED = "events.iterate.com/capability-host/script-execution-completed";
|
|
53
|
+
const CODEMODE_SCRIPT_EXECUTION_REQUESTED = "events.iterate.com/codemode/script-execution-requested";
|
|
54
|
+
const CODEMODE_SCRIPT_EXECUTION_COMPLETED = "events.iterate.com/codemode/script-execution-completed";
|
|
55
|
+
const STREAM_SUBSCRIBER_CONNECTED = "events.iterate.com/stream/subscriber-connected";
|
|
56
|
+
const STREAM_SUBSCRIBER_DISCONNECTED = "events.iterate.com/stream/subscriber-disconnected";
|
|
57
|
+
const STREAM_WOKEN = "events.iterate.com/stream/woken";
|
|
58
|
+
function reduceAgentUiEvent(previous, event, ops) {
|
|
59
|
+
const state = {
|
|
60
|
+
...previous,
|
|
61
|
+
queuedUserMessages: previous.queuedUserMessages ?? [],
|
|
62
|
+
eventCount: previous.eventCount + 1
|
|
63
|
+
};
|
|
64
|
+
const timestampMs = Date.parse(event.createdAt);
|
|
65
|
+
switch (event.type) {
|
|
66
|
+
case "events.iterate.com/agents/user-message-received": {
|
|
67
|
+
const text = readString(event, "content");
|
|
68
|
+
if (text == null) return state;
|
|
69
|
+
const hasRunningStep = state.live?.steps.some((step) => step.status === "running") ?? false;
|
|
70
|
+
const item = {
|
|
71
|
+
kind: "user",
|
|
72
|
+
id: `user-${event.offset}`,
|
|
73
|
+
text,
|
|
74
|
+
timestampMs
|
|
75
|
+
};
|
|
76
|
+
if (hasRunningStep) return {
|
|
77
|
+
...state,
|
|
78
|
+
queuedUserMessages: [...state.queuedUserMessages, item]
|
|
79
|
+
};
|
|
80
|
+
return emitItem(hasRunningStep ? state : settleLive(state, timestampMs, ops), ops, item);
|
|
81
|
+
}
|
|
82
|
+
case "events.iterate.com/agents/web-message-sent":
|
|
83
|
+
case "events.iterate.com/agents/tui-message-sent": {
|
|
84
|
+
const text = readString(event, "message");
|
|
85
|
+
if (text == null) return state;
|
|
86
|
+
return emitItem(settleLive(state, timestampMs, ops), ops, {
|
|
87
|
+
kind: "assistant",
|
|
88
|
+
id: `assistant-${event.offset}`,
|
|
89
|
+
text,
|
|
90
|
+
timestampMs
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
case AGENT_LLM_REQUEST_REQUESTED: {
|
|
94
|
+
const base = state.queuedUserMessages.length === 0 ? state : flushQueuedUserMessages(settleLive(state, timestampMs, ops), ops);
|
|
95
|
+
const live = ensureLive(base, event.offset, timestampMs);
|
|
96
|
+
const model = readString(event, "model");
|
|
97
|
+
const step = {
|
|
98
|
+
kind: "llm",
|
|
99
|
+
id: `llm-${event.offset}`,
|
|
100
|
+
llmRequestId: event.offset,
|
|
101
|
+
status: "running",
|
|
102
|
+
...model == null ? {} : { model },
|
|
103
|
+
thinkingText: "",
|
|
104
|
+
responseText: "",
|
|
105
|
+
startedAtMs: timestampMs
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
...base,
|
|
109
|
+
live: {
|
|
110
|
+
...live,
|
|
111
|
+
steps: [...live.steps, step]
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
case OPENAI_WS_REQUEST_STARTED:
|
|
116
|
+
case CLOUDFLARE_AI_REQUEST_STARTED: {
|
|
117
|
+
const llmRequestId = readNumber(event, "llmRequestId");
|
|
118
|
+
const model = readString(event, "model");
|
|
119
|
+
if (llmRequestId == null || model == null) return state;
|
|
120
|
+
return updateLlmStep(state, llmRequestId, (step) => ({
|
|
121
|
+
...step,
|
|
122
|
+
model
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
case OPENAI_WS_RESPONSE_CHUNK: {
|
|
126
|
+
const llmRequestId = readNumber(event, "llmRequestId");
|
|
127
|
+
const message = readRecord(event, "chunk");
|
|
128
|
+
if (llmRequestId == null || message == null) return state;
|
|
129
|
+
const frameType = typeof message.type === "string" ? message.type : "";
|
|
130
|
+
const delta = typeof message.delta === "string" ? message.delta : "";
|
|
131
|
+
if (frameType === "response.output_text.delta" && delta !== "") return updateLlmStep(state, llmRequestId, (step) => ({
|
|
132
|
+
...step,
|
|
133
|
+
responseText: step.status === "running" ? step.responseText + delta : step.responseText
|
|
134
|
+
}));
|
|
135
|
+
if ((frameType === "response.reasoning_summary_text.delta" || frameType === "response.reasoning_text.delta") && delta !== "") return updateLlmStep(state, llmRequestId, (step) => ({
|
|
136
|
+
...step,
|
|
137
|
+
thinkingText: step.status === "running" ? step.thinkingText + delta : step.thinkingText
|
|
138
|
+
}));
|
|
139
|
+
if (frameType === "response.reasoning_summary_part.added") return updateLlmStep(state, llmRequestId, (step) => ({
|
|
140
|
+
...step,
|
|
141
|
+
thinkingText: step.status !== "running" || step.thinkingText === "" ? step.thinkingText : `${step.thinkingText}\n\n`
|
|
142
|
+
}));
|
|
143
|
+
return state;
|
|
144
|
+
}
|
|
145
|
+
case CLOUDFLARE_AI_RESPONSE_CHUNK: {
|
|
146
|
+
const llmRequestId = readNumber(event, "llmRequestId");
|
|
147
|
+
const chunk = readPayloadRecord(event)?.chunk;
|
|
148
|
+
if (llmRequestId == null) return state;
|
|
149
|
+
const { responseDelta, thinkingDelta } = extractCloudflareChunkDeltas(chunk);
|
|
150
|
+
if (responseDelta === "" && thinkingDelta === "") return state;
|
|
151
|
+
return updateLlmStep(state, llmRequestId, (step) => ({
|
|
152
|
+
...step,
|
|
153
|
+
responseText: step.status === "running" ? step.responseText + responseDelta : step.responseText,
|
|
154
|
+
thinkingText: step.status === "running" ? step.thinkingText + thinkingDelta : step.thinkingText
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
case AGENT_OUTPUT_ADDED: {
|
|
158
|
+
const llmRequestId = readNumber(event, "llmRequestId");
|
|
159
|
+
const content = readString(event, "content");
|
|
160
|
+
if (llmRequestId == null || content == null) return state;
|
|
161
|
+
return updateLlmStep(state, llmRequestId, (step) => step.status === "running" ? {
|
|
162
|
+
...step,
|
|
163
|
+
responseText: content
|
|
164
|
+
} : step);
|
|
165
|
+
}
|
|
166
|
+
case AGENT_LLM_REQUEST_COMPLETED: {
|
|
167
|
+
const llmRequestId = readNumber(event, "llmRequestId");
|
|
168
|
+
if (llmRequestId == null) return state;
|
|
169
|
+
const payload = readPayloadRecord(event);
|
|
170
|
+
const result = isRecord(payload?.result) ? payload.result : void 0;
|
|
171
|
+
const status = typeof result?.status === "string" ? result.status : "success";
|
|
172
|
+
const usage = readUsageTokens(result?.usage);
|
|
173
|
+
const errorMessage = isRecord(result?.error) && typeof result.error.message === "string" ? result.error.message : void 0;
|
|
174
|
+
return updateLlmStep(state, llmRequestId, (step) => step.outcome === "cancelled" ? step : {
|
|
175
|
+
...step,
|
|
176
|
+
status: "done",
|
|
177
|
+
outcome: status === "success" ? "completed" : "failed",
|
|
178
|
+
...typeof payload?.provider === "string" ? { provider: payload.provider } : {},
|
|
179
|
+
...typeof payload?.durationMs === "number" ? { durationMs: payload.durationMs } : {},
|
|
180
|
+
...usage.input == null ? {} : { inputTokens: usage.input },
|
|
181
|
+
...usage.output == null ? {} : { outputTokens: usage.output },
|
|
182
|
+
...errorMessage == null ? {} : { errorMessage },
|
|
183
|
+
...typeof result?.providerResponseId === "string" ? { providerResponseId: result.providerResponseId } : {}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
case AGENT_LLM_REQUEST_CANCELLED: {
|
|
187
|
+
const llmRequestId = readNumber(event, "llmRequestId");
|
|
188
|
+
if (llmRequestId == null) return state;
|
|
189
|
+
return updateLlmStep(state, llmRequestId, (step) => ({
|
|
190
|
+
...step,
|
|
191
|
+
status: "done",
|
|
192
|
+
outcome: "cancelled"
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
case SCRIPT_EXECUTION_REQUESTED:
|
|
196
|
+
case CODEMODE_SCRIPT_EXECUTION_REQUESTED: {
|
|
197
|
+
const payload = readPayloadRecord(event);
|
|
198
|
+
const executionId = typeof payload?.executionId === "string" ? payload.executionId : `exec-${event.offset}`;
|
|
199
|
+
const live = ensureLive(state, event.offset, timestampMs);
|
|
200
|
+
const step = {
|
|
201
|
+
kind: "code",
|
|
202
|
+
id: `code-${executionId}`,
|
|
203
|
+
executionId,
|
|
204
|
+
status: "running",
|
|
205
|
+
code: typeof payload?.code === "string" ? payload.code : "",
|
|
206
|
+
startedAtMs: timestampMs
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
...state,
|
|
210
|
+
live: {
|
|
211
|
+
...live,
|
|
212
|
+
steps: [...live.steps, step]
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
case SCRIPT_EXECUTION_COMPLETED:
|
|
217
|
+
case CODEMODE_SCRIPT_EXECUTION_COMPLETED: {
|
|
218
|
+
const payload = readPayloadRecord(event);
|
|
219
|
+
if (payload == null || state.live == null) return state;
|
|
220
|
+
const executionId = typeof payload.executionId === "string" ? payload.executionId : null;
|
|
221
|
+
const outcome = readCodeOutcome(payload);
|
|
222
|
+
const steps = [...state.live.steps];
|
|
223
|
+
const index = executionId == null ? steps.findLastIndex((step) => step.kind === "code" && step.status === "running") : steps.findIndex((step) => step.kind === "code" && step.executionId === executionId);
|
|
224
|
+
const step = steps[index];
|
|
225
|
+
if (step == null || step.kind !== "code") return state;
|
|
226
|
+
steps[index] = {
|
|
227
|
+
...step,
|
|
228
|
+
status: "done",
|
|
229
|
+
...outcome
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
...state,
|
|
233
|
+
live: {
|
|
234
|
+
...state.live,
|
|
235
|
+
steps
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
case AGENT_STATUS_UPDATED:
|
|
240
|
+
if (readString(event, "status") !== "idle") return state;
|
|
241
|
+
if (state.live == null) return state;
|
|
242
|
+
return {
|
|
243
|
+
...state,
|
|
244
|
+
live: {
|
|
245
|
+
...state.live,
|
|
246
|
+
status: "waiting"
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
case STREAM_SUBSCRIBER_CONNECTED: {
|
|
250
|
+
const payload = readPayloadRecord(event);
|
|
251
|
+
if (payload == null) return state;
|
|
252
|
+
const subscriptionKey = typeof payload.subscriptionKey === "string" ? payload.subscriptionKey : null;
|
|
253
|
+
if (subscriptionKey == null) return state;
|
|
254
|
+
const direction = payload.direction === "inbound" ? "inbound" : "outbound";
|
|
255
|
+
const subscriber = isRecord(payload.subscriber) ? payload.subscriber : void 0;
|
|
256
|
+
const announcement = readProcessorAnnouncement(subscriber?.processor);
|
|
257
|
+
const entry = {
|
|
258
|
+
subscriptionKey,
|
|
259
|
+
direction,
|
|
260
|
+
connected: true,
|
|
261
|
+
...typeof subscriber?.description === "string" ? { description: subscriber.description } : {},
|
|
262
|
+
...announcement == null ? {} : { processor: announcement }
|
|
263
|
+
};
|
|
264
|
+
const existingIndex = state.presence.findIndex((candidate) => candidate.subscriptionKey === subscriptionKey);
|
|
265
|
+
const presence = existingIndex === -1 ? [...state.presence, entry] : state.presence.map((candidate, index) => index === existingIndex ? {
|
|
266
|
+
...candidate,
|
|
267
|
+
...entry
|
|
268
|
+
} : candidate);
|
|
269
|
+
return {
|
|
270
|
+
...state,
|
|
271
|
+
presence
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
case STREAM_SUBSCRIBER_DISCONNECTED: {
|
|
275
|
+
const subscriptionKey = readString(event, "subscriptionKey");
|
|
276
|
+
if (subscriptionKey == null) return state;
|
|
277
|
+
return {
|
|
278
|
+
...state,
|
|
279
|
+
presence: state.presence.map((entry) => entry.subscriptionKey === subscriptionKey ? {
|
|
280
|
+
...entry,
|
|
281
|
+
connected: false
|
|
282
|
+
} : entry)
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
case STREAM_WOKEN: return {
|
|
286
|
+
...state,
|
|
287
|
+
presence: state.presence.map((entry) => entry.connected ? {
|
|
288
|
+
...entry,
|
|
289
|
+
connected: false
|
|
290
|
+
} : entry)
|
|
291
|
+
};
|
|
292
|
+
default: return state;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function ensureLive(state, offset, startedAtMs) {
|
|
296
|
+
if (state.live != null) return {
|
|
297
|
+
...state.live,
|
|
298
|
+
status: "running"
|
|
299
|
+
};
|
|
300
|
+
return {
|
|
301
|
+
kind: "activity",
|
|
302
|
+
id: `activity-${offset}`,
|
|
303
|
+
status: "running",
|
|
304
|
+
steps: [],
|
|
305
|
+
startedAtMs
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/** Closes the live activity (if any) and emits it as a settled item. */
|
|
309
|
+
function settleLive(state, endedAtMs, ops) {
|
|
310
|
+
if (state.live == null) return state;
|
|
311
|
+
if (state.live.steps.length === 0) return {
|
|
312
|
+
...state,
|
|
313
|
+
live: null
|
|
314
|
+
};
|
|
315
|
+
const settled = {
|
|
316
|
+
...state.live,
|
|
317
|
+
status: "done",
|
|
318
|
+
endedAtMs,
|
|
319
|
+
steps: state.live.steps.map((step) => step.status === "running" ? {
|
|
320
|
+
...step,
|
|
321
|
+
status: "done"
|
|
322
|
+
} : step)
|
|
323
|
+
};
|
|
324
|
+
return emitItem({
|
|
325
|
+
...state,
|
|
326
|
+
live: null
|
|
327
|
+
}, ops, settled);
|
|
328
|
+
}
|
|
329
|
+
function flushQueuedUserMessages(state, ops) {
|
|
330
|
+
let next = {
|
|
331
|
+
...state,
|
|
332
|
+
queuedUserMessages: []
|
|
333
|
+
};
|
|
334
|
+
for (const item of state.queuedUserMessages) next = emitItem(next, ops, item);
|
|
335
|
+
return next;
|
|
336
|
+
}
|
|
337
|
+
function updateLlmStep(state, llmRequestId, update) {
|
|
338
|
+
if (state.live == null) return state;
|
|
339
|
+
const index = state.live.steps.findIndex((step) => step.kind === "llm" && step.llmRequestId === llmRequestId);
|
|
340
|
+
const step = state.live.steps[index];
|
|
341
|
+
if (step == null || step.kind !== "llm") return state;
|
|
342
|
+
const steps = [...state.live.steps];
|
|
343
|
+
steps[index] = update(step);
|
|
344
|
+
return {
|
|
345
|
+
...state,
|
|
346
|
+
live: {
|
|
347
|
+
...state.live,
|
|
348
|
+
steps
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function extractCloudflareChunkDeltas(chunk) {
|
|
353
|
+
if (typeof chunk === "string") return {
|
|
354
|
+
responseDelta: chunk,
|
|
355
|
+
thinkingDelta: ""
|
|
356
|
+
};
|
|
357
|
+
if (!isRecord(chunk)) return {
|
|
358
|
+
responseDelta: "",
|
|
359
|
+
thinkingDelta: ""
|
|
360
|
+
};
|
|
361
|
+
if (typeof chunk.response === "string") return {
|
|
362
|
+
responseDelta: chunk.response,
|
|
363
|
+
thinkingDelta: ""
|
|
364
|
+
};
|
|
365
|
+
if (Array.isArray(chunk.choices) && isRecord(chunk.choices[0])) {
|
|
366
|
+
const delta = isRecord(chunk.choices[0].delta) ? chunk.choices[0].delta : void 0;
|
|
367
|
+
return {
|
|
368
|
+
responseDelta: typeof delta?.content === "string" ? delta.content : "",
|
|
369
|
+
thinkingDelta: typeof delta?.reasoning_content === "string" ? delta.reasoning_content : ""
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (isRecord(chunk.delta)) return {
|
|
373
|
+
responseDelta: typeof chunk.delta.text === "string" ? chunk.delta.text : "",
|
|
374
|
+
thinkingDelta: typeof chunk.delta.thinking === "string" ? chunk.delta.thinking : ""
|
|
375
|
+
};
|
|
376
|
+
return {
|
|
377
|
+
responseDelta: "",
|
|
378
|
+
thinkingDelta: ""
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function readCodeOutcome(payload) {
|
|
382
|
+
const outcome = isRecord(payload.outcome) ? payload.outcome : void 0;
|
|
383
|
+
const success = payload.ok !== false && payload.error == null && outcome?.status !== "threw";
|
|
384
|
+
const result = "result" in payload ? payload.result : outcome?.value;
|
|
385
|
+
const error = typeof payload.error === "string" ? payload.error : outcome?.status === "threw" ? stringifyUnknown(outcome.error) : void 0;
|
|
386
|
+
const logs = Array.isArray(payload.logs) ? payload.logs.filter((line) => typeof line === "string") : void 0;
|
|
387
|
+
return {
|
|
388
|
+
success,
|
|
389
|
+
...result === void 0 ? {} : { result },
|
|
390
|
+
...error == null ? {} : { errorMessage: error },
|
|
391
|
+
...logs == null ? {} : { logs },
|
|
392
|
+
...typeof payload.durationMs === "number" ? { durationMs: payload.durationMs } : {}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function readUsageTokens(usage) {
|
|
396
|
+
if (!isRecord(usage)) return {};
|
|
397
|
+
const input = usage.input_tokens ?? usage.prompt_tokens;
|
|
398
|
+
const output = usage.output_tokens ?? usage.completion_tokens;
|
|
399
|
+
return {
|
|
400
|
+
...typeof input === "number" ? { input } : {},
|
|
401
|
+
...typeof output === "number" ? { output } : {}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function readProcessorAnnouncement(value) {
|
|
405
|
+
if (!isRecord(value)) return null;
|
|
406
|
+
const announcement = isRecord(value.announcement) ? value.announcement : value;
|
|
407
|
+
if (typeof announcement.slug !== "string" || typeof announcement.version !== "string") return null;
|
|
408
|
+
return {
|
|
409
|
+
slug: announcement.slug,
|
|
410
|
+
version: announcement.version,
|
|
411
|
+
description: typeof announcement.description === "string" ? announcement.description : "",
|
|
412
|
+
consumes: readStringArray(announcement.consumes),
|
|
413
|
+
emits: readStringArray(announcement.emits),
|
|
414
|
+
ownedEvents: Array.isArray(announcement.ownedEvents) ? announcement.ownedEvents.filter((owned) => isRecord(owned)).filter((owned) => typeof owned.type === "string").map((owned) => ({
|
|
415
|
+
type: owned.type,
|
|
416
|
+
...typeof owned.description === "string" ? { description: owned.description } : {}
|
|
417
|
+
})) : []
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function readStringArray(value) {
|
|
421
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
422
|
+
}
|
|
423
|
+
function readString(event, key) {
|
|
424
|
+
const value = readPayloadRecord(event)?.[key];
|
|
425
|
+
return typeof value === "string" ? value : null;
|
|
426
|
+
}
|
|
427
|
+
function readNumber(event, key) {
|
|
428
|
+
const value = readPayloadRecord(event)?.[key];
|
|
429
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
430
|
+
}
|
|
431
|
+
function readRecord(event, key) {
|
|
432
|
+
const value = readPayloadRecord(event)?.[key];
|
|
433
|
+
return isRecord(value) ? value : null;
|
|
434
|
+
}
|
|
435
|
+
function readPayloadRecord(event) {
|
|
436
|
+
return isRecord(event.payload) ? event.payload : null;
|
|
437
|
+
}
|
|
438
|
+
function isRecord(value) {
|
|
439
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
440
|
+
}
|
|
441
|
+
function stringifyUnknown(value) {
|
|
442
|
+
if (typeof value === "string") return value;
|
|
443
|
+
try {
|
|
444
|
+
return JSON.stringify(value);
|
|
445
|
+
} catch {
|
|
446
|
+
return String(value);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
//#endregion
|
|
450
|
+
//#region src/stream-tui/agent-feed-model.ts
|
|
451
|
+
/**
|
|
452
|
+
* In-memory agent conversation feed for the chat TUI.
|
|
453
|
+
*
|
|
454
|
+
* This is the node-side sibling of the browser mirror's agent-ui processor
|
|
455
|
+
* (apps/os/src/domains/streams/client-libraries/processors/agent-ui-processor.ts):
|
|
456
|
+
* both fold the SAME shared reducer (`planAgentUiOps` from @iterate-com/ui)
|
|
457
|
+
* over agent stream events. The browser persists settled items into SQLite for
|
|
458
|
+
* a virtual list; a terminal session is ephemeral and small, so this model
|
|
459
|
+
* keeps the settled items in a plain array and the streaming live activity in
|
|
460
|
+
* the reduced state — no database, no processor host.
|
|
461
|
+
*/
|
|
462
|
+
function createAgentFeedModel() {
|
|
463
|
+
let state = initialAgentUiState();
|
|
464
|
+
let items = [];
|
|
465
|
+
let lastOffset = 0;
|
|
466
|
+
let snapshot = {
|
|
467
|
+
items,
|
|
468
|
+
live: null,
|
|
469
|
+
eventCount: 0,
|
|
470
|
+
lastOffset
|
|
471
|
+
};
|
|
472
|
+
return {
|
|
473
|
+
snapshot: () => snapshot,
|
|
474
|
+
applyEvents(events) {
|
|
475
|
+
const fresh = events.filter((event) => event.offset > lastOffset);
|
|
476
|
+
if (fresh.length === 0) return false;
|
|
477
|
+
lastOffset = fresh[fresh.length - 1].offset;
|
|
478
|
+
const { endState, ops } = planAgentUiOps(state, fresh);
|
|
479
|
+
if (ops.length > 0) {
|
|
480
|
+
items = [...items];
|
|
481
|
+
for (const op of ops) items[op.localIndex] = op.item;
|
|
482
|
+
}
|
|
483
|
+
state = endState;
|
|
484
|
+
snapshot = {
|
|
485
|
+
items,
|
|
486
|
+
live: state.live,
|
|
487
|
+
eventCount: state.eventCount,
|
|
488
|
+
lastOffset
|
|
489
|
+
};
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/stream-tui/agent-connection.ts
|
|
496
|
+
const RECONNECT_DELAY_MS = 1e3;
|
|
497
|
+
const MAX_RECONNECT_DELAY_MS = 15e3;
|
|
498
|
+
/**
|
|
499
|
+
* Resolve itx credentials for the TUI, in priority order: an admin API secret
|
|
500
|
+
* from the environment (doppler / e2e lanes), an explicit bearer token, then
|
|
501
|
+
* the stored `iterate login` session for the named config. The launcher
|
|
502
|
+
* (`iterate chat`) refreshes the stored session before spawning the TUI, so a
|
|
503
|
+
* plain bearer read is enough here.
|
|
504
|
+
*/
|
|
505
|
+
function resolveItxAuth(input) {
|
|
506
|
+
const adminSecret = readEnv("APP_CONFIG_ADMIN_API_SECRET");
|
|
507
|
+
if (adminSecret) return {
|
|
508
|
+
type: "admin-secret",
|
|
509
|
+
secret: adminSecret
|
|
510
|
+
};
|
|
511
|
+
const bearerToken = readEnv("ITERATE_BEARER_TOKEN");
|
|
512
|
+
if (bearerToken) return {
|
|
513
|
+
type: "bearer",
|
|
514
|
+
token: bearerToken
|
|
515
|
+
};
|
|
516
|
+
if (input.configName) {
|
|
517
|
+
const config = readConfig(input.configName, { throw: true });
|
|
518
|
+
if (config.session?.token) return {
|
|
519
|
+
type: "bearer",
|
|
520
|
+
token: config.session.token
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
throw new Error("No credentials: run `iterate login`, or set an admin API secret (APP_CONFIG_ADMIN_API_SECRET) or a bearer token (ITERATE_BEARER_TOKEN).");
|
|
524
|
+
}
|
|
525
|
+
function connectAgentFeed(input) {
|
|
526
|
+
let disposed = false;
|
|
527
|
+
let agent;
|
|
528
|
+
let subscription;
|
|
529
|
+
let consecutiveFailures = 0;
|
|
530
|
+
const disposeAgent = () => {
|
|
531
|
+
try {
|
|
532
|
+
subscription?.[Symbol.dispose]?.();
|
|
533
|
+
agent?.[Symbol.dispose]?.();
|
|
534
|
+
} catch {}
|
|
535
|
+
subscription = void 0;
|
|
536
|
+
agent = void 0;
|
|
537
|
+
};
|
|
538
|
+
const scheduleReconnect = (detail) => {
|
|
539
|
+
if (disposed) return;
|
|
540
|
+
disposeAgent();
|
|
541
|
+
consecutiveFailures += 1;
|
|
542
|
+
const delay = Math.min(RECONNECT_DELAY_MS * consecutiveFailures, MAX_RECONNECT_DELAY_MS);
|
|
543
|
+
input.onStatus({
|
|
544
|
+
kind: "reconnecting",
|
|
545
|
+
detail
|
|
546
|
+
});
|
|
547
|
+
setTimeout(() => void establish(), delay);
|
|
548
|
+
};
|
|
549
|
+
async function establish() {
|
|
550
|
+
if (disposed) return;
|
|
551
|
+
input.onStatus({ kind: "connecting" });
|
|
552
|
+
const nextAgent = connectItx({
|
|
553
|
+
auth: input.auth,
|
|
554
|
+
baseUrl: input.baseUrl,
|
|
555
|
+
projectId: input.projectId,
|
|
556
|
+
agentPath: input.agentPath
|
|
557
|
+
});
|
|
558
|
+
agent = nextAgent;
|
|
559
|
+
nextAgent.onRpcBroken?.((error) => {
|
|
560
|
+
if (agent !== nextAgent) return;
|
|
561
|
+
scheduleReconnect(errorMessage(error));
|
|
562
|
+
});
|
|
563
|
+
try {
|
|
564
|
+
subscription = await nextAgent.stream.subscribe({
|
|
565
|
+
processEventBatch: (batch) => input.onEvents(batch.events),
|
|
566
|
+
replayAfterOffset: input.replayAfterOffset(),
|
|
567
|
+
subscriber: { description: "iterate chat TUI" }
|
|
568
|
+
});
|
|
569
|
+
if (disposed) {
|
|
570
|
+
disposeAgent();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
consecutiveFailures = 0;
|
|
574
|
+
input.onStatus({ kind: "live" });
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (agent === nextAgent) scheduleReconnect(errorMessage(error));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
establish();
|
|
580
|
+
return {
|
|
581
|
+
async sendMessage(text) {
|
|
582
|
+
if (agent === void 0) throw new Error("not connected");
|
|
583
|
+
await agent.sendMessage(text);
|
|
584
|
+
},
|
|
585
|
+
dispose() {
|
|
586
|
+
disposed = true;
|
|
587
|
+
disposeAgent();
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function errorMessage(error) {
|
|
592
|
+
return error instanceof Error ? error.message : String(error);
|
|
593
|
+
}
|
|
594
|
+
function readEnv(name) {
|
|
595
|
+
const value = process.env[name]?.trim();
|
|
596
|
+
return value ? value : void 0;
|
|
597
|
+
}
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/stream-tui/feed-format.ts
|
|
600
|
+
function formatActivitySummary(activity) {
|
|
601
|
+
const codeCount = activity.steps.filter((step) => step.kind === "code").length;
|
|
602
|
+
const requestCount = activity.steps.filter((step) => step.kind === "llm").length;
|
|
603
|
+
const interrupted = activity.steps.some((step) => step.kind === "llm" && step.outcome === "cancelled");
|
|
604
|
+
const parts = [];
|
|
605
|
+
if (codeCount > 0) parts.push(`Ran code ${codeCount}×`);
|
|
606
|
+
parts.push(`${requestCount} request${requestCount === 1 ? "" : "s"}`);
|
|
607
|
+
if (interrupted) parts.push("interrupted");
|
|
608
|
+
const totalMs = activity.endedAtMs == null ? null : Math.max(0, activity.endedAtMs - activity.startedAtMs);
|
|
609
|
+
if (totalMs != null && totalMs > 0) parts.push(formatSeconds(totalMs));
|
|
610
|
+
return parts.join(" · ");
|
|
611
|
+
}
|
|
612
|
+
function formatStepLine(step) {
|
|
613
|
+
const parts = [step.kind === "code" ? "Ran code" : step.model ?? step.provider ?? "LLM request"];
|
|
614
|
+
if (step.kind === "llm") {
|
|
615
|
+
if (step.inputTokens != null || step.outputTokens != null) parts.push(`${formatTokens(step.inputTokens)} → ${formatTokens(step.outputTokens)} tok`);
|
|
616
|
+
if (step.outcome === "failed") parts.push("failed");
|
|
617
|
+
if (step.outcome === "cancelled") parts.push("interrupted");
|
|
618
|
+
} else if (step.success === false) parts.push("failed");
|
|
619
|
+
if (step.durationMs != null) parts.push(formatSeconds(step.durationMs));
|
|
620
|
+
if (step.status === "running") parts.push("running");
|
|
621
|
+
return parts.join(" · ");
|
|
622
|
+
}
|
|
623
|
+
function formatSeconds(ms) {
|
|
624
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
625
|
+
}
|
|
626
|
+
function formatTokens(count) {
|
|
627
|
+
if (count == null) return "?";
|
|
628
|
+
if (count < 1e3) return String(count);
|
|
629
|
+
return `${(count / 1e3).toFixed(1)}k`;
|
|
630
|
+
}
|
|
631
|
+
/** The last `maxChars` of streamed text, trimmed to whole lines where possible. */
|
|
632
|
+
function streamingTail(text, maxChars = 600) {
|
|
633
|
+
const trimmed = text.trimEnd();
|
|
634
|
+
if (trimmed.length <= maxChars) return trimmed;
|
|
635
|
+
const tail = trimmed.slice(-maxChars);
|
|
636
|
+
const firstNewline = tail.indexOf("\n");
|
|
637
|
+
return `…${firstNewline === -1 ? tail : tail.slice(firstNewline)}`;
|
|
638
|
+
}
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/stream-tui/agent-chat-terminal.tsx
|
|
641
|
+
/** @jsxImportSource @opentui/react */
|
|
642
|
+
/**
|
|
643
|
+
* React/OpenTUI terminal chat with one project agent.
|
|
644
|
+
*
|
|
645
|
+
* The data layer is the shared client stack, not a bespoke stream client:
|
|
646
|
+
* `connectItx` (apps/os/src/itx-client.ts) hands us the same `Agent`
|
|
647
|
+
* capability the web app uses, a live `stream.subscribe` pumps events into
|
|
648
|
+
* the shared agent-ui reducer (@iterate-com/ui), and sends go through
|
|
649
|
+
* `agent.sendMessage`. This file owns only terminal runtime state and
|
|
650
|
+
* rendering.
|
|
651
|
+
*/
|
|
652
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("iterate chat requires an interactive terminal.");
|
|
653
|
+
const COLORS = {
|
|
654
|
+
bg: "#0b0f14",
|
|
655
|
+
surface: "#27272a",
|
|
656
|
+
border: "#3f3f46",
|
|
657
|
+
accent: "#22c55e",
|
|
658
|
+
warning: "#facc15",
|
|
659
|
+
danger: "#ef4444",
|
|
660
|
+
text: "#e5e7eb",
|
|
661
|
+
textSecondary: "#9ca3af",
|
|
662
|
+
textBody: "#d1d5db",
|
|
663
|
+
textMuted: "#6b7280",
|
|
664
|
+
agent: "#a78bfa"
|
|
665
|
+
};
|
|
666
|
+
const args = parseArgs(process.argv.slice(2));
|
|
667
|
+
const model = createAgentFeedModel();
|
|
668
|
+
let appState = {
|
|
669
|
+
feed: model.snapshot(),
|
|
670
|
+
status: { kind: "connecting" },
|
|
671
|
+
notice: ""
|
|
672
|
+
};
|
|
673
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
674
|
+
function patchAppState(patch) {
|
|
675
|
+
appState = {
|
|
676
|
+
...appState,
|
|
677
|
+
...patch
|
|
678
|
+
};
|
|
679
|
+
for (const listener of listeners) listener();
|
|
680
|
+
}
|
|
681
|
+
const connection = connectAgentFeed({
|
|
682
|
+
auth: resolveItxAuth({ configName: process.env.ITERATE_CONFIG_NAME }),
|
|
683
|
+
baseUrl: args.baseUrl,
|
|
684
|
+
projectId: args.projectId,
|
|
685
|
+
agentPath: args.agentPath,
|
|
686
|
+
replayAfterOffset: () => model.snapshot().lastOffset,
|
|
687
|
+
onEvents: (events) => {
|
|
688
|
+
if (model.applyEvents(events)) patchAppState({ feed: model.snapshot() });
|
|
689
|
+
},
|
|
690
|
+
onStatus: (status) => patchAppState({ status })
|
|
691
|
+
});
|
|
692
|
+
function AgentChatApp() {
|
|
693
|
+
const state = useSyncExternalStore(useCallback((listener) => {
|
|
694
|
+
listeners.add(listener);
|
|
695
|
+
return () => listeners.delete(listener);
|
|
696
|
+
}, []), () => appState);
|
|
697
|
+
const [composerValue, setComposerValue] = useState("");
|
|
698
|
+
const [composerRevision, setComposerRevision] = useState(0);
|
|
699
|
+
const clearComposer = useCallback(() => {
|
|
700
|
+
setComposerValue("");
|
|
701
|
+
setComposerRevision((previous) => previous + 1);
|
|
702
|
+
}, []);
|
|
703
|
+
useKeyboard((key) => {
|
|
704
|
+
if (key.name === "escape") clearComposer();
|
|
705
|
+
});
|
|
706
|
+
const submit = useCallback((value) => {
|
|
707
|
+
const message = value.trim();
|
|
708
|
+
if (message === "") return;
|
|
709
|
+
clearComposer();
|
|
710
|
+
patchAppState({ notice: "sending…" });
|
|
711
|
+
connection.sendMessage(message).then(() => patchAppState({ notice: "" })).catch((error) => {
|
|
712
|
+
patchAppState({ notice: `send failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
713
|
+
});
|
|
714
|
+
}, [clearComposer]);
|
|
715
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
716
|
+
width: "100%",
|
|
717
|
+
height: "100%",
|
|
718
|
+
flexDirection: "column",
|
|
719
|
+
backgroundColor: COLORS.bg,
|
|
720
|
+
children: [
|
|
721
|
+
/* @__PURE__ */ jsx(ChatHeader, {
|
|
722
|
+
status: state.status,
|
|
723
|
+
notice: state.notice,
|
|
724
|
+
eventCount: state.feed.eventCount
|
|
725
|
+
}),
|
|
726
|
+
/* @__PURE__ */ jsxs("scrollbox", {
|
|
727
|
+
width: "100%",
|
|
728
|
+
flexGrow: 1,
|
|
729
|
+
border: true,
|
|
730
|
+
borderStyle: "single",
|
|
731
|
+
borderColor: COLORS.border,
|
|
732
|
+
backgroundColor: COLORS.bg,
|
|
733
|
+
stickyScroll: true,
|
|
734
|
+
stickyStart: "bottom",
|
|
735
|
+
contentOptions: {
|
|
736
|
+
flexDirection: "column",
|
|
737
|
+
paddingLeft: 1,
|
|
738
|
+
paddingRight: 1,
|
|
739
|
+
gap: 1
|
|
740
|
+
},
|
|
741
|
+
children: [
|
|
742
|
+
state.feed.items.length === 0 && state.feed.live == null ? /* @__PURE__ */ jsxs("text", {
|
|
743
|
+
fg: COLORS.textMuted,
|
|
744
|
+
children: [
|
|
745
|
+
"No messages yet — say something to ",
|
|
746
|
+
args.agentPath.slice(8),
|
|
747
|
+
"."
|
|
748
|
+
]
|
|
749
|
+
}) : null,
|
|
750
|
+
state.feed.items.map((item) => /* @__PURE__ */ jsx(FeedItem, { item }, item.id)),
|
|
751
|
+
state.feed.live == null ? null : /* @__PURE__ */ jsx(LiveActivity, { activity: state.feed.live })
|
|
752
|
+
]
|
|
753
|
+
}),
|
|
754
|
+
/* @__PURE__ */ jsx("box", {
|
|
755
|
+
width: "100%",
|
|
756
|
+
height: 3,
|
|
757
|
+
border: true,
|
|
758
|
+
borderStyle: "single",
|
|
759
|
+
borderColor: COLORS.accent,
|
|
760
|
+
backgroundColor: COLORS.bg,
|
|
761
|
+
paddingLeft: 1,
|
|
762
|
+
paddingRight: 1,
|
|
763
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
764
|
+
width: "100%",
|
|
765
|
+
value: composerValue,
|
|
766
|
+
placeholder: "Message the agent (Enter to send, Ctrl+C to quit)",
|
|
767
|
+
focused: true,
|
|
768
|
+
backgroundColor: "transparent",
|
|
769
|
+
focusedBackgroundColor: "transparent",
|
|
770
|
+
textColor: COLORS.text,
|
|
771
|
+
focusedTextColor: COLORS.text,
|
|
772
|
+
placeholderColor: COLORS.textMuted,
|
|
773
|
+
cursorColor: COLORS.accent,
|
|
774
|
+
onInput: setComposerValue,
|
|
775
|
+
onSubmit: (value) => {
|
|
776
|
+
if (typeof value === "string") submit(value);
|
|
777
|
+
}
|
|
778
|
+
}, composerRevision)
|
|
779
|
+
})
|
|
780
|
+
]
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
function ChatHeader(props) {
|
|
784
|
+
const statusLabel = props.status.kind === "live" ? "live" : props.status.kind === "connecting" ? "connecting" : `reconnecting (${props.status.detail})`;
|
|
785
|
+
const statusColor = props.status.kind === "live" ? COLORS.accent : props.status.kind === "connecting" ? COLORS.warning : COLORS.danger;
|
|
786
|
+
const meta = [
|
|
787
|
+
`${props.eventCount} event${props.eventCount === 1 ? "" : "s"}`,
|
|
788
|
+
statusLabel,
|
|
789
|
+
props.notice
|
|
790
|
+
].filter(Boolean).join(" · ");
|
|
791
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
792
|
+
width: "100%",
|
|
793
|
+
height: 3,
|
|
794
|
+
border: true,
|
|
795
|
+
borderStyle: "single",
|
|
796
|
+
borderColor: COLORS.border,
|
|
797
|
+
backgroundColor: COLORS.surface,
|
|
798
|
+
flexDirection: "row",
|
|
799
|
+
paddingLeft: 1,
|
|
800
|
+
paddingRight: 1,
|
|
801
|
+
gap: 1,
|
|
802
|
+
children: [
|
|
803
|
+
/* @__PURE__ */ jsx("text", {
|
|
804
|
+
width: 6,
|
|
805
|
+
content: getBrandMarkText()
|
|
806
|
+
}),
|
|
807
|
+
/* @__PURE__ */ jsx("text", {
|
|
808
|
+
flexGrow: 1,
|
|
809
|
+
fg: COLORS.text,
|
|
810
|
+
content: `${args.projectId} ${args.agentPath}`
|
|
811
|
+
}),
|
|
812
|
+
/* @__PURE__ */ jsx("text", {
|
|
813
|
+
fg: COLORS.textSecondary,
|
|
814
|
+
content: meta
|
|
815
|
+
}),
|
|
816
|
+
/* @__PURE__ */ jsx("text", {
|
|
817
|
+
width: 2,
|
|
818
|
+
fg: statusColor,
|
|
819
|
+
children: "●"
|
|
820
|
+
})
|
|
821
|
+
]
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function FeedItem(props) {
|
|
825
|
+
if (props.item.kind === "activity") return /* @__PURE__ */ jsx(SettledActivity, { activity: props.item });
|
|
826
|
+
return /* @__PURE__ */ jsx(Message, { item: props.item });
|
|
827
|
+
}
|
|
828
|
+
function Message(props) {
|
|
829
|
+
const isUser = props.item.kind === "user";
|
|
830
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
831
|
+
flexDirection: "column",
|
|
832
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
833
|
+
fg: isUser ? COLORS.accent : COLORS.agent,
|
|
834
|
+
children: [isUser ? "you ›" : "agent ›", /* @__PURE__ */ jsxs("span", {
|
|
835
|
+
fg: COLORS.textMuted,
|
|
836
|
+
children: [" ", formatClock(props.item.timestampMs)]
|
|
837
|
+
})]
|
|
838
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
839
|
+
fg: COLORS.textBody,
|
|
840
|
+
children: props.item.text
|
|
841
|
+
})]
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function SettledActivity(props) {
|
|
845
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
846
|
+
flexDirection: "column",
|
|
847
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
848
|
+
fg: COLORS.textMuted,
|
|
849
|
+
children: ["✦ ", formatActivitySummary(props.activity)]
|
|
850
|
+
}), props.activity.steps.map((step) => /* @__PURE__ */ jsxs("text", {
|
|
851
|
+
fg: COLORS.textMuted,
|
|
852
|
+
children: [
|
|
853
|
+
" ",
|
|
854
|
+
"· ",
|
|
855
|
+
formatStepLine(step)
|
|
856
|
+
]
|
|
857
|
+
}, step.id))]
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
function LiveActivity(props) {
|
|
861
|
+
const lastStep = props.activity.steps[props.activity.steps.length - 1];
|
|
862
|
+
const thinking = lastStep?.kind === "llm" ? streamingTail(lastStep.thinkingText) : "";
|
|
863
|
+
const streamed = lastStep?.kind === "llm" ? streamingTail(lastStep.responseText) : lastStep?.kind === "code" ? streamingTail(lastStep.code) : "";
|
|
864
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
865
|
+
flexDirection: "column",
|
|
866
|
+
children: [
|
|
867
|
+
/* @__PURE__ */ jsxs("text", {
|
|
868
|
+
fg: props.activity.status === "waiting" ? COLORS.textMuted : COLORS.warning,
|
|
869
|
+
children: ["✦ ", props.activity.status === "waiting" ? "waiting" : "working…"]
|
|
870
|
+
}),
|
|
871
|
+
props.activity.steps.map((step) => /* @__PURE__ */ jsxs("text", {
|
|
872
|
+
fg: COLORS.textMuted,
|
|
873
|
+
children: [
|
|
874
|
+
" ",
|
|
875
|
+
"· ",
|
|
876
|
+
formatStepLine(step)
|
|
877
|
+
]
|
|
878
|
+
}, step.id)),
|
|
879
|
+
thinking === "" ? null : /* @__PURE__ */ jsx("text", {
|
|
880
|
+
fg: COLORS.textMuted,
|
|
881
|
+
children: thinking
|
|
882
|
+
}),
|
|
883
|
+
streamed === "" ? null : /* @__PURE__ */ jsx("text", {
|
|
884
|
+
fg: COLORS.textSecondary,
|
|
885
|
+
children: streamed
|
|
886
|
+
})
|
|
887
|
+
]
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
function getBrandMarkText() {
|
|
891
|
+
return new StyledText([
|
|
892
|
+
fg("#000000")(bg(COLORS.surface)("▐")),
|
|
893
|
+
bg("#000000")(fg("#ffffff")(" 𝑖 ")),
|
|
894
|
+
fg("#000000")(bg(COLORS.surface)("▌"))
|
|
895
|
+
]);
|
|
896
|
+
}
|
|
897
|
+
function formatClock(timestampMs) {
|
|
898
|
+
return new Date(timestampMs).toLocaleTimeString(void 0, {
|
|
899
|
+
hour: "2-digit",
|
|
900
|
+
minute: "2-digit"
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
function parseArgs(argv) {
|
|
904
|
+
const baseUrl = readFlag(argv, "--base-url");
|
|
905
|
+
const projectId = readFlag(argv, "--project-id");
|
|
906
|
+
const agentPath = readFlag(argv, "--agent-path");
|
|
907
|
+
if (baseUrl == null || projectId == null || agentPath == null) throw new Error("Usage: bun agent-chat-terminal.tsx --base-url <url> --project-id <prj_id> --agent-path </agents/name>");
|
|
908
|
+
if (!agentPath.startsWith("/agents/")) throw new Error(`--agent-path must start with "/agents/", got "${agentPath}".`);
|
|
909
|
+
return {
|
|
910
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
911
|
+
projectId,
|
|
912
|
+
agentPath
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function readFlag(argv, flagName) {
|
|
916
|
+
const index = argv.indexOf(flagName);
|
|
917
|
+
if (index === -1) return void 0;
|
|
918
|
+
const value = argv[index + 1];
|
|
919
|
+
if (value == null || value.startsWith("--")) throw new Error(`${flagName} requires a value.`);
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
922
|
+
const renderer = await createCliRenderer({
|
|
923
|
+
exitOnCtrlC: true,
|
|
924
|
+
targetFps: 30,
|
|
925
|
+
screenMode: "alternate-screen",
|
|
926
|
+
consoleMode: "disabled"
|
|
927
|
+
});
|
|
928
|
+
process.on("exit", () => connection.dispose());
|
|
929
|
+
createRoot(renderer).render(/* @__PURE__ */ jsx(AgentChatApp, {}));
|
|
930
|
+
//#endregion
|
|
931
|
+
export {};
|
|
932
|
+
|
|
933
|
+
//# sourceMappingURL=agent-chat-terminal.mjs.map
|