@robota-sdk/agent-cli 3.0.0-beta.4 → 3.0.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -86
- package/dist/node/bin.js +11 -1
- package/dist/node/chunk-KX3JUGSB.js +2348 -0
- package/dist/node/index.cjs +1825 -685
- package/dist/node/index.js +1 -1
- package/package.json +5 -4
- package/dist/node/bin.cjs +0 -1217
- package/dist/node/bin.d.cts +0 -1
- package/dist/node/chunk-4DYVT4WI.js +0 -1196
|
@@ -0,0 +1,2348 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import {
|
|
6
|
+
loadConfig,
|
|
7
|
+
loadContext,
|
|
8
|
+
detectProject,
|
|
9
|
+
createSession,
|
|
10
|
+
SessionStore,
|
|
11
|
+
FileSessionLogger,
|
|
12
|
+
projectPaths
|
|
13
|
+
} from "@robota-sdk/agent-sdk";
|
|
14
|
+
import { promptForApproval } from "@robota-sdk/agent-sdk";
|
|
15
|
+
|
|
16
|
+
// src/utils/cli-args.ts
|
|
17
|
+
import { parseArgs } from "util";
|
|
18
|
+
var VALID_MODES = ["plan", "default", "acceptEdits", "bypassPermissions"];
|
|
19
|
+
function parsePermissionMode(raw) {
|
|
20
|
+
if (raw === void 0) return void 0;
|
|
21
|
+
if (!VALID_MODES.includes(raw)) {
|
|
22
|
+
process.stderr.write(`Invalid --permission-mode "${raw}". Valid: ${VALID_MODES.join(" | ")}
|
|
23
|
+
`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
return raw;
|
|
27
|
+
}
|
|
28
|
+
function parseMaxTurns(raw) {
|
|
29
|
+
if (raw === void 0) return void 0;
|
|
30
|
+
const n = parseInt(raw, 10);
|
|
31
|
+
if (isNaN(n) || n <= 0) {
|
|
32
|
+
process.stderr.write(`Invalid --max-turns "${raw}". Must be a positive integer.
|
|
33
|
+
`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
function parseCliArgs() {
|
|
39
|
+
const { values, positionals } = parseArgs({
|
|
40
|
+
allowPositionals: true,
|
|
41
|
+
options: {
|
|
42
|
+
p: { type: "boolean", short: "p", default: false },
|
|
43
|
+
c: { type: "boolean", short: "c", default: false },
|
|
44
|
+
r: { type: "string", short: "r" },
|
|
45
|
+
model: { type: "string" },
|
|
46
|
+
language: { type: "string" },
|
|
47
|
+
"permission-mode": { type: "string" },
|
|
48
|
+
"max-turns": { type: "string" },
|
|
49
|
+
version: { type: "boolean", default: false },
|
|
50
|
+
reset: { type: "boolean", default: false }
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
positional: positionals,
|
|
55
|
+
printMode: values["p"] ?? false,
|
|
56
|
+
continueMode: values["c"] ?? false,
|
|
57
|
+
resumeId: values["r"],
|
|
58
|
+
model: values["model"],
|
|
59
|
+
language: values["language"],
|
|
60
|
+
permissionMode: parsePermissionMode(values["permission-mode"]),
|
|
61
|
+
maxTurns: parseMaxTurns(values["max-turns"]),
|
|
62
|
+
version: values["version"] ?? false,
|
|
63
|
+
reset: values["reset"] ?? false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/utils/settings-io.ts
|
|
68
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
69
|
+
import { join, dirname } from "path";
|
|
70
|
+
function getUserSettingsPath() {
|
|
71
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
|
|
72
|
+
return join(home, ".robota", "settings.json");
|
|
73
|
+
}
|
|
74
|
+
function readSettings(path) {
|
|
75
|
+
if (!existsSync(path)) return {};
|
|
76
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
+
}
|
|
78
|
+
function writeSettings(path, settings) {
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
80
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
81
|
+
}
|
|
82
|
+
function updateModelInSettings(settingsPath, modelId) {
|
|
83
|
+
const settings = readSettings(settingsPath);
|
|
84
|
+
const provider = settings.provider ?? {};
|
|
85
|
+
provider.model = modelId;
|
|
86
|
+
settings.provider = provider;
|
|
87
|
+
writeSettings(settingsPath, settings);
|
|
88
|
+
}
|
|
89
|
+
function deleteSettings(path) {
|
|
90
|
+
if (existsSync(path)) {
|
|
91
|
+
unlinkSync(path);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/print-terminal.ts
|
|
98
|
+
import * as readline from "readline";
|
|
99
|
+
var PrintTerminal = class {
|
|
100
|
+
write(text) {
|
|
101
|
+
process.stdout.write(text);
|
|
102
|
+
}
|
|
103
|
+
writeLine(text) {
|
|
104
|
+
process.stdout.write(text + "\n");
|
|
105
|
+
}
|
|
106
|
+
writeMarkdown(md) {
|
|
107
|
+
process.stdout.write(md);
|
|
108
|
+
}
|
|
109
|
+
writeError(text) {
|
|
110
|
+
process.stderr.write(text + "\n");
|
|
111
|
+
}
|
|
112
|
+
prompt(question) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const rl = readline.createInterface({
|
|
115
|
+
input: process.stdin,
|
|
116
|
+
output: process.stdout,
|
|
117
|
+
terminal: false,
|
|
118
|
+
historySize: 0
|
|
119
|
+
});
|
|
120
|
+
rl.question(question, (answer) => {
|
|
121
|
+
rl.close();
|
|
122
|
+
resolve(answer);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async select(options, initialIndex = 0) {
|
|
127
|
+
for (let i = 0; i < options.length; i++) {
|
|
128
|
+
const marker = i === initialIndex ? ">" : " ";
|
|
129
|
+
process.stdout.write(` ${marker} ${i + 1}) ${options[i]}
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
const answer = await this.prompt(
|
|
133
|
+
` Choose [1-${options.length}] (default: ${options[initialIndex]}): `
|
|
134
|
+
);
|
|
135
|
+
const trimmed = answer.trim().toLowerCase();
|
|
136
|
+
if (trimmed === "") return initialIndex;
|
|
137
|
+
const num = parseInt(trimmed, 10);
|
|
138
|
+
if (!isNaN(num) && num >= 1 && num <= options.length) return num - 1;
|
|
139
|
+
return initialIndex;
|
|
140
|
+
}
|
|
141
|
+
spinner(_message) {
|
|
142
|
+
return { stop() {
|
|
143
|
+
}, update() {
|
|
144
|
+
} };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/ui/render.tsx
|
|
149
|
+
import { render } from "ink";
|
|
150
|
+
|
|
151
|
+
// src/ui/App.tsx
|
|
152
|
+
import { useState as useState9, useRef as useRef7 } from "react";
|
|
153
|
+
import { Box as Box11, Text as Text13, useApp, useInput as useInput7 } from "ink";
|
|
154
|
+
import { getModelName, createSystemMessage as createSystemMessage2 } from "@robota-sdk/agent-core";
|
|
155
|
+
|
|
156
|
+
// src/ui/hooks/useInteractiveSession.ts
|
|
157
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
158
|
+
import { homedir } from "os";
|
|
159
|
+
import { join as join3 } from "path";
|
|
160
|
+
import {
|
|
161
|
+
InteractiveSession,
|
|
162
|
+
CommandRegistry,
|
|
163
|
+
BuiltinCommandSource,
|
|
164
|
+
SkillCommandSource,
|
|
165
|
+
SystemCommandExecutor,
|
|
166
|
+
BundlePluginLoader
|
|
167
|
+
} from "@robota-sdk/agent-sdk";
|
|
168
|
+
import { createSystemMessage } from "@robota-sdk/agent-core";
|
|
169
|
+
|
|
170
|
+
// src/commands/plugin-source.ts
|
|
171
|
+
var PluginCommandSource = class {
|
|
172
|
+
name = "plugin";
|
|
173
|
+
plugins;
|
|
174
|
+
constructor(plugins) {
|
|
175
|
+
this.plugins = plugins;
|
|
176
|
+
}
|
|
177
|
+
getCommands() {
|
|
178
|
+
const commands = [];
|
|
179
|
+
for (const plugin of this.plugins) {
|
|
180
|
+
for (const skill of plugin.skills) {
|
|
181
|
+
const baseName = skill.name.includes("@") ? skill.name.split("@")[0] : skill.name;
|
|
182
|
+
commands.push({
|
|
183
|
+
name: baseName,
|
|
184
|
+
description: `(${plugin.manifest.name}) ${skill.description}`,
|
|
185
|
+
source: "plugin",
|
|
186
|
+
skillContent: skill.skillContent,
|
|
187
|
+
pluginDir: plugin.pluginDir
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
for (const cmd of plugin.commands) {
|
|
191
|
+
commands.push({
|
|
192
|
+
name: cmd.name,
|
|
193
|
+
description: cmd.description,
|
|
194
|
+
source: "plugin",
|
|
195
|
+
skillContent: cmd.skillContent,
|
|
196
|
+
pluginDir: plugin.pluginDir
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return commands;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/utils/skill-prompt.ts
|
|
205
|
+
import { execSync } from "child_process";
|
|
206
|
+
function substituteVariables(content, args, context) {
|
|
207
|
+
const argParts = args ? args.split(/\s+/) : [];
|
|
208
|
+
let result = content;
|
|
209
|
+
result = result.replace(/\$ARGUMENTS\[(\d+)]/g, (_match, index) => {
|
|
210
|
+
return argParts[Number(index)] ?? "";
|
|
211
|
+
});
|
|
212
|
+
result = result.replace(/\$ARGUMENTS/g, args);
|
|
213
|
+
result = result.replace(/\$(\d)(?!\d|\w|\[)/g, (_match, digit) => {
|
|
214
|
+
return argParts[Number(digit)] ?? "";
|
|
215
|
+
});
|
|
216
|
+
result = result.replace(/\$\{CLAUDE_SESSION_ID}/g, context?.sessionId ?? "");
|
|
217
|
+
result = result.replace(/\$\{CLAUDE_SKILL_DIR}/g, context?.skillDir ?? "");
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
async function preprocessShellCommands(content) {
|
|
221
|
+
const shellPattern = /!`([^`]+)`/g;
|
|
222
|
+
if (!shellPattern.test(content)) {
|
|
223
|
+
return content;
|
|
224
|
+
}
|
|
225
|
+
shellPattern.lastIndex = 0;
|
|
226
|
+
let result = content;
|
|
227
|
+
let match;
|
|
228
|
+
const matches = [];
|
|
229
|
+
while ((match = shellPattern.exec(content)) !== null) {
|
|
230
|
+
matches.push({ full: match[0], command: match[1] });
|
|
231
|
+
}
|
|
232
|
+
for (const { full, command } of matches) {
|
|
233
|
+
let output = "";
|
|
234
|
+
try {
|
|
235
|
+
output = execSync(command, {
|
|
236
|
+
timeout: 5e3,
|
|
237
|
+
encoding: "utf-8",
|
|
238
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
239
|
+
}).trimEnd();
|
|
240
|
+
} catch {
|
|
241
|
+
output = "";
|
|
242
|
+
}
|
|
243
|
+
result = result.replace(full, output);
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
async function buildSkillPrompt(input, registry, context) {
|
|
248
|
+
const parts = input.slice(1).split(/\s+/);
|
|
249
|
+
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
250
|
+
const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
251
|
+
if (!skillCmd) return null;
|
|
252
|
+
const args = parts.slice(1).join(" ").trim();
|
|
253
|
+
const userInstruction = args || skillCmd.description;
|
|
254
|
+
if (skillCmd.skillContent) {
|
|
255
|
+
let processed = await preprocessShellCommands(skillCmd.skillContent);
|
|
256
|
+
processed = substituteVariables(processed, args, context);
|
|
257
|
+
return `<skill name="${cmd}">
|
|
258
|
+
${processed}
|
|
259
|
+
</skill>
|
|
260
|
+
|
|
261
|
+
Execute the "${cmd}" skill: ${userInstruction}`;
|
|
262
|
+
}
|
|
263
|
+
return `Use the "${cmd}" skill: ${userInstruction}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/ui/hooks/plugin-hooks-merger.ts
|
|
267
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
268
|
+
function buildPluginEnv(plugin) {
|
|
269
|
+
const dataDir = join2(dirname2(dirname2(plugin.pluginDir)), "data", plugin.manifest.name);
|
|
270
|
+
return {
|
|
271
|
+
CLAUDE_PLUGIN_ROOT: plugin.pluginDir,
|
|
272
|
+
CLAUDE_PLUGIN_PATH: plugin.pluginDir,
|
|
273
|
+
CLAUDE_PLUGIN_DATA: dataDir
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function resolvePluginRoot(group, pluginDir) {
|
|
277
|
+
if (Array.isArray(group.hooks)) {
|
|
278
|
+
return {
|
|
279
|
+
...group,
|
|
280
|
+
hooks: group.hooks.map((h) => {
|
|
281
|
+
if (typeof h.command === "string") {
|
|
282
|
+
return {
|
|
283
|
+
...h,
|
|
284
|
+
command: h.command.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginDir)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return h;
|
|
288
|
+
})
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return group;
|
|
292
|
+
}
|
|
293
|
+
function mergePluginHooks(plugins) {
|
|
294
|
+
const merged = {};
|
|
295
|
+
for (const plugin of plugins) {
|
|
296
|
+
const hooksObj = plugin.hooks;
|
|
297
|
+
if (!hooksObj) continue;
|
|
298
|
+
const pluginEnv = buildPluginEnv(plugin);
|
|
299
|
+
const innerHooks = hooksObj.hooks ?? hooksObj;
|
|
300
|
+
for (const [event, groups] of Object.entries(innerHooks)) {
|
|
301
|
+
if (!Array.isArray(groups)) continue;
|
|
302
|
+
if (!merged[event]) merged[event] = [];
|
|
303
|
+
const resolved = groups.map((group) => {
|
|
304
|
+
const r = resolvePluginRoot(group, plugin.pluginDir);
|
|
305
|
+
r.env = pluginEnv;
|
|
306
|
+
return r;
|
|
307
|
+
});
|
|
308
|
+
merged[event].push(...resolved);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return merged;
|
|
312
|
+
}
|
|
313
|
+
function mergeHooksIntoConfig(configHooks, pluginHooks) {
|
|
314
|
+
const pluginKeys = Object.keys(pluginHooks);
|
|
315
|
+
if (pluginKeys.length === 0) return configHooks;
|
|
316
|
+
const merged = {};
|
|
317
|
+
for (const [event, groups] of Object.entries(pluginHooks)) {
|
|
318
|
+
merged[event] = [...groups];
|
|
319
|
+
}
|
|
320
|
+
if (configHooks) {
|
|
321
|
+
for (const [event, groups] of Object.entries(configHooks)) {
|
|
322
|
+
if (!Array.isArray(groups)) continue;
|
|
323
|
+
if (!merged[event]) merged[event] = [];
|
|
324
|
+
merged[event].push(...groups);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return merged;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/ui/hooks/useInteractiveSession.ts
|
|
331
|
+
var MAX_RENDERED_MESSAGES = 100;
|
|
332
|
+
function initializeSession(props, permissionHandler) {
|
|
333
|
+
const cwd = props.cwd ?? process.cwd();
|
|
334
|
+
const registry = new CommandRegistry();
|
|
335
|
+
registry.addSource(new BuiltinCommandSource());
|
|
336
|
+
registry.addSource(new SkillCommandSource(cwd));
|
|
337
|
+
let pluginHooks = {};
|
|
338
|
+
const pluginsDir = join3(homedir(), ".robota", "plugins");
|
|
339
|
+
const loader = new BundlePluginLoader(pluginsDir);
|
|
340
|
+
try {
|
|
341
|
+
const plugins = loader.loadPluginsSync();
|
|
342
|
+
if (plugins.length > 0) {
|
|
343
|
+
registry.addSource(new PluginCommandSource(plugins));
|
|
344
|
+
pluginHooks = mergePluginHooks(plugins);
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
const mergedConfig = {
|
|
349
|
+
...props.config,
|
|
350
|
+
hooks: mergeHooksIntoConfig(
|
|
351
|
+
props.config.hooks,
|
|
352
|
+
pluginHooks
|
|
353
|
+
)
|
|
354
|
+
};
|
|
355
|
+
const interactiveSession = new InteractiveSession({
|
|
356
|
+
config: mergedConfig,
|
|
357
|
+
context: props.context,
|
|
358
|
+
projectInfo: props.projectInfo,
|
|
359
|
+
sessionStore: props.sessionStore,
|
|
360
|
+
permissionMode: props.permissionMode,
|
|
361
|
+
maxTurns: props.maxTurns,
|
|
362
|
+
cwd,
|
|
363
|
+
permissionHandler
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
interactiveSession,
|
|
367
|
+
registry,
|
|
368
|
+
commandExecutor: new SystemCommandExecutor(),
|
|
369
|
+
pluginHooks
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function useInteractiveSession(props) {
|
|
373
|
+
const [messages, setMessages] = useState([]);
|
|
374
|
+
const addMessage = useCallback((msg) => {
|
|
375
|
+
setMessages((prev) => {
|
|
376
|
+
const updated = [...prev, msg];
|
|
377
|
+
return updated.length > MAX_RENDERED_MESSAGES ? updated.slice(-MAX_RENDERED_MESSAGES) : updated;
|
|
378
|
+
});
|
|
379
|
+
}, []);
|
|
380
|
+
const [streamingText, setStreamingText] = useState("");
|
|
381
|
+
const [activeTools, setActiveTools] = useState([]);
|
|
382
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
383
|
+
const [isAborting, setIsAborting] = useState(false);
|
|
384
|
+
const [pendingPrompt, setPendingPrompt] = useState(null);
|
|
385
|
+
const [contextState, setContextState] = useState({ percentage: 0, usedTokens: 0, maxTokens: 0 });
|
|
386
|
+
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
387
|
+
const permissionQueueRef = useRef([]);
|
|
388
|
+
const processingRef = useRef(false);
|
|
389
|
+
const processNextPermission = useCallback(() => {
|
|
390
|
+
if (processingRef.current) return;
|
|
391
|
+
const next = permissionQueueRef.current[0];
|
|
392
|
+
if (!next) {
|
|
393
|
+
setPermissionRequest(null);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
processingRef.current = true;
|
|
397
|
+
setPermissionRequest({
|
|
398
|
+
toolName: next.toolName,
|
|
399
|
+
toolArgs: next.toolArgs,
|
|
400
|
+
resolve: (result) => {
|
|
401
|
+
permissionQueueRef.current.shift();
|
|
402
|
+
processingRef.current = false;
|
|
403
|
+
setPermissionRequest(null);
|
|
404
|
+
next.resolve(result);
|
|
405
|
+
setTimeout(() => processNextPermission(), 0);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}, []);
|
|
409
|
+
const permissionHandler = useCallback(
|
|
410
|
+
(toolName, toolArgs) => new Promise((resolve) => {
|
|
411
|
+
permissionQueueRef.current.push({ toolName, toolArgs, resolve });
|
|
412
|
+
processNextPermission();
|
|
413
|
+
}),
|
|
414
|
+
[processNextPermission]
|
|
415
|
+
);
|
|
416
|
+
const stateRef = useRef(null);
|
|
417
|
+
if (stateRef.current === null) {
|
|
418
|
+
stateRef.current = initializeSession(props, permissionHandler);
|
|
419
|
+
}
|
|
420
|
+
const { interactiveSession, registry, commandExecutor } = stateRef.current;
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
let streamBuf = "";
|
|
423
|
+
const onTextDelta = (delta) => {
|
|
424
|
+
streamBuf += delta;
|
|
425
|
+
setStreamingText(streamBuf);
|
|
426
|
+
};
|
|
427
|
+
const onToolStart = (state) => {
|
|
428
|
+
setActiveTools((prev) => [...prev, state]);
|
|
429
|
+
};
|
|
430
|
+
const onToolEnd = (state) => {
|
|
431
|
+
setActiveTools(
|
|
432
|
+
(prev) => prev.map((t) => t.toolName === state.toolName && t.isRunning ? state : t)
|
|
433
|
+
);
|
|
434
|
+
};
|
|
435
|
+
const onThinking = (thinking) => {
|
|
436
|
+
setIsThinking(thinking);
|
|
437
|
+
if (!thinking) {
|
|
438
|
+
setIsAborting(false);
|
|
439
|
+
streamBuf = "";
|
|
440
|
+
setStreamingText("");
|
|
441
|
+
setActiveTools([]);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const onComplete = (result) => {
|
|
445
|
+
setContextState({
|
|
446
|
+
percentage: result.contextState.usedPercentage,
|
|
447
|
+
usedTokens: result.contextState.usedTokens,
|
|
448
|
+
maxTokens: result.contextState.maxTokens
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
const onInterrupted = () => {
|
|
452
|
+
};
|
|
453
|
+
const onError = () => {
|
|
454
|
+
};
|
|
455
|
+
interactiveSession.on("text_delta", onTextDelta);
|
|
456
|
+
interactiveSession.on("tool_start", onToolStart);
|
|
457
|
+
interactiveSession.on("tool_end", onToolEnd);
|
|
458
|
+
interactiveSession.on("thinking", onThinking);
|
|
459
|
+
interactiveSession.on("complete", onComplete);
|
|
460
|
+
interactiveSession.on("interrupted", onInterrupted);
|
|
461
|
+
interactiveSession.on("error", onError);
|
|
462
|
+
return () => {
|
|
463
|
+
interactiveSession.off("text_delta", onTextDelta);
|
|
464
|
+
interactiveSession.off("tool_start", onToolStart);
|
|
465
|
+
interactiveSession.off("tool_end", onToolEnd);
|
|
466
|
+
interactiveSession.off("thinking", onThinking);
|
|
467
|
+
interactiveSession.off("complete", onComplete);
|
|
468
|
+
interactiveSession.off("interrupted", onInterrupted);
|
|
469
|
+
interactiveSession.off("error", onError);
|
|
470
|
+
};
|
|
471
|
+
}, [interactiveSession]);
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
if (!isThinking) {
|
|
474
|
+
const sessionMessages = interactiveSession.getMessages();
|
|
475
|
+
if (sessionMessages.length > 0) {
|
|
476
|
+
setMessages(
|
|
477
|
+
sessionMessages.length > MAX_RENDERED_MESSAGES ? sessionMessages.slice(-MAX_RENDERED_MESSAGES) : [...sessionMessages]
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
setPendingPrompt(interactiveSession.getPendingPrompt());
|
|
481
|
+
}
|
|
482
|
+
}, [isThinking, interactiveSession]);
|
|
483
|
+
const handleSubmit = useCallback(
|
|
484
|
+
async (input) => {
|
|
485
|
+
if (input.startsWith("/")) {
|
|
486
|
+
const parts = input.slice(1).split(/\s+/);
|
|
487
|
+
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
488
|
+
const args = parts.slice(1).join(" ");
|
|
489
|
+
const result = await commandExecutor.execute(cmd, interactiveSession, args);
|
|
490
|
+
if (result) {
|
|
491
|
+
addMessage(createSystemMessage(result.message));
|
|
492
|
+
const effects = interactiveSession;
|
|
493
|
+
if (result.data?.modelId) {
|
|
494
|
+
effects._pendingModelId = result.data.modelId;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (result.data?.language) {
|
|
498
|
+
effects._pendingLanguage = result.data.language;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (result.data?.resetRequested) {
|
|
502
|
+
effects._resetRequested = true;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const ctx = interactiveSession.getContextState();
|
|
506
|
+
setContextState({
|
|
507
|
+
percentage: ctx.usedPercentage,
|
|
508
|
+
usedTokens: ctx.usedTokens,
|
|
509
|
+
maxTokens: ctx.maxTokens
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
514
|
+
if (skillCmd) {
|
|
515
|
+
const prompt = await buildSkillPrompt(input, registry);
|
|
516
|
+
if (prompt) {
|
|
517
|
+
await interactiveSession.submit(prompt);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (cmd === "exit") {
|
|
522
|
+
interactiveSession._exitRequested = true;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (cmd === "plugin") {
|
|
526
|
+
interactiveSession._triggerPluginTUI = true;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
addMessage(createSystemMessage(`Unknown command "/${cmd}". Type /help for help.`));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
await interactiveSession.submit(input);
|
|
533
|
+
},
|
|
534
|
+
[interactiveSession, commandExecutor, registry, addMessage]
|
|
535
|
+
);
|
|
536
|
+
const handleAbort = useCallback(() => {
|
|
537
|
+
setIsAborting(true);
|
|
538
|
+
interactiveSession.abort();
|
|
539
|
+
}, [interactiveSession]);
|
|
540
|
+
const handleCancelQueue = useCallback(() => {
|
|
541
|
+
interactiveSession.cancelQueue();
|
|
542
|
+
setPendingPrompt(null);
|
|
543
|
+
}, [interactiveSession]);
|
|
544
|
+
if (contextState.maxTokens === 0) {
|
|
545
|
+
const ctx = interactiveSession.getContextState();
|
|
546
|
+
setContextState({
|
|
547
|
+
percentage: ctx.usedPercentage,
|
|
548
|
+
usedTokens: ctx.usedTokens,
|
|
549
|
+
maxTokens: ctx.maxTokens
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
interactiveSession,
|
|
554
|
+
registry,
|
|
555
|
+
commandExecutor,
|
|
556
|
+
pluginHooks: stateRef.current.pluginHooks,
|
|
557
|
+
messages,
|
|
558
|
+
addMessage,
|
|
559
|
+
setMessages,
|
|
560
|
+
streamingText,
|
|
561
|
+
activeTools,
|
|
562
|
+
isThinking,
|
|
563
|
+
isAborting,
|
|
564
|
+
pendingPrompt,
|
|
565
|
+
permissionRequest,
|
|
566
|
+
contextState,
|
|
567
|
+
handleSubmit,
|
|
568
|
+
handleAbort,
|
|
569
|
+
handleCancelQueue
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/ui/hooks/usePluginCallbacks.ts
|
|
574
|
+
import { useMemo } from "react";
|
|
575
|
+
import { homedir as homedir2 } from "os";
|
|
576
|
+
import { join as join4 } from "path";
|
|
577
|
+
import {
|
|
578
|
+
PluginSettingsStore,
|
|
579
|
+
BundlePluginLoader as BundlePluginLoader2,
|
|
580
|
+
BundlePluginInstaller,
|
|
581
|
+
MarketplaceClient
|
|
582
|
+
} from "@robota-sdk/agent-sdk";
|
|
583
|
+
function usePluginCallbacks(cwd) {
|
|
584
|
+
return useMemo(() => {
|
|
585
|
+
const home = homedir2();
|
|
586
|
+
const pluginsDir = join4(home, ".robota", "plugins");
|
|
587
|
+
const userSettingsPath = join4(home, ".robota", "settings.json");
|
|
588
|
+
const settingsStore = new PluginSettingsStore(userSettingsPath);
|
|
589
|
+
const marketplace = new MarketplaceClient({ pluginsDir });
|
|
590
|
+
const installer = new BundlePluginInstaller({
|
|
591
|
+
pluginsDir,
|
|
592
|
+
settingsStore,
|
|
593
|
+
marketplaceClient: marketplace
|
|
594
|
+
});
|
|
595
|
+
const loader = new BundlePluginLoader2(pluginsDir);
|
|
596
|
+
return {
|
|
597
|
+
listInstalled: async () => {
|
|
598
|
+
const plugins = await loader.loadAll();
|
|
599
|
+
const enabledMap = settingsStore.getEnabledPlugins();
|
|
600
|
+
return plugins.map((p) => {
|
|
601
|
+
const parts = p.pluginDir.split("/");
|
|
602
|
+
const cacheIdx = parts.indexOf("cache");
|
|
603
|
+
const marketplaceName = cacheIdx >= 0 ? parts[cacheIdx + 1] : "";
|
|
604
|
+
const fullId = marketplaceName ? `${p.manifest.name}@${marketplaceName}` : p.manifest.name;
|
|
605
|
+
return {
|
|
606
|
+
name: fullId,
|
|
607
|
+
description: p.manifest.description,
|
|
608
|
+
enabled: enabledMap[fullId] !== false && enabledMap[p.manifest.name] !== false
|
|
609
|
+
};
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
listAvailablePlugins: async (marketplaceName) => {
|
|
613
|
+
let manifest;
|
|
614
|
+
try {
|
|
615
|
+
manifest = marketplace.fetchManifest(marketplaceName);
|
|
616
|
+
} catch {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
const installed = installer.getInstalledPlugins();
|
|
620
|
+
const installedNames = new Set(Object.values(installed).map((r) => r.pluginName));
|
|
621
|
+
return manifest.plugins.map((p) => ({
|
|
622
|
+
name: p.name,
|
|
623
|
+
description: p.description,
|
|
624
|
+
installed: installedNames.has(p.name)
|
|
625
|
+
}));
|
|
626
|
+
},
|
|
627
|
+
install: async (pluginId, scope) => {
|
|
628
|
+
const [name, marketplaceName] = pluginId.split("@");
|
|
629
|
+
if (!name || !marketplaceName) {
|
|
630
|
+
throw new Error("Plugin ID must be in format: name@marketplace");
|
|
631
|
+
}
|
|
632
|
+
if (scope === "project") {
|
|
633
|
+
const projectPluginsDir = join4(cwd, ".robota", "plugins");
|
|
634
|
+
const projectInstaller = new BundlePluginInstaller({
|
|
635
|
+
pluginsDir: projectPluginsDir,
|
|
636
|
+
settingsStore,
|
|
637
|
+
marketplaceClient: marketplace
|
|
638
|
+
});
|
|
639
|
+
await projectInstaller.install(name, marketplaceName);
|
|
640
|
+
} else {
|
|
641
|
+
await installer.install(name, marketplaceName);
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
uninstall: async (pluginId) => {
|
|
645
|
+
await installer.uninstall(pluginId);
|
|
646
|
+
},
|
|
647
|
+
enable: async (pluginId) => {
|
|
648
|
+
await installer.enable(pluginId);
|
|
649
|
+
},
|
|
650
|
+
disable: async (pluginId) => {
|
|
651
|
+
await installer.disable(pluginId);
|
|
652
|
+
},
|
|
653
|
+
marketplaceAdd: async (source) => {
|
|
654
|
+
if (source.includes("/") && !source.includes(":")) {
|
|
655
|
+
return marketplace.addMarketplace({ type: "github", repo: source });
|
|
656
|
+
} else {
|
|
657
|
+
return marketplace.addMarketplace({ type: "git", url: source });
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
marketplaceRemove: async (name) => {
|
|
661
|
+
const installedFromMarketplace = installer.getPluginsByMarketplace(name);
|
|
662
|
+
for (const record of installedFromMarketplace) {
|
|
663
|
+
await installer.uninstall(`${record.pluginName}@${record.marketplace}`);
|
|
664
|
+
}
|
|
665
|
+
marketplace.removeMarketplace(name);
|
|
666
|
+
},
|
|
667
|
+
marketplaceUpdate: async (name) => {
|
|
668
|
+
marketplace.updateMarketplace(name);
|
|
669
|
+
},
|
|
670
|
+
marketplaceList: async () => {
|
|
671
|
+
return marketplace.listMarketplaces().map((m) => ({
|
|
672
|
+
name: m.name,
|
|
673
|
+
type: m.source.type
|
|
674
|
+
}));
|
|
675
|
+
},
|
|
676
|
+
reloadPlugins: async () => {
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}, [cwd]);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/ui/MessageList.tsx
|
|
683
|
+
import React2 from "react";
|
|
684
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
685
|
+
import { isToolMessage, isAssistantMessage } from "@robota-sdk/agent-core";
|
|
686
|
+
|
|
687
|
+
// src/ui/render-markdown.ts
|
|
688
|
+
import { marked } from "marked";
|
|
689
|
+
import TerminalRenderer from "marked-terminal";
|
|
690
|
+
marked.setOptions({
|
|
691
|
+
renderer: new TerminalRenderer()
|
|
692
|
+
});
|
|
693
|
+
function renderMarkdown(md) {
|
|
694
|
+
const result = marked.parse(md);
|
|
695
|
+
return typeof result === "string" ? result.trimEnd() : md;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/ui/DiffBlock.tsx
|
|
699
|
+
import { Box, Text } from "ink";
|
|
700
|
+
import { jsxs } from "react/jsx-runtime";
|
|
701
|
+
var MAX_DIFF_LINES = 12;
|
|
702
|
+
var TRUNCATED_SHOW = 10;
|
|
703
|
+
function DiffBlock({ file, lines }) {
|
|
704
|
+
const truncated = lines.length > MAX_DIFF_LINES;
|
|
705
|
+
const visible = truncated ? lines.slice(0, TRUNCATED_SHOW) : lines;
|
|
706
|
+
const remaining = lines.length - TRUNCATED_SHOW;
|
|
707
|
+
const maxLineNum = Math.max(...visible.map((l) => l.lineNumber), 0);
|
|
708
|
+
const numWidth = String(maxLineNum).length;
|
|
709
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [
|
|
710
|
+
file && /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
|
|
711
|
+
"\u2502 ",
|
|
712
|
+
file
|
|
713
|
+
] }),
|
|
714
|
+
visible.map((line, i) => {
|
|
715
|
+
const lineNum = String(line.lineNumber).padStart(numWidth, " ");
|
|
716
|
+
if (line.type === "context") {
|
|
717
|
+
return /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
|
|
718
|
+
"\u2502 ",
|
|
719
|
+
lineNum,
|
|
720
|
+
" ",
|
|
721
|
+
line.text
|
|
722
|
+
] }, i);
|
|
723
|
+
}
|
|
724
|
+
const prefix = line.type === "remove" ? "-" : "+";
|
|
725
|
+
const bgColor = line.type === "remove" ? "#5c1a1a" : "#1a3d1a";
|
|
726
|
+
const fgColor = line.type === "remove" ? "#ff9999" : "#99ff99";
|
|
727
|
+
return /* @__PURE__ */ jsxs(Text, { color: fgColor, backgroundColor: bgColor, children: [
|
|
728
|
+
"\u2502 ",
|
|
729
|
+
lineNum,
|
|
730
|
+
" ",
|
|
731
|
+
prefix,
|
|
732
|
+
" ",
|
|
733
|
+
line.text
|
|
734
|
+
] }, i);
|
|
735
|
+
}),
|
|
736
|
+
truncated && /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
|
|
737
|
+
"\u2502 ... and ",
|
|
738
|
+
remaining,
|
|
739
|
+
" more lines"
|
|
740
|
+
] })
|
|
741
|
+
] });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/ui/MessageList.tsx
|
|
745
|
+
import { Fragment, jsx, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
746
|
+
function RoleLabel({ role }) {
|
|
747
|
+
switch (role) {
|
|
748
|
+
case "user":
|
|
749
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "green", bold: true, children: [
|
|
750
|
+
"You:",
|
|
751
|
+
" "
|
|
752
|
+
] });
|
|
753
|
+
case "assistant":
|
|
754
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "cyan", bold: true, children: [
|
|
755
|
+
"Robota:",
|
|
756
|
+
" "
|
|
757
|
+
] });
|
|
758
|
+
case "system":
|
|
759
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "yellow", bold: true, children: [
|
|
760
|
+
"System:",
|
|
761
|
+
" "
|
|
762
|
+
] });
|
|
763
|
+
case "tool":
|
|
764
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
765
|
+
"Tool:",
|
|
766
|
+
" "
|
|
767
|
+
] });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function ToolMessage({ message }) {
|
|
771
|
+
if (!isToolMessage(message)) {
|
|
772
|
+
return /* @__PURE__ */ jsx(Fragment, {});
|
|
773
|
+
}
|
|
774
|
+
const toolName = message.name;
|
|
775
|
+
const content = message.content;
|
|
776
|
+
let summaries = null;
|
|
777
|
+
try {
|
|
778
|
+
const parsed = JSON.parse(content);
|
|
779
|
+
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0].line === "string") {
|
|
780
|
+
summaries = parsed;
|
|
781
|
+
}
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
if (summaries) {
|
|
785
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
786
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
787
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
788
|
+
"Tool:",
|
|
789
|
+
" "
|
|
790
|
+
] }),
|
|
791
|
+
toolName && /* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
|
|
792
|
+
"[",
|
|
793
|
+
toolName,
|
|
794
|
+
"]"
|
|
795
|
+
] })
|
|
796
|
+
] }),
|
|
797
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
798
|
+
summaries.map((s, i) => /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
799
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
800
|
+
" ",
|
|
801
|
+
"\u2713",
|
|
802
|
+
" ",
|
|
803
|
+
s.line
|
|
804
|
+
] }),
|
|
805
|
+
s.diffLines && s.diffLines.length > 0 && /* @__PURE__ */ jsx(DiffBlock, { file: s.diffFile, lines: s.diffLines })
|
|
806
|
+
] }, i))
|
|
807
|
+
] });
|
|
808
|
+
}
|
|
809
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
810
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
811
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
812
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
813
|
+
"Tool:",
|
|
814
|
+
" "
|
|
815
|
+
] }),
|
|
816
|
+
toolName && /* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
|
|
817
|
+
"[",
|
|
818
|
+
toolName,
|
|
819
|
+
"]"
|
|
820
|
+
] })
|
|
821
|
+
] }),
|
|
822
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
823
|
+
lines.map((line, i) => /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
824
|
+
" ",
|
|
825
|
+
"\u2713",
|
|
826
|
+
" ",
|
|
827
|
+
line
|
|
828
|
+
] }, i))
|
|
829
|
+
] });
|
|
830
|
+
}
|
|
831
|
+
var MessageItem = React2.memo(function MessageItem2({
|
|
832
|
+
message
|
|
833
|
+
}) {
|
|
834
|
+
if (isToolMessage(message)) {
|
|
835
|
+
return /* @__PURE__ */ jsx(ToolMessage, { message });
|
|
836
|
+
}
|
|
837
|
+
const content = message.content ?? "";
|
|
838
|
+
const isInterrupted = message.state === "interrupted";
|
|
839
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
840
|
+
/* @__PURE__ */ jsx(Box2, { children: /* @__PURE__ */ jsx(RoleLabel, { role: message.role }) }),
|
|
841
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
842
|
+
/* @__PURE__ */ jsx(Box2, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text2, { wrap: "wrap", children: isAssistantMessage(message) ? renderMarkdown(content + (isInterrupted ? "\n\n_(interrupted)_" : "")) : content }) })
|
|
843
|
+
] });
|
|
844
|
+
});
|
|
845
|
+
function MessageList({ messages }) {
|
|
846
|
+
return /* @__PURE__ */ jsx(Box2, { flexDirection: "column", children: messages.map((msg) => /* @__PURE__ */ jsx(MessageItem, { message: msg }, msg.id)) });
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/ui/StatusBar.tsx
|
|
850
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
851
|
+
import { formatTokenCount } from "@robota-sdk/agent-core";
|
|
852
|
+
import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
853
|
+
var CONTEXT_YELLOW_THRESHOLD = 70;
|
|
854
|
+
var CONTEXT_RED_THRESHOLD = 90;
|
|
855
|
+
function getContextColor(percentage) {
|
|
856
|
+
if (percentage >= CONTEXT_RED_THRESHOLD) return "red";
|
|
857
|
+
if (percentage >= CONTEXT_YELLOW_THRESHOLD) return "yellow";
|
|
858
|
+
return "green";
|
|
859
|
+
}
|
|
860
|
+
function StatusBar({
|
|
861
|
+
permissionMode,
|
|
862
|
+
modelName,
|
|
863
|
+
sessionId: _sessionId,
|
|
864
|
+
messageCount,
|
|
865
|
+
isThinking,
|
|
866
|
+
contextPercentage,
|
|
867
|
+
contextUsedTokens,
|
|
868
|
+
contextMaxTokens
|
|
869
|
+
}) {
|
|
870
|
+
const contextColor = getContextColor(contextPercentage);
|
|
871
|
+
return /* @__PURE__ */ jsxs3(
|
|
872
|
+
Box3,
|
|
873
|
+
{
|
|
874
|
+
borderStyle: "single",
|
|
875
|
+
borderColor: "gray",
|
|
876
|
+
paddingLeft: 1,
|
|
877
|
+
paddingRight: 1,
|
|
878
|
+
justifyContent: "space-between",
|
|
879
|
+
children: [
|
|
880
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
881
|
+
/* @__PURE__ */ jsx2(Text3, { color: "cyan", bold: true, children: "Mode:" }),
|
|
882
|
+
" ",
|
|
883
|
+
/* @__PURE__ */ jsx2(Text3, { children: permissionMode }),
|
|
884
|
+
" | ",
|
|
885
|
+
/* @__PURE__ */ jsx2(Text3, { dimColor: true, children: modelName }),
|
|
886
|
+
" | ",
|
|
887
|
+
/* @__PURE__ */ jsxs3(Text3, { color: contextColor, children: [
|
|
888
|
+
"Context: ",
|
|
889
|
+
Math.round(contextPercentage),
|
|
890
|
+
"% (",
|
|
891
|
+
formatTokenCount(contextUsedTokens),
|
|
892
|
+
"/",
|
|
893
|
+
formatTokenCount(contextMaxTokens),
|
|
894
|
+
")"
|
|
895
|
+
] })
|
|
896
|
+
] }),
|
|
897
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
898
|
+
isThinking && /* @__PURE__ */ jsx2(Text3, { color: "yellow", children: "Thinking... " }),
|
|
899
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
900
|
+
"msgs: ",
|
|
901
|
+
messageCount
|
|
902
|
+
] })
|
|
903
|
+
] })
|
|
904
|
+
]
|
|
905
|
+
}
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/ui/InputArea.tsx
|
|
910
|
+
import React5, { useState as useState4, useCallback as useCallback2, useMemo as useMemo2, useRef as useRef3 } from "react";
|
|
911
|
+
import { Box as Box5, Text as Text7, useInput as useInput2, useStdout } from "ink";
|
|
912
|
+
|
|
913
|
+
// src/ui/CjkTextInput.tsx
|
|
914
|
+
import { useRef as useRef2, useState as useState2 } from "react";
|
|
915
|
+
import { Text as Text4, useInput } from "ink";
|
|
916
|
+
import chalk from "chalk";
|
|
917
|
+
import stringWidth from "string-width";
|
|
918
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
919
|
+
var PASTE_START = "[200~";
|
|
920
|
+
var PASTE_END = "[201~";
|
|
921
|
+
function filterPrintable(input) {
|
|
922
|
+
if (!input || input.length === 0) return "";
|
|
923
|
+
return input.replace(/[\x00-\x1f\x7f]/g, "");
|
|
924
|
+
}
|
|
925
|
+
function insertAtCursor(value, cursor, input) {
|
|
926
|
+
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
927
|
+
return { value: next, cursor: cursor + input.length };
|
|
928
|
+
}
|
|
929
|
+
function displayOffset(chars, charIndex, width) {
|
|
930
|
+
let offset = 0;
|
|
931
|
+
for (let i = 0; i < charIndex && i < chars.length; i++) {
|
|
932
|
+
const w = stringWidth(chars[i]);
|
|
933
|
+
const col = offset % width;
|
|
934
|
+
if (col + w > width) offset += width - col;
|
|
935
|
+
offset += w;
|
|
936
|
+
}
|
|
937
|
+
return offset;
|
|
938
|
+
}
|
|
939
|
+
function charIndexAtDisplayOffset(chars, target, width) {
|
|
940
|
+
let offset = 0;
|
|
941
|
+
for (let i = 0; i < chars.length; i++) {
|
|
942
|
+
if (offset >= target) return i;
|
|
943
|
+
const w = stringWidth(chars[i]);
|
|
944
|
+
const col = offset % width;
|
|
945
|
+
if (col + w > width) offset += width - col;
|
|
946
|
+
offset += w;
|
|
947
|
+
}
|
|
948
|
+
return chars.length;
|
|
949
|
+
}
|
|
950
|
+
function CjkTextInput({
|
|
951
|
+
value,
|
|
952
|
+
onChange,
|
|
953
|
+
onSubmit,
|
|
954
|
+
onPaste,
|
|
955
|
+
placeholder = "",
|
|
956
|
+
focus = true,
|
|
957
|
+
showCursor = true,
|
|
958
|
+
availableWidth
|
|
959
|
+
}) {
|
|
960
|
+
const valueRef = useRef2(value);
|
|
961
|
+
const cursorRef = useRef2(value.length);
|
|
962
|
+
const [, forceRender] = useState2(0);
|
|
963
|
+
const isPastingRef = useRef2(false);
|
|
964
|
+
const pasteBufferRef = useRef2("");
|
|
965
|
+
if (value !== valueRef.current) {
|
|
966
|
+
valueRef.current = value;
|
|
967
|
+
if (cursorRef.current > value.length) {
|
|
968
|
+
cursorRef.current = value.length;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
useInput(
|
|
972
|
+
(input, key) => {
|
|
973
|
+
try {
|
|
974
|
+
if (input === PASTE_START || input.startsWith(PASTE_START)) {
|
|
975
|
+
isPastingRef.current = true;
|
|
976
|
+
const afterMarker = input.slice(PASTE_START.length);
|
|
977
|
+
if (afterMarker.length > 0) {
|
|
978
|
+
pasteBufferRef.current += afterMarker;
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (isPastingRef.current) {
|
|
983
|
+
if (input === PASTE_END || input.includes(PASTE_END)) {
|
|
984
|
+
const beforeMarker = input.split(PASTE_END)[0] ?? "";
|
|
985
|
+
pasteBufferRef.current += beforeMarker;
|
|
986
|
+
const text = pasteBufferRef.current.replace(/\r\n?/g, "\n");
|
|
987
|
+
pasteBufferRef.current = "";
|
|
988
|
+
isPastingRef.current = false;
|
|
989
|
+
if (text.length > 0) {
|
|
990
|
+
if (text.includes("\n") && onPaste) {
|
|
991
|
+
onPaste(text);
|
|
992
|
+
} else {
|
|
993
|
+
const printable2 = filterPrintable(text);
|
|
994
|
+
if (printable2.length > 0) {
|
|
995
|
+
const result2 = insertAtCursor(valueRef.current, cursorRef.current, printable2);
|
|
996
|
+
cursorRef.current = result2.cursor;
|
|
997
|
+
valueRef.current = result2.value;
|
|
998
|
+
onChange(result2.value);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
} else {
|
|
1003
|
+
pasteBufferRef.current += input;
|
|
1004
|
+
}
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (key.ctrl && input === "c" || key.tab || key.shift && key.tab) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (key.upArrow || key.downArrow) {
|
|
1011
|
+
if (availableWidth && availableWidth > 0) {
|
|
1012
|
+
const chars = [...valueRef.current];
|
|
1013
|
+
const offset = displayOffset(chars, cursorRef.current, availableWidth);
|
|
1014
|
+
const target = key.upArrow ? offset - availableWidth : offset + availableWidth;
|
|
1015
|
+
if (target >= 0) {
|
|
1016
|
+
const newCursor = charIndexAtDisplayOffset(chars, target, availableWidth);
|
|
1017
|
+
if (newCursor !== cursorRef.current) {
|
|
1018
|
+
cursorRef.current = newCursor;
|
|
1019
|
+
forceRender((n) => n + 1);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (key.return) {
|
|
1026
|
+
onSubmit?.(valueRef.current);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (input.length > 1 && (input.includes("\n") || input.includes("\r")) && onPaste) {
|
|
1030
|
+
onPaste(input.replace(/\r\n?/g, "\n"));
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (key.leftArrow) {
|
|
1034
|
+
if (cursorRef.current > 0) {
|
|
1035
|
+
cursorRef.current -= 1;
|
|
1036
|
+
forceRender((n) => n + 1);
|
|
1037
|
+
}
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (key.rightArrow) {
|
|
1041
|
+
if (cursorRef.current < valueRef.current.length) {
|
|
1042
|
+
cursorRef.current += 1;
|
|
1043
|
+
forceRender((n) => n + 1);
|
|
1044
|
+
}
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (key.backspace || key.delete) {
|
|
1048
|
+
if (cursorRef.current > 0) {
|
|
1049
|
+
const v = valueRef.current;
|
|
1050
|
+
const next = v.slice(0, cursorRef.current - 1) + v.slice(cursorRef.current);
|
|
1051
|
+
cursorRef.current -= 1;
|
|
1052
|
+
valueRef.current = next;
|
|
1053
|
+
onChange(next);
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const printable = filterPrintable(input);
|
|
1058
|
+
if (printable.length === 0) return;
|
|
1059
|
+
const result = insertAtCursor(valueRef.current, cursorRef.current, printable);
|
|
1060
|
+
cursorRef.current = result.cursor;
|
|
1061
|
+
valueRef.current = result.value;
|
|
1062
|
+
onChange(result.value);
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
{ isActive: focus }
|
|
1067
|
+
);
|
|
1068
|
+
return /* @__PURE__ */ jsx3(Text4, { children: renderWithCursor(valueRef.current, cursorRef.current, placeholder, showCursor && focus) });
|
|
1069
|
+
}
|
|
1070
|
+
function renderWithCursor(value, cursorOffset, placeholder, showCursor) {
|
|
1071
|
+
if (!showCursor) {
|
|
1072
|
+
return value.length > 0 ? value : placeholder ? chalk.gray(placeholder) : "";
|
|
1073
|
+
}
|
|
1074
|
+
if (value.length === 0) {
|
|
1075
|
+
if (placeholder.length > 0) {
|
|
1076
|
+
return chalk.inverse(placeholder[0]) + chalk.gray(placeholder.slice(1));
|
|
1077
|
+
}
|
|
1078
|
+
return chalk.inverse(" ");
|
|
1079
|
+
}
|
|
1080
|
+
const chars = [...value];
|
|
1081
|
+
let rendered = "";
|
|
1082
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1083
|
+
const char = chars[i] ?? "";
|
|
1084
|
+
rendered += i === cursorOffset ? chalk.inverse(char) : char;
|
|
1085
|
+
}
|
|
1086
|
+
if (cursorOffset >= chars.length) {
|
|
1087
|
+
rendered += chalk.inverse(" ");
|
|
1088
|
+
}
|
|
1089
|
+
return rendered;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/ui/WaveText.tsx
|
|
1093
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
1094
|
+
import { Text as Text5 } from "ink";
|
|
1095
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1096
|
+
var WAVE_COLORS = ["#666666", "#888888", "#aaaaaa", "#888888"];
|
|
1097
|
+
var INTERVAL_MS = 400;
|
|
1098
|
+
var CHARS_PER_GROUP = 4;
|
|
1099
|
+
function WaveText({ text }) {
|
|
1100
|
+
const [tick, setTick] = useState3(0);
|
|
1101
|
+
useEffect2(() => {
|
|
1102
|
+
const timer = setInterval(() => {
|
|
1103
|
+
setTick((prev) => prev + 1);
|
|
1104
|
+
}, INTERVAL_MS);
|
|
1105
|
+
return () => clearInterval(timer);
|
|
1106
|
+
}, []);
|
|
1107
|
+
const chars = [...text];
|
|
1108
|
+
return /* @__PURE__ */ jsx4(Text5, { children: chars.map((char, i) => {
|
|
1109
|
+
const group = Math.floor(i / CHARS_PER_GROUP);
|
|
1110
|
+
const colorIndex = (tick + group) % WAVE_COLORS.length;
|
|
1111
|
+
return /* @__PURE__ */ jsx4(Text5, { color: WAVE_COLORS[colorIndex], children: char }, i);
|
|
1112
|
+
}) });
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/ui/SlashAutocomplete.tsx
|
|
1116
|
+
import { Box as Box4, Text as Text6 } from "ink";
|
|
1117
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1118
|
+
var MAX_VISIBLE = 8;
|
|
1119
|
+
function CommandRow(props) {
|
|
1120
|
+
const { cmd, isSelected, showSlash } = props;
|
|
1121
|
+
const indicator = isSelected ? "\u25B8 " : " ";
|
|
1122
|
+
const nameColor = isSelected ? "cyan" : void 0;
|
|
1123
|
+
const dimmed = !isSelected;
|
|
1124
|
+
return /* @__PURE__ */ jsx5(Box4, { children: /* @__PURE__ */ jsxs4(Text6, { color: nameColor, dimColor: dimmed, children: [
|
|
1125
|
+
indicator,
|
|
1126
|
+
showSlash ? `/${cmd.name} ${cmd.description}` : cmd.description
|
|
1127
|
+
] }) });
|
|
1128
|
+
}
|
|
1129
|
+
function SlashAutocomplete({
|
|
1130
|
+
commands,
|
|
1131
|
+
selectedIndex,
|
|
1132
|
+
visible,
|
|
1133
|
+
isSubcommandMode
|
|
1134
|
+
}) {
|
|
1135
|
+
if (!visible || commands.length === 0) return null;
|
|
1136
|
+
const scrollOffset = computeScrollOffset(selectedIndex, commands.length);
|
|
1137
|
+
const visibleCommands = commands.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
|
|
1138
|
+
return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: visibleCommands.map((cmd, i) => /* @__PURE__ */ jsx5(
|
|
1139
|
+
CommandRow,
|
|
1140
|
+
{
|
|
1141
|
+
cmd,
|
|
1142
|
+
isSelected: scrollOffset + i === selectedIndex,
|
|
1143
|
+
showSlash: !isSubcommandMode
|
|
1144
|
+
},
|
|
1145
|
+
cmd.name
|
|
1146
|
+
)) });
|
|
1147
|
+
}
|
|
1148
|
+
function computeScrollOffset(selectedIndex, total) {
|
|
1149
|
+
if (total <= MAX_VISIBLE) return 0;
|
|
1150
|
+
if (selectedIndex < MAX_VISIBLE) return 0;
|
|
1151
|
+
const maxOffset = total - MAX_VISIBLE;
|
|
1152
|
+
return Math.min(selectedIndex - MAX_VISIBLE + 1, maxOffset);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/utils/paste-labels.ts
|
|
1156
|
+
var PASTE_LABEL_RE = /\[Pasted text #(\d+)(?: \+\d+ lines)?\]/g;
|
|
1157
|
+
function expandPasteLabels(text, store) {
|
|
1158
|
+
return text.replace(PASTE_LABEL_RE, (_, id) => store.get(Number(id)) ?? "");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/ui/InputArea.tsx
|
|
1162
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1163
|
+
function parseSlashInput(value) {
|
|
1164
|
+
if (!value.startsWith("/")) return { isSlash: false, parentCommand: "", filter: "" };
|
|
1165
|
+
const afterSlash = value.slice(1);
|
|
1166
|
+
const spaceIndex = afterSlash.indexOf(" ");
|
|
1167
|
+
if (spaceIndex === -1) return { isSlash: true, parentCommand: "", filter: afterSlash };
|
|
1168
|
+
const parent = afterSlash.slice(0, spaceIndex);
|
|
1169
|
+
const rest = afterSlash.slice(spaceIndex + 1);
|
|
1170
|
+
return { isSlash: true, parentCommand: parent, filter: rest };
|
|
1171
|
+
}
|
|
1172
|
+
function useAutocomplete(value, registry) {
|
|
1173
|
+
const [selectedIndex, setSelectedIndex] = useState4(0);
|
|
1174
|
+
const [dismissed, setDismissed] = useState4(false);
|
|
1175
|
+
const prevValueRef = React5.useRef(value);
|
|
1176
|
+
if (prevValueRef.current !== value) {
|
|
1177
|
+
prevValueRef.current = value;
|
|
1178
|
+
if (dismissed) setDismissed(false);
|
|
1179
|
+
}
|
|
1180
|
+
const parsed = parseSlashInput(value);
|
|
1181
|
+
const isSubcommandMode = parsed.isSlash && parsed.parentCommand.length > 0;
|
|
1182
|
+
const filteredCommands = useMemo2(() => {
|
|
1183
|
+
if (!registry || !parsed.isSlash || dismissed) return [];
|
|
1184
|
+
if (isSubcommandMode) {
|
|
1185
|
+
const subs = registry.getSubcommands(parsed.parentCommand);
|
|
1186
|
+
if (subs.length === 0) return [];
|
|
1187
|
+
if (!parsed.filter) return subs;
|
|
1188
|
+
const lower = parsed.filter.toLowerCase();
|
|
1189
|
+
return subs.filter((c) => c.name.toLowerCase().startsWith(lower));
|
|
1190
|
+
}
|
|
1191
|
+
return registry.getCommands(parsed.filter);
|
|
1192
|
+
}, [registry, parsed.isSlash, parsed.parentCommand, parsed.filter, dismissed, isSubcommandMode]);
|
|
1193
|
+
const showPopup = parsed.isSlash && filteredCommands.length > 0 && !dismissed;
|
|
1194
|
+
if (selectedIndex >= filteredCommands.length && filteredCommands.length > 0) {
|
|
1195
|
+
setSelectedIndex(filteredCommands.length - 1);
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
showPopup,
|
|
1199
|
+
filteredCommands,
|
|
1200
|
+
selectedIndex,
|
|
1201
|
+
setSelectedIndex,
|
|
1202
|
+
isSubcommandMode,
|
|
1203
|
+
setShowPopup: (val) => {
|
|
1204
|
+
if (typeof val === "function") {
|
|
1205
|
+
setDismissed((prev) => {
|
|
1206
|
+
const nextVal = val(!prev);
|
|
1207
|
+
return !nextVal;
|
|
1208
|
+
});
|
|
1209
|
+
} else {
|
|
1210
|
+
setDismissed(!val);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
var BORDER_HORIZONTAL = 2;
|
|
1216
|
+
var PADDING_LEFT = 1;
|
|
1217
|
+
var PROMPT_WIDTH = 2;
|
|
1218
|
+
var INPUT_AREA_OVERHEAD = BORDER_HORIZONTAL + PADDING_LEFT + PROMPT_WIDTH;
|
|
1219
|
+
function InputArea({
|
|
1220
|
+
onSubmit,
|
|
1221
|
+
onCancelQueue,
|
|
1222
|
+
isDisabled,
|
|
1223
|
+
isAborting,
|
|
1224
|
+
pendingPrompt,
|
|
1225
|
+
registry
|
|
1226
|
+
}) {
|
|
1227
|
+
const [value, setValue] = useState4("");
|
|
1228
|
+
const pasteStore = useRef3(/* @__PURE__ */ new Map());
|
|
1229
|
+
const { stdout } = useStdout();
|
|
1230
|
+
const terminalColumns = stdout?.columns ?? 80;
|
|
1231
|
+
const availableWidth = Math.max(1, terminalColumns - INPUT_AREA_OVERHEAD);
|
|
1232
|
+
const pasteIdRef = useRef3(0);
|
|
1233
|
+
const {
|
|
1234
|
+
showPopup,
|
|
1235
|
+
filteredCommands,
|
|
1236
|
+
selectedIndex,
|
|
1237
|
+
setSelectedIndex,
|
|
1238
|
+
isSubcommandMode,
|
|
1239
|
+
setShowPopup
|
|
1240
|
+
} = useAutocomplete(value, registry);
|
|
1241
|
+
const handlePaste = useCallback2((text) => {
|
|
1242
|
+
pasteIdRef.current += 1;
|
|
1243
|
+
const id = pasteIdRef.current;
|
|
1244
|
+
pasteStore.current.set(id, text);
|
|
1245
|
+
const lineCount = text.split("\n").length;
|
|
1246
|
+
const label = `[Pasted text #${id} +${lineCount} lines]`;
|
|
1247
|
+
setValue((prev) => prev ? `${prev} ${label}` : label);
|
|
1248
|
+
}, []);
|
|
1249
|
+
const handleSubmit = useCallback2(
|
|
1250
|
+
(text) => {
|
|
1251
|
+
const trimmed = text.trim();
|
|
1252
|
+
if (trimmed.length === 0) return;
|
|
1253
|
+
if (showPopup && filteredCommands[selectedIndex]) {
|
|
1254
|
+
selectCommand(filteredCommands[selectedIndex]);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const expanded = expandPasteLabels(trimmed, pasteStore.current);
|
|
1258
|
+
setValue("");
|
|
1259
|
+
pasteStore.current.clear();
|
|
1260
|
+
pasteIdRef.current = 0;
|
|
1261
|
+
onSubmit(expanded);
|
|
1262
|
+
},
|
|
1263
|
+
[showPopup, filteredCommands, selectedIndex, onSubmit]
|
|
1264
|
+
);
|
|
1265
|
+
const selectCommand = useCallback2(
|
|
1266
|
+
(cmd) => {
|
|
1267
|
+
const parsed = parseSlashInput(value);
|
|
1268
|
+
if (parsed.parentCommand) {
|
|
1269
|
+
const fullCommand = `/${parsed.parentCommand} ${cmd.name}`;
|
|
1270
|
+
setValue("");
|
|
1271
|
+
onSubmit(fullCommand);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (cmd.subcommands && cmd.subcommands.length > 0) {
|
|
1275
|
+
setValue(`/${cmd.name} `);
|
|
1276
|
+
setSelectedIndex(0);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
setValue("");
|
|
1280
|
+
onSubmit(`/${cmd.name}`);
|
|
1281
|
+
},
|
|
1282
|
+
[value, onSubmit, setSelectedIndex]
|
|
1283
|
+
);
|
|
1284
|
+
useInput2(
|
|
1285
|
+
(_input, key) => {
|
|
1286
|
+
if (!showPopup) return;
|
|
1287
|
+
if (key.upArrow) {
|
|
1288
|
+
setSelectedIndex((prev) => prev > 0 ? prev - 1 : filteredCommands.length - 1);
|
|
1289
|
+
} else if (key.downArrow) {
|
|
1290
|
+
setSelectedIndex((prev) => prev < filteredCommands.length - 1 ? prev + 1 : 0);
|
|
1291
|
+
} else if (key.escape) {
|
|
1292
|
+
setShowPopup(false);
|
|
1293
|
+
} else if (key.tab) {
|
|
1294
|
+
const cmd = filteredCommands[selectedIndex];
|
|
1295
|
+
if (cmd) selectCommand(cmd);
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1298
|
+
{ isActive: showPopup && !isDisabled }
|
|
1299
|
+
);
|
|
1300
|
+
useInput2(
|
|
1301
|
+
(_input, key) => {
|
|
1302
|
+
if ((key.backspace || key.delete) && pendingPrompt) {
|
|
1303
|
+
onCancelQueue?.();
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
{ isActive: !!pendingPrompt }
|
|
1307
|
+
);
|
|
1308
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
1309
|
+
showPopup && /* @__PURE__ */ jsx6(
|
|
1310
|
+
SlashAutocomplete,
|
|
1311
|
+
{
|
|
1312
|
+
commands: filteredCommands,
|
|
1313
|
+
selectedIndex,
|
|
1314
|
+
visible: showPopup,
|
|
1315
|
+
isSubcommandMode
|
|
1316
|
+
}
|
|
1317
|
+
),
|
|
1318
|
+
/* @__PURE__ */ jsx6(
|
|
1319
|
+
Box5,
|
|
1320
|
+
{
|
|
1321
|
+
borderStyle: "single",
|
|
1322
|
+
borderColor: isAborting ? "yellow" : pendingPrompt ? "cyan" : isDisabled ? "gray" : "green",
|
|
1323
|
+
paddingLeft: 1,
|
|
1324
|
+
children: isAborting ? /* @__PURE__ */ jsx6(Text7, { color: "yellow", children: " Interrupting..." }) : pendingPrompt ? /* @__PURE__ */ jsxs5(Text7, { color: "cyan", children: [
|
|
1325
|
+
" ",
|
|
1326
|
+
"Queued: ",
|
|
1327
|
+
pendingPrompt.length > 50 ? pendingPrompt.slice(0, 47) + "..." : pendingPrompt,
|
|
1328
|
+
" ",
|
|
1329
|
+
/* @__PURE__ */ jsx6(Text7, { dimColor: true, children: "(Backspace to cancel)" })
|
|
1330
|
+
] }) : isDisabled ? /* @__PURE__ */ jsx6(WaveText, { text: " Waiting for response... (ESC to interrupt)" }) : /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1331
|
+
/* @__PURE__ */ jsx6(Text7, { color: "green", bold: true, children: "> " }),
|
|
1332
|
+
/* @__PURE__ */ jsx6(
|
|
1333
|
+
CjkTextInput,
|
|
1334
|
+
{
|
|
1335
|
+
value,
|
|
1336
|
+
onChange: setValue,
|
|
1337
|
+
onSubmit: handleSubmit,
|
|
1338
|
+
onPaste: handlePaste,
|
|
1339
|
+
placeholder: "Type a message or /help",
|
|
1340
|
+
availableWidth
|
|
1341
|
+
}
|
|
1342
|
+
)
|
|
1343
|
+
] })
|
|
1344
|
+
}
|
|
1345
|
+
)
|
|
1346
|
+
] });
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/ui/ConfirmPrompt.tsx
|
|
1350
|
+
import { useState as useState5, useCallback as useCallback3, useRef as useRef4 } from "react";
|
|
1351
|
+
import { Box as Box6, Text as Text8, useInput as useInput3 } from "ink";
|
|
1352
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1353
|
+
function ConfirmPrompt({
|
|
1354
|
+
message,
|
|
1355
|
+
options = ["Yes", "No"],
|
|
1356
|
+
onSelect
|
|
1357
|
+
}) {
|
|
1358
|
+
const [selected, setSelected] = useState5(0);
|
|
1359
|
+
const resolvedRef = useRef4(false);
|
|
1360
|
+
const doSelect = useCallback3(
|
|
1361
|
+
(index) => {
|
|
1362
|
+
if (resolvedRef.current) return;
|
|
1363
|
+
resolvedRef.current = true;
|
|
1364
|
+
onSelect(index);
|
|
1365
|
+
},
|
|
1366
|
+
[onSelect]
|
|
1367
|
+
);
|
|
1368
|
+
useInput3((input, key) => {
|
|
1369
|
+
if (resolvedRef.current) return;
|
|
1370
|
+
if (key.leftArrow || key.upArrow) {
|
|
1371
|
+
setSelected((prev) => prev > 0 ? prev - 1 : prev);
|
|
1372
|
+
} else if (key.rightArrow || key.downArrow) {
|
|
1373
|
+
setSelected((prev) => prev < options.length - 1 ? prev + 1 : prev);
|
|
1374
|
+
} else if (key.return) {
|
|
1375
|
+
doSelect(selected);
|
|
1376
|
+
} else if (input === "y" && options.length === 2) {
|
|
1377
|
+
doSelect(0);
|
|
1378
|
+
} else if (input === "n" && options.length === 2) {
|
|
1379
|
+
doSelect(1);
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
1383
|
+
/* @__PURE__ */ jsx7(Text8, { color: "yellow", children: message }),
|
|
1384
|
+
/* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsx7(Box6, { marginRight: 2, children: /* @__PURE__ */ jsxs6(Text8, { color: i === selected ? "cyan" : void 0, bold: i === selected, children: [
|
|
1385
|
+
i === selected ? "> " : " ",
|
|
1386
|
+
opt
|
|
1387
|
+
] }) }, opt)) }),
|
|
1388
|
+
/* @__PURE__ */ jsx7(Text8, { dimColor: true, children: " arrow keys to select, Enter to confirm" })
|
|
1389
|
+
] });
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/ui/PermissionPrompt.tsx
|
|
1393
|
+
import React7 from "react";
|
|
1394
|
+
import { Box as Box7, Text as Text9, useInput as useInput4 } from "ink";
|
|
1395
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1396
|
+
var OPTIONS = ["Allow", "Allow always (this session)", "Deny"];
|
|
1397
|
+
function formatArgs(args) {
|
|
1398
|
+
const entries = Object.entries(args);
|
|
1399
|
+
if (entries.length === 0) return "(no arguments)";
|
|
1400
|
+
return entries.map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
|
|
1401
|
+
}
|
|
1402
|
+
function PermissionPrompt({ request }) {
|
|
1403
|
+
const [selected, setSelected] = React7.useState(0);
|
|
1404
|
+
const resolvedRef = React7.useRef(false);
|
|
1405
|
+
const prevRequestRef = React7.useRef(request);
|
|
1406
|
+
if (prevRequestRef.current !== request) {
|
|
1407
|
+
prevRequestRef.current = request;
|
|
1408
|
+
resolvedRef.current = false;
|
|
1409
|
+
setSelected(0);
|
|
1410
|
+
}
|
|
1411
|
+
const doResolve = React7.useCallback(
|
|
1412
|
+
(index) => {
|
|
1413
|
+
if (resolvedRef.current) return;
|
|
1414
|
+
resolvedRef.current = true;
|
|
1415
|
+
if (index === 0) request.resolve(true);
|
|
1416
|
+
else if (index === 1) request.resolve("allow-session");
|
|
1417
|
+
else request.resolve(false);
|
|
1418
|
+
},
|
|
1419
|
+
[request]
|
|
1420
|
+
);
|
|
1421
|
+
useInput4((input, key) => {
|
|
1422
|
+
if (resolvedRef.current) return;
|
|
1423
|
+
if (key.upArrow || key.leftArrow) {
|
|
1424
|
+
setSelected((prev) => prev > 0 ? prev - 1 : prev);
|
|
1425
|
+
} else if (key.downArrow || key.rightArrow) {
|
|
1426
|
+
setSelected((prev) => prev < OPTIONS.length - 1 ? prev + 1 : prev);
|
|
1427
|
+
} else if (key.return) {
|
|
1428
|
+
doResolve(selected);
|
|
1429
|
+
} else if (input === "y" || input === "1") {
|
|
1430
|
+
doResolve(0);
|
|
1431
|
+
} else if (input === "a" || input === "2") {
|
|
1432
|
+
doResolve(1);
|
|
1433
|
+
} else if (input === "n" || input === "d" || input === "3") {
|
|
1434
|
+
doResolve(2);
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
1438
|
+
/* @__PURE__ */ jsx8(Text9, { color: "yellow", bold: true, children: "[Permission Required]" }),
|
|
1439
|
+
/* @__PURE__ */ jsxs7(Text9, { children: [
|
|
1440
|
+
"Tool:",
|
|
1441
|
+
" ",
|
|
1442
|
+
/* @__PURE__ */ jsx8(Text9, { color: "cyan", bold: true, children: request.toolName })
|
|
1443
|
+
] }),
|
|
1444
|
+
/* @__PURE__ */ jsxs7(Text9, { dimColor: true, children: [
|
|
1445
|
+
" ",
|
|
1446
|
+
formatArgs(request.toolArgs)
|
|
1447
|
+
] }),
|
|
1448
|
+
/* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsx8(Box7, { marginRight: 2, children: /* @__PURE__ */ jsxs7(Text9, { color: i === selected ? "cyan" : void 0, bold: i === selected, children: [
|
|
1449
|
+
i === selected ? "> " : " ",
|
|
1450
|
+
opt
|
|
1451
|
+
] }) }, opt)) }),
|
|
1452
|
+
/* @__PURE__ */ jsx8(Text9, { dimColor: true, children: " left/right to select, Enter to confirm" })
|
|
1453
|
+
] });
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/ui/StreamingIndicator.tsx
|
|
1457
|
+
import { Box as Box8, Text as Text10 } from "ink";
|
|
1458
|
+
import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1459
|
+
function getToolStyle(t) {
|
|
1460
|
+
if (t.isRunning) return { color: "yellow", icon: "\u27F3", strikethrough: false };
|
|
1461
|
+
if (t.result === "error") return { color: "red", icon: "\u2717", strikethrough: true };
|
|
1462
|
+
if (t.result === "denied") return { color: "yellowBright", icon: "\u2298", strikethrough: true };
|
|
1463
|
+
return { color: "green", icon: "\u2713", strikethrough: false };
|
|
1464
|
+
}
|
|
1465
|
+
function StreamingIndicator({ text, activeTools }) {
|
|
1466
|
+
const hasTools = activeTools.length > 0;
|
|
1467
|
+
const hasText = text.length > 0;
|
|
1468
|
+
if (!hasTools && !hasText) {
|
|
1469
|
+
return /* @__PURE__ */ jsx9(Fragment2, {});
|
|
1470
|
+
}
|
|
1471
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1472
|
+
hasTools && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
|
|
1473
|
+
/* @__PURE__ */ jsx9(Text10, { color: "white", bold: true, children: "Tools:" }),
|
|
1474
|
+
/* @__PURE__ */ jsx9(Text10, { children: " " }),
|
|
1475
|
+
activeTools.map((t, i) => {
|
|
1476
|
+
const { color, icon, strikethrough } = getToolStyle(t);
|
|
1477
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1478
|
+
/* @__PURE__ */ jsxs8(Text10, { color, strikethrough, children: [
|
|
1479
|
+
" ",
|
|
1480
|
+
icon,
|
|
1481
|
+
" ",
|
|
1482
|
+
t.toolName,
|
|
1483
|
+
"(",
|
|
1484
|
+
t.firstArg,
|
|
1485
|
+
")"
|
|
1486
|
+
] }),
|
|
1487
|
+
t.diffLines && t.diffLines.length > 0 && /* @__PURE__ */ jsx9(DiffBlock, { file: t.diffFile, lines: t.diffLines })
|
|
1488
|
+
] }, `${t.toolName}-${i}`);
|
|
1489
|
+
})
|
|
1490
|
+
] }),
|
|
1491
|
+
hasText && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
|
|
1492
|
+
/* @__PURE__ */ jsx9(Text10, { color: "cyan", bold: true, children: "Robota:" }),
|
|
1493
|
+
/* @__PURE__ */ jsx9(Text10, { children: " " }),
|
|
1494
|
+
/* @__PURE__ */ jsx9(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text10, { wrap: "wrap", children: renderMarkdown(text) }) })
|
|
1495
|
+
] })
|
|
1496
|
+
] });
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/ui/PluginTUI.tsx
|
|
1500
|
+
import { useState as useState8, useEffect as useEffect3, useCallback as useCallback6 } from "react";
|
|
1501
|
+
|
|
1502
|
+
// src/ui/MenuSelect.tsx
|
|
1503
|
+
import { useState as useState6, useCallback as useCallback4, useRef as useRef5 } from "react";
|
|
1504
|
+
import { Box as Box9, Text as Text11, useInput as useInput5 } from "ink";
|
|
1505
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1506
|
+
function MenuSelect({
|
|
1507
|
+
title,
|
|
1508
|
+
items,
|
|
1509
|
+
onSelect,
|
|
1510
|
+
onBack,
|
|
1511
|
+
loading,
|
|
1512
|
+
error
|
|
1513
|
+
}) {
|
|
1514
|
+
const [selected, setSelected] = useState6(0);
|
|
1515
|
+
const selectedRef = useRef5(0);
|
|
1516
|
+
const resolvedRef = useRef5(false);
|
|
1517
|
+
const doSelect = useCallback4(
|
|
1518
|
+
(index) => {
|
|
1519
|
+
if (resolvedRef.current || items.length === 0) return;
|
|
1520
|
+
resolvedRef.current = true;
|
|
1521
|
+
onSelect(items[index].value);
|
|
1522
|
+
},
|
|
1523
|
+
[items, onSelect]
|
|
1524
|
+
);
|
|
1525
|
+
useInput5((input, key) => {
|
|
1526
|
+
if (resolvedRef.current) return;
|
|
1527
|
+
if (key.escape) {
|
|
1528
|
+
resolvedRef.current = true;
|
|
1529
|
+
onBack();
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if (loading || error || items.length === 0) return;
|
|
1533
|
+
if (key.upArrow) {
|
|
1534
|
+
const next = selectedRef.current > 0 ? selectedRef.current - 1 : selectedRef.current;
|
|
1535
|
+
selectedRef.current = next;
|
|
1536
|
+
setSelected(next);
|
|
1537
|
+
} else if (key.downArrow) {
|
|
1538
|
+
const next = selectedRef.current < items.length - 1 ? selectedRef.current + 1 : selectedRef.current;
|
|
1539
|
+
selectedRef.current = next;
|
|
1540
|
+
setSelected(next);
|
|
1541
|
+
} else if (key.return) {
|
|
1542
|
+
doSelect(selectedRef.current);
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
1546
|
+
/* @__PURE__ */ jsx10(Text11, { color: "yellow", bold: true, children: title }),
|
|
1547
|
+
loading && /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text11, { dimColor: true, children: "Loading..." }) }),
|
|
1548
|
+
error && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
1549
|
+
/* @__PURE__ */ jsx10(Text11, { color: "red", children: error }),
|
|
1550
|
+
/* @__PURE__ */ jsx10(Text11, { dimColor: true, children: "Press Esc to go back" })
|
|
1551
|
+
] }),
|
|
1552
|
+
!loading && !error && /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginTop: 1, children: items.map((item, i) => /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1553
|
+
/* @__PURE__ */ jsxs9(Text11, { color: i === selected ? "cyan" : void 0, bold: i === selected, children: [
|
|
1554
|
+
i === selected ? "> " : " ",
|
|
1555
|
+
item.label
|
|
1556
|
+
] }),
|
|
1557
|
+
item.hint && /* @__PURE__ */ jsxs9(Text11, { dimColor: true, children: [
|
|
1558
|
+
" ",
|
|
1559
|
+
item.hint
|
|
1560
|
+
] })
|
|
1561
|
+
] }, item.value)) }),
|
|
1562
|
+
/* @__PURE__ */ jsx10(Text11, { dimColor: true, children: loading || error ? "" : " \u2191\u2193 Navigate Enter Select Esc Back" })
|
|
1563
|
+
] });
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/ui/TextPrompt.tsx
|
|
1567
|
+
import { useState as useState7, useRef as useRef6, useCallback as useCallback5 } from "react";
|
|
1568
|
+
import { Box as Box10, Text as Text12, useInput as useInput6 } from "ink";
|
|
1569
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1570
|
+
function TextPrompt({
|
|
1571
|
+
title,
|
|
1572
|
+
placeholder,
|
|
1573
|
+
onSubmit,
|
|
1574
|
+
onCancel,
|
|
1575
|
+
validate
|
|
1576
|
+
}) {
|
|
1577
|
+
const [value, setValue] = useState7("");
|
|
1578
|
+
const [error, setError] = useState7();
|
|
1579
|
+
const resolvedRef = useRef6(false);
|
|
1580
|
+
const valueRef = useRef6("");
|
|
1581
|
+
const handleSubmit = useCallback5(() => {
|
|
1582
|
+
if (resolvedRef.current) return;
|
|
1583
|
+
const trimmed = valueRef.current.trim();
|
|
1584
|
+
if (!trimmed) return;
|
|
1585
|
+
if (validate) {
|
|
1586
|
+
const err = validate(trimmed);
|
|
1587
|
+
if (err) {
|
|
1588
|
+
setError(err);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
resolvedRef.current = true;
|
|
1593
|
+
onSubmit(trimmed);
|
|
1594
|
+
}, [validate, onSubmit]);
|
|
1595
|
+
useInput6((input, key) => {
|
|
1596
|
+
if (resolvedRef.current) return;
|
|
1597
|
+
if (key.escape) {
|
|
1598
|
+
resolvedRef.current = true;
|
|
1599
|
+
onCancel();
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (key.return) {
|
|
1603
|
+
handleSubmit();
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (key.backspace || key.delete) {
|
|
1607
|
+
valueRef.current = valueRef.current.slice(0, -1);
|
|
1608
|
+
setValue(valueRef.current);
|
|
1609
|
+
setError(void 0);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
if (input && !key.ctrl && !key.meta) {
|
|
1613
|
+
valueRef.current = valueRef.current + input;
|
|
1614
|
+
setValue(valueRef.current);
|
|
1615
|
+
setError(void 0);
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
1619
|
+
/* @__PURE__ */ jsx11(Text12, { color: "yellow", bold: true, children: title }),
|
|
1620
|
+
/* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
|
|
1621
|
+
/* @__PURE__ */ jsx11(Text12, { color: "cyan", children: "> " }),
|
|
1622
|
+
value ? /* @__PURE__ */ jsx11(Text12, { children: value }) : placeholder ? /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: placeholder }) : null,
|
|
1623
|
+
/* @__PURE__ */ jsx11(Text12, { color: "cyan", children: "\u2588" })
|
|
1624
|
+
] }),
|
|
1625
|
+
error && /* @__PURE__ */ jsx11(Text12, { color: "red", children: error }),
|
|
1626
|
+
/* @__PURE__ */ jsx11(Text12, { dimColor: true, children: " Enter Submit Esc Cancel" })
|
|
1627
|
+
] });
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/ui/plugin-tui-handlers.ts
|
|
1631
|
+
function handleMainSelect(value, nav) {
|
|
1632
|
+
if (value === "marketplace") {
|
|
1633
|
+
nav.push({ screen: "marketplace-list" });
|
|
1634
|
+
} else if (value === "installed") {
|
|
1635
|
+
nav.push({ screen: "installed-list" });
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
function handleMarketplaceListSelect(value, nav) {
|
|
1639
|
+
if (value === "__add__") {
|
|
1640
|
+
nav.push({ screen: "marketplace-add" });
|
|
1641
|
+
} else {
|
|
1642
|
+
nav.push({ screen: "marketplace-action", context: { marketplace: value } });
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function handleMarketplaceActionSelect(value, marketplace, callbacks, nav) {
|
|
1646
|
+
if (value === "browse") {
|
|
1647
|
+
nav.push({ screen: "marketplace-browse", context: { marketplace } });
|
|
1648
|
+
} else if (value === "update") {
|
|
1649
|
+
callbacks.marketplaceUpdate(marketplace).then(() => {
|
|
1650
|
+
nav.notify(`Updated marketplace "${marketplace}".`);
|
|
1651
|
+
nav.pop();
|
|
1652
|
+
}).catch((err) => {
|
|
1653
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1654
|
+
});
|
|
1655
|
+
} else if (value === "remove") {
|
|
1656
|
+
nav.setConfirm({
|
|
1657
|
+
message: `Remove marketplace "${marketplace}" and all its plugins?`,
|
|
1658
|
+
onConfirm: () => {
|
|
1659
|
+
nav.setConfirm(void 0);
|
|
1660
|
+
callbacks.marketplaceRemove(marketplace).then(() => {
|
|
1661
|
+
nav.notify(`Removed marketplace "${marketplace}".`);
|
|
1662
|
+
nav.popN(2);
|
|
1663
|
+
}).catch((err) => {
|
|
1664
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1665
|
+
});
|
|
1666
|
+
},
|
|
1667
|
+
onCancel: () => nav.setConfirm(void 0)
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
function handleMarketplaceBrowseSelect(value, marketplace, items, nav) {
|
|
1672
|
+
const fullId = `${value}@${marketplace}`;
|
|
1673
|
+
const item = items.find((i) => i.value === value);
|
|
1674
|
+
if (item?.hint === "installed") {
|
|
1675
|
+
nav.push({ screen: "installed-action", context: { pluginId: fullId } });
|
|
1676
|
+
} else {
|
|
1677
|
+
nav.push({ screen: "marketplace-install-scope", context: { marketplace, pluginId: fullId } });
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function handleInstallScopeSelect(value, pluginId, callbacks, nav) {
|
|
1681
|
+
const scope = value;
|
|
1682
|
+
callbacks.install(pluginId, scope).then(() => {
|
|
1683
|
+
nav.notify(`Installed plugin "${pluginId}" (${scope} scope).`);
|
|
1684
|
+
nav.popN(2);
|
|
1685
|
+
}).catch((err) => {
|
|
1686
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
function handleInstalledListSelect(value, callbacks, nav) {
|
|
1690
|
+
nav.setConfirm({
|
|
1691
|
+
message: `Uninstall plugin "${value}"?`,
|
|
1692
|
+
onConfirm: () => {
|
|
1693
|
+
nav.setConfirm(void 0);
|
|
1694
|
+
callbacks.uninstall(value).then(() => {
|
|
1695
|
+
nav.notify(`Uninstalled plugin "${value}".`);
|
|
1696
|
+
nav.refresh();
|
|
1697
|
+
}).catch((err) => {
|
|
1698
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1699
|
+
});
|
|
1700
|
+
},
|
|
1701
|
+
onCancel: () => nav.setConfirm(void 0)
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
function handleInstalledActionSelect(value, pluginId, callbacks, nav) {
|
|
1705
|
+
if (value === "uninstall") {
|
|
1706
|
+
nav.setConfirm({
|
|
1707
|
+
message: `Uninstall plugin "${pluginId}"?`,
|
|
1708
|
+
onConfirm: () => {
|
|
1709
|
+
nav.setConfirm(void 0);
|
|
1710
|
+
callbacks.uninstall(pluginId).then(() => {
|
|
1711
|
+
nav.notify(`Uninstalled plugin "${pluginId}".`);
|
|
1712
|
+
nav.popN(2);
|
|
1713
|
+
}).catch((err) => {
|
|
1714
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1715
|
+
});
|
|
1716
|
+
},
|
|
1717
|
+
onCancel: () => nav.setConfirm(void 0)
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// src/ui/PluginTUI.tsx
|
|
1723
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
1724
|
+
function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
1725
|
+
const [stack, setStack] = useState8([{ screen: "main" }]);
|
|
1726
|
+
const [items, setItems] = useState8([]);
|
|
1727
|
+
const [loading, setLoading] = useState8(false);
|
|
1728
|
+
const [error, setError] = useState8();
|
|
1729
|
+
const [confirm, setConfirm] = useState8();
|
|
1730
|
+
const [refreshCounter, setRefreshCounter] = useState8(0);
|
|
1731
|
+
const current = stack[stack.length - 1] ?? { screen: "main" };
|
|
1732
|
+
const push = useCallback6((state) => {
|
|
1733
|
+
setStack((prev) => [...prev, state]);
|
|
1734
|
+
setItems([]);
|
|
1735
|
+
setError(void 0);
|
|
1736
|
+
}, []);
|
|
1737
|
+
const pop = useCallback6(() => {
|
|
1738
|
+
setStack((prev) => {
|
|
1739
|
+
if (prev.length <= 1) {
|
|
1740
|
+
onClose();
|
|
1741
|
+
return prev;
|
|
1742
|
+
}
|
|
1743
|
+
return prev.slice(0, -1);
|
|
1744
|
+
});
|
|
1745
|
+
setItems([]);
|
|
1746
|
+
setError(void 0);
|
|
1747
|
+
}, [onClose]);
|
|
1748
|
+
const popN = useCallback6(
|
|
1749
|
+
(n) => {
|
|
1750
|
+
setStack((prev) => {
|
|
1751
|
+
const next = prev.slice(0, Math.max(1, prev.length - n));
|
|
1752
|
+
if (next.length === 0) {
|
|
1753
|
+
onClose();
|
|
1754
|
+
return prev;
|
|
1755
|
+
}
|
|
1756
|
+
return next;
|
|
1757
|
+
});
|
|
1758
|
+
setItems([]);
|
|
1759
|
+
setError(void 0);
|
|
1760
|
+
},
|
|
1761
|
+
[onClose]
|
|
1762
|
+
);
|
|
1763
|
+
const notify = useCallback6(
|
|
1764
|
+
(content) => {
|
|
1765
|
+
addMessage?.({ role: "system", content });
|
|
1766
|
+
},
|
|
1767
|
+
[addMessage]
|
|
1768
|
+
);
|
|
1769
|
+
const refresh = useCallback6(() => {
|
|
1770
|
+
setItems([]);
|
|
1771
|
+
setRefreshCounter((c) => c + 1);
|
|
1772
|
+
}, []);
|
|
1773
|
+
const nav = { push, pop, popN, notify, setConfirm, refresh };
|
|
1774
|
+
useEffect3(() => {
|
|
1775
|
+
const screen2 = current.screen;
|
|
1776
|
+
if (screen2 === "marketplace-list") {
|
|
1777
|
+
setLoading(true);
|
|
1778
|
+
callbacks.marketplaceList().then((sources) => {
|
|
1779
|
+
const baseItems = [{ label: "Add Marketplace", value: "__add__" }];
|
|
1780
|
+
const sourceItems = sources.map((s) => ({
|
|
1781
|
+
label: s.name,
|
|
1782
|
+
value: s.name,
|
|
1783
|
+
hint: s.type
|
|
1784
|
+
}));
|
|
1785
|
+
setItems([...baseItems, ...sourceItems]);
|
|
1786
|
+
setLoading(false);
|
|
1787
|
+
}).catch((err) => {
|
|
1788
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1789
|
+
setLoading(false);
|
|
1790
|
+
});
|
|
1791
|
+
} else if (screen2 === "marketplace-browse") {
|
|
1792
|
+
const marketplace = current.context?.marketplace ?? "";
|
|
1793
|
+
setLoading(true);
|
|
1794
|
+
callbacks.listAvailablePlugins(marketplace).then((plugins) => {
|
|
1795
|
+
setItems(
|
|
1796
|
+
plugins.map((p) => ({
|
|
1797
|
+
label: p.name,
|
|
1798
|
+
value: p.name,
|
|
1799
|
+
hint: p.installed ? "installed" : p.description
|
|
1800
|
+
}))
|
|
1801
|
+
);
|
|
1802
|
+
setLoading(false);
|
|
1803
|
+
}).catch((err) => {
|
|
1804
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1805
|
+
setLoading(false);
|
|
1806
|
+
});
|
|
1807
|
+
} else if (screen2 === "installed-list") {
|
|
1808
|
+
setLoading(true);
|
|
1809
|
+
callbacks.listInstalled().then((plugins) => {
|
|
1810
|
+
setItems(
|
|
1811
|
+
plugins.map((p) => ({
|
|
1812
|
+
label: p.name,
|
|
1813
|
+
value: p.name,
|
|
1814
|
+
hint: p.description
|
|
1815
|
+
}))
|
|
1816
|
+
);
|
|
1817
|
+
setLoading(false);
|
|
1818
|
+
}).catch((err) => {
|
|
1819
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1820
|
+
setLoading(false);
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
}, [stack.length, current.screen, current.context?.marketplace, callbacks, refreshCounter]);
|
|
1824
|
+
const handleSelect = useCallback6(
|
|
1825
|
+
(value) => {
|
|
1826
|
+
const screen2 = current.screen;
|
|
1827
|
+
const ctx = current.context;
|
|
1828
|
+
if (screen2 === "main") handleMainSelect(value, nav);
|
|
1829
|
+
else if (screen2 === "marketplace-list") handleMarketplaceListSelect(value, nav);
|
|
1830
|
+
else if (screen2 === "marketplace-action")
|
|
1831
|
+
handleMarketplaceActionSelect(value, ctx?.marketplace ?? "", callbacks, nav);
|
|
1832
|
+
else if (screen2 === "marketplace-browse")
|
|
1833
|
+
handleMarketplaceBrowseSelect(value, ctx?.marketplace ?? "", items, nav);
|
|
1834
|
+
else if (screen2 === "marketplace-install-scope")
|
|
1835
|
+
handleInstallScopeSelect(value, ctx?.pluginId ?? "", callbacks, nav);
|
|
1836
|
+
else if (screen2 === "installed-list") handleInstalledListSelect(value, callbacks, nav);
|
|
1837
|
+
else if (screen2 === "installed-action")
|
|
1838
|
+
handleInstalledActionSelect(value, ctx?.pluginId ?? "", callbacks, nav);
|
|
1839
|
+
},
|
|
1840
|
+
[current, items, callbacks, push, pop, popN, notify, setConfirm, refresh]
|
|
1841
|
+
);
|
|
1842
|
+
const handleTextSubmit = useCallback6(
|
|
1843
|
+
(value) => {
|
|
1844
|
+
if (current.screen === "marketplace-add") {
|
|
1845
|
+
callbacks.marketplaceAdd(value).then((name) => {
|
|
1846
|
+
notify(`Added marketplace "${name}" from ${value}.`);
|
|
1847
|
+
pop();
|
|
1848
|
+
}).catch((err) => {
|
|
1849
|
+
notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1850
|
+
pop();
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
},
|
|
1854
|
+
[current.screen, callbacks, notify, pop]
|
|
1855
|
+
);
|
|
1856
|
+
if (confirm) {
|
|
1857
|
+
return /* @__PURE__ */ jsx12(
|
|
1858
|
+
ConfirmPrompt,
|
|
1859
|
+
{
|
|
1860
|
+
message: confirm.message,
|
|
1861
|
+
onSelect: (index) => {
|
|
1862
|
+
if (index === 0) confirm.onConfirm();
|
|
1863
|
+
else confirm.onCancel();
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
const screen = current.screen;
|
|
1869
|
+
if (screen === "marketplace-add") {
|
|
1870
|
+
return /* @__PURE__ */ jsx12(
|
|
1871
|
+
TextPrompt,
|
|
1872
|
+
{
|
|
1873
|
+
title: "Add Marketplace Source",
|
|
1874
|
+
placeholder: "owner/repo or git URL",
|
|
1875
|
+
onSubmit: handleTextSubmit,
|
|
1876
|
+
onCancel: pop,
|
|
1877
|
+
validate: (v) => !v.includes("/") ? "Must be owner/repo or a git URL" : void 0
|
|
1878
|
+
}
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
if (screen === "marketplace-action") {
|
|
1882
|
+
return /* @__PURE__ */ jsx12(
|
|
1883
|
+
MenuSelect,
|
|
1884
|
+
{
|
|
1885
|
+
title: `Marketplace: ${current.context?.marketplace ?? ""}`,
|
|
1886
|
+
items: [
|
|
1887
|
+
{ label: "Browse plugins", value: "browse" },
|
|
1888
|
+
{ label: "Update", value: "update" },
|
|
1889
|
+
{ label: "Remove", value: "remove" }
|
|
1890
|
+
],
|
|
1891
|
+
onSelect: handleSelect,
|
|
1892
|
+
onBack: pop
|
|
1893
|
+
},
|
|
1894
|
+
stack.length
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
if (screen === "marketplace-install-scope") {
|
|
1898
|
+
return /* @__PURE__ */ jsx12(
|
|
1899
|
+
MenuSelect,
|
|
1900
|
+
{
|
|
1901
|
+
title: `Install scope for "${current.context?.pluginId ?? ""}"`,
|
|
1902
|
+
items: [
|
|
1903
|
+
{ label: "User scope", value: "user" },
|
|
1904
|
+
{ label: "Project scope", value: "project" }
|
|
1905
|
+
],
|
|
1906
|
+
onSelect: handleSelect,
|
|
1907
|
+
onBack: pop
|
|
1908
|
+
},
|
|
1909
|
+
stack.length
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
if (screen === "installed-action") {
|
|
1913
|
+
return /* @__PURE__ */ jsx12(
|
|
1914
|
+
MenuSelect,
|
|
1915
|
+
{
|
|
1916
|
+
title: `Plugin: ${current.context?.pluginId ?? ""}`,
|
|
1917
|
+
items: [{ label: "Uninstall", value: "uninstall" }],
|
|
1918
|
+
onSelect: handleSelect,
|
|
1919
|
+
onBack: pop
|
|
1920
|
+
},
|
|
1921
|
+
stack.length
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
const titleMap = {
|
|
1925
|
+
main: "Plugin Management",
|
|
1926
|
+
"marketplace-list": "Marketplace",
|
|
1927
|
+
"marketplace-browse": `Browse: ${current.context?.marketplace ?? ""}`,
|
|
1928
|
+
"installed-list": "Installed Plugins"
|
|
1929
|
+
};
|
|
1930
|
+
const staticItemsMap = {
|
|
1931
|
+
main: [
|
|
1932
|
+
{ label: "Marketplace", value: "marketplace" },
|
|
1933
|
+
{ label: "Installed Plugins", value: "installed" }
|
|
1934
|
+
]
|
|
1935
|
+
};
|
|
1936
|
+
return /* @__PURE__ */ jsx12(
|
|
1937
|
+
MenuSelect,
|
|
1938
|
+
{
|
|
1939
|
+
title: titleMap[screen] ?? "Plugin Management",
|
|
1940
|
+
items: staticItemsMap[screen] ?? items,
|
|
1941
|
+
onSelect: handleSelect,
|
|
1942
|
+
onBack: pop,
|
|
1943
|
+
loading,
|
|
1944
|
+
error
|
|
1945
|
+
},
|
|
1946
|
+
`${screen}-${stack.length}-${refreshCounter}`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/ui/App.tsx
|
|
1951
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1952
|
+
var EXIT_DELAY_MS = 500;
|
|
1953
|
+
function App(props) {
|
|
1954
|
+
const { exit } = useApp();
|
|
1955
|
+
const cwd = props.cwd ?? process.cwd();
|
|
1956
|
+
const {
|
|
1957
|
+
interactiveSession,
|
|
1958
|
+
registry,
|
|
1959
|
+
messages,
|
|
1960
|
+
addMessage,
|
|
1961
|
+
streamingText,
|
|
1962
|
+
activeTools,
|
|
1963
|
+
isThinking,
|
|
1964
|
+
isAborting,
|
|
1965
|
+
pendingPrompt,
|
|
1966
|
+
permissionRequest,
|
|
1967
|
+
contextState,
|
|
1968
|
+
handleSubmit: baseHandleSubmit,
|
|
1969
|
+
handleAbort,
|
|
1970
|
+
handleCancelQueue
|
|
1971
|
+
} = useInteractiveSession({
|
|
1972
|
+
config: props.config,
|
|
1973
|
+
context: props.context,
|
|
1974
|
+
projectInfo: props.projectInfo,
|
|
1975
|
+
sessionStore: props.sessionStore,
|
|
1976
|
+
permissionMode: props.permissionMode,
|
|
1977
|
+
maxTurns: props.maxTurns,
|
|
1978
|
+
cwd
|
|
1979
|
+
});
|
|
1980
|
+
const pluginCallbacks = usePluginCallbacks(cwd);
|
|
1981
|
+
const [pendingModelId, setPendingModelId] = useState9(null);
|
|
1982
|
+
const pendingModelChangeRef = useRef7(null);
|
|
1983
|
+
const [showPluginTUI, setShowPluginTUI] = useState9(false);
|
|
1984
|
+
const handleSubmit = async (input) => {
|
|
1985
|
+
await baseHandleSubmit(input);
|
|
1986
|
+
const sideEffects = interactiveSession;
|
|
1987
|
+
if (sideEffects._pendingModelId) {
|
|
1988
|
+
const modelId = sideEffects._pendingModelId;
|
|
1989
|
+
delete sideEffects._pendingModelId;
|
|
1990
|
+
pendingModelChangeRef.current = modelId;
|
|
1991
|
+
setPendingModelId(modelId);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (sideEffects._pendingLanguage) {
|
|
1995
|
+
const lang = sideEffects._pendingLanguage;
|
|
1996
|
+
delete sideEffects._pendingLanguage;
|
|
1997
|
+
const settingsPath = getUserSettingsPath();
|
|
1998
|
+
const settings = readSettings(settingsPath);
|
|
1999
|
+
settings.language = lang;
|
|
2000
|
+
writeSettings(settingsPath, settings);
|
|
2001
|
+
addMessage(createSystemMessage2(`Language set to "${lang}". Restarting...`));
|
|
2002
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
if (sideEffects._resetRequested) {
|
|
2006
|
+
delete sideEffects._resetRequested;
|
|
2007
|
+
const settingsPath = getUserSettingsPath();
|
|
2008
|
+
if (deleteSettings(settingsPath)) {
|
|
2009
|
+
addMessage(createSystemMessage2(`Deleted ${settingsPath}. Exiting...`));
|
|
2010
|
+
} else {
|
|
2011
|
+
addMessage(createSystemMessage2("No user settings found."));
|
|
2012
|
+
}
|
|
2013
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
if (sideEffects._exitRequested) {
|
|
2017
|
+
delete sideEffects._exitRequested;
|
|
2018
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
if (sideEffects._triggerPluginTUI) {
|
|
2022
|
+
delete sideEffects._triggerPluginTUI;
|
|
2023
|
+
setShowPluginTUI(true);
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
useInput7(
|
|
2028
|
+
(_input, key) => {
|
|
2029
|
+
if (key.escape && isThinking) {
|
|
2030
|
+
handleAbort();
|
|
2031
|
+
}
|
|
2032
|
+
},
|
|
2033
|
+
{ isActive: !permissionRequest && !showPluginTUI }
|
|
2034
|
+
);
|
|
2035
|
+
const session = interactiveSession.getSession();
|
|
2036
|
+
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
|
|
2037
|
+
/* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
|
|
2038
|
+
/* @__PURE__ */ jsx13(Text13, { color: "cyan", bold: true, children: `
|
|
2039
|
+
____ ___ ____ ___ _____ _
|
|
2040
|
+
| _ \\ / _ \\| __ ) / _ \\_ _|/ \\
|
|
2041
|
+
| |_) | | | | _ \\| | | || | / _ \\
|
|
2042
|
+
| _ <| |_| | |_) | |_| || |/ ___ \\
|
|
2043
|
+
|_| \\_\\\\___/|____/ \\___/ |_/_/ \\_\\
|
|
2044
|
+
` }),
|
|
2045
|
+
/* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
|
|
2046
|
+
" v",
|
|
2047
|
+
props.version ?? "0.0.0"
|
|
2048
|
+
] })
|
|
2049
|
+
] }),
|
|
2050
|
+
/* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
2051
|
+
/* @__PURE__ */ jsx13(MessageList, { messages }),
|
|
2052
|
+
isThinking && /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx13(StreamingIndicator, { text: streamingText, activeTools }) })
|
|
2053
|
+
] }),
|
|
2054
|
+
permissionRequest && /* @__PURE__ */ jsx13(PermissionPrompt, { request: permissionRequest }),
|
|
2055
|
+
pendingModelId && /* @__PURE__ */ jsx13(
|
|
2056
|
+
ConfirmPrompt,
|
|
2057
|
+
{
|
|
2058
|
+
message: `Change model to ${getModelName(pendingModelId)}? This will restart the session.`,
|
|
2059
|
+
onSelect: (index) => {
|
|
2060
|
+
setPendingModelId(null);
|
|
2061
|
+
pendingModelChangeRef.current = null;
|
|
2062
|
+
if (index === 0) {
|
|
2063
|
+
try {
|
|
2064
|
+
const settingsPath = getUserSettingsPath();
|
|
2065
|
+
updateModelInSettings(settingsPath, pendingModelId);
|
|
2066
|
+
addMessage(
|
|
2067
|
+
createSystemMessage2(
|
|
2068
|
+
`Model changed to ${getModelName(pendingModelId)}. Restarting...`
|
|
2069
|
+
)
|
|
2070
|
+
);
|
|
2071
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
addMessage(
|
|
2074
|
+
createSystemMessage2(
|
|
2075
|
+
`Failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2076
|
+
)
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
} else {
|
|
2080
|
+
addMessage(createSystemMessage2("Model change cancelled."));
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
),
|
|
2085
|
+
showPluginTUI && /* @__PURE__ */ jsx13(
|
|
2086
|
+
PluginTUI,
|
|
2087
|
+
{
|
|
2088
|
+
callbacks: pluginCallbacks,
|
|
2089
|
+
onClose: () => setShowPluginTUI(false),
|
|
2090
|
+
addMessage: (msg) => addMessage(createSystemMessage2(msg.content))
|
|
2091
|
+
}
|
|
2092
|
+
),
|
|
2093
|
+
/* @__PURE__ */ jsx13(
|
|
2094
|
+
StatusBar,
|
|
2095
|
+
{
|
|
2096
|
+
permissionMode: session.getPermissionMode(),
|
|
2097
|
+
modelName: getModelName(props.config.provider.model),
|
|
2098
|
+
sessionId: session.getSessionId(),
|
|
2099
|
+
messageCount: messages.length,
|
|
2100
|
+
isThinking,
|
|
2101
|
+
contextPercentage: contextState.percentage,
|
|
2102
|
+
contextUsedTokens: contextState.usedTokens,
|
|
2103
|
+
contextMaxTokens: contextState.maxTokens
|
|
2104
|
+
}
|
|
2105
|
+
),
|
|
2106
|
+
/* @__PURE__ */ jsx13(
|
|
2107
|
+
InputArea,
|
|
2108
|
+
{
|
|
2109
|
+
onSubmit: handleSubmit,
|
|
2110
|
+
onCancelQueue: handleCancelQueue,
|
|
2111
|
+
isDisabled: !!permissionRequest || showPluginTUI || isThinking && !!pendingPrompt,
|
|
2112
|
+
isAborting,
|
|
2113
|
+
pendingPrompt,
|
|
2114
|
+
registry
|
|
2115
|
+
}
|
|
2116
|
+
),
|
|
2117
|
+
/* @__PURE__ */ jsx13(Text13, { children: " " })
|
|
2118
|
+
] });
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// src/ui/render.tsx
|
|
2122
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
2123
|
+
function renderApp(options) {
|
|
2124
|
+
process.on("unhandledRejection", (reason) => {
|
|
2125
|
+
process.stderr.write(`
|
|
2126
|
+
[UNHANDLED REJECTION] ${reason}
|
|
2127
|
+
`);
|
|
2128
|
+
if (reason instanceof Error) {
|
|
2129
|
+
process.stderr.write(`${reason.stack}
|
|
2130
|
+
`);
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
2134
|
+
process.stdout.write("\x1B[?2004h");
|
|
2135
|
+
}
|
|
2136
|
+
const instance = render(/* @__PURE__ */ jsx14(App, { ...options }), {
|
|
2137
|
+
exitOnCtrlC: true
|
|
2138
|
+
});
|
|
2139
|
+
instance.waitUntilExit().then(() => {
|
|
2140
|
+
if (process.stdout.isTTY) {
|
|
2141
|
+
process.stdout.write("\x1B[?2004l");
|
|
2142
|
+
}
|
|
2143
|
+
process.exit(0);
|
|
2144
|
+
}).catch((err) => {
|
|
2145
|
+
if (err) {
|
|
2146
|
+
process.stderr.write(`
|
|
2147
|
+
[EXIT ERROR] ${err}
|
|
2148
|
+
`);
|
|
2149
|
+
}
|
|
2150
|
+
process.exit(1);
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// src/cli.ts
|
|
2155
|
+
function checkSettingsFile(filePath) {
|
|
2156
|
+
if (!existsSync2(filePath)) return "missing";
|
|
2157
|
+
try {
|
|
2158
|
+
const raw = readFileSync2(filePath, "utf8").trim();
|
|
2159
|
+
if (raw.length === 0) return "incomplete";
|
|
2160
|
+
const parsed = JSON.parse(raw);
|
|
2161
|
+
const provider = parsed.provider;
|
|
2162
|
+
if (!provider?.apiKey) return "incomplete";
|
|
2163
|
+
return "valid";
|
|
2164
|
+
} catch {
|
|
2165
|
+
return "corrupt";
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
function readVersion() {
|
|
2169
|
+
try {
|
|
2170
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
2171
|
+
const dir = dirname3(thisFile);
|
|
2172
|
+
const candidates = [join5(dir, "..", "..", "package.json"), join5(dir, "..", "package.json")];
|
|
2173
|
+
for (const pkgPath of candidates) {
|
|
2174
|
+
try {
|
|
2175
|
+
const raw = readFileSync2(pkgPath, "utf-8");
|
|
2176
|
+
const pkg = JSON.parse(raw);
|
|
2177
|
+
if (pkg.version !== void 0 && pkg.name !== void 0) {
|
|
2178
|
+
return pkg.version;
|
|
2179
|
+
}
|
|
2180
|
+
} catch {
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return "0.0.0";
|
|
2184
|
+
} catch {
|
|
2185
|
+
return "0.0.0";
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
function promptInput(label, masked = false) {
|
|
2189
|
+
return new Promise((resolve) => {
|
|
2190
|
+
process.stdout.write(label);
|
|
2191
|
+
let input = "";
|
|
2192
|
+
const stdin = process.stdin;
|
|
2193
|
+
const wasRaw = stdin.isRaw;
|
|
2194
|
+
stdin.setRawMode(true);
|
|
2195
|
+
stdin.resume();
|
|
2196
|
+
stdin.setEncoding("utf8");
|
|
2197
|
+
const onData = (data) => {
|
|
2198
|
+
for (const ch of data) {
|
|
2199
|
+
if (ch === "\r" || ch === "\n") {
|
|
2200
|
+
stdin.removeListener("data", onData);
|
|
2201
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
2202
|
+
stdin.pause();
|
|
2203
|
+
process.stdout.write("\n");
|
|
2204
|
+
resolve(input.trim());
|
|
2205
|
+
return;
|
|
2206
|
+
} else if (ch === "\x7F" || ch === "\b") {
|
|
2207
|
+
if (input.length > 0) {
|
|
2208
|
+
input = input.slice(0, -1);
|
|
2209
|
+
process.stdout.write("\b \b");
|
|
2210
|
+
}
|
|
2211
|
+
} else if (ch === "") {
|
|
2212
|
+
process.stdout.write("\n");
|
|
2213
|
+
process.exit(0);
|
|
2214
|
+
} else if (ch.charCodeAt(0) >= 32) {
|
|
2215
|
+
input += ch;
|
|
2216
|
+
process.stdout.write(masked ? "*" : ch);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
stdin.on("data", onData);
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
async function ensureConfig(cwd) {
|
|
2224
|
+
const userPath = getUserSettingsPath();
|
|
2225
|
+
const projectPath = join5(cwd, ".robota", "settings.json");
|
|
2226
|
+
const localPath = join5(cwd, ".robota", "settings.local.json");
|
|
2227
|
+
const paths = [userPath, projectPath, localPath];
|
|
2228
|
+
const checks = paths.map((p) => ({ path: p, status: checkSettingsFile(p) }));
|
|
2229
|
+
if (checks.some((c) => c.status === "valid")) {
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const corrupt = checks.filter((c) => c.status === "corrupt");
|
|
2233
|
+
const incomplete = checks.filter((c) => c.status === "incomplete");
|
|
2234
|
+
process.stdout.write("\n");
|
|
2235
|
+
if (corrupt.length > 0) {
|
|
2236
|
+
for (const c of corrupt) {
|
|
2237
|
+
process.stderr.write(` ERROR: Settings file is corrupt (invalid JSON): ${c.path}
|
|
2238
|
+
`);
|
|
2239
|
+
}
|
|
2240
|
+
process.stdout.write("\n");
|
|
2241
|
+
}
|
|
2242
|
+
if (incomplete.length > 0) {
|
|
2243
|
+
for (const c of incomplete) {
|
|
2244
|
+
process.stderr.write(` WARNING: Settings file is missing provider.apiKey: ${c.path}
|
|
2245
|
+
`);
|
|
2246
|
+
}
|
|
2247
|
+
process.stdout.write("\n");
|
|
2248
|
+
}
|
|
2249
|
+
if (corrupt.length === 0 && incomplete.length === 0) {
|
|
2250
|
+
process.stdout.write(" Welcome to Robota CLI!\n");
|
|
2251
|
+
process.stdout.write(" No configuration found. Let's set up.\n");
|
|
2252
|
+
} else {
|
|
2253
|
+
process.stdout.write(" Reconfiguring...\n");
|
|
2254
|
+
}
|
|
2255
|
+
process.stdout.write("\n");
|
|
2256
|
+
const apiKey = await promptInput(" Anthropic API key: ", true);
|
|
2257
|
+
if (!apiKey) {
|
|
2258
|
+
process.stderr.write("\n No API key provided. Exiting.\n");
|
|
2259
|
+
process.exit(1);
|
|
2260
|
+
}
|
|
2261
|
+
const language = await promptInput(" Response language (ko/en/ja/zh, default: en): ");
|
|
2262
|
+
const settingsDir = dirname3(userPath);
|
|
2263
|
+
mkdirSync2(settingsDir, { recursive: true });
|
|
2264
|
+
const settings = {
|
|
2265
|
+
provider: {
|
|
2266
|
+
name: "anthropic",
|
|
2267
|
+
model: "claude-sonnet-4-6",
|
|
2268
|
+
apiKey
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
if (language) {
|
|
2272
|
+
settings.language = language;
|
|
2273
|
+
}
|
|
2274
|
+
writeFileSync2(userPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
2275
|
+
process.stdout.write(`
|
|
2276
|
+
Config saved to ${userPath}
|
|
2277
|
+
|
|
2278
|
+
`);
|
|
2279
|
+
}
|
|
2280
|
+
function resetConfig() {
|
|
2281
|
+
const userPath = getUserSettingsPath();
|
|
2282
|
+
if (deleteSettings(userPath)) {
|
|
2283
|
+
process.stdout.write(`Deleted ${userPath}
|
|
2284
|
+
`);
|
|
2285
|
+
} else {
|
|
2286
|
+
process.stdout.write("No user settings found.\n");
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
async function startCli() {
|
|
2290
|
+
const args = parseCliArgs();
|
|
2291
|
+
if (args.version) {
|
|
2292
|
+
process.stdout.write(`robota ${readVersion()}
|
|
2293
|
+
`);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
if (args.reset) {
|
|
2297
|
+
resetConfig();
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
const cwd = process.cwd();
|
|
2301
|
+
await ensureConfig(cwd);
|
|
2302
|
+
const [config, context, projectInfo] = await Promise.all([
|
|
2303
|
+
loadConfig(cwd),
|
|
2304
|
+
loadContext(cwd),
|
|
2305
|
+
detectProject(cwd)
|
|
2306
|
+
]);
|
|
2307
|
+
if (args.model !== void 0) {
|
|
2308
|
+
config.provider.model = args.model;
|
|
2309
|
+
}
|
|
2310
|
+
if (args.language !== void 0) {
|
|
2311
|
+
config.language = args.language;
|
|
2312
|
+
}
|
|
2313
|
+
const sessionStore = new SessionStore();
|
|
2314
|
+
if (args.printMode) {
|
|
2315
|
+
const prompt = args.positional.join(" ").trim();
|
|
2316
|
+
if (prompt.length === 0) {
|
|
2317
|
+
process.stderr.write("Print mode (-p) requires a prompt argument.\n");
|
|
2318
|
+
process.exit(1);
|
|
2319
|
+
}
|
|
2320
|
+
const terminal = new PrintTerminal();
|
|
2321
|
+
const paths = projectPaths(cwd);
|
|
2322
|
+
const session = createSession({
|
|
2323
|
+
config,
|
|
2324
|
+
context,
|
|
2325
|
+
terminal,
|
|
2326
|
+
sessionLogger: new FileSessionLogger(paths.logs),
|
|
2327
|
+
projectInfo,
|
|
2328
|
+
permissionMode: args.permissionMode,
|
|
2329
|
+
promptForApproval
|
|
2330
|
+
});
|
|
2331
|
+
const response = await session.run(prompt);
|
|
2332
|
+
process.stdout.write(response + "\n");
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
renderApp({
|
|
2336
|
+
config,
|
|
2337
|
+
context,
|
|
2338
|
+
projectInfo,
|
|
2339
|
+
sessionStore,
|
|
2340
|
+
permissionMode: args.permissionMode,
|
|
2341
|
+
maxTurns: args.maxTurns,
|
|
2342
|
+
version: readVersion()
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
export {
|
|
2347
|
+
startCli
|
|
2348
|
+
};
|