@robota-sdk/agent-cli 3.0.0-beta.40 → 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 +129 -113
- package/dist/node/bin.js +1 -1
- package/dist/node/{chunk-CRPNSO52.js → chunk-KX3JUGSB.js} +459 -1274
- package/dist/node/index.cjs +510 -1323
- package/dist/node/index.js +1 -1
- package/package.json +4 -4
- package/dist/node/bin.cjs +0 -3180
- package/dist/node/bin.d.cts +0 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
|
-
import { readFileSync as
|
|
2
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3
3
|
import { join as join5, dirname as dirname3 } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import {
|
|
6
6
|
loadConfig,
|
|
7
7
|
loadContext,
|
|
8
8
|
detectProject,
|
|
9
|
-
createSession
|
|
9
|
+
createSession,
|
|
10
10
|
SessionStore,
|
|
11
|
-
FileSessionLogger
|
|
12
|
-
projectPaths
|
|
11
|
+
FileSessionLogger,
|
|
12
|
+
projectPaths
|
|
13
13
|
} from "@robota-sdk/agent-sdk";
|
|
14
14
|
import { promptForApproval } from "@robota-sdk/agent-sdk";
|
|
15
15
|
|
|
@@ -149,615 +149,57 @@ var PrintTerminal = class {
|
|
|
149
149
|
import { render } from "ink";
|
|
150
150
|
|
|
151
151
|
// src/ui/App.tsx
|
|
152
|
-
import { useState as
|
|
152
|
+
import { useState as useState9, useRef as useRef7 } from "react";
|
|
153
153
|
import { Box as Box11, Text as Text13, useApp, useInput as useInput7 } from "ink";
|
|
154
|
-
import { getModelName } from "@robota-sdk/agent-core";
|
|
155
|
-
import { createSystemMessage as createSystemMessage3 } from "@robota-sdk/agent-core";
|
|
154
|
+
import { getModelName, createSystemMessage as createSystemMessage2 } from "@robota-sdk/agent-core";
|
|
156
155
|
|
|
157
|
-
// src/ui/hooks/
|
|
158
|
-
import { useState, useCallback,
|
|
159
|
-
import {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
for (let i = 0; i < oldLines.length; i++) {
|
|
170
|
-
lines.push({ type: "remove", text: oldLines[i], lineNumber: startLine + i });
|
|
171
|
-
}
|
|
172
|
-
for (let i = 0; i < newLines.length; i++) {
|
|
173
|
-
lines.push({ type: "add", text: newLines[i], lineNumber: startLine + i });
|
|
174
|
-
}
|
|
175
|
-
return lines;
|
|
176
|
-
}
|
|
177
|
-
function generateDiffLinesWithContext(oldStr, newStr, startLine, filePath) {
|
|
178
|
-
if (oldStr === newStr) return [];
|
|
179
|
-
const diffLines = generateDiffLines(oldStr, newStr, startLine);
|
|
180
|
-
let fileLines;
|
|
181
|
-
try {
|
|
182
|
-
fileLines = readFileSync2(filePath, "utf-8").split("\n");
|
|
183
|
-
} catch {
|
|
184
|
-
return diffLines;
|
|
185
|
-
}
|
|
186
|
-
const result = [];
|
|
187
|
-
const contextStart = Math.max(0, startLine - 1 - CONTEXT_LINES);
|
|
188
|
-
for (let i = contextStart; i < startLine - 1; i++) {
|
|
189
|
-
if (i < fileLines.length) {
|
|
190
|
-
result.push({ type: "context", text: fileLines[i], lineNumber: i + 1 });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
result.push(...diffLines);
|
|
194
|
-
const newLineCount = newStr.split("\n").length;
|
|
195
|
-
const afterStart = startLine - 1 + newLineCount;
|
|
196
|
-
for (let i = afterStart; i < afterStart + CONTEXT_LINES; i++) {
|
|
197
|
-
if (i < fileLines.length) {
|
|
198
|
-
result.push({ type: "context", text: fileLines[i], lineNumber: i + 1 });
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return result;
|
|
202
|
-
}
|
|
203
|
-
function extractEditDiff(toolName, toolArgs, startLine) {
|
|
204
|
-
if (toolName !== "Edit" || !toolArgs) return null;
|
|
205
|
-
const filePath = toolArgs.file_path ?? toolArgs.filePath;
|
|
206
|
-
const oldStr = toolArgs.old_string ?? toolArgs.oldString;
|
|
207
|
-
const newStr = toolArgs.new_string ?? toolArgs.newString;
|
|
208
|
-
if (typeof filePath !== "string") return null;
|
|
209
|
-
if (typeof oldStr !== "string" || typeof newStr !== "string") return null;
|
|
210
|
-
let sl = startLine ?? 0;
|
|
211
|
-
if (!sl) {
|
|
212
|
-
try {
|
|
213
|
-
const fileContent = readFileSync2(filePath, "utf-8");
|
|
214
|
-
const idx = fileContent.indexOf(newStr);
|
|
215
|
-
if (idx >= 0) {
|
|
216
|
-
sl = fileContent.substring(0, idx).split("\n").length;
|
|
217
|
-
} else {
|
|
218
|
-
sl = 1;
|
|
219
|
-
}
|
|
220
|
-
} catch {
|
|
221
|
-
sl = 1;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const lines = generateDiffLinesWithContext(oldStr, newStr, sl, filePath);
|
|
225
|
-
if (lines.length === 0) return null;
|
|
226
|
-
return { file: filePath, lines };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// src/ui/hooks/useSession.ts
|
|
230
|
-
var TOOL_ARG_DISPLAY_MAX = 80;
|
|
231
|
-
var TAIL_KEEP = 30;
|
|
232
|
-
var MAX_COMPLETED_TOOLS = 50;
|
|
233
|
-
var NOOP_TERMINAL = {
|
|
234
|
-
write: () => {
|
|
235
|
-
},
|
|
236
|
-
writeLine: () => {
|
|
237
|
-
},
|
|
238
|
-
writeMarkdown: () => {
|
|
239
|
-
},
|
|
240
|
-
writeError: () => {
|
|
241
|
-
},
|
|
242
|
-
prompt: () => Promise.resolve(""),
|
|
243
|
-
select: () => Promise.resolve(0),
|
|
244
|
-
spinner: () => ({ stop: () => {
|
|
245
|
-
}, update: () => {
|
|
246
|
-
} })
|
|
247
|
-
};
|
|
248
|
-
function useSession(props) {
|
|
249
|
-
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
250
|
-
const [streamingText, setStreamingText] = useState("");
|
|
251
|
-
const streamingTextRef = useRef("");
|
|
252
|
-
const [activeTools, setActiveTools] = useState([]);
|
|
253
|
-
const permissionQueueRef = useRef([]);
|
|
254
|
-
const processingRef = useRef(false);
|
|
255
|
-
const processNextPermission = useCallback(() => {
|
|
256
|
-
if (processingRef.current) return;
|
|
257
|
-
const next = permissionQueueRef.current[0];
|
|
258
|
-
if (!next) {
|
|
259
|
-
setPermissionRequest(null);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
processingRef.current = true;
|
|
263
|
-
setPermissionRequest({
|
|
264
|
-
toolName: next.toolName,
|
|
265
|
-
toolArgs: next.toolArgs,
|
|
266
|
-
resolve: (result) => {
|
|
267
|
-
permissionQueueRef.current.shift();
|
|
268
|
-
processingRef.current = false;
|
|
269
|
-
setPermissionRequest(null);
|
|
270
|
-
next.resolve(result);
|
|
271
|
-
setTimeout(() => processNextPermission(), 0);
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
}, []);
|
|
275
|
-
const sessionRef = useRef(null);
|
|
276
|
-
if (sessionRef.current === null) {
|
|
277
|
-
const permissionHandler = (toolName, toolArgs) => {
|
|
278
|
-
return new Promise((resolve) => {
|
|
279
|
-
permissionQueueRef.current.push({ toolName, toolArgs, resolve });
|
|
280
|
-
processNextPermission();
|
|
281
|
-
});
|
|
282
|
-
};
|
|
283
|
-
let flushTimer = null;
|
|
284
|
-
const onTextDelta = (delta) => {
|
|
285
|
-
streamingTextRef.current += delta;
|
|
286
|
-
if (!flushTimer) {
|
|
287
|
-
flushTimer = setTimeout(() => {
|
|
288
|
-
setStreamingText(streamingTextRef.current);
|
|
289
|
-
flushTimer = null;
|
|
290
|
-
}, 16);
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
const onToolExecution = (event) => {
|
|
294
|
-
if (event.type === "start") {
|
|
295
|
-
let firstArg = "";
|
|
296
|
-
if (event.toolArgs) {
|
|
297
|
-
const firstVal = Object.values(event.toolArgs)[0];
|
|
298
|
-
const raw = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal ?? "");
|
|
299
|
-
firstArg = raw.length > TOOL_ARG_DISPLAY_MAX ? raw.slice(0, TOOL_ARG_DISPLAY_MAX - TAIL_KEEP - 3) + "..." + raw.slice(-TAIL_KEEP) : raw;
|
|
300
|
-
}
|
|
301
|
-
setActiveTools((prev) => [
|
|
302
|
-
...prev,
|
|
303
|
-
{ toolName: event.toolName, firstArg, isRunning: true, _toolArgs: event.toolArgs }
|
|
304
|
-
]);
|
|
305
|
-
} else {
|
|
306
|
-
const toolResult = event.denied ? "denied" : event.success === false ? "error" : "success";
|
|
307
|
-
setActiveTools((prev) => {
|
|
308
|
-
const updated = prev.map((t) => {
|
|
309
|
-
if (!(t.toolName === event.toolName && t.isRunning)) return t;
|
|
310
|
-
let startLine;
|
|
311
|
-
if (event.toolResultData && event.toolName === "Edit") {
|
|
312
|
-
try {
|
|
313
|
-
const parsed = JSON.parse(event.toolResultData);
|
|
314
|
-
if (typeof parsed.startLine === "number") {
|
|
315
|
-
startLine = parsed.startLine;
|
|
316
|
-
}
|
|
317
|
-
} catch {
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
const editDiff = extractEditDiff(
|
|
321
|
-
event.toolName,
|
|
322
|
-
t._toolArgs,
|
|
323
|
-
startLine
|
|
324
|
-
);
|
|
325
|
-
const finished = {
|
|
326
|
-
...t,
|
|
327
|
-
isRunning: false,
|
|
328
|
-
result: toolResult
|
|
329
|
-
};
|
|
330
|
-
if (editDiff) {
|
|
331
|
-
finished.diffLines = editDiff.lines;
|
|
332
|
-
finished.diffFile = editDiff.file;
|
|
333
|
-
}
|
|
334
|
-
delete finished._toolArgs;
|
|
335
|
-
return finished;
|
|
336
|
-
});
|
|
337
|
-
const completed = updated.filter((t) => !t.isRunning);
|
|
338
|
-
if (completed.length > MAX_COMPLETED_TOOLS) {
|
|
339
|
-
const excess = completed.length - MAX_COMPLETED_TOOLS;
|
|
340
|
-
let removed = 0;
|
|
341
|
-
return updated.filter((t) => {
|
|
342
|
-
if (!t.isRunning && removed < excess) {
|
|
343
|
-
removed++;
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
return true;
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
return updated;
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
};
|
|
353
|
-
const paths = projectPaths(props.cwd ?? process.cwd());
|
|
354
|
-
sessionRef.current = createSession({
|
|
355
|
-
config: props.config,
|
|
356
|
-
context: props.context,
|
|
357
|
-
terminal: NOOP_TERMINAL,
|
|
358
|
-
sessionLogger: new FileSessionLogger(paths.logs),
|
|
359
|
-
projectInfo: props.projectInfo,
|
|
360
|
-
sessionStore: props.sessionStore,
|
|
361
|
-
permissionMode: props.permissionMode,
|
|
362
|
-
maxTurns: props.maxTurns,
|
|
363
|
-
permissionHandler,
|
|
364
|
-
onTextDelta,
|
|
365
|
-
onToolExecution
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
const clearStreamingText = useCallback(() => {
|
|
369
|
-
setStreamingText("");
|
|
370
|
-
streamingTextRef.current = "";
|
|
371
|
-
setActiveTools([]);
|
|
372
|
-
}, []);
|
|
373
|
-
return {
|
|
374
|
-
session: sessionRef.current,
|
|
375
|
-
permissionRequest,
|
|
376
|
-
streamingText,
|
|
377
|
-
clearStreamingText,
|
|
378
|
-
activeTools
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// src/ui/hooks/useMessages.ts
|
|
383
|
-
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
384
|
-
var MAX_RENDERED_MESSAGES = 100;
|
|
385
|
-
function useMessages() {
|
|
386
|
-
const [messages, setMessages] = useState2([]);
|
|
387
|
-
const addMessage = useCallback2((msg) => {
|
|
388
|
-
setMessages((prev) => {
|
|
389
|
-
const updated = [...prev, msg];
|
|
390
|
-
if (updated.length > MAX_RENDERED_MESSAGES) {
|
|
391
|
-
return updated.slice(-MAX_RENDERED_MESSAGES);
|
|
392
|
-
}
|
|
393
|
-
return updated;
|
|
394
|
-
});
|
|
395
|
-
}, []);
|
|
396
|
-
return { messages, setMessages, addMessage };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// src/ui/hooks/useSlashCommands.ts
|
|
400
|
-
import { useCallback as useCallback3 } from "react";
|
|
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";
|
|
401
168
|
import { createSystemMessage } from "@robota-sdk/agent-core";
|
|
402
169
|
|
|
403
|
-
// src/commands/
|
|
404
|
-
var
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
" /compact [instr] \u2014 Compact context (optional focus instructions)",
|
|
410
|
-
" /mode [m] \u2014 Show/change permission mode",
|
|
411
|
-
" /language [lang] \u2014 Set response language (ko, en, ja, zh)",
|
|
412
|
-
" /cost \u2014 Show session info",
|
|
413
|
-
" /reset \u2014 Delete settings and exit",
|
|
414
|
-
" /exit \u2014 Exit CLI"
|
|
415
|
-
].join("\n");
|
|
416
|
-
function handleHelp(addMessage) {
|
|
417
|
-
addMessage({ role: "system", content: HELP_TEXT });
|
|
418
|
-
return { handled: true };
|
|
419
|
-
}
|
|
420
|
-
function handleClear(addMessage, clearMessages, session) {
|
|
421
|
-
clearMessages();
|
|
422
|
-
session.clearHistory();
|
|
423
|
-
addMessage({ role: "system", content: "Conversation cleared." });
|
|
424
|
-
return { handled: true };
|
|
425
|
-
}
|
|
426
|
-
async function handleCompact(args, session, addMessage) {
|
|
427
|
-
const instructions = args.trim() || void 0;
|
|
428
|
-
const before = session.getContextState().usedPercentage;
|
|
429
|
-
addMessage({ role: "system", content: "Compacting context..." });
|
|
430
|
-
await session.compact(instructions);
|
|
431
|
-
const after = session.getContextState().usedPercentage;
|
|
432
|
-
addMessage({
|
|
433
|
-
role: "system",
|
|
434
|
-
content: `Context compacted: ${Math.round(before)}% -> ${Math.round(after)}%`
|
|
435
|
-
});
|
|
436
|
-
return { handled: true };
|
|
437
|
-
}
|
|
438
|
-
function handleMode(arg, session, addMessage) {
|
|
439
|
-
if (!arg) {
|
|
440
|
-
addMessage({ role: "system", content: `Current mode: ${session.getPermissionMode()}` });
|
|
441
|
-
} else if (VALID_MODES2.includes(arg)) {
|
|
442
|
-
session.setPermissionMode(arg);
|
|
443
|
-
addMessage({ role: "system", content: `Permission mode set to: ${arg}` });
|
|
444
|
-
} else {
|
|
445
|
-
addMessage({ role: "system", content: `Invalid mode. Valid: ${VALID_MODES2.join(" | ")}` });
|
|
446
|
-
}
|
|
447
|
-
return { handled: true };
|
|
448
|
-
}
|
|
449
|
-
function handleModel(modelId, addMessage) {
|
|
450
|
-
if (!modelId) {
|
|
451
|
-
addMessage({ role: "system", content: "Select a model from the /model submenu." });
|
|
452
|
-
return { handled: true };
|
|
453
|
-
}
|
|
454
|
-
return { handled: true, pendingModelId: modelId };
|
|
455
|
-
}
|
|
456
|
-
function handleCost(session, addMessage) {
|
|
457
|
-
addMessage({
|
|
458
|
-
role: "system",
|
|
459
|
-
content: `Session: ${session.getSessionId()}
|
|
460
|
-
Messages: ${session.getMessageCount()}`
|
|
461
|
-
});
|
|
462
|
-
return { handled: true };
|
|
463
|
-
}
|
|
464
|
-
function handlePermissions(session, addMessage) {
|
|
465
|
-
const mode = session.getPermissionMode();
|
|
466
|
-
const sessionAllowed = session.getSessionAllowedTools();
|
|
467
|
-
const lines = [`Permission mode: ${mode}`];
|
|
468
|
-
if (sessionAllowed.length > 0) {
|
|
469
|
-
lines.push(`Session-approved tools: ${sessionAllowed.join(", ")}`);
|
|
470
|
-
} else {
|
|
471
|
-
lines.push("No session-approved tools.");
|
|
472
|
-
}
|
|
473
|
-
addMessage({ role: "system", content: lines.join("\n") });
|
|
474
|
-
return { handled: true };
|
|
475
|
-
}
|
|
476
|
-
function handleContext(session, addMessage) {
|
|
477
|
-
const ctx = session.getContextState();
|
|
478
|
-
addMessage({
|
|
479
|
-
role: "system",
|
|
480
|
-
content: `Context: ${ctx.usedTokens.toLocaleString()} / ${ctx.maxTokens.toLocaleString()} tokens (${Math.round(ctx.usedPercentage)}%)`
|
|
481
|
-
});
|
|
482
|
-
return { handled: true };
|
|
483
|
-
}
|
|
484
|
-
function handleLanguage(lang, addMessage) {
|
|
485
|
-
if (!lang) {
|
|
486
|
-
addMessage({ role: "system", content: "Usage: /language <code> (e.g., ko, en, ja, zh)" });
|
|
487
|
-
return { handled: true };
|
|
488
|
-
}
|
|
489
|
-
const settingsPath = getUserSettingsPath();
|
|
490
|
-
const settings = readSettings(settingsPath);
|
|
491
|
-
settings.language = lang;
|
|
492
|
-
writeSettings(settingsPath, settings);
|
|
493
|
-
addMessage({ role: "system", content: `Language set to "${lang}". Restarting...` });
|
|
494
|
-
return { handled: true, exitRequested: true };
|
|
495
|
-
}
|
|
496
|
-
function handleReset(addMessage) {
|
|
497
|
-
const settingsPath = getUserSettingsPath();
|
|
498
|
-
if (deleteSettings(settingsPath)) {
|
|
499
|
-
addMessage({ role: "system", content: `Deleted ${settingsPath}. Exiting...` });
|
|
500
|
-
} else {
|
|
501
|
-
addMessage({ role: "system", content: "No user settings found." });
|
|
502
|
-
}
|
|
503
|
-
return { handled: true, exitRequested: true };
|
|
504
|
-
}
|
|
505
|
-
async function handlePluginCommand(args, addMessage, callbacks) {
|
|
506
|
-
const parts = args.trim().split(/\s+/);
|
|
507
|
-
const subcommand = parts[0] ?? "";
|
|
508
|
-
const subArgs = parts.slice(1).join(" ").trim();
|
|
509
|
-
try {
|
|
510
|
-
switch (subcommand) {
|
|
511
|
-
case "":
|
|
512
|
-
case void 0:
|
|
513
|
-
case "manage": {
|
|
514
|
-
return { handled: true, triggerPluginTUI: true };
|
|
515
|
-
}
|
|
516
|
-
case "install": {
|
|
517
|
-
if (!subArgs) {
|
|
518
|
-
addMessage({ role: "system", content: "Usage: /plugin install <name>@<marketplace>" });
|
|
519
|
-
return { handled: true };
|
|
520
|
-
}
|
|
521
|
-
await callbacks.install(subArgs);
|
|
522
|
-
addMessage({ role: "system", content: `Installed plugin: ${subArgs}` });
|
|
523
|
-
return { handled: true };
|
|
524
|
-
}
|
|
525
|
-
case "uninstall": {
|
|
526
|
-
if (!subArgs) {
|
|
527
|
-
addMessage({ role: "system", content: "Usage: /plugin uninstall <name>@<marketplace>" });
|
|
528
|
-
return { handled: true };
|
|
529
|
-
}
|
|
530
|
-
await callbacks.uninstall(subArgs);
|
|
531
|
-
addMessage({ role: "system", content: `Uninstalled plugin: ${subArgs}` });
|
|
532
|
-
return { handled: true };
|
|
533
|
-
}
|
|
534
|
-
case "enable": {
|
|
535
|
-
if (!subArgs) {
|
|
536
|
-
addMessage({ role: "system", content: "Usage: /plugin enable <name>@<marketplace>" });
|
|
537
|
-
return { handled: true };
|
|
538
|
-
}
|
|
539
|
-
await callbacks.enable(subArgs);
|
|
540
|
-
addMessage({ role: "system", content: `Enabled plugin: ${subArgs}` });
|
|
541
|
-
return { handled: true };
|
|
542
|
-
}
|
|
543
|
-
case "disable": {
|
|
544
|
-
if (!subArgs) {
|
|
545
|
-
addMessage({ role: "system", content: "Usage: /plugin disable <name>@<marketplace>" });
|
|
546
|
-
return { handled: true };
|
|
547
|
-
}
|
|
548
|
-
await callbacks.disable(subArgs);
|
|
549
|
-
addMessage({ role: "system", content: `Disabled plugin: ${subArgs}` });
|
|
550
|
-
return { handled: true };
|
|
551
|
-
}
|
|
552
|
-
case "marketplace": {
|
|
553
|
-
const mpParts = subArgs.split(/\s+/);
|
|
554
|
-
const mpSubcommand = mpParts[0] ?? "";
|
|
555
|
-
const mpArgs = mpParts.slice(1).join(" ").trim();
|
|
556
|
-
if (mpSubcommand === "add" && mpArgs) {
|
|
557
|
-
const registeredName = await callbacks.marketplaceAdd(mpArgs);
|
|
558
|
-
addMessage({
|
|
559
|
-
role: "system",
|
|
560
|
-
content: `Added marketplace: "${registeredName}" (from ${mpArgs})
|
|
561
|
-
Install plugins with: /plugin install <name>@${registeredName}`
|
|
562
|
-
});
|
|
563
|
-
return { handled: true };
|
|
564
|
-
} else if (mpSubcommand === "remove" && mpArgs) {
|
|
565
|
-
await callbacks.marketplaceRemove(mpArgs);
|
|
566
|
-
addMessage({
|
|
567
|
-
role: "system",
|
|
568
|
-
content: `Removed marketplace "${mpArgs}" and uninstalled its plugins.`
|
|
569
|
-
});
|
|
570
|
-
return { handled: true };
|
|
571
|
-
} else if (mpSubcommand === "update" && mpArgs) {
|
|
572
|
-
await callbacks.marketplaceUpdate(mpArgs);
|
|
573
|
-
addMessage({
|
|
574
|
-
role: "system",
|
|
575
|
-
content: `Updated marketplace "${mpArgs}".`
|
|
576
|
-
});
|
|
577
|
-
return { handled: true };
|
|
578
|
-
} else if (mpSubcommand === "list") {
|
|
579
|
-
const sources = await callbacks.marketplaceList();
|
|
580
|
-
if (sources.length === 0) {
|
|
581
|
-
addMessage({ role: "system", content: "No marketplace sources configured." });
|
|
582
|
-
} else {
|
|
583
|
-
const lines = sources.map((s) => ` ${s.name} (${s.type})`);
|
|
584
|
-
addMessage({ role: "system", content: `Marketplace sources:
|
|
585
|
-
${lines.join("\n")}` });
|
|
586
|
-
}
|
|
587
|
-
return { handled: true };
|
|
588
|
-
} else {
|
|
589
|
-
addMessage({
|
|
590
|
-
role: "system",
|
|
591
|
-
content: "Usage: /plugin marketplace add <source> | remove <name> | update <name> | list"
|
|
592
|
-
});
|
|
593
|
-
return { handled: true };
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
default:
|
|
597
|
-
addMessage({ role: "system", content: `Unknown plugin subcommand: ${subcommand}` });
|
|
598
|
-
return { handled: true };
|
|
599
|
-
}
|
|
600
|
-
} catch (error) {
|
|
601
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
602
|
-
addMessage({ role: "system", content: `Plugin error: ${message}` });
|
|
603
|
-
return { handled: true };
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
async function handleReloadPlugins(addMessage, callbacks) {
|
|
607
|
-
await callbacks.reloadPlugins();
|
|
608
|
-
addMessage({ role: "system", content: "Plugins reload complete." });
|
|
609
|
-
return { handled: true };
|
|
610
|
-
}
|
|
611
|
-
async function executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry, pluginCallbacks) {
|
|
612
|
-
switch (cmd) {
|
|
613
|
-
case "help":
|
|
614
|
-
return handleHelp(addMessage);
|
|
615
|
-
case "clear":
|
|
616
|
-
return handleClear(addMessage, clearMessages, session);
|
|
617
|
-
case "compact":
|
|
618
|
-
return handleCompact(args, session, addMessage);
|
|
619
|
-
case "mode":
|
|
620
|
-
return handleMode(args.split(/\s+/)[0] || void 0, session, addMessage);
|
|
621
|
-
case "model":
|
|
622
|
-
return handleModel(args.split(/\s+/)[0] || void 0, addMessage);
|
|
623
|
-
case "language":
|
|
624
|
-
return handleLanguage(args.split(/\s+/)[0] || void 0, addMessage);
|
|
625
|
-
case "cost":
|
|
626
|
-
return handleCost(session, addMessage);
|
|
627
|
-
case "permissions":
|
|
628
|
-
return handlePermissions(session, addMessage);
|
|
629
|
-
case "context":
|
|
630
|
-
return handleContext(session, addMessage);
|
|
631
|
-
case "reset":
|
|
632
|
-
return handleReset(addMessage);
|
|
633
|
-
case "exit":
|
|
634
|
-
return { handled: true, exitRequested: true };
|
|
635
|
-
case "plugin":
|
|
636
|
-
if (pluginCallbacks) {
|
|
637
|
-
return handlePluginCommand(args, addMessage, pluginCallbacks);
|
|
638
|
-
}
|
|
639
|
-
addMessage({ role: "system", content: "Plugin management is not available." });
|
|
640
|
-
return { handled: true };
|
|
641
|
-
case "reload-plugins":
|
|
642
|
-
if (pluginCallbacks) {
|
|
643
|
-
return handleReloadPlugins(addMessage, pluginCallbacks);
|
|
644
|
-
}
|
|
645
|
-
addMessage({ role: "system", content: "Plugin management is not available." });
|
|
646
|
-
return { handled: true };
|
|
647
|
-
default: {
|
|
648
|
-
const dynamicCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
649
|
-
if (dynamicCmd) {
|
|
650
|
-
addMessage({ role: "system", content: `Invoking ${dynamicCmd.source}: ${cmd}` });
|
|
651
|
-
return { handled: false };
|
|
652
|
-
}
|
|
653
|
-
addMessage({ role: "system", content: `Unknown command "/${cmd}". Type /help for help.` });
|
|
654
|
-
return { handled: true };
|
|
655
|
-
}
|
|
170
|
+
// src/commands/plugin-source.ts
|
|
171
|
+
var PluginCommandSource = class {
|
|
172
|
+
name = "plugin";
|
|
173
|
+
plugins;
|
|
174
|
+
constructor(plugins) {
|
|
175
|
+
this.plugins = plugins;
|
|
656
176
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
addMessage(createSystemMessage(msg.content));
|
|
670
|
-
};
|
|
671
|
-
const result = await executeSlashCommand(
|
|
672
|
-
cmd,
|
|
673
|
-
args,
|
|
674
|
-
session,
|
|
675
|
-
slashAddMessage,
|
|
676
|
-
clearMessages,
|
|
677
|
-
registry,
|
|
678
|
-
pluginCallbacks
|
|
679
|
-
);
|
|
680
|
-
if (result.pendingModelId) {
|
|
681
|
-
pendingModelChangeRef.current = result.pendingModelId;
|
|
682
|
-
setPendingModelId(result.pendingModelId);
|
|
683
|
-
}
|
|
684
|
-
if (result.triggerPluginTUI) {
|
|
685
|
-
setShowPluginTUI?.(true);
|
|
686
|
-
}
|
|
687
|
-
if (result.exitRequested) {
|
|
688
|
-
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
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
|
+
});
|
|
689
189
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
pendingModelChangeRef,
|
|
699
|
-
setPendingModelId,
|
|
700
|
-
pluginCallbacks,
|
|
701
|
-
setShowPluginTUI
|
|
702
|
-
]
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// src/ui/hooks/useSubmitHandler.ts
|
|
707
|
-
import { useCallback as useCallback4 } from "react";
|
|
708
|
-
import { randomUUID } from "crypto";
|
|
709
|
-
import {
|
|
710
|
-
createSubagentSession,
|
|
711
|
-
getBuiltInAgent,
|
|
712
|
-
retrieveAgentToolDeps
|
|
713
|
-
} from "@robota-sdk/agent-sdk";
|
|
714
|
-
import {
|
|
715
|
-
createUserMessage,
|
|
716
|
-
createAssistantMessage,
|
|
717
|
-
createSystemMessage as createSystemMessage2,
|
|
718
|
-
createToolMessage
|
|
719
|
-
} from "@robota-sdk/agent-core";
|
|
720
|
-
|
|
721
|
-
// src/utils/tool-call-extractor.ts
|
|
722
|
-
var TOOL_ARG_MAX_LENGTH = 80;
|
|
723
|
-
var TAIL_KEEP2 = 30;
|
|
724
|
-
function extractToolCallsWithDiff(history, startIndex) {
|
|
725
|
-
const summaries = [];
|
|
726
|
-
for (let i = startIndex; i < history.length; i++) {
|
|
727
|
-
const msg = history[i];
|
|
728
|
-
if (msg.role === "assistant" && msg.toolCalls) {
|
|
729
|
-
for (const tc of msg.toolCalls) {
|
|
730
|
-
const value = parseFirstArgValue(tc.function.arguments);
|
|
731
|
-
const truncated = value.length > TOOL_ARG_MAX_LENGTH ? value.slice(0, TOOL_ARG_MAX_LENGTH - TAIL_KEEP2 - 3) + "..." + value.slice(-TAIL_KEEP2) : value;
|
|
732
|
-
const summary = {
|
|
733
|
-
line: `${tc.function.name}(${truncated})`
|
|
734
|
-
};
|
|
735
|
-
if (tc.function.name === "Edit") {
|
|
736
|
-
try {
|
|
737
|
-
const args = JSON.parse(tc.function.arguments);
|
|
738
|
-
const diff = extractEditDiff("Edit", args);
|
|
739
|
-
if (diff) {
|
|
740
|
-
summary.diffLines = diff.lines;
|
|
741
|
-
summary.diffFile = diff.file;
|
|
742
|
-
}
|
|
743
|
-
} catch {
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
summaries.push(summary);
|
|
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
|
+
});
|
|
747
198
|
}
|
|
748
199
|
}
|
|
200
|
+
return commands;
|
|
749
201
|
}
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
function parseFirstArgValue(argsJson) {
|
|
753
|
-
try {
|
|
754
|
-
const parsed = JSON.parse(argsJson);
|
|
755
|
-
const firstVal = Object.values(parsed)[0];
|
|
756
|
-
return typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal);
|
|
757
|
-
} catch {
|
|
758
|
-
return argsJson;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
202
|
+
};
|
|
761
203
|
|
|
762
204
|
// src/utils/skill-prompt.ts
|
|
763
205
|
import { execSync } from "child_process";
|
|
@@ -821,488 +263,33 @@ Execute the "${cmd}" skill: ${userInstruction}`;
|
|
|
821
263
|
return `Use the "${cmd}" skill: ${userInstruction}`;
|
|
822
264
|
}
|
|
823
265
|
|
|
824
|
-
// src/
|
|
825
|
-
|
|
826
|
-
if (!skill.skillContent) return null;
|
|
827
|
-
return substituteVariables(skill.skillContent, args, context);
|
|
828
|
-
}
|
|
829
|
-
function buildInjectPrompt(skill, args, context) {
|
|
830
|
-
const processed = buildProcessedContent(skill, args, context);
|
|
831
|
-
if (processed) {
|
|
832
|
-
const userInstruction = args || skill.description;
|
|
833
|
-
return `<skill name="${skill.name}">
|
|
834
|
-
${processed}
|
|
835
|
-
</skill>
|
|
836
|
-
|
|
837
|
-
Execute the "${skill.name}" skill: ${userInstruction}`;
|
|
838
|
-
}
|
|
839
|
-
return `Use the "${skill.name}" skill: ${args || skill.description}`;
|
|
840
|
-
}
|
|
841
|
-
async function executeSkill(skill, args, callbacks, context) {
|
|
842
|
-
if (skill.context === "fork") {
|
|
843
|
-
if (!callbacks.runInFork) {
|
|
844
|
-
throw new Error("Fork execution is not available. Agent tool deps may not be initialized.");
|
|
845
|
-
}
|
|
846
|
-
const content = buildProcessedContent(skill, args, context);
|
|
847
|
-
const prompt2 = content ?? `Use the "${skill.name}" skill: ${args || skill.description}`;
|
|
848
|
-
const options = {};
|
|
849
|
-
if (skill.agent) options.agent = skill.agent;
|
|
850
|
-
if (skill.allowedTools) options.allowedTools = skill.allowedTools;
|
|
851
|
-
const result = await callbacks.runInFork(prompt2, options);
|
|
852
|
-
return { mode: "fork", result };
|
|
853
|
-
}
|
|
854
|
-
const prompt = buildInjectPrompt(skill, args, context);
|
|
855
|
-
return { mode: "inject", prompt };
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// src/ui/hooks/useSubmitHandler.ts
|
|
859
|
-
function syncContextState(session, setter) {
|
|
860
|
-
const ctx = session.getContextState();
|
|
861
|
-
setter({ percentage: ctx.usedPercentage, usedTokens: ctx.usedTokens, maxTokens: ctx.maxTokens });
|
|
862
|
-
}
|
|
863
|
-
async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState, rawInput) {
|
|
864
|
-
setIsThinking(true);
|
|
865
|
-
clearStreamingText();
|
|
866
|
-
const historyBefore = session.getHistory().length;
|
|
867
|
-
try {
|
|
868
|
-
const response = await session.run(prompt, rawInput);
|
|
869
|
-
clearStreamingText();
|
|
870
|
-
const history = session.getHistory();
|
|
871
|
-
const toolSummaries = extractToolCallsWithDiff(
|
|
872
|
-
history,
|
|
873
|
-
historyBefore
|
|
874
|
-
);
|
|
875
|
-
if (toolSummaries.length > 0) {
|
|
876
|
-
addMessage(
|
|
877
|
-
createToolMessage(JSON.stringify(toolSummaries), {
|
|
878
|
-
toolCallId: randomUUID(),
|
|
879
|
-
name: `${toolSummaries.length} tools`
|
|
880
|
-
})
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
addMessage(createAssistantMessage(response || "(empty response)"));
|
|
884
|
-
syncContextState(session, setContextState);
|
|
885
|
-
} catch (err) {
|
|
886
|
-
clearStreamingText();
|
|
887
|
-
const isAbortError = err instanceof DOMException && err.name === "AbortError" || err instanceof Error && (err.message.includes("aborted") || err.message.includes("abort"));
|
|
888
|
-
if (isAbortError) {
|
|
889
|
-
const history = session.getHistory();
|
|
890
|
-
const toolSummaries = extractToolCallsWithDiff(
|
|
891
|
-
history,
|
|
892
|
-
historyBefore
|
|
893
|
-
);
|
|
894
|
-
if (toolSummaries.length > 0) {
|
|
895
|
-
addMessage(
|
|
896
|
-
createToolMessage(JSON.stringify(toolSummaries), {
|
|
897
|
-
toolCallId: randomUUID(),
|
|
898
|
-
name: `${toolSummaries.length} tools`
|
|
899
|
-
})
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
const assistantParts = [];
|
|
903
|
-
let lastAssistantState = "complete";
|
|
904
|
-
for (let i = historyBefore; i < history.length; i++) {
|
|
905
|
-
const msg = history[i];
|
|
906
|
-
if (msg && msg.role === "assistant" && msg.content) {
|
|
907
|
-
assistantParts.push(msg.content);
|
|
908
|
-
if (msg.state === "interrupted") lastAssistantState = "interrupted";
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
if (assistantParts.length > 0) {
|
|
912
|
-
addMessage(
|
|
913
|
-
createAssistantMessage(assistantParts.join("\n\n"), { state: lastAssistantState })
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
addMessage(createSystemMessage2("Interrupted by user."));
|
|
917
|
-
} else {
|
|
918
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
919
|
-
addMessage(createSystemMessage2(`Error: ${errMsg}`));
|
|
920
|
-
}
|
|
921
|
-
} finally {
|
|
922
|
-
setIsThinking(false);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
function createForkRunner(sessionKey) {
|
|
926
|
-
const deps = retrieveAgentToolDeps(sessionKey);
|
|
927
|
-
if (!deps) return void 0;
|
|
928
|
-
return async (content, options) => {
|
|
929
|
-
const agentType = options.agent ?? "general-purpose";
|
|
930
|
-
const agentDef = getBuiltInAgent(agentType) ?? deps.customAgentRegistry?.(agentType);
|
|
931
|
-
if (!agentDef) {
|
|
932
|
-
throw new Error(`Unknown agent type for fork execution: ${agentType}`);
|
|
933
|
-
}
|
|
934
|
-
const effectiveDef = options.allowedTools ? { ...agentDef, tools: options.allowedTools } : agentDef;
|
|
935
|
-
const subSession = createSubagentSession({
|
|
936
|
-
agentDefinition: effectiveDef,
|
|
937
|
-
parentConfig: deps.config,
|
|
938
|
-
parentContext: deps.context,
|
|
939
|
-
parentTools: deps.tools,
|
|
940
|
-
terminal: deps.terminal,
|
|
941
|
-
isForkWorker: true,
|
|
942
|
-
permissionHandler: deps.permissionHandler,
|
|
943
|
-
onTextDelta: deps.onTextDelta,
|
|
944
|
-
onToolExecution: deps.onToolExecution
|
|
945
|
-
});
|
|
946
|
-
return await subSession.run(content);
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
function findSkillCommand(input, registry) {
|
|
950
|
-
const parts = input.slice(1).split(/\s+/);
|
|
951
|
-
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
952
|
-
const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
953
|
-
if (!skillCmd) return null;
|
|
954
|
-
return { skill: skillCmd, args: parts.slice(1).join(" ").trim() };
|
|
955
|
-
}
|
|
956
|
-
function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextState, registry) {
|
|
957
|
-
return useCallback4(
|
|
958
|
-
async (input) => {
|
|
959
|
-
if (input.startsWith("/")) {
|
|
960
|
-
const handled = await handleSlashCommand(input);
|
|
961
|
-
if (handled) {
|
|
962
|
-
syncContextState(session, setContextState);
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
const found = findSkillCommand(input, registry);
|
|
966
|
-
if (!found) return;
|
|
967
|
-
const { skill, args } = found;
|
|
968
|
-
if (skill.context === "fork") {
|
|
969
|
-
const runInFork = createForkRunner(session);
|
|
970
|
-
const result = await executeSkill(skill, args, { runInFork });
|
|
971
|
-
if (result.mode === "fork") {
|
|
972
|
-
addMessage(createAssistantMessage(result.result ?? "(empty response)"));
|
|
973
|
-
syncContextState(session, setContextState);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (result.prompt) {
|
|
977
|
-
const cmdName2 = input.slice(1).split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
978
|
-
const qualifiedName2 = registry.resolveQualifiedName(cmdName2);
|
|
979
|
-
const hookInput2 = qualifiedName2 ? `/${qualifiedName2}${input.slice(1 + cmdName2.length)}` : input;
|
|
980
|
-
return runSessionPrompt(
|
|
981
|
-
result.prompt,
|
|
982
|
-
session,
|
|
983
|
-
addMessage,
|
|
984
|
-
clearStreamingText,
|
|
985
|
-
setIsThinking,
|
|
986
|
-
setContextState,
|
|
987
|
-
hookInput2
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
const prompt = await buildSkillPrompt(input, registry);
|
|
993
|
-
if (!prompt) return;
|
|
994
|
-
const cmdName = input.slice(1).split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
995
|
-
const qualifiedName = registry.resolveQualifiedName(cmdName);
|
|
996
|
-
const hookInput = qualifiedName ? `/${qualifiedName}${input.slice(1 + cmdName.length)}` : input;
|
|
997
|
-
return runSessionPrompt(
|
|
998
|
-
prompt,
|
|
999
|
-
session,
|
|
1000
|
-
addMessage,
|
|
1001
|
-
clearStreamingText,
|
|
1002
|
-
setIsThinking,
|
|
1003
|
-
setContextState,
|
|
1004
|
-
hookInput
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
addMessage(createUserMessage(input));
|
|
1008
|
-
return runSessionPrompt(
|
|
1009
|
-
input,
|
|
1010
|
-
session,
|
|
1011
|
-
addMessage,
|
|
1012
|
-
clearStreamingText,
|
|
1013
|
-
setIsThinking,
|
|
1014
|
-
setContextState
|
|
1015
|
-
);
|
|
1016
|
-
},
|
|
1017
|
-
[
|
|
1018
|
-
session,
|
|
1019
|
-
addMessage,
|
|
1020
|
-
handleSlashCommand,
|
|
1021
|
-
clearStreamingText,
|
|
1022
|
-
setIsThinking,
|
|
1023
|
-
setContextState,
|
|
1024
|
-
registry
|
|
1025
|
-
]
|
|
1026
|
-
);
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// src/ui/hooks/useCommandRegistry.ts
|
|
1030
|
-
import { useRef as useRef2 } from "react";
|
|
1031
|
-
import { homedir as homedir2 } from "os";
|
|
1032
|
-
import { join as join3, dirname as dirname2 } from "path";
|
|
1033
|
-
import { BundlePluginLoader } from "@robota-sdk/agent-sdk";
|
|
1034
|
-
|
|
1035
|
-
// src/commands/command-registry.ts
|
|
1036
|
-
var CommandRegistry = class {
|
|
1037
|
-
sources = [];
|
|
1038
|
-
addSource(source) {
|
|
1039
|
-
this.sources.push(source);
|
|
1040
|
-
}
|
|
1041
|
-
/** Get all commands, optionally filtered by prefix */
|
|
1042
|
-
getCommands(filter) {
|
|
1043
|
-
const all = [];
|
|
1044
|
-
for (const source of this.sources) {
|
|
1045
|
-
all.push(...source.getCommands());
|
|
1046
|
-
}
|
|
1047
|
-
if (!filter) return all;
|
|
1048
|
-
const lower = filter.toLowerCase();
|
|
1049
|
-
return all.filter((cmd) => cmd.name.toLowerCase().startsWith(lower));
|
|
1050
|
-
}
|
|
1051
|
-
/** Resolve a short name to its fully qualified plugin:name form */
|
|
1052
|
-
resolveQualifiedName(shortName) {
|
|
1053
|
-
const matches = this.getCommands().filter(
|
|
1054
|
-
(c) => c.source === "plugin" && c.name.includes(":") && c.name.endsWith(`:${shortName}`)
|
|
1055
|
-
);
|
|
1056
|
-
if (matches.length !== 1) return null;
|
|
1057
|
-
return matches[0].name;
|
|
1058
|
-
}
|
|
1059
|
-
/** Get subcommands for a specific command */
|
|
1060
|
-
getSubcommands(commandName) {
|
|
1061
|
-
const lower = commandName.toLowerCase();
|
|
1062
|
-
for (const source of this.sources) {
|
|
1063
|
-
for (const cmd of source.getCommands()) {
|
|
1064
|
-
if (cmd.name.toLowerCase() === lower && cmd.subcommands) {
|
|
1065
|
-
return cmd.subcommands;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
return [];
|
|
1070
|
-
}
|
|
1071
|
-
};
|
|
1072
|
-
|
|
1073
|
-
// src/commands/builtin-source.ts
|
|
1074
|
-
import { CLAUDE_MODELS, formatTokenCount } from "@robota-sdk/agent-core";
|
|
1075
|
-
function buildModelSubcommands() {
|
|
1076
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1077
|
-
const commands = [];
|
|
1078
|
-
for (const model of Object.values(CLAUDE_MODELS)) {
|
|
1079
|
-
if (seen.has(model.name)) continue;
|
|
1080
|
-
seen.add(model.name);
|
|
1081
|
-
commands.push({
|
|
1082
|
-
name: model.id,
|
|
1083
|
-
description: `${model.name} (${formatTokenCount(model.contextWindow).toUpperCase()})`,
|
|
1084
|
-
source: "builtin"
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
return commands;
|
|
1088
|
-
}
|
|
1089
|
-
function createBuiltinCommands() {
|
|
1090
|
-
return [
|
|
1091
|
-
{ name: "help", description: "Show available commands", source: "builtin" },
|
|
1092
|
-
{ name: "clear", description: "Clear conversation history", source: "builtin" },
|
|
1093
|
-
{
|
|
1094
|
-
name: "mode",
|
|
1095
|
-
description: "Permission mode",
|
|
1096
|
-
source: "builtin",
|
|
1097
|
-
subcommands: [
|
|
1098
|
-
{ name: "plan", description: "Plan only, no execution", source: "builtin" },
|
|
1099
|
-
{ name: "default", description: "Ask before risky actions", source: "builtin" },
|
|
1100
|
-
{ name: "acceptEdits", description: "Auto-approve file edits", source: "builtin" },
|
|
1101
|
-
{ name: "bypassPermissions", description: "Skip all permission checks", source: "builtin" }
|
|
1102
|
-
]
|
|
1103
|
-
},
|
|
1104
|
-
{
|
|
1105
|
-
name: "model",
|
|
1106
|
-
description: "Select AI model",
|
|
1107
|
-
source: "builtin",
|
|
1108
|
-
subcommands: buildModelSubcommands()
|
|
1109
|
-
},
|
|
1110
|
-
{
|
|
1111
|
-
name: "language",
|
|
1112
|
-
description: "Set response language",
|
|
1113
|
-
source: "builtin",
|
|
1114
|
-
subcommands: [
|
|
1115
|
-
{ name: "ko", description: "Korean", source: "builtin" },
|
|
1116
|
-
{ name: "en", description: "English", source: "builtin" },
|
|
1117
|
-
{ name: "ja", description: "Japanese", source: "builtin" },
|
|
1118
|
-
{ name: "zh", description: "Chinese", source: "builtin" }
|
|
1119
|
-
]
|
|
1120
|
-
},
|
|
1121
|
-
{ name: "compact", description: "Compress context window", source: "builtin" },
|
|
1122
|
-
{ name: "cost", description: "Show session info", source: "builtin" },
|
|
1123
|
-
{ name: "context", description: "Context window info", source: "builtin" },
|
|
1124
|
-
{ name: "permissions", description: "Permission rules", source: "builtin" },
|
|
1125
|
-
{ name: "plugin", description: "Manage plugins", source: "builtin" },
|
|
1126
|
-
{ name: "reload-plugins", description: "Reload all plugin resources", source: "builtin" },
|
|
1127
|
-
{ name: "reset", description: "Delete settings and exit", source: "builtin" },
|
|
1128
|
-
{ name: "exit", description: "Exit CLI", source: "builtin" }
|
|
1129
|
-
];
|
|
1130
|
-
}
|
|
1131
|
-
var BuiltinCommandSource = class {
|
|
1132
|
-
name = "builtin";
|
|
1133
|
-
commands;
|
|
1134
|
-
constructor() {
|
|
1135
|
-
this.commands = createBuiltinCommands();
|
|
1136
|
-
}
|
|
1137
|
-
getCommands() {
|
|
1138
|
-
return this.commands;
|
|
1139
|
-
}
|
|
1140
|
-
};
|
|
1141
|
-
|
|
1142
|
-
// src/commands/skill-source.ts
|
|
1143
|
-
import { readdirSync, readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
1144
|
-
import { join as join2, basename } from "path";
|
|
1145
|
-
import { homedir } from "os";
|
|
1146
|
-
var BOOLEAN_KEYS = /* @__PURE__ */ new Set(["disable-model-invocation", "user-invocable"]);
|
|
1147
|
-
var LIST_KEYS = /* @__PURE__ */ new Set(["allowed-tools"]);
|
|
1148
|
-
function kebabToCamel(key) {
|
|
1149
|
-
return key.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
1150
|
-
}
|
|
1151
|
-
function parseFrontmatter(content) {
|
|
1152
|
-
const lines = content.split("\n");
|
|
1153
|
-
if (lines[0]?.trim() !== "---") return null;
|
|
1154
|
-
const result = {};
|
|
1155
|
-
for (let i = 1; i < lines.length; i++) {
|
|
1156
|
-
const line = lines[i];
|
|
1157
|
-
if (line.trim() === "---") break;
|
|
1158
|
-
const match = line.match(/^([a-z][a-z0-9-]*):\s*(.+)/);
|
|
1159
|
-
if (!match) continue;
|
|
1160
|
-
const key = match[1];
|
|
1161
|
-
const rawValue = match[2].trim();
|
|
1162
|
-
const camelKey = kebabToCamel(key);
|
|
1163
|
-
if (BOOLEAN_KEYS.has(key)) {
|
|
1164
|
-
result[camelKey] = rawValue === "true";
|
|
1165
|
-
} else if (LIST_KEYS.has(key)) {
|
|
1166
|
-
result[camelKey] = rawValue.split(",").map((s) => s.trim());
|
|
1167
|
-
} else {
|
|
1168
|
-
result[camelKey] = rawValue;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
return Object.keys(result).length > 0 ? result : null;
|
|
1172
|
-
}
|
|
1173
|
-
function buildCommand(frontmatter, content, fallbackName) {
|
|
1174
|
-
const cmd = {
|
|
1175
|
-
name: frontmatter?.name ?? fallbackName,
|
|
1176
|
-
description: frontmatter?.description ?? `Skill: ${fallbackName}`,
|
|
1177
|
-
source: "skill",
|
|
1178
|
-
skillContent: content
|
|
1179
|
-
};
|
|
1180
|
-
if (frontmatter?.argumentHint !== void 0) cmd.argumentHint = frontmatter.argumentHint;
|
|
1181
|
-
if (frontmatter?.disableModelInvocation !== void 0)
|
|
1182
|
-
cmd.disableModelInvocation = frontmatter.disableModelInvocation;
|
|
1183
|
-
if (frontmatter?.userInvocable !== void 0) cmd.userInvocable = frontmatter.userInvocable;
|
|
1184
|
-
if (frontmatter?.allowedTools !== void 0) cmd.allowedTools = frontmatter.allowedTools;
|
|
1185
|
-
if (frontmatter?.model !== void 0) cmd.model = frontmatter.model;
|
|
1186
|
-
if (frontmatter?.effort !== void 0) cmd.effort = frontmatter.effort;
|
|
1187
|
-
if (frontmatter?.context !== void 0) cmd.context = frontmatter.context;
|
|
1188
|
-
if (frontmatter?.agent !== void 0) cmd.agent = frontmatter.agent;
|
|
1189
|
-
return cmd;
|
|
1190
|
-
}
|
|
1191
|
-
function scanSkillsDir(skillsDir) {
|
|
1192
|
-
if (!existsSync2(skillsDir)) return [];
|
|
1193
|
-
const commands = [];
|
|
1194
|
-
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
1195
|
-
for (const entry of entries) {
|
|
1196
|
-
if (!entry.isDirectory()) continue;
|
|
1197
|
-
const skillFile = join2(skillsDir, entry.name, "SKILL.md");
|
|
1198
|
-
if (!existsSync2(skillFile)) continue;
|
|
1199
|
-
const content = readFileSync3(skillFile, "utf-8");
|
|
1200
|
-
const frontmatter = parseFrontmatter(content);
|
|
1201
|
-
commands.push(buildCommand(frontmatter, content, entry.name));
|
|
1202
|
-
}
|
|
1203
|
-
return commands;
|
|
1204
|
-
}
|
|
1205
|
-
function scanCommandsDir(commandsDir) {
|
|
1206
|
-
if (!existsSync2(commandsDir)) return [];
|
|
1207
|
-
const commands = [];
|
|
1208
|
-
const entries = readdirSync(commandsDir, { withFileTypes: true });
|
|
1209
|
-
for (const entry of entries) {
|
|
1210
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1211
|
-
const filePath = join2(commandsDir, entry.name);
|
|
1212
|
-
const content = readFileSync3(filePath, "utf-8");
|
|
1213
|
-
const frontmatter = parseFrontmatter(content);
|
|
1214
|
-
const fallbackName = basename(entry.name, ".md");
|
|
1215
|
-
commands.push(buildCommand(frontmatter, content, fallbackName));
|
|
1216
|
-
}
|
|
1217
|
-
return commands;
|
|
1218
|
-
}
|
|
1219
|
-
var SkillCommandSource = class {
|
|
1220
|
-
name = "skill";
|
|
1221
|
-
cwd;
|
|
1222
|
-
home;
|
|
1223
|
-
cachedCommands = null;
|
|
1224
|
-
constructor(cwd, home) {
|
|
1225
|
-
this.cwd = cwd;
|
|
1226
|
-
this.home = home ?? homedir();
|
|
1227
|
-
}
|
|
1228
|
-
getCommands() {
|
|
1229
|
-
if (this.cachedCommands) return this.cachedCommands;
|
|
1230
|
-
const sources = [
|
|
1231
|
-
scanSkillsDir(join2(this.cwd, ".claude", "skills")),
|
|
1232
|
-
// 1. project .claude/skills
|
|
1233
|
-
scanCommandsDir(join2(this.cwd, ".claude", "commands")),
|
|
1234
|
-
// 2. project .claude/commands (legacy)
|
|
1235
|
-
scanSkillsDir(join2(this.home, ".robota", "skills")),
|
|
1236
|
-
// 3. user ~/.robota/skills
|
|
1237
|
-
scanSkillsDir(join2(this.cwd, ".agents", "skills"))
|
|
1238
|
-
// 4. project .agents/skills
|
|
1239
|
-
];
|
|
1240
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1241
|
-
const merged = [];
|
|
1242
|
-
for (const commands of sources) {
|
|
1243
|
-
for (const cmd of commands) {
|
|
1244
|
-
if (!seen.has(cmd.name)) {
|
|
1245
|
-
seen.add(cmd.name);
|
|
1246
|
-
merged.push(cmd);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
this.cachedCommands = merged;
|
|
1251
|
-
return this.cachedCommands;
|
|
1252
|
-
}
|
|
1253
|
-
/** Get skills that models can invoke (excludes disableModelInvocation: true) */
|
|
1254
|
-
getModelInvocableSkills() {
|
|
1255
|
-
return this.getCommands().filter((cmd) => cmd.disableModelInvocation !== true);
|
|
1256
|
-
}
|
|
1257
|
-
/** Get skills that users can invoke (excludes userInvocable: false) */
|
|
1258
|
-
getUserInvocableSkills() {
|
|
1259
|
-
return this.getCommands().filter((cmd) => cmd.userInvocable !== false);
|
|
1260
|
-
}
|
|
1261
|
-
};
|
|
1262
|
-
|
|
1263
|
-
// src/commands/plugin-source.ts
|
|
1264
|
-
var PluginCommandSource = class {
|
|
1265
|
-
name = "plugin";
|
|
1266
|
-
plugins;
|
|
1267
|
-
constructor(plugins) {
|
|
1268
|
-
this.plugins = plugins;
|
|
1269
|
-
}
|
|
1270
|
-
getCommands() {
|
|
1271
|
-
const commands = [];
|
|
1272
|
-
for (const plugin of this.plugins) {
|
|
1273
|
-
for (const skill of plugin.skills) {
|
|
1274
|
-
const baseName = skill.name.includes("@") ? skill.name.split("@")[0] : skill.name;
|
|
1275
|
-
commands.push({
|
|
1276
|
-
name: baseName,
|
|
1277
|
-
description: `(${plugin.manifest.name}) ${skill.description}`,
|
|
1278
|
-
source: "plugin",
|
|
1279
|
-
skillContent: skill.skillContent,
|
|
1280
|
-
pluginDir: plugin.pluginDir
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
for (const cmd of plugin.commands) {
|
|
1284
|
-
commands.push({
|
|
1285
|
-
name: cmd.name,
|
|
1286
|
-
description: cmd.description,
|
|
1287
|
-
source: "plugin",
|
|
1288
|
-
skillContent: cmd.skillContent,
|
|
1289
|
-
pluginDir: plugin.pluginDir
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
return commands;
|
|
1294
|
-
}
|
|
1295
|
-
};
|
|
1296
|
-
|
|
1297
|
-
// src/ui/hooks/useCommandRegistry.ts
|
|
266
|
+
// src/ui/hooks/plugin-hooks-merger.ts
|
|
267
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
1298
268
|
function buildPluginEnv(plugin) {
|
|
1299
|
-
const dataDir =
|
|
269
|
+
const dataDir = join2(dirname2(dirname2(plugin.pluginDir)), "data", plugin.manifest.name);
|
|
1300
270
|
return {
|
|
1301
271
|
CLAUDE_PLUGIN_ROOT: plugin.pluginDir,
|
|
1302
272
|
CLAUDE_PLUGIN_PATH: plugin.pluginDir,
|
|
1303
273
|
CLAUDE_PLUGIN_DATA: dataDir
|
|
1304
274
|
};
|
|
1305
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
|
+
}
|
|
1306
293
|
function mergePluginHooks(plugins) {
|
|
1307
294
|
const merged = {};
|
|
1308
295
|
for (const plugin of plugins) {
|
|
@@ -1314,63 +301,278 @@ function mergePluginHooks(plugins) {
|
|
|
1314
301
|
if (!Array.isArray(groups)) continue;
|
|
1315
302
|
if (!merged[event]) merged[event] = [];
|
|
1316
303
|
const resolved = groups.map((group) => {
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
}
|
|
1321
|
-
return resolved2;
|
|
304
|
+
const r = resolvePluginRoot(group, plugin.pluginDir);
|
|
305
|
+
r.env = pluginEnv;
|
|
306
|
+
return r;
|
|
1322
307
|
});
|
|
1323
308
|
merged[event].push(...resolved);
|
|
1324
309
|
}
|
|
1325
310
|
}
|
|
1326
311
|
return merged;
|
|
1327
312
|
}
|
|
1328
|
-
function
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
hooks: obj.hooks.map((h) => {
|
|
1335
|
-
if (typeof h !== "object" || h === null) return h;
|
|
1336
|
-
const hook = h;
|
|
1337
|
-
if (typeof hook.command === "string") {
|
|
1338
|
-
return {
|
|
1339
|
-
...hook,
|
|
1340
|
-
command: hook.command.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginDir)
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
return hook;
|
|
1344
|
-
})
|
|
1345
|
-
};
|
|
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];
|
|
1346
319
|
}
|
|
1347
|
-
|
|
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;
|
|
1348
328
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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);
|
|
1363
406
|
}
|
|
1364
|
-
}
|
|
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());
|
|
1365
481
|
}
|
|
1366
|
-
|
|
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
|
+
});
|
|
1367
551
|
}
|
|
1368
|
-
return
|
|
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
|
+
};
|
|
1369
571
|
}
|
|
1370
572
|
|
|
1371
573
|
// src/ui/hooks/usePluginCallbacks.ts
|
|
1372
574
|
import { useMemo } from "react";
|
|
1373
|
-
import { homedir as
|
|
575
|
+
import { homedir as homedir2 } from "os";
|
|
1374
576
|
import { join as join4 } from "path";
|
|
1375
577
|
import {
|
|
1376
578
|
PluginSettingsStore,
|
|
@@ -1380,7 +582,7 @@ import {
|
|
|
1380
582
|
} from "@robota-sdk/agent-sdk";
|
|
1381
583
|
function usePluginCallbacks(cwd) {
|
|
1382
584
|
return useMemo(() => {
|
|
1383
|
-
const home =
|
|
585
|
+
const home = homedir2();
|
|
1384
586
|
const pluginsDir = join4(home, ".robota", "plugins");
|
|
1385
587
|
const userSettingsPath = join4(home, ".robota", "settings.json");
|
|
1386
588
|
const settingsStore = new PluginSettingsStore(userSettingsPath);
|
|
@@ -1646,7 +848,7 @@ function MessageList({ messages }) {
|
|
|
1646
848
|
|
|
1647
849
|
// src/ui/StatusBar.tsx
|
|
1648
850
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
1649
|
-
import { formatTokenCount
|
|
851
|
+
import { formatTokenCount } from "@robota-sdk/agent-core";
|
|
1650
852
|
import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1651
853
|
var CONTEXT_YELLOW_THRESHOLD = 70;
|
|
1652
854
|
var CONTEXT_RED_THRESHOLD = 90;
|
|
@@ -1686,9 +888,9 @@ function StatusBar({
|
|
|
1686
888
|
"Context: ",
|
|
1687
889
|
Math.round(contextPercentage),
|
|
1688
890
|
"% (",
|
|
1689
|
-
|
|
891
|
+
formatTokenCount(contextUsedTokens),
|
|
1690
892
|
"/",
|
|
1691
|
-
|
|
893
|
+
formatTokenCount(contextMaxTokens),
|
|
1692
894
|
")"
|
|
1693
895
|
] })
|
|
1694
896
|
] }),
|
|
@@ -1705,11 +907,11 @@ function StatusBar({
|
|
|
1705
907
|
}
|
|
1706
908
|
|
|
1707
909
|
// src/ui/InputArea.tsx
|
|
1708
|
-
import React5, { useState as
|
|
910
|
+
import React5, { useState as useState4, useCallback as useCallback2, useMemo as useMemo2, useRef as useRef3 } from "react";
|
|
1709
911
|
import { Box as Box5, Text as Text7, useInput as useInput2, useStdout } from "ink";
|
|
1710
912
|
|
|
1711
913
|
// src/ui/CjkTextInput.tsx
|
|
1712
|
-
import { useRef as
|
|
914
|
+
import { useRef as useRef2, useState as useState2 } from "react";
|
|
1713
915
|
import { Text as Text4, useInput } from "ink";
|
|
1714
916
|
import chalk from "chalk";
|
|
1715
917
|
import stringWidth from "string-width";
|
|
@@ -1755,11 +957,11 @@ function CjkTextInput({
|
|
|
1755
957
|
showCursor = true,
|
|
1756
958
|
availableWidth
|
|
1757
959
|
}) {
|
|
1758
|
-
const valueRef =
|
|
1759
|
-
const cursorRef =
|
|
1760
|
-
const [, forceRender] =
|
|
1761
|
-
const isPastingRef =
|
|
1762
|
-
const pasteBufferRef =
|
|
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("");
|
|
1763
965
|
if (value !== valueRef.current) {
|
|
1764
966
|
valueRef.current = value;
|
|
1765
967
|
if (cursorRef.current > value.length) {
|
|
@@ -1888,15 +1090,15 @@ function renderWithCursor(value, cursorOffset, placeholder, showCursor) {
|
|
|
1888
1090
|
}
|
|
1889
1091
|
|
|
1890
1092
|
// src/ui/WaveText.tsx
|
|
1891
|
-
import { useState as
|
|
1093
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
1892
1094
|
import { Text as Text5 } from "ink";
|
|
1893
1095
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1894
1096
|
var WAVE_COLORS = ["#666666", "#888888", "#aaaaaa", "#888888"];
|
|
1895
1097
|
var INTERVAL_MS = 400;
|
|
1896
1098
|
var CHARS_PER_GROUP = 4;
|
|
1897
1099
|
function WaveText({ text }) {
|
|
1898
|
-
const [tick, setTick] =
|
|
1899
|
-
|
|
1100
|
+
const [tick, setTick] = useState3(0);
|
|
1101
|
+
useEffect2(() => {
|
|
1900
1102
|
const timer = setInterval(() => {
|
|
1901
1103
|
setTick((prev) => prev + 1);
|
|
1902
1104
|
}, INTERVAL_MS);
|
|
@@ -1968,8 +1170,8 @@ function parseSlashInput(value) {
|
|
|
1968
1170
|
return { isSlash: true, parentCommand: parent, filter: rest };
|
|
1969
1171
|
}
|
|
1970
1172
|
function useAutocomplete(value, registry) {
|
|
1971
|
-
const [selectedIndex, setSelectedIndex] =
|
|
1972
|
-
const [dismissed, setDismissed] =
|
|
1173
|
+
const [selectedIndex, setSelectedIndex] = useState4(0);
|
|
1174
|
+
const [dismissed, setDismissed] = useState4(false);
|
|
1973
1175
|
const prevValueRef = React5.useRef(value);
|
|
1974
1176
|
if (prevValueRef.current !== value) {
|
|
1975
1177
|
prevValueRef.current = value;
|
|
@@ -2022,12 +1224,12 @@ function InputArea({
|
|
|
2022
1224
|
pendingPrompt,
|
|
2023
1225
|
registry
|
|
2024
1226
|
}) {
|
|
2025
|
-
const [value, setValue] =
|
|
2026
|
-
const pasteStore =
|
|
1227
|
+
const [value, setValue] = useState4("");
|
|
1228
|
+
const pasteStore = useRef3(/* @__PURE__ */ new Map());
|
|
2027
1229
|
const { stdout } = useStdout();
|
|
2028
1230
|
const terminalColumns = stdout?.columns ?? 80;
|
|
2029
1231
|
const availableWidth = Math.max(1, terminalColumns - INPUT_AREA_OVERHEAD);
|
|
2030
|
-
const pasteIdRef =
|
|
1232
|
+
const pasteIdRef = useRef3(0);
|
|
2031
1233
|
const {
|
|
2032
1234
|
showPopup,
|
|
2033
1235
|
filteredCommands,
|
|
@@ -2036,7 +1238,7 @@ function InputArea({
|
|
|
2036
1238
|
isSubcommandMode,
|
|
2037
1239
|
setShowPopup
|
|
2038
1240
|
} = useAutocomplete(value, registry);
|
|
2039
|
-
const handlePaste =
|
|
1241
|
+
const handlePaste = useCallback2((text) => {
|
|
2040
1242
|
pasteIdRef.current += 1;
|
|
2041
1243
|
const id = pasteIdRef.current;
|
|
2042
1244
|
pasteStore.current.set(id, text);
|
|
@@ -2044,7 +1246,7 @@ function InputArea({
|
|
|
2044
1246
|
const label = `[Pasted text #${id} +${lineCount} lines]`;
|
|
2045
1247
|
setValue((prev) => prev ? `${prev} ${label}` : label);
|
|
2046
1248
|
}, []);
|
|
2047
|
-
const handleSubmit =
|
|
1249
|
+
const handleSubmit = useCallback2(
|
|
2048
1250
|
(text) => {
|
|
2049
1251
|
const trimmed = text.trim();
|
|
2050
1252
|
if (trimmed.length === 0) return;
|
|
@@ -2060,7 +1262,7 @@ function InputArea({
|
|
|
2060
1262
|
},
|
|
2061
1263
|
[showPopup, filteredCommands, selectedIndex, onSubmit]
|
|
2062
1264
|
);
|
|
2063
|
-
const selectCommand =
|
|
1265
|
+
const selectCommand = useCallback2(
|
|
2064
1266
|
(cmd) => {
|
|
2065
1267
|
const parsed = parseSlashInput(value);
|
|
2066
1268
|
if (parsed.parentCommand) {
|
|
@@ -2145,7 +1347,7 @@ function InputArea({
|
|
|
2145
1347
|
}
|
|
2146
1348
|
|
|
2147
1349
|
// src/ui/ConfirmPrompt.tsx
|
|
2148
|
-
import { useState as
|
|
1350
|
+
import { useState as useState5, useCallback as useCallback3, useRef as useRef4 } from "react";
|
|
2149
1351
|
import { Box as Box6, Text as Text8, useInput as useInput3 } from "ink";
|
|
2150
1352
|
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2151
1353
|
function ConfirmPrompt({
|
|
@@ -2153,9 +1355,9 @@ function ConfirmPrompt({
|
|
|
2153
1355
|
options = ["Yes", "No"],
|
|
2154
1356
|
onSelect
|
|
2155
1357
|
}) {
|
|
2156
|
-
const [selected, setSelected] =
|
|
2157
|
-
const resolvedRef =
|
|
2158
|
-
const doSelect =
|
|
1358
|
+
const [selected, setSelected] = useState5(0);
|
|
1359
|
+
const resolvedRef = useRef4(false);
|
|
1360
|
+
const doSelect = useCallback3(
|
|
2159
1361
|
(index) => {
|
|
2160
1362
|
if (resolvedRef.current) return;
|
|
2161
1363
|
resolvedRef.current = true;
|
|
@@ -2295,10 +1497,10 @@ function StreamingIndicator({ text, activeTools }) {
|
|
|
2295
1497
|
}
|
|
2296
1498
|
|
|
2297
1499
|
// src/ui/PluginTUI.tsx
|
|
2298
|
-
import { useState as
|
|
1500
|
+
import { useState as useState8, useEffect as useEffect3, useCallback as useCallback6 } from "react";
|
|
2299
1501
|
|
|
2300
1502
|
// src/ui/MenuSelect.tsx
|
|
2301
|
-
import { useState as
|
|
1503
|
+
import { useState as useState6, useCallback as useCallback4, useRef as useRef5 } from "react";
|
|
2302
1504
|
import { Box as Box9, Text as Text11, useInput as useInput5 } from "ink";
|
|
2303
1505
|
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2304
1506
|
function MenuSelect({
|
|
@@ -2309,10 +1511,10 @@ function MenuSelect({
|
|
|
2309
1511
|
loading,
|
|
2310
1512
|
error
|
|
2311
1513
|
}) {
|
|
2312
|
-
const [selected, setSelected] =
|
|
2313
|
-
const selectedRef =
|
|
2314
|
-
const resolvedRef =
|
|
2315
|
-
const doSelect =
|
|
1514
|
+
const [selected, setSelected] = useState6(0);
|
|
1515
|
+
const selectedRef = useRef5(0);
|
|
1516
|
+
const resolvedRef = useRef5(false);
|
|
1517
|
+
const doSelect = useCallback4(
|
|
2316
1518
|
(index) => {
|
|
2317
1519
|
if (resolvedRef.current || items.length === 0) return;
|
|
2318
1520
|
resolvedRef.current = true;
|
|
@@ -2362,7 +1564,7 @@ function MenuSelect({
|
|
|
2362
1564
|
}
|
|
2363
1565
|
|
|
2364
1566
|
// src/ui/TextPrompt.tsx
|
|
2365
|
-
import { useState as
|
|
1567
|
+
import { useState as useState7, useRef as useRef6, useCallback as useCallback5 } from "react";
|
|
2366
1568
|
import { Box as Box10, Text as Text12, useInput as useInput6 } from "ink";
|
|
2367
1569
|
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2368
1570
|
function TextPrompt({
|
|
@@ -2372,11 +1574,11 @@ function TextPrompt({
|
|
|
2372
1574
|
onCancel,
|
|
2373
1575
|
validate
|
|
2374
1576
|
}) {
|
|
2375
|
-
const [value, setValue] =
|
|
2376
|
-
const [error, setError] =
|
|
2377
|
-
const resolvedRef =
|
|
2378
|
-
const valueRef =
|
|
2379
|
-
const handleSubmit =
|
|
1577
|
+
const [value, setValue] = useState7("");
|
|
1578
|
+
const [error, setError] = useState7();
|
|
1579
|
+
const resolvedRef = useRef6(false);
|
|
1580
|
+
const valueRef = useRef6("");
|
|
1581
|
+
const handleSubmit = useCallback5(() => {
|
|
2380
1582
|
if (resolvedRef.current) return;
|
|
2381
1583
|
const trimmed = valueRef.current.trim();
|
|
2382
1584
|
if (!trimmed) return;
|
|
@@ -2520,19 +1722,19 @@ function handleInstalledActionSelect(value, pluginId, callbacks, nav) {
|
|
|
2520
1722
|
// src/ui/PluginTUI.tsx
|
|
2521
1723
|
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
2522
1724
|
function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
2523
|
-
const [stack, setStack] =
|
|
2524
|
-
const [items, setItems] =
|
|
2525
|
-
const [loading, setLoading] =
|
|
2526
|
-
const [error, setError] =
|
|
2527
|
-
const [confirm, setConfirm] =
|
|
2528
|
-
const [refreshCounter, setRefreshCounter] =
|
|
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);
|
|
2529
1731
|
const current = stack[stack.length - 1] ?? { screen: "main" };
|
|
2530
|
-
const push =
|
|
1732
|
+
const push = useCallback6((state) => {
|
|
2531
1733
|
setStack((prev) => [...prev, state]);
|
|
2532
1734
|
setItems([]);
|
|
2533
1735
|
setError(void 0);
|
|
2534
1736
|
}, []);
|
|
2535
|
-
const pop =
|
|
1737
|
+
const pop = useCallback6(() => {
|
|
2536
1738
|
setStack((prev) => {
|
|
2537
1739
|
if (prev.length <= 1) {
|
|
2538
1740
|
onClose();
|
|
@@ -2543,7 +1745,7 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2543
1745
|
setItems([]);
|
|
2544
1746
|
setError(void 0);
|
|
2545
1747
|
}, [onClose]);
|
|
2546
|
-
const popN =
|
|
1748
|
+
const popN = useCallback6(
|
|
2547
1749
|
(n) => {
|
|
2548
1750
|
setStack((prev) => {
|
|
2549
1751
|
const next = prev.slice(0, Math.max(1, prev.length - n));
|
|
@@ -2558,18 +1760,18 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2558
1760
|
},
|
|
2559
1761
|
[onClose]
|
|
2560
1762
|
);
|
|
2561
|
-
const notify =
|
|
1763
|
+
const notify = useCallback6(
|
|
2562
1764
|
(content) => {
|
|
2563
1765
|
addMessage?.({ role: "system", content });
|
|
2564
1766
|
},
|
|
2565
1767
|
[addMessage]
|
|
2566
1768
|
);
|
|
2567
|
-
const refresh =
|
|
1769
|
+
const refresh = useCallback6(() => {
|
|
2568
1770
|
setItems([]);
|
|
2569
1771
|
setRefreshCounter((c) => c + 1);
|
|
2570
1772
|
}, []);
|
|
2571
1773
|
const nav = { push, pop, popN, notify, setConfirm, refresh };
|
|
2572
|
-
|
|
1774
|
+
useEffect3(() => {
|
|
2573
1775
|
const screen2 = current.screen;
|
|
2574
1776
|
if (screen2 === "marketplace-list") {
|
|
2575
1777
|
setLoading(true);
|
|
@@ -2619,7 +1821,7 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2619
1821
|
});
|
|
2620
1822
|
}
|
|
2621
1823
|
}, [stack.length, current.screen, current.context?.marketplace, callbacks, refreshCounter]);
|
|
2622
|
-
const handleSelect =
|
|
1824
|
+
const handleSelect = useCallback6(
|
|
2623
1825
|
(value) => {
|
|
2624
1826
|
const screen2 = current.screen;
|
|
2625
1827
|
const ctx = current.context;
|
|
@@ -2637,7 +1839,7 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2637
1839
|
},
|
|
2638
1840
|
[current, items, callbacks, push, pop, popN, notify, setConfirm, refresh]
|
|
2639
1841
|
);
|
|
2640
|
-
const handleTextSubmit =
|
|
1842
|
+
const handleTextSubmit = useCallback6(
|
|
2641
1843
|
(value) => {
|
|
2642
1844
|
if (current.screen === "marketplace-add") {
|
|
2643
1845
|
callbacks.marketplaceAdd(value).then((name) => {
|
|
@@ -2747,104 +1949,90 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2747
1949
|
|
|
2748
1950
|
// src/ui/App.tsx
|
|
2749
1951
|
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2750
|
-
var
|
|
2751
|
-
function mergeHooksIntoConfig(configHooks, pluginHooks) {
|
|
2752
|
-
const pluginKeys = Object.keys(pluginHooks);
|
|
2753
|
-
if (pluginKeys.length === 0) return configHooks;
|
|
2754
|
-
const merged = {};
|
|
2755
|
-
for (const [event, groups] of Object.entries(pluginHooks)) {
|
|
2756
|
-
merged[event] = [...groups];
|
|
2757
|
-
}
|
|
2758
|
-
if (configHooks) {
|
|
2759
|
-
for (const [event, groups] of Object.entries(configHooks)) {
|
|
2760
|
-
if (!Array.isArray(groups)) continue;
|
|
2761
|
-
if (!merged[event]) merged[event] = [];
|
|
2762
|
-
merged[event].push(...groups);
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
return merged;
|
|
2766
|
-
}
|
|
1952
|
+
var EXIT_DELAY_MS = 500;
|
|
2767
1953
|
function App(props) {
|
|
2768
1954
|
const { exit } = useApp();
|
|
2769
|
-
const
|
|
2770
|
-
const
|
|
2771
|
-
|
|
2772
|
-
hooks: mergeHooksIntoConfig(
|
|
2773
|
-
props.config.hooks,
|
|
2774
|
-
pluginHooks
|
|
2775
|
-
)
|
|
2776
|
-
};
|
|
2777
|
-
const { session, permissionRequest, streamingText, clearStreamingText, activeTools } = useSession(
|
|
2778
|
-
{ ...props, config: configWithPluginHooks }
|
|
2779
|
-
);
|
|
2780
|
-
const { messages, setMessages, addMessage } = useMessages();
|
|
2781
|
-
const [isThinking, setIsThinking] = useState10(false);
|
|
2782
|
-
const initialCtx = session.getContextState();
|
|
2783
|
-
const [contextState, setContextState] = useState10({
|
|
2784
|
-
percentage: initialCtx.usedPercentage,
|
|
2785
|
-
usedTokens: initialCtx.usedTokens,
|
|
2786
|
-
maxTokens: initialCtx.maxTokens
|
|
2787
|
-
});
|
|
2788
|
-
const pendingModelChangeRef = useRef8(null);
|
|
2789
|
-
const [pendingModelId, setPendingModelId] = useState10(null);
|
|
2790
|
-
const [showPluginTUI, setShowPluginTUI] = useState10(false);
|
|
2791
|
-
const [isAborting, setIsAborting] = useState10(false);
|
|
2792
|
-
const [pendingPrompt, setPendingPrompt] = useState10(null);
|
|
2793
|
-
const pendingPromptRef = useRef8(null);
|
|
2794
|
-
const pluginCallbacks = usePluginCallbacks(props.cwd ?? process.cwd());
|
|
2795
|
-
const handleSlashCommand = useSlashCommands(
|
|
2796
|
-
session,
|
|
2797
|
-
addMessage,
|
|
2798
|
-
setMessages,
|
|
2799
|
-
exit,
|
|
1955
|
+
const cwd = props.cwd ?? process.cwd();
|
|
1956
|
+
const {
|
|
1957
|
+
interactiveSession,
|
|
2800
1958
|
registry,
|
|
2801
|
-
|
|
2802
|
-
setPendingModelId,
|
|
2803
|
-
pluginCallbacks,
|
|
2804
|
-
setShowPluginTUI
|
|
2805
|
-
);
|
|
2806
|
-
const executePrompt = useSubmitHandler(
|
|
2807
|
-
session,
|
|
1959
|
+
messages,
|
|
2808
1960
|
addMessage,
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
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."));
|
|
2821
2012
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
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
|
+
};
|
|
2826
2027
|
useInput7(
|
|
2827
2028
|
(_input, key) => {
|
|
2828
2029
|
if (key.escape && isThinking) {
|
|
2829
|
-
|
|
2830
|
-
setPendingPrompt(null);
|
|
2831
|
-
pendingPromptRef.current = null;
|
|
2832
|
-
session.abort();
|
|
2030
|
+
handleAbort();
|
|
2833
2031
|
}
|
|
2834
2032
|
},
|
|
2835
2033
|
{ isActive: !permissionRequest && !showPluginTUI }
|
|
2836
2034
|
);
|
|
2837
|
-
|
|
2838
|
-
if (!isThinking) {
|
|
2839
|
-
setIsAborting(false);
|
|
2840
|
-
if (pendingPromptRef.current) {
|
|
2841
|
-
const prompt = pendingPromptRef.current;
|
|
2842
|
-
setPendingPrompt(null);
|
|
2843
|
-
pendingPromptRef.current = null;
|
|
2844
|
-
setTimeout(() => executePrompt(prompt), 0);
|
|
2845
|
-
}
|
|
2846
|
-
}
|
|
2847
|
-
}, [isThinking, pendingPrompt, executePrompt]);
|
|
2035
|
+
const session = interactiveSession.getSession();
|
|
2848
2036
|
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
|
|
2849
2037
|
/* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
|
|
2850
2038
|
/* @__PURE__ */ jsx13(Text13, { color: "cyan", bold: true, children: `
|
|
@@ -2876,20 +2064,20 @@ function App(props) {
|
|
|
2876
2064
|
const settingsPath = getUserSettingsPath();
|
|
2877
2065
|
updateModelInSettings(settingsPath, pendingModelId);
|
|
2878
2066
|
addMessage(
|
|
2879
|
-
|
|
2067
|
+
createSystemMessage2(
|
|
2880
2068
|
`Model changed to ${getModelName(pendingModelId)}. Restarting...`
|
|
2881
2069
|
)
|
|
2882
2070
|
);
|
|
2883
|
-
setTimeout(() => exit(),
|
|
2071
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2884
2072
|
} catch (err) {
|
|
2885
2073
|
addMessage(
|
|
2886
|
-
|
|
2074
|
+
createSystemMessage2(
|
|
2887
2075
|
`Failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2888
2076
|
)
|
|
2889
2077
|
);
|
|
2890
2078
|
}
|
|
2891
2079
|
} else {
|
|
2892
|
-
addMessage(
|
|
2080
|
+
addMessage(createSystemMessage2("Model change cancelled."));
|
|
2893
2081
|
}
|
|
2894
2082
|
}
|
|
2895
2083
|
}
|
|
@@ -2899,7 +2087,7 @@ function App(props) {
|
|
|
2899
2087
|
{
|
|
2900
2088
|
callbacks: pluginCallbacks,
|
|
2901
2089
|
onClose: () => setShowPluginTUI(false),
|
|
2902
|
-
addMessage: (msg) => addMessage(
|
|
2090
|
+
addMessage: (msg) => addMessage(createSystemMessage2(msg.content))
|
|
2903
2091
|
}
|
|
2904
2092
|
),
|
|
2905
2093
|
/* @__PURE__ */ jsx13(
|
|
@@ -2919,10 +2107,7 @@ function App(props) {
|
|
|
2919
2107
|
InputArea,
|
|
2920
2108
|
{
|
|
2921
2109
|
onSubmit: handleSubmit,
|
|
2922
|
-
onCancelQueue:
|
|
2923
|
-
setPendingPrompt(null);
|
|
2924
|
-
pendingPromptRef.current = null;
|
|
2925
|
-
},
|
|
2110
|
+
onCancelQueue: handleCancelQueue,
|
|
2926
2111
|
isDisabled: !!permissionRequest || showPluginTUI || isThinking && !!pendingPrompt,
|
|
2927
2112
|
isAborting,
|
|
2928
2113
|
pendingPrompt,
|
|
@@ -2968,9 +2153,9 @@ function renderApp(options) {
|
|
|
2968
2153
|
|
|
2969
2154
|
// src/cli.ts
|
|
2970
2155
|
function checkSettingsFile(filePath) {
|
|
2971
|
-
if (!
|
|
2156
|
+
if (!existsSync2(filePath)) return "missing";
|
|
2972
2157
|
try {
|
|
2973
|
-
const raw =
|
|
2158
|
+
const raw = readFileSync2(filePath, "utf8").trim();
|
|
2974
2159
|
if (raw.length === 0) return "incomplete";
|
|
2975
2160
|
const parsed = JSON.parse(raw);
|
|
2976
2161
|
const provider = parsed.provider;
|
|
@@ -2987,7 +2172,7 @@ function readVersion() {
|
|
|
2987
2172
|
const candidates = [join5(dir, "..", "..", "package.json"), join5(dir, "..", "package.json")];
|
|
2988
2173
|
for (const pkgPath of candidates) {
|
|
2989
2174
|
try {
|
|
2990
|
-
const raw =
|
|
2175
|
+
const raw = readFileSync2(pkgPath, "utf-8");
|
|
2991
2176
|
const pkg = JSON.parse(raw);
|
|
2992
2177
|
if (pkg.version !== void 0 && pkg.name !== void 0) {
|
|
2993
2178
|
return pkg.version;
|
|
@@ -3133,12 +2318,12 @@ async function startCli() {
|
|
|
3133
2318
|
process.exit(1);
|
|
3134
2319
|
}
|
|
3135
2320
|
const terminal = new PrintTerminal();
|
|
3136
|
-
const paths =
|
|
3137
|
-
const session =
|
|
2321
|
+
const paths = projectPaths(cwd);
|
|
2322
|
+
const session = createSession({
|
|
3138
2323
|
config,
|
|
3139
2324
|
context,
|
|
3140
2325
|
terminal,
|
|
3141
|
-
sessionLogger: new
|
|
2326
|
+
sessionLogger: new FileSessionLogger(paths.logs),
|
|
3142
2327
|
projectInfo,
|
|
3143
2328
|
permissionMode: args.permissionMode,
|
|
3144
2329
|
promptForApproval
|