wave-code 0.9.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,270 @@
1
+ import { Agent as WaveAgent, AgentOptions } from "wave-agent-sdk";
2
+ import { logger } from "../utils/logger.js";
3
+ import type {
4
+ Agent as AcpAgent,
5
+ AgentSideConnection,
6
+ InitializeResponse,
7
+ NewSessionRequest,
8
+ NewSessionResponse,
9
+ LoadSessionRequest,
10
+ LoadSessionResponse,
11
+ PromptRequest,
12
+ PromptResponse,
13
+ CancelNotification,
14
+ AuthenticateResponse,
15
+ SessionId as AcpSessionId,
16
+ ToolCallStatus,
17
+ StopReason,
18
+ } from "@agentclientprotocol/sdk";
19
+
20
+ export class WaveAcpAgent implements AcpAgent {
21
+ private agents: Map<string, WaveAgent> = new Map();
22
+ private connection: AgentSideConnection;
23
+
24
+ constructor(connection: AgentSideConnection) {
25
+ this.connection = connection;
26
+ }
27
+
28
+ async initialize(): Promise<InitializeResponse> {
29
+ logger.info("Initializing WaveAcpAgent");
30
+ return {
31
+ protocolVersion: 1,
32
+ agentInfo: {
33
+ name: "wave-agent",
34
+ version: "0.1.0",
35
+ },
36
+ agentCapabilities: {
37
+ loadSession: true,
38
+ },
39
+ };
40
+ }
41
+
42
+ async authenticate(): Promise<AuthenticateResponse | void> {
43
+ // No authentication required for now
44
+ }
45
+
46
+ async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
47
+ const { cwd } = params;
48
+ logger.info(`Creating new session in ${cwd}`);
49
+ const callbacks: AgentOptions["callbacks"] = {};
50
+ const agent = await WaveAgent.create({
51
+ workdir: cwd,
52
+ callbacks: {
53
+ onAssistantContentUpdated: (chunk: string) =>
54
+ callbacks.onAssistantContentUpdated?.(chunk, ""),
55
+ onAssistantReasoningUpdated: (chunk: string) =>
56
+ callbacks.onAssistantReasoningUpdated?.(chunk, ""),
57
+ onToolBlockUpdated: (params: unknown) => {
58
+ const cb = callbacks.onToolBlockUpdated as
59
+ | ((params: unknown) => void)
60
+ | undefined;
61
+ cb?.(params);
62
+ },
63
+ onTasksChange: (tasks: unknown[]) => {
64
+ const cb = callbacks.onTasksChange as
65
+ | ((tasks: unknown[]) => void)
66
+ | undefined;
67
+ cb?.(tasks);
68
+ },
69
+ },
70
+ });
71
+
72
+ const sessionId = agent.sessionId;
73
+ logger.info(`New session created: ${sessionId}`);
74
+ this.agents.set(sessionId, agent);
75
+
76
+ // Update the callbacks object with the correct sessionId
77
+ Object.assign(callbacks, this.createCallbacks(sessionId));
78
+
79
+ return {
80
+ sessionId: sessionId as AcpSessionId,
81
+ };
82
+ }
83
+
84
+ async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
85
+ const { sessionId } = params;
86
+ logger.info(`Loading session: ${sessionId}`);
87
+ const callbacks: AgentOptions["callbacks"] = {};
88
+ const agent = await WaveAgent.create({
89
+ restoreSessionId: sessionId,
90
+ callbacks: {
91
+ onAssistantContentUpdated: (chunk: string) =>
92
+ callbacks.onAssistantContentUpdated?.(chunk, ""),
93
+ onAssistantReasoningUpdated: (chunk: string) =>
94
+ callbacks.onAssistantReasoningUpdated?.(chunk, ""),
95
+ onToolBlockUpdated: (params: unknown) => {
96
+ const cb = callbacks.onToolBlockUpdated as
97
+ | ((params: unknown) => void)
98
+ | undefined;
99
+ cb?.(params);
100
+ },
101
+ onTasksChange: (tasks: unknown[]) => {
102
+ const cb = callbacks.onTasksChange as
103
+ | ((tasks: unknown[]) => void)
104
+ | undefined;
105
+ cb?.(tasks);
106
+ },
107
+ },
108
+ });
109
+
110
+ this.agents.set(sessionId, agent);
111
+ logger.info(`Session loaded: ${sessionId}`);
112
+
113
+ // Update the callbacks object with the correct sessionId
114
+ Object.assign(callbacks, this.createCallbacks(sessionId));
115
+
116
+ return {};
117
+ }
118
+
119
+ async prompt(params: PromptRequest): Promise<PromptResponse> {
120
+ const { sessionId, prompt } = params;
121
+ logger.info(`Received prompt for session ${sessionId}`);
122
+ const agent = this.agents.get(sessionId);
123
+ if (!agent) {
124
+ logger.error(`Session ${sessionId} not found`);
125
+ throw new Error(`Session ${sessionId} not found`);
126
+ }
127
+
128
+ // Map ACP prompt to Wave Agent sendMessage
129
+ const textContent = prompt
130
+ .filter((block) => block.type === "text")
131
+ .map((block) => (block as { text: string }).text)
132
+ .join("\n");
133
+
134
+ const images = prompt
135
+ .filter((block) => block.type === "image")
136
+ .map((block) => {
137
+ const img = block as { data: string; mimeType: string };
138
+ return {
139
+ path: `data:${img.mimeType};base64,${img.data}`,
140
+ mimeType: img.mimeType,
141
+ };
142
+ });
143
+
144
+ try {
145
+ logger.info(
146
+ `Sending message to agent: ${textContent.substring(0, 50)}...`,
147
+ );
148
+ await agent.sendMessage(
149
+ textContent,
150
+ images.length > 0 ? images : undefined,
151
+ );
152
+ // Force save session so it can be loaded later
153
+ await (
154
+ agent as unknown as {
155
+ messageManager: { saveSession: () => Promise<void> };
156
+ }
157
+ ).messageManager.saveSession();
158
+ logger.info(`Message sent successfully for session ${sessionId}`);
159
+ return {
160
+ stopReason: "end_turn" as StopReason,
161
+ };
162
+ } catch (error) {
163
+ if (error instanceof Error && error.message.includes("abort")) {
164
+ logger.info(`Message aborted for session ${sessionId}`);
165
+ return {
166
+ stopReason: "cancelled" as StopReason,
167
+ };
168
+ }
169
+ logger.error(`Error sending message for session ${sessionId}:`, error);
170
+ throw error;
171
+ }
172
+ }
173
+
174
+ async cancel(params: CancelNotification): Promise<void> {
175
+ const { sessionId } = params;
176
+ logger.info(`Cancelling message for session ${sessionId}`);
177
+ const agent = this.agents.get(sessionId);
178
+ if (agent) {
179
+ agent.abortMessage();
180
+ }
181
+ }
182
+
183
+ private createCallbacks(sessionId: string): AgentOptions["callbacks"] {
184
+ return {
185
+ onAssistantContentUpdated: (chunk: string) => {
186
+ this.connection.sessionUpdate({
187
+ sessionId: sessionId as AcpSessionId,
188
+ update: {
189
+ sessionUpdate: "agent_message_chunk",
190
+ content: {
191
+ type: "text",
192
+ text: chunk,
193
+ },
194
+ },
195
+ });
196
+ },
197
+ onAssistantReasoningUpdated: (chunk: string) => {
198
+ this.connection.sessionUpdate({
199
+ sessionId: sessionId as AcpSessionId,
200
+ update: {
201
+ sessionUpdate: "agent_thought_chunk",
202
+ content: {
203
+ type: "text",
204
+ text: chunk,
205
+ },
206
+ },
207
+ });
208
+ },
209
+ onToolBlockUpdated: (params) => {
210
+ const { id, name, stage, success, error, result } = params;
211
+
212
+ if (stage === "start") {
213
+ this.connection.sessionUpdate({
214
+ sessionId: sessionId as AcpSessionId,
215
+ update: {
216
+ sessionUpdate: "tool_call",
217
+ toolCallId: id,
218
+ title: name || "Tool Call",
219
+ status: "pending",
220
+ },
221
+ });
222
+ return;
223
+ }
224
+
225
+ if (stage === "streaming") {
226
+ // We don't support streaming tool arguments in ACP yet
227
+ return;
228
+ }
229
+
230
+ const status: ToolCallStatus =
231
+ stage === "end"
232
+ ? success
233
+ ? "completed"
234
+ : "failed"
235
+ : stage === "running"
236
+ ? "in_progress"
237
+ : "pending";
238
+
239
+ this.connection.sessionUpdate({
240
+ sessionId: sessionId as AcpSessionId,
241
+ update: {
242
+ sessionUpdate: "tool_call_update",
243
+ toolCallId: id,
244
+ status,
245
+ title: name || "Tool Call",
246
+ rawOutput: result || error,
247
+ },
248
+ });
249
+ },
250
+ onTasksChange: (tasks) => {
251
+ this.connection.sessionUpdate({
252
+ sessionId: sessionId as AcpSessionId,
253
+ update: {
254
+ sessionUpdate: "plan",
255
+ entries: tasks.map((task) => ({
256
+ content: task.subject,
257
+ status:
258
+ task.status === "completed"
259
+ ? "completed"
260
+ : task.status === "in_progress"
261
+ ? "in_progress"
262
+ : "pending",
263
+ priority: "medium",
264
+ })),
265
+ },
266
+ });
267
+ },
268
+ };
269
+ }
270
+ }
@@ -0,0 +1,28 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
3
+ import { WaveAcpAgent } from "./agent.js";
4
+ import { logger } from "../utils/logger.js";
5
+
6
+ export async function startAcpCli() {
7
+ // Redirect console.log to logger to avoid interfering with JSON-RPC over stdio
8
+ console.log = (...args: unknown[]) => {
9
+ logger.info(...args);
10
+ };
11
+
12
+ logger.info("Starting ACP bridge...");
13
+
14
+ // Convert Node.js stdio to Web streams
15
+ const stdin = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;
16
+ const stdout = Writable.toWeb(process.stdout) as WritableStream<Uint8Array>;
17
+
18
+ // Create ACP stream
19
+ const stream = ndJsonStream(stdout, stdin);
20
+
21
+ // Initialize AgentSideConnection
22
+ const connection = new AgentSideConnection((conn) => {
23
+ return new WaveAcpAgent(conn);
24
+ }, stream);
25
+
26
+ // Wait for connection to close
27
+ await connection.closed;
28
+ }
package/src/acp-cli.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { startAcpCli } from "./acp/index.js";
2
+
3
+ export async function runAcp() {
4
+ await startAcpCli();
5
+ }
@@ -43,8 +43,11 @@ export async function listMarketplacesCommand() {
43
43
  sourceInfo = source.url + (source.ref ? `#${source.ref}` : "");
44
44
  }
