@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,117 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { config as envConfig } from "../config";
|
|
3
|
+
import { CoreAgent } from "../core/agent";
|
|
4
|
+
import { CompositePresenter } from "../presenters/composite";
|
|
5
|
+
import { ConsolePresenter } from "../presenters/console";
|
|
6
|
+
import { WebPresenter } from "../presenters/web";
|
|
7
|
+
import { ApiClient } from "../services/api-client";
|
|
8
|
+
import { logger } from "../utils/logger";
|
|
9
|
+
import { CLI_VERSION } from "../version";
|
|
10
|
+
export async function runAgent(config) {
|
|
11
|
+
// Configure logger
|
|
12
|
+
logger.setVerbose(config.verbose);
|
|
13
|
+
// --- Metadata Display (CLI only) ---
|
|
14
|
+
logger.raw("");
|
|
15
|
+
// Get git branch if available
|
|
16
|
+
let gitBranch = "";
|
|
17
|
+
try {
|
|
18
|
+
const { execSync } = await import("node:child_process");
|
|
19
|
+
gitBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
22
|
+
}).trim();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Not in a git repo or git not available
|
|
26
|
+
}
|
|
27
|
+
const metadataParts = [
|
|
28
|
+
chalk.dim("Supatest AI ") + chalk.cyan(`v${CLI_VERSION}`),
|
|
29
|
+
chalk.dim("Model: ") + chalk.cyan(envConfig.anthropicModelName),
|
|
30
|
+
];
|
|
31
|
+
if (gitBranch) {
|
|
32
|
+
metadataParts.push(chalk.dim("Branch: ") + chalk.cyan(gitBranch));
|
|
33
|
+
}
|
|
34
|
+
logger.raw(metadataParts.join(chalk.dim(" • ")));
|
|
35
|
+
logger.divider();
|
|
36
|
+
// --- Session & API Setup ---
|
|
37
|
+
const apiUrl = config.supatestApiUrl || "https://code-api.supatest.ai";
|
|
38
|
+
const apiClient = new ApiClient(apiUrl, config.supatestApiKey);
|
|
39
|
+
let sessionId;
|
|
40
|
+
let webUrl;
|
|
41
|
+
try {
|
|
42
|
+
// Truncate title to 50 characters (backend will auto-generate a better title later)
|
|
43
|
+
const truncatedTitle = config.task.length > 50 ? config.task.slice(0, 50) + "..." : config.task;
|
|
44
|
+
const session = await apiClient.createSession(truncatedTitle, {
|
|
45
|
+
cliVersion: CLI_VERSION,
|
|
46
|
+
cwd: config.cwd || process.cwd(),
|
|
47
|
+
});
|
|
48
|
+
sessionId = session.sessionId;
|
|
49
|
+
webUrl = session.webUrl;
|
|
50
|
+
logger.raw("");
|
|
51
|
+
logger.divider();
|
|
52
|
+
logger.raw(chalk.white.bold("View session live: ") +
|
|
53
|
+
chalk.cyan.underline(webUrl));
|
|
54
|
+
logger.divider();
|
|
55
|
+
logger.raw("");
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
logger.warn(`Failed to create session on backend: ${error.message}`);
|
|
59
|
+
logger.warn("Continuing without web streaming...");
|
|
60
|
+
}
|
|
61
|
+
// --- Environment Setup ---
|
|
62
|
+
// Build base URL with session ID embedded for the proxy
|
|
63
|
+
let baseUrl = `${apiUrl}/public`;
|
|
64
|
+
if (sessionId) {
|
|
65
|
+
baseUrl = `${apiUrl}/v1/sessions/${sessionId}/anthropic`;
|
|
66
|
+
}
|
|
67
|
+
// Set environment variables for the SDK to pick up (via CoreAgent)
|
|
68
|
+
process.env.ANTHROPIC_BASE_URL = baseUrl;
|
|
69
|
+
process.env.ANTHROPIC_API_KEY = config.supatestApiKey;
|
|
70
|
+
// --- Agent Execution ---
|
|
71
|
+
const presenters = [];
|
|
72
|
+
// 1. Console Presenter (stdout)
|
|
73
|
+
presenters.push(new ConsolePresenter({ verbose: config.verbose }));
|
|
74
|
+
// 2. Web Presenter (streaming)
|
|
75
|
+
if (sessionId) {
|
|
76
|
+
presenters.push(new WebPresenter(apiClient, sessionId));
|
|
77
|
+
}
|
|
78
|
+
const compositePresenter = new CompositePresenter(presenters);
|
|
79
|
+
const agent = new CoreAgent(compositePresenter);
|
|
80
|
+
try {
|
|
81
|
+
const result = await agent.run(config);
|
|
82
|
+
// Store the provider session ID for future resume capability
|
|
83
|
+
// This allows follow-up messages to continue the conversation with full context
|
|
84
|
+
if (sessionId && result.providerSessionId) {
|
|
85
|
+
try {
|
|
86
|
+
await apiClient.updateSession(sessionId, {
|
|
87
|
+
providerSessionId: result.providerSessionId,
|
|
88
|
+
});
|
|
89
|
+
logger.debug(`Stored provider session ID for resume capability`);
|
|
90
|
+
}
|
|
91
|
+
catch (updateError) {
|
|
92
|
+
// Non-critical - log but don't fail
|
|
93
|
+
logger.warn(`Failed to store provider session ID: ${updateError.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Display web URL again at completion
|
|
97
|
+
if (webUrl) {
|
|
98
|
+
logger.raw("");
|
|
99
|
+
logger.divider();
|
|
100
|
+
logger.raw(chalk.white.bold("View session: ") +
|
|
101
|
+
chalk.cyan.underline(webUrl));
|
|
102
|
+
logger.divider();
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
108
|
+
// Error is already logged by presenter.onError
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
summary: `Failed: ${errorMessage}`,
|
|
112
|
+
filesModified: [],
|
|
113
|
+
iterations: 0,
|
|
114
|
+
error: errorMessage,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive mode entry point
|
|
3
|
+
* Launches the Ink UI and runs the agent with real-time updates
|
|
4
|
+
*/
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
import React, { useEffect, useRef } from "react";
|
|
7
|
+
import { config as envConfig } from "../config";
|
|
8
|
+
import { CoreAgent } from "../core/agent";
|
|
9
|
+
import { ReactPresenter } from "../presenters/react";
|
|
10
|
+
import { ApiClient, ApiError } from "../services/api-client";
|
|
11
|
+
import { App } from "../ui/App";
|
|
12
|
+
import { KeypressProvider } from "../ui/contexts/KeypressContext";
|
|
13
|
+
import { MouseProvider } from "../ui/contexts/MouseContext";
|
|
14
|
+
import { SessionProvider, useSession } from "../ui/contexts/SessionContext";
|
|
15
|
+
import { useBracketedPaste } from "../ui/hooks/useBracketedPaste";
|
|
16
|
+
import { disableMouseEvents, enableMouseEvents } from "../ui/utils/mouse";
|
|
17
|
+
import { logger } from "../utils/logger";
|
|
18
|
+
import { createInkStdio, patchStdio } from "../utils/stdio";
|
|
19
|
+
import { CLI_VERSION } from "../version";
|
|
20
|
+
/**
|
|
21
|
+
* Get human-readable description for tool call (used when resuming sessions)
|
|
22
|
+
*/
|
|
23
|
+
function getToolDescription(toolName, input) {
|
|
24
|
+
switch (toolName) {
|
|
25
|
+
case "Read":
|
|
26
|
+
return input?.file_path || "file";
|
|
27
|
+
case "Write":
|
|
28
|
+
return input?.file_path || "file";
|
|
29
|
+
case "Edit":
|
|
30
|
+
return input?.file_path || "file";
|
|
31
|
+
case "Bash": {
|
|
32
|
+
const cmd = input?.command || "";
|
|
33
|
+
return cmd.length > 60 ? `${cmd.substring(0, 60)}...` : cmd;
|
|
34
|
+
}
|
|
35
|
+
case "Glob":
|
|
36
|
+
return `pattern: "${input?.pattern || "files"}"`;
|
|
37
|
+
case "Grep": {
|
|
38
|
+
const pattern = input?.pattern || "code";
|
|
39
|
+
const path = input?.path;
|
|
40
|
+
return path ? `"${pattern}" (in ${path})` : `"${pattern}"`;
|
|
41
|
+
}
|
|
42
|
+
case "Task":
|
|
43
|
+
return input?.subagent_type || "task";
|
|
44
|
+
case "TodoWrite":
|
|
45
|
+
return "Updated todos";
|
|
46
|
+
case "BashOutput":
|
|
47
|
+
case "Command Output":
|
|
48
|
+
return input?.bash_id || "shell output";
|
|
49
|
+
default:
|
|
50
|
+
return toolName;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Agent Runner Component
|
|
55
|
+
* Runs the agent via CoreAgent and updates the UI context in real-time
|
|
56
|
+
*/
|
|
57
|
+
const AgentRunner = ({ config, sessionId, apiClient, onComplete }) => {
|
|
58
|
+
const { addMessage, updateLastMessage, updateMessageByToolId, setIsAgentRunning, updateStats, setTodos, shouldInterruptAgent, setShouldInterruptAgent, agentMode, planFilePath, } = useSession();
|
|
59
|
+
// Keep a ref to the agent so we can call abort()
|
|
60
|
+
const agentRef = useRef(null);
|
|
61
|
+
// When shouldInterruptAgent changes to true, abort the agent
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (shouldInterruptAgent && agentRef.current) {
|
|
64
|
+
agentRef.current.abort();
|
|
65
|
+
setShouldInterruptAgent(false);
|
|
66
|
+
}
|
|
67
|
+
}, [shouldInterruptAgent, setShouldInterruptAgent]);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
let isMounted = true;
|
|
70
|
+
const runAgent = async () => {
|
|
71
|
+
setIsAgentRunning(true);
|
|
72
|
+
try {
|
|
73
|
+
// Set up environment for Claude Code
|
|
74
|
+
const proxyUrl = config.supatestApiUrl || "https://code-api.supatest.ai";
|
|
75
|
+
const baseUrl = `${proxyUrl}/v1/sessions/${sessionId}/anthropic`;
|
|
76
|
+
process.env.ANTHROPIC_BASE_URL = baseUrl;
|
|
77
|
+
process.env.ANTHROPIC_API_KEY = config.supatestApiKey;
|
|
78
|
+
// Create presenter with callbacks to React state
|
|
79
|
+
const presenter = new ReactPresenter({
|
|
80
|
+
addMessage: (msg) => {
|
|
81
|
+
if (isMounted)
|
|
82
|
+
addMessage(msg);
|
|
83
|
+
},
|
|
84
|
+
updateLastMessage: (update) => {
|
|
85
|
+
if (isMounted)
|
|
86
|
+
updateLastMessage(update);
|
|
87
|
+
},
|
|
88
|
+
updateMessageByToolId: (toolId, update) => {
|
|
89
|
+
if (isMounted)
|
|
90
|
+
updateMessageByToolId(toolId, update);
|
|
91
|
+
},
|
|
92
|
+
updateStats: (stats) => {
|
|
93
|
+
if (isMounted)
|
|
94
|
+
updateStats(stats);
|
|
95
|
+
},
|
|
96
|
+
setTodos: (todos) => {
|
|
97
|
+
if (isMounted)
|
|
98
|
+
setTodos(todos);
|
|
99
|
+
},
|
|
100
|
+
// Note: onComplete is now called after agent.run() returns
|
|
101
|
+
// to capture the providerSessionId from the result
|
|
102
|
+
onComplete: () => { },
|
|
103
|
+
}, apiClient, sessionId, config.verbose);
|
|
104
|
+
// Build mode-specific configuration
|
|
105
|
+
// Plan mode uses planSystemPrompt and restricts to read-only tools
|
|
106
|
+
const runConfig = {
|
|
107
|
+
...config,
|
|
108
|
+
mode: agentMode,
|
|
109
|
+
planFilePath,
|
|
110
|
+
systemPromptAppend: agentMode === 'plan'
|
|
111
|
+
? envConfig.planSystemPrompt
|
|
112
|
+
: config.systemPromptAppend,
|
|
113
|
+
};
|
|
114
|
+
// Run the agent through CoreAgent
|
|
115
|
+
// Store ref so we can abort from interrupt effect
|
|
116
|
+
const agent = new CoreAgent(presenter);
|
|
117
|
+
agentRef.current = agent;
|
|
118
|
+
const result = await agent.run(runConfig);
|
|
119
|
+
// Pass providerSessionId on completion for storage
|
|
120
|
+
if (isMounted) {
|
|
121
|
+
onComplete(result.success, result.providerSessionId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (isMounted) {
|
|
126
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
127
|
+
addMessage({
|
|
128
|
+
type: "error",
|
|
129
|
+
content: errorMessage,
|
|
130
|
+
errorType: "error",
|
|
131
|
+
});
|
|
132
|
+
await apiClient.streamEvent(sessionId, {
|
|
133
|
+
type: "session_error",
|
|
134
|
+
error: errorMessage,
|
|
135
|
+
});
|
|
136
|
+
onComplete(false);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (isMounted) {
|
|
141
|
+
setIsAgentRunning(false);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
runAgent();
|
|
146
|
+
return () => {
|
|
147
|
+
isMounted = false;
|
|
148
|
+
agentRef.current = null;
|
|
149
|
+
};
|
|
150
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
151
|
+
return null; // This component doesn't render anything
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Content component for InteractiveApp that has access to SessionContext
|
|
155
|
+
*/
|
|
156
|
+
const InteractiveAppContent = ({ config, sessionId: initialSessionId, webUrl, apiClient, onExit }) => {
|
|
157
|
+
const { addMessage, loadMessages, setSessionId: setContextSessionId, } = useSession();
|
|
158
|
+
const [sessionId, setSessionId] = React.useState(initialSessionId);
|
|
159
|
+
const [currentTask, setCurrentTask] = React.useState(config.task);
|
|
160
|
+
const [taskId, setTaskId] = React.useState(0);
|
|
161
|
+
const [shouldRunAgent, setShouldRunAgent] = React.useState(!!config.task);
|
|
162
|
+
const [taskQueue, setTaskQueue] = React.useState([]);
|
|
163
|
+
// Track provider session ID for resume capability
|
|
164
|
+
const [providerSessionId, setProviderSessionId] = React.useState();
|
|
165
|
+
const handleSubmitTask = async (task) => {
|
|
166
|
+
// Create session on first message if it doesn't exist
|
|
167
|
+
if (!sessionId) {
|
|
168
|
+
try {
|
|
169
|
+
const truncatedTitle = task.length > 50 ? task.slice(0, 50) + "..." : task;
|
|
170
|
+
const session = await apiClient.createSession(truncatedTitle, {
|
|
171
|
+
cliVersion: CLI_VERSION,
|
|
172
|
+
cwd: config.cwd || process.cwd(),
|
|
173
|
+
});
|
|
174
|
+
setSessionId(session.sessionId);
|
|
175
|
+
setContextSessionId(session.sessionId);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const errorMessage = error instanceof ApiError
|
|
179
|
+
? error.message
|
|
180
|
+
: `Failed to create session: ${error instanceof Error ? error.message : String(error)}`;
|
|
181
|
+
addMessage({
|
|
182
|
+
type: "error",
|
|
183
|
+
content: errorMessage,
|
|
184
|
+
errorType: error instanceof ApiError && error.isAuthError ? "warning" : "error",
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (shouldRunAgent) {
|
|
190
|
+
setTaskQueue((prev) => [...prev, task]);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
setCurrentTask(task);
|
|
194
|
+
addMessage({
|
|
195
|
+
type: "user",
|
|
196
|
+
content: task,
|
|
197
|
+
});
|
|
198
|
+
setTaskId((prev) => prev + 1);
|
|
199
|
+
setShouldRunAgent(true);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const handleAgentComplete = async (_success, newProviderSessionId) => {
|
|
203
|
+
setShouldRunAgent(false);
|
|
204
|
+
// Store the provider session ID for future resume capability
|
|
205
|
+
if (sessionId && newProviderSessionId && newProviderSessionId !== providerSessionId) {
|
|
206
|
+
setProviderSessionId(newProviderSessionId);
|
|
207
|
+
try {
|
|
208
|
+
await apiClient.updateSession(sessionId, {
|
|
209
|
+
providerSessionId: newProviderSessionId,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Non-critical - session will work without resume capability
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
// Process queued tasks when agent becomes idle
|
|
218
|
+
React.useEffect(() => {
|
|
219
|
+
if (!shouldRunAgent && taskQueue.length > 0) {
|
|
220
|
+
const [nextTask, ...remaining] = taskQueue;
|
|
221
|
+
setTaskQueue(remaining);
|
|
222
|
+
setCurrentTask(nextTask);
|
|
223
|
+
addMessage({
|
|
224
|
+
type: "user",
|
|
225
|
+
content: nextTask,
|
|
226
|
+
});
|
|
227
|
+
setTaskId((prev) => prev + 1);
|
|
228
|
+
setShouldRunAgent(true);
|
|
229
|
+
}
|
|
230
|
+
}, [shouldRunAgent, taskQueue, addMessage]);
|
|
231
|
+
return (React.createElement(React.Fragment, null,
|
|
232
|
+
React.createElement(App, { apiClient: apiClient, config: { ...config, task: currentTask }, onExit: onExit, onResumeSession: async (session) => {
|
|
233
|
+
try {
|
|
234
|
+
if (!apiClient) {
|
|
235
|
+
addMessage({
|
|
236
|
+
type: "error",
|
|
237
|
+
content: "API client not available. Cannot resume session.",
|
|
238
|
+
errorType: "error",
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const response = await apiClient.getSessionMessages(session.id);
|
|
243
|
+
const apiMessages = response.messages;
|
|
244
|
+
// First pass: collect all tool_results by tool_use_id
|
|
245
|
+
const toolResults = new Map();
|
|
246
|
+
for (const msg of apiMessages) {
|
|
247
|
+
const contentBlocks = msg.content;
|
|
248
|
+
if (!contentBlocks)
|
|
249
|
+
continue;
|
|
250
|
+
for (const block of contentBlocks) {
|
|
251
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
252
|
+
// Extract text content from tool result
|
|
253
|
+
let resultText = "";
|
|
254
|
+
if (typeof block.content === "string") {
|
|
255
|
+
resultText = block.content;
|
|
256
|
+
}
|
|
257
|
+
else if (Array.isArray(block.content)) {
|
|
258
|
+
resultText = block.content
|
|
259
|
+
.map((c) => c.text || "")
|
|
260
|
+
.join("\n");
|
|
261
|
+
}
|
|
262
|
+
toolResults.set(block.tool_use_id, resultText);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Convert API messages to UI Message format
|
|
267
|
+
const uiMessages = apiMessages.flatMap((msg) => {
|
|
268
|
+
const messages = [];
|
|
269
|
+
const contentBlocks = msg.content;
|
|
270
|
+
if (!contentBlocks || contentBlocks.length === 0) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
for (const block of contentBlocks) {
|
|
274
|
+
if (block.type === "text") {
|
|
275
|
+
messages.push({
|
|
276
|
+
id: `${msg.id}-${messages.length}`,
|
|
277
|
+
type: msg.role,
|
|
278
|
+
content: block.text,
|
|
279
|
+
timestamp: new Date(msg.createdAt).getTime(),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
else if (block.type === "thinking") {
|
|
283
|
+
messages.push({
|
|
284
|
+
id: `${msg.id}-${messages.length}`,
|
|
285
|
+
type: "thinking",
|
|
286
|
+
content: block.thinking,
|
|
287
|
+
timestamp: new Date(msg.createdAt).getTime(),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else if (block.type === "tool_use") {
|
|
291
|
+
// Look up the tool result for this tool_use
|
|
292
|
+
const toolResult = toolResults.get(block.id);
|
|
293
|
+
messages.push({
|
|
294
|
+
id: `${msg.id}-${messages.length}`,
|
|
295
|
+
type: "tool",
|
|
296
|
+
content: getToolDescription(block.name, block.input),
|
|
297
|
+
toolName: block.name,
|
|
298
|
+
toolInput: block.input,
|
|
299
|
+
toolResult: toolResult,
|
|
300
|
+
toolUseId: block.id,
|
|
301
|
+
timestamp: new Date(msg.createdAt).getTime(),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return messages;
|
|
306
|
+
});
|
|
307
|
+
setSessionId(session.id);
|
|
308
|
+
setContextSessionId(session.id);
|
|
309
|
+
// Set the provider session ID for resume capability
|
|
310
|
+
// The session object may have providerSessionId from the API
|
|
311
|
+
if (session.providerSessionId) {
|
|
312
|
+
setProviderSessionId(session.providerSessionId);
|
|
313
|
+
}
|
|
314
|
+
uiMessages.push({
|
|
315
|
+
id: `resume-info-${Date.now()}`,
|
|
316
|
+
type: "error",
|
|
317
|
+
content: `Resumed session: ${session.title || "Untitled"} (${apiMessages.length} messages loaded)`,
|
|
318
|
+
errorType: "info",
|
|
319
|
+
timestamp: Date.now(),
|
|
320
|
+
});
|
|
321
|
+
loadMessages(uiMessages);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
const errorMessage = error instanceof ApiError
|
|
325
|
+
? error.message
|
|
326
|
+
: `Failed to resume session: ${error instanceof Error ? error.message : String(error)}`;
|
|
327
|
+
addMessage({
|
|
328
|
+
type: "error",
|
|
329
|
+
content: errorMessage,
|
|
330
|
+
errorType: error instanceof ApiError && error.isAuthError
|
|
331
|
+
? "warning"
|
|
332
|
+
: "error",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}, onSubmitTask: handleSubmitTask, queuedTasks: taskQueue, sessionId: sessionId, webUrl: webUrl }),
|
|
336
|
+
shouldRunAgent && currentTask && sessionId && (React.createElement(AgentRunner, { apiClient: apiClient, config: { ...config, task: currentTask, providerSessionId }, key: `${taskId}`, onComplete: handleAgentComplete, sessionId: sessionId }))));
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Main wrapper component that combines UI and agent
|
|
340
|
+
*/
|
|
341
|
+
const InteractiveApp = (props) => {
|
|
342
|
+
useBracketedPaste();
|
|
343
|
+
return (React.createElement(KeypressProvider, null,
|
|
344
|
+
React.createElement(MouseProvider, { mouseEventsEnabled: true },
|
|
345
|
+
React.createElement(SessionProvider, null,
|
|
346
|
+
React.createElement(InteractiveAppContent, { ...props })))));
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* Run the interactive mode with Ink UI
|
|
350
|
+
*/
|
|
351
|
+
export async function runInteractive(config) {
|
|
352
|
+
let success = false;
|
|
353
|
+
let unmountFn = null;
|
|
354
|
+
let isExiting = false;
|
|
355
|
+
const cleanupStdio = patchStdio();
|
|
356
|
+
const gracefulExit = async (exitCode) => {
|
|
357
|
+
if (isExiting)
|
|
358
|
+
return;
|
|
359
|
+
isExiting = true;
|
|
360
|
+
disableMouseEvents();
|
|
361
|
+
cleanupStdio();
|
|
362
|
+
if (unmountFn) {
|
|
363
|
+
unmountFn();
|
|
364
|
+
}
|
|
365
|
+
process.exit(exitCode);
|
|
366
|
+
};
|
|
367
|
+
const handleSigInt = async () => {
|
|
368
|
+
await gracefulExit(0);
|
|
369
|
+
};
|
|
370
|
+
process.on("SIGINT", handleSigInt);
|
|
371
|
+
process.on("SIGTERM", handleSigInt);
|
|
372
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
373
|
+
logger.enableFileLogging(isDev);
|
|
374
|
+
const apiUrl = config.supatestApiUrl || "https://code-api.supatest.ai";
|
|
375
|
+
const apiClient = new ApiClient(apiUrl, config.supatestApiKey);
|
|
376
|
+
try {
|
|
377
|
+
process.stdout.write("\x1Bc");
|
|
378
|
+
console.clear();
|
|
379
|
+
enableMouseEvents();
|
|
380
|
+
const { stdout: inkStdout, stderr: inkStderr } = createInkStdio();
|
|
381
|
+
let sessionId;
|
|
382
|
+
let webUrl;
|
|
383
|
+
if (config.task) {
|
|
384
|
+
const truncatedTitle = config.task.length > 50 ? config.task.slice(0, 50) + "..." : config.task;
|
|
385
|
+
const session = await apiClient.createSession(truncatedTitle, {
|
|
386
|
+
cliVersion: CLI_VERSION,
|
|
387
|
+
cwd: config.cwd || process.cwd(),
|
|
388
|
+
});
|
|
389
|
+
sessionId = session.sessionId;
|
|
390
|
+
webUrl = session.webUrl;
|
|
391
|
+
}
|
|
392
|
+
const { unmount, waitUntilExit } = render(React.createElement(InteractiveApp, { apiClient: apiClient, config: config, onExit: (exitSuccess) => {
|
|
393
|
+
success = exitSuccess;
|
|
394
|
+
}, sessionId: sessionId, webUrl: webUrl }), {
|
|
395
|
+
stdout: inkStdout,
|
|
396
|
+
stderr: inkStderr,
|
|
397
|
+
stdin: process.stdin,
|
|
398
|
+
alternateBuffer: true,
|
|
399
|
+
exitOnCtrlC: false,
|
|
400
|
+
});
|
|
401
|
+
unmountFn = unmount;
|
|
402
|
+
await waitUntilExit();
|
|
403
|
+
unmount();
|
|
404
|
+
disableMouseEvents();
|
|
405
|
+
cleanupStdio();
|
|
406
|
+
process.off("SIGINT", handleSigInt);
|
|
407
|
+
process.off("SIGTERM", handleSigInt);
|
|
408
|
+
process.exit(success ? 0 : 1);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
disableMouseEvents();
|
|
412
|
+
cleanupStdio();
|
|
413
|
+
process.off("SIGINT", handleSigInt);
|
|
414
|
+
process.off("SIGTERM", handleSigInt);
|
|
415
|
+
console.error("Failed to start interactive mode:", error instanceof Error ? error.message : String(error));
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class CompositePresenter {
|
|
2
|
+
presenters;
|
|
3
|
+
constructor(presenters) {
|
|
4
|
+
this.presenters = presenters;
|
|
5
|
+
}
|
|
6
|
+
async onStart(config) {
|
|
7
|
+
await Promise.all(this.presenters.map((p) => p.onStart(config)));
|
|
8
|
+
}
|
|
9
|
+
onLog(message) {
|
|
10
|
+
for (const p of this.presenters) {
|
|
11
|
+
p.onLog(message);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async onAssistantText(text) {
|
|
15
|
+
await Promise.all(this.presenters.map((p) => p.onAssistantText(text)));
|
|
16
|
+
}
|
|
17
|
+
async onThinking(text) {
|
|
18
|
+
await Promise.all(this.presenters.map((p) => p.onThinking(text)));
|
|
19
|
+
}
|
|
20
|
+
async onToolUse(tool, input, toolId) {
|
|
21
|
+
await Promise.all(this.presenters.map((p) => p.onToolUse(tool, input, toolId)));
|
|
22
|
+
}
|
|
23
|
+
async onTurnComplete(content) {
|
|
24
|
+
await Promise.all(this.presenters.map((p) => p.onTurnComplete(content)));
|
|
25
|
+
}
|
|
26
|
+
async onError(error) {
|
|
27
|
+
await Promise.all(this.presenters.map((p) => p.onError(error)));
|
|
28
|
+
}
|
|
29
|
+
async onComplete(result) {
|
|
30
|
+
await Promise.all(this.presenters.map((p) => p.onComplete(result)));
|
|
31
|
+
}
|
|
32
|
+
}
|