@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1

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 (230) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/scripts/format-prompts.ts +1 -1
  6. package/src/autoresearch/helpers.ts +17 -0
  7. package/src/autoresearch/tools/log-experiment.ts +9 -17
  8. package/src/autoresearch/tools/run-experiment.ts +2 -17
  9. package/src/capability/skill.ts +7 -0
  10. package/src/cli/args.ts +2 -2
  11. package/src/cli/list-models.ts +1 -1
  12. package/src/cli/shell-cli.ts +3 -13
  13. package/src/cli/update-cli.ts +1 -1
  14. package/src/cli.ts +11 -29
  15. package/src/commands/acp.ts +24 -0
  16. package/src/commands/launch.ts +6 -4
  17. package/src/commit/agentic/prompts/system.md +1 -1
  18. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  19. package/src/commit/analysis/conventional.ts +8 -66
  20. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  21. package/src/commit/pipeline.ts +2 -2
  22. package/src/commit/shared-llm.ts +89 -0
  23. package/src/config/config-file.ts +210 -0
  24. package/src/config/model-equivalence.ts +8 -11
  25. package/src/config/model-registry.ts +13 -2
  26. package/src/config/model-resolver.ts +31 -4
  27. package/src/config/settings-schema.ts +102 -1
  28. package/src/config/settings.ts +1 -1
  29. package/src/config.ts +3 -219
  30. package/src/edit/index.ts +22 -1
  31. package/src/edit/modes/patch.ts +10 -0
  32. package/src/edit/modes/replace.ts +3 -0
  33. package/src/edit/renderer.ts +17 -1
  34. package/src/eval/js/context-manager.ts +1 -1
  35. package/src/eval/js/executor.ts +3 -0
  36. package/src/eval/js/shared/rewrite-imports.ts +122 -50
  37. package/src/eval/js/shared/runtime.ts +31 -4
  38. package/src/eval/js/tool-bridge.ts +43 -21
  39. package/src/eval/py/executor.ts +5 -0
  40. package/src/exa/factory.ts +2 -2
  41. package/src/exa/mcp-client.ts +74 -1
  42. package/src/exec/bash-executor.ts +5 -1
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +0 -11
  45. package/src/extensibility/extensions/runner.ts +55 -2
  46. package/src/extensibility/extensions/types.ts +98 -221
  47. package/src/extensibility/hooks/types.ts +89 -314
  48. package/src/extensibility/shared-events.ts +343 -0
  49. package/src/extensibility/skills.ts +42 -1
  50. package/src/goals/index.ts +3 -0
  51. package/src/goals/runtime.ts +500 -0
  52. package/src/goals/state.ts +37 -0
  53. package/src/goals/tools/goal-tool.ts +237 -0
  54. package/src/hashline/anchors.ts +2 -2
  55. package/src/hindsight/mental-models.ts +1 -1
  56. package/src/internal-urls/agent-protocol.ts +1 -20
  57. package/src/internal-urls/artifact-protocol.ts +1 -19
  58. package/src/internal-urls/docs-index.generated.ts +9 -10
  59. package/src/internal-urls/index.ts +1 -0
  60. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  61. package/src/internal-urls/registry-helpers.ts +25 -0
  62. package/src/internal-urls/router.ts +6 -3
  63. package/src/internal-urls/types.ts +22 -1
  64. package/src/main.ts +24 -11
  65. package/src/mcp/oauth-flow.ts +20 -0
  66. package/src/modes/acp/acp-agent.ts +412 -71
  67. package/src/modes/acp/acp-client-bridge.ts +152 -0
  68. package/src/modes/acp/acp-event-mapper.ts +180 -15
  69. package/src/modes/acp/terminal-auth.ts +37 -0
  70. package/src/modes/components/assistant-message.ts +14 -8
  71. package/src/modes/components/bash-execution.ts +24 -63
  72. package/src/modes/components/custom-message.ts +14 -40
  73. package/src/modes/components/eval-execution.ts +27 -57
  74. package/src/modes/components/execution-shared.ts +102 -0
  75. package/src/modes/components/hook-message.ts +17 -49
  76. package/src/modes/components/mcp-add-wizard.ts +26 -5
  77. package/src/modes/components/message-frame.ts +88 -0
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/read-tool-group.ts +29 -1
  80. package/src/modes/components/session-observer-overlay.ts +6 -2
  81. package/src/modes/components/session-selector.ts +1 -1
  82. package/src/modes/components/status-line/segments.ts +55 -4
  83. package/src/modes/components/status-line/types.ts +4 -0
  84. package/src/modes/components/status-line.ts +28 -10
  85. package/src/modes/components/tool-execution.ts +7 -8
  86. package/src/modes/controllers/command-controller-shared.ts +108 -0
  87. package/src/modes/controllers/command-controller.ts +27 -10
  88. package/src/modes/controllers/event-controller.ts +60 -18
  89. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  90. package/src/modes/controllers/input-controller.ts +85 -39
  91. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  92. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  93. package/src/modes/interactive-mode.ts +675 -39
  94. package/src/modes/print-mode.ts +16 -86
  95. package/src/modes/rpc/rpc-mode.ts +30 -88
  96. package/src/modes/runtime-init.ts +115 -0
  97. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  98. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  99. package/src/modes/theme/theme.ts +18 -6
  100. package/src/modes/types.ts +20 -5
  101. package/src/modes/utils/context-usage.ts +13 -13
  102. package/src/modes/utils/ui-helpers.ts +25 -6
  103. package/src/plan-mode/approved-plan.ts +35 -1
  104. package/src/prompts/agents/designer.md +5 -5
  105. package/src/prompts/agents/explore.md +7 -7
  106. package/src/prompts/agents/init.md +9 -9
  107. package/src/prompts/agents/librarian.md +14 -14
  108. package/src/prompts/agents/plan.md +4 -4
  109. package/src/prompts/agents/reviewer.md +5 -5
  110. package/src/prompts/agents/task.md +10 -10
  111. package/src/prompts/commands/orchestrate.md +2 -2
  112. package/src/prompts/compaction/branch-summary.md +3 -3
  113. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  114. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  115. package/src/prompts/compaction/compaction-summary.md +5 -5
  116. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  117. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  118. package/src/prompts/goals/goal-budget-limit.md +16 -0
  119. package/src/prompts/goals/goal-continuation.md +28 -0
  120. package/src/prompts/goals/goal-mode-active.md +23 -0
  121. package/src/prompts/memories/consolidation.md +2 -2
  122. package/src/prompts/memories/read-path.md +1 -1
  123. package/src/prompts/memories/stage_one_input.md +1 -1
  124. package/src/prompts/memories/stage_one_system.md +5 -5
  125. package/src/prompts/review-request.md +4 -4
  126. package/src/prompts/system/agent-creation-architect.md +17 -17
  127. package/src/prompts/system/agent-creation-user.md +2 -2
  128. package/src/prompts/system/commit-message-system.md +2 -2
  129. package/src/prompts/system/custom-system-prompt.md +2 -2
  130. package/src/prompts/system/eager-todo.md +6 -6
  131. package/src/prompts/system/handoff-document.md +1 -1
  132. package/src/prompts/system/plan-mode-active.md +25 -24
  133. package/src/prompts/system/plan-mode-approved.md +4 -4
  134. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  135. package/src/prompts/system/plan-mode-reference.md +2 -2
  136. package/src/prompts/system/plan-mode-subagent.md +8 -8
  137. package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
  138. package/src/prompts/system/project-prompt.md +4 -4
  139. package/src/prompts/system/subagent-system-prompt.md +7 -7
  140. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  141. package/src/prompts/system/system-prompt.md +72 -71
  142. package/src/prompts/system/ttsr-interrupt.md +1 -1
  143. package/src/prompts/tools/apply-patch.md +1 -1
  144. package/src/prompts/tools/ast-edit.md +3 -3
  145. package/src/prompts/tools/ast-grep.md +3 -3
  146. package/src/prompts/tools/bash.md +6 -0
  147. package/src/prompts/tools/browser.md +3 -3
  148. package/src/prompts/tools/checkpoint.md +3 -3
  149. package/src/prompts/tools/find.md +3 -3
  150. package/src/prompts/tools/github.md +2 -5
  151. package/src/prompts/tools/goal.md +13 -0
  152. package/src/prompts/tools/hashline.md +104 -116
  153. package/src/prompts/tools/image-gen.md +3 -3
  154. package/src/prompts/tools/irc.md +1 -1
  155. package/src/prompts/tools/lsp.md +2 -2
  156. package/src/prompts/tools/patch.md +6 -6
  157. package/src/prompts/tools/read.md +8 -7
  158. package/src/prompts/tools/replace.md +5 -5
  159. package/src/prompts/tools/resolve.md +6 -5
  160. package/src/prompts/tools/retain.md +1 -1
  161. package/src/prompts/tools/rewind.md +2 -2
  162. package/src/prompts/tools/search.md +2 -2
  163. package/src/prompts/tools/ssh.md +2 -2
  164. package/src/prompts/tools/task.md +12 -6
  165. package/src/prompts/tools/web-search.md +2 -2
  166. package/src/prompts/tools/write.md +3 -3
  167. package/src/sdk.ts +81 -17
  168. package/src/session/agent-session.ts +656 -125
  169. package/src/session/blob-store.ts +36 -3
  170. package/src/session/client-bridge.ts +81 -0
  171. package/src/session/compaction/errors.ts +31 -0
  172. package/src/session/compaction/index.ts +1 -0
  173. package/src/session/messages.ts +67 -2
  174. package/src/session/session-manager.ts +131 -12
  175. package/src/session/session-storage.ts +33 -15
  176. package/src/session/streaming-output.ts +309 -13
  177. package/src/slash-commands/acp-builtins.ts +46 -0
  178. package/src/slash-commands/builtin-registry.ts +717 -116
  179. package/src/slash-commands/helpers/context-report.ts +39 -0
  180. package/src/slash-commands/helpers/format.ts +23 -0
  181. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  182. package/src/slash-commands/helpers/mcp.ts +532 -0
  183. package/src/slash-commands/helpers/parse.ts +85 -0
  184. package/src/slash-commands/helpers/ssh.ts +193 -0
  185. package/src/slash-commands/helpers/todo.ts +279 -0
  186. package/src/slash-commands/helpers/usage-report.ts +91 -0
  187. package/src/slash-commands/types.ts +126 -0
  188. package/src/ssh/ssh-executor.ts +5 -0
  189. package/src/system-prompt.ts +4 -2
  190. package/src/task/executor.ts +27 -10
  191. package/src/task/index.ts +20 -1
  192. package/src/task/render.ts +27 -18
  193. package/src/task/types.ts +4 -0
  194. package/src/tools/ast-edit.ts +21 -120
  195. package/src/tools/ast-grep.ts +21 -119
  196. package/src/tools/bash-interactive.ts +9 -1
  197. package/src/tools/bash.ts +203 -6
  198. package/src/tools/browser/attach.ts +3 -3
  199. package/src/tools/browser/launch.ts +81 -18
  200. package/src/tools/browser/registry.ts +1 -5
  201. package/src/tools/browser/tab-supervisor.ts +51 -14
  202. package/src/tools/conflict-detect.ts +21 -10
  203. package/src/tools/eval.ts +3 -1
  204. package/src/tools/fetch.ts +15 -4
  205. package/src/tools/find.ts +39 -39
  206. package/src/tools/gh-renderer.ts +0 -12
  207. package/src/tools/gh.ts +689 -182
  208. package/src/tools/github-cache.ts +548 -0
  209. package/src/tools/index.ts +25 -11
  210. package/src/tools/inspect-image.ts +3 -10
  211. package/src/tools/output-meta.ts +176 -37
  212. package/src/tools/path-utils.ts +125 -2
  213. package/src/tools/read.ts +605 -239
  214. package/src/tools/render-utils.ts +92 -0
  215. package/src/tools/renderers.ts +2 -0
  216. package/src/tools/resolve.ts +72 -44
  217. package/src/tools/search.ts +120 -186
  218. package/src/tools/write.ts +67 -10
  219. package/src/tui/code-cell.ts +70 -2
  220. package/src/utils/file-mentions.ts +1 -1
  221. package/src/utils/image-loading.ts +7 -3
  222. package/src/utils/image-resize.ts +32 -43
  223. package/src/vim/parser.ts +0 -17
  224. package/src/vim/render.ts +1 -1
  225. package/src/vim/types.ts +1 -1
  226. package/src/web/search/providers/gemini.ts +35 -95
  227. package/src/prompts/tools/exit-plan-mode.md +0 -6
  228. package/src/tools/exit-plan-mode.ts +0 -97
  229. package/src/utils/fuzzy.ts +0 -108
  230. package/src/utils/image-convert.ts +0 -27