45
45
  const builtinLabel = m.isBuiltin ? " [builtin]" : "";
46
+ const lastUpdatedLabel = m.lastUpdated
47
+ ? ` (Last updated: ${new Date(m.lastUpdated).toLocaleString()})`
48
+ : "";
46
49
  console.log(
47
- `- ${m.name}${builtinLabel}: ${sourceInfo} (${m.source.source})`,
50
+ `- ${m.name}${builtinLabel}: ${sourceInfo} (${m.source.source})${lastUpdatedLabel}`,
48
51
  );
49
52
  });
50
53
  }
@@ -154,43 +154,87 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
154
154
 
155
155
  // Process line diffs
156
156
  const diffElements: React.ReactNode[] = [];
157
- lineDiffs.forEach((part, partIndex) => {
157
+ for (let i = 0; i < lineDiffs.length; i++) {
158
+ const part = lineDiffs[i];
158
159
  const lines = part.value.split("\n");
159
160
  // diffLines might return a trailing empty string if the content ends with a newline
160
161
  if (lines[lines.length - 1] === "") {
161
162
  lines.pop();
162
163
  }
163
164
 
164
- if (part.added) {
165
+ if (part.removed) {
166
+ // Look ahead for an added block
167
+ if (i + 1 < lineDiffs.length && lineDiffs[i + 1].added) {
168
+ const nextPart = lineDiffs[i + 1];
169
+ const addedLines = nextPart.value.split("\n");
170
+ if (addedLines[addedLines.length - 1] === "") {
171
+ addedLines.pop();
172
+ }
173
+
174
+ if (lines.length === addedLines.length) {
175
+ // Word-level diffing
176
+ lines.forEach((line, lineIndex) => {
177
+ const { removedParts, addedParts } = renderWordLevelDiff(
178
+ line,
179
+ addedLines[lineIndex],
180
+ `word-${changeIndex}-${i}-${lineIndex}`,
181
+ );
182
+ diffElements.push(
183
+ renderLine(
184
+ oldLineNum++,
185
+ null,
186
+ "-",
187
+ removedParts,
188
+ "red",
189
+ `remove-${changeIndex}-${i}-${lineIndex}`,
190
+ ),
191
+ );
192
+ diffElements.push(
193
+ renderLine(
194
+ null,
195
+ newLineNum++,
196
+ "+",
197
+ addedParts,
198
+ "green",
199
+ `add-${changeIndex}-${i}-${lineIndex}`,
200
+ ),
201
+ );
202
+ });
203
+ i++; // Skip the added block
204
+ continue;
205
+ }
206
+ }
207
+
208
+ // Fallback to standard removed rendering
165
209
  lines.forEach((line, lineIndex) => {
166
210
  diffElements.push(
167
211
  renderLine(
212
+ oldLineNum++,
168
213
  null,
169
- newLineNum++,
170
- "+",
214
+ "-",
171
215
  line,
172
- "green",
173
- `add-${changeIndex}-${partIndex}-${lineIndex}`,
216
+ "red",
217
+ `remove-${changeIndex}-${i}-${lineIndex}`,
174
218
  ),
175
219
  );
176
220
  });
177
- } else if (part.removed) {
221
+ } else if (part.added) {
178
222
  lines.forEach((line, lineIndex) => {
179
223
  diffElements.push(
180
224
  renderLine(
181
- oldLineNum++,
182
225
  null,
183
- "-",
226
+ newLineNum++,
227
+ "+",
184
228
  line,
185
- "red",
186
- `remove-${changeIndex}-${partIndex}-${lineIndex}`,
229
+ "green",
230
+ `add-${changeIndex}-${i}-${lineIndex}`,
187
231
  ),
188
232
  );
189
233
  });
190
234
  } else {
191
235
  // Context lines - show unchanged content
192
- const isFirstBlock = partIndex === 0;
193
- const isLastBlock = partIndex === lineDiffs.length - 1;
236
+ const isFirstBlock = i === 0;
237
+ const isLastBlock = i === lineDiffs.length - 1;
194
238
 
195
239
  let linesToDisplay = lines;
196
240
  let showEllipsisTop = false;
@@ -221,7 +265,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
221
265
 
222
266
  if (showEllipsisTop) {
223
267
  diffElements.push(
224
- <Box key={`ellipsis-top-${changeIndex}-${partIndex}`}>
268
+ <Box key={`ellipsis-top-${changeIndex}-${i}`}>
225
269
  <Text color="gray">{" ".repeat(maxDigits * 2 + 2)}...</Text>
226
270
  </Box>,
227
271
  );
@@ -239,7 +283,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
239
283
  oldLineNum += skipCount;
240
284
  newLineNum += skipCount;
241
285
  diffElements.push(
242
- <Box key={`ellipsis-mid-${changeIndex}-${partIndex}`}>
286
+ <Box key={`ellipsis-mid-${changeIndex}-${i}`}>
243
287
  <Text color="gray">
244
288
  {" ".repeat(maxDigits * 2 + 2)}...
245
289
  </Text>
@@ -254,7 +298,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
254
298
  " ",
255
299
  line,
256
300
  "white",
257
- `context-${changeIndex}-${partIndex}-${lineIndex}`,
301
+ `context-${changeIndex}-${i}-${lineIndex}`,
258
302
  ),
259
303
  );
260
304
  });
@@ -266,62 +310,14 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
266
310
  oldLineNum += skipCount;
267
311
  newLineNum += skipCount;
268
312
  diffElements.push(
269
- <Box key={`ellipsis-bottom-${changeIndex}-${partIndex}`}>
313
+ <Box key={`ellipsis-bottom-${changeIndex}-${i}`}>
270
314
  <Text color="gray">{" ".repeat(maxDigits * 2 + 2)}...</Text>
271
315
  </Box>,
272
316
  );
273
317
  }
274
318
  }
275
- });
276
-
277
- // If it's a single line change (one removed, one added), use word-level diff
278
- if (
279
- diffElements.length === 2 &&
280
- React.isValidElement(diffElements[0]) &&
281
- React.isValidElement(diffElements[1]) &&
282
- typeof diffElements[0].key === "string" &&
283
- diffElements[0].key.includes("remove-") &&
284
- typeof diffElements[1].key === "string" &&
285
- diffElements[1].key.includes("add-")
286
- ) {
287
- const removedText = extractTextFromElement(diffElements[0]);
288
- const addedText = extractTextFromElement(diffElements[1]);
289
- const oldLineNumVal = extractOldLineNumFromElement(diffElements[0]);
290
- const newLineNumVal = extractNewLineNumFromElement(diffElements[1]);
291
-
292
- if (removedText && addedText) {
293
- const { removedParts, addedParts } = renderWordLevelDiff(
294
- removedText,
295
- addedText,
296
- `word-${changeIndex}`,
297
- );
298
-
299
- allElements.push(
300
- renderLine(
301
- oldLineNumVal,
302
- null,
303
- "-",
304
- removedParts,
305
- "red",
306
- `word-diff-removed-${changeIndex}`,
307
- ),
308
- );
309
- allElements.push(
310
- renderLine(
311
- null,
312
- newLineNumVal,
313
- "+",
314
- addedParts,
315
- "green",
316
- `word-diff-added-${changeIndex}`,
317
- ),
318
- );
319
- } else {
320
- allElements.push(...diffElements);
321
- }
322
- } else {
323
- allElements.push(...diffElements);
324
319
  }
