@cdoing/opentuicli 0.1.6 → 0.1.18
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 +53 -38
- package/dist/index.js.map +4 -4
- package/package.json +5 -4
- package/src/app.tsx +260 -39
- package/src/components/dialog-command.tsx +110 -107
- package/src/components/dialog-help.tsx +48 -124
- package/src/components/dialog-model.tsx +98 -49
- package/src/components/dialog-status.tsx +46 -84
- package/src/components/dialog-theme.tsx +197 -171
- package/src/components/input-area.tsx +74 -12
- package/src/components/message-list.tsx +250 -42
- package/src/components/permission-prompt.tsx +2 -1
- package/src/components/session-browser.tsx +71 -60
- package/src/components/session-footer.tsx +2 -2
- package/src/components/session-header.tsx +1 -1
- package/src/components/setup-wizard.tsx +149 -70
- package/src/components/sidebar.tsx +66 -13
- package/src/components/status-bar.tsx +2 -2
- package/src/context/theme.tsx +109 -1
- package/src/lib/autocomplete.ts +5 -1
- package/src/routes/home.tsx +2 -2
- package/src/routes/session.tsx +141 -18
- package/src/store/settings.ts +107 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DialogModel — model picker dialog (Ctrl+
|
|
2
|
+
* DialogModel — model picker dialog (Ctrl+O)
|
|
3
|
+
*
|
|
4
|
+
* Uses OpenTUI <select> for the model list with proper
|
|
5
|
+
* highlight styling and keyboard navigation.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { TextAttributes } from "@opentui/core";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
9
|
+
import type { SelectOption } from "@opentui/core";
|
|
10
|
+
import { useState, useMemo } from "react";
|
|
11
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
8
12
|
import { useTheme } from "../context/theme";
|
|
13
|
+
import { getProviders } from "@cdoing/ai";
|
|
9
14
|
|
|
10
15
|
export interface ModelOption {
|
|
11
16
|
id: string;
|
|
@@ -13,22 +18,11 @@ export interface ModelOption {
|
|
|
13
18
|
hint?: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
};
|
|
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
|
+
}
|
|
32
26
|
|
|
33
27
|
export function DialogModel(props: {
|
|
34
28
|
provider: string;
|
|
@@ -36,23 +30,50 @@ export function DialogModel(props: {
|
|
|
36
30
|
onSelect: (model: string) => void;
|
|
37
31
|
onClose: () => void;
|
|
38
32
|
}) {
|
|
39
|
-
const { theme } = useTheme();
|
|
33
|
+
const { theme, customBg } = useTheme();
|
|
40
34
|
const t = theme;
|
|
35
|
+
const dims = useTerminalDimensions();
|
|
41
36
|
const models = MODELS[props.provider] || [];
|
|
42
|
-
const [
|
|
43
|
-
|
|
44
|
-
|
|
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));
|
|
45
59
|
|
|
46
60
|
useKeyboard((key: any) => {
|
|
47
61
|
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
62
|
+
if (isCustom) { setIsCustom(false); return; }
|
|
48
63
|
props.onClose();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const m =
|
|
55
|
-
if (m) props.onSelect(m
|
|
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);
|
|
56
77
|
}
|
|
57
78
|
});
|
|
58
79
|
|
|
@@ -60,34 +81,62 @@ export function DialogModel(props: {
|
|
|
60
81
|
<box
|
|
61
82
|
borderStyle="double"
|
|
62
83
|
borderColor={t.primary}
|
|
84
|
+
backgroundColor={customBg || t.bg}
|
|
63
85
|
paddingX={1}
|
|
64
86
|
paddingY={1}
|
|
65
87
|
flexDirection="column"
|
|
66
88
|
position="absolute"
|
|
67
|
-
top=
|
|
68
|
-
left=
|
|
69
|
-
width=
|
|
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)}
|
|
70
92
|
>
|
|
71
93
|
<text fg={t.primary} attributes={TextAttributes.BOLD}>
|
|
72
94
|
{" Select Model"}
|
|
73
95
|
</text>
|
|
74
96
|
<text fg={t.textDim}>{` Provider: ${props.provider}`}</text>
|
|
75
|
-
<text
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
<text
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
</
|
|
84
|
-
<text
|
|
85
|
-
<text fg={
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
)}
|
|
91
140
|
</box>
|
|
92
141
|
);
|
|
93
142
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DialogStatus — system status dialog showing provider, tools, config info.
|
|
3
|
-
*
|
|
3
|
+
* Uses native <scrollbox> for smooth scrolling through sections.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { TextAttributes } from "@opentui/core";
|
|
7
|
-
import { useState } from "react";
|
|
8
7
|
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
9
8
|
import { useTheme } from "../context/theme";
|
|
10
9
|
import { useSDK } from "../context/sdk";
|
|
11
10
|
|
|
12
11
|
export function DialogStatus(props: { onClose: () => void }) {
|
|
13
|
-
const { theme } = useTheme();
|
|
12
|
+
const { theme, customBg } = useTheme();
|
|
14
13
|
const t = theme;
|
|
15
14
|
const sdk = useSDK();
|
|
16
15
|
const dims = useTerminalDimensions();
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
const dialogWidth = Math.min(60, (dims.width || 80) - 4);
|
|
18
|
+
const dialogHeight = Math.max(10, (dims.height || 24) - 6);
|
|
18
19
|
|
|
19
20
|
// Gather status info
|
|
20
21
|
const allTools = sdk.registry.getAll ? sdk.registry.getAll() : [];
|
|
@@ -24,99 +25,60 @@ export function DialogStatus(props: { onClose: () => void }) {
|
|
|
24
25
|
)
|
|
25
26
|
: [];
|
|
26
27
|
|
|
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
28
|
useKeyboard((key: any) => {
|
|
71
29
|
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
30
|
});
|
|
77
31
|
|
|
78
|
-
const visible = lines.slice(scrollOffset, scrollOffset + maxVisible);
|
|
79
|
-
|
|
80
32
|
return (
|
|
81
33
|
<box
|
|
82
34
|
borderStyle="double"
|
|
83
35
|
borderColor={t.primary}
|
|
36
|
+
backgroundColor={customBg || t.bg}
|
|
84
37
|
paddingX={1}
|
|
85
38
|
paddingY={1}
|
|
86
39
|
flexDirection="column"
|
|
87
40
|
position="absolute"
|
|
88
|
-
top=
|
|
89
|
-
left=
|
|
90
|
-
width=
|
|
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}
|
|
91
45
|
>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
</box>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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>
|
|
120
82
|
</box>
|
|
121
83
|
);
|
|
122
84
|
}
|