@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.
- package/CHANGELOG.md +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +11 -29
- 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/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- 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 +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- 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/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- 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/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- 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 +25 -24
- 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 +3 -3
- 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/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- 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 +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -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 +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- 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/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -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/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- 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
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
337
|
+
} else if (arg === "headless" || arg === "hidden") {
|
|
226
338
|
next = true;
|
|
227
|
-
} else if (
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1561
|
+
handleTui: shutdownHandlerTui,
|
|
1006
1562
|
},
|
|
1007
1563
|
];
|
|
1008
1564
|
|
|
1009
|
-
const BUILTIN_SLASH_COMMAND_LOOKUP = new Map<string,
|
|
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
|
-
*
|
|
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
|
|
1031
|
-
* the input entirely. Returns a `string` when the command was handled
|
|
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 =
|
|
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
|
-
|
|
1048
|
-
|
|
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 };
|