320
+ allElements.push(...diffElements);
325
321
  } catch (error) {
326
322
  console.warn(
327
323
  `Error rendering diff for change ${changeIndex}:`,
@@ -361,71 +357,3 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
361
357
  </Box>
362
358
  );
363
359
  };
364
-
365
- // Helper function to extract text content from a React element
366
- const extractTextFromElement = (element: React.ReactNode): string | null => {
367
- if (!React.isValidElement(element)) return null;
368
-
369
- // Navigate through Box -> Text structure
370
- // Our new structure is: Box -> Text (old), Text (new), Text (|), Text (prefix), Text (content)
371
- const children = (
372
- element.props as unknown as { children?: React.ReactNode[] }
373
- ).children;
374
- if (Array.isArray(children) && children.length >= 5) {
375
- const textElement = children[4]; // Fifth child should be the Text with content
376
- if (React.isValidElement(textElement)) {
377
- const textChildren = (textElement.props as Record<string, unknown>)
378
- .children;
379
- return Array.isArray(textChildren)
380
- ? textChildren.join("")
381
- : String(textChildren || "");
382
- }
383
- }
384
- return null;
385
- };
386
-
387
- const extractOldLineNumFromElement = (
388
- element: React.ReactNode,
389
- ): number | null => {
390
- if (!React.isValidElement(element)) return null;
391
- const children = (
392
- element.props as unknown as { children?: React.ReactNode[] }
393
- ).children;
394
- if (Array.isArray(children) && children.length >= 1) {
395
- const textElement = children[0];
396
- if (React.isValidElement(textElement)) {
397
- const textChildren = (textElement.props as Record<string, unknown>)
398
- .children;
399
- const val = (
400
- Array.isArray(textChildren)
401
- ? textChildren.join("")
402
- : String(textChildren || "")
403
- ).trim();
404
- return val ? parseInt(val, 10) : null;
405
- }
406
- }
407
- return null;
408
- };
409
-
410
- const extractNewLineNumFromElement = (
411
- element: React.ReactNode,
412
- ): number | null => {
413
- if (!React.isValidElement(element)) return null;
414
- const children = (
415
- element.props as unknown as { children?: React.ReactNode[] }
416
- ).children;
417
- if (Array.isArray(children) && children.length >= 2) {
418
- const textElement = children[1];
419
- if (React.isValidElement(textElement)) {
420
- const textChildren = (textElement.props as Record<string, unknown>)
421
- .children;
422
- const val = (
423
- Array.isArray(textChildren)
424
- ? textChildren.join("")
425
- : String(textChildren || "")
426
- ).trim();
427
- return val ? parseInt(val, 10) : null;
428
- }
429
- }
430
- return null;
431
- };
@@ -69,7 +69,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
69
69
  );
