@oh-my-pi/pi-coding-agent 14.9.8 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +11 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/cli/args.ts +2 -2
- package/src/cli/stats-cli.ts +2 -0
- package/src/cli.ts +24 -1
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/config/model-resolver.ts +30 -0
- package/src/config/settings-schema.ts +61 -9
- package/src/config/settings.ts +18 -1
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +10 -0
- package/src/edit/streaming.ts +1 -1
- package/src/eval/js/context-manager.ts +10 -9
- package/src/eval/js/shared/rewrite-imports.ts +120 -48
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/extensibility/extensions/runner.ts +54 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +33 -1
- package/src/hashline/grammar.lark +1 -1
- package/src/hashline/input.ts +11 -5
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +13 -9
- package/src/modes/acp/acp-agent.ts +361 -54
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/controllers/command-controller.ts +14 -6
- package/src/modes/controllers/event-controller.ts +24 -11
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +72 -39
- package/src/modes/interactive-mode.ts +71 -7
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/ui-helpers.ts +15 -3
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +22 -21
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/exit-plan-mode.md +2 -2
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/hashline.md +20 -20
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +7 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +69 -12
- package/src/session/agent-session.ts +231 -22
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +699 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/task/executor.ts +10 -3
- package/src/task/index.ts +29 -51
- package/src/task/render.ts +6 -3
- package/src/task/worktree.ts +170 -239
- package/src/tools/bash.ts +176 -2
- package/src/tools/browser/tab-supervisor.ts +13 -13
- package/src/tools/conflict-detect.ts +6 -6
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +19 -1
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +682 -176
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/read.ts +110 -27
- package/src/tools/write.ts +23 -1
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/git.ts +5 -0
- package/src/task/isolation-backend.ts +0 -94
|
@@ -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
|
-
|
|
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<
|
|
60
|
+
const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
100
61
|
{
|
|
101
62
|
name: "settings",
|
|
102
63
|
description: "Open settings menu",
|
|
103
|
-
|
|
64
|
+
handleTui: (_command, runtime) => {
|
|
104
65
|
runtime.ctx.showSettingsSelector();
|
|
105
66
|
runtime.ctx.editor.setText("");
|
|
106
67
|
},
|
|
@@ -110,7 +71,7 @@ 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
|
-
|
|
74
|
+
handleTui: async (command, runtime) => {
|
|
114
75
|
await runtime.ctx.handlePlanModeCommand(command.args || undefined);
|
|
115
76
|
runtime.ctx.editor.setText("");
|
|
116
77
|
},
|
|
@@ -121,7 +82,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
121
82
|
"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
83
|
inlineHint: "[count|duration]",
|
|
123
84
|
allowArgs: true,
|
|
124
|
-
|
|
85
|
+
handleTui: async (command, runtime) => {
|
|
125
86
|
await runtime.ctx.handleLoopCommand(command.args);
|
|
126
87
|
runtime.ctx.editor.setText("");
|
|
127
88
|
},
|
|
@@ -130,7 +91,38 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
130
91
|
name: "model",
|
|
131
92
|
aliases: ["models"],
|
|
132
93
|
description: "Select model (opens selector UI)",
|
|
133
|
-
|
|
94
|
+
acpDescription: "Show current model selection",
|
|
95
|
+
handle: async (command, runtime) => {
|
|
96
|
+
if (command.args) {
|
|
97
|
+
const modelId = command.args.trim();
|
|
98
|
+
const availableModels = runtime.session.getAvailableModels?.() ?? [];
|
|
99
|
+
const match = availableModels.find(
|
|
100
|
+
model => model.id === modelId || `${model.provider}/${model.id}` === modelId,
|
|
101
|
+
);
|
|
102
|
+
if (!match) {
|
|
103
|
+
return usage(
|
|
104
|
+
`Unknown model: ${modelId}. Use ACP \`session/setModel\` for picker-driven selection or list available models with /model.`,
|
|
105
|
+
runtime,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await runtime.session.setModel(match);
|
|
110
|
+
await runtime.output(`Model set to ${match.provider}/${match.id}.`);
|
|
111
|
+
await runtime.notifyTitleChanged?.();
|
|
112
|
+
await runtime.notifyConfigChanged?.();
|
|
113
|
+
return commandConsumed();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return usage(`Failed to set model: ${errorMessage(err)}`, runtime);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const model = runtime.session.model;
|
|
120
|
+
await runtime.output(
|
|
121
|
+
model ? `Current model: ${model.provider}/${model.id}` : "No model is currently selected.",
|
|
122
|
+
);
|
|
123
|
+
return commandConsumed();
|
|
124
|
+
},
|
|
125
|
+
handleTui: (_command, runtime) => {
|
|
134
126
|
runtime.ctx.showModelSelector();
|
|
135
127
|
runtime.ctx.editor.setText("");
|
|
136
128
|
},
|
|
@@ -138,13 +130,38 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
138
130
|
{
|
|
139
131
|
name: "fast",
|
|
140
132
|
description: "Toggle fast mode (OpenAI service tier priority)",
|
|
133
|
+
acpDescription: "Toggle fast mode",
|
|
134
|
+
acpInputHint: "[on|off|status]",
|
|
141
135
|
subcommands: [
|
|
142
136
|
{ name: "on", description: "Enable fast mode" },
|
|
143
137
|
{ name: "off", description: "Disable fast mode" },
|
|
144
138
|
{ name: "status", description: "Show fast mode status" },
|
|
145
139
|
],
|
|
146
140
|
allowArgs: true,
|
|
147
|
-
handle: (command, runtime) => {
|
|
141
|
+
handle: async (command, runtime) => {
|
|
142
|
+
const arg = command.args.toLowerCase();
|
|
143
|
+
if (!arg || arg === "toggle") {
|
|
144
|
+
const enabled = runtime.session.toggleFastMode();
|
|
145
|
+
await runtime.output(`Fast mode ${enabled ? "enabled" : "disabled"}.`);
|
|
146
|
+
return commandConsumed();
|
|
147
|
+
}
|
|
148
|
+
if (arg === "on") {
|
|
149
|
+
runtime.session.setFastMode(true);
|
|
150
|
+
await runtime.output("Fast mode enabled.");
|
|
151
|
+
return commandConsumed();
|
|
152
|
+
}
|
|
153
|
+
if (arg === "off") {
|
|
154
|
+
runtime.session.setFastMode(false);
|
|
155
|
+
await runtime.output("Fast mode disabled.");
|
|
156
|
+
return commandConsumed();
|
|
157
|
+
}
|
|
158
|
+
if (arg === "status") {
|
|
159
|
+
await runtime.output(`Fast mode is ${runtime.session.isFastModeEnabled() ? "on" : "off"}.`);
|
|
160
|
+
return commandConsumed();
|
|
161
|
+
}
|
|
162
|
+
return usage("Usage: /fast [on|off|status]", runtime);
|
|
163
|
+
},
|
|
164
|
+
handleTui: (command, runtime) => {
|
|
148
165
|
const arg = command.args.trim().toLowerCase();
|
|
149
166
|
if (!arg || arg === "toggle") {
|
|
150
167
|
const enabled = runtime.ctx.session.toggleFastMode();
|
|
@@ -183,6 +200,23 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
183
200
|
inlineHint: "[path]",
|
|
184
201
|
allowArgs: true,
|
|
185
202
|
handle: async (command, runtime) => {
|
|
203
|
+
const arg = command.args.trim();
|
|
204
|
+
// Match the interactive `/export` behavior: clipboard aliases are not a
|
|
205
|
+
// valid export target. Without this, the literal value (`copy`,
|
|
206
|
+
// `--copy`, `clipboard`) is passed to `exportToHtml` and becomes the
|
|
207
|
+
// output filename.
|
|
208
|
+
if (arg === "--copy" || arg === "clipboard" || arg === "copy") {
|
|
209
|
+
return usage("Use /dump to copy the session to clipboard.", runtime);
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const filePath = await runtime.session.exportToHtml(arg || undefined);
|
|
213
|
+
await runtime.output(`Session exported to: ${filePath}`);
|
|
214
|
+
return commandConsumed();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return usage(`Failed to export session: ${errorMessage(err)}`, runtime);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
handleTui: async (command, runtime) => {
|
|
186
220
|
await runtime.ctx.handleExportCommand(command.text);
|
|
187
221
|
runtime.ctx.editor.setText("");
|
|
188
222
|
},
|
|
@@ -190,7 +224,13 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
190
224
|
{
|
|
191
225
|
name: "dump",
|
|
192
226
|
description: "Copy session transcript to clipboard",
|
|
227
|
+
acpDescription: "Return full transcript as plain text",
|
|
193
228
|
handle: async (_command, runtime) => {
|
|
229
|
+
const text = runtime.session.formatSessionAsText();
|
|
230
|
+
await runtime.output(text || "No messages to dump yet.");
|
|
231
|
+
return commandConsumed();
|
|
232
|
+
},
|
|
233
|
+
handleTui: async (_command, runtime) => {
|
|
194
234
|
await runtime.ctx.handleDumpCommand();
|
|
195
235
|
runtime.ctx.editor.setText("");
|
|
196
236
|
},
|
|
@@ -199,6 +239,32 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
199
239
|
name: "share",
|
|
200
240
|
description: "Share session as a secret GitHub gist",
|
|
201
241
|
handle: async (_command, runtime) => {
|
|
242
|
+
const tmpFile = path.join(os.tmpdir(), `${Snowflake.next()}.html`);
|
|
243
|
+
try {
|
|
244
|
+
try {
|
|
245
|
+
await runtime.session.exportToHtml(tmpFile);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return usage(`Failed to export session: ${errorMessage(err)}`, runtime);
|
|
248
|
+
}
|
|
249
|
+
const result = await $`gh gist create --public=false ${tmpFile}`.quiet().nothrow();
|
|
250
|
+
if (result.exitCode !== 0) {
|
|
251
|
+
return usage(
|
|
252
|
+
`Failed to create gist: ${result.stderr.toString("utf-8").trim() || "unknown error"}`,
|
|
253
|
+
runtime,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const gistUrl = result.stdout.toString("utf-8").trim();
|
|
257
|
+
const gistId = gistUrl.split("/").pop();
|
|
258
|
+
if (!gistId) return usage("Failed to parse gist ID from gh output", runtime);
|
|
259
|
+
await runtime.output(`Share URL: https://gistpreview.github.io/?${gistId}\nGist: ${gistUrl}`);
|
|
260
|
+
return commandConsumed();
|
|
261
|
+
} catch {
|
|
262
|
+
return usage("GitHub CLI (gh) is required for /share. Install it from https://cli.github.com/.", runtime);
|
|
263
|
+
} finally {
|
|
264
|
+
await fs.rm(tmpFile, { force: true }).catch(() => {});
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
handleTui: async (_command, runtime) => {
|
|
202
268
|
await runtime.ctx.handleShareCommand();
|
|
203
269
|
runtime.ctx.editor.setText("");
|
|
204
270
|
},
|
|
@@ -206,12 +272,40 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
206
272
|
{
|
|
207
273
|
name: "browser",
|
|
208
274
|
description: "Toggle browser headless vs visible mode",
|
|
275
|
+
acpInputHint: "[headless|visible]",
|
|
209
276
|
subcommands: [
|
|
210
277
|
{ name: "headless", description: "Switch to headless mode" },
|
|
211
278
|
{ name: "visible", description: "Switch to visible mode" },
|
|
212
279
|
],
|
|
213
280
|
allowArgs: true,
|
|
214
281
|
handle: async (command, runtime) => {
|
|
282
|
+
const arg = command.args.toLowerCase();
|
|
283
|
+
const enabled = runtime.settings.get("browser.enabled" as SettingPath) as boolean;
|
|
284
|
+
if (!enabled) return usage("Browser tool is disabled (enable in settings).", runtime);
|
|
285
|
+
const current = runtime.settings.get("browser.headless" as SettingPath) as boolean;
|
|
286
|
+
let next = current;
|
|
287
|
+
if (!arg) next = !current;
|
|
288
|
+
else if (arg === "headless" || arg === "hidden") next = true;
|
|
289
|
+
else if (arg === "visible" || arg === "show" || arg === "headful") next = false;
|
|
290
|
+
else return usage("Usage: /browser [headless|visible]", runtime);
|
|
291
|
+
runtime.settings.set("browser.headless" as SettingPath, next as SettingValue<SettingPath>);
|
|
292
|
+
const tool = runtime.session.getToolByName("browser");
|
|
293
|
+
if (tool && "restartForModeChange" in tool) {
|
|
294
|
+
try {
|
|
295
|
+
await (tool as { restartForModeChange: () => Promise<void> }).restartForModeChange();
|
|
296
|
+
} catch (err) {
|
|
297
|
+
// Setting was already mutated; surface the restart failure so the
|
|
298
|
+
// user knows the browser is in an inconsistent state.
|
|
299
|
+
await runtime.output(
|
|
300
|
+
`Browser mode set to ${next ? "headless" : "visible"}, but restart failed: ${errorMessage(err)}`,
|
|
301
|
+
);
|
|
302
|
+
return commandConsumed();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
await runtime.output(`Browser mode: ${next ? "headless" : "visible"}`);
|
|
306
|
+
return commandConsumed();
|
|
307
|
+
},
|
|
308
|
+
handleTui: async (command, runtime) => {
|
|
215
309
|
const arg = command.args.toLowerCase();
|
|
216
310
|
const current = settings.get("browser.headless" as SettingPath) as boolean;
|
|
217
311
|
let next = current;
|
|
@@ -222,9 +316,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
222
316
|
}
|
|
223
317
|
if (!arg) {
|
|
224
318
|
next = !current;
|
|
225
|
-
} else if (
|
|
319
|
+
} else if (arg === "headless" || arg === "hidden") {
|
|
226
320
|
next = true;
|
|
227
|
-
} else if (
|
|
321
|
+
} else if (arg === "visible" || arg === "show" || arg === "headful") {
|
|
228
322
|
next = false;
|
|
229
323
|
} else {
|
|
230
324
|
runtime.ctx.showStatus("Usage: /browser [headless|visible]");
|
|
@@ -237,9 +331,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
237
331
|
try {
|
|
238
332
|
await (tool as { restartForModeChange: () => Promise<void> }).restartForModeChange();
|
|
239
333
|
} catch (error) {
|
|
240
|
-
runtime.ctx.showWarning(
|
|
241
|
-
`Failed to restart browser: ${error instanceof Error ? error.message : String(error)}`,
|
|
242
|
-
);
|
|
334
|
+
runtime.ctx.showWarning(`Failed to restart browser: ${errorMessage(error)}`);
|
|
243
335
|
runtime.ctx.editor.setText("");
|
|
244
336
|
return;
|
|
245
337
|
}
|
|
@@ -258,7 +350,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
258
350
|
{ name: "cmd", description: "Copy last bash/python command" },
|
|
259
351
|
],
|
|
260
352
|
allowArgs: true,
|
|
261
|
-
|
|
353
|
+
handleTui: async (command, runtime) => {
|
|
262
354
|
const sub = command.args.trim().toLowerCase() || undefined;
|
|
263
355
|
await runtime.ctx.handleCopyCommand(sub);
|
|
264
356
|
runtime.ctx.editor.setText("");
|
|
@@ -267,6 +359,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
267
359
|
{
|
|
268
360
|
name: "todo",
|
|
269
361
|
description: "View or modify the agent's todo list",
|
|
362
|
+
acpDescription: "Manage todos",
|
|
363
|
+
acpInputHint: "<subcommand>",
|
|
270
364
|
subcommands: [
|
|
271
365
|
{ name: "edit", description: "Open todos in $EDITOR (Markdown round-trip)" },
|
|
272
366
|
{ name: "copy", description: "Copy todos as Markdown to clipboard" },
|
|
@@ -283,7 +377,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
283
377
|
{ name: "rm", description: "Remove task/phase/all (fuzzy-matched)", usage: "[<task|phase>]" },
|
|
284
378
|
],
|
|
285
379
|
allowArgs: true,
|
|
286
|
-
handle:
|
|
380
|
+
handle: handleTodoAcp,
|
|
381
|
+
handleTui: async (command, runtime) => {
|
|
287
382
|
await runtime.ctx.handleTodoCommand(command.args);
|
|
288
383
|
runtime.ctx.editor.setText("");
|
|
289
384
|
},
|
|
@@ -291,12 +386,46 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
291
386
|
{
|
|
292
387
|
name: "session",
|
|
293
388
|
description: "Session management commands",
|
|
389
|
+
acpDescription: "Show session information",
|
|
390
|
+
acpInputHint: "info|delete",
|
|
294
391
|
subcommands: [
|
|
295
392
|
{ name: "info", description: "Show session info and stats" },
|
|
296
393
|
{ name: "delete", description: "Delete current session and return to selector" },
|
|
297
394
|
],
|
|
298
395
|
allowArgs: true,
|
|
299
396
|
handle: async (command, runtime) => {
|
|
397
|
+
if (!command.args || command.args === "info") {
|
|
398
|
+
await runtime.output(
|
|
399
|
+
[
|
|
400
|
+
`Session: ${runtime.session.sessionId}`,
|
|
401
|
+
`Title: ${runtime.session.sessionName}`,
|
|
402
|
+
`CWD: ${runtime.cwd}`,
|
|
403
|
+
].join("\n"),
|
|
404
|
+
);
|
|
405
|
+
return commandConsumed();
|
|
406
|
+
}
|
|
407
|
+
if (command.args === "delete") {
|
|
408
|
+
if (runtime.session.isStreaming) return usage("Cannot delete the session while streaming.", runtime);
|
|
409
|
+
const sessionFile = runtime.sessionManager.getSessionFile();
|
|
410
|
+
if (!sessionFile) return usage("No session file to delete (in-memory session).", runtime);
|
|
411
|
+
// Route through the active SessionManager so the persist writer is
|
|
412
|
+
// closed before the file is deleted. Constructing a fresh
|
|
413
|
+
// FileSessionStorage and calling deleteSessionWithArtifacts leaves
|
|
414
|
+
// the active writer attached to the now-deleted path, so the next
|
|
415
|
+
// prompt would silently resurrect or corrupt the "deleted" file.
|
|
416
|
+
try {
|
|
417
|
+
await runtime.sessionManager.dropSession(sessionFile);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return usage(`Failed to delete session: ${errorMessage(err)}`, runtime);
|
|
420
|
+
}
|
|
421
|
+
await runtime.output(
|
|
422
|
+
`Session deleted: ${sessionFile}. Use ACP \`session/load\` to switch to another session.`,
|
|
423
|
+
);
|
|
424
|
+
return commandConsumed();
|
|
425
|
+
}
|
|
426
|
+
return usage("Usage: /session [info|delete]", runtime);
|
|
427
|
+
},
|
|
428
|
+
handleTui: async (command, runtime) => {
|
|
300
429
|
const sub = command.args.trim().toLowerCase() || "info";
|
|
301
430
|
if (sub === "delete") {
|
|
302
431
|
runtime.ctx.editor.setText("");
|
|
@@ -311,7 +440,35 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
311
440
|
{
|
|
312
441
|
name: "jobs",
|
|
313
442
|
description: "Show async background jobs status",
|
|
443
|
+
acpDescription: "Show background jobs",
|
|
314
444
|
handle: async (_command, runtime) => {
|
|
445
|
+
const snapshot = runtime.session.getAsyncJobSnapshot({ recentLimit: 5 });
|
|
446
|
+
if (!snapshot || (snapshot.running.length === 0 && snapshot.recent.length === 0)) {
|
|
447
|
+
await runtime.output(
|
|
448
|
+
"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.)",
|
|
449
|
+
);
|
|
450
|
+
return commandConsumed();
|
|
451
|
+
}
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
const lines: string[] = ["Background Jobs", `Running: ${snapshot.running.length}`];
|
|
454
|
+
if (snapshot.running.length > 0) {
|
|
455
|
+
lines.push("", "Running Jobs");
|
|
456
|
+
for (const job of snapshot.running) {
|
|
457
|
+
lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
|
|
458
|
+
lines.push(` ${job.label}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (snapshot.recent.length > 0) {
|
|
462
|
+
lines.push("", "Recent Jobs");
|
|
463
|
+
for (const job of snapshot.recent) {
|
|
464
|
+
lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
|
|
465
|
+
lines.push(` ${job.label}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
await runtime.output(lines.join("\n"));
|
|
469
|
+
return commandConsumed();
|
|
470
|
+
},
|
|
471
|
+
handleTui: async (_command, runtime) => {
|
|
315
472
|
await runtime.ctx.handleJobsCommand();
|
|
316
473
|
runtime.ctx.editor.setText("");
|
|
317
474
|
},
|
|
@@ -319,7 +476,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
319
476
|
{
|
|
320
477
|
name: "usage",
|
|
321
478
|
description: "Show provider usage and limits",
|
|
479
|
+
acpDescription: "Show token usage",
|
|
322
480
|
handle: async (_command, runtime) => {
|
|
481
|
+
await runtime.output(await buildUsageReportText(runtime));
|
|
482
|
+
return commandConsumed();
|
|
483
|
+
},
|
|
484
|
+
handleTui: async (_command, runtime) => {
|
|
323
485
|
await runtime.ctx.handleUsageCommand();
|
|
324
486
|
runtime.ctx.editor.setText("");
|
|
325
487
|
},
|
|
@@ -327,9 +489,28 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
327
489
|
{
|
|
328
490
|
name: "changelog",
|
|
329
491
|
description: "Show changelog entries",
|
|
492
|
+
acpDescription: "Show changelog",
|
|
493
|
+
acpInputHint: "[full]",
|
|
330
494
|
subcommands: [{ name: "full", description: "Show complete changelog" }],
|
|
331
495
|
allowArgs: true,
|
|
332
496
|
handle: async (command, runtime) => {
|
|
497
|
+
const changelogPath = getChangelogPath();
|
|
498
|
+
const allEntries = await parseChangelog(changelogPath);
|
|
499
|
+
const showFull = command.args.trim().toLowerCase() === "full";
|
|
500
|
+
const entriesToShow = showFull ? allEntries : allEntries.slice(0, 3);
|
|
501
|
+
if (entriesToShow.length === 0) {
|
|
502
|
+
await runtime.output("No changelog entries found.");
|
|
503
|
+
return commandConsumed();
|
|
504
|
+
}
|
|
505
|
+
await runtime.output(
|
|
506
|
+
[...entriesToShow]
|
|
507
|
+
.reverse()
|
|
508
|
+
.map(entry => entry.content)
|
|
509
|
+
.join("\n\n"),
|
|
510
|
+
);
|
|
511
|
+
return commandConsumed();
|
|
512
|
+
},
|
|
513
|
+
handleTui: async (command, runtime) => {
|
|
333
514
|
const showFull = command.args.split(/\s+/).filter(Boolean).includes("full");
|
|
334
515
|
await runtime.ctx.handleChangelogCommand(showFull);
|
|
335
516
|
runtime.ctx.editor.setText("");
|
|
@@ -338,7 +519,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
338
519
|
{
|
|
339
520
|
name: "hotkeys",
|
|
340
521
|
description: "Show all keyboard shortcuts",
|
|
341
|
-
|
|
522
|
+
handleTui: (_command, runtime) => {
|
|
342
523
|
runtime.ctx.handleHotkeysCommand();
|
|
343
524
|
runtime.ctx.editor.setText("");
|
|
344
525
|
},
|
|
@@ -346,7 +527,18 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
346
527
|
{
|
|
347
528
|
name: "tools",
|
|
348
529
|
description: "Show tools currently visible to the agent",
|
|
349
|
-
|
|
530
|
+
acpDescription: "Show available tools",
|
|
531
|
+
handle: async (_command, runtime) => {
|
|
532
|
+
const active = runtime.session.getActiveToolNames();
|
|
533
|
+
const all = runtime.session.getAllToolNames();
|
|
534
|
+
if (all.length === 0) {
|
|
535
|
+
await runtime.output("No tools are available.");
|
|
536
|
+
return commandConsumed();
|
|
537
|
+
}
|
|
538
|
+
await runtime.output(all.map(name => `${active.includes(name) ? "*" : "-"} ${name}`).join("\n"));
|
|
539
|
+
return commandConsumed();
|
|
540
|
+
},
|
|
541
|
+
handleTui: (_command, runtime) => {
|
|
350
542
|
runtime.ctx.handleToolsCommand();
|
|
351
543
|
runtime.ctx.editor.setText("");
|
|
352
544
|
},
|
|
@@ -354,7 +546,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
354
546
|
{
|
|
355
547
|
name: "context",
|
|
356
548
|
description: "Show estimated context usage breakdown",
|
|
357
|
-
|
|
549
|
+
acpDescription: "Show context usage",
|
|
550
|
+
handle: async (_command, runtime) => {
|
|
551
|
+
await runtime.output(buildContextReportText(runtime));
|
|
552
|
+
return commandConsumed();
|
|
553
|
+
},
|
|
554
|
+
handleTui: (_command, runtime) => {
|
|
358
555
|
runtime.ctx.handleContextCommand();
|
|
359
556
|
runtime.ctx.editor.setText("");
|
|
360
557
|
},
|
|
@@ -363,7 +560,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
363
560
|
name: "extensions",
|
|
364
561
|
aliases: ["status"],
|
|
365
562
|
description: "Open Extension Control Center dashboard",
|
|
366
|
-
|
|
563
|
+
handleTui: (_command, runtime) => {
|
|
367
564
|
runtime.ctx.showExtensionsDashboard();
|
|
368
565
|
runtime.ctx.editor.setText("");
|
|
369
566
|
},
|
|
@@ -371,7 +568,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
371
568
|
{
|
|
372
569
|
name: "agents",
|
|
373
570
|
description: "Open Agent Control Center dashboard",
|
|
374
|
-
|
|
571
|
+
handleTui: (_command, runtime) => {
|
|
375
572
|
runtime.ctx.showAgentsDashboard();
|
|
376
573
|
runtime.ctx.editor.setText("");
|
|
377
574
|
},
|
|
@@ -379,7 +576,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
379
576
|
{
|
|
380
577
|
name: "branch",
|
|
381
578
|
description: "Create a new branch from a previous message",
|
|
382
|
-
|
|
579
|
+
handleTui: (_command, runtime) => {
|
|
383
580
|
if (settings.get("doubleEscapeAction") === "tree") {
|
|
384
581
|
runtime.ctx.showTreeSelector();
|
|
385
582
|
} else {
|
|
@@ -391,7 +588,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
391
588
|
{
|
|
392
589
|
name: "fork",
|
|
393
590
|
description: "Create a new fork from a previous message",
|
|
394
|
-
|
|
591
|
+
handleTui: async (_command, runtime) => {
|
|
395
592
|
runtime.ctx.editor.setText("");
|
|
396
593
|
await runtime.ctx.handleForkCommand();
|
|
397
594
|
},
|
|
@@ -399,7 +596,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
399
596
|
{
|
|
400
597
|
name: "tree",
|
|
401
598
|
description: "Navigate session tree (switch branches)",
|
|
402
|
-
|
|
599
|
+
handleTui: (_command, runtime) => {
|
|
403
600
|
runtime.ctx.showTreeSelector();
|
|
404
601
|
runtime.ctx.editor.setText("");
|
|
405
602
|
},
|
|
@@ -409,7 +606,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
409
606
|
description: "Login with OAuth provider",
|
|
410
607
|
inlineHint: "[provider|redirect URL]",
|
|
411
608
|
allowArgs: true,
|
|
412
|
-
|
|
609
|
+
handleTui: (command, runtime) => {
|
|
413
610
|
const manualInput = runtime.ctx.oauthManualInput;
|
|
414
611
|
const args = command.args.trim();
|
|
415
612
|
if (args.length > 0) {
|
|
@@ -455,7 +652,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
455
652
|
{
|
|
456
653
|
name: "logout",
|
|
457
654
|
description: "Logout from OAuth provider",
|
|
458
|
-
|
|
655
|
+
handleTui: (_command, runtime) => {
|
|
459
656
|
void runtime.ctx.showOAuthSelector("logout");
|
|
460
657
|
runtime.ctx.editor.setText("");
|
|
461
658
|
},
|
|
@@ -463,6 +660,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
463
660
|
{
|
|
464
661
|
name: "mcp",
|
|
465
662
|
description: "Manage MCP servers (add, list, remove, test)",
|
|
663
|
+
acpDescription: "Manage MCP servers",
|
|
664
|
+
inlineHint: "<subcommand>",
|
|
466
665
|
subcommands: [
|
|
467
666
|
{
|
|
468
667
|
name: "add",
|
|
@@ -491,7 +690,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
491
690
|
{ name: "help", description: "Show help message" },
|
|
492
691
|
],
|
|
493
692
|
allowArgs: true,
|
|
494
|
-
handle:
|
|
693
|
+
handle: handleMcpAcp,
|
|
694
|
+
handleTui: async (command, runtime) => {
|
|
495
695
|
runtime.ctx.editor.addToHistory(command.text);
|
|
496
696
|
runtime.ctx.editor.setText("");
|
|
497
697
|
await runtime.ctx.handleMCPCommand(command.text);
|
|
@@ -500,6 +700,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
500
700
|
{
|
|
501
701
|
name: "ssh",
|
|
502
702
|
description: "Manage SSH hosts (add, list, remove)",
|
|
703
|
+
acpDescription: "Manage SSH connections",
|
|
704
|
+
inlineHint: "<subcommand>",
|
|
503
705
|
subcommands: [
|
|
504
706
|
{
|
|
505
707
|
name: "add",
|
|
@@ -511,7 +713,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
511
713
|
{ name: "help", description: "Show help message" },
|
|
512
714
|
],
|
|
513
715
|
allowArgs: true,
|
|
514
|
-
handle:
|
|
716
|
+
handle: handleSshAcp,
|
|
717
|
+
handleTui: async (command, runtime) => {
|
|
515
718
|
runtime.ctx.editor.addToHistory(command.text);
|
|
516
719
|
runtime.ctx.editor.setText("");
|
|
517
720
|
await runtime.ctx.handleSSHCommand(command.text);
|
|
@@ -520,7 +723,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
520
723
|
{
|
|
521
724
|
name: "new",
|
|
522
725
|
description: "Start a new session",
|
|
523
|
-
|
|
726
|
+
handleTui: async (_command, runtime) => {
|
|
524
727
|
runtime.ctx.editor.setText("");
|
|
525
728
|
await runtime.ctx.handleClearCommand();
|
|
526
729
|
},
|
|
@@ -528,7 +731,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
528
731
|
{
|
|
529
732
|
name: "drop",
|
|
530
733
|
description: "Delete the current session and start a new one",
|
|
531
|
-
|
|
734
|
+
handleTui: async (_command, runtime) => {
|
|
532
735
|
runtime.ctx.editor.setText("");
|
|
533
736
|
await runtime.ctx.handleDropCommand();
|
|
534
737
|
},
|
|
@@ -536,9 +739,31 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
536
739
|
{
|
|
537
740
|
name: "compact",
|
|
538
741
|
description: "Manually compact the session context",
|
|
742
|
+
acpDescription: "Compact the conversation",
|
|
539
743
|
inlineHint: "[focus instructions]",
|
|
540
744
|
allowArgs: true,
|
|
541
745
|
handle: async (command, runtime) => {
|
|
746
|
+
const before = runtime.session.getContextUsage?.();
|
|
747
|
+
const beforeTokens = before?.tokens;
|
|
748
|
+
try {
|
|
749
|
+
await runtime.session.compact(command.args || undefined);
|
|
750
|
+
} catch (err) {
|
|
751
|
+
// Compaction precondition failures (no model, already compacted, too
|
|
752
|
+
// small) and provider errors propagate as plain Errors; surface them
|
|
753
|
+
// via runtime.output so they don't fail the ACP prompt turn.
|
|
754
|
+
return usage(`Compaction failed: ${errorMessage(err)}`, runtime);
|
|
755
|
+
}
|
|
756
|
+
const after = runtime.session.getContextUsage?.();
|
|
757
|
+
const afterTokens = after?.tokens;
|
|
758
|
+
if (beforeTokens != null && afterTokens != null) {
|
|
759
|
+
const saved = beforeTokens - afterTokens;
|
|
760
|
+
await runtime.output(`Compaction complete. Tokens: ${beforeTokens} -> ${afterTokens} (saved ${saved}).`);
|
|
761
|
+
} else {
|
|
762
|
+
await runtime.output("Compaction complete.");
|
|
763
|
+
}
|
|
764
|
+
return commandConsumed();
|
|
765
|
+
},
|
|
766
|
+
handleTui: async (command, runtime) => {
|
|
542
767
|
const customInstructions = command.args || undefined;
|
|
543
768
|
runtime.ctx.editor.setText("");
|
|
544
769
|
await runtime.ctx.handleCompactCommand(customInstructions);
|
|
@@ -549,7 +774,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
549
774
|
description: "Hand off session context to a new session",
|
|
550
775
|
inlineHint: "[focus instructions]",
|
|
551
776
|
allowArgs: true,
|
|
552
|
-
|
|
777
|
+
handleTui: async (command, runtime) => {
|
|
553
778
|
const customInstructions = command.args || undefined;
|
|
554
779
|
runtime.ctx.editor.setText("");
|
|
555
780
|
await runtime.ctx.handleHandoffCommand(customInstructions);
|
|
@@ -558,7 +783,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
558
783
|
{
|
|
559
784
|
name: "resume",
|
|
560
785
|
description: "Resume a different session",
|
|
561
|
-
|
|
786
|
+
handleTui: (_command, runtime) => {
|
|
562
787
|
runtime.ctx.showSessionSelector();
|
|
563
788
|
runtime.ctx.editor.setText("");
|
|
564
789
|
},
|
|
@@ -568,7 +793,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
568
793
|
description: "Ask an ephemeral side question using the current session context",
|
|
569
794
|
inlineHint: "<question>",
|
|
570
795
|
allowArgs: true,
|
|
571
|
-
|
|
796
|
+
handleTui: async (command, runtime) => {
|
|
572
797
|
const question = command.text.slice(`/${command.name}`.length).trim();
|
|
573
798
|
runtime.ctx.editor.setText("");
|
|
574
799
|
await runtime.ctx.handleBtwCommand(question);
|
|
@@ -577,7 +802,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
577
802
|
{
|
|
578
803
|
name: "retry",
|
|
579
804
|
description: "Retry the last failed agent turn",
|
|
580
|
-
|
|
805
|
+
handleTui: async (_command, runtime) => {
|
|
581
806
|
const didRetry = await runtime.ctx.session.retry();
|
|
582
807
|
if (!didRetry) {
|
|
583
808
|
runtime.ctx.showStatus("Nothing to retry");
|
|
@@ -589,7 +814,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
589
814
|
name: "background",
|
|
590
815
|
aliases: ["bg"],
|
|
591
816
|
description: "Detach UI and continue running in background",
|
|
592
|
-
|
|
817
|
+
handleTui: (_command, runtime) => {
|
|
593
818
|
runtime.ctx.editor.setText("");
|
|
594
819
|
runtime.handleBackgroundCommand();
|
|
595
820
|
},
|
|
@@ -597,7 +822,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
597
822
|
{
|
|
598
823
|
name: "debug",
|
|
599
824
|
description: "Open debug tools selector",
|
|
600
|
-
|
|
825
|
+
handleTui: (_command, runtime) => {
|
|
601
826
|
runtime.ctx.showDebugSelector();
|
|
602
827
|
runtime.ctx.editor.setText("");
|
|
603
828
|
},
|
|
@@ -605,6 +830,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
605
830
|
{
|
|
606
831
|
name: "memory",
|
|
607
832
|
description: "Inspect and operate memory maintenance",
|
|
833
|
+
acpDescription: "Manage memory",
|
|
834
|
+
acpInputHint: "<subcommand>",
|
|
608
835
|
subcommands: [
|
|
609
836
|
{ name: "view", description: "Show current memory injection payload" },
|
|
610
837
|
{ name: "clear", description: "Clear persisted memory data and artifacts" },
|
|
@@ -624,6 +851,41 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
624
851
|
],
|
|
625
852
|
allowArgs: true,
|
|
626
853
|
handle: async (command, runtime) => {
|
|
854
|
+
const verb = (command.args.trim().split(/\s+/)[0] ?? "").toLowerCase() || "view";
|
|
855
|
+
const backend = resolveMemoryBackend(runtime.settings);
|
|
856
|
+
switch (verb) {
|
|
857
|
+
case "view": {
|
|
858
|
+
const payload = await backend.buildDeveloperInstructions(
|
|
859
|
+
runtime.settings.getAgentDir(),
|
|
860
|
+
runtime.settings,
|
|
861
|
+
runtime.session,
|
|
862
|
+
);
|
|
863
|
+
await runtime.output(payload || "Memory payload is empty.");
|
|
864
|
+
return commandConsumed();
|
|
865
|
+
}
|
|
866
|
+
case "clear":
|
|
867
|
+
case "reset": {
|
|
868
|
+
await backend.clear(runtime.settings.getAgentDir(), runtime.cwd, runtime.session);
|
|
869
|
+
await runtime.session.refreshBaseSystemPrompt();
|
|
870
|
+
await runtime.output("Memory cleared.");
|
|
871
|
+
return commandConsumed();
|
|
872
|
+
}
|
|
873
|
+
case "enqueue":
|
|
874
|
+
case "rebuild": {
|
|
875
|
+
await backend.enqueue(runtime.settings.getAgentDir(), runtime.cwd, runtime.session);
|
|
876
|
+
await runtime.output("Memory consolidation enqueued.");
|
|
877
|
+
return commandConsumed();
|
|
878
|
+
}
|
|
879
|
+
case "mm":
|
|
880
|
+
return usage(
|
|
881
|
+
"Mental-model maintenance via /memory mm is unsupported in ACP mode; use the hindsight HTTP API directly.",
|
|
882
|
+
runtime,
|
|
883
|
+
);
|
|
884
|
+
default:
|
|
885
|
+
return usage("Usage: /memory <view|clear|reset|enqueue|rebuild>", runtime);
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
handleTui: async (command, runtime) => {
|
|
627
889
|
runtime.ctx.editor.setText("");
|
|
628
890
|
await runtime.ctx.handleMemoryCommand(command.text);
|
|
629
891
|
},
|
|
@@ -634,6 +896,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
634
896
|
inlineHint: "<title>",
|
|
635
897
|
allowArgs: true,
|
|
636
898
|
handle: async (command, runtime) => {
|
|
899
|
+
if (!command.args) return usage("Usage: /rename <title>", runtime);
|
|
900
|
+
const ok = await runtime.sessionManager.setSessionName(command.args, "user");
|
|
901
|
+
if (!ok) {
|
|
902
|
+
await runtime.output("Session name not changed (a user-set name takes precedence).");
|
|
903
|
+
return commandConsumed();
|
|
904
|
+
}
|
|
905
|
+
await runtime.notifyTitleChanged?.();
|
|
906
|
+
await runtime.output(`Session renamed to ${command.args}.`);
|
|
907
|
+
return commandConsumed();
|
|
908
|
+
},
|
|
909
|
+
handleTui: async (command, runtime) => {
|
|
637
910
|
const title = command.args.trim();
|
|
638
911
|
if (!title) {
|
|
639
912
|
runtime.ctx.showError("Usage: /rename <title>");
|
|
@@ -644,13 +917,38 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
644
917
|
await runtime.ctx.handleRenameCommand(title);
|
|
645
918
|
},
|
|
646
919
|
},
|
|
647
|
-
|
|
648
920
|
{
|
|
649
921
|
name: "move",
|
|
650
922
|
description: "Move session to a different working directory",
|
|
923
|
+
acpDescription: "Move the current session file",
|
|
651
924
|
inlineHint: "<path>",
|
|
652
925
|
allowArgs: true,
|
|
653
926
|
handle: async (command, runtime) => {
|
|
927
|
+
if (runtime.session.isStreaming) return usage("Cannot move while streaming.", runtime);
|
|
928
|
+
if (!command.args) return usage("Usage: /move <path>", runtime);
|
|
929
|
+
const resolvedPath = path.resolve(runtime.cwd, command.args);
|
|
930
|
+
let isDirectory: boolean;
|
|
931
|
+
try {
|
|
932
|
+
isDirectory = (await fs.stat(resolvedPath)).isDirectory();
|
|
933
|
+
} catch {
|
|
934
|
+
return usage(`Directory does not exist or is not a directory: ${resolvedPath}`, runtime);
|
|
935
|
+
}
|
|
936
|
+
if (!isDirectory) return usage(`Directory does not exist or is not a directory: ${resolvedPath}`, runtime);
|
|
937
|
+
try {
|
|
938
|
+
await runtime.sessionManager.flush();
|
|
939
|
+
await runtime.sessionManager.moveTo(resolvedPath);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
return usage(`Move failed: ${errorMessage(err)}`, runtime);
|
|
942
|
+
}
|
|
943
|
+
setProjectDir(resolvedPath);
|
|
944
|
+
// Reload plugin/capability caches so the next prompt sees commands and
|
|
945
|
+
// capabilities scoped to the new cwd.
|
|
946
|
+
await runtime.reloadPlugins();
|
|
947
|
+
await runtime.notifyTitleChanged?.();
|
|
948
|
+
await runtime.output(`Session moved to ${runtime.sessionManager.getCwd()}.`);
|
|
949
|
+
return commandConsumed();
|
|
950
|
+
},
|
|
951
|
+
handleTui: async (command, runtime) => {
|
|
654
952
|
const targetPath = command.args;
|
|
655
953
|
if (!targetPath) {
|
|
656
954
|
runtime.ctx.showError("Usage: /move <path>");
|
|
@@ -664,11 +962,13 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
664
962
|
{
|
|
665
963
|
name: "exit",
|
|
666
964
|
description: "Exit the application",
|
|
667
|
-
|
|
965
|
+
handleTui: shutdownHandlerTui,
|
|
668
966
|
},
|
|
669
967
|
{
|
|
670
968
|
name: "marketplace",
|
|
671
969
|
description: "Manage marketplace plugin sources and installed plugins",
|
|
970
|
+
acpDescription: "Manage plugins from marketplaces",
|
|
971
|
+
acpInputHint: "<subcommand>",
|
|
672
972
|
subcommands: [
|
|
673
973
|
{ name: "add", description: "Add a marketplace source", usage: "<source>" },
|
|
674
974
|
{ name: "remove", description: "Remove a marketplace source", usage: "<name>" },
|
|
@@ -687,6 +987,175 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
687
987
|
],
|
|
688
988
|
allowArgs: true,
|
|
689
989
|
handle: async (command, runtime) => {
|
|
990
|
+
const { verb, rest } = parseSubcommand(command.args);
|
|
991
|
+
if (!verb) {
|
|
992
|
+
try {
|
|
993
|
+
const manager = await createMarketplaceManager(runtime);
|
|
994
|
+
const marketplaces = await manager.listMarketplaces();
|
|
995
|
+
if (marketplaces.length === 0) {
|
|
996
|
+
await runtime.output(
|
|
997
|
+
"No marketplaces configured.\n\nGet started:\n /marketplace add anthropics/claude-plugins-official\n\nThen browse with /marketplace discover",
|
|
998
|
+
);
|
|
999
|
+
} else {
|
|
1000
|
+
const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`);
|
|
1001
|
+
await runtime.output(
|
|
1002
|
+
`Marketplaces:\n${lines.join("\n")}\n\nUse /marketplace discover to browse plugins, or /marketplace help for all commands`,
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
return commandConsumed();
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
return usage(`Marketplace error: ${errorMessage(err)}`, runtime);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (verb === "help") {
|
|
1011
|
+
await runtime.output(
|
|
1012
|
+
[
|
|
1013
|
+
"Marketplace commands:",
|
|
1014
|
+
" /marketplace List configured marketplaces",
|
|
1015
|
+
" /marketplace add <source> Add a marketplace (e.g. owner/repo)",
|
|
1016
|
+
" /marketplace remove <name> Remove a marketplace",
|
|
1017
|
+
" /marketplace update [name] Re-fetch catalog(s)",
|
|
1018
|
+
" /marketplace list List configured marketplaces",
|
|
1019
|
+
" /marketplace discover [marketplace] Browse available plugins",
|
|
1020
|
+
" /marketplace install <name@marketplace> Install a plugin",
|
|
1021
|
+
" /marketplace uninstall <name@marketplace> Uninstall a plugin",
|
|
1022
|
+
" /marketplace installed List installed plugins",
|
|
1023
|
+
" /marketplace upgrade [name@marketplace] Upgrade plugin(s)",
|
|
1024
|
+
"",
|
|
1025
|
+
"Quick start:",
|
|
1026
|
+
" /marketplace add anthropics/claude-plugins-official",
|
|
1027
|
+
].join("\n"),
|
|
1028
|
+
);
|
|
1029
|
+
return commandConsumed();
|
|
1030
|
+
}
|
|
1031
|
+
if ((verb === "install" || verb === "uninstall") && !rest) {
|
|
1032
|
+
return usage(
|
|
1033
|
+
"Interactive plugin pickers are TUI-only. Pass an explicit name@marketplace argument.",
|
|
1034
|
+
runtime,
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
const manager = await createMarketplaceManager(runtime);
|
|
1039
|
+
switch (verb) {
|
|
1040
|
+
case "add": {
|
|
1041
|
+
if (!rest) return usage("Usage: /marketplace add <source>", runtime);
|
|
1042
|
+
const entry = await manager.addMarketplace(rest);
|
|
1043
|
+
await runtime.output(`Added marketplace: ${entry.name}`);
|
|
1044
|
+
return commandConsumed();
|
|
1045
|
+
}
|
|
1046
|
+
case "remove":
|
|
1047
|
+
case "rm": {
|
|
1048
|
+
if (!rest) return usage("Usage: /marketplace remove <name>", runtime);
|
|
1049
|
+
await manager.removeMarketplace(rest);
|
|
1050
|
+
await runtime.output(`Removed marketplace: ${rest}`);
|
|
1051
|
+
return commandConsumed();
|
|
1052
|
+
}
|
|
1053
|
+
case "update": {
|
|
1054
|
+
if (rest) {
|
|
1055
|
+
await manager.updateMarketplace(rest);
|
|
1056
|
+
await runtime.output(`Updated marketplace: ${rest}`);
|
|
1057
|
+
} else {
|
|
1058
|
+
const results = await manager.updateAllMarketplaces();
|
|
1059
|
+
await runtime.output(`Updated ${results.length} marketplace(s)`);
|
|
1060
|
+
}
|
|
1061
|
+
return commandConsumed();
|
|
1062
|
+
}
|
|
1063
|
+
case "list": {
|
|
1064
|
+
const marketplaces = await manager.listMarketplaces();
|
|
1065
|
+
if (marketplaces.length === 0) {
|
|
1066
|
+
await runtime.output("No marketplaces configured.");
|
|
1067
|
+
} else {
|
|
1068
|
+
const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`);
|
|
1069
|
+
await runtime.output(`Marketplaces:\n${lines.join("\n")}`);
|
|
1070
|
+
}
|
|
1071
|
+
return commandConsumed();
|
|
1072
|
+
}
|
|
1073
|
+
case "discover": {
|
|
1074
|
+
const plugins = await manager.listAvailablePlugins(rest || undefined);
|
|
1075
|
+
if (plugins.length === 0) {
|
|
1076
|
+
const marketplaces = await manager.listMarketplaces();
|
|
1077
|
+
await runtime.output(
|
|
1078
|
+
marketplaces.length === 0
|
|
1079
|
+
? "No marketplaces configured. Try:\n /marketplace add anthropics/claude-plugins-official"
|
|
1080
|
+
: "No plugins available in configured marketplaces",
|
|
1081
|
+
);
|
|
1082
|
+
return commandConsumed();
|
|
1083
|
+
}
|
|
1084
|
+
const lines = ["Available plugins:"];
|
|
1085
|
+
for (const plugin of plugins) {
|
|
1086
|
+
lines.push(` - ${plugin.name}${plugin.version ? `@${plugin.version}` : ""}`);
|
|
1087
|
+
if (plugin.description) lines.push(` ${plugin.description}`);
|
|
1088
|
+
}
|
|
1089
|
+
await runtime.output(lines.join("\n"));
|
|
1090
|
+
return commandConsumed();
|
|
1091
|
+
}
|
|
1092
|
+
case "install": {
|
|
1093
|
+
const parsed = parseMarketplaceInstallArgs(rest);
|
|
1094
|
+
if ("error" in parsed) return usage(parsed.error, runtime);
|
|
1095
|
+
const atIndex = parsed.installSpec.lastIndexOf("@");
|
|
1096
|
+
const pluginName = parsed.installSpec.slice(0, atIndex);
|
|
1097
|
+
const marketplace = parsed.installSpec.slice(atIndex + 1);
|
|
1098
|
+
await manager.installPlugin(pluginName, marketplace, { force: parsed.force, scope: parsed.scope });
|
|
1099
|
+
await runtime.reloadPlugins();
|
|
1100
|
+
await runtime.output(`Installed ${pluginName} from ${marketplace}`);
|
|
1101
|
+
return commandConsumed();
|
|
1102
|
+
}
|
|
1103
|
+
case "uninstall": {
|
|
1104
|
+
const parsed = parsePluginScopeArgs(
|
|
1105
|
+
rest,
|
|
1106
|
+
"Usage: /marketplace uninstall [--scope user|project] <name@marketplace>",
|
|
1107
|
+
);
|
|
1108
|
+
if ("error" in parsed) return usage(parsed.error, runtime);
|
|
1109
|
+
await manager.uninstallPlugin(parsed.pluginId, parsed.scope);
|
|
1110
|
+
await runtime.reloadPlugins();
|
|
1111
|
+
await runtime.output(`Uninstalled ${parsed.pluginId}`);
|
|
1112
|
+
return commandConsumed();
|
|
1113
|
+
}
|
|
1114
|
+
case "installed": {
|
|
1115
|
+
const installed = await manager.listInstalledPlugins();
|
|
1116
|
+
if (installed.length === 0) {
|
|
1117
|
+
await runtime.output("No marketplace plugins installed");
|
|
1118
|
+
} else {
|
|
1119
|
+
const lines = installed.map(
|
|
1120
|
+
p => ` ${p.id} [${p.scope}]${p.shadowedBy ? " [shadowed]" : ""} (${p.entries.length} entry)`,
|
|
1121
|
+
);
|
|
1122
|
+
await runtime.output(`Installed plugins:\n${lines.join("\n")}`);
|
|
1123
|
+
}
|
|
1124
|
+
return commandConsumed();
|
|
1125
|
+
}
|
|
1126
|
+
case "upgrade": {
|
|
1127
|
+
if (rest) {
|
|
1128
|
+
const parsed = parsePluginScopeArgs(
|
|
1129
|
+
rest,
|
|
1130
|
+
"Usage: /marketplace upgrade [--scope user|project] <name@marketplace>",
|
|
1131
|
+
);
|
|
1132
|
+
if ("error" in parsed) return usage(parsed.error, runtime);
|
|
1133
|
+
const result = await manager.upgradePlugin(parsed.pluginId, parsed.scope);
|
|
1134
|
+
await runtime.reloadPlugins();
|
|
1135
|
+
await runtime.output(`Upgraded ${parsed.pluginId} to ${result.version}`);
|
|
1136
|
+
return commandConsumed();
|
|
1137
|
+
}
|
|
1138
|
+
const results = await manager.upgradeAllPlugins();
|
|
1139
|
+
if (results.length === 0) {
|
|
1140
|
+
await runtime.output("All marketplace plugins are up to date");
|
|
1141
|
+
} else {
|
|
1142
|
+
await runtime.reloadPlugins();
|
|
1143
|
+
const lines = results.map(r => ` ${r.pluginId}: ${r.from} -> ${r.to}`);
|
|
1144
|
+
await runtime.output(`Upgraded ${results.length} plugin(s):\n${lines.join("\n")}`);
|
|
1145
|
+
}
|
|
1146
|
+
return commandConsumed();
|
|
1147
|
+
}
|
|
1148
|
+
default:
|
|
1149
|
+
return usage(
|
|
1150
|
+
`Unknown /marketplace subcommand: ${verb}. Use /marketplace help for available commands.`,
|
|
1151
|
+
runtime,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
return usage(`Marketplace error: ${errorMessage(err)}`, runtime);
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
handleTui: async (command, runtime) => {
|
|
690
1159
|
runtime.ctx.editor.setText("");
|
|
691
1160
|
const args = command.args.trim().split(/\s+/);
|
|
692
1161
|
const sub = args[0] || "install";
|
|
@@ -877,6 +1346,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
877
1346
|
{
|
|
878
1347
|
name: "plugins",
|
|
879
1348
|
description: "View and manage installed plugins",
|
|
1349
|
+
acpDescription: "Manage plugins",
|
|
1350
|
+
acpInputHint: "[list|enable|disable]",
|
|
880
1351
|
subcommands: [
|
|
881
1352
|
{ name: "list", description: "List all installed plugins (npm + marketplace)" },
|
|
882
1353
|
{ name: "enable", description: "Enable a marketplace plugin", usage: "<name@marketplace>" },
|
|
@@ -884,6 +1355,53 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
884
1355
|
],
|
|
885
1356
|
allowArgs: true,
|
|
886
1357
|
handle: async (command, runtime) => {
|
|
1358
|
+
const { verb, rest } = parseSubcommand(command.args);
|
|
1359
|
+
try {
|
|
1360
|
+
if (verb === "enable" || verb === "disable") {
|
|
1361
|
+
const parsed = parsePluginScopeArgs(
|
|
1362
|
+
rest,
|
|
1363
|
+
`Usage: /plugins ${verb} [--scope user|project] <name@marketplace>`,
|
|
1364
|
+
);
|
|
1365
|
+
if ("error" in parsed) return usage(parsed.error, runtime);
|
|
1366
|
+
const manager = await createMarketplaceManager(runtime);
|
|
1367
|
+
const isEnable = verb === "enable";
|
|
1368
|
+
await manager.setPluginEnabled(parsed.pluginId, isEnable, parsed.scope);
|
|
1369
|
+
await runtime.reloadPlugins();
|
|
1370
|
+
await runtime.output(`${isEnable ? "Enabled" : "Disabled"} ${parsed.pluginId}`);
|
|
1371
|
+
return commandConsumed();
|
|
1372
|
+
}
|
|
1373
|
+
// Default: list
|
|
1374
|
+
const lines: string[] = [];
|
|
1375
|
+
const npmManager = new PluginManager();
|
|
1376
|
+
const npmPlugins = await npmManager.list();
|
|
1377
|
+
if (npmPlugins.length > 0) {
|
|
1378
|
+
lines.push("npm plugins:");
|
|
1379
|
+
for (const plugin of npmPlugins) {
|
|
1380
|
+
const status = plugin.enabled === false ? " (disabled)" : "";
|
|
1381
|
+
lines.push(` ${plugin.name}@${plugin.version}${status}`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const marketplaceManager = await createMarketplaceManager(runtime);
|
|
1386
|
+
const marketplacePlugins = await marketplaceManager.listInstalledPlugins();
|
|
1387
|
+
if (marketplacePlugins.length > 0) {
|
|
1388
|
+
if (lines.length > 0) lines.push("");
|
|
1389
|
+
lines.push("marketplace plugins:");
|
|
1390
|
+
for (const plugin of marketplacePlugins) {
|
|
1391
|
+
const entry = plugin.entries[0];
|
|
1392
|
+
const status = entry?.enabled === false ? " (disabled)" : "";
|
|
1393
|
+
const shadowed = plugin.shadowedBy ? " [shadowed]" : "";
|
|
1394
|
+
lines.push(` ${plugin.id} v${entry?.version ?? "?"}${status} [${plugin.scope}]${shadowed}`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
await runtime.output(lines.length === 0 ? "No plugins installed" : lines.join("\n"));
|
|
1399
|
+
return commandConsumed();
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
return usage(`Plugin error: ${errorMessage(err)}`, runtime);
|
|
1402
|
+
}
|
|
1403
|
+
},
|
|
1404
|
+
handleTui: async (command, runtime) => {
|
|
887
1405
|
runtime.ctx.editor.setText("");
|
|
888
1406
|
const args = command.args.trim().split(/\s+/);
|
|
889
1407
|
const sub = args[0] || "list";
|
|
@@ -958,7 +1476,13 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
958
1476
|
{
|
|
959
1477
|
name: "reload-plugins",
|
|
960
1478
|
description: "Reload all plugins (skills, commands, hooks, tools, agents, MCP)",
|
|
1479
|
+
acpDescription: "Reload all plugins",
|
|
961
1480
|
handle: async (_command, runtime) => {
|
|
1481
|
+
await runtime.reloadPlugins();
|
|
1482
|
+
await runtime.output("Plugins reloaded.");
|
|
1483
|
+
return commandConsumed();
|
|
1484
|
+
},
|
|
1485
|
+
handleTui: async (_command, runtime) => {
|
|
962
1486
|
// Invalidate registry fs caches and the plugin roots cache so
|
|
963
1487
|
// listClaudePluginRoots re-reads from disk on next access.
|
|
964
1488
|
const projectPath = await resolveActiveProjectRegistryPath(runtime.ctx.sessionManager.getCwd());
|
|
@@ -971,9 +1495,23 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
971
1495
|
{
|
|
972
1496
|
name: "force",
|
|
973
1497
|
description: "Force next turn to use a specific tool",
|
|
1498
|
+
aliases: ["force:"],
|
|
974
1499
|
inlineHint: "<tool-name> [prompt]",
|
|
975
1500
|
allowArgs: true,
|
|
976
|
-
handle: (command, runtime) => {
|
|
1501
|
+
handle: async (command, runtime) => {
|
|
1502
|
+
const spaceIdx = command.args.indexOf(" ");
|
|
1503
|
+
const toolName = spaceIdx === -1 ? command.args : command.args.slice(0, spaceIdx);
|
|
1504
|
+
const prompt = spaceIdx === -1 ? "" : command.args.slice(spaceIdx + 1).trim();
|
|
1505
|
+
if (!toolName) return usage("Usage: /force:<tool-name> [prompt]", runtime);
|
|
1506
|
+
try {
|
|
1507
|
+
runtime.session.setForcedToolChoice(toolName);
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
return usage(errorMessage(err), runtime);
|
|
1510
|
+
}
|
|
1511
|
+
await runtime.output(`Next turn forced to use ${toolName}.`);
|
|
1512
|
+
return prompt ? { prompt } : commandConsumed();
|
|
1513
|
+
},
|
|
1514
|
+
handleTui: (command, runtime) => {
|
|
977
1515
|
const spaceIdx = command.args.indexOf(" ");
|
|
978
1516
|
const toolName = spaceIdx === -1 ? command.args : command.args.slice(0, spaceIdx);
|
|
979
1517
|
const prompt = spaceIdx === -1 ? "" : command.args.slice(spaceIdx + 1).trim();
|
|
@@ -988,7 +1526,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
988
1526
|
runtime.ctx.session.setForcedToolChoice(toolName);
|
|
989
1527
|
runtime.ctx.showStatus(`Next turn forced to use ${toolName}.`);
|
|
990
1528
|
} catch (error) {
|
|
991
|
-
runtime.ctx.showError(
|
|
1529
|
+
runtime.ctx.showError(errorMessage(error));
|
|
992
1530
|
runtime.ctx.editor.setText("");
|
|
993
1531
|
return;
|
|
994
1532
|
}
|
|
@@ -996,17 +1534,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
996
1534
|
runtime.ctx.editor.setText("");
|
|
997
1535
|
|
|
998
1536
|
// If a prompt was provided, pass it through as input
|
|
999
|
-
if (prompt) return prompt;
|
|
1537
|
+
if (prompt) return { prompt };
|
|
1000
1538
|
},
|
|
1001
1539
|
},
|
|
1002
1540
|
{
|
|
1003
1541
|
name: "quit",
|
|
1004
1542
|
description: "Quit the application",
|
|
1005
|
-
|
|
1543
|
+
handleTui: shutdownHandlerTui,
|
|
1006
1544
|
},
|
|
1007
1545
|
];
|
|
1008
1546
|
|
|
1009
|
-
const BUILTIN_SLASH_COMMAND_LOOKUP = new Map<string,
|
|
1547
|
+
const BUILTIN_SLASH_COMMAND_LOOKUP = new Map<string, SlashCommandSpec>();
|
|
1010
1548
|
for (const command of BUILTIN_SLASH_COMMAND_REGISTRY) {
|
|
1011
1549
|
BUILTIN_SLASH_COMMAND_LOOKUP.set(command.name, command);
|
|
1012
1550
|
for (const alias of command.aliases ?? []) {
|
|
@@ -1025,17 +1563,24 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
|
|
|
1025
1563
|
);
|
|
1026
1564
|
|
|
1027
1565
|
/**
|
|
1028
|
-
*
|
|
1566
|
+
* Unified registry exposed for cross-mode tooling. Each spec carries at least
|
|
1567
|
+
* one of `handle` / `handleTui`. The TUI dispatcher prefers `handleTui`; the
|
|
1568
|
+
* ACP dispatcher requires `handle` and skips TUI-only entries.
|
|
1569
|
+
*/
|
|
1570
|
+
export const BUILTIN_SLASH_COMMANDS_INTERNAL: ReadonlyArray<SlashCommandSpec> = BUILTIN_SLASH_COMMAND_REGISTRY;
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Execute a builtin slash command in the interactive TUI.
|
|
1029
1574
|
*
|
|
1030
|
-
* Returns `false` when no builtin matched. Returns `true` when a command
|
|
1031
|
-
* the input entirely. Returns a `string` when the command was handled
|
|
1032
|
-
* text should be sent as a prompt.
|
|
1575
|
+
* Returns `false` when no builtin matched. Returns `true` when a command
|
|
1576
|
+
* consumed the input entirely. Returns a `string` when the command was handled
|
|
1577
|
+
* but remaining text should be sent as a prompt.
|
|
1033
1578
|
*/
|
|
1034
1579
|
export async function executeBuiltinSlashCommand(
|
|
1035
1580
|
text: string,
|
|
1036
1581
|
runtime: BuiltinSlashCommandRuntime,
|
|
1037
1582
|
): Promise<string | boolean> {
|
|
1038
|
-
const parsed =
|
|
1583
|
+
const parsed = parseSlashCommand(text);
|
|
1039
1584
|
if (!parsed) return false;
|
|
1040
1585
|
|
|
1041
1586
|
const command = BUILTIN_SLASH_COMMAND_LOOKUP.get(parsed.name);
|
|
@@ -1043,7 +1588,45 @@ export async function executeBuiltinSlashCommand(
|
|
|
1043
1588
|
if (parsed.args.length > 0 && !command.allowArgs) {
|
|
1044
1589
|
return false;
|
|
1045
1590
|
}
|
|
1591
|
+
if (command.handleTui) {
|
|
1592
|
+
const result = await command.handleTui(parsed, runtime);
|
|
1593
|
+
if (result && typeof result === "object" && "prompt" in result) return result.prompt;
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
1596
|
+
if (command.handle) {
|
|
1597
|
+
// No TUI-specific override → adapt the ACP/text-mode `handle` to the
|
|
1598
|
+
// TUI by routing `runtime.output` through `ctx.showStatus`, clearing
|
|
1599
|
+
// the editor after the call, and reusing the active session's plugin
|
|
1600
|
+
// reload pipeline. Spec authors get a single body usable from either
|
|
1601
|
+
// dispatcher without forcing every TUI test to construct the full
|
|
1602
|
+
// `SlashCommandRuntime` shape.
|
|
1603
|
+
const ctx = runtime.ctx;
|
|
1604
|
+
const adapted: SlashCommandRuntime = {
|
|
1605
|
+
session: ctx.session,
|
|
1606
|
+
sessionManager: ctx.sessionManager,
|
|
1607
|
+
settings: ctx.settings,
|
|
1608
|
+
cwd: ctx.sessionManager.getCwd(),
|
|
1609
|
+
output: (text: string) => {
|
|
1610
|
+
ctx.showStatus(text);
|
|
1611
|
+
},
|
|
1612
|
+
refreshCommands: () => ctx.refreshSlashCommandState(),
|
|
1613
|
+
reloadPlugins: async () => {
|
|
1614
|
+
const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
|
|
1615
|
+
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
1616
|
+
await ctx.refreshSlashCommandState();
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
const result = await command.handle(parsed, adapted);
|
|
1620
|
+
ctx.editor.setText("");
|
|
1621
|
+
if (result && typeof result === "object" && "prompt" in result) return result.prompt;
|
|
1622
|
+
return true;
|
|
1623
|
+
}
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1046
1626
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1627
|
+
/** Look up a unified spec by name or alias. Used by the ACP dispatcher. */
|
|
1628
|
+
export function lookupBuiltinSlashCommand(name: string): SlashCommandSpec | undefined {
|
|
1629
|
+
return BUILTIN_SLASH_COMMAND_LOOKUP.get(name);
|
|
1049
1630
|
}
|
|
1631
|
+
|
|
1632
|
+
export type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime, SlashCommandSpec, TuiSlashCommandRuntime };
|