swiftroutercli 4.0.2 → 4.0.4
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 +24 -11
- package/dist/ui/Chat.js +45 -54
- package/package.json +2 -1
- package/src/index.ts +24 -11
- package/src/ui/Chat.tsx +59 -55
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import * as readline from "readline/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
import { loadConfig, saveConfig } from "./config.js";
|
|
7
|
-
import { fetchModels } from "./api/client.js";
|
|
8
7
|
import { startChat } from "./ui/Chat.js";
|
|
8
|
+
import { fetchModels } from "./api/client.js";
|
|
9
9
|
import { DEFAULT_BASE_URL, DEFAULT_MODEL, CONFIG_FILE } from "./constants.js";
|
|
10
10
|
import terminalImage from "terminal-image";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
@@ -52,10 +52,26 @@ async function ensureConfig() {
|
|
|
52
52
|
console.log(chalk.green("\n✅ Configuration saved securely! Starting your session...\n"));
|
|
53
53
|
return config;
|
|
54
54
|
}
|
|
55
|
+
async function resolveModel(config) {
|
|
56
|
+
const spinner = ora("Auto-detecting model from SwiftRouter...").start();
|
|
57
|
+
try {
|
|
58
|
+
const models = await fetchModels(config);
|
|
59
|
+
if (models.length > 0) {
|
|
60
|
+
const picked = models[0].id || models[0].name || DEFAULT_MODEL;
|
|
61
|
+
spinner.succeed(chalk.green(`Auto-selected model: ${picked}`));
|
|
62
|
+
return picked;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
// Silently fall back
|
|
67
|
+
}
|
|
68
|
+
spinner.warn(chalk.yellow(`Could not auto-detect. Falling back to: ${DEFAULT_MODEL}`));
|
|
69
|
+
return DEFAULT_MODEL;
|
|
70
|
+
}
|
|
55
71
|
program
|
|
56
72
|
.name("swiftroutercli")
|
|
57
73
|
.description("CLI for SwiftRouter AI Gateway")
|
|
58
|
-
.version("4.0.
|
|
74
|
+
.version("4.0.4");
|
|
59
75
|
program
|
|
60
76
|
.command("config")
|
|
61
77
|
.description("Manually configure the CLI with your SwiftRouter API Key and Base URL")
|
|
@@ -102,17 +118,13 @@ program
|
|
|
102
118
|
.command("chat")
|
|
103
119
|
.description("Start an interactive chat session")
|
|
104
120
|
.argument("[prompt]", "Initial prompt to start the chat")
|
|
105
|
-
.option("-m, --model <model>", "Model to use
|
|
121
|
+
.option("-m, --model <model>", "Model to use (auto-detected from server if not specified)")
|
|
106
122
|
.option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
|
|
107
123
|
.option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
|
|
108
124
|
.action(async (prompt, options) => {
|
|
109
125
|
const config = await ensureConfig();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
startChat(config, options.model, prompt, options.approvalMode, options.quiet);
|
|
115
|
-
}
|
|
126
|
+
const model = options.model || await resolveModel(config);
|
|
127
|
+
startChat(config, model, prompt || "", options.approvalMode, options.quiet);
|
|
116
128
|
});
|
|
117
129
|
program
|
|
118
130
|
.command("status")
|
|
@@ -152,11 +164,12 @@ program
|
|
|
152
164
|
// Default action: if no subcommand is given, launch chat (just like `codex`)
|
|
153
165
|
program
|
|
154
166
|
.argument("[prompt]", "Initial prompt to start chat directly")
|
|
155
|
-
.option("-m, --model <model>", "Model to use
|
|
167
|
+
.option("-m, --model <model>", "Model to use (auto-detected from server if not specified)")
|
|
156
168
|
.option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
|
|
157
169
|
.option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
|
|
158
170
|
.action(async (prompt, options) => {
|
|
159
171
|
const config = await ensureConfig();
|
|
160
|
-
|
|
172
|
+
const model = options.model || await resolveModel(config);
|
|
173
|
+
startChat(config, model, prompt || "", options.approvalMode, options.quiet);
|
|
161
174
|
});
|
|
162
175
|
program.parse();
|
package/dist/ui/Chat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { render, Box, Text, useInput, Static } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
3
4
|
import { streamChatCompletion, fetchModels } from "../api/client.js";
|
|
4
5
|
import chalk from "chalk";
|
|
5
6
|
import { loadHistory, saveHistory } from "../config.js";
|
|
@@ -12,11 +13,10 @@ marked.setOptions({
|
|
|
12
13
|
renderer: new TerminalRenderer()
|
|
13
14
|
});
|
|
14
15
|
const Chat = ({ config, model, initialPrompt, approvalMode }) => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
]);
|
|
16
|
+
const hasInitialPrompt = initialPrompt.trim().length > 0;
|
|
17
|
+
const [messages, setMessages] = useState(hasInitialPrompt ? [{ id: 0, role: "user", content: initialPrompt }] : []);
|
|
18
18
|
const [streamingContent, setStreamingContent] = useState("");
|
|
19
|
-
const [isStreaming, setIsStreaming] = useState(
|
|
19
|
+
const [isStreaming, setIsStreaming] = useState(hasInitialPrompt);
|
|
20
20
|
const [error, setError] = useState(null);
|
|
21
21
|
const [input, setInput] = useState("");
|
|
22
22
|
const [history, setHistory] = useState([]);
|
|
@@ -56,7 +56,6 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
|
|
|
56
56
|
{ id: prev.length, role: "system", content: `Sandbox Output:\n${output || "Done"}` }
|
|
57
57
|
]);
|
|
58
58
|
setIsExecuting(false);
|
|
59
|
-
// Recursive automation could go here in V5
|
|
60
59
|
});
|
|
61
60
|
}
|
|
62
61
|
else {
|
|
@@ -70,6 +69,7 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
|
|
|
70
69
|
setIsStreaming(false);
|
|
71
70
|
});
|
|
72
71
|
}, [messages, isStreaming, config, activeModel]);
|
|
72
|
+
// Handle command confirmation and history navigation
|
|
73
73
|
useInput((char, key) => {
|
|
74
74
|
if (isSelectingModel)
|
|
75
75
|
return;
|
|
@@ -93,44 +93,8 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
|
|
|
93
93
|
}
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
99
|
-
process.exit(0);
|
|
100
|
-
}
|
|
101
|
-
if (trimmed === "/clear") {
|
|
102
|
-
setMessages([]);
|
|
103
|
-
setInput("");
|
|
104
|
-
setHistoryIndex(-1);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (trimmed === "/models") {
|
|
108
|
-
setIsSelectingModel(true);
|
|
109
|
-
setIsLoadingModels(true);
|
|
110
|
-
setInput("");
|
|
111
|
-
fetchModels(config).then(models => {
|
|
112
|
-
const options = models.map(m => ({ label: m.id || m.name, value: m.id || m.name }));
|
|
113
|
-
setModelOptions(options.length ? options : [{ label: "mock-model", value: "mock-model" }]);
|
|
114
|
-
setIsLoadingModels(false);
|
|
115
|
-
}).catch(err => {
|
|
116
|
-
setError(err);
|
|
117
|
-
setIsSelectingModel(false);
|
|
118
|
-
setIsLoadingModels(false);
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (trimmed.length > 0) {
|
|
123
|
-
const newPrompt = trimmed;
|
|
124
|
-
setMessages((prev) => [...prev, { id: prev.length, role: "user", content: newPrompt }]);
|
|
125
|
-
const newHistory = [...history, newPrompt];
|
|
126
|
-
setHistory(newHistory);
|
|
127
|
-
saveHistory(newHistory);
|
|
128
|
-
setInput("");
|
|
129
|
-
setHistoryIndex(-1);
|
|
130
|
-
setIsStreaming(true);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else if (key.upArrow) {
|
|
96
|
+
// History navigation with Up/Down arrows
|
|
97
|
+
if (key.upArrow) {
|
|
134
98
|
if (history.length > 0 && historyIndex < history.length - 1) {
|
|
135
99
|
const newIdx = historyIndex + 1;
|
|
136
100
|
setHistoryIndex(newIdx);
|
|
@@ -148,13 +112,43 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
|
|
|
148
112
|
setInput("");
|
|
149
113
|
}
|
|
150
114
|
}
|
|
151
|
-
|
|
152
|
-
|
|
115
|
+
});
|
|
116
|
+
const handleSubmit = (value) => {
|
|
117
|
+
const trimmed = value.trim();
|
|
118
|
+
if (!trimmed)
|
|
119
|
+
return;
|
|
120
|
+
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
121
|
+
process.exit(0);
|
|
153
122
|
}
|
|
154
|
-
|
|
155
|
-
|
|
123
|
+
if (trimmed === "/clear") {
|
|
124
|
+
setMessages([]);
|
|
125
|
+
setInput("");
|
|
126
|
+
setHistoryIndex(-1);
|
|
127
|
+
return;
|
|
156
128
|
}
|
|
157
|
-
|
|
129
|
+
if (trimmed === "/models") {
|
|
130
|
+
setIsSelectingModel(true);
|
|
131
|
+
setIsLoadingModels(true);
|
|
132
|
+
setInput("");
|
|
133
|
+
fetchModels(config).then(models => {
|
|
134
|
+
const options = models.map(m => ({ label: m.id || m.name, value: m.id || m.name }));
|
|
135
|
+
setModelOptions(options.length ? options : [{ label: "mock-model", value: "mock-model" }]);
|
|
136
|
+
setIsLoadingModels(false);
|
|
137
|
+
}).catch(err => {
|
|
138
|
+
setError(err);
|
|
139
|
+
setIsSelectingModel(false);
|
|
140
|
+
setIsLoadingModels(false);
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
setMessages((prev) => [...prev, { id: prev.length, role: "user", content: trimmed }]);
|
|
145
|
+
const newHistory = [...history, trimmed];
|
|
146
|
+
setHistory(newHistory);
|
|
147
|
+
saveHistory(newHistory);
|
|
148
|
+
setInput("");
|
|
149
|
+
setHistoryIndex(-1);
|
|
150
|
+
setIsStreaming(true);
|
|
151
|
+
};
|
|
158
152
|
return (React.createElement(React.Fragment, null,
|
|
159
153
|
React.createElement(Static, { items: messages }, msg => (React.createElement(Box, { key: msg.id, flexDirection: "column", marginBottom: 1 },
|
|
160
154
|
React.createElement(Text, { bold: true, color: msg.role === "user" ? "blue" : "green" },
|
|
@@ -175,10 +169,9 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
|
|
|
175
169
|
setActiveModel(item.value);
|
|
176
170
|
setIsSelectingModel(false);
|
|
177
171
|
} })))) : (React.createElement(React.Fragment, null,
|
|
178
|
-
!isStreaming && !pendingCommand && !isExecuting && (React.createElement(Box, {
|
|
179
|
-
React.createElement(Text, { bold: true, color: "blue" }, "
|
|
180
|
-
React.createElement(
|
|
181
|
-
React.createElement(Text, { dimColor: true }, " \u2588"))),
|
|
172
|
+
!isStreaming && !pendingCommand && !isExecuting && (React.createElement(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1 },
|
|
173
|
+
React.createElement(Text, { bold: true, color: "blue" }, "\u276F "),
|
|
174
|
+
React.createElement(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "Type a message... (/models, /clear, /exit)" }))),
|
|
182
175
|
pendingCommand && !isExecuting && (React.createElement(Box, { flexDirection: "row", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1 },
|
|
183
176
|
React.createElement(Text, { color: "yellow" }, "Execute suggested command? "),
|
|
184
177
|
React.createElement(Text, { bold: true }, pendingCommand),
|
|
@@ -215,8 +208,6 @@ export function startChat(config, model, prompt, approvalMode = "suggest", quiet
|
|
|
215
208
|
fullResponse += text;
|
|
216
209
|
}, () => {
|
|
217
210
|
console.log("\n");
|
|
218
|
-
// If full-auto is requested in quiet mode, we would parse bash blocks and execute them here.
|
|
219
|
-
// For now, headless simply streams the raw text response for CI pipelines.
|
|
220
211
|
if (approvalMode === "full-auto") {
|
|
221
212
|
const bashMatch = fullResponse.match(/```(?:bash|sh)\n([\s\S]*?)\n```/);
|
|
222
213
|
if (bashMatch) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swiftroutercli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
4
4
|
"description": "The official SwiftRouter Command Line Interface using React Ink Components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"eventsource-parser": "^3.0.6",
|
|
25
25
|
"ink": "^6.8.0",
|
|
26
26
|
"ink-select-input": "^6.2.0",
|
|
27
|
+
"ink-text-input": "^6.0.0",
|
|
27
28
|
"marked": "^15.0.12",
|
|
28
29
|
"marked-terminal": "^7.3.0",
|
|
29
30
|
"ora": "^5.4.1",
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@ import * as readline from "readline/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
import { loadConfig, saveConfig, Config } from "./config.js";
|
|
7
|
-
import { fetchModels } from "./api/client.js";
|
|
8
7
|
import { startChat } from "./ui/Chat.js";
|
|
8
|
+
import { fetchModels } from "./api/client.js";
|
|
9
9
|
import { DEFAULT_BASE_URL, DEFAULT_MODEL, CONFIG_FILE } from "./constants.js";
|
|
10
10
|
import terminalImage from "terminal-image";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
@@ -65,10 +65,26 @@ async function ensureConfig(): Promise<Config> {
|
|
|
65
65
|
return config;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
async function resolveModel(config: Config): Promise<string> {
|
|
69
|
+
const spinner = ora("Auto-detecting model from SwiftRouter...").start();
|
|
70
|
+
try {
|
|
71
|
+
const models = await fetchModels(config);
|
|
72
|
+
if (models.length > 0) {
|
|
73
|
+
const picked = models[0].id || models[0].name || DEFAULT_MODEL;
|
|
74
|
+
spinner.succeed(chalk.green(`Auto-selected model: ${picked}`));
|
|
75
|
+
return picked;
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// Silently fall back
|
|
79
|
+
}
|
|
80
|
+
spinner.warn(chalk.yellow(`Could not auto-detect. Falling back to: ${DEFAULT_MODEL}`));
|
|
81
|
+
return DEFAULT_MODEL;
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
program
|
|
69
85
|
.name("swiftroutercli")
|
|
70
86
|
.description("CLI for SwiftRouter AI Gateway")
|
|
71
|
-
.version("4.0.
|
|
87
|
+
.version("4.0.4");
|
|
72
88
|
|
|
73
89
|
program
|
|
74
90
|
.command("config")
|
|
@@ -118,17 +134,13 @@ program
|
|
|
118
134
|
.command("chat")
|
|
119
135
|
.description("Start an interactive chat session")
|
|
120
136
|
.argument("[prompt]", "Initial prompt to start the chat")
|
|
121
|
-
.option("-m, --model <model>", "Model to use
|
|
137
|
+
.option("-m, --model <model>", "Model to use (auto-detected from server if not specified)")
|
|
122
138
|
.option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
|
|
123
139
|
.option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
|
|
124
140
|
.action(async (prompt, options) => {
|
|
125
141
|
const config = await ensureConfig();
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
startChat(config, options.model, "", options.approvalMode, options.quiet);
|
|
129
|
-
} else {
|
|
130
|
-
startChat(config, options.model, prompt, options.approvalMode, options.quiet);
|
|
131
|
-
}
|
|
142
|
+
const model = options.model || await resolveModel(config);
|
|
143
|
+
startChat(config, model, prompt || "", options.approvalMode, options.quiet);
|
|
132
144
|
});
|
|
133
145
|
|
|
134
146
|
program
|
|
@@ -171,12 +183,13 @@ program
|
|
|
171
183
|
// Default action: if no subcommand is given, launch chat (just like `codex`)
|
|
172
184
|
program
|
|
173
185
|
.argument("[prompt]", "Initial prompt to start chat directly")
|
|
174
|
-
.option("-m, --model <model>", "Model to use
|
|
186
|
+
.option("-m, --model <model>", "Model to use (auto-detected from server if not specified)")
|
|
175
187
|
.option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
|
|
176
188
|
.option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
|
|
177
189
|
.action(async (prompt, options) => {
|
|
178
190
|
const config = await ensureConfig();
|
|
179
|
-
|
|
191
|
+
const model = options.model || await resolveModel(config);
|
|
192
|
+
startChat(config, model, prompt || "", options.approvalMode, options.quiet);
|
|
180
193
|
});
|
|
181
194
|
|
|
182
195
|
program.parse();
|
package/src/ui/Chat.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { render, Box, Text, useInput, Static } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
3
4
|
import { streamChatCompletion, fetchModels } from "../api/client.js";
|
|
4
5
|
import chalk from "chalk";
|
|
5
6
|
import { Config, loadHistory, saveHistory } from "../config.js";
|
|
@@ -26,11 +27,13 @@ interface ChatProps {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode }) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
]
|
|
30
|
+
const hasInitialPrompt = initialPrompt.trim().length > 0;
|
|
31
|
+
|
|
32
|
+
const [messages, setMessages] = useState<{ id: number; role: string; content: string }[]>(
|
|
33
|
+
hasInitialPrompt ? [{ id: 0, role: "user", content: initialPrompt }] : []
|
|
34
|
+
);
|
|
32
35
|
const [streamingContent, setStreamingContent] = useState("");
|
|
33
|
-
const [isStreaming, setIsStreaming] = useState(
|
|
36
|
+
const [isStreaming, setIsStreaming] = useState(hasInitialPrompt);
|
|
34
37
|
const [error, setError] = useState<Error | null>(null);
|
|
35
38
|
const [input, setInput] = useState("");
|
|
36
39
|
const [history, setHistory] = useState<string[]>([]);
|
|
@@ -82,7 +85,6 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
|
|
|
82
85
|
{ id: prev.length, role: "system", content: `Sandbox Output:\n${output || "Done"}` }
|
|
83
86
|
]);
|
|
84
87
|
setIsExecuting(false);
|
|
85
|
-
// Recursive automation could go here in V5
|
|
86
88
|
});
|
|
87
89
|
} else {
|
|
88
90
|
setPendingCommand(command);
|
|
@@ -99,6 +101,7 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
|
|
|
99
101
|
);
|
|
100
102
|
}, [messages, isStreaming, config, activeModel]);
|
|
101
103
|
|
|
104
|
+
// Handle command confirmation and history navigation
|
|
102
105
|
useInput((char: string, key: any) => {
|
|
103
106
|
if (isSelectingModel) return;
|
|
104
107
|
if (isStreaming) return;
|
|
@@ -121,46 +124,8 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
|
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
127
|
-
process.exit(0);
|
|
128
|
-
}
|
|
129
|
-
if (trimmed === "/clear") {
|
|
130
|
-
setMessages([]);
|
|
131
|
-
setInput("");
|
|
132
|
-
setHistoryIndex(-1);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (trimmed === "/models") {
|
|
136
|
-
setIsSelectingModel(true);
|
|
137
|
-
setIsLoadingModels(true);
|
|
138
|
-
setInput("");
|
|
139
|
-
fetchModels(config).then(models => {
|
|
140
|
-
const options = models.map(m => ({ label: m.id || m.name, value: m.id || m.name }));
|
|
141
|
-
setModelOptions(options.length ? options : [{ label: "mock-model", value: "mock-model" }]);
|
|
142
|
-
setIsLoadingModels(false);
|
|
143
|
-
}).catch(err => {
|
|
144
|
-
setError(err);
|
|
145
|
-
setIsSelectingModel(false);
|
|
146
|
-
setIsLoadingModels(false);
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (trimmed.length > 0) {
|
|
152
|
-
const newPrompt = trimmed;
|
|
153
|
-
setMessages((prev) => [...prev, { id: prev.length, role: "user", content: newPrompt }]);
|
|
154
|
-
|
|
155
|
-
const newHistory = [...history, newPrompt];
|
|
156
|
-
setHistory(newHistory);
|
|
157
|
-
saveHistory(newHistory);
|
|
158
|
-
|
|
159
|
-
setInput("");
|
|
160
|
-
setHistoryIndex(-1);
|
|
161
|
-
setIsStreaming(true);
|
|
162
|
-
}
|
|
163
|
-
} else if (key.upArrow) {
|
|
127
|
+
// History navigation with Up/Down arrows
|
|
128
|
+
if (key.upArrow) {
|
|
164
129
|
if (history.length > 0 && historyIndex < history.length - 1) {
|
|
165
130
|
const newIdx = historyIndex + 1;
|
|
166
131
|
setHistoryIndex(newIdx);
|
|
@@ -175,13 +140,49 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
|
|
|
175
140
|
setHistoryIndex(-1);
|
|
176
141
|
setInput("");
|
|
177
142
|
}
|
|
178
|
-
} else if (key.backspace || key.delete) {
|
|
179
|
-
setInput((prev) => prev.slice(0, -1));
|
|
180
|
-
} else if (char) {
|
|
181
|
-
setInput((prev) => prev + char);
|
|
182
143
|
}
|
|
183
144
|
});
|
|
184
145
|
|
|
146
|
+
const handleSubmit = (value: string) => {
|
|
147
|
+
const trimmed = value.trim();
|
|
148
|
+
if (!trimmed) return;
|
|
149
|
+
|
|
150
|
+
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
if (trimmed === "/clear") {
|
|
154
|
+
setMessages([]);
|
|
155
|
+
setInput("");
|
|
156
|
+
setHistoryIndex(-1);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (trimmed === "/models") {
|
|
160
|
+
setIsSelectingModel(true);
|
|
161
|
+
setIsLoadingModels(true);
|
|
162
|
+
setInput("");
|
|
163
|
+
fetchModels(config).then(models => {
|
|
164
|
+
const options = models.map(m => ({ label: m.id || m.name, value: m.id || m.name }));
|
|
165
|
+
setModelOptions(options.length ? options : [{ label: "mock-model", value: "mock-model" }]);
|
|
166
|
+
setIsLoadingModels(false);
|
|
167
|
+
}).catch(err => {
|
|
168
|
+
setError(err);
|
|
169
|
+
setIsSelectingModel(false);
|
|
170
|
+
setIsLoadingModels(false);
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setMessages((prev) => [...prev, { id: prev.length, role: "user", content: trimmed }]);
|
|
176
|
+
|
|
177
|
+
const newHistory = [...history, trimmed];
|
|
178
|
+
setHistory(newHistory);
|
|
179
|
+
saveHistory(newHistory);
|
|
180
|
+
|
|
181
|
+
setInput("");
|
|
182
|
+
setHistoryIndex(-1);
|
|
183
|
+
setIsStreaming(true);
|
|
184
|
+
};
|
|
185
|
+
|
|
185
186
|
return (
|
|
186
187
|
<>
|
|
187
188
|
<Static items={messages}>
|
|
@@ -224,10 +225,15 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
|
|
|
224
225
|
) : (
|
|
225
226
|
<>
|
|
226
227
|
{!isStreaming && !pendingCommand && !isExecuting && (
|
|
227
|
-
<Box
|
|
228
|
-
<Text bold color="blue"
|
|
229
|
-
|
|
230
|
-
<
|
|
228
|
+
<Box borderStyle="round" borderColor="blue" paddingX={1}>
|
|
229
|
+
<Text bold color="blue">❯ </Text>
|
|
230
|
+
{/* @ts-ignore */}
|
|
231
|
+
<TextInput
|
|
232
|
+
value={input}
|
|
233
|
+
onChange={setInput}
|
|
234
|
+
onSubmit={handleSubmit}
|
|
235
|
+
placeholder="Type a message... (/models, /clear, /exit)"
|
|
236
|
+
/>
|
|
231
237
|
</Box>
|
|
232
238
|
)}
|
|
233
239
|
|
|
@@ -293,8 +299,6 @@ export function startChat(config: Config, model: string, prompt: string, approva
|
|
|
293
299
|
() => {
|
|
294
300
|
console.log("\n");
|
|
295
301
|
|
|
296
|
-
// If full-auto is requested in quiet mode, we would parse bash blocks and execute them here.
|
|
297
|
-
// For now, headless simply streams the raw text response for CI pipelines.
|
|
298
302
|
if (approvalMode === "full-auto") {
|
|
299
303
|
const bashMatch = fullResponse.match(/```(?:bash|sh)\n([\s\S]*?)\n```/);
|
|
300
304
|
if (bashMatch) {
|