70
70
  }
71
71
 
72
- const maxDisplay = 10;
72
+ const maxDisplay = 5;
73
73
 
74
74
  // Calculate display window start and end positions
75
75
  const getDisplayWindow = () => {
@@ -77,20 +77,19 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
77
77
  0,
78
78
  Math.min(
79
79
  selectedIndex - Math.floor(maxDisplay / 2),
80
- files.length - maxDisplay,
80
+ Math.max(0, files.length - maxDisplay),
81
81
  ),
82
82
  );
83
83
  const endIndex = Math.min(files.length, startIndex + maxDisplay);
84
- const adjustedStartIndex = Math.max(0, endIndex - maxDisplay);
85
84
 
86
85
  return {
87
- startIndex: adjustedStartIndex,
88
- endIndex: endIndex,
89
- displayFiles: files.slice(adjustedStartIndex, endIndex),
86
+ startIndex,
87
+ endIndex,
88
+ displayFiles: files.slice(startIndex, endIndex),
90
89
  };
91
90
  };
92
91
 
93
- const { startIndex, endIndex, displayFiles } = getDisplayWindow();
92
+ const { startIndex, displayFiles } = getDisplayWindow();
94
93
 
95
94
  return (
96
95
  <Box
@@ -100,42 +99,36 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
100
99
  borderBottom={false}
101
100
  borderLeft={false}
102
101
  borderRight={false}
102
+ gap={1}
103
103
  >
104
- <Text color="cyan" bold>
105
- Select File/Directory {searchQuery && `(filtering: "${searchQuery}")`}
106
- </Text>
107
-
108
- {/* Show hint for more files above */}
109
- {startIndex > 0 && (
110
- <Text dimColor>... {startIndex} more files above</Text>
111
- )}
112
-
113
- {displayFiles.map((fileItem, displayIndex) => {
114
- const actualIndex = startIndex + displayIndex;
115
- const isSelected = actualIndex === selectedIndex;
116
-
117
- return (
118
- <Box key={fileItem.path}>
119
- <Text
120
- color={isSelected ? "black" : "white"}
121
- backgroundColor={isSelected ? "cyan" : undefined}
122
- >
123
- {fileItem.path}
124
- </Text>
125
- </Box>
126
- );
127
- })}
128
-
129
- {/* Show hint for more files below */}
130
- {endIndex < files.length && (
131
- <Text dimColor>... {files.length - endIndex} more files below</Text>
132
- )}
133
-
134
104
  <Box>
135
- <Text dimColor>
136
- Use ↑↓ to navigate, Enter/Tab to select, Escape to cancel
105
+ <Text color="cyan" bold>
106
+ Select File/Directory {searchQuery && `(filtering: "${searchQuery}")`}
137
107
  </Text>
138
108
  </Box>
109
+
110
+ <Box flexDirection="column">
111
+ {displayFiles.map((fileItem, displayIndex) => {
112
+ const actualIndex = startIndex + displayIndex;
113
+ const isSelected = actualIndex === selectedIndex;
114
+
115
+ return (
116
+ <Box key={fileItem.path}>
117
+ <Text
118
+ color={isSelected ? "black" : "white"}
119
+ backgroundColor={isSelected ? "cyan" : undefined}
120
+ >
121
+ {isSelected ? "▶ " : " "}
122
+ {fileItem.path}
123
+ </Text>
124
+ </Box>
125
+ );
126
+ })}
127
+ </Box>
128
+
129
+ <Box>
130
+ <Text dimColor>↑↓ navigate • Enter/Tab select • Esc cancel</Text>
131
+ </Box>
139
132
  </Box>
140
133
  );
141
134
  };