@cdoing/opentuicli 0.1.2

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,204 @@
1
+ /**
2
+ * DialogCommand — command palette dialog (Ctrl+X)
3
+ *
4
+ * A centered modal with all available commands grouped by category,
5
+ * fuzzy search/filtering, keyboard shortcuts, and vim-style navigation.
6
+ */
7
+
8
+ import { TextAttributes } from "@opentui/core";
9
+ import { useState, useMemo, type ReactNode } from "react";
10
+ import { useKeyboard } from "@opentui/react";
11
+ import { useTheme } from "../context/theme";
12
+
13
+ // ── Command Definition ───────────────────────────────────
14
+
15
+ export interface Command {
16
+ id: string;
17
+ label: string;
18
+ shortcut?: string;
19
+ category: string;
20
+ }
21
+
22
+ const COMMANDS: Command[] = [
23
+ // Session
24
+ { id: "session:new", label: "New Session", shortcut: "Ctrl+N", category: "Session" },
25
+ { id: "session:browse", label: "Browse Sessions", shortcut: "Ctrl+S", category: "Session" },
26
+ { id: "session:clear", label: "Clear History", shortcut: "", category: "Session" },
27
+
28
+ // Model & Provider
29
+ { id: "model:switch", label: "Switch Model", shortcut: "Ctrl+P", category: "Model" },
30
+
31
+ // Theme & Appearance
32
+ { id: "theme:picker", label: "Browse Themes", shortcut: "Ctrl+T", category: "Appearance" },
33
+ { id: "theme:dark", label: "Dark Mode", shortcut: "", category: "Appearance" },
34
+ { id: "theme:light", label: "Light Mode", shortcut: "", category: "Appearance" },
35
+ { id: "display:sidebar", label: "Toggle Sidebar", shortcut: "Ctrl+B", category: "Appearance" },
36
+
37
+ // System
38
+ { id: "system:status", label: "System Status", shortcut: "", category: "System" },
39
+ { id: "system:help", label: "Help", shortcut: "F1", category: "System" },
40
+ { id: "system:doctor", label: "Doctor", shortcut: "", category: "System" },
41
+ { id: "system:setup", label: "Setup Wizard", shortcut: "", category: "System" },
42
+ { id: "system:exit", label: "Exit", shortcut: "Ctrl+C", category: "System" },
43
+ ];
44
+
45
+ // ── Fuzzy Match ──────────────────────────────────────────
46
+
47
+ function fuzzyMatch(query: string, text: string): boolean {
48
+ if (!query) return true;
49
+ const lower = text.toLowerCase();
50
+ const q = query.toLowerCase();
51
+ let qi = 0;
52
+ for (let i = 0; i < lower.length && qi < q.length; i++) {
53
+ if (lower[i] === q[qi]) qi++;
54
+ }
55
+ return qi === q.length;
56
+ }
57
+
58
+ // ── Component ────────────────────────────────────────────
59
+
60
+ export function DialogCommand(props: {
61
+ onSelect: (commandId: string) => void;
62
+ onClose: () => void;
63
+ }) {
64
+ const { theme } = useTheme();
65
+ const t = theme;
66
+ const [query, setQuery] = useState("");
67
+ const [selected, setSelected] = useState(0);
68
+
69
+ // Filter commands by fuzzy search across label and category
70
+ const filtered = useMemo(() => {
71
+ return COMMANDS.filter(
72
+ (cmd) => fuzzyMatch(query, cmd.label) || fuzzyMatch(query, cmd.category)
73
+ );
74
+ }, [query]);
75
+
76
+ // Group filtered commands by category, preserving order
77
+ const groups = useMemo(() => {
78
+ const map = new Map<string, Command[]>();
79
+ for (const cmd of filtered) {
80
+ const list = map.get(cmd.category);
81
+ if (list) {
82
+ list.push(cmd);
83
+ } else {
84
+ map.set(cmd.category, [cmd]);
85
+ }
86
+ }
87
+ return map;
88
+ }, [filtered]);
89
+
90
+ // Flat list for index-based navigation
91
+ const flatList = filtered;
92
+
93
+ // Clamp selected when list shrinks
94
+ const clampedSelected = Math.min(selected, Math.max(0, flatList.length - 1));
95
+
96
+ useKeyboard((key: any) => {
97
+ if (key.name === "escape") {
98
+ props.onClose();
99
+ return;
100
+ }
101
+
102
+ if (key.name === "return") {
103
+ const cmd = flatList[clampedSelected];
104
+ if (cmd) props.onSelect(cmd.id);
105
+ return;
106
+ }
107
+
108
+ if (key.name === "up" || (key.name === "k" && !query)) {
109
+ setSelected((s) => Math.max(0, s - 1));
110
+ return;
111
+ }
112
+
113
+ if (key.name === "down" || (key.name === "j" && !query)) {
114
+ setSelected((s) => Math.min(flatList.length - 1, s + 1));
115
+ return;
116
+ }
117
+
118
+ // Backspace
119
+ if (key.name === "backspace") {
120
+ setQuery((q) => q.slice(0, -1));
121
+ setSelected(0);
122
+ return;
123
+ }
124
+
125
+ // Printable character — append to query
126
+ if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
127
+ setQuery((q) => q + key.sequence);
128
+ setSelected(0);
129
+ }
130
+ });
131
+
132
+ // Build rows: category headers + command items
133
+ let flatIndex = 0;
134
+ const rows: ReactNode[] = [];
135
+
136
+ for (const [category, cmds] of groups) {
137
+ // Category header
138
+ rows.push(
139
+ <text key={`cat-${category}`} fg={t.secondary} attributes={TextAttributes.BOLD}>
140
+ {` ${category}`}
141
+ </text>
142
+ );
143
+
144
+ for (const cmd of cmds) {
145
+ const isSel = flatIndex === clampedSelected;
146
+ const shortcutText = cmd.shortcut ? ` ${cmd.shortcut}` : "";
147
+ rows.push(
148
+ <box key={cmd.id} flexDirection="row">
149
+ <text
150
+ fg={isSel ? t.primary : t.text}
151
+ attributes={isSel ? TextAttributes.BOLD : undefined}
152
+ >
153
+ {` ${isSel ? ">" : " "} ${cmd.label}`}
154
+ </text>
155
+ <text fg={t.textDim}>{shortcutText}</text>
156
+ </box>
157
+ );
158
+ flatIndex++;
159
+ }
160
+
161
+ // Spacer between groups
162
+ rows.push(
163
+ <text key={`spacer-${category}`} fg={t.textDim}>{""}</text>
164
+ );
165
+ }
166
+
167
+ return (
168
+ <box
169
+ borderStyle="double"
170
+ borderColor={t.primary}
171
+ paddingX={1}
172
+ paddingY={1}
173
+ flexDirection="column"
174
+ position="absolute"
175
+ top="15%"
176
+ left="15%"
177
+ width="70%"
178
+ >
179
+ {/* Title */}
180
+ <text fg={t.primary} attributes={TextAttributes.BOLD}>
181
+ {" Command Palette"}
182
+ </text>
183
+ <text fg={t.textDim}>{""}</text>
184
+
185
+ {/* Search input */}
186
+ <box flexDirection="row">
187
+ <text fg={t.textMuted}>{" > "}</text>
188
+ <text fg={t.text}>{query || ""}</text>
189
+ <text fg={t.primary} attributes={TextAttributes.BOLD}>{"_"}</text>
190
+ </box>
191
+ <text fg={t.textDim}>{""}</text>
192
+
193
+ {/* Command list */}
194
+ {flatList.length > 0 ? (
195
+ rows
196
+ ) : (
197
+ <text fg={t.textDim}>{" No matching commands"}</text>
198
+ )}
199
+
200
+ {/* Footer */}
201
+ <text fg={t.textDim}>{" ↑↓/jk Navigate Enter Select Esc Close"}</text>
202
+ </box>
203
+ );
204
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * DialogHelp — help dialog showing keyboard shortcuts, slash commands, and @mentions
3
+ *
4
+ * Opens as a centered scrollable modal. Navigate with Up/Down/PgUp/PgDn,
5
+ * close with Esc or q.
6
+ */
7
+
8
+ import { TextAttributes } from "@opentui/core";
9
+ import { useState } from "react";
10
+ import { useKeyboard } from "@opentui/react";
11
+ import { useTheme } from "../context/theme";
12
+
13
+ // ── Content Sections ────────────────────────────────────
14
+
15
+ interface HelpEntry {
16
+ key: string;
17
+ description: string;
18
+ }
19
+
20
+ const KEYBOARD_SHORTCUTS: HelpEntry[] = [
21
+ { key: "Ctrl+N", description: "New session" },
22
+ { key: "Ctrl+P", description: "Switch model" },
23
+ { key: "Ctrl+S", description: "Browse sessions" },
24
+ { key: "Ctrl+X", description: "Command palette" },
25
+ { key: "F1", description: "Show this help" },
26
+ { key: "Ctrl+V", description: "Paste text or image" },
27
+ { key: "Ctrl+U", description: "Clear input line" },
28
+ { key: "Ctrl+W", description: "Delete last word" },
29
+ { key: "Tab / ->", description: "Accept autocomplete" },
30
+ { key: "Up / Down", description: "Navigate suggestions" },
31
+ { key: "Escape", description: "Close dialog / dropdown" },
32
+ { key: "Ctrl+C", description: "Quit" },
33
+ ];
34
+
35
+ const SLASH_COMMANDS: HelpEntry[] = [
36
+ { key: "/help", description: "Show help" },
37
+ { key: "/clear", description: "Clear chat history" },
38
+ { key: "/new", description: "Start new conversation" },
39
+ { key: "/compact", description: "Compress context window" },
40
+ { key: "/btw <question>", description: "Ask without adding to history" },
41
+ { key: "/model [name]", description: "Show/change model" },
42
+ { key: "/provider [name]", description: "Show/change provider" },
43
+ { key: "/mode", description: "Show permission mode" },
44
+ { key: "/dir [path]", description: "Show/change working directory" },
45
+ { key: "/config", description: "Show configuration" },
46
+ { key: "/config set k v", description: "Set a config value" },
47
+ { key: "/theme <mode>", description: "Switch theme (dark/light/auto)" },
48
+ { key: "/effort <level>", description: "Set effort level" },
49
+ { key: "/plan <on|off>", description: "Toggle plan mode" },
50
+ { key: "/history", description: "List saved conversations" },
51
+ { key: "/resume <id>", description: "Resume conversation" },
52
+ { key: "/view <id>", description: "View conversation messages" },
53
+ { key: "/fork [id]", description: "Fork conversation" },
54
+ { key: "/delete <id>", description: "Delete conversation" },
55
+ { key: "/bg <prompt>", description: "Run prompt in background" },
56
+ { key: "/jobs [id]", description: "List/inspect background jobs" },
57
+ { key: "/permissions", description: "Show permission rules" },
58
+ { key: "/hooks", description: "Show configured hooks" },
59
+ { key: "/rules", description: "Show project rules" },
60
+ { key: "/context", description: "Show context providers" },
61
+ { key: "/mcp", description: "MCP server management" },
62
+ { key: "/doctor", description: "System health check" },
63
+ { key: "/usage", description: "Show token usage" },
64
+ { key: "/auth-status", description: "Show authentication status" },
65
+ { key: "/setup", description: "Run setup wizard" },
66
+ { key: "/login", description: "Open setup wizard" },
67
+ { key: "/logout", description: "Clear OAuth tokens" },
68
+ { key: "/init", description: "Initialize project config" },
69
+ { key: "/exit", description: "Quit" },
70
+ ];
71
+
72
+ const AT_MENTIONS: HelpEntry[] = [
73
+ { key: "@terminal", description: "Recent terminal output" },
74
+ { key: "@url", description: "Fetch URL content" },
75
+ { key: "@tree", description: "Directory tree" },
76
+ { key: "@codebase", description: "Search codebase" },
77
+ { key: "@clip", description: "Clipboard contents" },
78
+ { key: "@file", description: "Include file" },
79
+ ];
80
+
81
+ // ── Build Lines ─────────────────────────────────────────
82
+
83
+ interface HelpLine {
84
+ type: "header" | "entry" | "blank";
85
+ text?: string;
86
+ key?: string;
87
+ description?: string;
88
+ }
89
+
90
+ function buildLines(): HelpLine[] {
91
+ const lines: HelpLine[] = [];
92
+
93
+ lines.push({ type: "header", text: "Keyboard Shortcuts" });
94
+ lines.push({ type: "blank" });
95
+ for (const entry of KEYBOARD_SHORTCUTS) {
96
+ lines.push({ type: "entry", key: entry.key, description: entry.description });
97
+ }
98
+
99
+ lines.push({ type: "blank" });
100
+ lines.push({ type: "header", text: "Slash Commands" });
101
+ lines.push({ type: "blank" });
102
+ for (const entry of SLASH_COMMANDS) {
103
+ lines.push({ type: "entry", key: entry.key, description: entry.description });
104
+ }
105
+
106
+ lines.push({ type: "blank" });
107
+ lines.push({ type: "header", text: "@Mentions" });
108
+ lines.push({ type: "blank" });
109
+ for (const entry of AT_MENTIONS) {
110
+ lines.push({ type: "entry", key: entry.key, description: entry.description });
111
+ }
112
+
113
+ return lines;
114
+ }
115
+
116
+ const ALL_LINES = buildLines();
117
+ const PAGE_SIZE = 15;
118
+
119
+ // ── Component ───────────────────────────────────────────
120
+
121
+ export function DialogHelp(props: {
122
+ onClose: () => void;
123
+ }) {
124
+ const { theme } = useTheme();
125
+ const t = theme;
126
+ const [scrollOffset, setScrollOffset] = useState(0);
127
+
128
+ const maxOffset = Math.max(0, ALL_LINES.length - PAGE_SIZE);
129
+
130
+ useKeyboard((key: any) => {
131
+ if (key.name === "escape" || key.name === "q") {
132
+ props.onClose();
133
+ return;
134
+ }
135
+
136
+ if (key.name === "up" || key.name === "k") {
137
+ setScrollOffset((s) => Math.max(0, s - 1));
138
+ return;
139
+ }
140
+
141
+ if (key.name === "down" || key.name === "j") {
142
+ setScrollOffset((s) => Math.min(maxOffset, s + 1));
143
+ return;
144
+ }
145
+
146
+ if (key.name === "pageup") {
147
+ setScrollOffset((s) => Math.max(0, s - PAGE_SIZE));
148
+ return;
149
+ }
150
+
151
+ if (key.name === "pagedown") {
152
+ setScrollOffset((s) => Math.min(maxOffset, s + PAGE_SIZE));
153
+ return;
154
+ }
155
+
156
+ // Home / End
157
+ if (key.name === "home") {
158
+ setScrollOffset(0);
159
+ return;
160
+ }
161
+ if (key.name === "end") {
162
+ setScrollOffset(maxOffset);
163
+ return;
164
+ }
165
+ });
166
+
167
+ const visibleLines = ALL_LINES.slice(scrollOffset, scrollOffset + PAGE_SIZE);
168
+ const keyColWidth = 20;
169
+
170
+ return (
171
+ <box
172
+ borderStyle="double"
173
+ borderColor={t.primary}
174
+ paddingX={2}
175
+ paddingY={1}
176
+ flexDirection="column"
177
+ position="absolute"
178
+ top="10%"
179
+ left="15%"
180
+ width="70%"
181
+ height="80%"
182
+ >
183
+ {/* Title */}
184
+ <text fg={t.primary} attributes={TextAttributes.BOLD}>
185
+ {" Help"}
186
+ </text>
187
+ <text fg={t.textDim}>{""}</text>
188
+
189
+ {/* Scrollable content */}
190
+ {visibleLines.map((line, i) => {
191
+ if (line.type === "blank") {
192
+ return <text key={`line-${scrollOffset + i}`} fg={t.textDim}>{""}</text>;
193
+ }
194
+
195
+ if (line.type === "header") {
196
+ return (
197
+ <text
198
+ key={`line-${scrollOffset + i}`}
199
+ fg={t.primary}
200
+ attributes={TextAttributes.BOLD}
201
+ >
202
+ {` ${line.text}`}
203
+ </text>
204
+ );
205
+ }
206
+
207
+ // entry
208
+ const paddedKey = (line.key || "").padEnd(keyColWidth);
209
+ return (
210
+ <box key={`line-${scrollOffset + i}`} flexDirection="row">
211
+ <text fg={t.secondary}>{` ${paddedKey}`}</text>
212
+ <text fg={t.textMuted}>{line.description || ""}</text>
213
+ </box>
214
+ );
215
+ })}
216
+
217
+ {/* Scroll indicator */}
218
+ <text fg={t.textDim}>{""}</text>
219
+ <text fg={t.textDim}>
220
+ {` ${scrollOffset > 0 ? "..." : " "} ${scrollOffset + 1}-${Math.min(scrollOffset + PAGE_SIZE, ALL_LINES.length)} of ${ALL_LINES.length} ${scrollOffset < maxOffset ? "..." : " "}`}
221
+ </text>
222
+
223
+ {/* Footer */}
224
+ <text fg={t.textDim}>{" Up/Down/j/k Scroll PgUp/PgDn Page Esc/q Close"}</text>
225
+ </box>
226
+ );
227
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * DialogModel — model picker dialog (Ctrl+P)
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 ModelOption {
11
+ id: string;
12
+ name: string;
13
+ hint?: string;
14
+ }
15
+
16
+ const MODELS: Record<string, ModelOption[]> = {
17
+ anthropic: [
18
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", hint: "fast & smart" },
19
+ { id: "claude-opus-4-6", name: "Claude Opus 4.6", hint: "most capable" },
20
+ { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", hint: "fastest" },
21
+ ],
22
+ openai: [
23
+ { id: "gpt-4o", name: "GPT-4o", hint: "recommended" },
24
+ { id: "gpt-4o-mini", name: "GPT-4o mini", hint: "fastest" },
25
+ { id: "o3", name: "o3", hint: "reasoning" },
26
+ ],
27
+ google: [
28
+ { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", hint: "fast" },
29
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", hint: "most capable" },
30
+ ],
31
+ };
32
+
33
+ export function DialogModel(props: {
34
+ provider: string;
35
+ currentModel: string;
36
+ onSelect: (model: string) => void;
37
+ onClose: () => void;
38
+ }) {
39
+ const { theme } = useTheme();
40
+ const t = theme;
41
+ const models = MODELS[props.provider] || [];
42
+ const [selected, setSelected] = useState(
43
+ Math.max(0, models.findIndex((m) => m.id === props.currentModel))
44
+ );
45
+
46
+ useKeyboard((key: any) => {
47
+ if (key.name === "escape" || (key.ctrl && key.name === "c")) {
48
+ props.onClose();
49
+ } else if (key.name === "up" || key.name === "k") {
50
+ setSelected((s) => Math.max(0, s - 1));
51
+ } else if (key.name === "down" || key.name === "j") {
52
+ setSelected((s) => Math.min(models.length - 1, s + 1));
53
+ } else if (key.name === "return") {
54
+ const m = models[selected];
55
+ if (m) props.onSelect(m.id);
56
+ }
57
+ });
58
+
59
+ return (
60
+ <box
61
+ borderStyle="double"
62
+ borderColor={t.primary}
63
+ paddingX={1}
64
+ paddingY={1}
65
+ flexDirection="column"
66
+ position="absolute"
67
+ top="30%"
68
+ left="20%"
69
+ width="60%"
70
+ >
71
+ <text fg={t.primary} attributes={TextAttributes.BOLD}>
72
+ {" Select Model"}
73
+ </text>
74
+ <text fg={t.textDim}>{` Provider: ${props.provider}`}</text>
75
+ <text fg={t.textDim}>{""}</text>
76
+ {models.map((model, i) => (
77
+ <box key={model.id}>
78
+ <text
79
+ fg={i === selected ? t.primary : t.text}
80
+ attributes={i === selected ? TextAttributes.BOLD : undefined}
81
+ >
82
+ {` ${i === selected ? "❯" : " "} ${model.name}`}
83
+ </text>
84
+ <text fg={t.textDim}>{model.hint ? ` ${model.hint}` : ""}</text>
85
+ <text fg={model.id === props.currentModel ? t.success : t.textDim}>
86
+ {model.id === props.currentModel ? " ●" : ""}
87
+ </text>
88
+ </box>
89
+ ))}
90
+ <text fg={t.textDim}>{"\n ↑↓ Navigate Enter Select Esc Close"}</text>
91
+ </box>
92
+ );
93
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * DialogStatus — system status dialog showing provider, tools, config info.
3
+ * Scrollable overlay with sections for Provider, System, and Tools.
4
+ */
5
+
6
+ import { TextAttributes } from "@opentui/core";
7
+ import { useState } from "react";
8
+ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
9
+ import { useTheme } from "../context/theme";
10
+ import { useSDK } from "../context/sdk";
11
+
12
+ export function DialogStatus(props: { onClose: () => void }) {
13
+ const { theme } = useTheme();
14
+ const t = theme;
15
+ const sdk = useSDK();
16
+ const dims = useTerminalDimensions();
17
+ const [scrollOffset, setScrollOffset] = useState(0);
18
+
19
+ // Gather status info
20
+ const allTools = sdk.registry.getAll ? sdk.registry.getAll() : [];
21
+ const toolNames = Array.isArray(allTools)
22
+ ? allTools.map((tool: any) =>
23
+ tool.definition?.name || tool.name || "unknown"
24
+ )
25
+ : [];
26
+
27
+ const sections: Array<{ title: string; rows: Array<[string, string]> }> = [
28
+ {
29
+ title: "Provider",
30
+ rows: [
31
+ ["Provider", sdk.provider],
32
+ ["Model", sdk.model],
33
+ ["Directory", sdk.workingDir],
34
+ ],
35
+ },
36
+ {
37
+ title: "System",
38
+ rows: [
39
+ ["Node", process.version],
40
+ ["Platform", `${process.platform} ${process.arch}`],
41
+ [
42
+ "Terminal",
43
+ process.env.TERM_PROGRAM || process.env.TERM || "unknown",
44
+ ],
45
+ ["Shell", process.env.SHELL || "unknown"],
46
+ ],
47
+ },
48
+ {
49
+ title: `Tools (${toolNames.length})`,
50
+ rows: toolNames.slice(0, 20).map((name: string) => ["\u2022", name]),
51
+ },
52
+ ];
53
+
54
+ // Build flat lines for scrolling
55
+ const lines: Array<{
56
+ type: "header" | "row";
57
+ text: string;
58
+ value?: string;
59
+ }> = [];
60
+ for (const section of sections) {
61
+ lines.push({ type: "header", text: section.title });
62
+ for (const [label, value] of section.rows) {
63
+ lines.push({ type: "row", text: label, value });
64
+ }
65
+ lines.push({ type: "row", text: "", value: "" }); // spacer
66
+ }
67
+
68
+ const maxVisible = Math.max(5, (dims.height || 24) - 10);
69
+
70
+ useKeyboard((key: any) => {
71
+ if (key.name === "escape" || key.name === "q") props.onClose();
72
+ if (key.name === "up" || key.name === "k")
73
+ setScrollOffset((s) => Math.max(0, s - 1));
74
+ if (key.name === "down" || key.name === "j")
75
+ setScrollOffset((s) => Math.min(lines.length - maxVisible, s + 1));
76
+ });
77
+
78
+ const visible = lines.slice(scrollOffset, scrollOffset + maxVisible);
79
+
80
+ return (
81
+ <box
82
+ borderStyle="double"
83
+ borderColor={t.primary}
84
+ paddingX={1}
85
+ paddingY={1}
86
+ flexDirection="column"
87
+ position="absolute"
88
+ top="10%"
89
+ left="15%"
90
+ width="70%"
91
+ >
92
+ <text fg={t.primary} attributes={TextAttributes.BOLD}>
93
+ {" System Status"}
94
+ </text>
95
+ <text>{""}</text>
96
+ {visible.map((line, i) => {
97
+ if (line.type === "header") {
98
+ return (
99
+ <text
100
+ key={`h-${i}`}
101
+ fg={t.secondary}
102
+ attributes={TextAttributes.BOLD}
103
+ >
104
+ {` ${line.text}`}
105
+ </text>
106
+ );
107
+ }
108
+ if (!line.text && !line.value) return <text key={`s-${i}`}>{""}</text>;
109
+ return (
110
+ <box key={`r-${i}`} flexDirection="row">
111
+ <text fg={t.textMuted}>{` ${line.text}`}</text>
112
+ {line.value && <text fg={t.text}>{` ${line.value}`}</text>}
113
+ </box>
114
+ );
115
+ })}
116
+ <text>{""}</text>
117
+ <text fg={t.textDim}>
118
+ {" \u2191\u2193 Scroll Esc Close"}
119
+ </text>
120
+ </box>
121
+ );
122
+ }