@cdoing/opentuicli 0.1.21 → 0.1.26

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.
@@ -1,28 +0,0 @@
1
- /**
2
- * LoadingSpinner — animated thinking/tool indicator
3
- */
4
-
5
- import { useState, useEffect } from "react";
6
- import { useTheme } from "../context/theme";
7
-
8
- const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
-
10
- export function LoadingSpinner(props: { label?: string }) {
11
- const { theme } = useTheme();
12
- const t = theme;
13
- const [frame, setFrame] = useState(0);
14
-
15
- useEffect(() => {
16
- const interval = setInterval(() => {
17
- setFrame((f) => (f + 1) % FRAMES.length);
18
- }, 80);
19
- return () => clearInterval(interval);
20
- }, []);
21
-
22
- return (
23
- <box paddingX={1} height={1} flexDirection="row">
24
- <text fg={t.primary}>{FRAMES[frame]}</text>
25
- <text fg={t.textMuted}>{` ${props.label || "Thinking..."}`}</text>
26
- </box>
27
- );
28
- }
@@ -1,546 +0,0 @@
1
- /**
2
- * MessageList — renders chat messages with tool calls, streaming, and markdown
3
- *
4
- * Uses a custom inline markdown renderer for assistant messages (matching the CLI's
5
- * RenderMarkdown approach) with OpenTUI's <markdown> component for fenced code blocks.
6
- * The scrollbox is managed by the parent (session.tsx) to ensure proper flex height
7
- * calculation (matching OpenCode's pattern).
8
- */
9
-
10
- import { TextAttributes } from "@opentui/core";
11
- import { useTheme, type Theme } from "../context/theme";
12
-
13
- // ── Types ──────────────────────────────────────────────
14
-
15
- export interface Message {
16
- id: string;
17
- role: "user" | "assistant" | "system" | "tool";
18
- content: string;
19
- toolName?: string;
20
- toolStatus?: "running" | "done" | "error";
21
- toolInput?: Record<string, any>;
22
- isError?: boolean;
23
- timestamp: number;
24
- }
25
-
26
- // ── Tool Config ───────────────────────────────────────
27
-
28
- interface ToolConfig {
29
- label: string;
30
- icon: string;
31
- verb: string;
32
- }
33
-
34
- const TOOL_CONFIG: Record<string, ToolConfig> = {
35
- file_read: { label: "Read", icon: "◇", verb: "Reading" },
36
- file_write: { label: "Write", icon: "◈", verb: "Writing" },
37
- file_edit: { label: "Edit", icon: "◈", verb: "Editing" },
38
- multi_edit: { label: "MultiEdit", icon: "◈", verb: "Editing" },
39
- apply_patch: { label: "Patch", icon: "◈", verb: "Patching" },
40
- shell_exec: { label: "Bash", icon: "$", verb: "Running" },
41
- file_run: { label: "Run", icon: "▶", verb: "Running" },
42
- glob_search: { label: "Search files", icon: "◎", verb: "Searching" },
43
- grep_search: { label: "Search", icon: "◎", verb: "Searching" },
44
- codebase_search: { label: "Codebase", icon: "◎", verb: "Searching" },
45
- web_fetch: { label: "Fetch", icon: "◌", verb: "Fetching" },
46
- web_search: { label: "Web Search", icon: "◌", verb: "Searching" },
47
- sub_agent: { label: "Agent", icon: "◆", verb: "Running" },
48
- todo: { label: "Todo", icon: "☐", verb: "Updating" },
49
- list_dir: { label: "List Dir", icon: "├", verb: "Listing" },
50
- view_diff: { label: "Diff", icon: "±", verb: "Viewing" },
51
- view_repo_map: { label: "Repo Map", icon: "⊞", verb: "Mapping" },
52
- code_verify: { label: "Verify", icon: "✓", verb: "Verifying" },
53
- system_info: { label: "System Info", icon: "i", verb: "Checking" },
54
- ast_edit: { label: "AST Edit", icon: "⌥", verb: "Editing" },
55
- notebook_edit: { label: "Notebook", icon: "⊡", verb: "Editing" },
56
- };
57
-
58
- // ── Inline Markdown Helpers ──────────────────────────────
59
-
60
- /** Strip markdown inline syntax markers: **bold** → bold, *italic* → italic, `code` → code */
61
- function stripInlineMarkdown(text: string): string {
62
- return text
63
- .replace(/`([^`]+)`/g, "$1")
64
- .replace(/\*\*([^*]+)\*\*/g, "$1")
65
- .replace(/(?<!\w)\*([^*]+)\*(?!\w)/g, "$1");
66
- }
67
-
68
- // ── Custom Markdown Renderer ─────────────────────────────
69
- // Renders markdown content using OpenTUI primitives with proper styling.
70
- // Strips markdown syntax (##, **, *, `, ---) and renders styled text.
71
- // Uses <markdown> component only for fenced code blocks (syntax highlighting).
72
-
73
- function RenderMarkdown(props: { text: string; theme: Theme }) {
74
- const t = props.theme;
75
- const { syntaxStyle } = useTheme();
76
- const lines = props.text.split("\n");
77
-
78
- const rendered: React.ReactNode[] = [];
79
- let i = 0;
80
-
81
- while (i < lines.length) {
82
- const line = lines[i];
83
-
84
- // ── Fenced code block — use OpenTUI <markdown> for syntax highlighting ──
85
- if (line.startsWith("```")) {
86
- const codeLines: string[] = [line];
87
- i++;
88
- while (i < lines.length) {
89
- codeLines.push(lines[i]);
90
- if (lines[i].startsWith("```")) {
91
- i++;
92
- break;
93
- }
94
- i++;
95
- }
96
- const codeBlock = codeLines.join("\n");
97
- rendered.push(
98
- <box key={`code-${i}`} marginY={0}>
99
- <markdown
100
- syntaxStyle={syntaxStyle}
101
- streaming={false}
102
- content={codeBlock}
103
- conceal={true}
104
- />
105
- </box>
106
- );
107
- continue;
108
- }
109
-
110
- // ── Headers ──
111
- if (line.startsWith("### ")) {
112
- rendered.push(
113
- <text key={i} fg={t.info} attributes={TextAttributes.BOLD}>
114
- {` ▸ ${stripInlineMarkdown(line.slice(4))}`}
115
- </text>
116
- );
117
- i++;
118
- continue;
119
- }
120
- if (line.startsWith("## ")) {
121
- rendered.push(
122
- <text key={i} fg={t.primary} attributes={TextAttributes.BOLD}>
123
- {` ▸▸ ${stripInlineMarkdown(line.slice(3))}`}
124
- </text>
125
- );
126
- i++;
127
- continue;
128
- }
129
- if (line.startsWith("# ")) {
130
- rendered.push(
131
- <text key={i} fg={t.primary} attributes={TextAttributes.BOLD}>
132
- {`▸▸▸ ${stripInlineMarkdown(line.slice(2))}`}
133
- </text>
134
- );
135
- i++;
136
- continue;
137
- }
138
-
139
- // ── Horizontal rule ──
140
- if (/^---+$/.test(line) || /^===+$/.test(line) || /^\*\*\*+$/.test(line)) {
141
- rendered.push(
142
- <text key={i} fg={t.textDim}>{"─".repeat(40)}</text>
143
- );
144
- i++;
145
- continue;
146
- }
147
-
148
- // ── Bullet list ──
149
- const bulletMatch = line.match(/^(\s*)[-*] (.*)/);
150
- if (bulletMatch) {
151
- const indent = bulletMatch[1] || "";
152
- const content = stripInlineMarkdown(bulletMatch[2]);
153
- rendered.push(
154
- <text key={i}>{`${indent}● ${content}`}</text>
155
- );
156
- i++;
157
- continue;
158
- }
159
-
160
- // ── Numbered list ──
161
- const numMatch = line.match(/^(\s*)(\d+)\. (.*)/);
162
- if (numMatch) {
163
- const content = stripInlineMarkdown(numMatch[3]);
164
- rendered.push(
165
- <text key={i}>{`${numMatch[1]}${numMatch[2]}. ${content}`}</text>
166
- );
167
- i++;
168
- continue;
169
- }
170
-
171
- // ── Blockquote ──
172
- if (line.startsWith("> ")) {
173
- rendered.push(
174
- <text key={i} fg={t.textMuted}>{`│ ${stripInlineMarkdown(line.slice(2))}`}</text>
175
- );
176
- i++;
177
- continue;
178
- }
179
-
180
- // ── Empty line ──
181
- if (!line.trim()) {
182
- rendered.push(<text key={i}>{" "}</text>);
183
- i++;
184
- continue;
185
- }
186
-
187
- // ── Plain text — strip markdown syntax ──
188
- rendered.push(
189
- <text key={i}>{stripInlineMarkdown(line)}</text>
190
- );
191
- i++;
192
- }
193
-
194
- return <box flexDirection="column">{rendered}</box>;
195
- }
196
-
197
- // ── Component ──────────────────────────────────────────
198
-
199
- function formatTimestamp(ts: number): string {
200
- const d = new Date(ts);
201
- let h = d.getHours();
202
- const m = d.getMinutes();
203
- const ampm = h >= 12 ? "PM" : "AM";
204
- h = h % 12 || 12;
205
- return `${h}:${m.toString().padStart(2, "0")} ${ampm}`;
206
- }
207
-
208
- /**
209
- * Renders message content only (no scrollbox wrapper).
210
- * Parent should wrap this in a <scrollbox> for proper flex height.
211
- */
212
- export function MessageList(props: {
213
- messages: Message[];
214
- streamingText?: string;
215
- isStreaming?: boolean;
216
- showTimestamps?: boolean;
217
- }) {
218
- const { theme } = useTheme();
219
- const t = theme;
220
-
221
- return (
222
- <>
223
- {/* Empty state */}
224
- {props.messages.length === 0 && !props.isStreaming && (
225
- <box paddingX={2} paddingY={1}>
226
- <text fg={t.textMuted}>
227
- {"Type a message to start chatting. Use / for commands, @ for context."}
228
- </text>
229
- </box>
230
- )}
231
-
232
- {/* Messages */}
233
- {props.messages.map((msg) => {
234
- if (msg.role === "user") {
235
- return (
236
- <box key={msg.id} paddingX={1} paddingY={0} flexDirection="row">
237
- <text fg={t.userText} attributes={TextAttributes.BOLD}>
238
- {"❯ "}
239
- </text>
240
- <text fg={t.userText} flexGrow={1}>{msg.content}</text>
241
- {props.showTimestamps && msg.timestamp && (
242
- <text fg={t.textDim}>{` ${formatTimestamp(msg.timestamp)}`}</text>
243
- )}
244
- </box>
245
- );
246
- }
247
-
248
- if (msg.role === "assistant") {
249
- return (
250
- <box key={msg.id} paddingLeft={1} marginTop={1} flexShrink={0} flexDirection="column">
251
- <box flexDirection="row">
252
- <text fg={t.primary} attributes={TextAttributes.BOLD}>
253
- {"◆ "}
254
- </text>
255
- {props.showTimestamps && msg.timestamp && (
256
- <text fg={t.textDim}>{` ${formatTimestamp(msg.timestamp)}`}</text>
257
- )}
258
- </box>
259
- <box paddingLeft={2}>
260
- <RenderMarkdown text={msg.content.trim()} theme={t} />
261
- </box>
262
- </box>
263
- );
264
- }
265
-
266
- if (msg.role === "system") {
267
- return (
268
- <box key={msg.id} paddingX={1} flexDirection="row">
269
- <text fg={t.systemText} flexGrow={1}>{`⚡ ${msg.content}`}</text>
270
- {props.showTimestamps && msg.timestamp && (
271
- <text fg={t.textDim}>{` ${formatTimestamp(msg.timestamp)}`}</text>
272
- )}
273
- </box>
274
- );
275
- }
276
-
277
- if (msg.role === "tool") {
278
- return (
279
- <ToolCallRow
280
- key={msg.id}
281
- name={msg.toolName || "unknown"}
282
- content={msg.content}
283
- status={msg.toolStatus || (msg.isError ? "error" : "done")}
284
- input={msg.toolInput}
285
- />
286
- );
287
- }
288
-
289
- return null;
290
- })}
291
-
292
- {/* Streaming indicator */}
293
- {props.isStreaming && (
294
- <box paddingLeft={1} marginTop={1} flexShrink={0} flexDirection="column">
295
- <box flexDirection="row">
296
- <text fg={t.primary} attributes={TextAttributes.BOLD}>
297
- {"◆ "}
298
- </text>
299
- <text fg={t.primary}>{"▊"}</text>
300
- </box>
301
- {(props.streamingText || "").trim() && (
302
- <box paddingLeft={2}>
303
- <RenderMarkdown text={(props.streamingText || "").trim()} theme={t} />
304
- </box>
305
- )}
306
- </box>
307
- )}
308
- </>
309
- );
310
- }
311
-
312
- // ── Tool Helpers ──────────────────────────────────────
313
-
314
- function trimText(s: string, max: number): string {
315
- const first = s.split("\n")[0] || "";
316
- return first.length > max ? first.substring(0, max) + "…" : first;
317
- }
318
-
319
- function shortPath(p: string): string {
320
- const home = process.env.HOME || "";
321
- let s = (home && p.startsWith(home)) ? "~" + p.slice(home.length) : p;
322
- // Show last 2 segments if too long
323
- if (s.length > 50) {
324
- const parts = s.split("/");
325
- s = "…/" + parts.slice(-2).join("/");
326
- }
327
- return s;
328
- }
329
-
330
- function countLines(s: string): number {
331
- return s ? s.split("\n").length : 0;
332
- }
333
-
334
- /** Extract a short description from tool input, per tool type */
335
- function getToolDescription(name: string, input?: Record<string, any>): string {
336
- if (!input) return "";
337
- switch (name) {
338
- case "file_read":
339
- return input.file_path ? shortPath(input.file_path) : "";
340
- case "file_write":
341
- return input.file_path ? shortPath(input.file_path) : "";
342
- case "file_edit":
343
- case "multi_edit":
344
- case "apply_patch":
345
- return input.file_path ? shortPath(input.file_path) : "";
346
- case "shell_exec":
347
- return input.command ? trimText(input.command, 55) : "";
348
- case "file_run":
349
- return input.file_path ? shortPath(input.file_path) : "";
350
- case "glob_search":
351
- return input.pattern ? `"${trimText(input.pattern, 40)}"` : "";
352
- case "grep_search":
353
- return input.pattern ? `"${trimText(input.pattern, 30)}"${input.path ? ` in ${shortPath(input.path)}` : ""}` : "";
354
- case "codebase_search":
355
- return input.query ? `"${trimText(input.query, 40)}"` : "";
356
- case "web_fetch":
357
- return input.url ? trimText(input.url, 50) : "";
358
- case "web_search":
359
- return input.query ? `"${trimText(input.query, 40)}"` : "";
360
- case "sub_agent":
361
- return input.description ? trimText(input.description, 50) : "";
362
- case "list_dir":
363
- return input.path ? shortPath(input.path) : "";
364
- default:
365
- return input.description || "";
366
- }
367
- }
368
-
369
- /** Get a summary of the output for the collapsed view */
370
- function getOutputSummary(name: string, output: string, isError?: boolean): string {
371
- if (!output) return "";
372
- if (isError) return trimText(output, 50);
373
-
374
- const lines = countLines(output);
375
- switch (name) {
376
- case "file_read":
377
- return `${lines} lines`;
378
- case "shell_exec":
379
- case "file_run":
380
- return lines > 1 ? `${lines} lines` : trimText(output, 40);
381
- case "grep_search":
382
- case "glob_search":
383
- case "codebase_search": {
384
- const results = output.split("\n").filter((l) => l.trim()).length;
385
- return `${results} result${results !== 1 ? "s" : ""}`;
386
- }
387
- case "file_write":
388
- case "file_edit":
389
- case "multi_edit":
390
- case "apply_patch":
391
- return output.includes("+") || output.includes("-") ? trimText(output, 40) : "done";
392
- default:
393
- return lines > 1 ? `${lines} lines` : trimText(output, 40);
394
- }
395
- }
396
-
397
- // ── Tool Call Row ──────────────────────────────────────
398
-
399
- function ToolCallRow(props: {
400
- name: string;
401
- content: string;
402
- status: "running" | "done" | "error";
403
- input?: Record<string, any>;
404
- }) {
405
- const { theme } = useTheme();
406
- const t = theme;
407
-
408
- const config = TOOL_CONFIG[props.name] || { label: props.name.replace(/_/g, " "), icon: "⚙", verb: "Running" };
409
- const isRunning = props.status === "running";
410
- const isError = props.status === "error";
411
-
412
- // Status indicator
413
- const statusIcon = isRunning ? "⟳" : isError ? "✗" : "✓";
414
- const statusColor = isRunning ? t.toolRunning : isError ? t.toolError : t.toolDone;
415
-
416
- // Description from input args
417
- const description = getToolDescription(props.name, props.input);
418
-
419
- // Output summary (shown after → arrow)
420
- const outputSummary = !isRunning ? getOutputSummary(props.name, props.content, isError) : "";
421
-
422
- // For shell commands, show the command inline
423
- const isShell = props.name === "shell_exec" || props.name === "file_run";
424
- const shellCmd = isShell && props.input?.command ? trimText(props.input.command, 55) : "";
425
-
426
- // For file ops, show the path
427
- const isFileOp = ["file_read", "file_write", "file_edit", "multi_edit", "apply_patch"].includes(props.name);
428
- const filePath = isFileOp && props.input?.file_path ? shortPath(props.input.file_path) : "";
429
-
430
- // For search ops, show the pattern
431
- const isSearch = ["grep_search", "glob_search", "codebase_search"].includes(props.name);
432
- const searchPattern = isSearch && (props.input?.pattern || props.input?.query) ?
433
- trimText(props.input?.pattern || props.input?.query, 35) : "";
434
-
435
- // Diff preview for edits
436
- const hasEditDiff = (props.name === "file_edit" || props.name === "multi_edit") &&
437
- props.input?.old_string && props.input?.new_string;
438
- const oldStr = hasEditDiff ? trimText(props.input!.old_string, 50) : "";
439
- const newStr = hasEditDiff ? trimText(props.input!.new_string, 50) : "";
440
-
441
- // Output content lines (for expanded view)
442
- const outputLines = props.content ? props.content.split("\n") : [];
443
- const maxPreviewLines = 6;
444
- const previewLines = outputLines.slice(0, maxPreviewLines);
445
- const hasMoreLines = outputLines.length > maxPreviewLines;
446
-
447
- return (
448
- <box flexDirection="column" paddingLeft={2} paddingRight={1}>
449
- {/* ── Header row: status + icon + label + description + output summary ── */}
450
- <box flexDirection="row" height={1}>
451
- <text fg={statusColor} attributes={isRunning ? TextAttributes.BOLD : undefined}>
452
- {`${statusIcon} `}
453
- </text>
454
- <text fg={t.textMuted}>{`${config.icon} `}</text>
455
- <text fg={isRunning ? t.toolRunning : t.text} attributes={TextAttributes.BOLD}>
456
- {isRunning ? `${config.verb}...` : config.label}
457
- </text>
458
-
459
- {/* Inline detail: file path, command, or search pattern */}
460
- {filePath && !isRunning ? (
461
- <text fg={t.info}>{` ${filePath}`}</text>
462
- ) : shellCmd ? (
463
- <text fg={isRunning ? t.textDim : t.textMuted}>{` $ ${shellCmd}`}</text>
464
- ) : searchPattern ? (
465
- <text fg={t.warning}>{` "${searchPattern}"`}</text>
466
- ) : description && !filePath && !shellCmd ? (
467
- <text fg={t.textDim}>{` ${description}`}</text>
468
- ) : null}
469
-
470
- {/* Output summary after arrow */}
471
- {outputSummary && !isRunning ? (
472
- <>
473
- <text fg={t.textDim}>{" → "}</text>
474
- <text fg={isError ? t.error : t.toolDone}>{outputSummary}</text>
475
- </>
476
- ) : null}
477
- </box>
478
-
479
- {/* ── Edit diff preview (for file_edit) ── */}
480
- {hasEditDiff && !isRunning && (
481
- <box flexDirection="column" paddingLeft={3}>
482
- <box flexDirection="row" height={1}>
483
- <text fg={t.diffRemove}>{` - ${oldStr}`}</text>
484
- </box>
485
- <box flexDirection="row" height={1}>
486
- <text fg={t.diffAdd}>{` + ${newStr}`}</text>
487
- </box>
488
- </box>
489
- )}
490
-
491
- {/* ── Shell output preview (for bash/run) ── */}
492
- {isShell && !isRunning && previewLines.length > 0 && (
493
- <box flexDirection="column" paddingLeft={3}>
494
- <box height={1}>
495
- <text fg={t.border}>{" ┌" + "─".repeat(40)}</text>
496
- </box>
497
- {previewLines.map((line, i) => (
498
- <box key={i} height={1}>
499
- <text fg={t.textDim}>{` │ ${trimText(line, 55)}`}</text>
500
- </box>
501
- ))}
502
- {hasMoreLines && (
503
- <box height={1}>
504
- <text fg={t.textDim}>{` │ … ${outputLines.length - maxPreviewLines} more lines`}</text>
505
- </box>
506
- )}
507
- <box height={1}>
508
- <text fg={t.border}>{" └" + "─".repeat(40)}</text>
509
- </box>
510
- </box>
511
- )}
512
-
513
- {/* ── Search results preview ── */}
514
- {isSearch && !isRunning && previewLines.length > 0 && (
515
- <box flexDirection="column" paddingLeft={3}>
516
- {previewLines.slice(0, 4).map((line, i) => (
517
- <box key={i} height={1}>
518
- <text fg={t.textDim}>{` ${trimText(line, 60)}`}</text>
519
- </box>
520
- ))}
521
- {outputLines.length > 4 && (
522
- <box height={1}>
523
- <text fg={t.textDim}>{` … ${outputLines.length - 4} more`}</text>
524
- </box>
525
- )}
526
- </box>
527
- )}
528
-
529
- {/* ── Error output ── */}
530
- {isError && props.content && (
531
- <box flexDirection="column" paddingLeft={3}>
532
- <box height={1}>
533
- <text fg={t.error}>{` ✗ ${trimText(props.content, 70)}`}</text>
534
- </box>
535
- </box>
536
- )}
537
-
538
- {/* ── Running indicator for shell commands ── */}
539
- {isShell && isRunning && (
540
- <box paddingLeft={3} height={1}>
541
- <text fg={t.toolRunning}>{" ▊ Executing command..."}</text>
542
- </box>
543
- )}
544
- </box>
545
- );
546
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * PermissionPrompt — asks user to allow/deny a tool action
3
- */
4
-
5
- import { TextAttributes } from "@opentui/core";
6
- import { useState } from "react";
7
- import { useKeyboard } from "@opentui/react";
8
- import { useTheme } from "../context/theme";
9
-
10
- export interface PermissionPromptProps {
11
- toolName: string;
12
- message: string;
13
- onDecision: (decision: "allow" | "always" | "deny") => void;
14
- }
15
-
16
- const OPTIONS = [
17
- { key: "1", label: "Allow once", value: "allow" as const },
18
- { key: "2", label: "Always allow", value: "always" as const },
19
- { key: "3", label: "Deny", value: "deny" as const },
20
- ];
21
-
22
- export function PermissionPrompt(props: PermissionPromptProps) {
23
- const { theme, customBg } = useTheme();
24
- const t = theme;
25
- const [selected, setSelected] = useState(0);
26
-
27
- useKeyboard((key: any) => {
28
- if (key.name === "up" || key.name === "k") {
29
- setSelected((s) => Math.max(0, s - 1));
30
- } else if (key.name === "down" || key.name === "j") {
31
- setSelected((s) => Math.min(OPTIONS.length - 1, s + 1));
32
- } else if (key.name === "return") {
33
- props.onDecision(OPTIONS[selected].value);
34
- } else if (key.name === "1") {
35
- props.onDecision("allow");
36
- } else if (key.name === "2") {
37
- props.onDecision("always");
38
- } else if (key.name === "3") {
39
- props.onDecision("deny");
40
- }
41
- });
42
-
43
- return (
44
- <box
45
- borderStyle="single"
46
- borderColor={t.warning}
47
- backgroundColor={customBg || t.bg}
48
- paddingX={1}
49
- paddingY={0}
50
- flexDirection="column"
51
- >
52
- <text fg={t.warning} attributes={TextAttributes.BOLD}>
53
- {"🔐 Permission Required"}
54
- </text>
55
- <text fg={t.text}>
56
- {` ${props.toolName}: ${props.message}`}
57
- </text>
58
- <text fg={t.textDim}>{""}</text>
59
- {OPTIONS.map((opt, i) => (
60
- <box key={opt.key}>
61
- <text
62
- fg={selected === i ? t.primary : t.textMuted}
63
- attributes={selected === i ? TextAttributes.BOLD : undefined}
64
- >
65
- {` ${selected === i ? "❯" : " "} [${opt.key}] ${opt.label}`}
66
- </text>
67
- </box>
68
- ))}
69
- <text fg={t.textDim}>{"\n ↑↓ Navigate Enter Select 1-3 Quick pick"}</text>
70
- </box>
71
- );
72
- }