@@ -1,4 +1,9 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
1
4
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
5
+ import { Snowflake, setProjectDir } from "@oh-my-pi/pi-utils";
6
+ import { $ } from "bun";
2
7
  import type { SettingPath, SettingValue } from "../config/settings";
3
8
  import { settings } from "../config/settings";
4
9
  import {
@@ -14,8 +19,31 @@ import {
14
19
  getPluginsCacheDir,
15
20
  MarketplaceManager,
16
21
  } from "../extensibility/plugins/marketplace";
22
+ import { resolveMemoryBackend } from "../memory-backend";
17
23
  import type { InteractiveModeContext } from "../modes/types";
24
+ import { getChangelogPath, parseChangelog } from "../utils/changelog";
25
+ import { buildContextReportText } from "./helpers/context-report";
26
+ import { formatDuration } from "./helpers/format";
27
+ import { createMarketplaceManager } from "./helpers/marketplace-manager";
28
+ import { handleMcpAcp } from "./helpers/mcp";
29
+ import { commandConsumed, errorMessage, parseSlashCommand, parseSubcommand, usage } from "./helpers/parse";
30
+ import { handleSshAcp } from "./helpers/ssh";
31
+ import { handleTodoAcp } from "./helpers/todo";
32
+ import { buildUsageReportText } from "./helpers/usage-report";
18
33
  import { parseMarketplaceInstallArgs, parsePluginScopeArgs } from "./marketplace-install-parser";
34
+ import type {
35
+ BuiltinSlashCommand,
36
+ ParsedSlashCommand,
37
+ SlashCommandResult,
38
+ SlashCommandRuntime,
39
+ SlashCommandSpec,
40
+ TuiSlashCommandRuntime,
41
+ } from "./types";
42
+
43
+ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
44
+
45
+ /** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
46
+ export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
19
47
 
20
48
  function refreshStatusLine(ctx: InteractiveModeContext): void {
21
49
  ctx.statusLine.invalidate();
@@ -23,84 +51,17 @@ function refreshStatusLine(ctx: InteractiveModeContext): void {
23
51
  ctx.ui.requestRender();
24
52
  }
25
53
 
26
- /** Declarative subcommand definition for commands like /mcp. */
27
- export interface SubcommandDef {
28
- name: string;
29
- description: string;
30
- /** Usage hint shown as dim ghost text, e.g. "<name> [--scope project|user]". */
31
- usage?: string;
32
- }
33
-
34
- /** Declarative builtin slash command definition used by autocomplete and help UI. */
35
- export interface BuiltinSlashCommand {
36
- name: string;
37
- description: string;
38
- /** Subcommands for dropdown completion (e.g. /mcp add, /mcp list). */
39
- subcommands?: SubcommandDef[];
40
- /** Static inline hint when command takes a simple argument (no subcommands). */
41
- inlineHint?: string;
42
- }
43
-
44
- interface ParsedBuiltinSlashCommand {
45
- name: string;
46
- args: string;
47
- text: string;
48
- }
49
-
50
- interface BuiltinSlashCommandSpec extends BuiltinSlashCommand {
51
- aliases?: string[];
52
- allowArgs?: boolean;
53
- /**
54
- * Handle the command. Return a string to pass remaining text through as prompt input.
55
- * Return void/undefined to consume the input entirely.
56
- */
57
- handle: (
58
- command: ParsedBuiltinSlashCommand,
59
- runtime: BuiltinSlashCommandRuntime,
60
- // biome-ignore lint/suspicious/noConfusingVoidType: void needed so async handlers returning nothing are assignable
61
- ) => Promise<string | void> | string | void;
62
- }
63
-
64
- export interface BuiltinSlashCommandRuntime {
65
- ctx: InteractiveModeContext;
66
- handleBackgroundCommand: () => void;
67
- }
68
-
69
- function parseBuiltinSlashCommand(text: string): ParsedBuiltinSlashCommand | null {
70
- if (!text.startsWith("/")) return null;
71
- const body = text.slice(1);
72
- if (!body) return null;
73
-
74
- const firstWhitespace = body.search(/\s/);
75
- const firstColon = body.indexOf(":");
76
- const firstSeparator =
77
- firstWhitespace === -1 ? firstColon : firstColon === -1 ? firstWhitespace : Math.min(firstWhitespace, firstColon);
78
-
79
- if (firstSeparator === -1) {
80
- return {
81
- name: body,
82
- args: "",
83
- text,
84
- };
85
- }
86
-
87
- return {
88
- name: body.slice(0, firstSeparator),
89
- args: body.slice(firstSeparator + 1).trim(),
90
- text,
91
- };
92
- }
93
-
94
- const shutdownHandler = (_command: ParsedBuiltinSlashCommand, runtime: BuiltinSlashCommandRuntime): void => {
54
+ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashCommandRuntime): SlashCommandResult => {
95
55
  runtime.ctx.editor.setText("");
96
56
  void runtime.ctx.shutdown();
57
+ return commandConsumed();
97
58
  };
98
59
 
99
- const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
60
+ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
100
61
  {
101
62
  name: "settings",
102
63
  description: "Open settings menu",
103
- handle: (_command, runtime) => {
64
+ handleTui: (_command, runtime) => {
104
65
  runtime.ctx.showSettingsSelector();
105
66
  runtime.ctx.editor.setText("");
106
67
  },
@@ -110,18 +71,36 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
110
71
  description: "Toggle plan mode (agent plans before executing)",
111
72
  inlineHint: "[prompt]",
112
73
  allowArgs: true,
113
- handle: async (command, runtime) => {
74
+ handleTui: async (command, runtime) => {
114
75
  await runtime.ctx.handlePlanModeCommand(command.args || undefined);
115
76
  runtime.ctx.editor.setText("");
116
77
  },
117
78
  },
79
+ {
80
+ name: "goal",
81
+ description: "Toggle goal mode (persistent autonomous objective for this session)",
82
+ subcommands: [
83
+ { name: "set", description: "Set or replace the goal", usage: "<objective>" },
84
+ { name: "show", description: "Show current goal details" },
85
+ { name: "pause", description: "Pause the current goal" },
86
+ { name: "resume", description: "Resume a paused goal" },
87
+ { name: "drop", description: "Drop the current goal" },
88
+ { name: "budget", description: "Adjust the token budget", usage: "<N|off>" },
89
+ ],
90
+ inlineHint: "[objective]",
91
+ allowArgs: true,
92
+ handleTui: async (command, runtime) => {
93
+ await runtime.ctx.handleGoalModeCommand(command.args || undefined);
94
+ runtime.ctx.editor.setText("");
95
+ },
96
+ },
118
97
  {
119
98
  name: "loop",
120
99
  description:
121
100
  "Toggle loop mode. While enabled, the next prompt you send re-submits after every yield. Esc cancels the current iteration; /loop again to disable.",
122
101
  inlineHint: "[count|duration]",
123
102
  allowArgs: true,
124
- handle: async (command, runtime) => {
103
+ handleTui: async (command, runtime) => {
125
104
  await runtime.ctx.handleLoopCommand(command.args);
126
105
  runtime.ctx.editor.setText("");
127
106
  },
@@ -130,7 +109,38 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
130
109
  name: "model",
131
110
  aliases: ["models"],
132
111
  description: "Select model (opens selector UI)",
133
- handle: (_command, runtime) => {
112
+ acpDescription: "Show current model selection",
113
+ handle: async (command, runtime) => {
114
+ if (command.args) {
115
+ const modelId = command.args.trim();
116
+ const availableModels = runtime.session.getAvailableModels?.() ?? [];
117
+ const match = availableModels.find(
118
+ model => model.id === modelId || `${model.provider}/${model.id}` === modelId,
119
+ );
120
+ if (!match) {
121
+ return usage(
122
+ `Unknown model: ${modelId}. Use ACP \`session/setModel\` for picker-driven selection or list available models with /model.`,
123
+ runtime,
124
+ );
125
+ }
126
+ try {
127
+ await runtime.session.setModel(match);
128
+ await runtime.output(`Model set to ${match.provider}/${match.id}.`);
129
+ await runtime.notifyTitleChanged?.();
130
+ await runtime.notifyConfigChanged?.();
131
+ return commandConsumed();
132
+ } catch (err) {
133
+ return usage(`Failed to set model: ${errorMessage(err)}`, runtime);
134
+ }
135
+ }
136
+
137
+ const model = runtime.session.model;
138
+ await runtime.output(
139
+ model ? `Current model: ${model.provider}/${model.id}` : "No model is currently selected.",
140
+ );
141
+ return commandConsumed();
142
+ },
143
+ handleTui: (_command, runtime) => {
134
144
  runtime.ctx.showModelSelector();
135
145
  runtime.ctx.editor.setText("");
136
146
  },
@@ -138,13 +148,38 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
138
148
  {
139
149
  name: "fast",
140
150
  description: "Toggle fast mode (OpenAI service tier priority)",
151
+ acpDescription: "Toggle fast mode",
152
+ acpInputHint: "[on|off|status]",
141
153
  subcommands: [
142
154
  { name: "on", description: "Enable fast mode" },
143
155
  { name: "off", description: "Disable fast mode" },
144
156
  { name: "status", description: "Show fast mode status" },
145
157
  ],
146
158
  allowArgs: true,
147
- handle: (command, runtime) => {
159
+ handle: async (command, runtime) => {
160
+ const arg = command.args.toLowerCase();
161
+ if (!arg || arg === "toggle") {
162
+ const enabled = runtime.session.toggleFastMode();
163
+ await runtime.output(`Fast mode ${enabled ? "enabled" : "disabled"}.`);
164
+ return commandConsumed();
165
+ }
166
+ if (arg === "on") {
167
+ runtime.session.setFastMode(true);
168
+ await runtime.output("Fast mode enabled.");
169
+ return commandConsumed();
170
+ }
171
+ if (arg === "off") {
172
+ runtime.session.setFastMode(false);
173
+ await runtime.output("Fast mode disabled.");
174
+ return commandConsumed();
175
+ }
176
+ if (arg === "status") {
177
+ await runtime.output(`Fast mode is ${runtime.session.isFastModeEnabled() ? "on" : "off"}.`);
178
+ return commandConsumed();
179
+ }
180
+ return usage("Usage: /fast [on|off|status]", runtime);
181
+ },
182
+ handleTui: (command, runtime) => {
148
183
  const arg = command.args.trim().toLowerCase();
149
184
  if (!arg || arg === "toggle") {
150
185
  const enabled = runtime.ctx.session.toggleFastMode();
@@ -183,6 +218,23 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
183
218
  inlineHint: "[path]",
184
219
  allowArgs: true,
185
220
  handle: async (command, runtime) => {
221
+ const arg = command.args.trim();
222
+ // Match the interactive `/export` behavior: clipboard aliases are not a
223
+ // valid export target. Without this, the literal value (`copy`,
224
+ // `--copy`, `clipboard`) is passed to `exportToHtml` and becomes the
225
+ // output filename.
226
+ if (arg === "--copy" || arg === "clipboard" || arg === "copy") {
227
+ return usage("Use /dump to copy the session to clipboard.", runtime);
228
+ }
229
+ try {
230
+ const filePath = await runtime.session.exportToHtml(arg || undefined);
231
+ await runtime.output(`Session exported to: ${filePath}`);
232
+ return commandConsumed();
233
+ } catch (err) {
234
+ return usage(`Failed to export session: ${errorMessage(err)}`, runtime);
235
+ }
236
+ },
237
+ handleTui: async (command, runtime) => {
186
238
  await runtime.ctx.handleExportCommand(command.text);
187
239
  runtime.ctx.editor.setText("");
188
240
  },
@@ -190,7 +242,13 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
190
242
  {
191
243
  name: "dump",
192
244
  description: "Copy session transcript to clipboard",
245
+ acpDescription: "Return full transcript as plain text",
193
246
  handle: async (_command, runtime) => {
247
+ const text = runtime.session.formatSessionAsText();
248
+ await runtime.output(text || "No messages to dump yet.");
249
+ return commandConsumed();
250
+ },
251
+ handleTui: async (_command, runtime) => {
194
252
  await runtime.ctx.handleDumpCommand();
195
253
  runtime.ctx.editor.setText("");
196
254
  },
@@ -199,6 +257,32 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
199
257
  name: "share",
200
258
  description: "Share session as a secret GitHub gist",
201
259
  handle: async (_command, runtime) => {
260
+ const tmpFile = path.join(os.tmpdir(), `${Snowflake.next()}.html`);
261
+ try {
262
+ try {
263
+ await runtime.session.exportToHtml(tmpFile);
264
+ } catch (err) {
265
+ return usage(`Failed to export session: ${errorMessage(err)}`, runtime);
266
+ }
267
+ const result = await $`gh gist create --public=false ${tmpFile}`.quiet().nothrow();
268
+ if (result.exitCode !== 0) {
269
+ return usage(
270
+ `Failed to create gist: ${result.stderr.toString("utf-8").trim() || "unknown error"}`,
271
+ runtime,
272
+ );
273
+ }
274
+ const gistUrl = result.stdout.toString("utf-8").trim();
275
+ const gistId = gistUrl.split("/").pop();
276
+ if (!gistId) return usage("Failed to parse gist ID from gh output", runtime);
277
+ await runtime.output(`Share URL: https://gistpreview.github.io/?${gistId}\nGist: ${gistUrl}`);
278
+ return commandConsumed();
279
+ } catch {
280
+ return usage("GitHub CLI (gh) is required for /share. Install it from https://cli.github.com/.", runtime);
281
+ } finally {
282
+ await fs.rm(tmpFile, { force: true }).catch(() => {});
283
+ }
284
+ },
285
+ handleTui: async (_command, runtime) => {
202
286
  await runtime.ctx.handleShareCommand();
203
287
  runtime.ctx.editor.setText("");
204
288
  },
@@ -206,12 +290,40 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
206
290
  {
207
291
  name: "browser",
208
292
  description: "Toggle browser headless vs visible mode",
293
+ acpInputHint: "[headless|visible]",
209
294
  subcommands: [
210
295
  { name: "headless", description: "Switch to headless mode" },
211
296
  { name: "visible", description: "Switch to visible mode" },
212
297
  ],
213
298
  allowArgs: true,
214
299
  handle: async (command, runtime) => {
300
+ const arg = command.args.toLowerCase();
301
+ const enabled = runtime.settings.get("browser.enabled" as SettingPath) as boolean;
302
+ if (!enabled) return usage("Browser tool is disabled (enable in settings).", runtime);
303
+ const current = runtime.settings.get("browser.headless" as SettingPath) as boolean;
304
+ let next = current;
305
+ if (!arg) next = !current;
306
+ else if (arg === "headless" || arg === "hidden") next = true;
307
+ else if (arg === "visible" || arg === "show" || arg === "headful") next = false;
308
+ else return usage("Usage: /browser [headless|visible]", runtime);
309
+ runtime.settings.set("browser.headless" as SettingPath, next as SettingValue<SettingPath>);
310
+ const tool = runtime.session.getToolByName("browser");
311
+ if (tool && "restartForModeChange" in tool) {
312
+ try {
313
+ await (tool as { restartForModeChange: () => Promise<void> }).restartForModeChange();
314
+ } catch (err) {
315
+ // Setting was already mutated; surface the restart failure so the
316
+ // user knows the browser is in an inconsistent state.
317
+ await runtime.output(
318
+ `Browser mode set to ${next ? "headless" : "visible"}, but restart failed: ${errorMessage(err)}`,
319
+ );
320
+ return commandConsumed();
321
+ }
322
+ }
323
+ await runtime.output(`Browser mode: ${next ? "headless" : "visible"}`);
324
+ return commandConsumed();
325
+ },
326
+ handleTui: async (command, runtime) => {
215
327
  const arg = command.args.toLowerCase();
216
328
  const current = settings.get("browser.headless" as SettingPath) as boolean;
217
329
  let next = current;
@@ -222,9 +334,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
222
334
  }
223
335
  if (!arg) {
224
336
  next = !current;
225
- } else if (["headless", "hidden"].includes(arg)) {
337
+ } else if (arg === "headless" || arg === "hidden") {
226
338
  next = true;
227
- } else if (["visible", "show", "headful"].includes(arg)) {
339
+ } else if (arg === "visible" || arg === "show" || arg === "headful") {
228
340
  next = false;
229
341
  } else {
230
342
  runtime.ctx.showStatus("Usage: /browser [headless|visible]");
@@ -237,9 +349,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
237
349
  try {
238
350
  await (tool as { restartForModeChange: () => Promise<void> }).restartForModeChange();
239
351
  } catch (error) {
240
- runtime.ctx.showWarning(
241
- `Failed to restart browser: ${error instanceof Error ? error.message : String(error)}`,
242
- );
352
+ runtime.ctx.showWarning(`Failed to restart browser: ${errorMessage(error)}`);
243
353
  runtime.ctx.editor.setText("");
244
354
  return;
245
355
  }
@@ -258,7 +368,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
258
368
  { name: "cmd", description: "Copy last bash/python command" },
259
369
  ],
260
370
  allowArgs: true,
261
- handle: async (command, runtime) => {
371
+ handleTui: async (command, runtime) => {
262
372
  const sub = command.args.trim().toLowerCase() || undefined;
263
373
  await runtime.ctx.handleCopyCommand(sub);
264
374
  runtime.ctx.editor.setText("");
@@ -267,6 +377,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
267
377
  {
268
378
  name: "todo",
269
379
  description: "View or modify the agent's todo list",
380
+ acpDescription: "Manage todos",
381
+ acpInputHint: "<subcommand>",
270
382
  subcommands: [
271
383
  { name: "edit", description: "Open todos in $EDITOR (Markdown round-trip)" },
272
384
  { name: "copy", description: "Copy todos as Markdown to clipboard" },
@@ -283,7 +395,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
283
395
  { name: "rm", description: "Remove task/phase/all (fuzzy-matched)", usage: "[<task|phase>]" },
284
396
  ],
285
397
  allowArgs: true,
286
- handle: async (command, runtime) => {
398
+ handle: handleTodoAcp,
399
+ handleTui: async (command, runtime) => {
287
400
  await runtime.ctx.handleTodoCommand(command.args);
288
401
  runtime.ctx.editor.setText("");
289
402
  },
@@ -291,12 +404,46 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
291
404
  {
292
405
  name: "session",
293
406
  description: "Session management commands",
407
+ acpDescription: "Show session information",
408
+ acpInputHint: "info|delete",
294
409
  subcommands: [
295
410
  { name: "info", description: "Show session info and stats" },
296
411
  { name: "delete", description: "Delete current session and return to selector" },
297
412
  ],
298
413
  allowArgs: true,
299
414
  handle: async (command, runtime) => {
415
+ if (!command.args || command.args === "info") {
416
+ await runtime.output(
417
+ [
418
+ `Session: ${runtime.session.sessionId}`,
419
+ `Title: ${runtime.session.sessionName}`,
420
+ `CWD: ${runtime.cwd}`,
421
+ ].join("\n"),
422
+ );
423
+ return commandConsumed();
424
+ }
425
+ if (command.args === "delete") {
426
+ if (runtime.session.isStreaming) return usage("Cannot delete the session while streaming.", runtime);
427
+ const sessionFile = runtime.sessionManager.getSessionFile();
428
+ if (!sessionFile) return usage("No session file to delete (in-memory session).", runtime);
429
+ // Route through the active SessionManager so the persist writer is
430
+ // closed before the file is deleted. Constructing a fresh
431
+ // FileSessionStorage and calling deleteSessionWithArtifacts leaves
432
+ // the active writer attached to the now-deleted path, so the next
433
+ // prompt would silently resurrect or corrupt the "deleted" file.
434
+ try {
435
+ await runtime.sessionManager.dropSession(sessionFile);
436
+ } catch (err) {
437
+ return usage(`Failed to delete session: ${errorMessage(err)}`, runtime);
438
+ }
439
+ await runtime.output(
440
+ `Session deleted: ${sessionFile}. Use ACP \`session/load\` to switch to another session.`,
441
+ );
442
+ return commandConsumed();
443
+ }
444
+ return usage("Usage: /session [info|delete]", runtime);
445
+ },
446
+ handleTui: async (command, runtime) => {
300
447
  const sub = command.args.trim().toLowerCase() || "info";
301
448
  if (sub === "delete") {
302
449
  runtime.ctx.editor.setText("");
@@ -311,7 +458,35 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
311
458
  {
312
459
  name: "jobs",
313
460
  description: "Show async background jobs status",
461
+ acpDescription: "Show background jobs",
314
462
  handle: async (_command, runtime) => {
463
+ const snapshot = runtime.session.getAsyncJobSnapshot({ recentLimit: 5 });
464
+ if (!snapshot || (snapshot.running.length === 0 && snapshot.recent.length === 0)) {
465
+ await runtime.output(
466
+ "No background jobs running. (Background jobs run async tools — e.g. long-running bash, debug, or task subagents that would otherwise tie up a turn. They appear here while alive and for ~5 minutes after.)",
467
+ );
468
+ return commandConsumed();
469
+ }
470
+ const now = Date.now();
471
+ const lines: string[] = ["Background Jobs", `Running: ${snapshot.running.length}`];
472
+ if (snapshot.running.length > 0) {
473
+ lines.push("", "Running Jobs");
474
+ for (const job of snapshot.running) {
475
+ lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
476
+ lines.push(` ${job.label}`);
477
+ }
478
+ }
479
+ if (snapshot.recent.length > 0) {
480
+ lines.push("", "Recent Jobs");
481
+ for (const job of snapshot.recent) {
482
+ lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
483
+ lines.push(` ${job.label}`);
484
+ }
485
+ }
486
+ await runtime.output(lines.join("\n"));
487
+ return commandConsumed();
488
+ },
489
+ handleTui: async (_command, runtime) => {
315
490
  await runtime.ctx.handleJobsCommand();
316
491
  runtime.ctx.editor.setText("");
317
492
  },
@@ -319,7 +494,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
319
494
  {
320
495
  name: "usage",
321
496
  description: "Show provider usage and limits",
497
+ acpDescription: "Show token usage",
322
498
  handle: async (_command, runtime) => {
499
+ await runtime.output(await buildUsageReportText(runtime));
500
+ return commandConsumed();
501
+ },
502
+ handleTui: async (_command, runtime) => {
323
503
  await runtime.ctx.handleUsageCommand();
324
504
  runtime.ctx.editor.setText("");
325
505
  },
@@ -327,9 +507,28 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
327
507
  {
328
508
  name: "changelog",
329
509
  description: "Show changelog entries",
510
+ acpDescription: "Show changelog",
511
+ acpInputHint: "[full]",
330
512
  subcommands: [{ name: "full", description: "Show complete changelog" }],
331
513
  allowArgs: true,
332
514
  handle: async (command, runtime) => {
515
+ const changelogPath = getChangelogPath();
516
+ const allEntries = await parseChangelog(changelogPath);
517
+ const showFull = command.args.trim().toLowerCase() === "full";
518
+ const entriesToShow = showFull ? allEntries : allEntries.slice(0, 3);
519
+ if (entriesToShow.length === 0) {
520
+ await runtime.output("No changelog entries found.");
521
+ return commandConsumed();
522
+ }
523
+ await runtime.output(
524
+ [...entriesToShow]
525
+ .reverse()
526
+ .map(entry => entry.content)
527
+ .join("\n\n"),
528
+ );
529
+ return commandConsumed();
530
+ },
531
+ handleTui: async (command, runtime) => {
333
532
  const showFull = command.args.split(/\s+/).filter(Boolean).includes("full");
334
533
  await runtime.ctx.handleChangelogCommand(showFull);
335
534
  runtime.ctx.editor.setText("");
@@ -338,7 +537,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
338
537
  {
339
538
  name: "hotkeys",
340
539
  description: "Show all keyboard shortcuts",
341
- handle: (_command, runtime) => {
540
+ handleTui: (_command, runtime) => {
342
541
  runtime.ctx.handleHotkeysCommand();
343
542
  runtime.ctx.editor.setText("");
344
543
  },
@@ -346,7 +545,18 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
346
545
  {
347
546
  name: "tools",
348
547
  description: "Show tools currently visible to the agent",
349
- handle: (_command, runtime) => {
548
+ acpDescription: "Show available tools",
549
+ handle: async (_command, runtime) => {
550
+ const active = runtime.session.getActiveToolNames();
551
+ const all = runtime.session.getAllToolNames();
552
+ if (all.length === 0) {
553
+ await runtime.output("No tools are available.");
554
+ return commandConsumed();
555
+ }
556
+ await runtime.output(all.map(name => `${active.includes(name) ? "*" : "-"} ${name}`).join("\n"));
557
+ return commandConsumed();
558
+ },
559
+ handleTui: (_command, runtime) => {
350
560
  runtime.ctx.handleToolsCommand();
351
561
  runtime.ctx.editor.setText("");
352
562
  },
@@ -354,7 +564,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
354
564
  {
355
565
  name: "context",
356
566
  description: "Show estimated context usage breakdown",
357
- handle: (_command, runtime) => {
567
+ acpDescription: "Show context usage",
568
+ handle: async (_command, runtime) => {
569
+ await runtime.output(buildContextReportText(runtime));
570
+ return commandConsumed();
571
+ },
572
+ handleTui: (_command, runtime) => {
358
573
  runtime.ctx.handleContextCommand();
359
574
  runtime.ctx.editor.setText("");
360
575
  },
@@ -363,7 +578,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
363
578
  name: "extensions",
364
579
  aliases: ["status"],
365
580
  description: "Open Extension Control Center dashboard",
366
- handle: (_command, runtime) => {
581
+ handleTui: (_command, runtime) => {
367
582
  runtime.ctx.showExtensionsDashboard();
368
583
  runtime.ctx.editor.setText("");
369
584
  },
@@ -371,7 +586,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
371
586
  {
372
587
  name: "agents",
373
588
  description: "Open Agent Control Center dashboard",
374
- handle: (_command, runtime) => {
589
+ handleTui: (_command, runtime) => {
375
590
  runtime.ctx.showAgentsDashboard();
376
591
  runtime.ctx.editor.setText("");
377
592
  },
@@ -379,7 +594,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
379
594
  {
380
595
  name: "branch",
381
596
  description: "Create a new branch from a previous message",
382
- handle: (_command, runtime) => {
597
+ handleTui: (_command, runtime) => {
383
598
  if (settings.get("doubleEscapeAction") === "tree") {
384
599
  runtime.ctx.showTreeSelector();
385
600
  } else {
@@ -391,7 +606,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
391
606
  {
392
607
  name: "fork",
393
608
  description: "Create a new fork from a previous message",
394
- handle: async (_command, runtime) => {
609
+ handleTui: async (_command, runtime) => {
395
610
  runtime.ctx.editor.setText("");
396
611
  await runtime.ctx.handleForkCommand();
397
612
  },
@@ -399,7 +614,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
399
614
  {
400
615
  name: "tree",
401
616
  description: "Navigate session tree (switch branches)",
402
- handle: (_command, runtime) => {
617
+ handleTui: (_command, runtime) => {
403
618
  runtime.ctx.showTreeSelector();
404
619
  runtime.ctx.editor.setText("");
405
620
  },
@@ -409,7 +624,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
409
624
  description: "Login with OAuth provider",
410
625
  inlineHint: "[provider|redirect URL]",
411
626
  allowArgs: true,
412
- handle: (command, runtime) => {
627
+ handleTui: (command, runtime) => {
413
628
  const manualInput = runtime.ctx.oauthManualInput;
414
629
  const args = command.args.trim();
415
630
  if (args.length > 0) {
@@ -455,7 +670,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
455
670
  {
456
671
  name: "logout",
457
672
  description: "Logout from OAuth provider",
458
- handle: (_command, runtime) => {
673
+ handleTui: (_command, runtime) => {
459
674
  void runtime.ctx.showOAuthSelector("logout");
460
675
  runtime.ctx.editor.setText("");
461
676
  },
@@ -463,6 +678,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
463
678
  {
464
679
  name: "mcp",
465
680
  description: "Manage MCP servers (add, list, remove, test)",
681
+ acpDescription: "Manage MCP servers",
682
+ inlineHint: "<subcommand>",
466
683
  subcommands: [
467
684
  {
468
685
  name: "add",
@@ -491,7 +708,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
491
708
  { name: "help", description: "Show help message" },
492
709
  ],
493
710
  allowArgs: true,
494
- handle: async (command, runtime) => {
711
+ handle: handleMcpAcp,
712
+ handleTui: async (command, runtime) => {
495
713
  runtime.ctx.editor.addToHistory(command.text);
496
714
  runtime.ctx.editor.setText("");
497
715
  await runtime.ctx.handleMCPCommand(command.text);
@@ -500,6 +718,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
500
718
  {
501
719
  name: "ssh",
502
720
  description: "Manage SSH hosts (add, list, remove)",
721
+ acpDescription: "Manage SSH connections",
722
+ inlineHint: "<subcommand>",
503
723
  subcommands: [
504
724
  {
505
725
  name: "add",
@@ -511,7 +731,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
511
731
  { name: "help", description: "Show help message" },
512
732
  ],
513
733
  allowArgs: true,
514
- handle: async (command, runtime) => {
734
+ handle: handleSshAcp,
735
+ handleTui: async (command, runtime) => {
515
736
  runtime.ctx.editor.addToHistory(command.text);
516
737
  runtime.ctx.editor.setText("");
517
738
  await runtime.ctx.handleSSHCommand(command.text);
@@ -520,7 +741,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
520
741
  {
521
742
  name: "new",
522
743
  description: "Start a new session",
523
- handle: async (_command, runtime) => {
744
+ handleTui: async (_command, runtime) => {
524
745
  runtime.ctx.editor.setText("");
525
746
  await runtime.ctx.handleClearCommand();
526
747
  },
@@ -528,7 +749,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
528
749
  {
529
750
  name: "drop",
530
751
  description: "Delete the current session and start a new one",
531
- handle: async (_command, runtime) => {
752
+ handleTui: async (_command, runtime) => {
532
753
  runtime.ctx.editor.setText("");
533
754
  await runtime.ctx.handleDropCommand();
534
755
  },
@@ -536,9 +757,31 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
536
757
  {
537
758
  name: "compact",
538
759
  description: "Manually compact the session context",
760
+ acpDescription: "Compact the conversation",
539
761
  inlineHint: "[focus instructions]",
540
762
  allowArgs: true,
541
763
  handle: async (command, runtime) => {
764
+ const before = runtime.session.getContextUsage?.();
765
+ const beforeTokens = before?.tokens;
766
+ try {
767
+ await runtime.session.compact(command.args || undefined);
768
+ } catch (err) {
769
+ // Compaction precondition failures (no model, already compacted, too
770
+ // small) and provider errors propagate as plain Errors; surface them
771
+ // via runtime.output so they don't fail the ACP prompt turn.
772
+ return usage(`Compaction failed: ${errorMessage(err)}`, runtime);
773
+ }
774
+ const after = runtime.session.getContextUsage?.();
775
+ const afterTokens = after?.tokens;
776
+ if (beforeTokens != null && afterTokens != null) {
777
+ const saved = beforeTokens - afterTokens;
778
+ await runtime.output(`Compaction complete. Tokens: ${beforeTokens} -> ${afterTokens} (saved ${saved}).`);
779
+ } else {
780
+ await runtime.output("Compaction complete.");
781
+ }
782
+ return commandConsumed();
783
+ },
784
+ handleTui: async (command, runtime) => {
542
785
  const customInstructions = command.args || undefined;
543
786
  runtime.ctx.editor.setText("");
544
787
  await runtime.ctx.handleCompactCommand(customInstructions);
@@ -549,7 +792,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
549
792
  description: "Hand off session context to a new session",
550
793
  inlineHint: "[focus instructions]",
551
794
  allowArgs: true,
552
- handle: async (command, runtime) => {
795
+ handleTui: async (command, runtime) => {
553
796
  const customInstructions = command.args || undefined;
554
797
  runtime.ctx.editor.setText("");
555
798
  await runtime.ctx.handleHandoffCommand(customInstructions);
@@ -558,7 +801,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
558
801
  {
559
802
  name: "resume",
560
803
  description: "Resume a different session",
561
- handle: (_command, runtime) => {
804
+ handleTui: (_command, runtime) => {
562
805
  runtime.ctx.showSessionSelector();
563
806
  runtime.ctx.editor.setText("");
564
807
  },
@@ -568,7 +811,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
568
811
  description: "Ask an ephemeral side question using the current session context",
569
812
  inlineHint: "<question>",
570
813
  allowArgs: true,
571
- handle: async (command, runtime) => {
814
+ handleTui: async (command, runtime) => {
572
815
  const question = command.text.slice(`/${command.name}`.length).trim();
573
816
  runtime.ctx.editor.setText("");
574
817
  await runtime.ctx.handleBtwCommand(question);
@@ -577,7 +820,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
577
820
  {
578
821
  name: "retry",
579
822
  description: "Retry the last failed agent turn",
580
- handle: async (_command, runtime) => {
823
+ handleTui: async (_command, runtime) => {
581
824
  const didRetry = await runtime.ctx.session.retry();
582
825
  if (!didRetry) {
583
826
  runtime.ctx.showStatus("Nothing to retry");
@@ -589,7 +832,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
589
832
  name: "background",
590
833
  aliases: ["bg"],
591
834
  description: "Detach UI and continue running in background",
592
- handle: (_command, runtime) => {
835
+ handleTui: (_command, runtime) => {
593
836
  runtime.ctx.editor.setText("");
594
837
  runtime.handleBackgroundCommand();
595
838
  },
@@ -597,7 +840,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
597
840
  {
598
841
  name: "debug",
599
842
  description: "Open debug tools selector",
600
- handle: (_command, runtime) => {
843
+ handleTui: (_command, runtime) => {
601
844
  runtime.ctx.showDebugSelector();
602
845
  runtime.ctx.editor.setText("");
603
846
  },
@@ -605,6 +848,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
605
848
  {
606
849
  name: "memory",
607
850
  description: "Inspect and operate memory maintenance",
851
+ acpDescription: "Manage memory",
852
+ acpInputHint: "<subcommand>",
608
853
  subcommands: [
609
854
  { name: "view", description: "Show current memory injection payload" },
610
855
  { name: "clear", description: "Clear persisted memory data and artifacts" },
@@ -624,6 +869,41 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
624
869
  ],
625
870
  allowArgs: true,
626
871
  handle: async (command, runtime) => {
872
+ const verb = (command.args.trim().split(/\s+/)[0] ?? "").toLowerCase() || "view";
873
+ const backend = resolveMemoryBackend(runtime.settings);
874
+ switch (verb) {
875
+ case "view": {
876
+ const payload = await backend.buildDeveloperInstructions(
877
+ runtime.settings.getAgentDir(),
878
+ runtime.settings,
879
+ runtime.session,
880
+ );
881
+ await runtime.output(payload || "Memory payload is empty.");
882
+ return commandConsumed();
883
+ }
884
+ case "clear":
885
+ case "reset": {
886
+ await backend.clear(runtime.settings.getAgentDir(), runtime.cwd, runtime.session);
887
+ await runtime.session.refreshBaseSystemPrompt();
888
+ await runtime.output("Memory cleared.");
889
+ return commandConsumed();
890
+ }
891
+ case "enqueue":
892
+ case "rebuild": {
893
+ await backend.enqueue(runtime.settings.getAgentDir(), runtime.cwd, runtime.session);
894
+ await runtime.output("Memory consolidation enqueued.");
895
+ return commandConsumed();
896
+ }
897
+ case "mm":
898
+ return usage(
899
+ "Mental-model maintenance via /memory mm is unsupported in ACP mode; use the hindsight HTTP API directly.",
900
+ runtime,
901
+ );
902
+ default:
903
+ return usage("Usage: /memory <view|clear|reset|enqueue|rebuild>", runtime);
904
+ }
905
+ },
906
+ handleTui: async (command, runtime) => {
627
907
  runtime.ctx.editor.setText("");
628
908
  await runtime.ctx.handleMemoryCommand(command.text);
629
909
  },
@@ -634,6 +914,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
634
914
  inlineHint: "<title>",
635
915
  allowArgs: true,
636
916
  handle: async (command, runtime) => {
917
+ if (!command.args) return usage("Usage: /rename <title>", runtime);
918
+ const ok = await runtime.sessionManager.setSessionName(command.args, "user");
919
+ if (!ok) {
920
+ await runtime.output("Session name not changed (a user-set name takes precedence).");
921
+ return commandConsumed();
922
+ }
923
+ await runtime.notifyTitleChanged?.();
924
+ await runtime.output(`Session renamed to ${command.args}.`);
925
+ return commandConsumed();
926
+ },
927
+ handleTui: async (command, runtime) => {
637
928
  const title = command.args.trim();
638
929
  if (!title) {
639
930
  runtime.ctx.showError("Usage: /rename <title>");
@@ -644,13 +935,38 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
644
935
  await runtime.ctx.handleRenameCommand(title);
645
936
  },
646
937
  },
647
-
648
938
  {
649
939
  name: "move",
650
940
  description: "Move session to a different working directory",
941
+ acpDescription: "Move the current session file",
651
942
  inlineHint: "<path>",
652
943
  allowArgs: true,
653
944
  handle: async (command, runtime) => {
945
+ if (runtime.session.isStreaming) return usage("Cannot move while streaming.", runtime);
946
+ if (!command.args) return usage("Usage: /move <path>", runtime);
947
+ const resolvedPath = path.resolve(runtime.cwd, command.args);
948
+ let isDirectory: boolean;
949
+ try {
950
+ isDirectory = (await fs.stat(resolvedPath)).isDirectory();
951
+ } catch {
952
+ return usage(`Directory does not exist or is not a directory: ${resolvedPath}`, runtime);
953
+ }
954
+ if (!isDirectory) return usage(`Directory does not exist or is not a directory: ${resolvedPath}`, runtime);
955
+ try {
956
+ await runtime.sessionManager.flush();
957
+ await runtime.sessionManager.moveTo(resolvedPath);
958
+ } catch (err) {
959
+ return usage(`Move failed: ${errorMessage(err)}`, runtime);
960
+ }
961
+ setProjectDir(resolvedPath);
962
+ // Reload plugin/capability caches so the next prompt sees commands and
963
+ // capabilities scoped to the new cwd.
964
+ await runtime.reloadPlugins();
965
+ await runtime.notifyTitleChanged?.();
966
+ await runtime.output(`Session moved to ${runtime.sessionManager.getCwd()}.`);
967
+ return commandConsumed();
968
+ },
969
+ handleTui: async (command, runtime) => {
654
970
  const targetPath = command.args;
655
971
  if (!targetPath) {
656
972
  runtime.ctx.showError("Usage: /move <path>");
@@ -664,11 +980,13 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
664
980
  {
665
981
  name: "exit",
666
982
  description: "Exit the application",
667
- handle: shutdownHandler,
983
+ handleTui: shutdownHandlerTui,
668
984
  },
669
985
  {
670
986
  name: "marketplace",
671
987
  description: "Manage marketplace plugin sources and installed plugins",
988
+ acpDescription: "Manage plugins from marketplaces",
989
+ acpInputHint: "<subcommand>",
672
990
  subcommands: [
673
991
  { name: "add", description: "Add a marketplace source", usage: "<source>" },
674
992
  { name: "remove", description: "Remove a marketplace source", usage: "<name>" },
@@ -687,6 +1005,175 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
687
1005
  ],
688
1006
  allowArgs: true,
689
1007
  handle: async (command, runtime) => {
1008
+ const { verb, rest } = parseSubcommand(command.args);
1009
+ if (!verb) {
1010
+ try {
1011
+ const manager = await createMarketplaceManager(runtime);
1012
+ const marketplaces = await manager.listMarketplaces();
1013
+ if (marketplaces.length === 0) {
1014
+ await runtime.output(
1015
+ "No marketplaces configured.\n\nGet started:\n /marketplace add anthropics/claude-plugins-official\n\nThen browse with /marketplace discover",
1016
+ );
1017
+ } else {
1018
+ const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`);
1019
+ await runtime.output(
1020
+ `Marketplaces:\n${lines.join("\n")}\n\nUse /marketplace discover to browse plugins, or /marketplace help for all commands`,
1021
+ );
1022
+ }
1023
+ return commandConsumed();
1024
+ } catch (err) {
1025
+ return usage(`Marketplace error: ${errorMessage(err)}`, runtime);
1026
+ }
1027
+ }
1028
+ if (verb === "help") {
1029
+ await runtime.output(
1030
+ [
1031
+ "Marketplace commands:",
1032
+ " /marketplace List configured marketplaces",
1033
+ " /marketplace add <source> Add a marketplace (e.g. owner/repo)",
1034
+ " /marketplace remove <name> Remove a marketplace",
1035
+ " /marketplace update [name] Re-fetch catalog(s)",
1036
+ " /marketplace list List configured marketplaces",
1037
+ " /marketplace discover [marketplace] Browse available plugins",
1038
+ " /marketplace install <name@marketplace> Install a plugin",
1039
+ " /marketplace uninstall <name@marketplace> Uninstall a plugin",
1040
+ " /marketplace installed List installed plugins",
1041
+ " /marketplace upgrade [name@marketplace] Upgrade plugin(s)",
1042
+ "",
1043
+ "Quick start:",
1044
+ " /marketplace add anthropics/claude-plugins-official",
1045
+ ].join("\n"),
1046
+ );
1047
+ return commandConsumed();
1048
+ }
1049
+ if ((verb === "install" || verb === "uninstall") && !rest) {
1050
+ return usage(
1051
+ "Interactive plugin pickers are TUI-only. Pass an explicit name@marketplace argument.",
1052
+ runtime,
1053
+ );
1054
+ }
1055
+ try {
1056
+ const manager = await createMarketplaceManager(runtime);
1057
+ switch (verb) {
1058
+ case "add": {
1059
+ if (!rest) return usage("Usage: /marketplace add <source>", runtime);
1060
+ const entry = await manager.addMarketplace(rest);
1061
+ await runtime.output(`Added marketplace: ${entry.name}`);
1062
+ return commandConsumed();
1063
+ }
1064
+ case "remove":
1065
+ case "rm": {
1066
+ if (!rest) return usage("Usage: /marketplace remove <name>", runtime);
1067
+ await manager.removeMarketplace(rest);
1068
+ await runtime.output(`Removed marketplace: ${rest}`);
1069
+ return commandConsumed();
1070
+ }
1071
+ case "update": {
1072
+ if (rest) {
1073
+ await manager.updateMarketplace(rest);
1074
+ await runtime.output(`Updated marketplace: ${rest}`);
1075
+ } else {
1076
+ const results = await manager.updateAllMarketplaces();
1077
+ await runtime.output(`Updated ${results.length} marketplace(s)`);
1078
+ }
1079
+ return commandConsumed();
1080
+ }
1081
+ case "list": {
1082
+ const marketplaces = await manager.listMarketplaces();
1083
+ if (marketplaces.length === 0) {
1084
+ await runtime.output("No marketplaces configured.");
1085
+ } else {
1086
+ const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`);
1087
+ await runtime.output(`Marketplaces:\n${lines.join("\n")}`);
1088
+ }
1089
+ return commandConsumed();
1090
+ }
1091
+ case "discover": {
1092
+ const plugins = await manager.listAvailablePlugins(rest || undefined);
1093
+ if (plugins.length === 0) {
1094
+ const marketplaces = await manager.listMarketplaces();
1095
+ await runtime.output(
1096
+ marketplaces.length === 0
1097
+ ? "No marketplaces configured. Try:\n /marketplace add anthropics/claude-plugins-official"
1098
+ : "No plugins available in configured marketplaces",
1099
+ );
1100
+ return commandConsumed();
1101
+ }
1102
+ const lines = ["Available plugins:"];
1103
+ for (const plugin of plugins) {
1104
+ lines.push(` - ${plugin.name}${plugin.version ? `@${plugin.version}` : ""}`);
1105
+ if (plugin.description) lines.push(` ${plugin.description}`);
1106
+ }
1107
+ await runtime.output(lines.join("\n"));
1108
+ return commandConsumed();
1109
+ }
1110
+ case "install": {
1111
+ const parsed = parseMarketplaceInstallArgs(rest);
1112
+ if ("error" in parsed) return usage(parsed.error, runtime);
1113
+ const atIndex = parsed.installSpec.lastIndexOf("@");
1114
+ const pluginName = parsed.installSpec.slice(0, atIndex);
1115
+ const marketplace = parsed.installSpec.slice(atIndex + 1);
1116
+ await manager.installPlugin(pluginName, marketplace, { force: parsed.force, scope: parsed.scope });
1117
+ await runtime.reloadPlugins();
1118
+ await runtime.output(`Installed ${pluginName} from ${marketplace}`);
1119
+ return commandConsumed();
1120
+ }
1121
+ case "uninstall": {
1122
+ const parsed = parsePluginScopeArgs(
1123
+ rest,
1124
+ "Usage: /marketplace uninstall [--scope user|project] <name@marketplace>",
1125
+ );
1126
+ if ("error" in parsed) return usage(parsed.error, runtime);
1127
+ await manager.uninstallPlugin(parsed.pluginId, parsed.scope);
1128
+ await runtime.reloadPlugins();
1129
+ await runtime.output(`Uninstalled ${parsed.pluginId}`);
1130
+ return commandConsumed();
1131
+ }
1132
+ case "installed": {
1133
+ const installed = await manager.listInstalledPlugins();
1134
+ if (installed.length === 0) {
1135
+ await runtime.output("No marketplace plugins installed");
1136
+ } else {
1137
+ const lines = installed.map(
1138
+ p => ` ${p.id} [${p.scope}]${p.shadowedBy ? " [shadowed]" : ""} (${p.entries.length} entry)`,
1139
+ );
1140
+ await runtime.output(`Installed plugins:\n${lines.join("\n")}`);
1141
+ }
1142
+ return commandConsumed();
1143
+ }
1144
+ case "upgrade": {
1145
+ if (rest) {
1146
+ const parsed = parsePluginScopeArgs(
1147
+ rest,
1148
+ "Usage: /marketplace upgrade [--scope user|project] <name@marketplace>",
1149
+ );
1150
+ if ("error" in parsed) return usage(parsed.error, runtime);
1151
+ const result = await manager.upgradePlugin(parsed.pluginId, parsed.scope);
1152
+ await runtime.reloadPlugins();
1153
+ await runtime.output(`Upgraded ${parsed.pluginId} to ${result.version}`);
1154
+ return commandConsumed();
1155
+ }
1156
+ const results = await manager.upgradeAllPlugins();
1157
+ if (results.length === 0) {
1158
+ await runtime.output("All marketplace plugins are up to date");
1159
+ } else {
1160
+ await runtime.reloadPlugins();
1161
+ const lines = results.map(r => ` ${r.pluginId}: ${r.from} -> ${r.to}`);
1162
+ await runtime.output(`Upgraded ${results.length} plugin(s):\n${lines.join("\n")}`);
1163
+ }
1164
+ return commandConsumed();
1165
+ }
1166
+ default:
1167
+ return usage(
1168
+ `Unknown /marketplace subcommand: ${verb}. Use /marketplace help for available commands.`,
1169
+ runtime,
1170
+ );
1171
+ }
1172
+ } catch (err) {
1173
+ return usage(`Marketplace error: ${errorMessage(err)}`, runtime);
1174
+ }
1175
+ },
1176
+ handleTui: async (command, runtime) => {
690
1177
  runtime.ctx.editor.setText("");
691
1178
  const args = command.args.trim().split(/\s+/);
692
1179
  const sub = args[0] || "install";
@@ -877,6 +1364,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
877
1364
  {
878
1365
  name: "plugins",
879
1366
  description: "View and manage installed plugins",
1367
+ acpDescription: "Manage plugins",
1368
+ acpInputHint: "[list|enable|disable]",
880
1369
  subcommands: [
881
1370
  { name: "list", description: "List all installed plugins (npm + marketplace)" },
882
1371
  { name: "enable", description: "Enable a marketplace plugin", usage: "<name@marketplace>" },
@@ -884,6 +1373,53 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
884
1373
  ],
885
1374
  allowArgs: true,
886
1375
  handle: async (command, runtime) => {
1376
+ const { verb, rest } = parseSubcommand(command.args);
1377
+ try {
1378
+ if (verb === "enable" || verb === "disable") {
1379
+ const parsed = parsePluginScopeArgs(
1380
+ rest,
1381
+ `Usage: /plugins ${verb} [--scope user|project] <name@marketplace>`,
1382
+ );
1383
+ if ("error" in parsed) return usage(parsed.error, runtime);
1384
+ const manager = await createMarketplaceManager(runtime);
1385
+ const isEnable = verb === "enable";
1386
+ await manager.setPluginEnabled(parsed.pluginId, isEnable, parsed.scope);
1387
+ await runtime.reloadPlugins();
1388
+ await runtime.output(`${isEnable ? "Enabled" : "Disabled"} ${parsed.pluginId}`);
1389
+ return commandConsumed();
1390
+ }
1391
+ // Default: list
1392
+ const lines: string[] = [];
1393
+ const npmManager = new PluginManager();
1394
+ const npmPlugins = await npmManager.list();
1395
+ if (npmPlugins.length > 0) {
1396
+ lines.push("npm plugins:");
1397
+ for (const plugin of npmPlugins) {
1398
+ const status = plugin.enabled === false ? " (disabled)" : "";
1399
+ lines.push(` ${plugin.name}@${plugin.version}${status}`);
1400
+ }
1401
+ }
1402
+
1403
+ const marketplaceManager = await createMarketplaceManager(runtime);
1404
+ const marketplacePlugins = await marketplaceManager.listInstalledPlugins();
1405
+ if (marketplacePlugins.length > 0) {
1406
+ if (lines.length > 0) lines.push("");
1407
+ lines.push("marketplace plugins:");
1408
+ for (const plugin of marketplacePlugins) {
1409
+ const entry = plugin.entries[0];
1410
+ const status = entry?.enabled === false ? " (disabled)" : "";
1411
+ const shadowed = plugin.shadowedBy ? " [shadowed]" : "";
1412
+ lines.push(` ${plugin.id} v${entry?.version ?? "?"}${status} [${plugin.scope}]${shadowed}`);
1413
+ }
1414
+ }
1415
+
1416
+ await runtime.output(lines.length === 0 ? "No plugins installed" : lines.join("\n"));
1417
+ return commandConsumed();
1418
+ } catch (err) {
1419
+ return usage(`Plugin error: ${errorMessage(err)}`, runtime);
1420
+ }
1421
+ },
1422
+ handleTui: async (command, runtime) => {
887
1423
  runtime.ctx.editor.setText("");
888
1424
  const args = command.args.trim().split(/\s+/);
889
1425
  const sub = args[0] || "list";
@@ -958,7 +1494,13 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
958
1494
  {
959
1495
  name: "reload-plugins",
960
1496
  description: "Reload all plugins (skills, commands, hooks, tools, agents, MCP)",
1497
+ acpDescription: "Reload all plugins",
961
1498
  handle: async (_command, runtime) => {
1499
+ await runtime.reloadPlugins();
1500
+ await runtime.output("Plugins reloaded.");
1501
+ return commandConsumed();
1502
+ },
1503
+ handleTui: async (_command, runtime) => {
962
1504
  // Invalidate registry fs caches and the plugin roots cache so
963
1505
  // listClaudePluginRoots re-reads from disk on next access.
964
1506
  const projectPath = await resolveActiveProjectRegistryPath(runtime.ctx.sessionManager.getCwd());
@@ -971,9 +1513,23 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
971
1513
  {
972
1514
  name: "force",
973
1515
  description: "Force next turn to use a specific tool",
1516
+ aliases: ["force:"],
974
1517
  inlineHint: "<tool-name> [prompt]",
975
1518
  allowArgs: true,
976
- handle: (command, runtime) => {
1519
+ handle: async (command, runtime) => {
1520
+ const spaceIdx = command.args.indexOf(" ");
1521
+ const toolName = spaceIdx === -1 ? command.args : command.args.slice(0, spaceIdx);
1522
+ const prompt = spaceIdx === -1 ? "" : command.args.slice(spaceIdx + 1).trim();
1523
+ if (!toolName) return usage("Usage: /force:<tool-name> [prompt]", runtime);
1524
+ try {
1525
+ runtime.session.setForcedToolChoice(toolName);
1526
+ } catch (err) {
1527
+ return usage(errorMessage(err), runtime);
1528
+ }
1529
+ await runtime.output(`Next turn forced to use ${toolName}.`);
1530
+ return prompt ? { prompt } : commandConsumed();
1531
+ },
1532
+ handleTui: (command, runtime) => {
977
1533
  const spaceIdx = command.args.indexOf(" ");
978
1534
  const toolName = spaceIdx === -1 ? command.args : command.args.slice(0, spaceIdx);
979
1535
  const prompt = spaceIdx === -1 ? "" : command.args.slice(spaceIdx + 1).trim();
@@ -988,7 +1544,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
988
1544
  runtime.ctx.session.setForcedToolChoice(toolName);
989
1545
  runtime.ctx.showStatus(`Next turn forced to use ${toolName}.`);
990
1546
  } catch (error) {
991
- runtime.ctx.showError(error instanceof Error ? error.message : String(error));
1547
+ runtime.ctx.showError(errorMessage(error));
992
1548
  runtime.ctx.editor.setText("");
993
1549
  return;
994
1550
  }
@@ -996,17 +1552,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
996
1552
  runtime.ctx.editor.setText("");
997
1553
 
998
1554
  // If a prompt was provided, pass it through as input
999
- if (prompt) return prompt;
1555
+ if (prompt) return { prompt };
1000
1556
  },
1001
1557
  },
1002
1558
  {
1003
1559
  name: "quit",
1004
1560
  description: "Quit the application",
1005
- handle: shutdownHandler,
1561
+ handleTui: shutdownHandlerTui,
1006
1562
  },
1007
1563
  ];
1008
1564
 
1009
- const BUILTIN_SLASH_COMMAND_LOOKUP = new Map<string, BuiltinSlashCommandSpec>();
1565
+ const BUILTIN_SLASH_COMMAND_LOOKUP = new Map<string, SlashCommandSpec>();
1010
1566
  for (const command of BUILTIN_SLASH_COMMAND_REGISTRY) {
1011
1567
  BUILTIN_SLASH_COMMAND_LOOKUP.set(command.name, command);
1012
1568
  for (const alias of command.aliases ?? []) {
@@ -1025,17 +1581,24 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
1025
1581
  );
1026
1582
 
1027
1583
  /**
1028
- * Execute a builtin slash command when it matches known command syntax.
1584
+ * Unified registry exposed for cross-mode tooling. Each spec carries at least
1585
+ * one of `handle` / `handleTui`. The TUI dispatcher prefers `handleTui`; the
1586
+ * ACP dispatcher requires `handle` and skips TUI-only entries.
1587
+ */
1588
+ export const BUILTIN_SLASH_COMMANDS_INTERNAL: ReadonlyArray<SlashCommandSpec> = BUILTIN_SLASH_COMMAND_REGISTRY;
1589
+
1590
+ /**
1591
+ * Execute a builtin slash command in the interactive TUI.
1029
1592
  *
1030
- * Returns `false` when no builtin matched. Returns `true` when a command consumed
1031
- * the input entirely. Returns a `string` when the command was handled but remaining
1032
- * text should be sent as a prompt.
1593
+ * Returns `false` when no builtin matched. Returns `true` when a command
1594
+ * consumed the input entirely. Returns a `string` when the command was handled
1595
+ * but remaining text should be sent as a prompt.
1033
1596
  */
1034
1597
  export async function executeBuiltinSlashCommand(
1035
1598
  text: string,
1036
1599
  runtime: BuiltinSlashCommandRuntime,
1037
1600
  ): Promise<string | boolean> {
1038
- const parsed = parseBuiltinSlashCommand(text);
1601
+ const parsed = parseSlashCommand(text);
1039
1602
  if (!parsed) return false;
1040
1603
 
1041
1604
  const command = BUILTIN_SLASH_COMMAND_LOOKUP.get(parsed.name);
@@ -1043,7 +1606,45 @@ export async function executeBuiltinSlashCommand(
1043
1606
  if (parsed.args.length > 0 && !command.allowArgs) {
1044
1607
  return false;
1045
1608
  }
1609
+ if (command.handleTui) {
1610
+ const result = await command.handleTui(parsed, runtime);
1611
+ if (result && typeof result === "object" && "prompt" in result) return result.prompt;
1612
+ return true;
1613
+ }
1614
+ if (command.handle) {
1615
+ // No TUI-specific override → adapt the ACP/text-mode `handle` to the
1616
+ // TUI by routing `runtime.output` through `ctx.showStatus`, clearing
1617
+ // the editor after the call, and reusing the active session's plugin
1618
+ // reload pipeline. Spec authors get a single body usable from either
1619
+ // dispatcher without forcing every TUI test to construct the full
1620
+ // `SlashCommandRuntime` shape.
1621
+ const ctx = runtime.ctx;
1622
+ const adapted: SlashCommandRuntime = {
1623
+ session: ctx.session,
1624
+ sessionManager: ctx.sessionManager,
1625
+ settings: ctx.settings,
1626
+ cwd: ctx.sessionManager.getCwd(),
1627
+ output: (text: string) => {
1628
+ ctx.showStatus(text);
1629
+ },
1630
+ refreshCommands: () => ctx.refreshSlashCommandState(),
1631
+ reloadPlugins: async () => {
1632
+ const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
1633
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
1634
+ await ctx.refreshSlashCommandState();
1635
+ },
1636
+ };
1637
+ const result = await command.handle(parsed, adapted);
1638
+ ctx.editor.setText("");
1639
+ if (result && typeof result === "object" && "prompt" in result) return result.prompt;
1640
+ return true;
1641
+ }
1642
+ return false;
1643
+ }
1046
1644
 
1047
- const remaining = await command.handle(parsed, runtime);
1048
- return remaining ?? true;
1645
+ /** Look up a unified spec by name or alias. Used by the ACP dispatcher. */
1646
+ export function lookupBuiltinSlashCommand(name: string): SlashCommandSpec | undefined {
1647
+ return BUILTIN_SLASH_COMMAND_LOOKUP.get(name);
1049
1648
  }
1649
+
1650
+ export type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime, SlashCommandSpec, TuiSlashCommandRuntime };