neoctl 0.2.4 → 0.2.6

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.
Files changed (129) hide show
  1. package/README.md +16 -26
  2. package/dist/agents/agent-activity.d.ts +70 -0
  3. package/dist/agents/agent-activity.js +261 -0
  4. package/dist/agents/agent-activity.js.map +1 -0
  5. package/dist/agents/agent-definition.d.ts +7 -0
  6. package/dist/agents/agent-definition.js +44 -1
  7. package/dist/agents/agent-definition.js.map +1 -1
  8. package/dist/agents/agent-report-tool.d.ts +11 -0
  9. package/dist/agents/agent-report-tool.js +50 -0
  10. package/dist/agents/agent-report-tool.js.map +1 -0
  11. package/dist/agents/agent-tool.d.ts +3 -1
  12. package/dist/agents/agent-tool.js +56 -11
  13. package/dist/agents/agent-tool.js.map +1 -1
  14. package/dist/agents/local-agent-task.d.ts +3 -0
  15. package/dist/agents/local-agent-task.js +2 -0
  16. package/dist/agents/local-agent-task.js.map +1 -1
  17. package/dist/agents/smoke-agents.js +131 -7
  18. package/dist/agents/smoke-agents.js.map +1 -1
  19. package/dist/context/compaction.js +2 -1
  20. package/dist/context/compaction.js.map +1 -1
  21. package/dist/context/context-manager.d.ts +2 -1
  22. package/dist/context/context-manager.js +24 -11
  23. package/dist/context/context-manager.js.map +1 -1
  24. package/dist/context/prompts.js +4 -0
  25. package/dist/context/prompts.js.map +1 -1
  26. package/dist/context/smoke-context.js +14 -6
  27. package/dist/context/smoke-context.js.map +1 -1
  28. package/dist/core/context-metrics.d.ts +2 -1
  29. package/dist/core/context-metrics.js +3 -1
  30. package/dist/core/context-metrics.js.map +1 -1
  31. package/dist/core/image-registry.js +3 -3
  32. package/dist/core/image-registry.js.map +1 -1
  33. package/dist/core/image-storage.d.ts +14 -0
  34. package/dist/core/image-storage.js +31 -0
  35. package/dist/core/image-storage.js.map +1 -1
  36. package/dist/core/message-pipeline.d.ts +6 -0
  37. package/dist/core/message-pipeline.js +89 -10
  38. package/dist/core/message-pipeline.js.map +1 -1
  39. package/dist/core/prompt-cache-telemetry.d.ts +11 -0
  40. package/dist/core/prompt-cache-telemetry.js +71 -0
  41. package/dist/core/prompt-cache-telemetry.js.map +1 -0
  42. package/dist/core/query-engine.d.ts +3 -1
  43. package/dist/core/query-engine.js +21 -6
  44. package/dist/core/query-engine.js.map +1 -1
  45. package/dist/core/query.d.ts +4 -0
  46. package/dist/core/query.js +28 -5
  47. package/dist/core/query.js.map +1 -1
  48. package/dist/core/run-agent.d.ts +2 -0
  49. package/dist/core/run-agent.js +156 -21
  50. package/dist/core/run-agent.js.map +1 -1
  51. package/dist/core/smoke-core-loop.js +11 -2
  52. package/dist/core/smoke-core-loop.js.map +1 -1
  53. package/dist/index.d.ts +5 -2
  54. package/dist/index.js +5 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/model/anthropic-mapper.js +29 -3
  57. package/dist/model/anthropic-mapper.js.map +1 -1
  58. package/dist/model/config.d.ts +2 -8
  59. package/dist/model/config.js +1 -41
  60. package/dist/model/config.js.map +1 -1
  61. package/dist/model/env.js +6 -11
  62. package/dist/model/env.js.map +1 -1
  63. package/dist/model/model-metadata.json +1 -115
  64. package/dist/model/openai-chat-mapper.js +4 -19
  65. package/dist/model/openai-chat-mapper.js.map +1 -1
  66. package/dist/model/openai-mappers.js +15 -6
  67. package/dist/model/openai-mappers.js.map +1 -1
  68. package/dist/model/provider-factory.js +0 -32
  69. package/dist/model/provider-factory.js.map +1 -1
  70. package/dist/model/smoke-anthropic-mapper.js +6 -2
  71. package/dist/model/smoke-anthropic-mapper.js.map +1 -1
  72. package/dist/repl/commands.d.ts +14 -0
  73. package/dist/repl/commands.js +54 -0
  74. package/dist/repl/commands.js.map +1 -1
  75. package/dist/repl/index.js +1008 -117
  76. package/dist/repl/index.js.map +1 -1
  77. package/dist/secrets/secret-crypto.d.ts +22 -0
  78. package/dist/secrets/secret-crypto.js +58 -0
  79. package/dist/secrets/secret-crypto.js.map +1 -0
  80. package/dist/secrets/secret-redaction.d.ts +8 -0
  81. package/dist/secrets/secret-redaction.js +40 -0
  82. package/dist/secrets/secret-redaction.js.map +1 -0
  83. package/dist/secrets/secret-store.d.ts +28 -0
  84. package/dist/secrets/secret-store.js +158 -0
  85. package/dist/secrets/secret-store.js.map +1 -0
  86. package/dist/secrets/secret-types.d.ts +31 -0
  87. package/dist/secrets/secret-types.js +17 -0
  88. package/dist/secrets/secret-types.js.map +1 -0
  89. package/dist/secrets/smoke-secrets.js +68 -0
  90. package/dist/secrets/smoke-secrets.js.map +1 -0
  91. package/dist/session/session-export.js +2 -1
  92. package/dist/session/session-export.js.map +1 -1
  93. package/dist/skills/skill-filesystem.js +1 -1
  94. package/dist/skills/skill-filesystem.js.map +1 -1
  95. package/dist/skills/skill-tool.js +86 -22
  96. package/dist/skills/skill-tool.js.map +1 -1
  97. package/dist/tools/builtins/exec-tool.d.ts +20 -1
  98. package/dist/tools/builtins/exec-tool.js +167 -29
  99. package/dist/tools/builtins/exec-tool.js.map +1 -1
  100. package/dist/tools/builtins/image-generation-tool.js +22 -4
  101. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  102. package/dist/tools/builtins/plan-tool.d.ts +1 -0
  103. package/dist/tools/builtins/plan-tool.js +80 -27
  104. package/dist/tools/builtins/plan-tool.js.map +1 -1
  105. package/dist/tools/builtins/secret-tools.d.ts +10 -0
  106. package/dist/tools/builtins/secret-tools.js +75 -0
  107. package/dist/tools/builtins/secret-tools.js.map +1 -0
  108. package/dist/tools/run-tool-use.js +4 -2
  109. package/dist/tools/run-tool-use.js.map +1 -1
  110. package/dist/tools/smoke-tool-system.js +111 -10
  111. package/dist/tools/smoke-tool-system.js.map +1 -1
  112. package/dist/tools/tool.d.ts +4 -0
  113. package/dist/tools/tool.js.map +1 -1
  114. package/dist/types/events.d.ts +17 -0
  115. package/dist/ui/display-message.js +8 -4
  116. package/dist/ui/display-message.js.map +1 -1
  117. package/dist/web/html.js +1 -1
  118. package/dist/web/index.js +34 -56
  119. package/dist/web/index.js.map +1 -1
  120. package/package.json +3 -2
  121. package/dist/model/deepseek-adapter.d.ts +0 -29
  122. package/dist/model/deepseek-adapter.js +0 -108
  123. package/dist/model/deepseek-adapter.js.map +0 -1
  124. package/dist/model/kimi-adapter.d.ts +0 -29
  125. package/dist/model/kimi-adapter.js +0 -108
  126. package/dist/model/kimi-adapter.js.map +0 -1
  127. package/dist/model/smoke-deepseek-mapper.js +0 -65
  128. package/dist/model/smoke-deepseek-mapper.js.map +0 -1
  129. /package/dist/{model/smoke-deepseek-mapper.d.ts → secrets/smoke-secrets.d.ts} +0 -0
@@ -22,17 +22,60 @@ import { searchTool } from "../tools/builtins/search-tool.js";
22
22
  import { planTool } from "../tools/builtins/plan-tool.js";
23
23
  import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
24
24
  import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
25
+ import { createSecretTools } from "../tools/builtins/secret-tools.js";
26
+ import { SecretStore } from "../secrets/secret-store.js";
27
+ import { InMemorySecretRedactionRegistry } from "../secrets/secret-redaction.js";
25
28
  import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
29
+ import { AgentActivityStore } from "../agents/agent-activity.js";
26
30
  import { createTaskTools } from "../tasks/task-tools.js";
27
31
  import { TaskStore } from "../tasks/task-store.js";
