godspeed-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,377 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput, useApp } from "ink";
3
+ import { runAgent, type AgentStep } from "../lib/agent.js";
4
+ import { loadHistory, saveHistory } from "../lib/history.js";
5
+ import { getWorkspace } from "../tools/workspace.js";
6
+ import { getToolLabel } from "../tools/labels.js";
7
+ import {
8
+ loadAgentMemory,
9
+ saveAgentMemory,
10
+ updateAgentMemory,
11
+ } from "../lib/agentMemory.js";
12
+ import { streamLLMToCallback } from "../lib/llm.js";
13
+
14
+ type UiMessage = {
15
+ role: "user" | "ai";
16
+ text: string;
17
+ };
18
+
19
+ type AppProps = {
20
+ sessionName: string;
21
+ direct?: boolean;
22
+ };
23
+
24
+ export function App({ sessionName, direct = false }: AppProps) {
25
+ const { exit } = useApp();
26
+
27
+ const [input, setInput] = useState("");
28
+ const [agentMemory, setAgentMemory] = useState(loadAgentMemory(sessionName));
29
+
30
+ const [uiMessages, setUiMessages] = useState<UiMessage[]>(
31
+ loadHistory(sessionName).map((message) => ({
32
+ role: message.role === "user" ? "user" : "ai",
33
+ text: message.text,
34
+ })),
35
+ );
36
+
37
+ const [steps, setSteps] = useState<AgentStep[]>([]);
38
+ const [loading, setLoading] = useState(false);
39
+
40
+ const [pendingApproval, setPendingApproval] = useState<{
41
+ label: string;
42
+ resolve: (approved: boolean) => void;
43
+ } | null>(null);
44
+
45
+ const visibleSteps = steps.slice(-10);
46
+ const visibleMessages = uiMessages.slice(-8);
47
+
48
+ function persistHistory(messages: UiMessage[]) {
49
+ saveHistory(
50
+ sessionName,
51
+ messages.map((message) => ({
52
+ role: message.role === "user" ? "user" : "model",
53
+ text: message.text,
54
+ })),
55
+ );
56
+ }
57
+
58
+ function upsertStep(step: AgentStep) {
59
+ setSteps((prev) => {
60
+ const existingIndex = prev.findIndex(
61
+ (item) => item.step === step.step && item.action === step.action,
62
+ );
63
+
64
+ if (existingIndex === -1) {
65
+ return [...prev, step];
66
+ }
67
+
68
+ const next = [...prev];
69
+ next[existingIndex] = step;
70
+ return next;
71
+ });
72
+ }
73
+
74
+ async function streamAiMessage(text: string) {
75
+ let current = "";
76
+
77
+ setUiMessages((prev) => [
78
+ ...prev,
79
+ {
80
+ role: "ai",
81
+ text: "",
82
+ },
83
+ ]);
84
+
85
+ for (const char of text) {
86
+ current += char;
87
+
88
+ setUiMessages((prev) => {
89
+ const next = [...prev];
90
+
91
+ if (next.length === 0) {
92
+ return [
93
+ {
94
+ role: "ai",
95
+ text: current,
96
+ },
97
+ ];
98
+ }
99
+
100
+ next[next.length - 1] = {
101
+ role: "ai",
102
+ text: current,
103
+ };
104
+
105
+ return next;
106
+ });
107
+
108
+ await new Promise((resolve) => setTimeout(resolve, 5));
109
+ }
110
+ }
111
+
112
+ async function sendMessage() {
113
+ const prompt = input.trim();
114
+
115
+ if (!prompt || loading) return;
116
+
117
+ setInput("");
118
+ setLoading(true);
119
+ setSteps([]);
120
+
121
+ const userMessage: UiMessage = {
122
+ role: "user",
123
+ text: prompt,
124
+ };
125
+
126
+ setUiMessages((prev) => [...prev, userMessage]);
127
+
128
+ const isDirectChat = direct || prompt.startsWith("/chat ");
129
+
130
+ const directPrompt = prompt.startsWith("/chat ")
131
+ ? prompt.replace(/^\/chat\s+/, "")
132
+ : prompt;
133
+
134
+ if (isDirectChat) {
135
+ let fullAnswer = "";
136
+
137
+ setUiMessages((prev) => [
138
+ ...prev,
139
+ {
140
+ role: "ai",
141
+ text: "",
142
+ },
143
+ ]);
144
+
145
+ try {
146
+ await streamLLMToCallback(directPrompt, (token) => {
147
+ fullAnswer += token;
148
+
149
+ setUiMessages((prev) => {
150
+ const next = [...prev];
151
+
152
+ next[next.length - 1] = {
153
+ role: "ai",
154
+ text: fullAnswer,
155
+ };
156
+
157
+ return next;
158
+ });
159
+ });
160
+
161
+ setUiMessages((prev) => {
162
+ persistHistory(prev);
163
+ return prev;
164
+ });
165
+ } finally {
166
+ setLoading(false);
167
+ }
168
+
169
+ return;
170
+ }
171
+
172
+ try {
173
+ const answer = await runAgent(prompt, {
174
+ memory: agentMemory,
175
+ showSpinner: false,
176
+
177
+ async confirmTool(action) {
178
+ const needsApproval =
179
+ action.action === "WRITE_FILE" ||
180
+ action.action === "EDIT_FILE" ||
181
+ action.action === "DELETE_FILE" ||
182
+ action.action === "CREATE_DIRECTORY" ||
183
+ action.action === "MOVE_FILE" ||
184
+ action.action === "COPY_FILE" ||
185
+ action.action === "RUN_COMMAND";
186
+
187
+ if (!needsApproval) {
188
+ return true;
189
+ }
190
+
191
+ const label = getToolLabel(action);
192
+
193
+ return new Promise<boolean>((resolve) => {
194
+ setPendingApproval({
195
+ label,
196
+ resolve,
197
+ });
198
+ });
199
+ },
200
+
201
+ onStep(step) {
202
+ upsertStep(step);
203
+ },
204
+ });
205
+
206
+ const nextMemory = await updateAgentMemory({
207
+ previousMemory: agentMemory,
208
+ userPrompt: prompt,
209
+ finalAnswer: answer,
210
+ });
211
+
212
+ setAgentMemory(nextMemory);
213
+ saveAgentMemory(sessionName, nextMemory);
214
+
215
+ await streamAiMessage(answer || "Done.");
216
+
217
+ setUiMessages((prev) => {
218
+ persistHistory(prev);
219
+ return prev;
220
+ });
221
+ } catch (err) {
222
+ const errorMessage: UiMessage = {
223
+ role: "ai",
224
+ text: err instanceof Error ? err.message : String(err),
225
+ };
226
+
227
+ setUiMessages((prev) => {
228
+ const next = [...prev, errorMessage];
229
+ persistHistory(next);
230
+ return next;
231
+ });
232
+ } finally {
233
+ setLoading(false);
234
+ }
235
+ }
236
+
237
+ useInput((char, key) => {
238
+ if (pendingApproval) {
239
+ if (char?.toLowerCase() === "y") {
240
+ pendingApproval.resolve(true);
241
+ setPendingApproval(null);
242
+ }
243
+
244
+ if (char?.toLowerCase() === "n") {
245
+ pendingApproval.resolve(false);
246
+ setPendingApproval(null);
247
+ }
248
+
249
+ return;
250
+ }
251
+
252
+ if (key.escape || (key.ctrl && char === "c")) {
253
+ exit();
254
+ return;
255
+ }
256
+
257
+ if (loading) return;
258
+
259
+ if (key.backspace || key.delete) {
260
+ setInput((prev) => prev.slice(0, -1));
261
+ return;
262
+ }
263
+
264
+ if (key.return) {
265
+ sendMessage();
266
+ return;
267
+ }
268
+
269
+ if (char) {
270
+ setInput((prev) => prev + char);
271
+ }
272
+ });
273
+
274
+ return (
275
+ <Box flexDirection="column">
276
+ <Box
277
+ flexDirection="column"
278
+ borderStyle="round"
279
+ borderColor="green"
280
+ paddingX={1}
281
+ >
282
+ <Text color="green">TUI Starter</Text>
283
+ <Text color="gray">Session: {sessionName}</Text>
284
+ <Text color="gray">Workspace: {getWorkspace()}</Text>
285
+ <Text color="gray">Mode: {direct ? "Direct Chat" : "Agent"}</Text>
286
+ <Text color="gray">Press ESC or Ctrl+C to exit</Text>
287
+ </Box>
288
+
289
+ <Box marginTop={1} gap={2}>
290
+ <Box
291
+ width="40%"
292
+ flexDirection="column"
293
+ borderStyle="round"
294
+ borderColor="magenta"
295
+ paddingX={1}
296
+ >
297
+ <Text color="magenta">Steps</Text>
298
+
299
+ {visibleSteps.length === 0 && <Text color="gray">No steps yet</Text>}
300
+
301
+ {visibleSteps.map((step, index) => {
302
+ const icon =
303
+ step.status === "completed"
304
+ ? "✅"
305
+ : step.status === "failed"
306
+ ? "❌"
307
+ : "⏳";
308
+
309
+ const detail =
310
+ step.error ??
311
+ step.path ??
312
+ step.command ??
313
+ step.pattern ??
314
+ step.query ??
315
+ (step.from && step.to ? `${step.from} -> ${step.to}` : "");
316
+
317
+ const pointer = index === visibleSteps.length - 1 ? ">" : " ";
318
+
319
+ return (
320
+ <Text key={`${step.step}-${step.action}`}>
321
+ {pointer} {icon} {step.action} {detail}
322
+ </Text>
323
+ );
324
+ })}
325
+ </Box>
326
+
327
+ <Box
328
+ width="60%"
329
+ flexDirection="column"
330
+ borderStyle="round"
331
+ borderColor="cyan"
332
+ paddingX={1}
333
+ >
334
+ <Text color="cyan">Chat</Text>
335
+
336
+ {visibleMessages.length === 0 && (
337
+ <Text color="gray">No messages yet</Text>
338
+ )}
339
+
340
+ {visibleMessages.map((message, index) => (
341
+ <Box key={index} flexDirection="column" marginBottom={1}>
342
+ <Text color={message.role === "user" ? "cyan" : "yellow"}>
343
+ {message.role === "user" ? "You>" : "AI>"}
344
+ </Text>
345
+
346
+ <Text>{message.text}</Text>
347
+ </Box>
348
+ ))}
349
+ </Box>
350
+ </Box>
351
+
352
+ {pendingApproval && (
353
+ <Box
354
+ flexDirection="column"
355
+ borderStyle="round"
356
+ borderColor="yellow"
357
+ marginTop={1}
358
+ paddingX={1}
359
+ >
360
+ <Text color="yellow">Tool Approval Required</Text>
361
+ <Text>{pendingApproval.label}</Text>
362
+ <Text color="gray">Press Y to approve, N to deny</Text>
363
+ </Box>
364
+ )}
365
+
366
+ {loading && <Text color="gray">Thinking...</Text>}
367
+
368
+ <Box marginTop={1} borderStyle="round" borderColor="blue" paddingX={1}>
369
+ <Text color="cyan">You&gt; </Text>
370
+ <Text>
371
+ {input}
372
+ <Text color="green">|</Text>
373
+ </Text>
374
+ </Box>
375
+ </Box>
376
+ );
377
+ }