@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.
- package/README.md +22 -13
- package/dist/cdoing-tui-darwin-arm64/bin/cdoing-tui +0 -0
- package/package.json +12 -9
- package/dist/index.js +0 -64
- package/dist/index.js.map +0 -7
- package/esbuild.config.cjs +0 -45
- package/src/app.tsx +0 -787
- package/src/components/dialog-command.tsx +0 -207
- package/src/components/dialog-help.tsx +0 -151
- package/src/components/dialog-model.tsx +0 -142
- package/src/components/dialog-status.tsx +0 -84
- package/src/components/dialog-theme.tsx +0 -318
- package/src/components/input-area.tsx +0 -380
- package/src/components/loading-spinner.tsx +0 -28
- package/src/components/message-list.tsx +0 -546
- package/src/components/permission-prompt.tsx +0 -72
- package/src/components/session-browser.tsx +0 -231
- package/src/components/session-footer.tsx +0 -30
- package/src/components/session-header.tsx +0 -39
- package/src/components/setup-wizard.tsx +0 -542
- package/src/components/sidebar.tsx +0 -183
- package/src/components/status-bar.tsx +0 -76
- package/src/components/toast.tsx +0 -139
- package/src/context/sdk.tsx +0 -40
- package/src/context/theme.tsx +0 -640
- package/src/index.ts +0 -50
- package/src/lib/autocomplete.ts +0 -262
- package/src/lib/context-providers.ts +0 -98
- package/src/lib/history.ts +0 -164
- package/src/lib/terminal-title.ts +0 -15
- package/src/routes/home.tsx +0 -148
- package/src/routes/session.tsx +0 -1309
- package/src/store/settings.ts +0 -107
- package/tsconfig.json +0 -23
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DialogCommand — command palette (Ctrl+P)
|
|
3
|
-
*
|
|
4
|
-
* Full command palette with categories, fuzzy search, shortcuts,
|
|
5
|
-
* and keyboard navigation. Inspired by OpenCode's command palette.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { TextAttributes } from "@opentui/core";
|
|
9
|
-
import type { SelectOption } from "@opentui/core";
|
|
10
|
-
import { useState, useMemo } from "react";
|
|
11
|
-
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
12
|
-
import { useTheme } from "../context/theme";
|
|
13
|
-
|
|
14
|
-
// ── Command Definition ───────────────────────────────────
|
|
15
|
-
|
|
16
|
-
export interface Command {
|
|
17
|
-
id: string;
|
|
18
|
-
label: string;
|
|
19
|
-
shortcut?: string;
|
|
20
|
-
category: string;
|
|
21
|
-
icon?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const COMMANDS: Command[] = [
|
|
25
|
-
// Session
|
|
26
|
-
{ id: "session:new", label: "New Session", shortcut: "Ctrl+N", category: "Session", icon: "+" },
|
|
27
|
-
{ id: "session:browse", label: "Browse Sessions", shortcut: "Ctrl+S", category: "Session", icon: "◦" },
|
|
28
|
-
{ id: "session:clear", label: "Clear History", shortcut: "", category: "Session", icon: "✕" },
|
|
29
|
-
|
|
30
|
-
// Model & Provider
|
|
31
|
-
{ id: "model:switch", label: "Switch Model", shortcut: "Ctrl+O", category: "Model", icon: "◆" },
|
|
32
|
-
{ id: "model:provider", label: "Switch Provider", shortcut: "", category: "Model", icon: "◆" },
|
|
33
|
-
|
|
34
|
-
// Theme & Appearance
|
|
35
|
-
{ id: "theme:picker", label: "Browse Themes", shortcut: "Ctrl+T", category: "Appearance", icon: "◈" },
|
|
36
|
-
{ id: "theme:dark", label: "Dark Mode", shortcut: "", category: "Appearance", icon: "●" },
|
|
37
|
-
{ id: "theme:light", label: "Light Mode", shortcut: "", category: "Appearance", icon: "○" },
|
|
38
|
-
{ id: "display:sidebar", label: "Toggle Sidebar", shortcut: "Ctrl+B", category: "Appearance", icon: "▐" },
|
|
39
|
-
|
|
40
|
-
// Tools
|
|
41
|
-
{ id: "tool:shell", label: "Run Shell Command", shortcut: "", category: "Tools", icon: "$" },
|
|
42
|
-
{ id: "tool:search", label: "Search Codebase", shortcut: "", category: "Tools", icon: "?" },
|
|
43
|
-
{ id: "tool:tree", label: "File Tree", shortcut: "", category: "Tools", icon: "├" },
|
|
44
|
-
|
|
45
|
-
// System
|
|
46
|
-
{ id: "system:status", label: "System Status", shortcut: "", category: "System", icon: "i" },
|
|
47
|
-
{ id: "system:help", label: "Help", shortcut: "F1", category: "System", icon: "?" },
|
|
48
|
-
{ id: "system:doctor", label: "Doctor (Diagnostics)", shortcut: "", category: "System", icon: "+" },
|
|
49
|
-
{ id: "system:setup", label: "Setup Wizard (Connect Provider)", shortcut: "", category: "System", icon: "⚙" },
|
|
50
|
-
{ id: "system:exit", label: "Exit", shortcut: "Ctrl+C", category: "System", icon: "⏻" },
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
// ── Fuzzy Match ──────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
function fuzzyMatch(query: string, text: string): boolean {
|
|
56
|
-
if (!query) return true;
|
|
57
|
-
const lower = text.toLowerCase();
|
|
58
|
-
const q = query.toLowerCase();
|
|
59
|
-
let qi = 0;
|
|
60
|
-
for (let i = 0; i < lower.length && qi < q.length; i++) {
|
|
61
|
-
if (lower[i] === q[qi]) qi++;
|
|
62
|
-
}
|
|
63
|
-
return qi === q.length;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Component ────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
export function DialogCommand(props: {
|
|
69
|
-
onSelect: (commandId: string) => void;
|
|
70
|
-
onClose: () => void;
|
|
71
|
-
}) {
|
|
72
|
-
const { theme, customBg } = useTheme();
|
|
73
|
-
const t = theme;
|
|
74
|
-
const dims = useTerminalDimensions();
|
|
75
|
-
const [query, setQuery] = useState("");
|
|
76
|
-
|
|
77
|
-
const dialogWidth = Math.min(64, (dims.width || 80) - 4);
|
|
78
|
-
const maxVisible = Math.max(8, Math.floor((dims.height || 24) * 0.6));
|
|
79
|
-
|
|
80
|
-
// Filter commands by fuzzy search across label, category, and id
|
|
81
|
-
const filtered = useMemo(() => {
|
|
82
|
-
return COMMANDS.filter(
|
|
83
|
-
(cmd) =>
|
|
84
|
-
fuzzyMatch(query, cmd.label) ||
|
|
85
|
-
fuzzyMatch(query, cmd.category) ||
|
|
86
|
-
fuzzyMatch(query, cmd.id)
|
|
87
|
-
);
|
|
88
|
-
}, [query]);
|
|
89
|
-
|
|
90
|
-
// Group by category for display (with separators)
|
|
91
|
-
const selectOptions: SelectOption[] = useMemo(() => {
|
|
92
|
-
const opts: SelectOption[] = [];
|
|
93
|
-
let lastCategory = "";
|
|
94
|
-
for (const cmd of filtered) {
|
|
95
|
-
if (cmd.category !== lastCategory) {
|
|
96
|
-
lastCategory = cmd.category;
|
|
97
|
-
}
|
|
98
|
-
const shortcutStr = cmd.shortcut || "";
|
|
99
|
-
opts.push({
|
|
100
|
-
name: `${cmd.icon || " "} ${cmd.label}`,
|
|
101
|
-
description: [cmd.category, shortcutStr].filter(Boolean).join(" "),
|
|
102
|
-
value: cmd.id,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return opts;
|
|
106
|
-
}, [filtered]);
|
|
107
|
-
|
|
108
|
-
useKeyboard((key: any) => {
|
|
109
|
-
if (key.name === "escape" || (key.ctrl && key.name === "p")) {
|
|
110
|
-
props.onClose();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Let <select> handle up/down/return
|
|
115
|
-
if (key.name === "up" || key.name === "down" || key.name === "return") return;
|
|
116
|
-
|
|
117
|
-
// Backspace — search
|
|
118
|
-
if (key.name === "backspace") {
|
|
119
|
-
setQuery((q) => q.slice(0, -1));
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Ctrl+U — clear search
|
|
124
|
-
if (key.ctrl && key.name === "u") {
|
|
125
|
-
setQuery("");
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Printable character — append to query
|
|
130
|
-
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
131
|
-
setQuery((q) => q + key.sequence);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<box
|
|
137
|
-
borderStyle="double"
|
|
138
|
-
borderColor={t.primary}
|
|
139
|
-
backgroundColor={customBg || t.bg}
|
|
140
|
-
paddingX={1}
|
|
141
|
-
paddingY={0}
|
|
142
|
-
flexDirection="column"
|
|
143
|
-
position="absolute"
|
|
144
|
-
top={Math.max(1, Math.floor((dims.height || 24) * 0.1))}
|
|
145
|
-
left={Math.max(1, Math.floor(((dims.width || 80) - dialogWidth) / 2))}
|
|
146
|
-
width={dialogWidth}
|
|
147
|
-
>
|
|
148
|
-
{/* Title bar */}
|
|
149
|
-
<box flexDirection="row" flexShrink={0} height={1}>
|
|
150
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
|
|
151
|
-
{" Commands"}
|
|
152
|
-
</text>
|
|
153
|
-
<text fg={t.textDim}>{"Ctrl+P "}</text>
|
|
154
|
-
</box>
|
|
155
|
-
|
|
156
|
-
{/* Search input */}
|
|
157
|
-
<box height={1} flexShrink={0}>
|
|
158
|
-
<text fg={t.border}>{"─".repeat(dialogWidth - 4)}</text>
|
|
159
|
-
</box>
|
|
160
|
-
<box flexDirection="row" flexShrink={0} height={1}>
|
|
161
|
-
<text fg={t.textMuted}>{" > "}</text>
|
|
162
|
-
<text fg={t.text}>{query || ""}</text>
|
|
163
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD}>{"█"}</text>
|
|
164
|
-
</box>
|
|
165
|
-
<box height={1} flexShrink={0}>
|
|
166
|
-
<text fg={t.border}>{"─".repeat(dialogWidth - 4)}</text>
|
|
167
|
-
</box>
|
|
168
|
-
|
|
169
|
-
{/* Command list */}
|
|
170
|
-
{selectOptions.length > 0 ? (
|
|
171
|
-
<select
|
|
172
|
-
options={selectOptions}
|
|
173
|
-
focused={true}
|
|
174
|
-
height={Math.min(maxVisible, selectOptions.length)}
|
|
175
|
-
showDescription={true}
|
|
176
|
-
backgroundColor={customBg || undefined}
|
|
177
|
-
focusedBackgroundColor={customBg || undefined}
|
|
178
|
-
textColor={t.text}
|
|
179
|
-
focusedTextColor={t.text}
|
|
180
|
-
selectedBackgroundColor={t.primary}
|
|
181
|
-
selectedTextColor={t.bg}
|
|
182
|
-
descriptionColor={t.textDim}
|
|
183
|
-
selectedDescriptionColor={t.bg}
|
|
184
|
-
showScrollIndicator={selectOptions.length > maxVisible}
|
|
185
|
-
onSelect={(_index: number, option: SelectOption | null) => {
|
|
186
|
-
if (option?.value) props.onSelect(option.value);
|
|
187
|
-
}}
|
|
188
|
-
/>
|
|
189
|
-
) : (
|
|
190
|
-
<box height={1}>
|
|
191
|
-
<text fg={t.textDim}>{" No matching commands"}</text>
|
|
192
|
-
</box>
|
|
193
|
-
)}
|
|
194
|
-
|
|
195
|
-
{/* Footer */}
|
|
196
|
-
<box height={1} flexShrink={0}>
|
|
197
|
-
<text fg={t.border}>{"─".repeat(dialogWidth - 4)}</text>
|
|
198
|
-
</box>
|
|
199
|
-
<box flexDirection="row" height={1} flexShrink={0}>
|
|
200
|
-
<text fg={t.textDim}>{" ↑↓ navigate "}</text>
|
|
201
|
-
<text fg={t.textDim}>{"enter select "}</text>
|
|
202
|
-
<text fg={t.textDim}>{"type to filter "}</text>
|
|
203
|
-
<text fg={t.textDim}>{"esc close"}</text>
|
|
204
|
-
</box>
|
|
205
|
-
</box>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DialogHelp — help dialog showing keyboard shortcuts, slash commands, and @mentions
|
|
3
|
-
*
|
|
4
|
-
* Uses native <scrollbox> for smooth scrolling. Close with Esc or q.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { TextAttributes } from "@opentui/core";
|
|
8
|
-
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
9
|
-
import { useTheme } from "../context/theme";
|
|
10
|
-
|
|
11
|
-
// ── Content Sections ────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
interface HelpEntry {
|
|
14
|
-
key: string;
|
|
15
|
-
description: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const KEYBOARD_SHORTCUTS: HelpEntry[] = [
|
|
19
|
-
{ key: "Ctrl+P", description: "Command palette" },
|
|
20
|
-
{ key: "Ctrl+O", description: "Switch model" },
|
|
21
|
-
{ key: "Ctrl+N", description: "New session" },
|
|
22
|
-
{ key: "Ctrl+S", description: "Browse sessions" },
|
|
23
|
-
{ key: "F1", description: "Show this help" },
|
|
24
|
-
{ key: "Ctrl+V", description: "Paste text or image" },
|
|
25
|
-
{ key: "Ctrl+U", description: "Clear input line" },
|
|
26
|
-
{ key: "Ctrl+W", description: "Delete last word" },
|
|
27
|
-
{ key: "Tab", description: "Switch mode (Build / Plan)" },
|
|
28
|
-
{ key: "→ (Right)", description: "Accept autocomplete" },
|
|
29
|
-
{ key: "Up / Down", description: "Navigate suggestions" },
|
|
30
|
-
{ key: "Escape", description: "Close dialog / dropdown" },
|
|
31
|
-
{ key: "Ctrl+C", description: "Quit" },
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
const SLASH_COMMANDS: HelpEntry[] = [
|
|
35
|
-
{ key: "/help", description: "Show help" },
|
|
36
|
-
{ key: "/clear", description: "Clear chat history" },
|
|
37
|
-
{ key: "/new", description: "Start new conversation" },
|
|
38
|
-
{ key: "/compact", description: "Compress context window" },
|
|
39
|
-
{ key: "/btw <question>", description: "Ask without adding to history" },
|
|
40
|
-
{ key: "/model [name]", description: "Show/change model" },
|
|
41
|
-
{ key: "/provider [name]", description: "Show/change provider" },
|
|
42
|
-
{ key: "/mode", description: "Show permission mode" },
|
|
43
|
-
{ key: "/dir [path]", description: "Show/change working directory" },
|
|
44
|
-
{ key: "/config", description: "Show configuration" },
|
|
45
|
-
{ key: "/config set k v", description: "Set a config value" },
|
|
46
|
-
{ key: "/theme <mode>", description: "Switch theme (dark/light/auto)" },
|
|
47
|
-
{ key: "/effort <level>", description: "Set effort level" },
|
|
48
|
-
{ key: "/plan <on|off>", description: "Toggle plan mode" },
|
|
49
|
-
{ key: "/history", description: "List saved conversations" },
|
|
50
|
-
{ key: "/resume <id>", description: "Resume conversation" },
|
|
51
|
-
{ key: "/view <id>", description: "View conversation messages" },
|
|
52
|
-
{ key: "/fork [id]", description: "Fork conversation" },
|
|
53
|
-
{ key: "/delete <id>", description: "Delete conversation" },
|
|
54
|
-
{ key: "/bg <prompt>", description: "Run prompt in background" },
|
|
55
|
-
{ key: "/jobs [id]", description: "List/inspect background jobs" },
|
|
56
|
-
{ key: "/permissions", description: "Show permission rules" },
|
|
57
|
-
{ key: "/hooks", description: "Show configured hooks" },
|
|
58
|
-
{ key: "/rules", description: "Show project rules" },
|
|
59
|
-
{ key: "/context", description: "Show context providers" },
|
|
60
|
-
{ key: "/mcp", description: "MCP server management" },
|
|
61
|
-
{ key: "/doctor", description: "System health check" },
|
|
62
|
-
{ key: "/usage", description: "Show token usage" },
|
|
63
|
-
{ key: "/auth-status", description: "Show authentication status" },
|
|
64
|
-
{ key: "/setup", description: "Run setup wizard" },
|
|
65
|
-
{ key: "/login", description: "Open setup wizard" },
|
|
66
|
-
{ key: "/logout", description: "Clear OAuth tokens" },
|
|
67
|
-
{ key: "/init", description: "Initialize project config" },
|
|
68
|
-
{ key: "/exit", description: "Quit" },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
const AT_MENTIONS: HelpEntry[] = [
|
|
72
|
-
{ key: "@terminal", description: "Recent terminal output" },
|
|
73
|
-
{ key: "@url", description: "Fetch URL content" },
|
|
74
|
-
{ key: "@tree", description: "Directory tree" },
|
|
75
|
-
{ key: "@codebase", description: "Search codebase" },
|
|
76
|
-
{ key: "@clip", description: "Clipboard contents" },
|
|
77
|
-
{ key: "@file", description: "Include file" },
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
const keyColWidth = 20;
|
|
81
|
-
|
|
82
|
-
function Section(props: { title: string; entries: HelpEntry[]; theme: any }) {
|
|
83
|
-
const t = props.theme;
|
|
84
|
-
return (
|
|
85
|
-
<box flexDirection="column" flexShrink={0}>
|
|
86
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD}>
|
|
87
|
-
{` ${props.title}`}
|
|
88
|
-
</text>
|
|
89
|
-
<text>{""}</text>
|
|
90
|
-
{props.entries.map((entry) => (
|
|
91
|
-
<box key={entry.key} flexDirection="row">
|
|
92
|
-
<text fg={t.secondary}>{` ${entry.key.padEnd(keyColWidth)}`}</text>
|
|
93
|
-
<text fg={t.textMuted}>{entry.description}</text>
|
|
94
|
-
</box>
|
|
95
|
-
))}
|
|
96
|
-
<text>{""}</text>
|
|
97
|
-
</box>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── Component ───────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
export function DialogHelp(props: {
|
|
104
|
-
onClose: () => void;
|
|
105
|
-
}) {
|
|
106
|
-
const { theme, customBg } = useTheme();
|
|
107
|
-
const t = theme;
|
|
108
|
-
const dims = useTerminalDimensions();
|
|
109
|
-
const dialogWidth = Math.min(70, (dims.width || 80) - 4);
|
|
110
|
-
const dialogHeight = Math.max(10, (dims.height || 24) - 6);
|
|
111
|
-
|
|
112
|
-
useKeyboard((key: any) => {
|
|
113
|
-
if (key.name === "escape" || key.name === "q") {
|
|
114
|
-
props.onClose();
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<box
|
|
120
|
-
borderStyle="double"
|
|
121
|
-
borderColor={t.primary}
|
|
122
|
-
backgroundColor={customBg || t.bg}
|
|
123
|
-
paddingX={2}
|
|
124
|
-
paddingY={1}
|
|
125
|
-
flexDirection="column"
|
|
126
|
-
position="absolute"
|
|
127
|
-
top={Math.max(1, Math.floor((dims.height || 24) * 0.05))}
|
|
128
|
-
left={Math.max(1, Math.floor(((dims.width || 80) - dialogWidth) / 2))}
|
|
129
|
-
width={dialogWidth}
|
|
130
|
-
height={dialogHeight}
|
|
131
|
-
>
|
|
132
|
-
{/* Title bar */}
|
|
133
|
-
<box flexDirection="row" flexShrink={0}>
|
|
134
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
|
|
135
|
-
{" Help"}
|
|
136
|
-
</text>
|
|
137
|
-
<text fg={t.textDim}>{"esc"}</text>
|
|
138
|
-
</box>
|
|
139
|
-
<text flexShrink={0}>{""}</text>
|
|
140
|
-
|
|
141
|
-
{/* Scrollable content */}
|
|
142
|
-
<scrollbox flexGrow={1}>
|
|
143
|
-
<box flexShrink={0}>
|
|
144
|
-
<Section title="Keyboard Shortcuts" entries={KEYBOARD_SHORTCUTS} theme={t} />
|
|
145
|
-
<Section title="Slash Commands" entries={SLASH_COMMANDS} theme={t} />
|
|
146
|
-
<Section title="@Mentions" entries={AT_MENTIONS} theme={t} />
|
|
147
|
-
</box>
|
|
148
|
-
</scrollbox>
|
|
149
|
-
</box>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DialogModel — model picker dialog (Ctrl+O)
|
|
3
|
-
*
|
|
4
|
-
* Uses OpenTUI <select> for the model list with proper
|
|
5
|
-
* highlight styling and keyboard navigation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { TextAttributes } from "@opentui/core";
|
|
9
|
-
import type { SelectOption } from "@opentui/core";
|
|
10
|
-
import { useState, useMemo } from "react";
|
|
11
|
-
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
12
|
-
import { useTheme } from "../context/theme";
|
|
13
|
-
import { getProviders } from "@cdoing/ai";
|
|
14
|
-
|
|
15
|
-
export interface ModelOption {
|
|
16
|
-
id: string;
|
|
17
|
-
name: string;
|
|
18
|
-
hint?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Build model map from centralized catalog
|
|
22
|
-
const MODELS: Record<string, ModelOption[]> = {};
|
|
23
|
-
for (const p of getProviders() as Array<{ id: string; models: Array<{ id: string; label: string; hint?: string }> }>) {
|
|
24
|
-
MODELS[p.id] = p.models.map((m) => ({ id: m.id, name: m.label, hint: m.hint }));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function DialogModel(props: {
|
|
28
|
-
provider: string;
|
|
29
|
-
currentModel: string;
|
|
30
|
-
onSelect: (model: string) => void;
|
|
31
|
-
onClose: () => void;
|
|
32
|
-
}) {
|
|
33
|
-
const { theme, customBg } = useTheme();
|
|
34
|
-
const t = theme;
|
|
35
|
-
const dims = useTerminalDimensions();
|
|
36
|
-
const models = MODELS[props.provider] || [];
|
|
37
|
-
const [isCustom, setIsCustom] = useState(false);
|
|
38
|
-
const [customInput, setCustomInput] = useState("");
|
|
39
|
-
|
|
40
|
-
// Build SelectOption list: models + "Custom model..." at the end
|
|
41
|
-
const selectOptions: SelectOption[] = useMemo(() => {
|
|
42
|
-
const opts: SelectOption[] = models.map((m) => ({
|
|
43
|
-
name: m.name,
|
|
44
|
-
description: [
|
|
45
|
-
m.hint || "",
|
|
46
|
-
m.id === props.currentModel ? "● current" : "",
|
|
47
|
-
].filter(Boolean).join(" "),
|
|
48
|
-
value: m.id,
|
|
49
|
-
}));
|
|
50
|
-
opts.push({
|
|
51
|
-
name: "Custom model...",
|
|
52
|
-
description: "type any model name",
|
|
53
|
-
value: "__custom__",
|
|
54
|
-
});
|
|
55
|
-
return opts;
|
|
56
|
-
}, [models, props.currentModel]);
|
|
57
|
-
|
|
58
|
-
const initialIndex = Math.max(0, models.findIndex((m) => m.id === props.currentModel));
|
|
59
|
-
|
|
60
|
-
useKeyboard((key: any) => {
|
|
61
|
-
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
62
|
-
if (isCustom) { setIsCustom(false); return; }
|
|
63
|
-
props.onClose();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (!isCustom) return; // Let <select> handle navigation
|
|
67
|
-
// Custom model text input mode
|
|
68
|
-
if (key.name === "return") {
|
|
69
|
-
const m = customInput.trim();
|
|
70
|
-
if (m) props.onSelect(m);
|
|
71
|
-
} else if (key.name === "backspace") {
|
|
72
|
-
setCustomInput((s) => s.slice(0, -1));
|
|
73
|
-
} else if (key.ctrl && key.name === "u") {
|
|
74
|
-
setCustomInput("");
|
|
75
|
-
} else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
76
|
-
setCustomInput((s) => s + key.sequence);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<box
|
|
82
|
-
borderStyle="double"
|
|
83
|
-
borderColor={t.primary}
|
|
84
|
-
backgroundColor={customBg || t.bg}
|
|
85
|
-
paddingX={1}
|
|
86
|
-
paddingY={1}
|
|
87
|
-
flexDirection="column"
|
|
88
|
-
position="absolute"
|
|
89
|
-
top={Math.max(2, Math.floor((dims.height || 24) * 0.25))}
|
|
90
|
-
left={Math.max(1, Math.floor(((dims.width || 80) - Math.min(60, (dims.width || 80) - 4)) / 2))}
|
|
91
|
-
width={Math.min(60, (dims.width || 80) - 4)}
|
|
92
|
-
>
|
|
93
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD}>
|
|
94
|
-
{" Select Model"}
|
|
95
|
-
</text>
|
|
96
|
-
<text fg={t.textDim}>{` Provider: ${props.provider}`}</text>
|
|
97
|
-
<text>{""}</text>
|
|
98
|
-
{isCustom ? (
|
|
99
|
-
<>
|
|
100
|
-
<text fg={t.text}>{" Enter custom model ID:"}</text>
|
|
101
|
-
<box flexDirection="row">
|
|
102
|
-
<text fg={t.primary}>{" > "}</text>
|
|
103
|
-
<text fg={t.text}>{customInput}</text>
|
|
104
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD}>{"_"}</text>
|
|
105
|
-
</box>
|
|
106
|
-
<text>{""}</text>
|
|
107
|
-
<text fg={t.textDim}>{" Enter Confirm Ctrl+U Clear Esc Back"}</text>
|
|
108
|
-
</>
|
|
109
|
-
) : (
|
|
110
|
-
<>
|
|
111
|
-
<select
|
|
112
|
-
options={selectOptions}
|
|
113
|
-
focused={!isCustom}
|
|
114
|
-
selectedIndex={initialIndex}
|
|
115
|
-
height={Math.min(selectOptions.length, 10)}
|
|
116
|
-
showDescription={true}
|
|
117
|
-
backgroundColor={customBg || undefined}
|
|
118
|
-
focusedBackgroundColor={customBg || undefined}
|
|
119
|
-
textColor={t.text}
|
|
120
|
-
focusedTextColor={t.text}
|
|
121
|
-
selectedBackgroundColor={t.primary}
|
|
122
|
-
selectedTextColor={t.bg}
|
|
123
|
-
descriptionColor={t.textDim}
|
|
124
|
-
selectedDescriptionColor={t.bg}
|
|
125
|
-
showScrollIndicator={selectOptions.length > 10}
|
|
126
|
-
onSelect={(_index: number, option: SelectOption | null) => {
|
|
127
|
-
if (!option) return;
|
|
128
|
-
if (option.value === "__custom__") {
|
|
129
|
-
setIsCustom(true);
|
|
130
|
-
setCustomInput("");
|
|
131
|
-
} else {
|
|
132
|
-
props.onSelect(option.value);
|
|
133
|
-
}
|
|
134
|
-
}}
|
|
135
|
-
/>
|
|
136
|
-
<text>{""}</text>
|
|
137
|
-
<text fg={t.textDim}>{" ↑↓ Navigate Enter Select Esc Close"}</text>
|
|
138
|
-
</>
|
|
139
|
-
)}
|
|
140
|
-
</box>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DialogStatus — system status dialog showing provider, tools, config info.
|
|
3
|
-
* Uses native <scrollbox> for smooth scrolling through sections.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { TextAttributes } from "@opentui/core";
|
|
7
|
-
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
8
|
-
import { useTheme } from "../context/theme";
|
|
9
|
-
import { useSDK } from "../context/sdk";
|
|
10
|
-
|
|
11
|
-
export function DialogStatus(props: { onClose: () => void }) {
|
|
12
|
-
const { theme, customBg } = useTheme();
|
|
13
|
-
const t = theme;
|
|
14
|
-
const sdk = useSDK();
|
|
15
|
-
const dims = useTerminalDimensions();
|
|
16
|
-
|
|
17
|
-
const dialogWidth = Math.min(60, (dims.width || 80) - 4);
|
|
18
|
-
const dialogHeight = Math.max(10, (dims.height || 24) - 6);
|
|
19
|
-
|
|
20
|
-
// Gather status info
|
|
21
|
-
const allTools = sdk.registry.getAll ? sdk.registry.getAll() : [];
|
|
22
|
-
const toolNames = Array.isArray(allTools)
|
|
23
|
-
? allTools.map((tool: any) =>
|
|
24
|
-
tool.definition?.name || tool.name || "unknown"
|
|
25
|
-
)
|
|
26
|
-
: [];
|
|
27
|
-
|
|
28
|
-
useKeyboard((key: any) => {
|
|
29
|
-
if (key.name === "escape" || key.name === "q") props.onClose();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<box
|
|
34
|
-
borderStyle="double"
|
|
35
|
-
borderColor={t.primary}
|
|
36
|
-
backgroundColor={customBg || t.bg}
|
|
37
|
-
paddingX={1}
|
|
38
|
-
paddingY={1}
|
|
39
|
-
flexDirection="column"
|
|
40
|
-
position="absolute"
|
|
41
|
-
top={Math.max(1, Math.floor((dims.height || 24) * 0.1))}
|
|
42
|
-
left={Math.max(1, Math.floor(((dims.width || 80) - dialogWidth) / 2))}
|
|
43
|
-
width={dialogWidth}
|
|
44
|
-
height={dialogHeight}
|
|
45
|
-
>
|
|
46
|
-
{/* Title bar */}
|
|
47
|
-
<box flexDirection="row" flexShrink={0}>
|
|
48
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
|
|
49
|
-
{" System Status"}
|
|
50
|
-
</text>
|
|
51
|
-
<text fg={t.textDim}>{"esc"}</text>
|
|
52
|
-
</box>
|
|
53
|
-
<text flexShrink={0}>{""}</text>
|
|
54
|
-
|
|
55
|
-
<scrollbox flexGrow={1}>
|
|
56
|
-
<box flexShrink={0}>
|
|
57
|
-
{/* Provider */}
|
|
58
|
-
<text fg={t.secondary} attributes={TextAttributes.BOLD}>{" Provider"}</text>
|
|
59
|
-
<box flexDirection="row"><text fg={t.textMuted}>{" Provider "}</text><text fg={t.text}>{sdk.provider}</text></box>
|
|
60
|
-
<box flexDirection="row"><text fg={t.textMuted}>{" Model "}</text><text fg={t.text}>{sdk.model}</text></box>
|
|
61
|
-
<box flexDirection="row"><text fg={t.textMuted}>{" Directory "}</text><text fg={t.text}>{sdk.workingDir}</text></box>
|
|
62
|
-
<text>{""}</text>
|
|
63
|
-
|
|
64
|
-
{/* System */}
|
|
65
|
-
<text fg={t.secondary} attributes={TextAttributes.BOLD}>{" System"}</text>
|
|
66
|
-
<box flexDirection="row"><text fg={t.textMuted}>{" Node "}</text><text fg={t.text}>{process.version}</text></box>
|
|
67
|
-
<box flexDirection="row"><text fg={t.textMuted}>{" Platform "}</text><text fg={t.text}>{`${process.platform} ${process.arch}`}</text></box>
|
|
68
|
-
<box flexDirection="row"><text fg={t.textMuted}>{" Terminal "}</text><text fg={t.text}>{process.env.TERM_PROGRAM || process.env.TERM || "unknown"}</text></box>
|
|
69
|
-
<box flexDirection="row"><text fg={t.textMuted}>{" Shell "}</text><text fg={t.text}>{process.env.SHELL || "unknown"}</text></box>
|
|
70
|
-
<text>{""}</text>
|
|
71
|
-
|
|
72
|
-
{/* Tools */}
|
|
73
|
-
<text fg={t.secondary} attributes={TextAttributes.BOLD}>{` Tools (${toolNames.length})`}</text>
|
|
74
|
-
{toolNames.map((name: string) => (
|
|
75
|
-
<box key={name} flexDirection="row">
|
|
76
|
-
<text fg={t.textMuted}>{" • "}</text>
|
|
77
|
-
<text fg={t.text}>{name}</text>
|
|
78
|
-
</box>
|
|
79
|
-
))}
|
|
80
|
-
</box>
|
|
81
|
-
</scrollbox>
|
|
82
|
-
</box>
|
|
83
|
-
);
|
|
84
|
-
}
|