@supatest/cli 0.0.2 → 0.0.3

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 (75) hide show
  1. package/README.md +58 -315
  2. package/dist/agent-runner.js +224 -52
  3. package/dist/commands/login.js +392 -0
  4. package/dist/commands/setup.js +234 -0
  5. package/dist/config.js +29 -0
  6. package/dist/core/agent.js +270 -0
  7. package/dist/index.js +118 -31
  8. package/dist/modes/headless.js +117 -0
  9. package/dist/modes/interactive.js +430 -0
  10. package/dist/presenters/composite.js +32 -0
  11. package/dist/presenters/console.js +163 -0
  12. package/dist/presenters/react.js +220 -0
  13. package/dist/presenters/types.js +1 -0
  14. package/dist/presenters/web.js +78 -0
  15. package/dist/prompts/builder.js +181 -0
  16. package/dist/prompts/fixer.js +148 -0
  17. package/dist/prompts/headless.md +97 -0
  18. package/dist/prompts/index.js +3 -0
  19. package/dist/prompts/interactive.md +43 -0
  20. package/dist/prompts/plan.md +41 -0
  21. package/dist/prompts/planner.js +70 -0
  22. package/dist/prompts/prompts/builder.md +97 -0
  23. package/dist/prompts/prompts/fixer.md +100 -0
  24. package/dist/prompts/prompts/plan.md +41 -0
  25. package/dist/prompts/prompts/planner.md +41 -0
  26. package/dist/services/api-client.js +244 -0
  27. package/dist/services/event-streamer.js +130 -0
  28. package/dist/ui/App.js +322 -0
  29. package/dist/ui/components/AuthBanner.js +20 -0
  30. package/dist/ui/components/AuthDialog.js +32 -0
  31. package/dist/ui/components/Banner.js +12 -0
  32. package/dist/ui/components/ExpandableSection.js +17 -0
  33. package/dist/ui/components/Header.js +49 -0
  34. package/dist/ui/components/HelpMenu.js +89 -0
  35. package/dist/ui/components/InputPrompt.js +292 -0
  36. package/dist/ui/components/MessageList.js +42 -0
  37. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  38. package/dist/ui/components/Scrollable.js +103 -0
  39. package/dist/ui/components/SessionSelector.js +196 -0
  40. package/dist/ui/components/StatusBar.js +45 -0
  41. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  42. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  43. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  44. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  45. package/dist/ui/components/messages/TodoMessage.js +44 -0
  46. package/dist/ui/components/messages/ToolMessage.js +218 -0
  47. package/dist/ui/components/messages/UserMessage.js +14 -0
  48. package/dist/ui/contexts/KeypressContext.js +527 -0
  49. package/dist/ui/contexts/MouseContext.js +98 -0
  50. package/dist/ui/contexts/SessionContext.js +131 -0
  51. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  52. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  53. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  54. package/dist/ui/hooks/useFocus.js +50 -0
  55. package/dist/ui/hooks/useKeypress.js +26 -0
  56. package/dist/ui/hooks/useModeToggle.js +25 -0
  57. package/dist/ui/types/auth.js +13 -0
  58. package/dist/ui/utils/file-completion.js +56 -0
  59. package/dist/ui/utils/input.js +50 -0
  60. package/dist/ui/utils/markdown.js +376 -0
  61. package/dist/ui/utils/mouse.js +189 -0
  62. package/dist/ui/utils/theme.js +59 -0
  63. package/dist/utils/banner.js +7 -14
  64. package/dist/utils/encryption.js +71 -0
  65. package/dist/utils/events.js +36 -0
  66. package/dist/utils/keychain-storage.js +120 -0
  67. package/dist/utils/logger.js +103 -1
  68. package/dist/utils/node-version.js +1 -3
  69. package/dist/utils/plan-file.js +75 -0
  70. package/dist/utils/project-instructions.js +23 -0
  71. package/dist/utils/rich-logger.js +1 -1
  72. package/dist/utils/stdio.js +80 -0
  73. package/dist/utils/summary.js +1 -5
  74. package/dist/utils/token-storage.js +242 -0
  75. package/package.json +35 -15
