@robota-sdk/agent-cli 3.0.0-beta.40 → 3.0.0-beta.42
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 +133 -112
- package/dist/node/bin.js +1 -1
- package/dist/node/{chunk-CRPNSO52.js → chunk-SYGOHAAL.js} +489 -1298
- package/dist/node/index.cjs +516 -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";
|
|
@@ -793,516 +235,61 @@ async function preprocessShellCommands(content) {
|
|
|
793
235
|
output = execSync(command, {
|
|
794
236
|
timeout: 5e3,
|
|
795
237
|
encoding: "utf-8",
|
|
796
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
797
|
-
}).trimEnd();
|
|
798
|
-
} catch {
|
|
799
|
-
output = "";
|
|
800
|
-
}
|
|
801
|
-
result = result.replace(full, output);
|
|
802
|
-
}
|
|
803
|
-
return result;
|
|
804
|
-
}
|
|
805
|
-
async function buildSkillPrompt(input, registry, context) {
|
|
806
|
-
const parts = input.slice(1).split(/\s+/);
|
|
807
|
-
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
808
|
-
const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
809
|
-
if (!skillCmd) return null;
|
|
810
|
-
const args = parts.slice(1).join(" ").trim();
|
|
811
|
-
const userInstruction = args || skillCmd.description;
|
|
812
|
-
if (skillCmd.skillContent) {
|
|
813
|
-
let processed = await preprocessShellCommands(skillCmd.skillContent);
|
|
814
|
-
processed = substituteVariables(processed, args, context);
|
|
815
|
-
return `<skill name="${cmd}">
|
|
816
|
-
${processed}
|
|
817
|
-
</skill>
|
|
818
|
-
|
|
819
|
-
Execute the "${cmd}" skill: ${userInstruction}`;
|
|
820
|
-
}
|
|
821
|
-
return `Use the "${cmd}" skill: ${userInstruction}`;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// src/commands/skill-executor.ts
|
|
825
|
-
function buildProcessedContent(skill, args, context) {
|
|
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
|
-
}
|
|
238
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
239
|
+
}).trimEnd();
|
|
240
|
+
} catch {
|
|
241
|
+
output = "";
|
|
1249
242
|
}
|
|
1250
|
-
|
|
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);
|
|
243
|
+
result = result.replace(full, output);
|
|
1260
244
|
}
|
|
1261
|
-
|
|
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>
|
|
1262
260
|
|
|
1263
|
-
|
|
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;
|
|
261
|
+
Execute the "${cmd}" skill: ${userInstruction}`;
|
|
1294
262
|
}
|
|
1295
|
-
}
|
|
263
|
+
return `Use the "${cmd}" skill: ${userInstruction}`;
|
|
264
|
+
}
|
|
1296
265
|
|
|
1297
|
-
// src/ui/hooks/
|
|
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,284 @@ 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
|
+
streamBuf = "";
|
|
439
|
+
setStreamingText("");
|
|
440
|
+
setActiveTools([]);
|
|
441
|
+
} else {
|
|
442
|
+
setIsAborting(false);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
const onComplete = (result) => {
|
|
446
|
+
setContextState({
|
|
447
|
+
percentage: result.contextState.usedPercentage,
|
|
448
|
+
usedTokens: result.contextState.usedTokens,
|
|
449
|
+
maxTokens: result.contextState.maxTokens
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
const onInterrupted = () => {
|
|
453
|
+
};
|
|
454
|
+
const onError = () => {
|
|
455
|
+
};
|
|
456
|
+
interactiveSession.on("text_delta", onTextDelta);
|
|
457
|
+
interactiveSession.on("tool_start", onToolStart);
|
|
458
|
+
interactiveSession.on("tool_end", onToolEnd);
|
|
459
|
+
interactiveSession.on("thinking", onThinking);
|
|
460
|
+
interactiveSession.on("complete", onComplete);
|
|
461
|
+
interactiveSession.on("interrupted", onInterrupted);
|
|
462
|
+
interactiveSession.on("error", onError);
|
|
463
|
+
return () => {
|
|
464
|
+
interactiveSession.off("text_delta", onTextDelta);
|
|
465
|
+
interactiveSession.off("tool_start", onToolStart);
|
|
466
|
+
interactiveSession.off("tool_end", onToolEnd);
|
|
467
|
+
interactiveSession.off("thinking", onThinking);
|
|
468
|
+
interactiveSession.off("complete", onComplete);
|
|
469
|
+
interactiveSession.off("interrupted", onInterrupted);
|
|
470
|
+
interactiveSession.off("error", onError);
|
|
471
|
+
};
|
|
472
|
+
}, [interactiveSession]);
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
if (!isThinking) {
|
|
475
|
+
const sessionMessages = interactiveSession.getMessages();
|
|
476
|
+
if (sessionMessages.length > 0) {
|
|
477
|
+
setMessages(
|
|
478
|
+
sessionMessages.length > MAX_RENDERED_MESSAGES ? sessionMessages.slice(-MAX_RENDERED_MESSAGES) : [...sessionMessages]
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
setPendingPrompt(interactiveSession.getPendingPrompt());
|
|
1365
482
|
}
|
|
1366
|
-
|
|
483
|
+
}, [isThinking, interactiveSession]);
|
|
484
|
+
const handleSubmit = useCallback(
|
|
485
|
+
async (input) => {
|
|
486
|
+
if (input.startsWith("/")) {
|
|
487
|
+
const parts = input.slice(1).split(/\s+/);
|
|
488
|
+
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
489
|
+
const args = parts.slice(1).join(" ");
|
|
490
|
+
const result = await commandExecutor.execute(cmd, interactiveSession, args);
|
|
491
|
+
if (result) {
|
|
492
|
+
addMessage(createSystemMessage(result.message));
|
|
493
|
+
const effects = interactiveSession;
|
|
494
|
+
if (result.data?.modelId) {
|
|
495
|
+
effects._pendingModelId = result.data.modelId;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (result.data?.language) {
|
|
499
|
+
effects._pendingLanguage = result.data.language;
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (result.data?.resetRequested) {
|
|
503
|
+
effects._resetRequested = true;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const ctx = interactiveSession.getContextState();
|
|
507
|
+
setContextState({
|
|
508
|
+
percentage: ctx.usedPercentage,
|
|
509
|
+
usedTokens: ctx.usedTokens,
|
|
510
|
+
maxTokens: ctx.maxTokens
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
515
|
+
if (skillCmd) {
|
|
516
|
+
addMessage(createSystemMessage(`Invoking ${skillCmd.source}: ${cmd}`));
|
|
517
|
+
const prompt = await buildSkillPrompt(input, registry);
|
|
518
|
+
if (prompt) {
|
|
519
|
+
const qualifiedName = registry.resolveQualifiedName(cmd);
|
|
520
|
+
const hookInput = qualifiedName ? `/${qualifiedName}${input.slice(1 + cmd.length)}` : input;
|
|
521
|
+
await interactiveSession.submit(prompt, input, hookInput);
|
|
522
|
+
setPendingPrompt(interactiveSession.getPendingPrompt());
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (cmd === "exit") {
|
|
527
|
+
interactiveSession._exitRequested = true;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (cmd === "plugin") {
|
|
531
|
+
interactiveSession._triggerPluginTUI = true;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
addMessage(createSystemMessage(`Unknown command "/${cmd}". Type /help for help.`));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
await interactiveSession.submit(input);
|
|
538
|
+
setPendingPrompt(interactiveSession.getPendingPrompt());
|
|
539
|
+
},
|
|
540
|
+
[interactiveSession, commandExecutor, registry, addMessage]
|
|
541
|
+
);
|
|
542
|
+
const handleAbort = useCallback(() => {
|
|
543
|
+
setIsAborting(true);
|
|
544
|
+
interactiveSession.abort();
|
|
545
|
+
}, [interactiveSession]);
|
|
546
|
+
const handleCancelQueue = useCallback(() => {
|
|
547
|
+
interactiveSession.cancelQueue();
|
|
548
|
+
setPendingPrompt(null);
|
|
549
|
+
}, [interactiveSession]);
|
|
550
|
+
if (contextState.maxTokens === 0) {
|
|
551
|
+
const ctx = interactiveSession.getContextState();
|
|
552
|
+
setContextState({
|
|
553
|
+
percentage: ctx.usedPercentage,
|
|
554
|
+
usedTokens: ctx.usedTokens,
|
|
555
|
+
maxTokens: ctx.maxTokens
|
|
556
|
+
});
|
|
1367
557
|
}
|
|
1368
|
-
return
|
|
558
|
+
return {
|
|
559
|
+
interactiveSession,
|
|
560
|
+
registry,
|
|
561
|
+
commandExecutor,
|
|
562
|
+
pluginHooks: stateRef.current.pluginHooks,
|
|
563
|
+
messages,
|
|
564
|
+
addMessage,
|
|
565
|
+
setMessages,
|
|
566
|
+
streamingText,
|
|
567
|
+
activeTools,
|
|
568
|
+
isThinking,
|
|
569
|
+
isAborting,
|
|
570
|
+
pendingPrompt,
|
|
571
|
+
permissionRequest,
|
|
572
|
+
contextState,
|
|
573
|
+
handleSubmit,
|
|
574
|
+
handleAbort,
|
|
575
|
+
handleCancelQueue
|
|
576
|
+
};
|
|
1369
577
|
}
|
|
1370
578
|
|
|
1371
579
|
// src/ui/hooks/usePluginCallbacks.ts
|
|
1372
580
|
import { useMemo } from "react";
|
|
1373
|
-
import { homedir as
|
|
581
|
+
import { homedir as homedir2 } from "os";
|
|
1374
582
|
import { join as join4 } from "path";
|
|
1375
583
|
import {
|
|
1376
584
|
PluginSettingsStore,
|
|
@@ -1380,7 +588,7 @@ import {
|
|
|
1380
588
|
} from "@robota-sdk/agent-sdk";
|
|
1381
589
|
function usePluginCallbacks(cwd) {
|
|
1382
590
|
return useMemo(() => {
|
|
1383
|
-
const home =
|
|
591
|
+
const home = homedir2();
|
|
1384
592
|
const pluginsDir = join4(home, ".robota", "plugins");
|
|
1385
593
|
const userSettingsPath = join4(home, ".robota", "settings.json");
|
|
1386
594
|
const settingsStore = new PluginSettingsStore(userSettingsPath);
|
|
@@ -1646,7 +854,7 @@ function MessageList({ messages }) {
|
|
|
1646
854
|
|
|
1647
855
|
// src/ui/StatusBar.tsx
|
|
1648
856
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
1649
|
-
import { formatTokenCount
|
|
857
|
+
import { formatTokenCount } from "@robota-sdk/agent-core";
|
|
1650
858
|
import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1651
859
|
var CONTEXT_YELLOW_THRESHOLD = 70;
|
|
1652
860
|
var CONTEXT_RED_THRESHOLD = 90;
|
|
@@ -1686,9 +894,9 @@ function StatusBar({
|
|
|
1686
894
|
"Context: ",
|
|
1687
895
|
Math.round(contextPercentage),
|
|
1688
896
|
"% (",
|
|
1689
|
-
|
|
897
|
+
formatTokenCount(contextUsedTokens),
|
|
1690
898
|
"/",
|
|
1691
|
-
|
|
899
|
+
formatTokenCount(contextMaxTokens),
|
|
1692
900
|
")"
|
|
1693
901
|
] })
|
|
1694
902
|
] }),
|
|
@@ -1705,11 +913,11 @@ function StatusBar({
|
|
|
1705
913
|
}
|
|
1706
914
|
|
|
1707
915
|
// src/ui/InputArea.tsx
|
|
1708
|
-
import React5, { useState as
|
|
916
|
+
import React5, { useState as useState4, useCallback as useCallback2, useMemo as useMemo2, useRef as useRef3 } from "react";
|
|
1709
917
|
import { Box as Box5, Text as Text7, useInput as useInput2, useStdout } from "ink";
|
|
1710
918
|
|
|
1711
919
|
// src/ui/CjkTextInput.tsx
|
|
1712
|
-
import { useRef as
|
|
920
|
+
import { useRef as useRef2, useState as useState2 } from "react";
|
|
1713
921
|
import { Text as Text4, useInput } from "ink";
|
|
1714
922
|
import chalk from "chalk";
|
|
1715
923
|
import stringWidth from "string-width";
|
|
@@ -1755,11 +963,11 @@ function CjkTextInput({
|
|
|
1755
963
|
showCursor = true,
|
|
1756
964
|
availableWidth
|
|
1757
965
|
}) {
|
|
1758
|
-
const valueRef =
|
|
1759
|
-
const cursorRef =
|
|
1760
|
-
const [, forceRender] =
|
|
1761
|
-
const isPastingRef =
|
|
1762
|
-
const pasteBufferRef =
|
|
966
|
+
const valueRef = useRef2(value);
|
|
967
|
+
const cursorRef = useRef2(value.length);
|
|
968
|
+
const [, forceRender] = useState2(0);
|
|
969
|
+
const isPastingRef = useRef2(false);
|
|
970
|
+
const pasteBufferRef = useRef2("");
|
|
1763
971
|
if (value !== valueRef.current) {
|
|
1764
972
|
valueRef.current = value;
|
|
1765
973
|
if (cursorRef.current > value.length) {
|
|
@@ -1888,15 +1096,15 @@ function renderWithCursor(value, cursorOffset, placeholder, showCursor) {
|
|
|
1888
1096
|
}
|
|
1889
1097
|
|
|
1890
1098
|
// src/ui/WaveText.tsx
|
|
1891
|
-
import { useState as
|
|
1099
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
1892
1100
|
import { Text as Text5 } from "ink";
|
|
1893
1101
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1894
1102
|
var WAVE_COLORS = ["#666666", "#888888", "#aaaaaa", "#888888"];
|
|
1895
1103
|
var INTERVAL_MS = 400;
|
|
1896
1104
|
var CHARS_PER_GROUP = 4;
|
|
1897
1105
|
function WaveText({ text }) {
|
|
1898
|
-
const [tick, setTick] =
|
|
1899
|
-
|
|
1106
|
+
const [tick, setTick] = useState3(0);
|
|
1107
|
+
useEffect2(() => {
|
|
1900
1108
|
const timer = setInterval(() => {
|
|
1901
1109
|
setTick((prev) => prev + 1);
|
|
1902
1110
|
}, INTERVAL_MS);
|
|
@@ -1968,8 +1176,8 @@ function parseSlashInput(value) {
|
|
|
1968
1176
|
return { isSlash: true, parentCommand: parent, filter: rest };
|
|
1969
1177
|
}
|
|
1970
1178
|
function useAutocomplete(value, registry) {
|
|
1971
|
-
const [selectedIndex, setSelectedIndex] =
|
|
1972
|
-
const [dismissed, setDismissed] =
|
|
1179
|
+
const [selectedIndex, setSelectedIndex] = useState4(0);
|
|
1180
|
+
const [dismissed, setDismissed] = useState4(false);
|
|
1973
1181
|
const prevValueRef = React5.useRef(value);
|
|
1974
1182
|
if (prevValueRef.current !== value) {
|
|
1975
1183
|
prevValueRef.current = value;
|
|
@@ -2022,12 +1230,12 @@ function InputArea({
|
|
|
2022
1230
|
pendingPrompt,
|
|
2023
1231
|
registry
|
|
2024
1232
|
}) {
|
|
2025
|
-
const [value, setValue] =
|
|
2026
|
-
const pasteStore =
|
|
1233
|
+
const [value, setValue] = useState4("");
|
|
1234
|
+
const pasteStore = useRef3(/* @__PURE__ */ new Map());
|
|
2027
1235
|
const { stdout } = useStdout();
|
|
2028
1236
|
const terminalColumns = stdout?.columns ?? 80;
|
|
2029
1237
|
const availableWidth = Math.max(1, terminalColumns - INPUT_AREA_OVERHEAD);
|
|
2030
|
-
const pasteIdRef =
|
|
1238
|
+
const pasteIdRef = useRef3(0);
|
|
2031
1239
|
const {
|
|
2032
1240
|
showPopup,
|
|
2033
1241
|
filteredCommands,
|
|
@@ -2036,7 +1244,7 @@ function InputArea({
|
|
|
2036
1244
|
isSubcommandMode,
|
|
2037
1245
|
setShowPopup
|
|
2038
1246
|
} = useAutocomplete(value, registry);
|
|
2039
|
-
const handlePaste =
|
|
1247
|
+
const handlePaste = useCallback2((text) => {
|
|
2040
1248
|
pasteIdRef.current += 1;
|
|
2041
1249
|
const id = pasteIdRef.current;
|
|
2042
1250
|
pasteStore.current.set(id, text);
|
|
@@ -2044,7 +1252,7 @@ function InputArea({
|
|
|
2044
1252
|
const label = `[Pasted text #${id} +${lineCount} lines]`;
|
|
2045
1253
|
setValue((prev) => prev ? `${prev} ${label}` : label);
|
|
2046
1254
|
}, []);
|
|
2047
|
-
const handleSubmit =
|
|
1255
|
+
const handleSubmit = useCallback2(
|
|
2048
1256
|
(text) => {
|
|
2049
1257
|
const trimmed = text.trim();
|
|
2050
1258
|
if (trimmed.length === 0) return;
|
|
@@ -2060,7 +1268,7 @@ function InputArea({
|
|
|
2060
1268
|
},
|
|
2061
1269
|
[showPopup, filteredCommands, selectedIndex, onSubmit]
|
|
2062
1270
|
);
|
|
2063
|
-
const selectCommand =
|
|
1271
|
+
const selectCommand = useCallback2(
|
|
2064
1272
|
(cmd) => {
|
|
2065
1273
|
const parsed = parseSlashInput(value);
|
|
2066
1274
|
if (parsed.parentCommand) {
|
|
@@ -2145,7 +1353,7 @@ function InputArea({
|
|
|
2145
1353
|
}
|
|
2146
1354
|
|
|
2147
1355
|
// src/ui/ConfirmPrompt.tsx
|
|
2148
|
-
import { useState as
|
|
1356
|
+
import { useState as useState5, useCallback as useCallback3, useRef as useRef4 } from "react";
|
|
2149
1357
|
import { Box as Box6, Text as Text8, useInput as useInput3 } from "ink";
|
|
2150
1358
|
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2151
1359
|
function ConfirmPrompt({
|
|
@@ -2153,9 +1361,9 @@ function ConfirmPrompt({
|
|
|
2153
1361
|
options = ["Yes", "No"],
|
|
2154
1362
|
onSelect
|
|
2155
1363
|
}) {
|
|
2156
|
-
const [selected, setSelected] =
|
|
2157
|
-
const resolvedRef =
|
|
2158
|
-
const doSelect =
|
|
1364
|
+
const [selected, setSelected] = useState5(0);
|
|
1365
|
+
const resolvedRef = useRef4(false);
|
|
1366
|
+
const doSelect = useCallback3(
|
|
2159
1367
|
(index) => {
|
|
2160
1368
|
if (resolvedRef.current) return;
|
|
2161
1369
|
resolvedRef.current = true;
|
|
@@ -2295,10 +1503,10 @@ function StreamingIndicator({ text, activeTools }) {
|
|
|
2295
1503
|
}
|
|
2296
1504
|
|
|
2297
1505
|
// src/ui/PluginTUI.tsx
|
|
2298
|
-
import { useState as
|
|
1506
|
+
import { useState as useState8, useEffect as useEffect3, useCallback as useCallback6 } from "react";
|
|
2299
1507
|
|
|
2300
1508
|
// src/ui/MenuSelect.tsx
|
|
2301
|
-
import { useState as
|
|
1509
|
+
import { useState as useState6, useCallback as useCallback4, useRef as useRef5 } from "react";
|
|
2302
1510
|
import { Box as Box9, Text as Text11, useInput as useInput5 } from "ink";
|
|
2303
1511
|
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2304
1512
|
function MenuSelect({
|
|
@@ -2309,10 +1517,10 @@ function MenuSelect({
|
|
|
2309
1517
|
loading,
|
|
2310
1518
|
error
|
|
2311
1519
|
}) {
|
|
2312
|
-
const [selected, setSelected] =
|
|
2313
|
-
const selectedRef =
|
|
2314
|
-
const resolvedRef =
|
|
2315
|
-
const doSelect =
|
|
1520
|
+
const [selected, setSelected] = useState6(0);
|
|
1521
|
+
const selectedRef = useRef5(0);
|
|
1522
|
+
const resolvedRef = useRef5(false);
|
|
1523
|
+
const doSelect = useCallback4(
|
|
2316
1524
|
(index) => {
|
|
2317
1525
|
if (resolvedRef.current || items.length === 0) return;
|
|
2318
1526
|
resolvedRef.current = true;
|
|
@@ -2362,7 +1570,7 @@ function MenuSelect({
|
|
|
2362
1570
|
}
|
|
2363
1571
|
|
|
2364
1572
|
// src/ui/TextPrompt.tsx
|
|
2365
|
-
import { useState as
|
|
1573
|
+
import { useState as useState7, useRef as useRef6, useCallback as useCallback5 } from "react";
|
|
2366
1574
|
import { Box as Box10, Text as Text12, useInput as useInput6 } from "ink";
|
|
2367
1575
|
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2368
1576
|
function TextPrompt({
|
|
@@ -2372,11 +1580,11 @@ function TextPrompt({
|
|
|
2372
1580
|
onCancel,
|
|
2373
1581
|
validate
|
|
2374
1582
|
}) {
|
|
2375
|
-
const [value, setValue] =
|
|
2376
|
-
const [error, setError] =
|
|
2377
|
-
const resolvedRef =
|
|
2378
|
-
const valueRef =
|
|
2379
|
-
const handleSubmit =
|
|
1583
|
+
const [value, setValue] = useState7("");
|
|
1584
|
+
const [error, setError] = useState7();
|
|
1585
|
+
const resolvedRef = useRef6(false);
|
|
1586
|
+
const valueRef = useRef6("");
|
|
1587
|
+
const handleSubmit = useCallback5(() => {
|
|
2380
1588
|
if (resolvedRef.current) return;
|
|
2381
1589
|
const trimmed = valueRef.current.trim();
|
|
2382
1590
|
if (!trimmed) return;
|
|
@@ -2520,19 +1728,19 @@ function handleInstalledActionSelect(value, pluginId, callbacks, nav) {
|
|
|
2520
1728
|
// src/ui/PluginTUI.tsx
|
|
2521
1729
|
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
2522
1730
|
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] =
|
|
1731
|
+
const [stack, setStack] = useState8([{ screen: "main" }]);
|
|
1732
|
+
const [items, setItems] = useState8([]);
|
|
1733
|
+
const [loading, setLoading] = useState8(false);
|
|
1734
|
+
const [error, setError] = useState8();
|
|
1735
|
+
const [confirm, setConfirm] = useState8();
|
|
1736
|
+
const [refreshCounter, setRefreshCounter] = useState8(0);
|
|
2529
1737
|
const current = stack[stack.length - 1] ?? { screen: "main" };
|
|
2530
|
-
const push =
|
|
1738
|
+
const push = useCallback6((state) => {
|
|
2531
1739
|
setStack((prev) => [...prev, state]);
|
|
2532
1740
|
setItems([]);
|
|
2533
1741
|
setError(void 0);
|
|
2534
1742
|
}, []);
|
|
2535
|
-
const pop =
|
|
1743
|
+
const pop = useCallback6(() => {
|
|
2536
1744
|
setStack((prev) => {
|
|
2537
1745
|
if (prev.length <= 1) {
|
|
2538
1746
|
onClose();
|
|
@@ -2543,7 +1751,7 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2543
1751
|
setItems([]);
|
|
2544
1752
|
setError(void 0);
|
|
2545
1753
|
}, [onClose]);
|
|
2546
|
-
const popN =
|
|
1754
|
+
const popN = useCallback6(
|
|
2547
1755
|
(n) => {
|
|
2548
1756
|
setStack((prev) => {
|
|
2549
1757
|
const next = prev.slice(0, Math.max(1, prev.length - n));
|
|
@@ -2558,18 +1766,18 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2558
1766
|
},
|
|
2559
1767
|
[onClose]
|
|
2560
1768
|
);
|
|
2561
|
-
const notify =
|
|
1769
|
+
const notify = useCallback6(
|
|
2562
1770
|
(content) => {
|
|
2563
1771
|
addMessage?.({ role: "system", content });
|
|
2564
1772
|
},
|
|
2565
1773
|
[addMessage]
|
|
2566
1774
|
);
|
|
2567
|
-
const refresh =
|
|
1775
|
+
const refresh = useCallback6(() => {
|
|
2568
1776
|
setItems([]);
|
|
2569
1777
|
setRefreshCounter((c) => c + 1);
|
|
2570
1778
|
}, []);
|
|
2571
1779
|
const nav = { push, pop, popN, notify, setConfirm, refresh };
|
|
2572
|
-
|
|
1780
|
+
useEffect3(() => {
|
|
2573
1781
|
const screen2 = current.screen;
|
|
2574
1782
|
if (screen2 === "marketplace-list") {
|
|
2575
1783
|
setLoading(true);
|
|
@@ -2619,7 +1827,7 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2619
1827
|
});
|
|
2620
1828
|
}
|
|
2621
1829
|
}, [stack.length, current.screen, current.context?.marketplace, callbacks, refreshCounter]);
|
|
2622
|
-
const handleSelect =
|
|
1830
|
+
const handleSelect = useCallback6(
|
|
2623
1831
|
(value) => {
|
|
2624
1832
|
const screen2 = current.screen;
|
|
2625
1833
|
const ctx = current.context;
|
|
@@ -2637,7 +1845,7 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2637
1845
|
},
|
|
2638
1846
|
[current, items, callbacks, push, pop, popN, notify, setConfirm, refresh]
|
|
2639
1847
|
);
|
|
2640
|
-
const handleTextSubmit =
|
|
1848
|
+
const handleTextSubmit = useCallback6(
|
|
2641
1849
|
(value) => {
|
|
2642
1850
|
if (current.screen === "marketplace-add") {
|
|
2643
1851
|
callbacks.marketplaceAdd(value).then((name) => {
|
|
@@ -2747,104 +1955,90 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
|
2747
1955
|
|
|
2748
1956
|
// src/ui/App.tsx
|
|
2749
1957
|
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
|
-
}
|
|
1958
|
+
var EXIT_DELAY_MS = 500;
|
|
2767
1959
|
function App(props) {
|
|
2768
1960
|
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,
|
|
1961
|
+
const cwd = props.cwd ?? process.cwd();
|
|
1962
|
+
const {
|
|
1963
|
+
interactiveSession,
|
|
2800
1964
|
registry,
|
|
2801
|
-
|
|
2802
|
-
setPendingModelId,
|
|
2803
|
-
pluginCallbacks,
|
|
2804
|
-
setShowPluginTUI
|
|
2805
|
-
);
|
|
2806
|
-
const executePrompt = useSubmitHandler(
|
|
2807
|
-
session,
|
|
1965
|
+
messages,
|
|
2808
1966
|
addMessage,
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
1967
|
+
streamingText,
|
|
1968
|
+
activeTools,
|
|
1969
|
+
isThinking,
|
|
1970
|
+
isAborting,
|
|
1971
|
+
pendingPrompt,
|
|
1972
|
+
permissionRequest,
|
|
1973
|
+
contextState,
|
|
1974
|
+
handleSubmit: baseHandleSubmit,
|
|
1975
|
+
handleAbort,
|
|
1976
|
+
handleCancelQueue
|
|
1977
|
+
} = useInteractiveSession({
|
|
1978
|
+
config: props.config,
|
|
1979
|
+
context: props.context,
|
|
1980
|
+
projectInfo: props.projectInfo,
|
|
1981
|
+
sessionStore: props.sessionStore,
|
|
1982
|
+
permissionMode: props.permissionMode,
|
|
1983
|
+
maxTurns: props.maxTurns,
|
|
1984
|
+
cwd
|
|
1985
|
+
});
|
|
1986
|
+
const pluginCallbacks = usePluginCallbacks(cwd);
|
|
1987
|
+
const [pendingModelId, setPendingModelId] = useState9(null);
|
|
1988
|
+
const pendingModelChangeRef = useRef7(null);
|
|
1989
|
+
const [showPluginTUI, setShowPluginTUI] = useState9(false);
|
|
1990
|
+
const handleSubmit = async (input) => {
|
|
1991
|
+
await baseHandleSubmit(input);
|
|
1992
|
+
const sideEffects = interactiveSession;
|
|
1993
|
+
if (sideEffects._pendingModelId) {
|
|
1994
|
+
const modelId = sideEffects._pendingModelId;
|
|
1995
|
+
delete sideEffects._pendingModelId;
|
|
1996
|
+
pendingModelChangeRef.current = modelId;
|
|
1997
|
+
setPendingModelId(modelId);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (sideEffects._pendingLanguage) {
|
|
2001
|
+
const lang = sideEffects._pendingLanguage;
|
|
2002
|
+
delete sideEffects._pendingLanguage;
|
|
2003
|
+
const settingsPath = getUserSettingsPath();
|
|
2004
|
+
const settings = readSettings(settingsPath);
|
|
2005
|
+
settings.language = lang;
|
|
2006
|
+
writeSettings(settingsPath, settings);
|
|
2007
|
+
addMessage(createSystemMessage2(`Language set to "${lang}". Restarting...`));
|
|
2008
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (sideEffects._resetRequested) {
|
|
2012
|
+
delete sideEffects._resetRequested;
|
|
2013
|
+
const settingsPath = getUserSettingsPath();
|
|
2014
|
+
if (deleteSettings(settingsPath)) {
|
|
2015
|
+
addMessage(createSystemMessage2(`Deleted ${settingsPath}. Exiting...`));
|
|
2016
|
+
} else {
|
|
2017
|
+
addMessage(createSystemMessage2("No user settings found."));
|
|
2821
2018
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2019
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
if (sideEffects._exitRequested) {
|
|
2023
|
+
delete sideEffects._exitRequested;
|
|
2024
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
if (sideEffects._triggerPluginTUI) {
|
|
2028
|
+
delete sideEffects._triggerPluginTUI;
|
|
2029
|
+
setShowPluginTUI(true);
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2826
2033
|
useInput7(
|
|
2827
2034
|
(_input, key) => {
|
|
2828
2035
|
if (key.escape && isThinking) {
|
|
2829
|
-
|
|
2830
|
-
setPendingPrompt(null);
|
|
2831
|
-
pendingPromptRef.current = null;
|
|
2832
|
-
session.abort();
|
|
2036
|
+
handleAbort();
|
|
2833
2037
|
}
|
|
2834
2038
|
},
|
|
2835
2039
|
{ isActive: !permissionRequest && !showPluginTUI }
|
|
2836
2040
|
);
|
|
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]);
|
|
2041
|
+
const session = interactiveSession.getSession();
|
|
2848
2042
|
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
|
|
2849
2043
|
/* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
|
|
2850
2044
|
/* @__PURE__ */ jsx13(Text13, { color: "cyan", bold: true, children: `
|
|
@@ -2861,7 +2055,7 @@ function App(props) {
|
|
|
2861
2055
|
] }),
|
|
2862
2056
|
/* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
2863
2057
|
/* @__PURE__ */ jsx13(MessageList, { messages }),
|
|
2864
|
-
isThinking && /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx13(StreamingIndicator, { text: streamingText, activeTools }) })
|
|
2058
|
+
(isThinking || activeTools.length > 0) && /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx13(StreamingIndicator, { text: streamingText, activeTools }) })
|
|
2865
2059
|
] }),
|
|
2866
2060
|
permissionRequest && /* @__PURE__ */ jsx13(PermissionPrompt, { request: permissionRequest }),
|
|
2867
2061
|
pendingModelId && /* @__PURE__ */ jsx13(
|
|
@@ -2876,20 +2070,20 @@ function App(props) {
|
|
|
2876
2070
|
const settingsPath = getUserSettingsPath();
|
|
2877
2071
|
updateModelInSettings(settingsPath, pendingModelId);
|
|
2878
2072
|
addMessage(
|
|
2879
|
-
|
|
2073
|
+
createSystemMessage2(
|
|
2880
2074
|
`Model changed to ${getModelName(pendingModelId)}. Restarting...`
|
|
2881
2075
|
)
|
|
2882
2076
|
);
|
|
2883
|
-
setTimeout(() => exit(),
|
|
2077
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
2884
2078
|
} catch (err) {
|
|
2885
2079
|
addMessage(
|
|
2886
|
-
|
|
2080
|
+
createSystemMessage2(
|
|
2887
2081
|
`Failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2888
2082
|
)
|
|
2889
2083
|
);
|
|
2890
2084
|
}
|
|
2891
2085
|
} else {
|
|
2892
|
-
addMessage(
|
|
2086
|
+
addMessage(createSystemMessage2("Model change cancelled."));
|
|
2893
2087
|
}
|
|
2894
2088
|
}
|
|
2895
2089
|
}
|
|
@@ -2899,7 +2093,7 @@ function App(props) {
|
|
|
2899
2093
|
{
|
|
2900
2094
|
callbacks: pluginCallbacks,
|
|
2901
2095
|
onClose: () => setShowPluginTUI(false),
|
|
2902
|
-
addMessage: (msg) => addMessage(
|
|
2096
|
+
addMessage: (msg) => addMessage(createSystemMessage2(msg.content))
|
|
2903
2097
|
}
|
|
2904
2098
|
),
|
|
2905
2099
|
/* @__PURE__ */ jsx13(
|
|
@@ -2919,10 +2113,7 @@ function App(props) {
|
|
|
2919
2113
|
InputArea,
|
|
2920
2114
|
{
|
|
2921
2115
|
onSubmit: handleSubmit,
|
|
2922
|
-
onCancelQueue:
|
|
2923
|
-
setPendingPrompt(null);
|
|
2924
|
-
pendingPromptRef.current = null;
|
|
2925
|
-
},
|
|
2116
|
+
onCancelQueue: handleCancelQueue,
|
|
2926
2117
|
isDisabled: !!permissionRequest || showPluginTUI || isThinking && !!pendingPrompt,
|
|
2927
2118
|
isAborting,
|
|
2928
2119
|
pendingPrompt,
|
|
@@ -2968,9 +2159,9 @@ function renderApp(options) {
|
|
|
2968
2159
|
|
|
2969
2160
|
// src/cli.ts
|
|
2970
2161
|
function checkSettingsFile(filePath) {
|
|
2971
|
-
if (!
|
|
2162
|
+
if (!existsSync2(filePath)) return "missing";
|
|
2972
2163
|
try {
|
|
2973
|
-
const raw =
|
|
2164
|
+
const raw = readFileSync2(filePath, "utf8").trim();
|
|
2974
2165
|
if (raw.length === 0) return "incomplete";
|
|
2975
2166
|
const parsed = JSON.parse(raw);
|
|
2976
2167
|
const provider = parsed.provider;
|
|
@@ -2987,7 +2178,7 @@ function readVersion() {
|
|
|
2987
2178
|
const candidates = [join5(dir, "..", "..", "package.json"), join5(dir, "..", "package.json")];
|
|
2988
2179
|
for (const pkgPath of candidates) {
|
|
2989
2180
|
try {
|
|
2990
|
-
const raw =
|
|
2181
|
+
const raw = readFileSync2(pkgPath, "utf-8");
|
|
2991
2182
|
const pkg = JSON.parse(raw);
|
|
2992
2183
|
if (pkg.version !== void 0 && pkg.name !== void 0) {
|
|
2993
2184
|
return pkg.version;
|
|
@@ -3133,12 +2324,12 @@ async function startCli() {
|
|
|
3133
2324
|
process.exit(1);
|
|
3134
2325
|
}
|
|
3135
2326
|
const terminal = new PrintTerminal();
|
|
3136
|
-
const paths =
|
|
3137
|
-
const session =
|
|
2327
|
+
const paths = projectPaths(cwd);
|
|
2328
|
+
const session = createSession({
|
|
3138
2329
|
config,
|
|
3139
2330
|
context,
|
|
3140
2331
|
terminal,
|
|
3141
|
-
sessionLogger: new
|
|
2332
|
+
sessionLogger: new FileSessionLogger(paths.logs),
|
|
3142
2333
|
projectInfo,
|
|
3143
2334
|
permissionMode: args.permissionMode,
|
|
3144
2335
|
promptForApproval
|