@cephalization/math 0.2.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.
- package/README.md +190 -0
- package/index.ts +155 -0
- package/package.json +50 -0
- package/src/agent.test.ts +179 -0
- package/src/agent.ts +275 -0
- package/src/commands/init.ts +65 -0
- package/src/commands/iterate.ts +92 -0
- package/src/commands/plan.ts +16 -0
- package/src/commands/prune.ts +63 -0
- package/src/commands/run.test.ts +27 -0
- package/src/commands/run.ts +16 -0
- package/src/commands/status.ts +55 -0
- package/src/constants.ts +1 -0
- package/src/loop.test.ts +537 -0
- package/src/loop.ts +325 -0
- package/src/plan.ts +263 -0
- package/src/prune.test.ts +174 -0
- package/src/prune.ts +146 -0
- package/src/tasks.ts +204 -0
- package/src/templates.ts +172 -0
- package/src/ui/app.test.ts +228 -0
- package/src/ui/buffer.test.ts +222 -0
- package/src/ui/buffer.ts +131 -0
- package/src/ui/server.test.ts +271 -0
- package/src/ui/server.ts +124 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import type { WebSocketMessage } from "./server";
|
|
3
|
+
import type { BufferLogEntry, BufferAgentOutput } from "./buffer";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for the React app module.
|
|
7
|
+
* Since the app is primarily UI code that mounts to the DOM,
|
|
8
|
+
* we test that the module exports correctly and the types align.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
describe("app.tsx", () => {
|
|
12
|
+
test("module exists and can be imported", async () => {
|
|
13
|
+
// The app module should exist at the expected path
|
|
14
|
+
const file = Bun.file("./src/ui/app.tsx");
|
|
15
|
+
const exists = await file.exists();
|
|
16
|
+
expect(exists).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("imports react and react-dom", async () => {
|
|
20
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
21
|
+
|
|
22
|
+
expect(content).toContain('from "react"');
|
|
23
|
+
expect(content).toContain('from "react-dom/client"');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("uses createRoot for React 18", async () => {
|
|
27
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
28
|
+
|
|
29
|
+
expect(content).toContain("createRoot");
|
|
30
|
+
expect(content).toContain('document.getElementById("root")');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("connects to WebSocket at /ws", async () => {
|
|
34
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
35
|
+
|
|
36
|
+
expect(content).toContain("WebSocket");
|
|
37
|
+
expect(content).toContain("/ws");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("renders Loop Status section", async () => {
|
|
41
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
42
|
+
|
|
43
|
+
expect(content).toContain("Loop Status");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("renders Agent Output section", async () => {
|
|
47
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
48
|
+
|
|
49
|
+
expect(content).toContain("Agent Output");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("handles WebSocket message types", async () => {
|
|
53
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
54
|
+
|
|
55
|
+
// Should handle all message types from server
|
|
56
|
+
expect(content).toContain('"connected"');
|
|
57
|
+
expect(content).toContain('"history"');
|
|
58
|
+
expect(content).toContain('"log"');
|
|
59
|
+
expect(content).toContain('"output"');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("stores logs and output in state", async () => {
|
|
63
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
64
|
+
|
|
65
|
+
// Should use useState for logs and output
|
|
66
|
+
expect(content).toContain("useState<BufferLogEntry[]>");
|
|
67
|
+
expect(content).toContain("useState<BufferAgentOutput[]>");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("shows connection status", async () => {
|
|
71
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
72
|
+
|
|
73
|
+
expect(content).toContain("Connected");
|
|
74
|
+
expect(content).toContain("Disconnected");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("stream-display features", () => {
|
|
79
|
+
test("defines category colors for all log types", async () => {
|
|
80
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
81
|
+
|
|
82
|
+
// Should define colors for all categories
|
|
83
|
+
expect(content).toContain("categoryColors");
|
|
84
|
+
expect(content).toContain("info:");
|
|
85
|
+
expect(content).toContain("success:");
|
|
86
|
+
expect(content).toContain("warning:");
|
|
87
|
+
expect(content).toContain("error:");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("uses correct terminal colors for categories", async () => {
|
|
91
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
92
|
+
|
|
93
|
+
// Blue for info
|
|
94
|
+
expect(content).toMatch(/info.*#60a5fa|#60a5fa.*info/i);
|
|
95
|
+
// Green for success
|
|
96
|
+
expect(content).toMatch(/success.*#4ade80|#4ade80.*success/i);
|
|
97
|
+
// Yellow for warning
|
|
98
|
+
expect(content).toMatch(/warning.*#facc15|#facc15.*warning/i);
|
|
99
|
+
// Red for error
|
|
100
|
+
expect(content).toMatch(/error.*#f87171|#f87171.*error/i);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("has refs for auto-scroll containers", async () => {
|
|
104
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
105
|
+
|
|
106
|
+
// Should use refs for both containers
|
|
107
|
+
expect(content).toContain("logContainerRef");
|
|
108
|
+
expect(content).toContain("outputContainerRef");
|
|
109
|
+
expect(content).toContain("useRef<HTMLDivElement>");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("implements auto-scroll on content changes", async () => {
|
|
113
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
114
|
+
|
|
115
|
+
// Should scroll to bottom on logs and output changes
|
|
116
|
+
expect(content).toContain("scrollTop");
|
|
117
|
+
expect(content).toContain("scrollHeight");
|
|
118
|
+
|
|
119
|
+
// Should have useEffect hooks with appropriate dependencies
|
|
120
|
+
// The pattern: useEffect that uses logContainerRef and depends on [logs]
|
|
121
|
+
expect(content).toContain("logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight");
|
|
122
|
+
expect(content).toContain("}, [logs])");
|
|
123
|
+
|
|
124
|
+
// The pattern: useEffect that uses outputContainerRef and depends on [output]
|
|
125
|
+
expect(content).toContain("outputContainerRef.current.scrollTop = outputContainerRef.current.scrollHeight");
|
|
126
|
+
expect(content).toContain("}, [output])");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("renders preformatted monospace agent output", async () => {
|
|
130
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
131
|
+
|
|
132
|
+
// Should use <pre> tag for agent output
|
|
133
|
+
expect(content).toContain("<pre");
|
|
134
|
+
// Should have monospace font for output
|
|
135
|
+
expect(content).toContain("fontFamily: \"monospace\"");
|
|
136
|
+
// Should preserve whitespace
|
|
137
|
+
expect(content).toContain("pre-wrap");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("has visual connection status indicator", async () => {
|
|
141
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
142
|
+
|
|
143
|
+
// Should have a status dot element
|
|
144
|
+
expect(content).toContain("statusDot");
|
|
145
|
+
// Should use different colors based on connection
|
|
146
|
+
expect(content).toContain("backgroundColor:");
|
|
147
|
+
// Should have a container for status
|
|
148
|
+
expect(content).toContain("statusContainer");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("applies category color to timestamp and category label", async () => {
|
|
152
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
153
|
+
|
|
154
|
+
// Should apply color to timestamp
|
|
155
|
+
expect(content).toContain("getCategoryColor(log.category)");
|
|
156
|
+
// Should be used in style objects
|
|
157
|
+
expect(content).toMatch(/color:\s*getCategoryColor/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("imports LogCategory type", async () => {
|
|
161
|
+
const content = await Bun.file("./src/ui/app.tsx").text();
|
|
162
|
+
|
|
163
|
+
// Should import LogCategory from agent
|
|
164
|
+
expect(content).toContain('import type { LogCategory } from "../agent"');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("WebSocketMessage type compatibility", () => {
|
|
169
|
+
test("history message has correct structure", () => {
|
|
170
|
+
const logs: BufferLogEntry[] = [
|
|
171
|
+
{ timestamp: new Date(), category: "info", message: "test" },
|
|
172
|
+
];
|
|
173
|
+
const output: BufferAgentOutput[] = [
|
|
174
|
+
{ timestamp: new Date(), text: "output" },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const message: WebSocketMessage = {
|
|
178
|
+
type: "history",
|
|
179
|
+
logs,
|
|
180
|
+
output,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
expect(message.type).toBe("history");
|
|
184
|
+
expect(message.logs).toHaveLength(1);
|
|
185
|
+
expect(message.output).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("log message has correct structure", () => {
|
|
189
|
+
const entry: BufferLogEntry = {
|
|
190
|
+
timestamp: new Date(),
|
|
191
|
+
category: "error",
|
|
192
|
+
message: "test error",
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const message: WebSocketMessage = {
|
|
196
|
+
type: "log",
|
|
197
|
+
entry,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
expect(message.type).toBe("log");
|
|
201
|
+
expect(message.entry.category).toBe("error");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("output message has correct structure", () => {
|
|
205
|
+
const entry: BufferAgentOutput = {
|
|
206
|
+
timestamp: new Date(),
|
|
207
|
+
text: "agent text",
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const message: WebSocketMessage = {
|
|
211
|
+
type: "output",
|
|
212
|
+
entry,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
expect(message.type).toBe("output");
|
|
216
|
+
expect(message.entry.text).toBe("agent text");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("connected message has correct structure", () => {
|
|
220
|
+
const message: WebSocketMessage = {
|
|
221
|
+
type: "connected",
|
|
222
|
+
id: "test-uuid",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
expect(message.type).toBe("connected");
|
|
226
|
+
expect(message.id).toBe("test-uuid");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
OutputBuffer,
|
|
4
|
+
createOutputBuffer,
|
|
5
|
+
type BufferLogEntry,
|
|
6
|
+
type BufferAgentOutput,
|
|
7
|
+
} from "./buffer";
|
|
8
|
+
|
|
9
|
+
describe("OutputBuffer", () => {
|
|
10
|
+
describe("log entries", () => {
|
|
11
|
+
test("appendLog adds entry with timestamp", () => {
|
|
12
|
+
const buffer = createOutputBuffer();
|
|
13
|
+
const before = new Date();
|
|
14
|
+
|
|
15
|
+
const entry = buffer.appendLog("info", "Test message");
|
|
16
|
+
|
|
17
|
+
const after = new Date();
|
|
18
|
+
expect(entry.category).toBe("info");
|
|
19
|
+
expect(entry.message).toBe("Test message");
|
|
20
|
+
expect(entry.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
21
|
+
expect(entry.timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("getLogs returns all appended logs", () => {
|
|
25
|
+
const buffer = createOutputBuffer();
|
|
26
|
+
|
|
27
|
+
buffer.appendLog("info", "Message 1");
|
|
28
|
+
buffer.appendLog("success", "Message 2");
|
|
29
|
+
buffer.appendLog("warning", "Message 3");
|
|
30
|
+
buffer.appendLog("error", "Message 4");
|
|
31
|
+
|
|
32
|
+
const logs = buffer.getLogs();
|
|
33
|
+
|
|
34
|
+
expect(logs).toHaveLength(4);
|
|
35
|
+
expect(logs[0]!.category).toBe("info");
|
|
36
|
+
expect(logs[0]!.message).toBe("Message 1");
|
|
37
|
+
expect(logs[1]!.category).toBe("success");
|
|
38
|
+
expect(logs[1]!.message).toBe("Message 2");
|
|
39
|
+
expect(logs[2]!.category).toBe("warning");
|
|
40
|
+
expect(logs[2]!.message).toBe("Message 3");
|
|
41
|
+
expect(logs[3]!.category).toBe("error");
|
|
42
|
+
expect(logs[3]!.message).toBe("Message 4");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("getLogs returns a copy, not the internal array", () => {
|
|
46
|
+
const buffer = createOutputBuffer();
|
|
47
|
+
buffer.appendLog("info", "Test");
|
|
48
|
+
|
|
49
|
+
const logs1 = buffer.getLogs();
|
|
50
|
+
const logs2 = buffer.getLogs();
|
|
51
|
+
|
|
52
|
+
expect(logs1).not.toBe(logs2);
|
|
53
|
+
expect(logs1).toEqual(logs2);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("agent output", () => {
|
|
58
|
+
test("appendOutput adds entry with timestamp", () => {
|
|
59
|
+
const buffer = createOutputBuffer();
|
|
60
|
+
const before = new Date();
|
|
61
|
+
|
|
62
|
+
const output = buffer.appendOutput("Test output");
|
|
63
|
+
|
|
64
|
+
const after = new Date();
|
|
65
|
+
expect(output.text).toBe("Test output");
|
|
66
|
+
expect(output.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
67
|
+
expect(output.timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("getOutput returns all appended output", () => {
|
|
71
|
+
const buffer = createOutputBuffer();
|
|
72
|
+
|
|
73
|
+
buffer.appendOutput("Output 1\n");
|
|
74
|
+
buffer.appendOutput("Output 2\n");
|
|
75
|
+
buffer.appendOutput("Output 3\n");
|
|
76
|
+
|
|
77
|
+
const output = buffer.getOutput();
|
|
78
|
+
|
|
79
|
+
expect(output).toHaveLength(3);
|
|
80
|
+
expect(output[0]!.text).toBe("Output 1\n");
|
|
81
|
+
expect(output[1]!.text).toBe("Output 2\n");
|
|
82
|
+
expect(output[2]!.text).toBe("Output 3\n");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("getOutput returns a copy, not the internal array", () => {
|
|
86
|
+
const buffer = createOutputBuffer();
|
|
87
|
+
buffer.appendOutput("Test");
|
|
88
|
+
|
|
89
|
+
const output1 = buffer.getOutput();
|
|
90
|
+
const output2 = buffer.getOutput();
|
|
91
|
+
|
|
92
|
+
expect(output1).not.toBe(output2);
|
|
93
|
+
expect(output1).toEqual(output2);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("subscriptions", () => {
|
|
98
|
+
test("subscribeLogs notifies on new log entries", () => {
|
|
99
|
+
const buffer = createOutputBuffer();
|
|
100
|
+
const received: BufferLogEntry[] = [];
|
|
101
|
+
|
|
102
|
+
buffer.subscribeLogs((entry) => received.push(entry));
|
|
103
|
+
buffer.appendLog("info", "Test 1");
|
|
104
|
+
buffer.appendLog("warning", "Test 2");
|
|
105
|
+
|
|
106
|
+
expect(received).toHaveLength(2);
|
|
107
|
+
expect(received[0]!.message).toBe("Test 1");
|
|
108
|
+
expect(received[1]!.message).toBe("Test 2");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("subscribeOutput notifies on new output", () => {
|
|
112
|
+
const buffer = createOutputBuffer();
|
|
113
|
+
const received: BufferAgentOutput[] = [];
|
|
114
|
+
|
|
115
|
+
buffer.subscribeOutput((output) => received.push(output));
|
|
116
|
+
buffer.appendOutput("Output 1");
|
|
117
|
+
buffer.appendOutput("Output 2");
|
|
118
|
+
|
|
119
|
+
expect(received).toHaveLength(2);
|
|
120
|
+
expect(received[0]!.text).toBe("Output 1");
|
|
121
|
+
expect(received[1]!.text).toBe("Output 2");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("unsubscribe from logs stops notifications", () => {
|
|
125
|
+
const buffer = createOutputBuffer();
|
|
126
|
+
const received: BufferLogEntry[] = [];
|
|
127
|
+
|
|
128
|
+
const unsubscribe = buffer.subscribeLogs((entry) => received.push(entry));
|
|
129
|
+
buffer.appendLog("info", "Before");
|
|
130
|
+
|
|
131
|
+
unsubscribe();
|
|
132
|
+
buffer.appendLog("info", "After");
|
|
133
|
+
|
|
134
|
+
expect(received).toHaveLength(1);
|
|
135
|
+
expect(received[0]!.message).toBe("Before");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("unsubscribe from output stops notifications", () => {
|
|
139
|
+
const buffer = createOutputBuffer();
|
|
140
|
+
const received: BufferAgentOutput[] = [];
|
|
141
|
+
|
|
142
|
+
const unsubscribe = buffer.subscribeOutput((output) => received.push(output));
|
|
143
|
+
buffer.appendOutput("Before");
|
|
144
|
+
|
|
145
|
+
unsubscribe();
|
|
146
|
+
buffer.appendOutput("After");
|
|
147
|
+
|
|
148
|
+
expect(received).toHaveLength(1);
|
|
149
|
+
expect(received[0]!.text).toBe("Before");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("multiple log subscribers receive notifications", () => {
|
|
153
|
+
const buffer = createOutputBuffer();
|
|
154
|
+
const received1: string[] = [];
|
|
155
|
+
const received2: string[] = [];
|
|
156
|
+
|
|
157
|
+
buffer.subscribeLogs((entry) => received1.push(entry.message));
|
|
158
|
+
buffer.subscribeLogs((entry) => received2.push(entry.message));
|
|
159
|
+
buffer.appendLog("info", "Test");
|
|
160
|
+
|
|
161
|
+
expect(received1).toEqual(["Test"]);
|
|
162
|
+
expect(received2).toEqual(["Test"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("multiple output subscribers receive notifications", () => {
|
|
166
|
+
const buffer = createOutputBuffer();
|
|
167
|
+
const received1: string[] = [];
|
|
168
|
+
const received2: string[] = [];
|
|
169
|
+
|
|
170
|
+
buffer.subscribeOutput((output) => received1.push(output.text));
|
|
171
|
+
buffer.subscribeOutput((output) => received2.push(output.text));
|
|
172
|
+
buffer.appendOutput("Test");
|
|
173
|
+
|
|
174
|
+
expect(received1).toEqual(["Test"]);
|
|
175
|
+
expect(received2).toEqual(["Test"]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("clear", () => {
|
|
180
|
+
test("clear removes all logs and output", () => {
|
|
181
|
+
const buffer = createOutputBuffer();
|
|
182
|
+
|
|
183
|
+
buffer.appendLog("info", "Log 1");
|
|
184
|
+
buffer.appendLog("success", "Log 2");
|
|
185
|
+
buffer.appendOutput("Output 1");
|
|
186
|
+
buffer.appendOutput("Output 2");
|
|
187
|
+
|
|
188
|
+
expect(buffer.getLogs()).toHaveLength(2);
|
|
189
|
+
expect(buffer.getOutput()).toHaveLength(2);
|
|
190
|
+
|
|
191
|
+
buffer.clear();
|
|
192
|
+
|
|
193
|
+
expect(buffer.getLogs()).toHaveLength(0);
|
|
194
|
+
expect(buffer.getOutput()).toHaveLength(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("subscriptions still work after clear", () => {
|
|
198
|
+
const buffer = createOutputBuffer();
|
|
199
|
+
const receivedLogs: string[] = [];
|
|
200
|
+
const receivedOutput: string[] = [];
|
|
201
|
+
|
|
202
|
+
buffer.subscribeLogs((entry) => receivedLogs.push(entry.message));
|
|
203
|
+
buffer.subscribeOutput((output) => receivedOutput.push(output.text));
|
|
204
|
+
|
|
205
|
+
buffer.appendLog("info", "Before");
|
|
206
|
+
buffer.appendOutput("Before");
|
|
207
|
+
buffer.clear();
|
|
208
|
+
buffer.appendLog("info", "After");
|
|
209
|
+
buffer.appendOutput("After");
|
|
210
|
+
|
|
211
|
+
expect(receivedLogs).toEqual(["Before", "After"]);
|
|
212
|
+
expect(receivedOutput).toEqual(["Before", "After"]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("createOutputBuffer", () => {
|
|
218
|
+
test("creates a new OutputBuffer instance", () => {
|
|
219
|
+
const buffer = createOutputBuffer();
|
|
220
|
+
expect(buffer).toBeInstanceOf(OutputBuffer);
|
|
221
|
+
});
|
|
222
|
+
});
|
package/src/ui/buffer.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output buffer module for storing loop logs and agent output.
|
|
3
|
+
* Provides append, get history, and subscribe functionality.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LogCategory } from "../agent";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A log entry in the buffer with timestamp and category.
|
|
10
|
+
*/
|
|
11
|
+
export interface BufferLogEntry {
|
|
12
|
+
timestamp: Date;
|
|
13
|
+
category: LogCategory;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An agent output entry in the buffer.
|
|
19
|
+
*/
|
|
20
|
+
export interface BufferAgentOutput {
|
|
21
|
+
timestamp: Date;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback for log entry subscriptions.
|
|
27
|
+
*/
|
|
28
|
+
export type LogSubscriber = (entry: BufferLogEntry) => void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Callback for agent output subscriptions.
|
|
32
|
+
*/
|
|
33
|
+
export type OutputSubscriber = (output: BufferAgentOutput) => void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Output buffer that stores loop logs and agent output separately.
|
|
37
|
+
*/
|
|
38
|
+
export class OutputBuffer {
|
|
39
|
+
private logs: BufferLogEntry[] = [];
|
|
40
|
+
private agentOutput: BufferAgentOutput[] = [];
|
|
41
|
+
private logSubscribers: Set<LogSubscriber> = new Set();
|
|
42
|
+
private outputSubscribers: Set<OutputSubscriber> = new Set();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Append a log entry to the buffer.
|
|
46
|
+
*/
|
|
47
|
+
appendLog(category: LogCategory, message: string): BufferLogEntry {
|
|
48
|
+
const entry: BufferLogEntry = {
|
|
49
|
+
timestamp: new Date(),
|
|
50
|
+
category,
|
|
51
|
+
message,
|
|
52
|
+
};
|
|
53
|
+
this.logs.push(entry);
|
|
54
|
+
|
|
55
|
+
// Notify subscribers
|
|
56
|
+
for (const subscriber of this.logSubscribers) {
|
|
57
|
+
subscriber(entry);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Append agent output to the buffer.
|
|
65
|
+
*/
|
|
66
|
+
appendOutput(text: string): BufferAgentOutput {
|
|
67
|
+
const output: BufferAgentOutput = {
|
|
68
|
+
timestamp: new Date(),
|
|
69
|
+
text,
|
|
70
|
+
};
|
|
71
|
+
this.agentOutput.push(output);
|
|
72
|
+
|
|
73
|
+
// Notify subscribers
|
|
74
|
+
for (const subscriber of this.outputSubscribers) {
|
|
75
|
+
subscriber(output);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return output;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all log entries.
|
|
83
|
+
*/
|
|
84
|
+
getLogs(): BufferLogEntry[] {
|
|
85
|
+
return [...this.logs];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get all agent output entries.
|
|
90
|
+
*/
|
|
91
|
+
getOutput(): BufferAgentOutput[] {
|
|
92
|
+
return [...this.agentOutput];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Subscribe to new log entries.
|
|
97
|
+
* Returns an unsubscribe function.
|
|
98
|
+
*/
|
|
99
|
+
subscribeLogs(callback: LogSubscriber): () => void {
|
|
100
|
+
this.logSubscribers.add(callback);
|
|
101
|
+
return () => {
|
|
102
|
+
this.logSubscribers.delete(callback);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Subscribe to new agent output.
|
|
108
|
+
* Returns an unsubscribe function.
|
|
109
|
+
*/
|
|
110
|
+
subscribeOutput(callback: OutputSubscriber): () => void {
|
|
111
|
+
this.outputSubscribers.add(callback);
|
|
112
|
+
return () => {
|
|
113
|
+
this.outputSubscribers.delete(callback);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clear all logs and output.
|
|
119
|
+
*/
|
|
120
|
+
clear(): void {
|
|
121
|
+
this.logs = [];
|
|
122
|
+
this.agentOutput = [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a new output buffer instance.
|
|
128
|
+
*/
|
|
129
|
+
export function createOutputBuffer(): OutputBuffer {
|
|
130
|
+
return new OutputBuffer();
|
|
131
|
+
}
|