@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.
Files changed (69) hide show
  1. package/dist/commands/login.js +392 -0
  2. package/dist/commands/setup.js +234 -0
  3. package/dist/config.js +29 -0
  4. package/dist/core/agent.js +259 -0
  5. package/dist/index.js +154 -6586
  6. package/dist/modes/headless.js +117 -0
  7. package/dist/modes/interactive.js +418 -0
  8. package/dist/presenters/composite.js +32 -0
  9. package/dist/presenters/console.js +163 -0
  10. package/dist/presenters/react.js +217 -0
  11. package/dist/presenters/types.js +1 -0
  12. package/dist/presenters/web.js +78 -0
  13. package/dist/prompts/builder.js +181 -0
  14. package/dist/prompts/fixer.js +148 -0
  15. package/dist/prompts/index.js +3 -0
  16. package/dist/prompts/planner.js +70 -0
  17. package/dist/services/api-client.js +244 -0
  18. package/dist/services/event-streamer.js +130 -0
  19. package/dist/types.js +1 -0
  20. package/dist/ui/App.js +322 -0
  21. package/dist/ui/components/AuthBanner.js +24 -0
  22. package/dist/ui/components/AuthDialog.js +32 -0
  23. package/dist/ui/components/Banner.js +12 -0
  24. package/dist/ui/components/ExpandableSection.js +17 -0
  25. package/dist/ui/components/Header.js +51 -0
  26. package/dist/ui/components/HelpMenu.js +89 -0
  27. package/dist/ui/components/InputPrompt.js +286 -0
  28. package/dist/ui/components/MessageList.js +42 -0
  29. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  30. package/dist/ui/components/Scrollable.js +103 -0
  31. package/dist/ui/components/SessionSelector.js +196 -0
  32. package/dist/ui/components/StatusBar.js +34 -0
  33. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  34. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  35. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  36. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  37. package/dist/ui/components/messages/TodoMessage.js +44 -0
  38. package/dist/ui/components/messages/ToolMessage.js +218 -0
  39. package/dist/ui/components/messages/UserMessage.js +14 -0
  40. package/dist/ui/contexts/KeypressContext.js +527 -0
  41. package/dist/ui/contexts/MouseContext.js +98 -0
  42. package/dist/ui/contexts/SessionContext.js +129 -0
  43. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  44. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  45. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  46. package/dist/ui/hooks/useFocus.js +50 -0
  47. package/dist/ui/hooks/useKeypress.js +26 -0
  48. package/dist/ui/hooks/useModeToggle.js +25 -0
  49. package/dist/ui/types/auth.js +13 -0
  50. package/dist/ui/utils/file-completion.js +56 -0
  51. package/dist/ui/utils/input.js +50 -0
  52. package/dist/ui/utils/markdown.js +376 -0
  53. package/dist/ui/utils/mouse.js +189 -0
  54. package/dist/ui/utils/theme.js +59 -0
  55. package/dist/utils/banner.js +9 -0
  56. package/dist/utils/encryption.js +71 -0
  57. package/dist/utils/events.js +36 -0
  58. package/dist/utils/keychain-storage.js +120 -0
  59. package/dist/utils/logger.js +209 -0
  60. package/dist/utils/node-version.js +89 -0
  61. package/dist/utils/plan-file.js +75 -0
  62. package/dist/utils/project-instructions.js +23 -0
  63. package/dist/utils/rich-logger.js +208 -0
  64. package/dist/utils/stdin.js +25 -0
  65. package/dist/utils/stdio.js +80 -0
  66. package/dist/utils/summary.js +94 -0
  67. package/dist/utils/token-storage.js +242 -0
  68. package/dist/version.js +6 -0
  69. 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
+ }