@@ -0,0 +1,244 @@
1
+ import { logger } from "../utils/logger";
2
+ /**
3
+ * API Error class with user-friendly messages
4
+ *
5
+ * Based on Gemini CLI (Apache 2.0 License)
6
+ * https://github.com/google-gemini/gemini-cli
7
+ * Copyright 2025 Google LLC
8
+ */
9
+ export class ApiError extends Error {
10
+ status;
11
+ isAuthError;
12
+ constructor(status, statusText, body) {
13
+ let message;
14
+ if (status === 401) {
15
+ message = "Authentication required. Use /login to authenticate.";
16
+ }
17
+ else if (status === 403) {
18
+ message = "Access denied. Your token may have been revoked.";
19
+ }
20
+ else {
21
+ message = `API error: ${status} ${statusText}`;
22
+ if (body) {
23
+ // Try to extract a cleaner error message from JSON body
24
+ try {
25
+ const parsed = JSON.parse(body);
26
+ if (parsed.error) {
27
+ message = parsed.error;
28
+ }
29
+ else if (parsed.message) {
30
+ message = parsed.message;
31
+ }
32
+ }
33
+ catch {
34
+ // If not JSON, append raw body if short
35
+ if (body.length < 200) {
36
+ message += ` - ${body}`;
37
+ }
38
+ }
39
+ }
40
+ }
41
+ super(message);
42
+ this.name = "ApiError";
43
+ this.status = status;
44
+ this.isAuthError = status === 401 || status === 403;
45
+ }
46
+ }
47
+ /**
48
+ * API Client for CLI to communicate with Supatest backend
49
+ */
50
+ export class ApiClient {
51
+ apiUrl;
52
+ apiKey;
53
+ constructor(apiUrl, apiKey) {
54
+ this.apiUrl = apiUrl;
55
+ this.apiKey = apiKey;
56
+ }
57
+ /**
58
+ * Update the API key (used when user logs in during session)
59
+ */
60
+ setApiKey(apiKey) {
61
+ this.apiKey = apiKey;
62
+ }
63
+ /**
64
+ * Clear the API key (used when user logs out)
65
+ */
66
+ clearApiKey() {
67
+ this.apiKey = undefined;
68
+ }
69
+ /**
70
+ * Check if the client has an API key set
71
+ */
72
+ hasApiKey() {
73
+ return !!this.apiKey;
74
+ }
75
+ /**
76
+ * Create a new CLI session on the backend
77
+ * @param title - The session title
78
+ * @param originMetadata - Optional metadata about the session origin
79
+ * @returns The session ID and web URL
80
+ */
81
+ async createSession(title, originMetadata) {
82
+ const url = `${this.apiUrl}/v1/agent/sessions`;
83
+ const response = await fetch(url, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ Authorization: `Bearer ${this.apiKey}`,
88
+ },
89
+ body: JSON.stringify({
90
+ title,
91
+ originMetadata,
92
+ }),
93
+ });
94
+ if (!response.ok) {
95
+ const errorText = await response.text();
96
+ throw new ApiError(response.status, response.statusText, errorText);
97
+ }
98
+ const data = await response.json();
99
+ return data;
100
+ }
101
+ /**
102
+ * Stream an event to the backend
103
+ * @param sessionId - The session ID
104
+ * @param event - The CLI event to stream
105
+ * @returns Success status
106
+ */
107
+ async streamEvent(sessionId, event) {
108
+ const url = `${this.apiUrl}/v1/agent/sessions/${sessionId}/events`;
109
+ const response = await fetch(url, {
110
+ method: "POST",
111
+ headers: {
112
+ "Content-Type": "application/json",
113
+ Authorization: `Bearer ${this.apiKey}`,
114
+ },
115
+ body: JSON.stringify(event),
116
+ });
117
+ if (!response.ok) {
118
+ const errorText = await response.text();
119
+ throw new ApiError(response.status, response.statusText, errorText);
120
+ }
121
+ const data = await response.json();
122
+ return data;
123
+ }
124
+ /**
125
+ * Get session details from the backend
126
+ * @param sessionId - The session ID
127
+ * @returns Session details
128
+ */
129
+ async getSession(sessionId) {
130
+ const url = `${this.apiUrl}/v1/agent/sessions/${sessionId}`;
131
+ const response = await fetch(url, {
132
+ method: "GET",
133
+ headers: {
134
+ Authorization: `Bearer ${this.apiKey}`,
135
+ },
136
+ });
137
+ if (!response.ok) {
138
+ const errorText = await response.text();
139
+ throw new ApiError(response.status, response.statusText, errorText);
140
+ }
141
+ return await response.json();
142
+ }
143
+ /**
144
+ * Get paginated sessions accessible to the user
145
+ * @param limit - Maximum number of sessions to return
146
+ * @param offset - Number of sessions to skip
147
+ * @returns Paginated sessions with total count
148
+ */
149
+ async getSessions(limit, offset) {
150
+ const urlParams = new URLSearchParams({
151
+ limit: limit.toString(),
152
+ offset: offset.toString(),
153
+ });
154
+ const url = `${this.apiUrl}/v1/sessions?${urlParams.toString()}`;
155
+ logger.debug(`Fetching sessions: ${url}`);
156
+ const response = await fetch(url, {
157
+ method: "GET",
158
+ headers: {
159
+ Authorization: `Bearer ${this.apiKey}`,
160
+ },
161
+ });
162
+ if (!response.ok) {
163
+ const errorText = await response.text();
164
+ throw new ApiError(response.status, response.statusText, errorText);
165
+ }
166
+ const data = await response.json();
167
+ logger.debug(`Fetched ${data.sessions.length} sessions (${data.pagination.offset + 1}-${data.pagination.offset + data.sessions.length} of ${data.pagination.total})`);
168
+ return data;
169
+ }
170
+ /**
171
+ * Complete usage tracking for a message turn
172
+ * @param messageId - The assistant message ID
173
+ * @returns Success status
174
+ */
175
+ async completeUsage(messageId) {
176
+ const url = `${this.apiUrl}/v1/usage/complete`;
177
+ const response = await fetch(url, {
178
+ method: "POST",
179
+ headers: {
180
+ "Content-Type": "application/json",
181
+ Authorization: `Bearer ${this.apiKey}`,
182
+ },
183
+ body: JSON.stringify({ messageId }),
184
+ });
185
+ if (!response.ok) {
186
+ const errorText = await response.text();
187
+ logger.warn(`Failed to complete usage tracking: ${response.status} ${response.statusText} - ${errorText}`);
188
+ // Don't throw - this is best effort
189
+ return { success: false };
190
+ }
191
+ const data = await response.json();
192
+ return data;
193
+ }
194
+ /**
195
+ * Get messages for a session
196
+ * @param sessionId - The session ID
197
+ * @returns Messages with pagination info
198
+ */
199
+ async getSessionMessages(sessionId) {
200
+ const url = `${this.apiUrl}/v1/sessions/${sessionId}/messages`;
201
+ logger.debug(`Fetching messages for session: ${sessionId}`);
202
+ const response = await fetch(url, {
203
+ method: "GET",
204
+ headers: {
205
+ Authorization: `Bearer ${this.apiKey}`,
206
+ },
207
+ });
208
+ if (!response.ok) {
209
+ const errorText = await response.text();
210
+ throw new ApiError(response.status, response.statusText, errorText);
211
+ }
212
+ const data = await response.json();
213
+ logger.debug(`Fetched ${data.messages.length} messages for session: ${sessionId}`);
214
+ return data;
215
+ }
216
+ /**
217
+ * Update a session (title, providerSessionId)
218
+ * @param sessionId - The session ID
219
+ * @param data - The data to update
220
+ * @returns Updated session
221
+ */
222
+ async updateSession(sessionId, data) {
223
+ const url = `${this.apiUrl}/v1/sessions/${sessionId}`;
224
+ logger.debug(`Updating session: ${sessionId}`, {
225
+ hasTitle: !!data.title,
226
+ hasProviderSessionId: !!data.providerSessionId,
227
+ });
228
+ const response = await fetch(url, {
229
+ method: "PATCH",
230
+ headers: {
231
+ "Content-Type": "application/json",
232
+ Authorization: `Bearer ${this.apiKey}`,
233
+ },
234
+ body: JSON.stringify(data),
235
+ });
236
+ if (!response.ok) {
237
+ const errorText = await response.text();
238
+ throw new ApiError(response.status, response.statusText, errorText);
239
+ }
240
+ const result = await response.json();
241
+ logger.debug(`Session updated: ${sessionId}`);
242
+ return result;
243
+ }
244
+ }
@@ -0,0 +1,130 @@
1
+ import { logger } from "../utils/logger";
2
+ const BATCH_SIZE = 10; // Send events every 10 events
3
+ const BATCH_INTERVAL_MS = 100; // Or every 100ms, whichever comes first
4
+ const MAX_RETRY_ATTEMPTS = 3;
5
+ const RETRY_DELAY_MS = 1000; // Start with 1s delay
6
+ /**
7
+ * Event Streamer - Batches and streams CLI events to the backend with retry logic
8
+ */
9
+ export class EventStreamer {
10
+ apiClient;
11
+ sessionId;
12
+ eventQueue = [];
13
+ batchTimer = null;
14
+ isFlushing = false;
15
+ isShutdown = false;
16
+ constructor(apiClient, sessionId) {
17
+ this.apiClient = apiClient;
18
+ this.sessionId = sessionId;
19
+ }
20
+ /**
21
+ * Queue an event for streaming
22
+ * @param event - The CLI event to queue
23
+ */
24
+ async queueEvent(event) {
25
+ if (this.isShutdown) {
26
+ logger.warn("EventStreamer is shutdown, cannot queue event");
27
+ return;
28
+ }
29
+ // Send streaming delta events immediately for real-time updates
30
+ // Batch other events to reduce API calls
31
+ if (event.type === "assistant_text" || event.type === "assistant_thinking") {
32
+ await this.sendEventWithRetry(event);
33
+ return;
34
+ }
35
+ this.eventQueue.push(event);
36
+ // Flush if we've reached batch size
37
+ if (this.eventQueue.length >= BATCH_SIZE) {
38
+ await this.flush();
39
+ }
40
+ else {
41
+ // Reset the batch timer
42
+ this.resetBatchTimer();
43
+ }
44
+ }
45
+ /**
46
+ * Reset the batch timer
47
+ */
48
+ resetBatchTimer() {
49
+ if (this.batchTimer) {
50
+ clearTimeout(this.batchTimer);
51
+ }
52
+ this.batchTimer = setTimeout(() => {
53
+ this.flush().catch((error) => {
54
+ logger.error(`Failed to flush events on timer: ${error.message}`);
55
+ });
56
+ }, BATCH_INTERVAL_MS);
57
+ }
58
+ /**
59
+ * Flush all queued events to the backend
60
+ */
61
+ async flush() {
62
+ if (this.isFlushing || this.eventQueue.length === 0) {
63
+ return;
64
+ }
65
+ this.isFlushing = true;
66
+ // Clear the batch timer
67
+ if (this.batchTimer) {
68
+ clearTimeout(this.batchTimer);
69
+ this.batchTimer = null;
70
+ }
71
+ // Take all events from the queue
72
+ const eventsToSend = [...this.eventQueue];
73
+ this.eventQueue = [];
74
+ // Send each event with retry logic
75
+ for (const event of eventsToSend) {
76
+ await this.sendEventWithRetry(event);
77
+ }
78
+ this.isFlushing = false;
79
+ }
80
+ /**
81
+ * Send a single event with retry logic
82
+ * @param event - The event to send
83
+ */
84
+ async sendEventWithRetry(event) {
85
+ let attempts = 0;
86
+ let lastError = null;
87
+ while (attempts < MAX_RETRY_ATTEMPTS) {
88
+ try {
89
+ await this.apiClient.streamEvent(this.sessionId, event);
90
+ return; // Success
91
+ }
92
+ catch (error) {
93
+ lastError = error;
94
+ attempts++;
95
+ if (attempts < MAX_RETRY_ATTEMPTS) {
96
+ const delay = RETRY_DELAY_MS * Math.pow(2, attempts - 1); // Exponential backoff
97
+ logger.warn(`Failed to stream event (attempt ${attempts}/${MAX_RETRY_ATTEMPTS}), retrying in ${delay}ms...`);
98
+ await this.sleep(delay);
99
+ }
100
+ }
101
+ }
102
+ // All retries failed
103
+ logger.error(`Failed to stream event after ${MAX_RETRY_ATTEMPTS} attempts: ${lastError?.message}`);
104
+ // Don't throw - continue processing other events
105
+ // The event is lost, but we don't want to crash the CLI
106
+ }
107
+ /**
108
+ * Sleep for a given duration
109
+ * @param ms - Duration in milliseconds
110
+ */
111
+ sleep(ms) {
112
+ return new Promise((resolve) => setTimeout(resolve, ms));
113
+ }
114
+ /**
115
+ * Shutdown the event streamer and flush any remaining events
116
+ */
117
+ async shutdown() {
118
+ if (this.isShutdown) {
119
+ return;
120
+ }
121
+ this.isShutdown = true;
122
+ // Clear the batch timer
123
+ if (this.batchTimer) {
124
+ clearTimeout(this.batchTimer);
125
+ this.batchTimer = null;
126
+ }
127
+ // Flush any remaining events
128
+ await this.flush();
129
+ }
130
+ }
package/dist/ui/App.js ADDED
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Main App Component
3
+ * Root component for the interactive CLI UI
4
+ */
5
+ import { execSync } from "node:child_process";
6
+ import { homedir } from "node:os";
7
+ import { Box, Text, useApp, useInput } from "ink";
8
+ import React, { useEffect, useRef, useState } from "react";
9
+ import { loginCommand } from "../commands/login.js";
10
+ import { setupCommand } from "../commands/setup.js";
11
+ import { isLoggedIn, removeToken, saveToken, } from "../utils/token-storage.js";
12
+ import { AuthBanner } from "./components/AuthBanner.js";
13
+ import { AuthDialog } from "./components/AuthDialog.js";
14
+ import { HelpMenu } from "./components/HelpMenu.js";
15
+ import { InputPrompt } from "./components/InputPrompt.js";
16
+ import { MessageList } from "./components/MessageList.js";
17
+ import { QueuedMessageDisplay } from "./components/QueuedMessageDisplay.js";
18
+ import { SessionSelector } from "./components/SessionSelector.js";
19
+ import { useSession } from "./contexts/SessionContext.js";
20
+ import { useModeToggle } from "./hooks/useModeToggle.js";
21
+ import { AuthState } from "./types/auth.js";
22
+ const getGitBranch = () => {
23
+ try {
24
+ return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
25
+ }
26
+ catch {
27
+ return "";
28
+ }
29
+ };
30
+ const getCurrentFolder = () => {
31
+ const cwd = process.cwd();
32
+ const home = homedir();
33
+ if (cwd.startsWith(home)) {
34
+ return `~${cwd.slice(home.length)}`;
35
+ }
36
+ return cwd;
37
+ };
38
+ /**
39
+ * Main app content (inside SessionProvider)
40
+ */
41
+ const AppContent = ({ config, sessionId, webUrl, queuedTasks = [], onExit, onSubmitTask, apiClient, onResumeSession }) => {
42
+ const { exit } = useApp();
43
+ const { addMessage, clearMessages, isAgentRunning, messages, setSessionId, setWebUrl, setShouldInterruptAgent, toggleAllToolOutputs, allToolsExpanded } = useSession();
44
+ // Enable Shift+Tab mode toggle
45
+ useModeToggle();
46
+ const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
47
+ const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows || 24);
48
+ const [showHelp, setShowHelp] = useState(false);
49
+ const [showInput, setShowInput] = useState(true); // Always show input
50
+ const [gitBranch] = useState(() => getGitBranch());
51
+ const [currentFolder] = useState(() => getCurrentFolder());
52
+ const [hasInputContent, setHasInputContent] = useState(false);
53
+ const [exitWarning, setExitWarning] = useState(null);
54
+ const inputPromptRef = useRef(null);
55
+ const [showSessionSelector, setShowSessionSelector] = useState(false);
56
+ // Auth state
57
+ const [authState, setAuthState] = useState(() => isLoggedIn() ? AuthState.Authenticated : AuthState.Unauthenticated);
58
+ const [showAuthDialog, setShowAuthDialog] = useState(false);
59
+ // Show auth dialog on first start if unauthenticated and no API key provided
60
+ useEffect(() => {
61
+ if (!isLoggedIn() && !config.supatestApiKey) {
62
+ setShowAuthDialog(true);
63
+ }
64
+ }, [config.supatestApiKey]);
65
+ // Initialize session data
66
+ useEffect(() => {
67
+ if (sessionId) {
68
+ setSessionId(sessionId);
69
+ }
70
+ if (webUrl) {
71
+ setWebUrl(webUrl);
72
+ }
73
+ }, [sessionId, webUrl, setSessionId, setWebUrl]);
74
+ // Handle login flow
75
+ const handleLogin = async () => {
76
+ setShowAuthDialog(false);
77
+ setAuthState(AuthState.Authenticating);
78
+ addMessage({
79
+ type: "assistant",
80
+ content: "Opening browser for authentication...",
81
+ });
82
+ try {
83
+ const result = await loginCommand();
84
+ saveToken(result.token, result.expiresAt);
85
+ // Update ApiClient with the new token
86
+ if (apiClient) {
87
+ apiClient.setApiKey(result.token);
88
+ }
89
+ setAuthState(AuthState.Authenticated);
90
+ addMessage({
91
+ type: "assistant",
92
+ content: "Successfully logged in!",
93
+ });
94
+ }
95
+ catch (error) {
96
+ setAuthState(AuthState.Unauthenticated);
97
+ addMessage({
98
+ type: "error",
99
+ content: `Login failed: ${error instanceof Error ? error.message : "Unknown error"}`,
100
+ errorType: "error",
101
+ });
102
+ }
103
+ };
104
+ // Handle task submission from input prompt
105
+ const handleSubmitTask = async (task) => {
106
+ const trimmedTask = task.trim();
107
+ // Handle Slash Commands
108
+ if (trimmedTask.startsWith("/")) {
109
+ const command = trimmedTask.toLowerCase();
110
+ if (command === "/clear") {
111
+ clearMessages();
112
+ return;
113
+ }
114
+ if (command === "/exit") {
115
+ exit();
116
+ onExit(true);
117
+ return;
118
+ }
119
+ if (command === "/help" || command === "/?") {
120
+ setShowHelp((prev) => !prev);
121
+ return;
122
+ }
123
+ if (command === "/login") {
124
+ if (authState === AuthState.Authenticated) {
125
+ addMessage({
126
+ type: "assistant",
127
+ content: "Already logged in. Use /logout first to log out.",
128
+ });
129
+ return;
130
+ }
131
+ setShowAuthDialog(true);
132
+ return;
133
+ }
134
+ if (command === "/logout") {
135
+ if (authState !== AuthState.Authenticated) {
136
+ addMessage({
137
+ type: "assistant",
138
+ content: "Not currently logged in.",
139
+ });
140
+ return;
141
+ }
142
+ removeToken();
143
+ // Clear ApiClient token to prevent further API calls
144
+ if (apiClient) {
145
+ apiClient.clearApiKey();
146
+ }
147
+ setAuthState(AuthState.Unauthenticated);
148
+ addMessage({
149
+ type: "assistant",
150
+ content: "Successfully logged out!",
151
+ });
152
+ return;
153
+ }
154
+ if (command === "/resume") {
155
+ if (!apiClient) {
156
+ addMessage({
157
+ type: "error",
158
+ content: "API client not available. Cannot fetch sessions.",
159
+ errorType: "error",
160
+ });
161
+ return;
162
+ }
163
+ setShowSessionSelector(true);
164
+ return;
165
+ }
166
+ if (command === "/setup") {
167
+ addMessage({
168
+ type: "assistant",
169
+ content: "Running setup...",
170
+ });
171
+ try {
172
+ const result = await setupCommand();
173
+ addMessage({
174
+ type: "assistant",
175
+ content: result.output,
176
+ });
177
+ }
178
+ catch (error) {
179
+ addMessage({
180
+ type: "error",
181
+ content: `Setup failed: ${error instanceof Error ? error.message : String(error)}`,
182
+ errorType: "error",
183
+ });
184
+ }
185
+ return;
186
+ }
187
+ }
188
+ // Check authentication before allowing task submission
189
+ if (authState !== AuthState.Authenticated) {
190
+ addMessage({
191
+ type: "error",
192
+ content: "Authentication required. Please login to continue.",
193
+ errorType: "warning",
194
+ });
195
+ setShowAuthDialog(true);
196
+ return;
197
+ }
198
+ setHasInputContent(false); // Reset content flag
199
+ setExitWarning(null);
200
+ // Don't add message here - let InteractiveApp handle it
201
+ // addMessage({
202
+ // type: "user",
203
+ // content: task,
204
+ // });
205
+ if (onSubmitTask) {
206
+ onSubmitTask(task);
207
+ }
208
+ };
209
+ // Handle session selection
210
+ const handleSessionSelect = (session) => {
211
+ setShowSessionSelector(false);
212
+ addMessage({
213
+ type: "assistant",
214
+ content: `Switching to session: ${session.title || "Untitled"} (${session.id})`,
215
+ });
216
+ if (onResumeSession) {
217
+ onResumeSession(session);
218
+ }
219
+ };
220
+ const handleSessionSelectorCancel = () => {
221
+ setShowSessionSelector(false);
222
+ };
223
+ // Handle terminal resize
224
+ useEffect(() => {
225
+ const handleResize = () => {
226
+ setTerminalWidth(process.stdout.columns || 80);
227
+ setTerminalHeight(process.stdout.rows || 24);
228
+ };
229
+ process.stdout.on("resize", handleResize);
230
+ return () => {
231
+ process.stdout.off("resize", handleResize);
232
+ };
233
+ }, []);
234
+ // Keyboard shortcuts
235
+ useInput((input, key) => {
236
+ // ESC: Cancel current request (abort running operations)
237
+ if (key.escape && isAgentRunning) {
238
+ setShouldInterruptAgent(true);
239
+ addMessage({
240
+ type: "error",
241
+ content: "Request cancelled",
242
+ errorType: "info",
243
+ });
244
+ }
245
+ // Ctrl+C: Cancel if running, or exit with warning
246
+ if (key.ctrl && input === "c") {
247
+ if (isAgentRunning) {
248
+ // Cancel the running request (same as Escape)
249
+ setShouldInterruptAgent(true);
250
+ addMessage({
251
+ type: "error",
252
+ content: "Request cancelled",
253
+ errorType: "info",
254
+ });
255
+ // Show exit warning
256
+ setExitWarning("Press Ctrl+C again to exit");
257
+ setTimeout(() => setExitWarning(null), 1500);
258
+ }
259
+ else if (exitWarning) {
260
+ // Second Ctrl+C within warning period - exit
261
+ exit();
262
+ onExit(true);
263
+ }
264
+ else if (showInput && hasInputContent) {
265
+ // Clear input and show warning
266
+ inputPromptRef.current?.clear();
267
+ setExitWarning("Press Ctrl+C again to exit");
268
+ setTimeout(() => setExitWarning(null), 1500);
269
+ }
270
+ else {
271
+ // Nothing running, no input - show warning first
272
+ setExitWarning("Press Ctrl+C again to exit");
273
+ setTimeout(() => setExitWarning(null), 1500);
274
+ }
275
+ }
276
+ // Ctrl+D: Exit immediately
277
+ if (key.ctrl && input === "d") {
278
+ exit();
279
+ onExit(true);
280
+ }
281
+ // Ctrl+H: Toggle help (removing '?' check here as it conflicts with input)
282
+ if (key.ctrl && input === "h") {
283
+ setShowHelp((prev) => !prev);
284
+ }
285
+ // Ctrl+L: Clear screen (not messages, just terminal)
286
+ if (key.ctrl && input === "l") {
287
+ console.clear();
288
+ }
289
+ // Ctrl+O: Toggle all tool outputs
290
+ if (key.ctrl && input === "o") {
291
+ toggleAllToolOutputs();
292
+ }
293
+ });
294
+ // Show initial user message if task was provided
295
+ useEffect(() => {
296
+ if (config.task) {
297
+ addMessage({
298
+ type: "user",
299
+ content: config.task,
300
+ });
301
+ }
302
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
303
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 0, flexShrink: 0, height: terminalHeight, overflow: "hidden", paddingX: 1 },
304
+ React.createElement(MessageList, { currentFolder: currentFolder, gitBranch: gitBranch, terminalWidth: terminalWidth }),
305
+ showHelp && React.createElement(HelpMenu, { isAuthenticated: authState === AuthState.Authenticated }),
306
+ showSessionSelector && apiClient && (React.createElement(SessionSelector, { apiClient: apiClient, onCancel: handleSessionSelectorCancel, onSelect: handleSessionSelect })),
307
+ showAuthDialog && (React.createElement(AuthDialog, { onLogin: handleLogin })),
308
+ React.createElement(Box, { flexDirection: "column", flexGrow: 0, flexShrink: 0 },
309
+ React.createElement(QueuedMessageDisplay, { messageQueue: queuedTasks }),
310
+ !showAuthDialog && React.createElement(AuthBanner, { authState: authState }),
311
+ showInput && !showSessionSelector && !showAuthDialog && (React.createElement(Box, { flexDirection: "column", marginTop: 0, width: "100%" },
312
+ exitWarning && (React.createElement(Box, { marginBottom: 0, paddingX: 1 },
313
+ React.createElement(Text, { color: "yellow" }, exitWarning))),
314
+ React.createElement(InputPrompt, { currentFolder: currentFolder, gitBranch: gitBranch, onHelpToggle: () => setShowHelp(prev => !prev), onInputChange: (val) => setHasInputContent(val.trim().length > 0), onSubmit: handleSubmitTask, placeholder: "Enter your task...", ref: inputPromptRef }))))));
315
+ };
316
+ /**
317
+ * Root App component
318
+ * Note: SessionProvider should be provided by the parent component
319
+ */
320
+ export const App = (props) => {
321
+ return React.createElement(AppContent, { ...props });
322
+ };