@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.
- package/dist/index.js +48 -0
- package/dist/index.js.map +7 -0
- package/esbuild.config.cjs +44 -0
- package/package.json +34 -0
- package/src/app.tsx +566 -0
- package/src/components/dialog-command.tsx +204 -0
- package/src/components/dialog-help.tsx +227 -0
- package/src/components/dialog-model.tsx +93 -0
- package/src/components/dialog-status.tsx +122 -0
- package/src/components/dialog-theme.tsx +292 -0
- package/src/components/input-area.tsx +318 -0
- package/src/components/loading-spinner.tsx +28 -0
- package/src/components/message-list.tsx +338 -0
- package/src/components/permission-prompt.tsx +71 -0
- package/src/components/session-browser.tsx +220 -0
- package/src/components/session-footer.tsx +30 -0
- package/src/components/session-header.tsx +39 -0
- package/src/components/setup-wizard.tsx +463 -0
- package/src/components/sidebar.tsx +130 -0
- package/src/components/status-bar.tsx +76 -0
- package/src/components/toast.tsx +139 -0
- package/src/context/sdk.tsx +40 -0
- package/src/context/theme.tsx +532 -0
- package/src/index.ts +50 -0
- package/src/lib/autocomplete.ts +258 -0
- package/src/lib/context-providers.ts +98 -0
- package/src/lib/history.ts +164 -0
- package/src/lib/terminal-title.ts +15 -0
- package/src/routes/home.tsx +148 -0
- package/src/routes/session.tsx +1186 -0
- package/tsconfig.json +23 -0
|
@@ -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
|
+
}
|