@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.
@@ -1,15 +1,15 @@
1
1
  // src/cli.ts
2
- import { readFileSync as readFileSync4, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
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 as createSession2,
9
+ createSession,
10
10
  SessionStore,
11
- FileSessionLogger as FileSessionLogger2,
12
- projectPaths as projectPaths2
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 useState10, useRef as useRef8, useEffect as useEffect3, useCallback as useCallback10 } from "react";
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/useSession.ts
158
- import { useState, useCallback, useRef } from "react";
159
- import { createSession, FileSessionLogger, projectPaths } from "@robota-sdk/agent-sdk";
160
-
161
- // src/utils/edit-diff.ts
162
- import { readFileSync as readFileSync2 } from "fs";
163
- var CONTEXT_LINES = 2;
164
- function generateDiffLines(oldStr, newStr, startLine = 1) {
165
- if (oldStr === newStr) return [];
166
- const lines = [];
167
- const oldLines = oldStr.split("\n");
168
- const newLines = newStr.split("\n");
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/slash-executor.ts
404
- var VALID_MODES2 = ["plan", "default", "acceptEdits", "bypassPermissions"];
405
- var HELP_TEXT = [
406
- "Available commands:",
407
- " /help \u2014 Show this help",
408
- " /clear \u2014 Clear conversation",
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
- // src/ui/hooks/useSlashCommands.ts
660
- var EXIT_DELAY_MS = 500;
661
- function useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId, pluginCallbacks, setShowPluginTUI) {
662
- return useCallback3(
663
- async (input) => {
664
- const parts = input.slice(1).split(/\s+/);
665
- const cmd = parts[0]?.toLowerCase() ?? "";
666
- const args = parts.slice(1).join(" ");
667
- const clearMessages = () => setMessages([]);
668
- const slashAddMessage = (msg) => {
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
- return result.handled;
691
- },
692
- [
693
- session,
694
- addMessage,
695
- setMessages,
696
- exit,
697
- registry,
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
- return summaries;
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
- 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);
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
- // 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;
261
+ Execute the "${cmd}" skill: ${userInstruction}`;
1294
262
  }
1295
- };
263
+ return `Use the "${cmd}" skill: ${userInstruction}`;
264
+ }
1296
265
 
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 = join3(dirname2(dirname2(plugin.pluginDir)), "data", plugin.manifest.name);
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 resolved2 = resolvePluginRoot(group, plugin.pluginDir);
1318
- if (typeof resolved2 === "object" && resolved2 !== null) {
1319
- resolved2.env = pluginEnv;
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 resolvePluginRoot(group, pluginDir) {
1329
- if (typeof group !== "object" || group === null) return group;
1330
- const obj = group;
1331
- if (Array.isArray(obj.hooks)) {
1332
- return {
1333
- ...obj,
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
- return group;
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
- function useCommandRegistry(cwd) {
1350
- const resultRef = useRef2(null);
1351
- if (resultRef.current === null) {
1352
- const registry = new CommandRegistry();
1353
- registry.addSource(new BuiltinCommandSource());
1354
- registry.addSource(new SkillCommandSource(cwd));
1355
- let pluginHooks = {};
1356
- const pluginsDir = join3(homedir2(), ".robota", "plugins");
1357
- const loader = new BundlePluginLoader(pluginsDir);
1358
- try {
1359
- const plugins = loader.loadPluginsSync();
1360
- if (plugins.length > 0) {
1361
- registry.addSource(new PluginCommandSource(plugins));
1362
- pluginHooks = mergePluginHooks(plugins);
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
- } catch {
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
- resultRef.current = { registry, pluginHooks };
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 resultRef.current;
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 homedir3 } from "os";
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 = homedir3();
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 as formatTokenCount2 } from "@robota-sdk/agent-core";
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
- formatTokenCount2(contextUsedTokens),
897
+ formatTokenCount(contextUsedTokens),
1690
898
  "/",
1691
- formatTokenCount2(contextMaxTokens),
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 useState5, useCallback as useCallback5, useMemo as useMemo2, useRef as useRef4 } from "react";
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 useRef3, useState as useState3 } from "react";
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 = useRef3(value);
1759
- const cursorRef = useRef3(value.length);
1760
- const [, forceRender] = useState3(0);
1761
- const isPastingRef = useRef3(false);
1762
- const pasteBufferRef = useRef3("");
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 useState4, useEffect } from "react";
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] = useState4(0);
1899
- useEffect(() => {
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] = useState5(0);
1972
- const [dismissed, setDismissed] = useState5(false);
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] = useState5("");
2026
- const pasteStore = useRef4(/* @__PURE__ */ new Map());
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 = useRef4(0);
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 = useCallback5((text) => {
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 = useCallback5(
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 = useCallback5(
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 useState6, useCallback as useCallback6, useRef as useRef5 } from "react";
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] = useState6(0);
2157
- const resolvedRef = useRef5(false);
2158
- const doSelect = useCallback6(
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 useState9, useEffect as useEffect2, useCallback as useCallback9 } from "react";
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 useState7, useCallback as useCallback7, useRef as useRef6 } from "react";
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] = useState7(0);
2313
- const selectedRef = useRef6(0);
2314
- const resolvedRef = useRef6(false);
2315
- const doSelect = useCallback7(
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 useState8, useRef as useRef7, useCallback as useCallback8 } from "react";
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] = useState8("");
2376
- const [error, setError] = useState8();
2377
- const resolvedRef = useRef7(false);
2378
- const valueRef = useRef7("");
2379
- const handleSubmit = useCallback8(() => {
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] = useState9([{ screen: "main" }]);
2524
- const [items, setItems] = useState9([]);
2525
- const [loading, setLoading] = useState9(false);
2526
- const [error, setError] = useState9();
2527
- const [confirm, setConfirm] = useState9();
2528
- const [refreshCounter, setRefreshCounter] = useState9(0);
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 = useCallback9((state) => {
1738
+ const push = useCallback6((state) => {
2531
1739
  setStack((prev) => [...prev, state]);
2532
1740
  setItems([]);
2533
1741
  setError(void 0);
2534
1742
  }, []);
2535
- const pop = useCallback9(() => {
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 = useCallback9(
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 = useCallback9(
1769
+ const notify = useCallback6(
2562
1770
  (content) => {
2563
1771
  addMessage?.({ role: "system", content });
2564
1772
  },
2565
1773
  [addMessage]
2566
1774
  );
2567
- const refresh = useCallback9(() => {
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
- useEffect2(() => {
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 = useCallback9(
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 = useCallback9(
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 EXIT_DELAY_MS2 = 500;
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 { registry, pluginHooks } = useCommandRegistry(props.cwd ?? process.cwd());
2770
- const configWithPluginHooks = {
2771
- ...props.config,
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
- pendingModelChangeRef,
2802
- setPendingModelId,
2803
- pluginCallbacks,
2804
- setShowPluginTUI
2805
- );
2806
- const executePrompt = useSubmitHandler(
2807
- session,
1965
+ messages,
2808
1966
  addMessage,
2809
- handleSlashCommand,
2810
- clearStreamingText,
2811
- setIsThinking,
2812
- setContextState,
2813
- registry
2814
- );
2815
- const handleSubmit = useCallback10(
2816
- async (input) => {
2817
- if (isThinking) {
2818
- setPendingPrompt(input);
2819
- pendingPromptRef.current = input;
2820
- return;
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
- await executePrompt(input);
2823
- },
2824
- [isThinking, executePrompt]
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
- setIsAborting(true);
2830
- setPendingPrompt(null);
2831
- pendingPromptRef.current = null;
2832
- session.abort();
2036
+ handleAbort();
2833
2037
  }
2834
2038
  },
2835
2039
  { isActive: !permissionRequest && !showPluginTUI }
2836
2040
  );
2837
- useEffect3(() => {
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
- createSystemMessage3(
2073
+ createSystemMessage2(
2880
2074
  `Model changed to ${getModelName(pendingModelId)}. Restarting...`
2881
2075
  )
2882
2076
  );
2883
- setTimeout(() => exit(), EXIT_DELAY_MS2);
2077
+ setTimeout(() => exit(), EXIT_DELAY_MS);
2884
2078
  } catch (err) {
2885
2079
  addMessage(
2886
- createSystemMessage3(
2080
+ createSystemMessage2(
2887
2081
  `Failed: ${err instanceof Error ? err.message : String(err)}`
2888
2082
  )
2889
2083
  );
2890
2084
  }
2891
2085
  } else {
2892
- addMessage(createSystemMessage3("Model change cancelled."));
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(createSystemMessage3(msg.content))
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 (!existsSync3(filePath)) return "missing";
2162
+ if (!existsSync2(filePath)) return "missing";
2972
2163
  try {
2973
- const raw = readFileSync4(filePath, "utf8").trim();
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 = readFileSync4(pkgPath, "utf-8");
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 = projectPaths2(cwd);
3137
- const session = createSession2({
2327
+ const paths = projectPaths(cwd);
2328
+ const session = createSession({
3138
2329
  config,
3139
2330
  context,
3140
2331
  terminal,
3141
- sessionLogger: new FileSessionLogger2(paths.logs),
2332
+ sessionLogger: new FileSessionLogger(paths.logs),
3142
2333
  projectInfo,
3143
2334
  permissionMode: args.permissionMode,
3144
2335
  promptForApproval