28
32
  import { cliHelpText, isModelReasoningArgument, isValidReplCommandLine, parseCliReplCommandArgs, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
29
33
  import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
34
+ import { DefaultContextManager } from "../context/context-manager.js";
35
+ import { buildEffectiveSystemPrompt } from "../context/prompts.js";
30
36
  import { writeSessionMarkdownExport } from "../session/session-export.js";
31
37
  import { readClipboard } from "./clipboard.js";
32
38
  import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
33
39
  import { openDirectory } from "../open-directory.js";
34
40
  import { runWebServer } from "../web/index.js";
41
+ import { getNeoctlHome } from "../paths.js";
42
+ import { FileSystemSkillCatalog } from "../skills/skill-filesystem.js";
43
+ import { createSkillAwareCanUseTool, createSkillTool, requireSkillName } from "../skills/skill-tool.js";
44
+ import { createSkillManagementTools } from "../skills/skill-management-tools.js";
35
45
  const e = React.createElement;
46
+ class ReplForegroundExecDetachRegistry {
47
+ handle;
48
+ subscribers = new Set();
49
+ set(handle) {
50
+ this.handle = handle;
51
+ this.notify();
52
+ return () => {
53
+ if (this.handle === handle) {
54
+ this.handle = undefined;
55
+ this.notify();
56
+ }
57
+ };
58
+ }
59
+ current() {
60
+ return this.handle;
61
+ }
62
+ subscribe(listener) {
63
+ this.subscribers.add(listener);
64
+ return () => {
65
+ this.subscribers.delete(listener);
66
+ };
67
+ }
68
+ detachCurrent() {
69
+ const handle = this.handle;
70
+ if (!handle)
71
+ return { ok: false, message: "No foreground exec command is currently running" };
72
+ return handle.detach();
73
+ }
74
+ notify() {
75
+ for (const listener of this.subscribers)
76
+ listener();
77
+ }
78
+ }
36
79
  class SessionUsageTracker {
37
80
  totals = emptyUsageTotals();
38
81
  lastUsage;
@@ -128,6 +171,50 @@ function binaryName() {
128
171
  const name = parsed.name || "neo";
129
172
  return name === "index" ? "neo" : name;
130
173
  }
174
+ class SkillCatalogContextManager {
175
+ catalog;
176
+ base;
177
+ constructor(catalog, base = new DefaultContextManager()) {
178
+ this.catalog = catalog;
179
+ this.base = base;
180
+ }
181
+ async build(input) {
182
+ const runtimeContext = await this.base.build(input);
183
+ const skillSection = await buildSkillCatalogPromptSection(this.catalog);
184
+ if (!skillSection)
185
+ return runtimeContext;
186
+ const promptSections = [...runtimeContext.promptSections, skillSection];
187
+ return {
188
+ ...runtimeContext,
189
+ promptSections,
190
+ systemPrompt: buildEffectiveSystemPrompt(promptSections, input),
191
+ };
192
+ }
193
+ }
194
+ async function buildSkillCatalogPromptSection(catalog) {
195
+ const skills = await catalog.list();
196
+ if (skills.length === 0)
197
+ return undefined;
198
+ const visible = skills.slice(0, 80);
199
+ const lines = visible.map((skill) => {
200
+ const tags = skill.tags?.length ? `; tags=${skill.tags.join(",")}` : "";
201
+ const tools = skill.allowedTools?.length ? `; allowedTools=${skill.allowedTools.join(",")}` : "";
202
+ return `- ${skill.name}: ${skill.description} (execution=${skill.execution}${tags}${tools})`;
203
+ });
204
+ if (skills.length > visible.length)
205
+ lines.push(`- ... ${skills.length - visible.length} more skills available; use skill_list for the full catalog.`);
206
+ return {
207
+ name: "Available Skills",
208
+ cacheStable: false,
209
+ content: [
210
+ "Reusable skills are available through the `skill` tool and the `/skill` REPL command.",
211
+ "When the user's task matches a skill name, description, tags, or domain capability, proactively call the `skill` tool before doing the work directly.",
212
+ "Do not wait for the user to explicitly say 'use skill'. Use skill_list/skill_read if you need to inspect details.",
213
+ "Available skill catalog:",
214
+ ...lines,
215
+ ].join("\n"),
216
+ };
217
+ }
131
218
  function createTaskNotificationSource(taskStore) {
132
219
  return {
133
220
  collectUnnotifiedCompletions() {
@@ -150,10 +237,22 @@ async function createRuntime() {
150
237
  const communicationLogger = new CommunicationLogger();
151
238
  const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
152
239
  const taskStore = new TaskStore();
240
+ const agentActivityStore = new AgentActivityStore();
241
+ const foregroundExecDetach = new ReplForegroundExecDetachRegistry();
242
+ const secretStore = await SecretStore.open();
243
+ const secretRedactions = new InMemorySecretRedactionRegistry();
153
244
  const tools = new ToolRegistry();
245
+ const skillWorkspaceRoot = path.resolve(process.cwd(), ".neo", "skills");
246
+ const skills = new FileSystemSkillCatalog({
247
+ roots: [
248
+ { root: skillWorkspaceRoot, kind: "workspace" },
249
+ { root: path.resolve(getNeoctlHome(), "skills"), kind: "user" },
250
+ ],
251
+ createRoot: skillWorkspaceRoot,
252
+ });
154
253
  tools.register(editTool);
155
254
  tools.register(writeTool);
156
- tools.register(createExecTool({ taskStore }));
255
+ tools.register(createExecTool({ taskStore, foregroundDetachRegistry: foregroundExecDetach }));
157
256
  tools.register(listDirectoryTool);
158
257
  tools.register(readFileTool);
159
258
  tools.register(grepTool);
@@ -162,13 +261,20 @@ async function createRuntime() {
162
261
  if (modelConfig?.provider === "openai")
163
262
  tools.register(createOpenAIImageGenerationTool());
164
263
  tools.register(planTool);
165
- const agentRuntime = { modelGateway, tools, taskStore };
264
+ for (const tool of createSecretTools())
265
+ tools.register(tool);
266
+ tools.register(createSkillTool(skills));
267
+ for (const tool of createSkillManagementTools(skills, { requireApproval: true, allowDelete: false }))
268
+ tools.register(tool);
269
+ const agentRuntime = { modelGateway, tools, taskStore, agentActivityStore };
166
270
  tools.register(createAgentTool(agentRuntime));
167
271
  const resumeHandler = async (taskId, directive) => {
168
272
  const dummyContext = {
169
273
  agentId: "main",
170
274
  tools,
171
275
  appState: new (await import("../app/app-state.js")).InMemoryAppState("main"),
276
+ secrets: secretStore,
277
+ secretRedactions,
172
278
  emit: () => undefined,
173
279
  };
174
280
  return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
@@ -183,6 +289,10 @@ async function createRuntime() {
183
289
  reasoning: modelConfig?.defaultReasoning,
184
290
  modelGateway,
185
291
  tools,
292
+ contextManager: new SkillCatalogContextManager(skills),
293
+ canUseTool: createSkillAwareCanUseTool(skills),
294
+ secrets: secretStore,
295
+ secretRedactions,
186
296
  taskNotificationSource,
187
297
  commands: replCommandDefinitions.map((command) => command.usage),
188
298
  session: {
@@ -204,7 +314,12 @@ async function createRuntime() {
204
314
  agentRuntime,
205
315
  usage: new SessionUsageTracker(),
206
316
  taskStore,
317
+ agentActivityStore,
318
+ foregroundExecDetach,
207
319
  tools,
320
+ skills,
321
+ secretStore,
322
+ skillWorkspaceRoot,
208
323
  initialMetrics,
209
324
  defaultReasoning: modelConfig?.defaultReasoning,
210
325
  envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
@@ -217,7 +332,7 @@ function syncImageGenerationTool(runtime, provider) {
217
332
  runtime.tools.register(createOpenAIImageGenerationTool());
218
333
  }
219
334
  function formatCreatedEnvNotice(path) {
220
- return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (for example OPENAI_API_KEY, ANTHROPIC_API_KEY, or KIMI_API_KEY), then restart neo.`;
335
+ return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (OPENAI_API_KEY or ANTHROPIC_API_KEY), then restart neo.`;
221
336
  }
222
337
  function parseResumeFlag(value) {
223
338
  if (!value)
@@ -227,6 +342,34 @@ function parseResumeFlag(value) {
227
342
  function activeBackgroundTasks(runtime) {
228
343
  return runtime.taskStore.list().filter((task) => !runtime.taskStore.isTerminal(task));
229
344
  }
345
+ function debounceVoid(callback, delayMs) {
346
+ let timer;
347
+ return {
348
+ run: () => {
349
+ if (timer)
350
+ clearTimeout(timer);
351
+ timer = setTimeout(() => {
352
+ timer = undefined;
353
+ callback();
354
+ }, delayMs);
355
+ },
356
+ cancel: () => {
357
+ if (!timer)
358
+ return;
359
+ clearTimeout(timer);
360
+ timer = undefined;
361
+ },
362
+ };
363
+ }
364
+ function activeAgentActivities(runtime) {
365
+ const now = Date.now();
366
+ return runtime.agentActivityStore.list().filter((activity) => {
367
+ if (activity.status === "running" || activity.status === "pending")
368
+ return true;
369
+ const completedAt = activity.completedAt ? new Date(activity.completedAt).getTime() : new Date(activity.updatedAt).getTime();
370
+ return Number.isFinite(completedAt) && now - completedAt < SUBAGENT_COMPLETED_LINGER_MS;
371
+ });
372
+ }
230
373
  function runningSessionIds(runs) {
231
374
  return [...runs.keys()];
232
375
  }
@@ -393,6 +536,9 @@ function InkRepl({ runtime, initialCommandLine }) {
393
536
  const [status, setStatus] = useState(() => initialStatus(runtime));
394
537
  const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
395
538
  const [backgroundTasks, setBackgroundTasks] = useState(() => activeBackgroundTasks(runtime));
539
+ const [agentActivities, setAgentActivities] = useState(() => runtime.agentActivityStore.list());
540
+ const [foregroundExecDetachHandle, setForegroundExecDetachHandle] = useState(() => runtime.foregroundExecDetach.current());
541
+ const [showForegroundExecDetachHint, setShowForegroundExecDetachHint] = useState(false);
396
542
  const [backgroundSessionRuns, setBackgroundSessionRuns] = useState([]);
397
543
  const backgroundSessionRunsRef = useRef(new Map());
398
544
  const suppressReattachedStreamingRef = useRef(new Set());
@@ -403,6 +549,8 @@ function InkRepl({ runtime, initialCommandLine }) {
403
549
  const backgroundTaskCount = backgroundTasks.length;
404
550
  const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0 || backgroundSessionRuns.length > 0;
405
551
  const [sessionsBrowser, setSessionsBrowser] = useState(undefined);
552
+ const [skillsBrowser, setSkillsBrowser] = useState(undefined);
553
+ const [secretsBrowser, setSecretsBrowser] = useState(undefined);
406
554
  const inputRef = useRef(input);
407
555
  const queuedInputRef = useRef(undefined);
408
556
  const cursorRef = useRef(cursor);
@@ -418,6 +566,8 @@ function InkRepl({ runtime, initialCommandLine }) {
418
566
  const [pasteStatus, setPasteStatus] = useState(undefined);
419
567
  const pasteStatusTimerRef = useRef(undefined);
420
568
  const [slashCompletionIndex, setSlashCompletionIndex] = useState(0);
569
+ const [skillCompletions, setSkillCompletions] = useState([]);
570
+ const [secretCompletions, setSecretCompletions] = useState([]);
421
571
  const [loginForm, setLoginForm] = useState(undefined);
422
572
  const loginFormRef = useRef(undefined);
423
573
  useEffect(() => {
@@ -429,16 +579,48 @@ function InkRepl({ runtime, initialCommandLine }) {
429
579
  };
430
580
  }, []);
431
581
  useEffect(() => {
432
- if (!busy && backgroundTaskCount === 0 && backgroundSessionRuns.length === 0)
582
+ if (!busy && backgroundTaskCount === 0 && backgroundSessionRuns.length === 0 && agentActivities.length === 0)
433
583
  return undefined;
434
- const interval = setInterval(() => setAnimationTick((current) => current + 1), REPL_ANIMATION_INTERVAL_MS);
584
+ const interval = setInterval(() => {
585
+ setAnimationTick((current) => current + 1);
586
+ setAgentActivities(activeAgentActivities(runtime));
587
+ }, REPL_ANIMATION_INTERVAL_MS);
435
588
  return () => clearInterval(interval);
436
- }, [busy, backgroundTaskCount, backgroundSessionRuns.length]);
589
+ }, [busy, backgroundTaskCount, backgroundSessionRuns.length, agentActivities.length, runtime]);
437
590
  useEffect(() => {
438
591
  const updateBackgroundTasks = () => setBackgroundTasks(activeBackgroundTasks(runtime));
439
592
  updateBackgroundTasks();
440
593
  return runtime.taskStore.subscribe(updateBackgroundTasks);
441
594
  }, [runtime]);
595
+ useEffect(() => {
596
+ const updateAgentActivities = () => setAgentActivities(activeAgentActivities(runtime));
597
+ const debouncedUpdateAgentActivities = debounceVoid(updateAgentActivities, SUBAGENT_ACTIVITY_UPDATE_DEBOUNCE_MS);
598
+ updateAgentActivities();
599
+ const unsubscribe = runtime.agentActivityStore.subscribe(debouncedUpdateAgentActivities.run);
600
+ return () => {
601
+ unsubscribe();
602
+ debouncedUpdateAgentActivities.cancel();
603
+ };
604
+ }, [runtime]);
605
+ useEffect(() => {
606
+ const updateForegroundExecDetachHandle = () => setForegroundExecDetachHandle(runtime.foregroundExecDetach.current());
607
+ updateForegroundExecDetachHandle();
608
+ return runtime.foregroundExecDetach.subscribe(updateForegroundExecDetachHandle);
609
+ }, [runtime]);
610
+ useEffect(() => {
611
+ if (!foregroundExecDetachHandle) {
612
+ setShowForegroundExecDetachHint(false);
613
+ return undefined;
614
+ }
615
+ const elapsedMs = Date.now() - foregroundExecDetachHandle.startedAt;
616
+ if (elapsedMs >= FOREGROUND_EXEC_DETACH_HINT_DELAY_MS) {
617
+ setShowForegroundExecDetachHint(true);
618
+ return undefined;
619
+ }
620
+ setShowForegroundExecDetachHint(false);
621
+ const timer = setTimeout(() => setShowForegroundExecDetachHint(true), FOREGROUND_EXEC_DETACH_HINT_DELAY_MS - elapsedMs);
622
+ return () => clearTimeout(timer);
623
+ }, [foregroundExecDetachHandle]);
442
624
  useEffect(() => {
443
625
  if (!terminalTitleWorking) {
444
626
  setTerminalTitlePrefix(TERMINAL_TITLE_READY_PREFIX);
@@ -488,6 +670,30 @@ function InkRepl({ runtime, initialCommandLine }) {
488
670
  loginFormRef.current = next;
489
671
  setLoginForm(next);
490
672
  };
673
+ const refreshSkillCompletions = useCallback(async () => {
674
+ try {
675
+ const skills = await runtime.skills.list();
676
+ setSkillCompletions(skills.map((skill) => ({ name: skill.name, description: skill.description, execution: skill.execution, tags: skill.tags })));
677
+ }
678
+ catch {
679
+ setSkillCompletions([]);
680
+ }
681
+ }, [runtime]);
682
+ const refreshSecretCompletions = useCallback(async () => {
683
+ try {
684
+ const secrets = await runtime.secretStore.list();
685
+ setSecretCompletions(secrets.map((secret) => ({ key: secret.key, status: secret.status, length: secret.length, requestReason: secret.requestReason })));
686
+ }
687
+ catch {
688
+ setSecretCompletions([]);
689
+ }
690
+ }, [runtime]);
691
+ useEffect(() => {
692
+ void refreshSkillCompletions();
693
+ }, [refreshSkillCompletions]);
694
+ useEffect(() => {
695
+ void refreshSecretCompletions();
696
+ }, [refreshSecretCompletions]);
491
697
  const syncAttachmentsForText = (text) => {
492
698
  const next = attachmentsRef.current.filter((attachment) => text.includes(attachment.label));
493
699
  if (next.length === attachmentsRef.current.length)
@@ -855,6 +1061,7 @@ function InkRepl({ runtime, initialCommandLine }) {
855
1061
  };
856
1062
  const handleCommandOrPrompt = async (text, submitAttachments = []) => {
857
1063
  const command = parseReplCommand(text);
1064
+ let promptText = command.type === "input" ? command.text : text;
858
1065
  if (command.type === "exit") {
859
1066
  app.exit();
860
1067
  return;
@@ -990,11 +1197,48 @@ function InkRepl({ runtime, initialCommandLine }) {
990
1197
  }
991
1198
  if (command.type === "sessions") {
992
1199
  detachRunningForeground("session browser");
1200
+ setSkillsBrowser(undefined);
1201
+ setSecretsBrowser(undefined);
993
1202
  await handleSessionsCommand(runtime, runningSessionIds(backgroundSessionRunsRef.current), setSessionsBrowser, (line) => append(line));
994
1203
  return;
995
1204
  }
1205
+ if (command.type === "secret") {
1206
+ try {
1207
+ if (command.action === "list") {
1208
+ setSessionsBrowser(undefined);
1209
+ setSkillsBrowser(undefined);
1210
+ await handleSecretsCommand(runtime, setSecretsBrowser, (line) => append(line));
1211
+ }
1212
+ else {
1213
+ append(await handleSecretCommand(command, runtime));
1214
+ void refreshSecretCompletions();
1215
+ }
1216
+ }
1217
+ catch (error) {
1218
+ append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
1219
+ }
1220
+ return;
1221
+ }
1222
+ if (command.type === "skill") {
1223
+ if (command.action === "list") {
1224
+ setSessionsBrowser(undefined);
1225
+ setSecretsBrowser(undefined);
1226
+ await handleSkillsCommand(runtime, setSkillsBrowser, (line) => append(line));
1227
+ return;
1228
+ }
1229
+ if (command.action !== "invoke" || !command.name || !command.args) {
1230
+ append(await handleSkillCommand(command, runtime));
1231
+ if (command.action === "import" || command.action === "delete")
1232
+ void refreshSkillCompletions();
1233
+ return;
1234
+ }
1235
+ promptText = renderSkillInvocationPrompt(command.name, command.args);
1236
+ text = `/skill ${command.name} ${command.args}`;
1237
+ }
996
1238
  if (command.type === "login") {
997
1239
  setSessionsBrowser(undefined);
1240
+ setSkillsBrowser(undefined);
1241
+ setSecretsBrowser(undefined);
998
1242
  setLoginFormState(createLoginFormState(runtime.envPath));
999
1243
  append(systemLine("Opening provider login. Use ↑/↓ to choose, Enter to continue/save, Esc to cancel."));
1000
1244
  return;
@@ -1023,11 +1267,11 @@ function InkRepl({ runtime, initialCommandLine }) {
1023
1267
  }
1024
1268
  return;
1025
1269
  }
1026
- if (text.trimStart().startsWith("/")) {
1270
+ if (command.type !== "skill" && text.trimStart().startsWith("/")) {
1027
1271
  append({ kind: "error", text: `Unknown or incomplete command: ${text.trim()}\nType /help for commands.` });
1028
1272
  return;
1029
1273
  }
1030
- const promptPayload = buildPromptPayload(command.text, submitAttachments);
1274
+ const promptPayload = buildPromptPayload(promptText, submitAttachments);
1031
1275
  append({ kind: "user", text });
1032
1276
  const runToken = ++foregroundRunTokenRef.current;
1033
1277
  const abortController = new AbortController();
@@ -1112,6 +1356,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1112
1356
  clearPendingToolResultTimers();
1113
1357
  setStatus(initialStatus(runtime));
1114
1358
  setSessionsBrowser(undefined);
1359
+ setSkillsBrowser(undefined);
1360
+ setSecretsBrowser(undefined);
1115
1361
  setLoginFormState(undefined);
1116
1362
  setQueuedPromptState(undefined);
1117
1363
  setPromptState("", 0);
@@ -1131,7 +1377,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1131
1377
  const promptDisplayCursor = cursor;
1132
1378
  const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
1133
1379
  const promptLayoutCursor = activePlaceholder ? 0 : promptDisplayCursor;
1134
- const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor);
1380
+ const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor, skillCompletions, secretCompletions);
1135
1381
  const visibleSlashCompletionCount = slashCompletions.length;
1136
1382
  const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
1137
1383
  ? 0
@@ -1147,10 +1393,18 @@ function InkRepl({ runtime, initialCommandLine }) {
1147
1393
  const blockIndex = staticLines.length + i;
1148
1394
  return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
1149
1395
  }, 0);
1150
- const statusRenderRows = STATUS_BAR_RENDER_ROWS + backgroundTaskStatusRenderRows(backgroundTasks.length);
1151
- const sessionsBrowserHeight = sessionsBrowser ? sessionsBrowserViewHeight(sessionsBrowser) : 0;
1396
+ const subagentRows = subagentLivePanelRenderRows(agentActivities, terminalSize.rows);
1397
+ const nonAgentBackgroundTasks = backgroundTasks.filter((task) => task.type !== "agent");
1398
+ const statusRenderRows = STATUS_BAR_RENDER_ROWS + (showForegroundExecDetachHint && foregroundExecDetachHandle ? FOREGROUND_EXEC_DETACH_HINT_RENDER_ROWS : 0) + subagentRows + backgroundTaskStatusRenderRows(subagentRows > 0 ? nonAgentBackgroundTasks.length : backgroundTasks.length);
1399
+ const managementBrowserHeight = sessionsBrowser
1400
+ ? sessionsBrowserViewHeight(sessionsBrowser)
1401
+ : skillsBrowser
1402
+ ? skillsBrowserViewHeight(skillsBrowser)
1403
+ : secretsBrowser
1404
+ ? secretsBrowserViewHeight(secretsBrowser)
1405
+ : 0;
1152
1406
  const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
1153
- const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - loginFormHeight - dynamicMarginOverhead - 1);
1407
+ const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - managementBrowserHeight - loginFormHeight - dynamicMarginOverhead - 1);
1154
1408
  useInput((value, key) => {
1155
1409
  if (isTerminalFocusInSequence(value)) {
1156
1410
  terminalFocusedRef.current = true;
@@ -1171,6 +1425,13 @@ function InkRepl({ runtime, initialCommandLine }) {
1171
1425
  void handleClipboardPaste();
1172
1426
  return;
1173
1427
  }
1428
+ if (key.ctrl && value.toLowerCase() === "b") {
1429
+ const result = runtime.foregroundExecDetach.detachCurrent();
1430
+ append(result.ok
1431
+ ? systemLine(`Detached foreground exec to background task ${result.taskId ?? "unknown"}.`)
1432
+ : systemLine(result.message));
1433
+ return;
1434
+ }
1174
1435
  if (key.ctrl && value === "c") {
1175
1436
  if (inputRef.current.length > 0) {
1176
1437
  setPromptState("", 0);
@@ -1245,15 +1506,128 @@ function InkRepl({ runtime, initialCommandLine }) {
1245
1506
  }
1246
1507
  return;
1247
1508
  }
1509
+ if (skillsBrowser) {
1510
+ if (key.escape) {
1511
+ setSkillsBrowser(undefined);
1512
+ return;
1513
+ }
1514
+ if (key.upArrow) {
1515
+ setSkillsBrowser((current) => current ? movePagedSelection(current, -1) : current);
1516
+ return;
1517
+ }
1518
+ if (key.downArrow) {
1519
+ setSkillsBrowser((current) => current ? movePagedSelection(current, 1) : current);
1520
+ return;
1521
+ }
1522
+ if (key.leftArrow || key.pageUp) {
1523
+ setSkillsBrowser((current) => current ? movePagedPage(current, -1) : current);
1524
+ return;
1525
+ }
1526
+ if (key.rightArrow || key.pageDown) {
1527
+ setSkillsBrowser((current) => current ? movePagedPage(current, 1) : current);
1528
+ return;
1529
+ }
1530
+ const selected = skillsBrowser.skills[pagedAbsoluteIndex(skillsBrowser)];
1531
+ if (key.return && selected) {
1532
+ setSkillsBrowser(undefined);
1533
+ append(systemLine(formatSkillDetails(selected), EXPANDED_SUMMARY_MAX_LINES));
1534
+ return;
1535
+ }
1536
+ if (value.toLowerCase() === "i" && selected) {
1537
+ setSkillsBrowser(undefined);
1538
+ setPromptState(`/skill ${selected.name} `, selected.name.length + 8);
1539
+ return;
1540
+ }
1541
+ if ((key.delete || key.backspace || value.toLowerCase() === "d") && selected) {
1542
+ void handleSkillDeleteByName(selected.name, runtime).then((line) => {
1543
+ append(line);
1544
+ void refreshSkillCompletions();
1545
+ void handleSkillsCommand(runtime, setSkillsBrowser, (nextLine) => append(nextLine));
1546
+ });
1547
+ return;
1548
+ }
1549
+ if (value.toLowerCase() === "a") {
1550
+ setSkillsBrowser(undefined);
1551
+ setPromptState("/skill import ", 14);
1552
+ return;
1553
+ }
1554
+ return;
1555
+ }
1556
+ if (secretsBrowser) {
1557
+ if (key.escape) {
1558
+ setSecretsBrowser(undefined);
1559
+ return;
1560
+ }
1561
+ if (key.upArrow) {
1562
+ setSecretsBrowser((current) => current ? movePagedSelection(current, -1) : current);
1563
+ return;
1564
+ }
1565
+ if (key.downArrow) {
1566
+ setSecretsBrowser((current) => current ? movePagedSelection(current, 1) : current);
1567
+ return;
1568
+ }
1569
+ if (key.leftArrow || key.pageUp) {
1570
+ setSecretsBrowser((current) => current ? movePagedPage(current, -1) : current);
1571
+ return;
1572
+ }
1573
+ if (key.rightArrow || key.pageDown) {
1574
+ setSecretsBrowser((current) => current ? movePagedPage(current, 1) : current);
1575
+ return;
1576
+ }
1577
+ const selected = secretsBrowser.secrets[pagedAbsoluteIndex(secretsBrowser)];
1578
+ if (key.return && selected) {
1579
+ setSecretsBrowser(undefined);
1580
+ void handleSecretCommand({ type: "secret", action: "info", key: selected.key }, runtime).then((line) => append(line));
1581
+ return;
1582
+ }
1583
+ if (value.toLowerCase() === "s" && selected) {
1584
+ setSecretsBrowser(undefined);
1585
+ setPromptState(`/secret set ${selected.key} `, selected.key.length + 13);
1586
+ return;
1587
+ }
1588
+ if (value.toLowerCase() === "r" && selected) {
1589
+ setSecretsBrowser(undefined);
1590
+ setPromptState(`/secret rename ${selected.key} `, selected.key.length + 16);
1591
+ return;
1592
+ }
1593
+ if ((key.delete || key.backspace || value.toLowerCase() === "d") && selected) {
1594
+ void handleSecretCommand({ type: "secret", action: "delete", key: selected.key }, runtime).then((line) => {
1595
+ append(line);
1596
+ void refreshSecretCompletions();
1597
+ void handleSecretsCommand(runtime, setSecretsBrowser, (nextLine) => append(nextLine));
1598
+ }).catch((error) => append({ kind: "error", text: error instanceof Error ? error.message : String(error) }));
1599
+ return;
1600
+ }
1601
+ if (value.toLowerCase() === "a") {
1602
+ setSecretsBrowser(undefined);
1603
+ setPromptState("/secret set ", 12);
1604
+ return;
1605
+ }
1606
+ if (value.toLowerCase() === "e") {
1607
+ setSecretsBrowser(undefined);
1608
+ setPromptState("/secret request ", 16);
1609
+ return;
1610
+ }
1611
+ return;
1612
+ }
1248
1613
  if (key.return) {
1249
1614
  const currentText = inputRef.current;
1250
1615
  const currentCursor = cursorRef.current;
1251
- const completion = selectedSlashCommandCompletion(currentText, currentCursor, slashCompletionIndexRef.current);
1616
+ const completion = selectedSlashCommandCompletion(currentText, currentCursor, slashCompletionIndexRef.current, skillCompletions, secretCompletions);
1252
1617
  if (completion !== undefined && completion.kind === "command" && completion.arguments !== "none") {
1253
1618
  const nextText = `${completion.insertText} ${currentText.slice(currentCursor)}`;
1254
1619
  setPromptState(nextText, completion.insertText.length + 1);
1255
1620
  return;
1256
1621
  }
1622
+ if (currentText.trimEnd() === "/skill" || currentText.trimEnd() === "/secret") {
1623
+ void submitLine(currentText);
1624
+ return;
1625
+ }
1626
+ if (completion !== undefined && (completion.kind === "skill-action" || completion.kind === "secret-action")) {
1627
+ const nextText = `${completion.insertText}${currentText.slice(currentCursor)}`;
1628
+ setPromptState(nextText, completion.insertText.length);
1629
+ return;
1630
+ }
1257
1631
  if (currentText.trimEnd() === "/model" && completion?.kind !== "command") {
1258
1632
  void submitLine(currentText);
1259
1633
  return;
@@ -1274,7 +1648,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1274
1648
  return;
1275
1649
  }
1276
1650
  if (key.leftArrow) {
1277
- const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1651
+ const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions, secretCompletions);
1278
1652
  if (completionCount > SLASH_COMPLETION_PAGE_SIZE) {
1279
1653
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1280
1654
  return;
@@ -1287,7 +1661,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1287
1661
  return;
1288
1662
  }
1289
1663
  if (key.rightArrow) {
1290
- const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1664
+ const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions, secretCompletions);
1291
1665
  if (completionCount > SLASH_COMPLETION_PAGE_SIZE) {
1292
1666
  setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1293
1667
  return;
@@ -1318,7 +1692,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1318
1692
  setTipIndex((current) => current - 1);
1319
1693
  return;
1320
1694
  }
1321
- const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1695
+ const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions, secretCompletions);
1322
1696
  if (completionCount > 0) {
1323
1697
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
1324
1698
  return;
@@ -1335,7 +1709,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1335
1709
  setTipIndex((current) => current + 1);
1336
1710
  return;
1337
1711
  }
1338
- const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1712
+ const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions, secretCompletions);
1339
1713
  if (completionCount > 0) {
1340
1714
  setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
1341
1715
  return;
@@ -1361,7 +1735,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1361
1735
  return;
1362
1736
  }
1363
1737
  const currentCursor = cursorRef.current;
1364
- const completions = slashCommandCompletions(currentText, currentCursor);
1738
+ const completions = slashCommandCompletions(currentText, currentCursor, skillCompletions, secretCompletions);
1365
1739
  const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
1366
1740
  if (completion !== undefined) {
1367
1741
  const nextText = `${completion.insertText}${currentText.slice(currentCursor)}`;
@@ -1374,7 +1748,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1374
1748
  return;
1375
1749
  }
1376
1750
  });
1377
- return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: dynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1751
+ return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: dynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, skillsBrowser ? e(SkillsBrowser, { state: skillsBrowser, width }) : null, secretsBrowser ? e(SecretsBrowser, { state: secretsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), showForegroundExecDetachHint && foregroundExecDetachHandle ? e(ForegroundExecDetachHintLine, { handle: foregroundExecDetachHandle, width }) : null, agentActivities.length > 0 ? e(SubagentLivePanel, { activities: agentActivities, width, terminalRows: terminalSize.rows, animationTick }) : null, agentActivities.length === 0 && backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, agentActivities.length > 0 && nonAgentBackgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: nonAgentBackgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1378
1752
  }
1379
1753
  const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1380
1754
  const contentWidth = messageContentWidth(width);
@@ -1731,6 +2105,139 @@ function backgroundTaskStatusRenderRows(taskCount) {
1731
2105
  return 0;
1732
2106
  return 1 + Math.min(taskCount, 2);
1733
2107
  }
2108
+ function ForegroundExecDetachHintLine({ handle, width: terminalWidth }) {
2109
+ const width = statusBarWidth(terminalWidth);
2110
+ const label = handle.description?.trim() || handle.command;
2111
+ const text = `↳ exec still running · Ctrl+B to detach · ${truncateMiddle(label, Math.max(12, width - 38))}`;
2112
+ return e(Text, { color: "yellow" }, fitToWidth(text, width));
2113
+ }
2114
+ function SubagentLivePanel({ activities, width: terminalWidth, terminalRows, animationTick }) {
2115
+ const width = statusBarWidth(terminalWidth);
2116
+ const rows = subagentLivePanelRenderRows(activities, terminalRows);
2117
+ if (rows <= 0)
2118
+ return null;
2119
+ const sorted = sortAgentActivitiesForPanel(activities);
2120
+ const selected = sorted[0];
2121
+ if (!selected)
2122
+ return null;
2123
+ const activeCount = activities.filter((activity) => activity.status === "running" || activity.status === "pending").length;
2124
+ const header = `◇ subagents: ${activeCount} active${activities.length > activeCount ? ` · ${activities.length - activeCount} recent` : ""} | auto: latest activity`;
2125
+ if (rows <= 1) {
2126
+ return e(Text, { color: "yellow" }, fitToWidth(`${header} | ${compactAgentSummary(selected, width - header.length - 3)}`, width));
2127
+ }
2128
+ const detailLines = buildSubagentDetailLines(selected, sorted, animationTick);
2129
+ return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(header, width)), ...detailLines.map((line, index) => e(Text, {
2130
+ key: `agent-detail-${selected.agentId}-${index}`,
2131
+ color: line.color,
2132
+ }, fitToWidth(line.text, width))));
2133
+ }
2134
+ const SUBAGENT_DETAIL_ROWS = 3;
2135
+ function subagentLivePanelRenderRows(activities, terminalRows) {
2136
+ if (activities.length === 0)
2137
+ return 0;
2138
+ if (terminalRows < 18)
2139
+ return 1;
2140
+ return 1 + SUBAGENT_DETAIL_ROWS;
2141
+ }
2142
+ function sortAgentActivitiesForPanel(activities) {
2143
+ const rank = (status) => {
2144
+ if (status === "running")
2145
+ return 0;
2146
+ if (status === "pending")
2147
+ return 1;
2148
+ if (status === "failed" || status === "killed")
2149
+ return 2;
2150
+ return 3;
2151
+ };
2152
+ return [...activities].sort((left, right) => rank(left.status) - rank(right.status) || right.updatedAt.localeCompare(left.updatedAt));
2153
+ }
2154
+ function buildSubagentDetailLines(selected, sorted, animationTick) {
2155
+ const spinner = selected.status === "running" ? spinnerFrame(animationTick) : statusGlyph(selected.status);
2156
+ const elapsed = formatElapsed(Date.now() - new Date(selected.startedAt).getTime());
2157
+ const headerLine = `${spinner} ${selected.description || selected.agentId} · ${agentModeLabel(selected)} · ${selected.agentType} · ${selected.status} · ${elapsed}`;
2158
+ const currentLine = selected.currentTool
2159
+ ? `→ ${selected.currentTool.name}${selected.currentTool.inputPreview ? ` · ${selected.currentTool.inputPreview}` : ""}`
2160
+ : selected.error
2161
+ ? `✖ ${selected.error}`
2162
+ : selected.resultPreview
2163
+ ? `✓ ${selected.resultPreview}`
2164
+ : selected.lastText
2165
+ ? `• ${selected.lastText}`
2166
+ : `• ${selected.prompt}`;
2167
+ const recent = selected.timeline.slice(-2).map((entry) => `${timelinePrefix(entry)} ${formatTimelineEntry(entry, 240)}`);
2168
+ const otherRunning = sorted
2169
+ .filter((activity) => activity.agentId !== selected.agentId && (activity.status === "running" || activity.status === "pending"))
2170
+ .slice(0, 2)
2171
+ .map((activity) => compactAgentSummary(activity, 180));
2172
+ const tail = [...recent, ...otherRunning.map((summary) => `· ${summary}`)].find((line) => line.trim()) ?? `tools:${selected.totalToolUseCount}`;
2173
+ return [
2174
+ { text: headerLine, color: statusColor(selected.status) },
2175
+ { text: currentLine, color: selected.error ? "red" : selected.currentTool ? "#d4b04c" : "yellow" },
2176
+ { text: tail, color: "gray" },
2177
+ ];
2178
+ }
2179
+ function compactAgentSummary(activity, maxLength) {
2180
+ const current = activity.currentTool
2181
+ ? `${activity.currentTool.name}${activity.currentTool.inputPreview ? ` ${activity.currentTool.inputPreview}` : ""}`
2182
+ : activity.lastText ?? activity.resultPreview ?? activity.error ?? activity.prompt;
2183
+ return truncateMiddle(`${activity.description || activity.agentId} · ${agentModeLabel(activity)} · ${activity.status} · tools:${activity.totalToolUseCount} · ${current.replace(/\s+/g, " ")}`, Math.max(8, maxLength));
2184
+ }
2185
+ function agentModeLabel(activity) {
2186
+ if (activity.mode === "explore")
2187
+ return "explore";
2188
+ return activity.mode;
2189
+ }
2190
+ function formatTimelineEntry(entry, maxLength) {
2191
+ const detail = entry.detail ? ` · ${entry.detail.replace(/\s+/g, " ")}` : "";
2192
+ return truncateMiddle(`${entry.title}${detail}`, Math.max(8, maxLength));
2193
+ }
2194
+ function timelinePrefix(entry) {
2195
+ if (entry.kind === "tool_start")
2196
+ return "→";
2197
+ if (entry.kind === "tool_result")
2198
+ return entry.status === "failed" ? "✖" : "←";
2199
+ if (entry.kind === "thinking")
2200
+ return "◆";
2201
+ if (entry.kind === "error")
2202
+ return "✖";
2203
+ if (entry.kind === "status")
2204
+ return "•";
2205
+ return "assistant:";
2206
+ }
2207
+ function timelineColor(entry) {
2208
+ if (entry.status === "failed" || entry.kind === "error")
2209
+ return "red";
2210
+ if (entry.kind === "tool_start" || entry.kind === "tool_result")
2211
+ return "#d4b04c";
2212
+ if (entry.kind === "thinking")
2213
+ return THINKING_COLOR;
2214
+ if (entry.kind === "status")
2215
+ return "gray";
2216
+ return "green";
2217
+ }
2218
+ function statusGlyph(status) {
2219
+ if (status === "completed")
2220
+ return "✓";
2221
+ if (status === "failed")
2222
+ return "✖";
2223
+ if (status === "killed")
2224
+ return "■";
2225
+ if (status === "pending")
2226
+ return "…";
2227
+ return "●";
2228
+ }
2229
+ function statusColor(status) {
2230
+ if (status === "completed")
2231
+ return "green";
2232
+ if (status === "failed" || status === "killed")
2233
+ return "red";
2234
+ if (status === "pending")
2235
+ return "gray";
2236
+ return "yellow";
2237
+ }
2238
+ function spinnerFrame(tick) {
2239
+ return ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][tick % 10] ?? "●";
2240
+ }
1734
2241
  function BackgroundTaskStatusLine({ tasks, width: terminalWidth }) {
1735
2242
  const width = statusBarWidth(terminalWidth);
1736
2243
  const summary = `◇ background tools: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`;
@@ -1806,7 +2313,21 @@ function fitStatusSegments(segments, width) {
1806
2313
  const SLASH_COMPLETION_PAGE_SIZE = 10;
1807
2314
  const MODEL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
1808
2315
  const MODEL_REASONING_CONTROL_CHOICES = ["default", "off"];
1809
- function slashCommandCompletions(text, cursor) {
2316
+ const SKILL_COMMAND_ACTIONS = [
2317
+ { name: "list", description: "Open the skill management browser", aliases: ["ls"] },
2318
+ { name: "import", description: "Import by linking a skill directory" },
2319
+ { name: "delete", description: "Delete a workspace skill link/directory", aliases: ["remove", "rm"] },
2320
+ ];
2321
+ const SECRET_COMMAND_ACTIONS = [
2322
+ { name: "list", description: "List secret keys/status/length; add --show to print values" },
2323
+ { name: "get", description: "Print one secret value in the REPL" },
2324
+ { name: "set", description: "Set a plaintext secret value" },
2325
+ { name: "request", description: "Create an empty placeholder secret", aliases: ["empty"] },
2326
+ { name: "info", description: "Show one secret's metadata" },
2327
+ { name: "rename", description: "Rename a secret key", aliases: ["mv"] },
2328
+ { name: "delete", description: "Delete a secret", aliases: ["remove", "rm"] },
2329
+ ];
2330
+ function slashCommandCompletions(text, cursor, skills = [], secrets = []) {
1810
2331
  const safeCursor = Math.max(0, Math.min(cursor, text.length));
1811
2332
  const prefix = text.slice(0, safeCursor);
1812
2333
  if (!prefix.startsWith("/") || /\r|\n/.test(prefix))
@@ -1819,6 +2340,12 @@ function slashCommandCompletions(text, cursor) {
1819
2340
  if (prefix.startsWith("/model") && (prefix.length === "/model".length || prefix["/model".length] === " ")) {
1820
2341
  return modelCommandCompletions(prefix);
1821
2342
  }
2343
+ if (prefix.startsWith("/skill") && (prefix.length === "/skill".length || prefix["/skill".length] === " ")) {
2344
+ return skillCommandCompletions(prefix, skills);
2345
+ }
2346
+ if (prefix.startsWith("/secret") && (prefix.length === "/secret".length || prefix["/secret".length] === " ")) {
2347
+ return secretCommandCompletions(prefix, secrets);
2348
+ }
1822
2349
  if (prefix.length > 1 && !/^\/[\w-]*$/.test(prefix))
1823
2350
  return [];
1824
2351
  const normalizedPrefix = prefix.toLowerCase();
@@ -1826,6 +2353,137 @@ function slashCommandCompletions(text, cursor) {
1826
2353
  .flatMap((command) => [command.name, ...(command.aliases ?? [])].map((name) => ({ value: name, insertText: name, description: command.description, arguments: command.arguments, kind: "command" })))
1827
2354
  .filter((command) => command.value.toLowerCase().startsWith(normalizedPrefix));
1828
2355
  }
2356
+ function skillCommandCompletions(prefix, skills) {
2357
+ const hasTrailingSpace = /\s$/.test(prefix);
2358
+ const tokens = prefix.trim().split(/\s+/).filter(Boolean);
2359
+ const argumentTokens = tokens.slice(1);
2360
+ if (!hasTrailingSpace && argumentTokens.length === 0 && !"/skill".startsWith(prefix.toLowerCase()))
2361
+ return [];
2362
+ if (argumentTokens.length === 0)
2363
+ return skillActionCompletions("");
2364
+ const [first = "", second = ""] = argumentTokens;
2365
+ if (first === "list" || first === "ls" || first === "import")
2366
+ return [];
2367
+ if (first === "delete" || first === "remove" || first === "rm") {
2368
+ if (argumentTokens.length > 1 && hasTrailingSpace)
2369
+ return [];
2370
+ return skillNameCompletions(skills, hasTrailingSpace ? "" : second, "delete");
2371
+ }
2372
+ if (argumentTokens.length > 1 || hasTrailingSpace)
2373
+ return [];
2374
+ return skillActionCompletions(first);
2375
+ }
2376
+ function skillActionCompletions(current) {
2377
+ return SKILL_COMMAND_ACTIONS
2378
+ .flatMap((action) => [action.name, ...("aliases" in action ? action.aliases ?? [] : [])].map((name) => ({ name, description: action.description })))
2379
+ .filter((action) => action.name.startsWith(current.toLowerCase()))
2380
+ .map((action) => ({
2381
+ value: action.name,
2382
+ insertText: action.name === "list" || action.name === "ls" ? `/skill ${action.name}` : `/skill ${action.name} `,
2383
+ description: action.description,
2384
+ arguments: "optional",
2385
+ kind: "skill-action",
2386
+ }));
2387
+ }
2388
+ function skillNameCompletions(skills, current, action) {
2389
+ return skills
2390
+ .filter((skill) => skill.name.toLowerCase().includes(current.toLowerCase()))
2391
+ .map((skill) => ({
2392
+ value: skill.name,
2393
+ insertText: action === "delete" ? `/skill delete ${skill.name}` : `/skill ${skill.name}`,
2394
+ description: formatSkillCompletionDescription(skill),
2395
+ arguments: "optional",
2396
+ kind: "skill",
2397
+ }));
2398
+ }
2399
+ function formatSkillCompletionDescription(skill) {
2400
+ const tags = skill.tags?.length ? ` · ${skill.tags.join(",")}` : "";
2401
+ return `${skill.description}${skill.execution ? ` · ${skill.execution}` : ""}${tags}`;
2402
+ }
2403
+ function secretCommandCompletions(prefix, secrets) {
2404
+ const hasTrailingSpace = /\s$/.test(prefix);
2405
+ const tokens = prefix.trim().split(/\s+/).filter(Boolean);
2406
+ const argumentTokens = tokens.slice(1);
2407
+ if (!hasTrailingSpace && argumentTokens.length === 0 && !"/secret".startsWith(prefix.toLowerCase()))
2408
+ return [];
2409
+ if (argumentTokens.length === 0)
2410
+ return secretActionCompletions("");
2411
+ const [action = "", key = "", newKey = ""] = argumentTokens;
2412
+ const normalizedAction = secretCanonicalAction(action);
2413
+ if (!normalizedAction) {
2414
+ if (argumentTokens.length > 1 || hasTrailingSpace)
2415
+ return [];
2416
+ return secretActionCompletions(action);
2417
+ }
2418
+ if (normalizedAction === "list") {
2419
+ if (argumentTokens.length === 1 && hasTrailingSpace)
2420
+ return [{ value: "--show", insertText: "/secret list --show", description: "Print plaintext values in the REPL", arguments: "optional", kind: "secret-action" }];
2421
+ if (argumentTokens.length === 2 && !hasTrailingSpace)
2422
+ return "--show".startsWith(key) ? [{ value: "--show", insertText: "/secret list --show", description: "Print plaintext values in the REPL", arguments: "optional", kind: "secret-action" }] : [];
2423
+ return [];
2424
+ }
2425
+ if (normalizedAction === "set" || normalizedAction === "request") {
2426
+ if (argumentTokens.length <= 1 && hasTrailingSpace)
2427
+ return [];
2428
+ return [];
2429
+ }
2430
+ if (normalizedAction === "rename") {
2431
+ if (argumentTokens.length <= 1)
2432
+ return hasTrailingSpace ? secretKeyCompletions(secrets, "", normalizedAction) : [];
2433
+ if (argumentTokens.length === 2 && !hasTrailingSpace)
2434
+ return secretKeyCompletions(secrets, key, normalizedAction);
2435
+ if (argumentTokens.length === 2 && hasTrailingSpace)
2436
+ return [];
2437
+ if (argumentTokens.length === 3 && !hasTrailingSpace && newKey)
2438
+ return [];
2439
+ return [];
2440
+ }
2441
+ if (normalizedAction === "get" || normalizedAction === "info" || normalizedAction === "delete") {
2442
+ if (argumentTokens.length <= 1)
2443
+ return hasTrailingSpace ? secretKeyCompletions(secrets, "", normalizedAction) : [];
2444
+ if (argumentTokens.length === 2 && !hasTrailingSpace)
2445
+ return secretKeyCompletions(secrets, key, normalizedAction);
2446
+ return [];
2447
+ }
2448
+ return [];
2449
+ }
2450
+ function secretCanonicalAction(action) {
2451
+ const lower = action.toLowerCase();
2452
+ if (lower === "ls")
2453
+ return "list";
2454
+ if (lower === "show")
2455
+ return "get";
2456
+ if (lower === "empty")
2457
+ return "request";
2458
+ if (lower === "mv")
2459
+ return "rename";
2460
+ if (lower === "remove" || lower === "rm")
2461
+ return "delete";
2462
+ return ["list", "get", "set", "request", "info", "rename", "delete"].includes(lower) ? lower : undefined;
2463
+ }
2464
+ function secretActionCompletions(current) {
2465
+ return SECRET_COMMAND_ACTIONS
2466
+ .flatMap((action) => [action.name, ...("aliases" in action ? action.aliases ?? [] : [])].map((name) => ({ name, description: action.description })))
2467
+ .filter((action) => action.name.startsWith(current.toLowerCase()))
2468
+ .map((action) => ({
2469
+ value: action.name,
2470
+ insertText: `/secret ${action.name} `,
2471
+ description: action.description,
2472
+ arguments: "optional",
2473
+ kind: "secret-action",
2474
+ }));
2475
+ }
2476
+ function secretKeyCompletions(secrets, current, action) {
2477
+ return secrets
2478
+ .filter((secret) => secret.key.toLowerCase().includes(current.toLowerCase()))
2479
+ .map((secret) => ({
2480
+ value: secret.key,
2481
+ insertText: `/secret ${action} ${secret.key}${action === "rename" ? " " : ""}`,
2482
+ description: `${secret.status} · length=${secret.length}${secret.requestReason ? ` · ${secret.requestReason}` : ""}`,
2483
+ arguments: "optional",
2484
+ kind: "secret-key",
2485
+ }));
2486
+ }
1829
2487
  function modelCommandCompletions(prefix) {
1830
2488
  const hasTrailingSpace = /\s$/.test(prefix);
1831
2489
  const tokens = prefix.trim().split(/\s+/).filter(Boolean);
@@ -1911,11 +2569,11 @@ function slashCompletionViewHeight(completions) {
1911
2569
  return 0;
1912
2570
  return Math.min(completions.length, SLASH_COMPLETION_PAGE_SIZE) + 2;
1913
2571
  }
1914
- function slashCompletionSelectableCount(text, cursor) {
1915
- return slashCommandCompletions(text, cursor).length;
2572
+ function slashCompletionSelectableCount(text, cursor, skills = [], secrets = []) {
2573
+ return slashCommandCompletions(text, cursor, skills, secrets).length;
1916
2574
  }
1917
- function selectedSlashCommandCompletion(text, cursor, selectedIndex) {
1918
- const completions = slashCommandCompletions(text, cursor);
2575
+ function selectedSlashCommandCompletion(text, cursor, selectedIndex, skills = [], secrets = []) {
2576
+ const completions = slashCommandCompletions(text, cursor, skills, secrets);
1919
2577
  if (completions.length === 0)
1920
2578
  return undefined;
1921
2579
  return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
@@ -1988,6 +2646,188 @@ function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
1988
2646
  e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
1989
2647
  ].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
1990
2648
  }
2649
+ async function handleSecretCommand(command, runtime) {
2650
+ const usage = "Usage: /secret <list|get|set|request|delete|rename|info> ...";
2651
+ const action = command.action ?? "list";
2652
+ const requireKey = () => {
2653
+ if (!command.key)
2654
+ throw new Error(usage);
2655
+ return command.key;
2656
+ };
2657
+ if (action === "list") {
2658
+ const entries = await runtime.secretStore.list();
2659
+ if (entries.length === 0)
2660
+ return systemLine("No secrets stored.");
2661
+ const lines = await Promise.all(entries.map(async (entry) => {
2662
+ if (command.show) {
2663
+ const value = entry.status === "set" ? await runtime.secretStore.getPlaintext(entry.key) : "";
2664
+ return `${entry.key} = ${value}`;
2665
+ }
2666
+ const reason = entry.requestReason ? ` reason=${JSON.stringify(entry.requestReason)}` : "";
2667
+ return `${entry.key}\t${entry.status}\tlength=${entry.length}${reason}`;
2668
+ }));
2669
+ return systemLine(lines.join("\n"), EXPANDED_SUMMARY_MAX_LINES);
2670
+ }
2671
+ if (action === "get") {
2672
+ const key = requireKey();
2673
+ const info = await runtime.secretStore.info(key);
2674
+ if (!info)
2675
+ return systemLine(`Secret "${key}" does not exist.`);
2676
+ const value = await runtime.secretStore.getPlaintext(key);
2677
+ return systemLine(info.status === "empty" ? `Secret "${key}" is empty.` : value, EXPANDED_SUMMARY_MAX_LINES);
2678
+ }
2679
+ if (action === "set") {
2680
+ const key = requireKey();
2681
+ const meta = await runtime.secretStore.setPlaintext(key, command.value ?? "");
2682
+ return systemLine(`Secret "${meta.key}" saved, status=${meta.status}, length=${meta.length}.`);
2683
+ }
2684
+ if (action === "request" || action === "empty") {
2685
+ const key = requireKey();
2686
+ const meta = await runtime.secretStore.requestEmpty(key, { reason: command.reason, requestedBy: "user" });
2687
+ return systemLine(`Secret "${meta.key}" is ${meta.status}. Fill it with: /secret set ${meta.key} <value>`);
2688
+ }
2689
+ if (action === "delete") {
2690
+ const key = requireKey();
2691
+ const deleted = await runtime.secretStore.delete(key);
2692
+ return systemLine(deleted ? `Secret "${key}" deleted.` : `Secret "${key}" did not exist.`);
2693
+ }
2694
+ if (action === "rename") {
2695
+ const key = requireKey();
2696
+ if (!command.newKey)
2697
+ throw new Error("Usage: /secret rename <oldKey> <newKey>");
2698
+ const meta = await runtime.secretStore.rename(key, command.newKey);
2699
+ return systemLine(`Secret renamed to "${meta.key}".`);
2700
+ }
2701
+ if (action === "info") {
2702
+ const key = requireKey();
2703
+ const info = await runtime.secretStore.info(key);
2704
+ return systemLine(info ? formatReplData(info, 4000) : `Secret "${key}" does not exist.`, EXPANDED_SUMMARY_MAX_LINES);
2705
+ }
2706
+ return systemLine(usage);
2707
+ }
2708
+ async function handleSkillCommand(command, runtime) {
2709
+ if (command.action === "import")
2710
+ return handleSkillImportCommand(command, runtime);
2711
+ if (command.action === "delete")
2712
+ return handleSkillDeleteCommand(command, runtime);
2713
+ if (!command.name) {
2714
+ const skills = await runtime.skills.list();
2715
+ return systemLine(formatSkillList(skills), EXPANDED_SUMMARY_MAX_LINES);
2716
+ }
2717
+ const skill = await runtime.skills.get(command.name);
2718
+ if (!skill)
2719
+ return { kind: "error", text: `Unknown skill: ${command.name}\nUse /skill to list available skills.` };
2720
+ return systemLine(formatSkillDetails(skill), EXPANDED_SUMMARY_MAX_LINES);
2721
+ }
2722
+ async function handleSkillImportCommand(command, runtime) {
2723
+ if (!command.path)
2724
+ return { kind: "error", text: "Usage: /skill import <path-to-skill-directory> [name]" };
2725
+ const sourceDirectory = path.resolve(command.path);
2726
+ const skillFile = path.join(sourceDirectory, "SKILL.md");
2727
+ try {
2728
+ const stat = await fs.stat(skillFile);
2729
+ if (!stat.isFile())
2730
+ return { kind: "error", text: `SKILL.md is not a file: ${skillFile}` };
2731
+ }
2732
+ catch (error) {
2733
+ return { kind: "error", text: `Invalid skill path: ${skillFile}\n${error instanceof Error ? error.message : String(error)}` };
2734
+ }
2735
+ const name = requireSkillName(command.name ?? path.basename(sourceDirectory));
2736
+ const linkPath = path.join(runtime.skillWorkspaceRoot, name);
2737
+ const relativeTarget = path.relative(path.dirname(linkPath), sourceDirectory) || sourceDirectory;
2738
+ try {
2739
+ await fs.mkdir(runtime.skillWorkspaceRoot, { recursive: true });
2740
+ const existing = await safeLstat(linkPath);
2741
+ if (existing)
2742
+ return { kind: "error", text: `Skill already exists at ${linkPath}. Delete it first with /skill delete ${name}.` };
2743
+ await fs.symlink(relativeTarget, linkPath, "junction");
2744
+ const imported = await runtime.skills.get(name);
2745
+ return systemLine(`Imported skill ${name}\nLink: ${linkPath}\nTarget: ${sourceDirectory}${imported ? `\nDescription: ${imported.description}` : ""}`);
2746
+ }
2747
+ catch (error) {
2748
+ return { kind: "error", text: `Failed to import skill ${name}: ${error instanceof Error ? error.message : String(error)}` };
2749
+ }
2750
+ }
2751
+ async function handleSkillDeleteCommand(command, runtime) {
2752
+ if (!command.name)
2753
+ return { kind: "error", text: "Usage: /skill delete <name>" };
2754
+ return handleSkillDeleteByName(command.name, runtime);
2755
+ }
2756
+ async function handleSkillDeleteByName(nameInput, runtime) {
2757
+ const name = requireSkillName(nameInput);
2758
+ const skillPath = path.join(runtime.skillWorkspaceRoot, name);
2759
+ const existing = await safeLstat(skillPath);
2760
+ if (!existing)
2761
+ return { kind: "error", text: `No workspace skill named ${name} at ${skillPath}` };
2762
+ try {
2763
+ if (existing.isSymbolicLink())
2764
+ await fs.unlink(skillPath);
2765
+ else
2766
+ await fs.rm(skillPath, { recursive: true, force: true });
2767
+ return systemLine(`Deleted workspace skill ${name}: ${skillPath}`);
2768
+ }
2769
+ catch (error) {
2770
+ return { kind: "error", text: `Failed to delete skill ${name}: ${error instanceof Error ? error.message : String(error)}` };
2771
+ }
2772
+ }
2773
+ async function safeLstat(file) {
2774
+ try {
2775
+ return await fs.lstat(file);
2776
+ }
2777
+ catch {
2778
+ return undefined;
2779
+ }
2780
+ }
2781
+ function renderSkillInvocationPrompt(name, args) {
2782
+ return `Use skill ${JSON.stringify(name)} with these arguments:\n${args}`;
2783
+ }
2784
+ function formatSkillList(skills) {
2785
+ if (skills.length === 0) {
2786
+ return [
2787
+ "No skills found.",
2788
+ "Skill roots:",
2789
+ " - .neo/skills/<name>/SKILL.md",
2790
+ " - ~/.neoctl/skills/<name>/SKILL.md",
2791
+ "Create one with the skill_create tool or add a SKILL.md file.",
2792
+ ].join("\n");
2793
+ }
2794
+ const width = Math.max(...skills.map((skill) => skill.name.length));
2795
+ return [
2796
+ "Available skills:",
2797
+ ...skills.map((skill) => {
2798
+ const tags = skill.tags?.length ? ` [${skill.tags.join(", ")}]` : "";
2799
+ const execution = skill.execution ? ` (${skill.execution})` : "";
2800
+ return ` ${skill.name.padEnd(width)} ${skill.description}${execution}${tags}`;
2801
+ }),
2802
+ "",
2803
+ "Usage:",
2804
+ " /skill <name> Show skill details",
2805
+ " /skill <name> <args> Invoke skill with arguments",
2806
+ " /skill import <path> [name] Import by linking a skill directory",
2807
+ " /skill delete <name> Delete workspace skill link/directory",
2808
+ ].join("\n");
2809
+ }
2810
+ function formatSkillDetails(skill) {
2811
+ const lines = [
2812
+ `Skill: ${skill.name}`,
2813
+ skill.title ? `Title: ${skill.title}` : undefined,
2814
+ `Description: ${skill.description}`,
2815
+ `Execution: ${skill.execution}`,
2816
+ skill.version ? `Version: ${skill.version}` : undefined,
2817
+ skill.tags?.length ? `Tags: ${skill.tags.join(", ")}` : undefined,
2818
+ skill.allowedTools?.length ? `Allowed tools: ${skill.allowedTools.join(", ")}` : undefined,
2819
+ skill.model ? `Model: ${skill.model}` : undefined,
2820
+ skill.effort ? `Effort: ${skill.effort}` : undefined,
2821
+ skill.trustLevel ? `Trust: ${skill.trustLevel}` : undefined,
2822
+ skill.source?.path ? `Path: ${skill.source.path}` : undefined,
2823
+ "",
2824
+ "Entrypoint:",
2825
+ skill.entrypoint,
2826
+ "",
2827
+ `Invoke: /skill ${skill.name} <args>`,
2828
+ ].filter((line) => line !== undefined);
2829
+ return lines.join("\n");
2830
+ }
1991
2831
  async function handleModelCommand(command, runtime) {
1992
2832
  const current = runtime.engine.getModelSettings();
1993
2833
  const nextModel = command.model ?? current.model;
@@ -2069,10 +2909,6 @@ function currentModelProvider() {
2069
2909
  function modelEnvKeyForProvider(provider) {
2070
2910
  if (provider === "anthropic")
2071
2911
  return "ANTHROPIC_MODEL";
2072
- if (provider === "deepseek")
2073
- return "DEEPSEEK_MODEL";
2074
- if (provider === "kimi")
2075
- return "KIMI_MODEL";
2076
2912
  return "OPENAI_MODEL";
2077
2913
  }
2078
2914
  function envValueForReasoning(reasoning) {
@@ -2180,6 +3016,8 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
2180
3016
  rendered = true;
2181
3017
  }
2182
3018
  if (block.type === "thinking") {
3019
+ if (options.includeThinkingBlocks === false)
3020
+ continue;
2183
3021
  append(thinkingLine(block.text));
2184
3022
  rendered = true;
2185
3023
  }
@@ -2323,7 +3161,25 @@ async function handleSessionsCommand(runtime, runningSessionIds, setBrowser, app
2323
3161
  append(systemLine("No saved sessions found."));
2324
3162
  return;
2325
3163
  }
2326
- setBrowser({ sessions, runningSessionIds, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
3164
+ setBrowser({ items: sessions, sessions, runningSessionIds, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
3165
+ }
3166
+ async function handleSkillsCommand(runtime, setBrowser, append) {
3167
+ const skills = await runtime.skills.list();
3168
+ if (skills.length === 0) {
3169
+ setBrowser(undefined);
3170
+ append(systemLine(formatSkillList(skills), EXPANDED_SUMMARY_MAX_LINES));
3171
+ return;
3172
+ }
3173
+ setBrowser({ items: skills, skills, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
3174
+ }
3175
+ async function handleSecretsCommand(runtime, setBrowser, append) {
3176
+ const secrets = await runtime.secretStore.list();
3177
+ if (secrets.length === 0) {
3178
+ setBrowser(undefined);
3179
+ append(systemLine("No secrets stored. Press /secret set <key> <value> to add one, or /secret request <key> to create an empty placeholder."));
3180
+ return;
3181
+ }
3182
+ setBrowser({ items: secrets, secrets, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
2327
3183
  }
2328
3184
  async function handleExportCommand(command, runtime) {
2329
3185
  const snapshot = runtime.engine.snapshot();
@@ -2371,6 +3227,7 @@ async function handleDeleteSessionCommand(sessionId, current, runtime, setBrowse
2371
3227
  const pageLength = nextSessions.slice(pageIndex * current.pageSize, pageIndex * current.pageSize + current.pageSize).length;
2372
3228
  setBrowser({
2373
3229
  ...current,
3230
+ items: nextSessions,
2374
3231
  sessions: nextSessions,
2375
3232
  runningSessionIds: current.runningSessionIds.filter((id) => id !== sessionId),
2376
3233
  pageIndex,
@@ -2408,11 +3265,11 @@ function restoredHistoryLines(runtime) {
2408
3265
  return lines.length;
2409
3266
  };
2410
3267
  for (const message of runtime.engine.getHistoryMessages()) {
2411
- renderMessage(message, append, undefined, { includeToolUseBlocks: true });
3268
+ renderMessage(message, append, undefined, { includeToolUseBlocks: true, includeThinkingBlocks: false });
2412
3269
  }
2413
3270
  return lines;
2414
3271
  }
2415
- const LOGIN_PROVIDERS = ["openai", "anthropic", "deepseek", "kimi"];
3272
+ const LOGIN_PROVIDERS = ["openai", "anthropic"];
2416
3273
  const SHARED_LOGIN_FIELDS = [
2417
3274
  { key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
2418
3275
  { key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
@@ -2438,20 +3295,6 @@ const LOGIN_FIELD_DEFINITIONS = {
2438
3295
  { key: "version", label: "Anthropic version", envKey: "ANTHROPIC_VERSION", scope: "provider", placeholder: "2023-06-01" },
2439
3296
  ...SHARED_LOGIN_FIELDS,
2440
3297
  ],
2441
- deepseek: [
2442
- { key: "apiKey", label: "API key", envKey: "DEEPSEEK_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
2443
- { key: "baseUrl", label: "Base URL", envKey: "DEEPSEEK_BASE_URL", scope: "provider", placeholder: "https://api.deepseek.com" },
2444
- { key: "model", label: "Model", envKey: "DEEPSEEK_MODEL", scope: "provider", required: true, placeholder: "deepseek-chat" },
2445
- { key: "fallbackModel", label: "Fallback model", envKey: "DEEPSEEK_FALLBACK_MODEL", scope: "provider" },
2446
- ...SHARED_LOGIN_FIELDS,
2447
- ],
2448
- kimi: [
2449
- { key: "apiKey", label: "API key", envKey: "KIMI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
2450
- { key: "baseUrl", label: "Base URL", envKey: "KIMI_BASE_URL", scope: "provider", placeholder: "https://api.moonshot.cn/v1" },
2451
- { key: "model", label: "Model", envKey: "KIMI_MODEL", scope: "provider", required: true, placeholder: "kimi-k2.6" },
2452
- { key: "fallbackModel", label: "Fallback model", envKey: "KIMI_FALLBACK_MODEL", scope: "provider" },
2453
- ...SHARED_LOGIN_FIELDS,
2454
- ],
2455
3298
  };
2456
3299
  const DEPRECATED_MODEL_ENV_KEYS = [
2457
3300
  "MODEL_API_KEY",
@@ -2472,55 +3315,55 @@ const DEPRECATED_MODEL_ENV_KEYS = [
2472
3315
  "ANTHROPIC_TIMEOUT_MS",
2473
3316
  "ANTHROPIC_STREAM_IDLE_TIMEOUT_MS",
2474
3317
  "ANTHROPIC_MAX_RETRIES",
2475
- "DEEPSEEK_REASONING_EFFORT",
2476
- "DEEPSEEK_REASONING_SUMMARY",
2477
- "DEEPSEEK_MAX_OUTPUT_TOKENS",
2478
- "DEEPSEEK_TIMEOUT_MS",
2479
- "DEEPSEEK_STREAM_IDLE_TIMEOUT_MS",
2480
- "DEEPSEEK_MAX_RETRIES",
2481
- "KIMI_REASONING_EFFORT",
2482
- "KIMI_REASONING_SUMMARY",
2483
- "KIMI_MAX_OUTPUT_TOKENS",
2484
- "KIMI_TIMEOUT_MS",
2485
- "KIMI_STREAM_IDLE_TIMEOUT_MS",
2486
- "KIMI_MAX_RETRIES",
2487
- "MOONSHOT_REASONING_EFFORT",
2488
- "MOONSHOT_REASONING_SUMMARY",
2489
- "MOONSHOT_MAX_OUTPUT_TOKENS",
2490
- "MOONSHOT_TIMEOUT_MS",
2491
- "MOONSHOT_STREAM_IDLE_TIMEOUT_MS",
2492
- "MOONSHOT_MAX_RETRIES",
2493
3318
  ];
2494
- function sessionsPageCount(state) {
2495
- return Math.max(1, Math.ceil(state.sessions.length / state.pageSize));
3319
+ function pagedPageCount(state) {
3320
+ return Math.max(1, Math.ceil(state.items.length / state.pageSize));
2496
3321
  }
2497
- function sessionsPageItems(state) {
3322
+ function pagedPageItems(state) {
2498
3323
  const start = state.pageIndex * state.pageSize;
2499
- return state.sessions.slice(start, start + state.pageSize);
3324
+ return state.items.slice(start, start + state.pageSize);
2500
3325
  }
2501
- function sessionAbsoluteIndex(state) {
3326
+ function pagedAbsoluteIndex(state) {
2502
3327
  return state.pageIndex * state.pageSize + state.selectedIndex;
2503
3328
  }
2504
- function moveSessionsSelection(state, delta) {
2505
- const pageLength = sessionsPageItems(state).length;
3329
+ function movePagedSelection(state, delta) {
3330
+ const pageLength = pagedPageItems(state).length;
2506
3331
  if (pageLength <= 0)
2507
3332
  return state;
2508
3333
  const selectedIndex = (state.selectedIndex + delta + pageLength) % pageLength;
2509
3334
  return { ...state, selectedIndex };
2510
3335
  }
2511
- function moveSessionsPage(state, delta) {
2512
- const pageCount = sessionsPageCount(state);
3336
+ function movePagedPage(state, delta) {
3337
+ const pageCount = pagedPageCount(state);
2513
3338
  if (pageCount <= 1)
2514
3339
  return state;
2515
3340
  const pageIndex = (state.pageIndex + delta + pageCount) % pageCount;
2516
- const pageLength = state.sessions.slice(pageIndex * state.pageSize, pageIndex * state.pageSize + state.pageSize).length;
3341
+ const pageLength = state.items.slice(pageIndex * state.pageSize, pageIndex * state.pageSize + state.pageSize).length;
2517
3342
  return { ...state, pageIndex, selectedIndex: Math.min(state.selectedIndex, Math.max(0, pageLength - 1)) };
2518
3343
  }
3344
+ function sessionsPageItems(state) {
3345
+ return pagedPageItems(state);
3346
+ }
3347
+ function sessionAbsoluteIndex(state) {
3348
+ return pagedAbsoluteIndex(state);
3349
+ }
3350
+ function moveSessionsSelection(state, delta) {
3351
+ return movePagedSelection(state, delta);
3352
+ }
3353
+ function moveSessionsPage(state, delta) {
3354
+ return movePagedPage(state, delta);
3355
+ }
2519
3356
  function sessionsBrowserViewHeight(state) {
2520
3357
  return sessionsPageItems(state).length + 3;
2521
3358
  }
3359
+ function skillsBrowserViewHeight(state) {
3360
+ return pagedPageItems(state).length + 3;
3361
+ }
3362
+ function secretsBrowserViewHeight(state) {
3363
+ return pagedPageItems(state).length + 3;
3364
+ }
2522
3365
  function SessionsBrowser({ state, width }) {
2523
- const pageCount = sessionsPageCount(state);
3366
+ const pageCount = pagedPageCount(state);
2524
3367
  const pageItems = sessionsPageItems(state);
2525
3368
  const showPagination = pageCount > 1;
2526
3369
  const contentWidth = Math.max(20, width);
@@ -2540,6 +3383,51 @@ function SessionsBrowser({ state, width }) {
2540
3383
  }, row.numberPrefix), row.rest);
2541
3384
  }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
2542
3385
  }
3386
+ function SkillsBrowser({ state, width }) {
3387
+ const pageCount = pagedPageCount(state);
3388
+ const pageItems = pagedPageItems(state);
3389
+ const showPagination = pageCount > 1;
3390
+ const contentWidth = Math.max(20, width);
3391
+ const header = showPagination
3392
+ ? `Skills (${state.skills.length}) · page ${state.pageIndex + 1}/${pageCount}`
3393
+ : `Skills (${state.skills.length})`;
3394
+ const footer = showPagination
3395
+ ? "↑/↓ select · ←/→ page · Enter details · i invoke · a import · d/Delete remove · Esc close"
3396
+ : "↑/↓ select · Enter details · i invoke · a import · d/Delete remove · Esc close";
3397
+ const nameWidth = Math.min(28, Math.max(...pageItems.map((skill) => skill.name.length)));
3398
+ return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((skill, index) => {
3399
+ const selected = index === state.selectedIndex;
3400
+ const absoluteIndex = state.pageIndex * state.pageSize + index;
3401
+ const prefix = `${absoluteIndex + 1}.`.padStart(String(state.skills.length).length + 1);
3402
+ const tags = skill.tags?.length ? ` [${skill.tags.join(",")}]` : "";
3403
+ const execution = skill.execution ? ` (${skill.execution})` : "";
3404
+ const restWidth = Math.max(0, contentWidth - prefix.length - nameWidth - 4);
3405
+ const rest = fitToWidth(`${skill.description}${execution}${tags}`, restWidth);
3406
+ return e(Text, { key: skill.name, color: "white" }, e(Text, { color: selected ? "black" : "white", backgroundColor: selected ? "cyan" : undefined }, prefix), e(Text, { color: "gray" }, " "), e(Text, { color: "cyan" }, fitToWidth(skill.name, nameWidth).padEnd(nameWidth)), e(Text, { color: "gray" }, " "), e(Text, { color: selected ? "white" : "gray" }, rest));
3407
+ }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
3408
+ }
3409
+ function SecretsBrowser({ state, width }) {
3410
+ const pageCount = pagedPageCount(state);
3411
+ const pageItems = pagedPageItems(state);
3412
+ const showPagination = pageCount > 1;
3413
+ const contentWidth = Math.max(20, width);
3414
+ const header = showPagination
3415
+ ? `Secrets (${state.secrets.length}) · page ${state.pageIndex + 1}/${pageCount}`
3416
+ : `Secrets (${state.secrets.length})`;
3417
+ const footer = showPagination
3418
+ ? "↑/↓ select · ←/→ page · Enter info · s set · r rename · a add · e empty · d/Delete remove · Esc close"
3419
+ : "↑/↓ select · Enter info · s set · r rename · a add · e empty · d/Delete remove · Esc close";
3420
+ const keyWidth = Math.min(32, Math.max(...pageItems.map((secret) => secret.key.length)));
3421
+ return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((secret, index) => {
3422
+ const selected = index === state.selectedIndex;
3423
+ const absoluteIndex = state.pageIndex * state.pageSize + index;
3424
+ const prefix = `${absoluteIndex + 1}.`.padStart(String(state.secrets.length).length + 1);
3425
+ const reason = secret.requestReason ? ` reason=${JSON.stringify(secret.requestReason)}` : "";
3426
+ const restWidth = Math.max(0, contentWidth - prefix.length - keyWidth - 4);
3427
+ const rest = fitToWidth(`${secret.status} · length=${secret.length}${reason}`, restWidth);
3428
+ return e(Text, { key: secret.key, color: "white" }, e(Text, { color: selected ? "black" : "white", backgroundColor: selected ? "cyan" : undefined }, prefix), e(Text, { color: "gray" }, " "), e(Text, { color: secret.status === "set" ? "green" : "yellow" }, fitToWidth(secret.key, keyWidth).padEnd(keyWidth)), e(Text, { color: "gray" }, " "), e(Text, { color: selected ? "white" : "gray" }, rest));
3429
+ }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
3430
+ }
2543
3431
  function handleLoginFormInput(value, key, state, setLoginFormState, runtime, append, setStatus) {
2544
3432
  if (key.escape) {
2545
3433
  if (state.step === "fields")
@@ -2708,12 +3596,6 @@ function loginValuesForProvider(provider, env) {
2708
3596
  for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
2709
3597
  values[field.key] = env[field.envKey] ?? "";
2710
3598
  }
2711
- if (provider === "kimi") {
2712
- values.apiKey ||= env.MOONSHOT_API_KEY ?? process.env.MOONSHOT_API_KEY ?? "";
2713
- values.baseUrl ||= env.MOONSHOT_BASE_URL ?? process.env.MOONSHOT_BASE_URL ?? "";
2714
- values.model ||= env.MOONSHOT_MODEL ?? process.env.MOONSHOT_MODEL ?? "";
2715
- values.fallbackModel ||= env.MOONSHOT_FALLBACK_MODEL ?? process.env.MOONSHOT_FALLBACK_MODEL ?? "";
2716
- }
2717
3599
  if (!values.baseUrl)
2718
3600
  values.baseUrl = defaultBaseUrlForLoginProvider(provider);
2719
3601
  if (!values.model)
@@ -2723,15 +3605,11 @@ function loginValuesForProvider(provider, env) {
2723
3605
  return values;
2724
3606
  }
2725
3607
  function parseLoginProvider(value) {
2726
- if (value === "openai" || value === "anthropic" || value === "deepseek" || value === "kimi")
3608
+ if (value === "openai" || value === "anthropic")
2727
3609
  return value;
2728
3610
  return undefined;
2729
3611
  }
2730
3612
  function guessLoginProvider(env) {
2731
- if (env.KIMI_API_KEY ?? env.MOONSHOT_API_KEY ?? process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY)
2732
- return "kimi";
2733
- if (env.DEEPSEEK_API_KEY ?? process.env.DEEPSEEK_API_KEY)
2734
- return "deepseek";
2735
3613
  if (env.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY)
2736
3614
  return "anthropic";
2737
3615
  return "openai";
@@ -2739,19 +3617,11 @@ function guessLoginProvider(env) {
2739
3617
  function defaultBaseUrlForLoginProvider(provider) {
2740
3618
  if (provider === "anthropic")
2741
3619
  return "https://api.anthropic.com";
2742
- if (provider === "deepseek")
2743
- return "https://api.deepseek.com";
2744
- if (provider === "kimi")
2745
- return "https://api.moonshot.cn/v1";
2746
3620
  return "https://api.openai.com";
2747
3621
  }
2748
3622
  function defaultModelForLoginProvider(provider) {
2749
3623
  if (provider === "anthropic")
2750
3624
  return "claude-sonnet-4-6";
2751
- if (provider === "deepseek")
2752
- return "deepseek-chat";
2753
- if (provider === "kimi")
2754
- return "kimi-k2.6";
2755
3625
  return "gpt-5.5";
2756
3626
  }
2757
3627
  function loginFormViewHeight(state) {
@@ -2770,7 +3640,7 @@ function LoginFormView({ state, width }) {
2770
3640
  const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
2771
3641
  const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
2772
3642
  return e(Text, { key: field.key, color: "white" }, e(Text, { color: selected ? "black" : "white", backgroundColor: selected ? "cyan" : undefined }, `${index + 1}.`.padStart(3)), e(Text, { color: field.required ? "yellow" : "gray" }, ` ${field.label.padEnd(maxLabel)} `), e(Text, { color: field.scope === "shared" ? "blue" : "gray" }, field.scope === "shared" ? "shared " : "provider "), e(Text, { color: rawValue ? "white" : "gray" }, fitToWidth(`${visibleValue}${placeholder}`, Math.max(8, contentWidth - maxLabel - 14))));
2773
- }), e(Text, { color: "gray" }, fitToWidth("↑/↓ field · ←/→ cursor · type edit · Tab cycle choices · Enter save · Esc back/cancel", contentWidth)), e(Text, { color: "gray" }, fitToWidth("Provider fields save as OPENAI_* / ANTHROPIC_* / DEEPSEEK_* / KIMI_*; shared runtime fields save as MODEL_*.", contentWidth)));
3643
+ }), e(Text, { color: "gray" }, fitToWidth("↑/↓ field · ←/→ cursor · type edit · Tab cycle choices · Enter save · Esc back/cancel", contentWidth)), e(Text, { color: "gray" }, fitToWidth("Provider fields save as OPENAI_* / ANTHROPIC_*; shared runtime fields save as MODEL_*.", contentWidth)));
2774
3644
  }
2775
3645
  function formatLoginFieldValue(field, value, cursor) {
2776
3646
  const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
@@ -2796,12 +3666,6 @@ function envEntriesForLoginForm(state) {
2796
3666
  const value = (state.values[field.key] ?? "").trim();
2797
3667
  entries[field.envKey] = value || undefined;
2798
3668
  }
2799
- if (state.provider === "kimi") {
2800
- entries.MOONSHOT_API_KEY = undefined;
2801
- entries.MOONSHOT_BASE_URL = undefined;
2802
- entries.MOONSHOT_MODEL = undefined;
2803
- entries.MOONSHOT_FALLBACK_MODEL = undefined;
2804
- }
2805
3669
  return entries;
2806
3670
  }
2807
3671
  function updateEnvContent(content, updates, removeKeys = []) {
@@ -2829,8 +3693,6 @@ function updateEnvContent(content, updates, removeKeys = []) {
2829
3693
  appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
2830
3694
  appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
2831
3695
  appendEnvGroup(updatedLines, "# Anthropic provider settings", grouped.anthropic);
2832
- appendEnvGroup(updatedLines, "# DeepSeek provider settings", grouped.deepseek);
2833
- appendEnvGroup(updatedLines, "# Kimi provider settings", grouped.kimi);
2834
3696
  appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
2835
3697
  }
2836
3698
  return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
@@ -2840,8 +3702,6 @@ function groupLoginEnvEntries(entries) {
2840
3702
  active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
2841
3703
  openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
2842
3704
  anthropic: entries.filter(([key]) => key.startsWith("ANTHROPIC_")),
2843
- deepseek: entries.filter(([key]) => key.startsWith("DEEPSEEK_")),
2844
- kimi: entries.filter(([key]) => key.startsWith("KIMI_") || key.startsWith("MOONSHOT_")),
2845
3705
  shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
2846
3706
  };
2847
3707
  }
@@ -3034,9 +3894,11 @@ function formatToolUse(toolUse) {
3034
3894
  text: formatPlanToolPayload(toolUse.input),
3035
3895
  };
3036
3896
  }
3897
+ const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
3037
3898
  return {
3038
3899
  kind: "tool",
3039
3900
  title: toolTitle(toolUse.name, "running"),
3901
+ bodyTitle: description,
3040
3902
  text: formatJson(toolUse.input, 1200),
3041
3903
  previewStyle: "summary",
3042
3904
  };
@@ -3063,9 +3925,11 @@ function formatToolResultLine(toolName, output, ok) {
3063
3925
  }
3064
3926
  function formatToolFinishedWithoutResult(toolUse, ok) {
3065
3927
  const inputText = formatJson(toolUse.input, 1200);
3928
+ const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
3066
3929
  return {
3067
3930
  kind: ok ? "tool" : "error",
3068
3931
  title: toolTitle(toolUse.name, "finished"),
3932
+ bodyTitle: description,
3069
3933
  titleStatus: ok ? "success" : "failure",
3070
3934
  text: inputText ? `${ok ? "finished" : "failed"}\n${inputText}` : ok ? "finished" : "failed",
3071
3935
  previewStyle: "summary",
@@ -3078,15 +3942,27 @@ function toolTitle(toolName, phase) {
3078
3942
  return `${phase === "running" ? "◇" : "◆"} plan`;
3079
3943
  return `${phase === "running" ? "◇" : "◆"} ${toolName}`;
3080
3944
  }
3945
+ function execDescriptionFromInput(input) {
3946
+ if (!isRecord(input))
3947
+ return undefined;
3948
+ const description = typeof input.description === "string" ? input.description.trim() : "";
3949
+ return description || undefined;
3950
+ }
3081
3951
  function isPlanToolPayload(value) {
3082
3952
  if (!isRecord(value) || !Array.isArray(value.items))
3083
3953
  return false;
3084
- return value.items.every((item) => {
3085
- if (!isRecord(item))
3086
- return false;
3087
- return (typeof item.description === "string" &&
3088
- (item.status === "pending" || item.status === "in_progress" || item.status === "completed"));
3089
- });
3954
+ return value.items.every(isPlanItemLike);
3955
+ }
3956
+ function isPlanItemLike(item) {
3957
+ if (!isRecord(item))
3958
+ return false;
3959
+ if (typeof item.description !== "string")
3960
+ return false;
3961
+ if (item.status !== "pending" && item.status !== "in_progress" && item.status !== "completed")
3962
+ return false;
3963
+ if (item.subitems === undefined)
3964
+ return true;
3965
+ return Array.isArray(item.subitems) && item.subitems.every(isPlanItemLike);
3090
3966
  }
3091
3967
  function planToolBodyTitle(payload) {
3092
3968
  const title = payload.title?.trim();
@@ -3098,16 +3974,25 @@ function formatPlanToolPayload(payload) {
3098
3974
  sections.push(payload.summary.trim());
3099
3975
  if (payload.note?.trim())
3100
3976
  sections.push(payload.note.trim());
3101
- sections.push(payload.items.map(formatPlanItem).join("\n"));
3977
+ sections.push(payload.items.flatMap((item) => formatPlanItem(item)).join("\n"));
3102
3978
  return sections.filter(Boolean).join("\n");
3103
3979
  }
3104
- function formatPlanItem(item) {
3980
+ function formatPlanItem(item, depth = 0) {
3981
+ const indent = " ".repeat(Math.max(0, depth));
3105
3982
  const text = escapePlanMarkdown(item.description.trim());
3106
- if (item.status === "completed")
3107
- return `- ~~${text}~~`;
3108
- if (item.status === "in_progress")
3109
- return `- ${text}`;
3110
- return `- ${text}`;
3983
+ const marker = planItemMarker(item.status);
3984
+ const line = item.status === "completed"
3985
+ ? `${indent}- ${marker} ~~${text}~~`
3986
+ : `${indent}- ${marker} ${text}`;
3987
+ const subitems = item.subitems?.flatMap((subitem) => formatPlanItem(subitem, depth + 1)) ?? [];
3988
+ return [line, ...subitems];
3989
+ }
3990
+ function planItemMarker(status) {
3991
+ if (status === "completed")
3992
+ return "✓";
3993
+ if (status === "in_progress")
3994
+ return "▶";
3995
+ return "○";
3111
3996
  }
3112
3997
  function escapePlanMarkdown(text) {
3113
3998
  return text.replace(/([\\`*_{}[\]()#+.!|>~-])/g, "\\$1");
@@ -3301,8 +4186,10 @@ function formatExecToolResult(output, ok) {
3301
4186
  : output.exitCode === 0
3302
4187
  ? "exit 0"
3303
4188
  : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
4189
+ const description = typeof output.description === "string" ? output.description.trim() : "";
3304
4190
  const lines = [
3305
4191
  "exec result",
4192
+ ...(description ? [`purpose: ${description}`] : []),
3306
4193
  `status: ${status}`,
3307
4194
  `duration: ${output.durationMs}ms`,
3308
4195
  `command: ${output.command}`,
@@ -3783,6 +4670,8 @@ const TERMINAL_TITLE_WORKING_PREFIX = "● ";
3783
4670
  const TERMINAL_TITLE_READY_PREFIX = "✓ ";
3784
4671
  const REPL_ANIMATION_INTERVAL_MS = 420;
3785
4672
  const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
4673
+ const SUBAGENT_ACTIVITY_UPDATE_DEBOUNCE_MS = 180;
4674
+ const SUBAGENT_COMPLETED_LINGER_MS = 8000;
3786
4675
  const TOKEN_PULSE_MS = 900;
3787
4676
  const ANIMATED_NUMBER_INTERVAL_MS = 50;
3788
4677
  const ANIMATED_NUMBER_MIN_DURATION_MS = 180;
@@ -3795,6 +4684,8 @@ const STATUS_SHIMMER_RADIUS = 1;
3795
4684
  const STATUS_SHIMMER_COLOR = "whiteBright";
3796
4685
  const STATUS_SEPARATOR = " · ";
3797
4686
  const STATUS_BAR_RENDER_ROWS = 2;
4687
+ const FOREGROUND_EXEC_DETACH_HINT_RENDER_ROWS = 1;
4688
+ const FOREGROUND_EXEC_DETACH_HINT_DELAY_MS = 2000;
3798
4689
  const BACKGROUND_TASK_STATUS_RENDER_ROWS = 1;
3799
4690
  const QUEUED_INPUT_RENDER_ROWS = 1;
3800
4691
  const EMPTY_CTRL_C_EXIT_PLACEHOLDER = "Press Ctrl+C again to exit";