@nextxai/cli 0.0.0-stage → 0.1.1
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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/ai/client.js +221 -0
- package/dist/auth/ci.js +64 -0
- package/dist/auth/credentials.js +192 -0
- package/dist/auth/device.js +76 -0
- package/dist/auth/login.js +145 -0
- package/dist/auth/loopback.js +98 -0
- package/dist/auth/mcp-oauth.js +174 -0
- package/dist/auth/session.js +205 -0
- package/dist/bin/nextx.js +5 -0
- package/dist/commands/ai.js +514 -0
- package/dist/commands/api.js +87 -0
- package/dist/commands/auth.js +165 -0
- package/dist/commands/docs.js +55 -0
- package/dist/commands/help.js +48 -0
- package/dist/commands/keys.js +85 -0
- package/dist/commands/mcp.js +202 -0
- package/dist/commands/tools.js +186 -0
- package/dist/commands/util.js +224 -0
- package/dist/commands/webhooks.js +249 -0
- package/dist/commands/workspace.js +44 -0
- package/dist/core/brand.js +51 -0
- package/dist/core/config.js +52 -0
- package/dist/core/context.js +42 -0
- package/dist/core/errors.js +28 -0
- package/dist/core/http.js +170 -0
- package/dist/core/i18n.js +90 -0
- package/dist/core/lock.js +44 -0
- package/dist/core/output.js +74 -0
- package/dist/core/paths.js +29 -0
- package/dist/core/prompt.js +137 -0
- package/dist/core/version.js +13 -0
- package/dist/main.js +38 -0
- package/dist/mcp/client.js +114 -0
- package/dist/program.js +102 -0
- package/dist/repl/App.js +771 -0
- package/dist/repl/components.js +66 -0
- package/dist/repl/index.js +12 -0
- package/dist/repl/markdown.js +57 -0
- package/dist/repl/slash.js +53 -0
- package/package.json +62 -4
package/dist/repl/App.js
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { existsSync, readdirSync, statSync, appendFileSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
import { brand } from "../core/brand.js";
|
|
8
|
+
import { context, setContext } from "../core/context.js";
|
|
9
|
+
import { isCliError } from "../core/errors.js";
|
|
10
|
+
import { tr } from "../core/i18n.js";
|
|
11
|
+
import { c, table } from "../core/output.js";
|
|
12
|
+
import { get, post } from "../core/http.js";
|
|
13
|
+
import { getProfile, updateProfile, loadConfig, saveConfig } from "../core/config.js";
|
|
14
|
+
import { historyFile } from "../core/paths.js";
|
|
15
|
+
import { VERSION } from "../core/version.js";
|
|
16
|
+
import { ask, attach, attachmentKind, composeText, confirmAction, escalate, feedback, history as fetchHistory, listConversations, openConversation, pinConversation, renameConversation, riskOf, confirmWord, usage as fetchUsage, } from "../ai/client.js";
|
|
17
|
+
import { listWorkspaces, switchWorkspace, findWorkspace, logoutServer } from "../auth/session.js";
|
|
18
|
+
import { deleteSession } from "../auth/credentials.js";
|
|
19
|
+
import { findProjectNotes, notesFileName, proposalCard, renderBlocks, runLines } from "../commands/ai.js";
|
|
20
|
+
import { markdown } from "./markdown.js";
|
|
21
|
+
import { Editor, Menu, Select, edit } from "./components.js";
|
|
22
|
+
import { SLASH, SHORTCUTS, matchSlash } from "./slash.js";
|
|
23
|
+
const MODES = ["propose", "auto", "read"];
|
|
24
|
+
const modeLabel = (m) => m === "propose" ? c.cyan(tr("⏵⏵ đề xuất", "⏵⏵ propose")) : m === "auto" ? c.yellow(tr("⏵⏵ tự ghi việc thường", "⏵⏵ auto-write normal")) : c.dim(tr("⏸ chỉ đọc", "⏸ read only"));
|
|
25
|
+
let seq = 0;
|
|
26
|
+
const nextId = () => ++seq;
|
|
27
|
+
function loadHistory() {
|
|
28
|
+
try {
|
|
29
|
+
return readFileSync(historyFile(), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-500);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function saveHistory(line) {
|
|
36
|
+
try {
|
|
37
|
+
appendFileSync(historyFile(), `${JSON.stringify(line)}\n`, { mode: 0o600 });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// history is a convenience
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Files under cwd usable with @ (supported types only, shallow + 2 levels, ignores node_modules/.git). */
|
|
44
|
+
function attachableFiles() {
|
|
45
|
+
const root = process.cwd();
|
|
46
|
+
const out = [];
|
|
47
|
+
const walk = (dir, depth) => {
|
|
48
|
+
if (out.length > 300 || depth > 2)
|
|
49
|
+
return;
|
|
50
|
+
let names = [];
|
|
51
|
+
try {
|
|
52
|
+
names = readdirSync(dir);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const n of names) {
|
|
58
|
+
if (n.startsWith(".") || n === "node_modules" || n === "dist")
|
|
59
|
+
continue;
|
|
60
|
+
const p = join(dir, n);
|
|
61
|
+
let st;
|
|
62
|
+
try {
|
|
63
|
+
st = statSync(p);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (st.isDirectory())
|
|
69
|
+
walk(p, depth + 1);
|
|
70
|
+
else if (attachmentKind(p))
|
|
71
|
+
out.push(relative(root, p));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
walk(root, 0);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
export function App(props) {
|
|
78
|
+
const { exit } = useApp();
|
|
79
|
+
const { stdout } = useStdout();
|
|
80
|
+
const width = Math.max(40, Math.min(stdout?.columns || 100, 120));
|
|
81
|
+
const b = brand();
|
|
82
|
+
const [done, setDone] = useState([]);
|
|
83
|
+
const [live, setLive] = useState(null);
|
|
84
|
+
const liveRef = useRef(null);
|
|
85
|
+
const [, setTick] = useState(0);
|
|
86
|
+
const [value, setValue] = useState("");
|
|
87
|
+
const [cursor, setCursor] = useState(0);
|
|
88
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
89
|
+
const [menuOff, setMenuOff] = useState(false);
|
|
90
|
+
const [modal, setModal] = useState(null);
|
|
91
|
+
const [mode, setMode] = useState(context().mode);
|
|
92
|
+
const [agent, setAgent] = useState(context().agent);
|
|
93
|
+
const [sessionId, setSessionId] = useState(props.sessionId);
|
|
94
|
+
const [pending, setPending] = useState([]);
|
|
95
|
+
const [usageText, setUsageText] = useState("");
|
|
96
|
+
const [busy, setBusy] = useState(false);
|
|
97
|
+
const [lastOutcome, setLastOutcome] = useState();
|
|
98
|
+
const [hist] = useState(loadHistory);
|
|
99
|
+
const histIdx = useRef(-1);
|
|
100
|
+
const abortRef = useRef(null);
|
|
101
|
+
const ctrlC = useRef(0);
|
|
102
|
+
const lastEsc = useRef(0);
|
|
103
|
+
const lastSent = useRef("");
|
|
104
|
+
const filesCache = useRef(null);
|
|
105
|
+
const agentsCache = useRef(null);
|
|
106
|
+
const say = useCallback((text) => setDone((d) => [...d, { id: nextId(), text }]), []);
|
|
107
|
+
const sayErr = useCallback((e) => {
|
|
108
|
+
const msg = isCliError(e) ? `${c.red("✗")} ${e.message}${e.hint ? `\n ${c.dim(e.hint)}` : ""}` : `${c.red("✗")} ${e?.message ?? String(e)}`;
|
|
109
|
+
setDone((d) => [...d, { id: nextId(), text: msg }]);
|
|
110
|
+
}, []);
|
|
111
|
+
const profile = () => getProfile(context().profile);
|
|
112
|
+
const refreshUsage = useCallback(() => {
|
|
113
|
+
fetchUsage()
|
|
114
|
+
.then((u) => setUsageText(u.limit ? `${u.planName ?? ""} · ${u.remaining}/${u.limit} ${tr("lượt", "turns")}` : (u.planName ?? "")))
|
|
115
|
+
.catch(() => undefined);
|
|
116
|
+
}, []);
|
|
117
|
+
// ── header, once ──────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const p = profile();
|
|
120
|
+
const env = context().env === "prod" ? c.red("prod") : c.yellow("sandbox");
|
|
121
|
+
const notes = findProjectNotes();
|
|
122
|
+
const inner = width - 4;
|
|
123
|
+
const line1 = ` ${p?.userName ?? ""} · ${c.bold(p?.workspaceName ?? "")} · ${env}`;
|
|
124
|
+
const line2 = ` ${c.dim(`${process.cwd().replace(process.env.HOME ?? "", "~")}${notes ? ` (${notesFileName()})` : ""} · CLI ${VERSION}`)}`;
|
|
125
|
+
const strip = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
126
|
+
const pad = (s) => s + " ".repeat(Math.max(0, inner + 1 - [...strip(s)].length));
|
|
127
|
+
const title = `─ ${b.aiName} `;
|
|
128
|
+
say([
|
|
129
|
+
c.cyan(`╭${title}${"─".repeat(Math.max(0, inner - title.length + 2))}╮`),
|
|
130
|
+
`${c.cyan("│")}${pad(line1)} ${c.cyan("│")}`,
|
|
131
|
+
`${c.cyan("│")}${pad(line2)} ${c.cyan("│")}`,
|
|
132
|
+
c.cyan(`╰${"─".repeat(inner + 2)}╯`),
|
|
133
|
+
c.dim(tr(" / lệnh · @ đính kèm · # ghi nhớ · Shift+Tab chế độ · Esc dừng · Ctrl+C thoát", " / commands · @ attach · # note · Shift+Tab mode · Esc stop · Ctrl+C exit")),
|
|
134
|
+
].join("\n"));
|
|
135
|
+
refreshUsage();
|
|
136
|
+
void (async () => {
|
|
137
|
+
if (props.resumed)
|
|
138
|
+
await showHistory(props.sessionId, 6);
|
|
139
|
+
for (const f of props.files)
|
|
140
|
+
await addAttachment(f);
|
|
141
|
+
if (props.initialQuestion)
|
|
142
|
+
void send(props.initialQuestion);
|
|
143
|
+
})();
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
+
}, []);
|
|
146
|
+
// ── live redraw throttle ──────────────────────────────────────────────────────────────────────
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!busy)
|
|
149
|
+
return;
|
|
150
|
+
const t = setInterval(() => {
|
|
151
|
+
setLive(liveRef.current ? { ...liveRef.current } : null);
|
|
152
|
+
setTick((x) => x + 1);
|
|
153
|
+
}, 50);
|
|
154
|
+
return () => clearInterval(t);
|
|
155
|
+
}, [busy]);
|
|
156
|
+
async function showHistory(id, last) {
|
|
157
|
+
try {
|
|
158
|
+
const msgs = ((await fetchHistory(id)) ?? []).slice(-last);
|
|
159
|
+
for (const m of msgs)
|
|
160
|
+
say(m.role === "User" ? c.bold(`› ${m.text}`) : `${markdown(m.text)}${renderBlocks(m.blocks?.filter((x) => x.type !== "text")).map((l) => `\n${l}`).join("")}`);
|
|
161
|
+
if (msgs.length)
|
|
162
|
+
say(c.dim(tr(" — tiếp tục hội thoại —", " — conversation resumed —")));
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
sayErr(e);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function addAttachment(path) {
|
|
169
|
+
try {
|
|
170
|
+
const a = await attach(path, sessionId);
|
|
171
|
+
setPending((p) => [...p, a]);
|
|
172
|
+
say(c.dim(` ⏺ ${tr("Đính kèm", "Attached")} ${a.name} · ${a.summary ?? ""}`));
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
sayErr(e);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function pick(title, items, initial = 0) {
|
|
179
|
+
return new Promise((resolve) => setModal({ kind: "select", title, items, initial, resolve: (v) => { setModal(null); resolve(v); } }));
|
|
180
|
+
}
|
|
181
|
+
function askConfirm(p) {
|
|
182
|
+
return new Promise((resolve) => setModal({ kind: "confirm", proposal: p, typed: "", resolve: (yes) => { setModal(null); resolve(yes); } }));
|
|
183
|
+
}
|
|
184
|
+
// ── proposals ─────────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
async function handleProposal(p) {
|
|
186
|
+
if (!p.writes) {
|
|
187
|
+
say(` ${c.cyan("↗")} ${p.label}${p.confirmPrompt ? ` — ${p.confirmPrompt}` : ""}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const risk = riskOf(p);
|
|
191
|
+
say(proposalCard(p).join("\n"));
|
|
192
|
+
if (mode === "read")
|
|
193
|
+
return say(c.dim(tr(" Chế độ chỉ đọc: không ghi. Shift+Tab để đổi.", " Read-only mode: nothing written. Shift+Tab to change.")));
|
|
194
|
+
if (risk === "finance")
|
|
195
|
+
return say(c.dim(tr(` Việc tài chính chỉ xác nhận trong app: ${context().endpoints.app}`, ` Finance actions are confirmed in the app: ${context().endpoints.app}`)));
|
|
196
|
+
const yes = mode === "auto" && risk === "normal" ? true : await askConfirm(p);
|
|
197
|
+
if (!yes)
|
|
198
|
+
return say(c.dim(tr(` Không ghi gì. Đề xuất còn chờ tới khi hết hạn.`, ` Nothing written. The proposal waits until it expires.`)));
|
|
199
|
+
try {
|
|
200
|
+
const r = await confirmAction(sessionId, p);
|
|
201
|
+
say(`${r.success ? c.green("✓") : c.yellow("⚠")} ${r.message}${renderBlocks(r.blocks).map((l) => `\n${l}`).join("")}${r.success ? c.dim(tr(" · hoàn tác: /undo", " · undo: /undo")) : ""}`);
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
sayErr(e);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ── sending ───────────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
async function send(raw) {
|
|
209
|
+
const text = raw.trim();
|
|
210
|
+
if (!text)
|
|
211
|
+
return;
|
|
212
|
+
if (text.startsWith("/"))
|
|
213
|
+
return runSlash(text);
|
|
214
|
+
if (text.startsWith("#"))
|
|
215
|
+
return note(text.slice(1).trim());
|
|
216
|
+
const follow = /^[1-3]$/.test(text) ? lastOutcome?.done?.followUps?.[Number(text) - 1] : undefined;
|
|
217
|
+
const question = follow ?? text;
|
|
218
|
+
// @agent:code and @file mentions
|
|
219
|
+
let thisAgent = agent;
|
|
220
|
+
const atts = [...pending];
|
|
221
|
+
for (const m of question.matchAll(/(^|\s)@(\S+)/g)) {
|
|
222
|
+
const token = m[2];
|
|
223
|
+
if (token.startsWith("agent:"))
|
|
224
|
+
thisAgent = token.slice(6);
|
|
225
|
+
else if (existsSync(token) && attachmentKind(token)) {
|
|
226
|
+
try {
|
|
227
|
+
atts.push(await attach(token, sessionId));
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
sayErr(e);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const clean = question.replace(/(^|\s)@agent:\S+/g, "$1").trim();
|
|
235
|
+
setPending([]);
|
|
236
|
+
lastSent.current = question;
|
|
237
|
+
saveHistory(question);
|
|
238
|
+
hist.push(question);
|
|
239
|
+
histIdx.current = -1;
|
|
240
|
+
say(`${c.bold("›")} ${question}${atts.length ? c.dim(` [${atts.map((a) => a.name).join(", ")}]`) : ""}`);
|
|
241
|
+
const notes = findProjectNotes();
|
|
242
|
+
liveRef.current = { activities: [], text: "" };
|
|
243
|
+
setLive(liveRef.current);
|
|
244
|
+
setBusy(true);
|
|
245
|
+
const ctl = new AbortController();
|
|
246
|
+
abortRef.current = ctl;
|
|
247
|
+
try {
|
|
248
|
+
const out = await ask({
|
|
249
|
+
sessionId,
|
|
250
|
+
text: composeText(clean, atts, notes?.text),
|
|
251
|
+
attachments: atts,
|
|
252
|
+
agent: thisAgent,
|
|
253
|
+
signal: ctl.signal,
|
|
254
|
+
onChunk: (ch) => {
|
|
255
|
+
const l = liveRef.current;
|
|
256
|
+
if (ch.type === "token")
|
|
257
|
+
l.text += ch.text ?? "";
|
|
258
|
+
else if (ch.type === "activity" && ch.activity)
|
|
259
|
+
l.activities.push(ch.activity);
|
|
260
|
+
else if (ch.type === "agent" && ch.agent)
|
|
261
|
+
l.agent = ch.agent;
|
|
262
|
+
l.reconnecting = undefined;
|
|
263
|
+
},
|
|
264
|
+
onRetry: (n) => liveRef.current && (liveRef.current.reconnecting = n),
|
|
265
|
+
});
|
|
266
|
+
setLastOutcome(out);
|
|
267
|
+
updateProfile(context().profile, { lastConversation: sessionId });
|
|
268
|
+
say(renderAnswer(out));
|
|
269
|
+
if (out.proposedAction)
|
|
270
|
+
await handleProposal(out.proposedAction);
|
|
271
|
+
if (out.ended === "escalate")
|
|
272
|
+
say(c.dim(tr(" Cần người hỗ trợ? /support <vấn đề>", " Need a human? /support <problem>")));
|
|
273
|
+
refreshUsage();
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
if (ctl.signal.aborted)
|
|
277
|
+
say(c.dim(tr(" ⏹ Đã dừng.", " ⏹ Stopped.")));
|
|
278
|
+
else
|
|
279
|
+
sayErr(e);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
abortRef.current = null;
|
|
283
|
+
liveRef.current = null;
|
|
284
|
+
setLive(null);
|
|
285
|
+
setBusy(false);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function renderAnswer(out) {
|
|
289
|
+
const parts = [];
|
|
290
|
+
if (out.agent && out.agent.code !== "general")
|
|
291
|
+
parts.push(c.cyan(`● ${out.agent.name}`) + (out.agent.requestedDenied ? c.yellow(tr(" (Agent bạn chọn chưa mở cho gói này)", " (the Agent you chose is not in your plan)")) : ""));
|
|
292
|
+
const tools = out.activities.filter((a) => a.kind === "tool");
|
|
293
|
+
for (const a of tools)
|
|
294
|
+
parts.push(c.dim(` ⏺ ${a.label}`));
|
|
295
|
+
if (out.text)
|
|
296
|
+
parts.push(markdown(out.text).replace(/^/gm, " "));
|
|
297
|
+
parts.push(...renderBlocks(out.done?.blocks?.filter((x) => x.type !== "text")));
|
|
298
|
+
for (const n of out.notices)
|
|
299
|
+
if (n.message && !out.text.includes(n.message.slice(0, 24)))
|
|
300
|
+
parts.push(` ${c.yellow("⚠")} ${n.message}`);
|
|
301
|
+
const fu = out.done?.followUps ?? [];
|
|
302
|
+
if (fu.length)
|
|
303
|
+
parts.push(c.dim(` ↳ ${fu.map((f, i) => `${i + 1}) ${f}`).join(" ")}`));
|
|
304
|
+
if (out.ended === "delivered_to_agent")
|
|
305
|
+
parts.push(c.cyan(tr(" Đã chuyển tới nhân viên hỗ trợ.", " Delivered to a support agent.")));
|
|
306
|
+
return parts.join("\n");
|
|
307
|
+
}
|
|
308
|
+
function note(text) {
|
|
309
|
+
if (!text)
|
|
310
|
+
return;
|
|
311
|
+
const f = join(process.cwd(), notesFileName());
|
|
312
|
+
const existing = findProjectNotes();
|
|
313
|
+
const target = existing?.path ?? f;
|
|
314
|
+
appendFileSync(target, `${existsSync(target) ? "\n" : `# ${b.aiName}\n\n`}- ${text}\n`);
|
|
315
|
+
say(`${c.green("✓")} ${tr(`Đã ghi vào ${relative(process.cwd(), target) || notesFileName()} — gửi kèm mọi câu hỏi trong thư mục này.`, `Saved to ${relative(process.cwd(), target) || notesFileName()} — sent with every question in this folder.`)}`);
|
|
316
|
+
}
|
|
317
|
+
// ── slash commands ────────────────────────────────────────────────────────────────────────────
|
|
318
|
+
async function runSlash(line) {
|
|
319
|
+
const [cmd0, ...rest] = line.slice(1).split(/\s+/);
|
|
320
|
+
const cmd = (cmd0 ?? "").toLowerCase();
|
|
321
|
+
const arg = rest.join(" ").trim();
|
|
322
|
+
try {
|
|
323
|
+
switch (cmd) {
|
|
324
|
+
case "help":
|
|
325
|
+
case "?": {
|
|
326
|
+
const w = Math.max(...SLASH.map((s) => s.name.length)) + 2;
|
|
327
|
+
say([...SLASH.map((s) => ` ${c.cyan(`/${s.name}`.padEnd(w + 1))} ${s.description()}`), "", ...SHORTCUTS.map((s) => ` ${s.keys.padEnd(30)} ${c.dim(s.description())}`)].join("\n"));
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case "new": {
|
|
331
|
+
const r = await openConversation(true);
|
|
332
|
+
setSessionId(r.sessionId);
|
|
333
|
+
setLastOutcome(undefined);
|
|
334
|
+
say(c.dim(tr(" — hội thoại mới —", " — new conversation —")));
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case "resume": {
|
|
338
|
+
let list = await listConversations(50);
|
|
339
|
+
if (arg)
|
|
340
|
+
list = list.filter((x) => `${x.title ?? ""} ${x.lastPreview ?? ""}`.toLowerCase().includes(arg.toLowerCase()));
|
|
341
|
+
const id = await pick(tr("Mở lại hội thoại:", "Resume conversation:"), list.map((x) => ({ label: `${x.isPinned ? "📌 " : ""}${(x.title ?? x.lastPreview ?? x.sessionId).slice(0, 60)}`, hint: x.updatedAt.slice(0, 16).replace("T", " "), value: x.sessionId })));
|
|
342
|
+
if (id) {
|
|
343
|
+
setSessionId(id);
|
|
344
|
+
await showHistory(id, 6);
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case "rename":
|
|
349
|
+
await renameConversation(sessionId, arg || null);
|
|
350
|
+
say(`${c.green("✓")} ${tr("Đã đặt tên.", "Renamed.")}`);
|
|
351
|
+
break;
|
|
352
|
+
case "pin": {
|
|
353
|
+
const cur = (await listConversations(100)).find((x) => x.sessionId === sessionId);
|
|
354
|
+
await pinConversation(sessionId, !cur?.isPinned);
|
|
355
|
+
say(`${c.green("✓")} ${cur?.isPinned ? tr("Đã bỏ ghim.", "Unpinned.") : tr("Đã ghim.", "Pinned.")}`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case "workspace":
|
|
359
|
+
case "ws": {
|
|
360
|
+
const list = await listWorkspaces();
|
|
361
|
+
const cur = profile()?.workspaceId;
|
|
362
|
+
const pickedId = arg ? undefined : await pick(tr("Chọn workspace:", "Choose a workspace:"), list.map((w) => ({ label: w.name, hint: [w.code, w.primaryGroupName].filter(Boolean).join(" · "), value: w.id })), Math.max(0, list.findIndex((w) => w.id === cur)));
|
|
363
|
+
const target = arg ? findWorkspace(list, arg) : list.find((w) => w.id === pickedId);
|
|
364
|
+
if (!target)
|
|
365
|
+
break;
|
|
366
|
+
await switchWorkspace(target);
|
|
367
|
+
const r = await openConversation(true);
|
|
368
|
+
setSessionId(r.sessionId);
|
|
369
|
+
say(`${c.green("✓")} ${tr(`Đang làm việc trên ${c.bold(target.name)} · hội thoại mới`, `Now on ${c.bold(target.name)} · new conversation`)}`);
|
|
370
|
+
refreshUsage();
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "agent": {
|
|
374
|
+
agentsCache.current ??= await get("/ai/agents");
|
|
375
|
+
const list = agentsCache.current;
|
|
376
|
+
const code = arg || (await pick(tr("Chọn Agent:", "Choose an Agent:"), list.map((a) => ({ label: a.name, hint: a.status === "available" ? a.code : `${a.code} · ${a.status}`, value: a.code })), Math.max(0, list.findIndex((a) => a.code === (agent ?? "general")))));
|
|
377
|
+
if (!code)
|
|
378
|
+
break;
|
|
379
|
+
const a = list.find((x) => x.code === code);
|
|
380
|
+
if (!a)
|
|
381
|
+
throw new Error(tr(`Không có Agent "${code}".`, `No Agent "${code}".`));
|
|
382
|
+
setAgent(code === "general" ? undefined : code);
|
|
383
|
+
updateProfile(context().profile, { agent: code === "general" ? undefined : code });
|
|
384
|
+
say(`${c.green("✓")} Agent: ${c.bold(a.name)}${a.status !== "available" ? c.yellow(tr(" (chưa mở cho gói hiện tại — máy chủ sẽ dùng Trợ lý chung)", " (not in your plan — the server will use the general assistant)")) : ""}`);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case "mode": {
|
|
388
|
+
const m = arg || MODES[(MODES.indexOf(mode) + 1) % MODES.length];
|
|
389
|
+
if (!MODES.includes(m))
|
|
390
|
+
throw new Error("read | propose | auto");
|
|
391
|
+
setMode(m);
|
|
392
|
+
setContext({ mode: m });
|
|
393
|
+
say(` ${modeLabel(m)}`);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case "model":
|
|
397
|
+
say(c.dim(tr(` Đang dùng ${b.aiName}. Model của bạn: chạy \`${b.bin} --model <tên>\` (xem \`${b.bin} model list\`).`, ` Using ${b.aiName}. Your own model: run \`${b.bin} --model <name>\` (see \`${b.bin} model list\`).`)));
|
|
398
|
+
break;
|
|
399
|
+
case "attach":
|
|
400
|
+
if (!arg)
|
|
401
|
+
throw new Error(tr("Cần đường dẫn tệp.", "Pass a file path."));
|
|
402
|
+
await addAttachment(arg);
|
|
403
|
+
break;
|
|
404
|
+
case "pending": {
|
|
405
|
+
const p = await get("/ai/pending", { query: { take: 20 } });
|
|
406
|
+
say(p.changesets.length ? table(["ID", tr("Việc", "Change"), tr("Trạng thái", "Status"), tr("Hết hạn", "Expires")], p.changesets.map((x) => [x.id.slice(0, 8), x.title, x.status, x.expiresAt.slice(0, 16).replace("T", " ")])) + c.dim(`\n ${b.bin} ai apply <id> · ${b.bin} ai reject <id>`) : tr(" Không có việc nào đang chờ.", " Nothing is waiting."));
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case "journal": {
|
|
410
|
+
const j = await get("/ai/journal", { query: { pageSize: 15 } });
|
|
411
|
+
say(j.items.length ? table(["ID", tr("Việc", "Change"), tr("Trạng thái", "Status"), tr("Hoàn tác tới", "Undo until")], j.items.map((x) => [x.id.slice(0, 8), x.title, x.status, x.undoable ? (x.undoUntil ?? "").slice(0, 10) : "—"])) : tr(" Nhật ký trống.", " The journal is empty."));
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case "undo": {
|
|
415
|
+
const j = await get("/ai/journal", { query: { pageSize: 30 } });
|
|
416
|
+
const target = arg ? j.items.find((x) => x.id.startsWith(arg)) : j.items.find((x) => x.undoable);
|
|
417
|
+
if (!target)
|
|
418
|
+
throw new Error(tr("Không có việc nào hoàn tác được.", "Nothing to undo."));
|
|
419
|
+
const ok = await pick(tr(`Hoàn tác "${target.title}"?`, `Undo "${target.title}"?`), [{ label: tr("Không", "No"), value: "n" }, { label: tr("Hoàn tác", "Undo"), value: "y" }]);
|
|
420
|
+
if (ok !== "y")
|
|
421
|
+
break;
|
|
422
|
+
const r = await post(`/ai/changesets/${target.id}/undo`, {});
|
|
423
|
+
say(`${c.green("✓")} ${tr(`Đã hoàn tác${r.reverted ? ` ${r.reverted} dòng` : ""}${r.conflicts ? `, ${r.conflicts} dòng xung đột không ghi đè` : ""}.`, `Undone${r.reverted ? ` ${r.reverted} rows` : ""}${r.conflicts ? `, ${r.conflicts} conflicts left untouched` : ""}.`)}`);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
case "why": {
|
|
427
|
+
const id = arg || lastOutcome?.runId;
|
|
428
|
+
if (!id)
|
|
429
|
+
throw new Error(tr("Chưa có câu trả lời nào trong phiên này.", "No answer in this session yet."));
|
|
430
|
+
const run = await get(`/ai/runs/${id}`);
|
|
431
|
+
say(runLines(run).join("\n"));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case "memory": {
|
|
435
|
+
const list = await get("/ai/memory");
|
|
436
|
+
say(list.length ? table(["ID", tr("Điều nhớ", "Memory"), tr("Dùng", "Uses"), ""], list.map((m) => [m.id.slice(0, 8), m.display, m.uses, m.lockedBy ? "🔒" : ""])) + c.dim(`\n ${b.bin} ai memory edit|lock|forget <id>`) : tr(" AI chưa nhớ gì về công ty.", " Nothing remembered yet."));
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case "schedule": {
|
|
440
|
+
const m = /^"([^"]+)"\s+(daily|weekly|monthly)\s+(?:(\d+)@)?(\d{1,2}:\d{2})$/.exec(arg);
|
|
441
|
+
if (m) {
|
|
442
|
+
const [, q, freq, day, time] = m;
|
|
443
|
+
const s = await post("/ai/schedules", { title: q.slice(0, 60), question: q, agentCode: agent ?? null, frequency: freq, timeOfDay: time, dayOfWeek: freq === "weekly" ? Number(day ?? 1) : null, dayOfMonth: freq === "monthly" ? Number(day ?? 1) : null, enabled: true });
|
|
444
|
+
say(`${c.green("✓")} ${tr(`Đã lên lịch · lần tới ${s.nextRunAt.slice(0, 16).replace("T", " ")}`, `Scheduled · next ${s.nextRunAt.slice(0, 16).replace("T", " ")}`)}`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
const list = await get("/ai/schedules");
|
|
448
|
+
say((list.length ? table(["ID", tr("Tên", "Title"), tr("Lịch", "When"), tr("Lần tới", "Next")], list.map((s) => [s.id.slice(0, 8), s.title, `${s.frequency} ${s.timeOfDay}`, s.nextRunAt.slice(0, 16).replace("T", " ")])) + "\n" : "") + c.dim(tr(' /schedule "doanh thu hôm qua" daily 08:00 · weekly 1@08:00 · monthly 1@08:00', ' /schedule "yesterday\'s revenue" daily 08:00 · weekly 1@08:00 · monthly 1@08:00')));
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "library": {
|
|
453
|
+
const list = await get("/ai/library");
|
|
454
|
+
say(list.length ? table(["#", tr("Loại", "Kind"), tr("Tên", "Name"), tr("Lúc", "At")], list.slice(0, 20).map((x, i) => [i + 1, x.kind, x.name, x.createdAt.slice(0, 16).replace("T", " ")])) + c.dim(`\n ${b.bin} ai library get <#>`) : tr(" Thư viện trống.", " The library is empty."));
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "export": {
|
|
458
|
+
const [fmt = "md", file] = arg.split(/\s+/);
|
|
459
|
+
const msgs = (await fetchHistory(sessionId)) ?? [];
|
|
460
|
+
const out = file ?? `${b.bin}-${sessionId.slice(0, 8)}.${fmt === "json" ? "json" : "md"}`;
|
|
461
|
+
writeFileSync(out, fmt === "json" ? JSON.stringify(msgs, null, 2) : msgs.map((m) => `${m.role === "User" ? "## " : ""}${m.text}\n`).join("\n"));
|
|
462
|
+
say(`${c.green("✓")} ${tr(`Đã xuất ${msgs.length} tin → ${out}`, `Exported ${msgs.length} messages → ${out}`)}`);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case "tools": {
|
|
466
|
+
const { publicTools } = await import("../commands/tools.js");
|
|
467
|
+
const list = await publicTools();
|
|
468
|
+
const by = new Map();
|
|
469
|
+
for (const t of list)
|
|
470
|
+
by.set(t.module, (by.get(t.module) ?? 0) + 1);
|
|
471
|
+
say([...by.entries()].map(([m, n]) => ` ${m.padEnd(12)} ${n}`).join("\n") + c.dim(tr(`\n ${b.bin} tools list · ${b.bin} tools describe <tên>`, `\n ${b.bin} tools list · ${b.bin} tools describe <name>`)));
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "usage": {
|
|
475
|
+
const u = await fetchUsage();
|
|
476
|
+
say(` ${tr("Gói", "Plan")} ${c.bold(u.planName ?? "-")} · ${tr("đã dùng", "used")} ${u.used ?? 0}${u.limit ? `/${u.limit}` : ""} · ${tr("mở lại", "resets")} ${u.resetsAt?.slice(0, 10) ?? ""}`);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case "feedback": {
|
|
480
|
+
const id = lastOutcome?.done?.messageId;
|
|
481
|
+
if (!id)
|
|
482
|
+
throw new Error(tr("Chưa có câu trả lời để góp ý.", "No answer to rate yet."));
|
|
483
|
+
await feedback(id, arg !== "down");
|
|
484
|
+
say(`${c.green("✓")} ${tr("Cảm ơn bạn đã góp ý.", "Thanks for the feedback.")}`);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case "support": {
|
|
488
|
+
if (!arg)
|
|
489
|
+
throw new Error(tr("Ghi rõ vấn đề: /support <vấn đề>", "Describe it: /support <problem>"));
|
|
490
|
+
const r = await escalate(sessionId, arg);
|
|
491
|
+
say(`${c.green("✓")} ${tr(`Đã chuyển cho nhân viên (${r.status}) · chờ khoảng ${r.estimatedWaitMinutes} phút`, `Handed over (${r.status}) · about ${r.estimatedWaitMinutes} min`)}`);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case "status": {
|
|
495
|
+
const p = profile();
|
|
496
|
+
say([` ${c.bold(p?.userName ?? "")} ${c.dim(p?.userEmail ?? "")}`, ` ${p?.workspaceName ?? ""} · ${context().env} · ${context().endpoints.api}`, ` Agent ${agent ?? "general"} · ${modeLabel(mode)} · ${usageText}`, ` CLI ${VERSION} · ${tr("hồ sơ", "profile")} ${context().profile}`].join("\n"));
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case "doctor": {
|
|
500
|
+
const { doctor } = await import("../commands/util.js");
|
|
501
|
+
const checks = await doctor();
|
|
502
|
+
say(checks.map((ch) => ` ${ch.ok === true ? c.green("✓") : ch.ok === "warn" ? c.yellow("⚠") : c.red("✗")} ${ch.name.padEnd(14)} ${ch.detail}`).join("\n"));
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case "config": {
|
|
506
|
+
const cfg = loadConfig();
|
|
507
|
+
const [k, v] = arg.split(/\s+/);
|
|
508
|
+
if (k && v !== undefined) {
|
|
509
|
+
const { SETTABLE } = await import("../core/config.js");
|
|
510
|
+
const parse = SETTABLE[k];
|
|
511
|
+
if (!parse)
|
|
512
|
+
throw new Error(Object.keys(SETTABLE).join(", "));
|
|
513
|
+
cfg[k] = parse(v);
|
|
514
|
+
saveConfig(cfg);
|
|
515
|
+
say(`${c.green("✓")} ${k} = ${String(cfg[k])}`);
|
|
516
|
+
}
|
|
517
|
+
else
|
|
518
|
+
say(["lang", "mode", "updateChannel", "updateCheck", "telemetry"].map((x) => ` ${x.padEnd(14)} ${String(cfg[x] ?? "—")}`).join("\n"));
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case "init": {
|
|
522
|
+
const f = join(process.cwd(), notesFileName());
|
|
523
|
+
if (existsSync(f))
|
|
524
|
+
throw new Error(tr(`${notesFileName()} đã có.`, `${notesFileName()} already exists.`));
|
|
525
|
+
writeFileSync(f, tr(`# Chỉ dẫn cho ${b.aiName}\n\n- Bối cảnh: …\n- Quy ước: …\n`, `# Instructions for ${b.aiName}\n\n- Context: …\n- Conventions: …\n`));
|
|
526
|
+
say(`${c.green("✓")} ${tr(`Đã tạo ${notesFileName()} — sửa tệp rồi hỏi tiếp.`, `Created ${notesFileName()} — edit it and keep asking.`)}`);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case "mcp":
|
|
530
|
+
say(` ${context().endpoints.mcp}\n${c.dim(` ${b.bin} mcp install`)}`);
|
|
531
|
+
break;
|
|
532
|
+
case "logout":
|
|
533
|
+
await logoutServer();
|
|
534
|
+
deleteSession(context().profile);
|
|
535
|
+
say(`${c.green("✓")} ${tr("Đã đăng xuất.", "Logged out.")}`);
|
|
536
|
+
setTimeout(() => exit(), 50);
|
|
537
|
+
break;
|
|
538
|
+
case "clear":
|
|
539
|
+
stdout?.write("\u001b[2J\u001b[3J\u001b[H");
|
|
540
|
+
setDone([]);
|
|
541
|
+
break;
|
|
542
|
+
case "exit":
|
|
543
|
+
case "quit":
|
|
544
|
+
exit();
|
|
545
|
+
break;
|
|
546
|
+
default: {
|
|
547
|
+
const near = matchSlash(cmd).map((s) => `/${s.name}`).slice(0, 3);
|
|
548
|
+
say(`${c.yellow("⚠")} ${tr(`Không có lệnh /${cmd}.`, `No command /${cmd}.`)}${near.length ? ` ${tr("Ý bạn là", "Did you mean")} ${near.join(", ")}?` : ""} ${c.dim("/help")}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
sayErr(e);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// ── menus ─────────────────────────────────────────────────────────────────────────────────────
|
|
557
|
+
const menuItems = (() => {
|
|
558
|
+
if (menuOff || modal || busy)
|
|
559
|
+
return [];
|
|
560
|
+
if (/^\/\S*$/.test(value))
|
|
561
|
+
return matchSlash(value).map((s) => ({ label: `/${s.name}`, hint: s.description(), value: `/${s.name}` }));
|
|
562
|
+
const at = /(^|\s)@(\S*)$/.exec(value.slice(0, cursor));
|
|
563
|
+
if (at) {
|
|
564
|
+
const q = at[2].toLowerCase();
|
|
565
|
+
filesCache.current ??= attachableFiles();
|
|
566
|
+
const agents = (agentsCache.current ?? []).map((a) => ({ label: `@agent:${a.code}`, hint: a.name, value: `@agent:${a.code}` }));
|
|
567
|
+
const files = filesCache.current.map((f) => ({ label: `@${f}`, hint: attachmentKind(f) ?? "", value: `@${f}` }));
|
|
568
|
+
return [...files, ...agents].filter((i) => i.label.toLowerCase().includes(q)).slice(0, 50);
|
|
569
|
+
}
|
|
570
|
+
return [];
|
|
571
|
+
})();
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (/(^|\s)@\S*$/.test(value) && !agentsCache.current)
|
|
574
|
+
get("/ai/agents").then((a) => (agentsCache.current = a)).catch(() => undefined);
|
|
575
|
+
setMenuIndex(0);
|
|
576
|
+
}, [value]);
|
|
577
|
+
function applyMenu(item) {
|
|
578
|
+
if (item.value.startsWith("/")) {
|
|
579
|
+
const v = `${item.value} `;
|
|
580
|
+
setValue(v);
|
|
581
|
+
setCursor(v.length);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const before = value.slice(0, cursor).replace(/@\S*$/, `${item.value} `);
|
|
585
|
+
const v = before + value.slice(cursor);
|
|
586
|
+
setValue(v);
|
|
587
|
+
setCursor(before.length);
|
|
588
|
+
}
|
|
589
|
+
// ── keys ──────────────────────────────────────────────────────────────────────────────────────
|
|
590
|
+
useInput((input, key) => {
|
|
591
|
+
if (modal?.kind === "select")
|
|
592
|
+
return; // Select handles its own keys
|
|
593
|
+
if (modal?.kind === "confirm") {
|
|
594
|
+
const p = modal.proposal;
|
|
595
|
+
const large = riskOf(p) === "large";
|
|
596
|
+
if (key.escape || (key.ctrl && input === "c"))
|
|
597
|
+
return modal.resolve(false);
|
|
598
|
+
if (large) {
|
|
599
|
+
if (key.return)
|
|
600
|
+
return modal.resolve(modal.typed === confirmWord(p));
|
|
601
|
+
if (key.backspace || key.delete)
|
|
602
|
+
return setModal({ ...modal, typed: modal.typed.slice(0, -1) });
|
|
603
|
+
if (input && !key.ctrl && !key.meta)
|
|
604
|
+
return setModal({ ...modal, typed: modal.typed + input });
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (input === "y" || input === "Y")
|
|
608
|
+
return modal.resolve(true);
|
|
609
|
+
if (input === "n" || input === "N" || key.return)
|
|
610
|
+
return modal.resolve(false);
|
|
611
|
+
if (input === "d" || input === "D") {
|
|
612
|
+
let pretty = p.payloadJson;
|
|
613
|
+
try {
|
|
614
|
+
pretty = JSON.stringify(JSON.parse(p.payloadJson), null, 2);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// raw
|
|
618
|
+
}
|
|
619
|
+
say(c.dim(pretty.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
620
|
+
}
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (key.ctrl && input === "c") {
|
|
624
|
+
if (busy)
|
|
625
|
+
return abortRef.current?.abort();
|
|
626
|
+
if (value) {
|
|
627
|
+
setValue("");
|
|
628
|
+
setCursor(0);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (Date.now() - ctrlC.current < 1500)
|
|
632
|
+
return exit();
|
|
633
|
+
ctrlC.current = Date.now();
|
|
634
|
+
say(c.dim(tr(" Nhấn Ctrl+C lần nữa để thoát.", " Press Ctrl+C again to exit.")));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (key.ctrl && input === "d") {
|
|
638
|
+
if (!value)
|
|
639
|
+
exit();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (key.escape) {
|
|
643
|
+
if (busy)
|
|
644
|
+
return abortRef.current?.abort();
|
|
645
|
+
if (menuItems.length)
|
|
646
|
+
return setMenuOff(true);
|
|
647
|
+
if (Date.now() - lastEsc.current < 600 && lastSent.current) {
|
|
648
|
+
setValue(lastSent.current);
|
|
649
|
+
setCursor(lastSent.current.length);
|
|
650
|
+
}
|
|
651
|
+
lastEsc.current = Date.now();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (key.ctrl && input === "l") {
|
|
655
|
+
stdout?.write("\u001b[2J\u001b[3J\u001b[H");
|
|
656
|
+
setDone([]);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (key.ctrl && input === "o") {
|
|
660
|
+
const acts = lastOutcome?.activities ?? [];
|
|
661
|
+
say(acts.length ? acts.map((a) => ` ${a.kind === "tool" ? c.cyan("⏺") : c.dim("·")} ${a.label}${a.elapsedMs ? c.dim(` +${(a.elapsedMs / 1000).toFixed(1)}s`) : ""}`).join("\n") : c.dim(tr(" Chưa có bước nào.", " No steps yet.")));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (key.tab && key.shift) {
|
|
665
|
+
const m = MODES[(MODES.indexOf(mode) + 1) % MODES.length];
|
|
666
|
+
setMode(m);
|
|
667
|
+
setContext({ mode: m });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (busy)
|
|
671
|
+
return;
|
|
672
|
+
if (menuItems.length && (key.upArrow || key.downArrow)) {
|
|
673
|
+
setMenuIndex((i) => (i + (key.upArrow ? -1 : 1) + menuItems.length) % menuItems.length);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (menuItems.length && key.tab)
|
|
677
|
+
return applyMenu(menuItems[menuIndex]);
|
|
678
|
+
if (key.return) {
|
|
679
|
+
// "\" at end of line or Alt+Enter = new line
|
|
680
|
+
if (key.meta || value.slice(0, cursor).endsWith("\\")) {
|
|
681
|
+
const base = value.slice(0, cursor).endsWith("\\") ? value.slice(0, cursor - 1) + value.slice(cursor) : value;
|
|
682
|
+
const at = value.slice(0, cursor).endsWith("\\") ? cursor - 1 : cursor;
|
|
683
|
+
const [v, cur] = edit.insert(base, at, "\n");
|
|
684
|
+
setValue(v);
|
|
685
|
+
setCursor(cur);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (menuItems.length && value.startsWith("/") && !value.includes(" ")) {
|
|
689
|
+
const chosen = menuItems[menuIndex].value;
|
|
690
|
+
setValue("");
|
|
691
|
+
setCursor(0);
|
|
692
|
+
void send(chosen);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (menuItems.length && !value.startsWith("/"))
|
|
696
|
+
return applyMenu(menuItems[menuIndex]);
|
|
697
|
+
const v = value;
|
|
698
|
+
setValue("");
|
|
699
|
+
setCursor(0);
|
|
700
|
+
setMenuOff(false);
|
|
701
|
+
void send(v);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (key.upArrow && !value.includes("\n")) {
|
|
705
|
+
if (!hist.length)
|
|
706
|
+
return;
|
|
707
|
+
histIdx.current = histIdx.current < 0 ? hist.length - 1 : Math.max(0, histIdx.current - 1);
|
|
708
|
+
const h = hist[histIdx.current];
|
|
709
|
+
setValue(h);
|
|
710
|
+
setCursor(h.length);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (key.downArrow && !value.includes("\n")) {
|
|
714
|
+
if (histIdx.current < 0)
|
|
715
|
+
return;
|
|
716
|
+
histIdx.current += 1;
|
|
717
|
+
const h = histIdx.current >= hist.length ? "" : hist[histIdx.current];
|
|
718
|
+
if (histIdx.current >= hist.length)
|
|
719
|
+
histIdx.current = -1;
|
|
720
|
+
setValue(h);
|
|
721
|
+
setCursor(h.length);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (key.leftArrow)
|
|
725
|
+
return setCursor((x) => Math.max(0, x - 1));
|
|
726
|
+
if (key.rightArrow)
|
|
727
|
+
return setCursor((x) => Math.min(value.length, x + 1));
|
|
728
|
+
if (key.ctrl && input === "a")
|
|
729
|
+
return setCursor(edit.home(value, cursor));
|
|
730
|
+
if (key.ctrl && input === "e")
|
|
731
|
+
return setCursor(edit.end(value, cursor));
|
|
732
|
+
if (key.ctrl && input === "u") {
|
|
733
|
+
setValue(value.slice(cursor));
|
|
734
|
+
setCursor(0);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (key.ctrl && input === "w") {
|
|
738
|
+
const [v, cur] = edit.wordBack(value, cursor);
|
|
739
|
+
setValue(v);
|
|
740
|
+
setCursor(cur);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (key.backspace || key.delete) {
|
|
744
|
+
const [v, cur] = key.delete && !key.backspace && cursor < value.length && process.platform !== "darwin" ? edit.del(value, cursor) : edit.backspace(value, cursor);
|
|
745
|
+
setValue(v);
|
|
746
|
+
setCursor(cur);
|
|
747
|
+
setMenuOff(false);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
// A pasted single line ending in Enter is sent; multi-line pastes stay in the editor.
|
|
751
|
+
if (input && input.length > 1 && input.endsWith("\r") && !/[\r\n]/.test(input.slice(0, -1)) && !key.ctrl && !key.meta) {
|
|
752
|
+
const [v] = edit.insert(value, cursor, input.slice(0, -1));
|
|
753
|
+
setValue("");
|
|
754
|
+
setCursor(0);
|
|
755
|
+
void send(v);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (input && !key.ctrl && !key.meta) {
|
|
759
|
+
const [v, cur] = edit.insert(value, cursor, input.replace(/\r/g, "\n"));
|
|
760
|
+
setValue(v);
|
|
761
|
+
setCursor(cur);
|
|
762
|
+
if (input === "/" || input === "@")
|
|
763
|
+
setMenuOff(false);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
// ── view ──────────────────────────────────────────────────────────────────────────────────────
|
|
767
|
+
const p = profile();
|
|
768
|
+
const footerRight = c.dim(`${p?.workspaceName ?? ""} · ${context().env}${usageText ? ` · ${usageText}` : ""}${agent ? ` · ${agent}` : ""}`);
|
|
769
|
+
const lastReason = live?.activities.filter((a) => a.kind !== "tool").at(-1);
|
|
770
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: done, children: (it) => _jsx(Text, { children: it.text }, it.id) }), live ? (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [live.agent && live.agent.code !== "general" ? _jsx(Text, { color: "cyan", children: `● ${live.agent.name}` }) : null, live.activities.filter((a) => a.kind === "tool").map((a, i) => (_jsx(Text, { color: "gray", children: ` ⏺ ${a.label}` }, i))), live.text ? _jsx(Text, { children: markdown(live.text).replace(/^/gm, " ") }) : null, _jsxs(Text, { color: "gray", children: [_jsx(Spinner, { type: "dots" }), " ", live.reconnecting ? tr(`Mất kết nối, đang nối lại (${live.reconnecting}/2)…`, `Reconnecting (${live.reconnecting}/2)…`) : (lastReason?.label ?? tr("Đang nghĩ…", "Thinking…")), " ", _jsx(Text, { dimColor: true, children: tr("Esc dừng", "Esc to stop") })] })] })) : null, modal?.kind === "select" ? (_jsx(Select, { title: modal.title, items: modal.items, initial: modal.initial, onPick: modal.resolve, hint: tr("↑↓ chọn · Enter · Esc huỷ", "↑↓ choose · Enter · Esc cancel") })) : modal?.kind === "confirm" ? (_jsx(Box, { marginTop: 0, children: riskOf(modal.proposal) === "large" ? (_jsxs(Text, { children: [c.yellow("└ "), tr(`Gõ ${c.bold(confirmWord(modal.proposal))} để xác nhận, Enter để huỷ`, `Type ${c.bold(confirmWord(modal.proposal))} to confirm, Enter to cancel`), " \u203A ", modal.typed, _jsx(Text, { inverse: true, children: " " })] })) : (_jsxs(Text, { children: [c.yellow("└ "), tr("Ghi?", "Write?"), " ", c.bold("[y]"), " ", tr("có", "yes"), " ", c.bold("[n]"), " ", tr("không", "no"), " ", c.bold("[d]"), " ", tr("xem dữ liệu", "show data")] })) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [pending.length ? _jsx(Text, { color: "gray", children: ` 📎 ${pending.map((a) => a.name).join(", ")}` }) : null, _jsx(Editor, { value: value, cursor: cursor, disabled: busy, placeholder: busy ? "" : tr(`Hỏi ${b.aiName}… (/ để xem lệnh)`, `Ask ${b.aiName}… (/ for commands)`) }), menuItems.length ? _jsx(Menu, { items: menuItems, index: menuIndex }) : null, _jsxs(Box, { justifyContent: "space-between", width: width, children: [_jsx(Text, { children: ` ${modeLabel(mode)} ${c.dim("(Shift+Tab)")}` }), _jsx(Text, { children: footerRight })] })] }))] }));
|
|
771
|
+
}
|