@supatest/cli 0.0.4 → 0.0.5
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/dist/commands/login.js +392 -0
- package/dist/commands/setup.js +234 -0
- package/dist/config.js +29 -0
- package/dist/core/agent.js +259 -0
- package/dist/index.js +154 -6586
- package/dist/modes/headless.js +117 -0
- package/dist/modes/interactive.js +418 -0
- package/dist/presenters/composite.js +32 -0
- package/dist/presenters/console.js +163 -0
- package/dist/presenters/react.js +217 -0
- package/dist/presenters/types.js +1 -0
- package/dist/presenters/web.js +78 -0
- package/dist/prompts/builder.js +181 -0
- package/dist/prompts/fixer.js +148 -0
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/planner.js +70 -0
- package/dist/services/api-client.js +244 -0
- package/dist/services/event-streamer.js +130 -0
- package/dist/types.js +1 -0
- package/dist/ui/App.js +322 -0
- package/dist/ui/components/AuthBanner.js +24 -0
- package/dist/ui/components/AuthDialog.js +32 -0
- package/dist/ui/components/Banner.js +12 -0
- package/dist/ui/components/ExpandableSection.js +17 -0
- package/dist/ui/components/Header.js +51 -0
- package/dist/ui/components/HelpMenu.js +89 -0
- package/dist/ui/components/InputPrompt.js +286 -0
- package/dist/ui/components/MessageList.js +42 -0
- package/dist/ui/components/QueuedMessageDisplay.js +31 -0
- package/dist/ui/components/Scrollable.js +103 -0
- package/dist/ui/components/SessionSelector.js +196 -0
- package/dist/ui/components/StatusBar.js +34 -0
- package/dist/ui/components/messages/AssistantMessage.js +20 -0
- package/dist/ui/components/messages/ErrorMessage.js +26 -0
- package/dist/ui/components/messages/LoadingMessage.js +28 -0
- package/dist/ui/components/messages/ThinkingMessage.js +17 -0
- package/dist/ui/components/messages/TodoMessage.js +44 -0
- package/dist/ui/components/messages/ToolMessage.js +218 -0
- package/dist/ui/components/messages/UserMessage.js +14 -0
- package/dist/ui/contexts/KeypressContext.js +527 -0
- package/dist/ui/contexts/MouseContext.js +98 -0
- package/dist/ui/contexts/SessionContext.js +129 -0
- package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
- package/dist/ui/hooks/useBatchedScroll.js +22 -0
- package/dist/ui/hooks/useBracketedPaste.js +31 -0
- package/dist/ui/hooks/useFocus.js +50 -0
- package/dist/ui/hooks/useKeypress.js +26 -0
- package/dist/ui/hooks/useModeToggle.js +25 -0
- package/dist/ui/types/auth.js +13 -0
- package/dist/ui/utils/file-completion.js +56 -0
- package/dist/ui/utils/input.js +50 -0
- package/dist/ui/utils/markdown.js +376 -0
- package/dist/ui/utils/mouse.js +189 -0
- package/dist/ui/utils/theme.js +59 -0
- package/dist/utils/banner.js +9 -0
- package/dist/utils/encryption.js +71 -0
- package/dist/utils/events.js +36 -0
- package/dist/utils/keychain-storage.js +120 -0
- package/dist/utils/logger.js +209 -0
- package/dist/utils/node-version.js +89 -0
- package/dist/utils/plan-file.js +75 -0
- package/dist/utils/project-instructions.js +23 -0
- package/dist/utils/rich-logger.js +208 -0
- package/dist/utils/stdin.js +25 -0
- package/dist/utils/stdio.js +80 -0
- package/dist/utils/summary.js +94 -0
- package/dist/utils/token-storage.js +242 -0
- package/dist/version.js +6 -0
- package/package.json +3 -4
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { getToolDisplayName } from "shared";
|
|
4
|
+
import { logger } from "../utils/logger";
|
|
5
|
+
import { generateSummary } from "../utils/summary";
|
|
6
|
+
// Fun spinner messages that rotate randomly
|
|
7
|
+
const SPINNER_MESSAGES = [
|
|
8
|
+
"Brainstorming...",
|
|
9
|
+
"Brewing coffee...",
|
|
10
|
+
"Sipping espresso...",
|
|
11
|
+
"Testing theories...",
|
|
12
|
+
"Making magic...",
|
|
13
|
+
"Multiplying matrices...",
|
|
14
|
+
];
|
|
15
|
+
function getRandomSpinnerMessage() {
|
|
16
|
+
return SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)];
|
|
17
|
+
}
|
|
18
|
+
function createShimmerFrames(text) {
|
|
19
|
+
const frames = [];
|
|
20
|
+
const baseText = text;
|
|
21
|
+
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
22
|
+
for (let i = 0; i <= baseText.length; i++) {
|
|
23
|
+
const spinnerIcon = spinnerFrames[i % spinnerFrames.length];
|
|
24
|
+
const before = chalk.white(baseText.slice(0, i));
|
|
25
|
+
const current = baseText[i] || "";
|
|
26
|
+
const after = chalk.white(baseText.slice(i + 1));
|
|
27
|
+
const shimmerText = before + chalk.cyan.bold(current) + after;
|
|
28
|
+
frames.push(`${chalk.cyan(spinnerIcon)} ${shimmerText}`);
|
|
29
|
+
}
|
|
30
|
+
return frames;
|
|
31
|
+
}
|
|
32
|
+
export class ConsolePresenter {
|
|
33
|
+
spinner = null;
|
|
34
|
+
verbose;
|
|
35
|
+
stats;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.verbose = options.verbose;
|
|
38
|
+
this.stats = {
|
|
39
|
+
startTime: Date.now(),
|
|
40
|
+
iterations: 0,
|
|
41
|
+
filesModified: new Set(),
|
|
42
|
+
commandsRun: [],
|
|
43
|
+
errors: [],
|
|
44
|
+
};
|
|
45
|
+
logger.setVerbose(options.verbose);
|
|
46
|
+
}
|
|
47
|
+
onStart(config) {
|
|
48
|
+
logger.raw("");
|
|
49
|
+
logger.raw(chalk.white.bold("Task:") + " " + chalk.cyan(config.task));
|
|
50
|
+
if (config.logs) {
|
|
51
|
+
logger.info("Processing provided logs...");
|
|
52
|
+
}
|
|
53
|
+
logger.raw("");
|
|
54
|
+
this.startSpinner();
|
|
55
|
+
}
|
|
56
|
+
onLog(message) {
|
|
57
|
+
if (this.verbose) {
|
|
58
|
+
this.stopSpinner();
|
|
59
|
+
logger.debug(message);
|
|
60
|
+
this.startSpinner();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
onAssistantText(text) {
|
|
64
|
+
this.stopSpinner();
|
|
65
|
+
logger.raw(text);
|
|
66
|
+
this.startSpinner();
|
|
67
|
+
}
|
|
68
|
+
onThinking(text) {
|
|
69
|
+
if (this.verbose) {
|
|
70
|
+
this.stopSpinner();
|
|
71
|
+
logger.debug(`Thinking: ${text}`);
|
|
72
|
+
this.startSpinner();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
onToolUse(tool, input, _toolId) {
|
|
76
|
+
if (this.spinner) {
|
|
77
|
+
const displayName = getToolDisplayName(tool);
|
|
78
|
+
this.spinner.text = `Using ${displayName}...`;
|
|
79
|
+
}
|
|
80
|
+
this.stopSpinner();
|
|
81
|
+
if (tool === "Read") {
|
|
82
|
+
logger.toolRead(input?.file_path || "file");
|
|
83
|
+
}
|
|
84
|
+
else if (tool === "Write") {
|
|
85
|
+
const filePath = input?.file_path;
|
|
86
|
+
if (filePath) {
|
|
87
|
+
this.stats.filesModified.add(filePath);
|
|
88
|
+
logger.toolWrite(filePath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (tool === "Edit") {
|
|
92
|
+
const filePath = input?.file_path;
|
|
93
|
+
if (filePath) {
|
|
94
|
+
this.stats.filesModified.add(filePath);
|
|
95
|
+
logger.toolEdit(filePath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (tool === "Bash") {
|
|
99
|
+
const command = input?.command;
|
|
100
|
+
if (command) {
|
|
101
|
+
this.stats.commandsRun.push(command);
|
|
102
|
+
const shortCmd = command.length > 60 ? `${command.substring(0, 60)}...` : command;
|
|
103
|
+
logger.toolBash(shortCmd);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (tool === "Glob") {
|
|
107
|
+
logger.toolSearch("files", input?.pattern || "");
|
|
108
|
+
}
|
|
109
|
+
else if (tool === "Grep") {
|
|
110
|
+
logger.toolSearch("code", input?.pattern || "");
|
|
111
|
+
}
|
|
112
|
+
else if (tool === "Task") {
|
|
113
|
+
logger.toolAgent(input?.subagent_type || "task");
|
|
114
|
+
}
|
|
115
|
+
else if (tool === "TodoWrite") {
|
|
116
|
+
const todos = input?.todos;
|
|
117
|
+
if (Array.isArray(todos)) {
|
|
118
|
+
logger.todoUpdate(todos);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
logger.info("📝 Updated todos");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
logger.debug(`🔧 Using tool: ${tool}`);
|
|
126
|
+
}
|
|
127
|
+
logger.raw("");
|
|
128
|
+
this.startSpinner();
|
|
129
|
+
}
|
|
130
|
+
onTurnComplete(_content) {
|
|
131
|
+
// No-op for console - we handle output in individual callbacks
|
|
132
|
+
}
|
|
133
|
+
onError(error) {
|
|
134
|
+
this.stopSpinner();
|
|
135
|
+
logger.error(error);
|
|
136
|
+
this.stats.errors.push(error);
|
|
137
|
+
}
|
|
138
|
+
onComplete(result) {
|
|
139
|
+
this.stopSpinner();
|
|
140
|
+
this.stats.endTime = Date.now();
|
|
141
|
+
this.stats.iterations = result.iterations;
|
|
142
|
+
const summaryText = generateSummary(this.stats, result, this.verbose);
|
|
143
|
+
logger.raw(summaryText);
|
|
144
|
+
}
|
|
145
|
+
startSpinner() {
|
|
146
|
+
if (!this.spinner && !logger.isSilent()) {
|
|
147
|
+
const message = getRandomSpinnerMessage();
|
|
148
|
+
this.spinner = ora({
|
|
149
|
+
spinner: {
|
|
150
|
+
interval: 80,
|
|
151
|
+
frames: createShimmerFrames(message),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
this.spinner.start();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
stopSpinner() {
|
|
158
|
+
if (this.spinner) {
|
|
159
|
+
this.spinner.stop();
|
|
160
|
+
this.spinner = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { getToolDisplayName } from "shared";
|
|
2
|
+
/**
|
|
3
|
+
* Get human-readable description for tool call
|
|
4
|
+
*/
|
|
5
|
+
function getToolDescription(toolName, input) {
|
|
6
|
+
switch (toolName) {
|
|
7
|
+
case "Read":
|
|
8
|
+
return input?.file_path || "file";
|
|
9
|
+
case "Write":
|
|
10
|
+
return input?.file_path || "file";
|
|
11
|
+
case "Edit":
|
|
12
|
+
return input?.file_path || "file";
|
|
13
|
+
case "Bash": {
|
|
14
|
+
const cmd = input?.command || "";
|
|
15
|
+
return cmd.length > 60 ? `${cmd.substring(0, 60)}...` : cmd;
|
|
16
|
+
}
|
|
17
|
+
case "Glob":
|
|
18
|
+
return `pattern: "${input?.pattern || "files"}"`;
|
|
19
|
+
case "Grep": {
|
|
20
|
+
const pattern = input?.pattern || "code";
|
|
21
|
+
const path = input?.path;
|
|
22
|
+
return path ? `"${pattern}" (in ${path})` : `"${pattern}"`;
|
|
23
|
+
}
|
|
24
|
+
case "Task":
|
|
25
|
+
return input?.subagent_type || "task";
|
|
26
|
+
case "TodoWrite":
|
|
27
|
+
return "Updated todos";
|
|
28
|
+
case "BashOutput":
|
|
29
|
+
case "Command Output":
|
|
30
|
+
return input?.bash_id || "shell output";
|
|
31
|
+
default:
|
|
32
|
+
return toolName;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class ReactPresenter {
|
|
36
|
+
callbacks;
|
|
37
|
+
apiClient;
|
|
38
|
+
sessionId;
|
|
39
|
+
verbose;
|
|
40
|
+
// Track message state for combining consecutive blocks
|
|
41
|
+
hasAssistantMessage = false;
|
|
42
|
+
currentAssistantText = "";
|
|
43
|
+
hasThinkingMessage = false;
|
|
44
|
+
currentThinkingText = "";
|
|
45
|
+
constructor(callbacks, apiClient, sessionId, verbose = false) {
|
|
46
|
+
this.callbacks = callbacks;
|
|
47
|
+
this.apiClient = apiClient;
|
|
48
|
+
this.sessionId = sessionId;
|
|
49
|
+
this.verbose = verbose;
|
|
50
|
+
}
|
|
51
|
+
async onStart(config) {
|
|
52
|
+
// Send initial user message event to API
|
|
53
|
+
const userMessageEvent = {
|
|
54
|
+
type: "user_message",
|
|
55
|
+
content: [{ type: "text", text: config.task }],
|
|
56
|
+
};
|
|
57
|
+
await this.apiClient.streamEvent(this.sessionId, userMessageEvent);
|
|
58
|
+
}
|
|
59
|
+
onLog(message) {
|
|
60
|
+
if (this.verbose) {
|
|
61
|
+
console.error(message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async onAssistantText(text) {
|
|
65
|
+
if (!this.hasAssistantMessage) {
|
|
66
|
+
// First text block - create new message
|
|
67
|
+
this.callbacks.addMessage({
|
|
68
|
+
type: "assistant",
|
|
69
|
+
content: text,
|
|
70
|
+
isPending: false,
|
|
71
|
+
});
|
|
72
|
+
this.hasAssistantMessage = true;
|
|
73
|
+
this.currentAssistantText = text;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Append to current message
|
|
77
|
+
this.currentAssistantText += text;
|
|
78
|
+
this.callbacks.updateLastMessage({
|
|
79
|
+
content: this.currentAssistantText,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Stream to API
|
|
83
|
+
const textEvent = {
|
|
84
|
+
type: "assistant_text",
|
|
85
|
+
delta: text,
|
|
86
|
+
};
|
|
87
|
+
await this.apiClient.streamEvent(this.sessionId, textEvent);
|
|
88
|
+
}
|
|
89
|
+
async onThinking(text) {
|
|
90
|
+
if (!this.hasThinkingMessage) {
|
|
91
|
+
// Create new thinking message
|
|
92
|
+
this.callbacks.addMessage({
|
|
93
|
+
type: "thinking",
|
|
94
|
+
content: text,
|
|
95
|
+
isExpanded: false,
|
|
96
|
+
});
|
|
97
|
+
this.hasThinkingMessage = true;
|
|
98
|
+
this.currentThinkingText = text;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Append to existing thinking message
|
|
102
|
+
this.currentThinkingText += text;
|
|
103
|
+
this.callbacks.updateLastMessage({
|
|
104
|
+
content: this.currentThinkingText,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Stream to API
|
|
108
|
+
const thinkingEvent = {
|
|
109
|
+
type: "assistant_thinking",
|
|
110
|
+
delta: text,
|
|
111
|
+
};
|
|
112
|
+
await this.apiClient.streamEvent(this.sessionId, thinkingEvent);
|
|
113
|
+
}
|
|
114
|
+
async onToolUse(tool, input, toolId) {
|
|
115
|
+
// Add tool message to UI
|
|
116
|
+
const message = {
|
|
117
|
+
type: "tool",
|
|
118
|
+
content: getToolDescription(tool, input),
|
|
119
|
+
toolName: getToolDisplayName(tool),
|
|
120
|
+
toolInput: input,
|
|
121
|
+
toolResult: undefined,
|
|
122
|
+
isExpanded: false,
|
|
123
|
+
toolUseId: toolId,
|
|
124
|
+
};
|
|
125
|
+
this.callbacks.addMessage(message);
|
|
126
|
+
// Reset message state - next text/thinking creates new messages
|
|
127
|
+
this.hasAssistantMessage = false;
|
|
128
|
+
this.hasThinkingMessage = false;
|
|
129
|
+
this.currentAssistantText = "";
|
|
130
|
+
this.currentThinkingText = "";
|
|
131
|
+
// Update stats for file modifications
|
|
132
|
+
if (tool === "Write" || tool === "Edit") {
|
|
133
|
+
const filePath = input?.file_path;
|
|
134
|
+
if (filePath) {
|
|
135
|
+
this.callbacks.updateStats({
|
|
136
|
+
filesModified: new Set([filePath]),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (tool === "Bash") {
|
|
141
|
+
const command = input?.command;
|
|
142
|
+
if (command) {
|
|
143
|
+
this.callbacks.updateStats({
|
|
144
|
+
commandsRun: [command],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (tool === "TodoWrite") {
|
|
149
|
+
const todos = input?.todos;
|
|
150
|
+
if (todos && Array.isArray(todos)) {
|
|
151
|
+
this.callbacks.setTodos(todos);
|
|
152
|
+
this.callbacks.addMessage({
|
|
153
|
+
type: "todo",
|
|
154
|
+
content: "",
|
|
155
|
+
todos: todos,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Stream tool use event to API
|
|
160
|
+
const toolUseEvent = {
|
|
161
|
+
type: "tool_use",
|
|
162
|
+
id: toolId,
|
|
163
|
+
name: tool,
|
|
164
|
+
input: input || {},
|
|
165
|
+
};
|
|
166
|
+
await this.apiClient.streamEvent(this.sessionId, toolUseEvent);
|
|
167
|
+
}
|
|
168
|
+
async onToolResult(toolId, result) {
|
|
169
|
+
// Update the tool message with the result
|
|
170
|
+
this.callbacks.updateMessageByToolId(toolId, {
|
|
171
|
+
toolResult: result,
|
|
172
|
+
});
|
|
173
|
+
// Stream tool result to API
|
|
174
|
+
const toolResultEvent = {
|
|
175
|
+
type: "tool_result",
|
|
176
|
+
tool_use_id: toolId,
|
|
177
|
+
content: result,
|
|
178
|
+
};
|
|
179
|
+
await this.apiClient.streamEvent(this.sessionId, toolResultEvent);
|
|
180
|
+
}
|
|
181
|
+
async onTurnComplete(content) {
|
|
182
|
+
// Stream message complete to API
|
|
183
|
+
const messageCompleteEvent = {
|
|
184
|
+
type: "message_complete",
|
|
185
|
+
message: {
|
|
186
|
+
role: "assistant",
|
|
187
|
+
content: content,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
await this.apiClient.streamEvent(this.sessionId, messageCompleteEvent);
|
|
191
|
+
}
|
|
192
|
+
async onError(error) {
|
|
193
|
+
this.callbacks.addMessage({
|
|
194
|
+
type: "error",
|
|
195
|
+
content: error,
|
|
196
|
+
errorType: "error",
|
|
197
|
+
});
|
|
198
|
+
await this.apiClient.streamEvent(this.sessionId, {
|
|
199
|
+
type: "session_error",
|
|
200
|
+
error: error,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async onComplete(result) {
|
|
204
|
+
if (result.success) {
|
|
205
|
+
await this.apiClient.streamEvent(this.sessionId, {
|
|
206
|
+
type: "session_complete",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
await this.apiClient.streamEvent(this.sessionId, {
|
|
211
|
+
type: "session_error",
|
|
212
|
+
error: result.error || "Unknown error",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
this.callbacks.onComplete(result.success);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { EventStreamer } from "../services/event-streamer";
|
|
2
|
+
export class WebPresenter {
|
|
3
|
+
streamer;
|
|
4
|
+
constructor(apiClient, sessionId) {
|
|
5
|
+
this.streamer = new EventStreamer(apiClient, sessionId);
|
|
6
|
+
}
|
|
7
|
+
async onStart(config) {
|
|
8
|
+
// Send the initial user message to establish context
|
|
9
|
+
const event = {
|
|
10
|
+
type: "user_message",
|
|
11
|
+
content: [
|
|
12
|
+
{
|
|
13
|
+
type: "text",
|
|
14
|
+
text: config.task,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
await this.streamer.queueEvent(event);
|
|
19
|
+
}
|
|
20
|
+
onLog(_message) {
|
|
21
|
+
// Debug logs are not streamed to web
|
|
22
|
+
}
|
|
23
|
+
async onAssistantText(text) {
|
|
24
|
+
const event = {
|
|
25
|
+
type: "assistant_text",
|
|
26
|
+
delta: text,
|
|
27
|
+
};
|
|
28
|
+
await this.streamer.queueEvent(event);
|
|
29
|
+
}
|
|
30
|
+
async onThinking(text) {
|
|
31
|
+
const event = {
|
|
32
|
+
type: "assistant_thinking",
|
|
33
|
+
delta: text,
|
|
34
|
+
};
|
|
35
|
+
await this.streamer.queueEvent(event);
|
|
36
|
+
}
|
|
37
|
+
async onToolUse(tool, input, toolId) {
|
|
38
|
+
const event = {
|
|
39
|
+
type: "tool_use",
|
|
40
|
+
id: toolId,
|
|
41
|
+
name: tool,
|
|
42
|
+
input: input || {},
|
|
43
|
+
};
|
|
44
|
+
await this.streamer.queueEvent(event);
|
|
45
|
+
}
|
|
46
|
+
async onTurnComplete(content) {
|
|
47
|
+
// Flush pending events first
|
|
48
|
+
await this.streamer.flush();
|
|
49
|
+
const event = {
|
|
50
|
+
type: "message_complete",
|
|
51
|
+
message: {
|
|
52
|
+
role: "assistant",
|
|
53
|
+
content: content,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
await this.streamer.queueEvent(event);
|
|
57
|
+
}
|
|
58
|
+
async onError(error) {
|
|
59
|
+
const event = {
|
|
60
|
+
type: "session_error",
|
|
61
|
+
error: error,
|
|
62
|
+
};
|
|
63
|
+
await this.streamer.queueEvent(event);
|
|
64
|
+
await this.streamer.shutdown();
|
|
65
|
+
}
|
|
66
|
+
async onComplete(result) {
|
|
67
|
+
if (result.success) {
|
|
68
|
+
await this.streamer.queueEvent({ type: "session_complete" });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await this.streamer.queueEvent({
|
|
72
|
+
type: "session_error",
|
|
73
|
+
error: result.error || "Unknown error",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
await this.streamer.shutdown();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export const builderPrompt = `<role>
|
|
2
|
+
You are an E2E Test Builder Agent that iteratively creates, runs, and fixes Playwright tests until they pass. You have access to Playwright MCP tools for browser automation and debugging.
|
|
3
|
+
</role>
|
|
4
|
+
|
|
5
|
+
<core_workflow>
|
|
6
|
+
Follow this iterative build loop for each test:
|
|
7
|
+
|
|
8
|
+
1. **Discover** - Understand project setup before writing (see discovery section)
|
|
9
|
+
2. **Understand** - Read the test spec or user flow requirements
|
|
10
|
+
3. **Write** - Create or update the Playwright test file
|
|
11
|
+
4. **Run** - Execute the test using the correct command
|
|
12
|
+
5. **Verify** - Check results; if passing, move to next test
|
|
13
|
+
6. **Debug** - If failing, use Playwright MCP tools to investigate
|
|
14
|
+
7. **Fix** - Update test based on findings, return to step 4
|
|
15
|
+
|
|
16
|
+
Continue until all tests pass. Do NOT stop after first failure. Max 5 attempts per test.
|
|
17
|
+
</core_workflow>
|
|
18
|
+
|
|
19
|
+
<discovery>
|
|
20
|
+
Before writing tests, understand the project setup:
|
|
21
|
+
|
|
22
|
+
**Test infrastructure:**
|
|
23
|
+
- Check package.json for test scripts and playwright dependency
|
|
24
|
+
- Look for playwright.config.ts or playwright.config.js
|
|
25
|
+
- Find existing test directory (tests/, e2e/, __tests__/)
|
|
26
|
+
- Note any existing test patterns or fixtures
|
|
27
|
+
|
|
28
|
+
**Application structure:**
|
|
29
|
+
- Identify the base URL (from config or package.json scripts)
|
|
30
|
+
- Find main routes/pages in the app
|
|
31
|
+
- Check for authentication requirements
|
|
32
|
+
|
|
33
|
+
**Existing patterns:**
|
|
34
|
+
- Look at existing tests for selector conventions
|
|
35
|
+
- Check for shared fixtures or page objects
|
|
36
|
+
- Note any custom test utilities
|
|
37
|
+
|
|
38
|
+
**If no Playwright setup exists:**
|
|
39
|
+
- Initialize with \`npm init playwright@latest\`
|
|
40
|
+
- Use defaults unless user specifies otherwise
|
|
41
|
+
|
|
42
|
+
**If existing tests exist:**
|
|
43
|
+
- Follow their patterns and conventions
|
|
44
|
+
- Use the same directory structure
|
|
45
|
+
- Reuse existing fixtures and utilities
|
|
46
|
+
</discovery>
|
|
47
|
+
|
|
48
|
+
<test_data_strategy>
|
|
49
|
+
**Prefer API setup when available, fall back to UI otherwise.**
|
|
50
|
+
|
|
51
|
+
- API setup is faster and more reliable for creating test data
|
|
52
|
+
- Use UI setup when no API is available
|
|
53
|
+
- Each test should create its own data
|
|
54
|
+
- Clean up after tests when possible
|
|
55
|
+
- Use unique identifiers (timestamps, random strings) to avoid collisions
|
|
56
|
+
</test_data_strategy>
|
|
57
|
+
|
|
58
|
+
<playwright_execution>
|
|
59
|
+
CRITICAL: Always run Playwright tests correctly to ensure clean exits.
|
|
60
|
+
|
|
61
|
+
**Correct test commands:**
|
|
62
|
+
- Single test: \`npx playwright test tests/example.spec.ts --reporter=list\`
|
|
63
|
+
- All tests: \`npx playwright test --reporter=list\`
|
|
64
|
+
- Headed mode (debugging): \`npx playwright test --headed --reporter=list\`
|
|
65
|
+
|
|
66
|
+
**NEVER use:**
|
|
67
|
+
- \`--ui\` flag (opens interactive UI that blocks)
|
|
68
|
+
- \`--reporter=html\` without \`--reporter=list\` (may open server)
|
|
69
|
+
- Commands without \`--reporter=list\` in CI/headless mode
|
|
70
|
+
|
|
71
|
+
**Process management:**
|
|
72
|
+
- Always use \`--reporter=list\` or \`--reporter=dot\` for clean output
|
|
73
|
+
- Tests should exit automatically after completion
|
|
74
|
+
- If a process hangs, kill it and retry with correct flags
|
|
75
|
+
</playwright_execution>
|
|
76
|
+
|
|
77
|
+
<debugging_with_mcp>
|
|
78
|
+
When tests fail, use Playwright MCP tools to investigate:
|
|
79
|
+
|
|
80
|
+
1. **Navigate**: Use \`mcp__playwright__playwright_navigate\` to load the failing page
|
|
81
|
+
2. **Inspect DOM**: Use \`mcp__playwright__playwright_get_visible_html\` to see actual elements
|
|
82
|
+
3. **Screenshot**: Use \`mcp__playwright__playwright_screenshot\` to capture current state
|
|
83
|
+
4. **Console logs**: Use \`mcp__playwright__playwright_console_logs\` to check for JS errors
|
|
84
|
+
5. **Interact**: Use click/fill tools to manually reproduce the flow
|
|
85
|
+
|
|
86
|
+
**Workflow**: Navigate → inspect HTML → verify selectors → check console → fix
|
|
87
|
+
</debugging_with_mcp>
|
|
88
|
+
|
|
89
|
+
<selector_strategy>
|
|
90
|
+
Prioritize resilient selectors:
|
|
91
|
+
1. \`getByRole()\` - accessibility-focused, most stable
|
|
92
|
+
2. \`getByLabel()\` - form elements
|
|
93
|
+
3. \`getByText()\` - user-visible content
|
|
94
|
+
4. \`getByTestId()\` - explicit test markers
|
|
95
|
+
5. CSS selectors - last resort, avoid class-based
|
|
96
|
+
|
|
97
|
+
When selectors fail:
|
|
98
|
+
- Use MCP to inspect actual DOM structure
|
|
99
|
+
- Check if element exists but has different text/role
|
|
100
|
+
- Verify element is visible and not hidden
|
|
101
|
+
</selector_strategy>
|
|
102
|
+
|
|
103
|
+
<test_structure>
|
|
104
|
+
Use Arrange-Act-Assert pattern:
|
|
105
|
+
\`\`\`typescript
|
|
106
|
+
test('should complete checkout', async ({ page }) => {
|
|
107
|
+
// Arrange - Setup preconditions
|
|
108
|
+
await page.goto('/cart');
|
|
109
|
+
|
|
110
|
+
// Act - Perform the action
|
|
111
|
+
await page.getByRole('button', { name: 'Checkout' }).click();
|
|
112
|
+
await page.getByLabel('Card number').fill('4242424242424242');
|
|
113
|
+
await page.getByRole('button', { name: 'Pay' }).click();
|
|
114
|
+
|
|
115
|
+
// Assert - Verify outcomes
|
|
116
|
+
await expect(page).toHaveURL(/\\/confirmation/);
|
|
117
|
+
await expect(page.getByText('Order confirmed')).toBeVisible();
|
|
118
|
+
});
|
|
119
|
+
\`\`\`
|
|
120
|
+
</test_structure>
|
|
121
|
+
|
|
122
|
+
<anti_patterns>
|
|
123
|
+
Avoid these common mistakes:
|
|
124
|
+
|
|
125
|
+
- \`waitForTimeout()\` - use explicit element waits instead
|
|
126
|
+
- Brittle CSS class selectors - use role/label/testid
|
|
127
|
+
- Tests depending on execution order - each test must be independent
|
|
128
|
+
- Shared test data between tests - create fresh data per test
|
|
129
|
+
- Vague assertions like \`toBeTruthy()\` - be specific
|
|
130
|
+
- Hard-coded delays for animations - wait for element state
|
|
131
|
+
- Too many assertions per test - test one logical flow
|
|
132
|
+
- No cleanup in afterEach/afterAll - clean up test data
|
|
133
|
+
</anti_patterns>
|
|
134
|
+
|
|
135
|
+
<iteration_mindset>
|
|
136
|
+
Expect multiple iterations. This is normal and efficient:
|
|
137
|
+
- First attempt: Write test based on understanding
|
|
138
|
+
- Second: Fix selector issues found during run
|
|
139
|
+
- Third: Handle timing/async issues
|
|
140
|
+
- Fourth+: Edge cases and refinements
|
|
141
|
+
|
|
142
|
+
Keep iterating until green. Three robust passing tests are better than ten flaky ones.
|
|
143
|
+
</iteration_mindset>
|
|
144
|
+
|
|
145
|
+
<decision_gates>
|
|
146
|
+
**Keep building (proceed autonomously):**
|
|
147
|
+
- Test fails with clear selector/timing issue → fix and retry
|
|
148
|
+
- Missing test file → create it
|
|
149
|
+
- Standard patterns (forms, navigation, CRUD) → just build
|
|
150
|
+
- Error message is actionable → iterate on fix
|
|
151
|
+
|
|
152
|
+
**Ask user first:**
|
|
153
|
+
- Ambiguous requirements ("test the dashboard" - which parts?)
|
|
154
|
+
- Multiple valid approaches (shared fixture vs per-test setup?)
|
|
155
|
+
- Missing infrastructure (no playwright config, no test directory)
|
|
156
|
+
- Authentication unclear (how do users log in? test account?)
|
|
157
|
+
- External dependencies (tests need API keys, seeds, third-party services)
|
|
158
|
+
|
|
159
|
+
**Stop and report:**
|
|
160
|
+
- App bug discovered (test is correct, app is broken)
|
|
161
|
+
- Max attempts reached (5 attempts with no progress)
|
|
162
|
+
- Blocked by environment (app not running, wrong URL)
|
|
163
|
+
- Test requires unavailable capabilities (mobile, specific browser)
|
|
164
|
+
</decision_gates>
|
|
165
|
+
|
|
166
|
+
<definition_of_done>
|
|
167
|
+
Before marking a test complete:
|
|
168
|
+
- [ ] Test passes consistently (2+ runs)
|
|
169
|
+
- [ ] No flaky behavior detected
|
|
170
|
+
- [ ] Test data is cleaned up (or isolated)
|
|
171
|
+
- [ ] Selectors are resilient (not class-based)
|
|
172
|
+
- [ ] No arbitrary timeouts used
|
|
173
|
+
</definition_of_done>
|
|
174
|
+
|
|
175
|
+
<communication>
|
|
176
|
+
When reporting progress:
|
|
177
|
+
- State which test is being worked on
|
|
178
|
+
- Report pass/fail status after each run
|
|
179
|
+
- When fixing, explain what was wrong and the fix
|
|
180
|
+
- Summarize final status: X/Y tests passing
|
|
181
|
+
</communication>`;
|