8cf3dce6fb
- builtin-registry.ts held a 2341-line array of every command spec with its handler inlined, plus the autocomplete builders and the TUI dispatcher. - Specs moved verbatim into modes, collaboration, session, lifecycle, marketplace and control modules; completions moved to builtin-completions. - builtin-registry.ts is now a 174-line composition/dispatch module and keeps all 16 exports. Command order is unchanged, which matters because it drives autocomplete ordering and BUILTIN_SLASH_COMMAND_DEFS.
175 lines
6.8 KiB
TypeScript
175 lines
6.8 KiB
TypeScript
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
|
|
import { COLLAB_GUEST_ALLOWED_COMMANDS } from "../collab/guest";
|
|
import { BUILTIN_COLLABORATION_SLASH_COMMANDS } from "./builtin-collaboration";
|
|
import {
|
|
buildArgumentCompletions,
|
|
buildDirectoryArgumentCompletions,
|
|
buildMcpArgumentCompletions,
|
|
buildStaticInlineHint,
|
|
buildSubcommandInlineHint,
|
|
} from "./builtin-completions";
|
|
import { BUILTIN_CONTROL_SLASH_COMMANDS } from "./builtin-control";
|
|
import { BUILTIN_LIFECYCLE_SLASH_COMMANDS } from "./builtin-lifecycle";
|
|
import { BUILTIN_MARKETPLACE_SLASH_COMMANDS, reloadTuiPluginState } from "./builtin-marketplace";
|
|
import { BUILTIN_MODE_SLASH_COMMANDS } from "./builtin-modes";
|
|
import { BUILTIN_SESSION_SLASH_COMMANDS } from "./builtin-session";
|
|
import { parseSlashCommand } from "./helpers/parse";
|
|
import type {
|
|
BuiltinSlashCommand,
|
|
ParsedSlashCommand,
|
|
SlashCommandResult,
|
|
SlashCommandRuntime,
|
|
SlashCommandSpec,
|
|
TuiSlashCommandRuntime,
|
|
} from "./types";
|
|
|
|
export type { BuiltinSlashCommand, SubcommandDef } from "./types";
|
|
|
|
/** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
|
|
export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
|
|
|
|
export interface TuiBuiltinSlashCommand extends BuiltinSlashCommand {
|
|
getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>;
|
|
getInlineHint?: (argumentText: string) => string | null;
|
|
getAutocompleteDescription?: () => string | undefined;
|
|
}
|
|
|
|
const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
...BUILTIN_MODE_SLASH_COMMANDS,
|
|
...BUILTIN_COLLABORATION_SLASH_COMMANDS,
|
|
...BUILTIN_SESSION_SLASH_COMMANDS,
|
|
...BUILTIN_LIFECYCLE_SLASH_COMMANDS,
|
|
...BUILTIN_MARKETPLACE_SLASH_COMMANDS,
|
|
...BUILTIN_CONTROL_SLASH_COMMANDS,
|
|
];
|
|
|
|
const BUILTIN_SLASH_COMMAND_LOOKUP = new Map<string, SlashCommandSpec>();
|
|
for (const command of BUILTIN_SLASH_COMMAND_REGISTRY) {
|
|
BUILTIN_SLASH_COMMAND_LOOKUP.set(command.name, command);
|
|
for (const alias of command.aliases ?? []) {
|
|
BUILTIN_SLASH_COMMAND_LOOKUP.set(alias, command);
|
|
}
|
|
}
|
|
|
|
export const BUILTIN_SLASH_COMMAND_RESERVED_NAMES: ReadonlySet<string> = new Set(BUILTIN_SLASH_COMMAND_LOOKUP.keys());
|
|
|
|
/** Builtin command metadata used for slash-command autocomplete and help text. */
|
|
export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BUILTIN_SLASH_COMMAND_REGISTRY.map(
|
|
command => ({
|
|
name: command.name,
|
|
aliases: command.aliases,
|
|
allowArgs: command.allowArgs === true,
|
|
description: command.description,
|
|
subcommands: command.subcommands,
|
|
inlineHint: command.inlineHint,
|
|
getTuiAutocompleteDescription: command.getTuiAutocompleteDescription,
|
|
}),
|
|
);
|
|
|
|
function materializeTuiBuiltinSlashCommand(
|
|
cmd: BuiltinSlashCommand,
|
|
runtime?: TuiSlashCommandRuntime,
|
|
): TuiBuiltinSlashCommand {
|
|
const materialized: TuiBuiltinSlashCommand = { ...cmd };
|
|
if (cmd.subcommands) {
|
|
materialized.getArgumentCompletions =
|
|
cmd.name === "mcp" && runtime
|
|
? buildMcpArgumentCompletions(cmd.subcommands, runtime)
|
|
: buildArgumentCompletions(cmd.subcommands);
|
|
materialized.getInlineHint = buildSubcommandInlineHint(cmd.subcommands);
|
|
} else if (cmd.name === "move") {
|
|
materialized.getArgumentCompletions = buildDirectoryArgumentCompletions();
|
|
if (cmd.inlineHint) materialized.getInlineHint = buildStaticInlineHint(cmd.inlineHint);
|
|
} else if (cmd.inlineHint) {
|
|
materialized.getInlineHint = buildStaticInlineHint(cmd.inlineHint);
|
|
}
|
|
if (runtime && cmd.getTuiAutocompleteDescription) {
|
|
materialized.getAutocompleteDescription = () => cmd.getTuiAutocompleteDescription?.(runtime);
|
|
}
|
|
return materialized;
|
|
}
|
|
|
|
/**
|
|
* Materialized builtin slash commands with completion functions derived from
|
|
* declarative subcommand/hint definitions.
|
|
*/
|
|
export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<TuiBuiltinSlashCommand> = BUILTIN_SLASH_COMMAND_DEFS.map(cmd =>
|
|
materializeTuiBuiltinSlashCommand(cmd),
|
|
);
|
|
|
|
export function buildTuiBuiltinSlashCommands(runtime: TuiSlashCommandRuntime): ReadonlyArray<TuiBuiltinSlashCommand> {
|
|
return BUILTIN_SLASH_COMMAND_DEFS.map(cmd => materializeTuiBuiltinSlashCommand(cmd, runtime));
|
|
}
|
|
|
|
/**
|
|
* Unified registry exposed for cross-mode tooling. Each spec carries at least
|
|
* one of `handle` / `handleTui`. The TUI dispatcher prefers `handleTui`; the
|
|
* ACP dispatcher requires `handle` and skips TUI-only entries.
|
|
*/
|
|
export const BUILTIN_SLASH_COMMANDS_INTERNAL: ReadonlyArray<SlashCommandSpec> = BUILTIN_SLASH_COMMAND_REGISTRY;
|
|
|
|
/**
|
|
* Execute a builtin slash command in the interactive TUI.
|
|
*
|
|
* Returns `false` when no builtin matched. Returns `true` when a command
|
|
* consumed the input entirely. Returns a `string` when the command was handled
|
|
* but remaining text should be sent as a prompt.
|
|
*/
|
|
export async function executeBuiltinSlashCommand(
|
|
text: string,
|
|
runtime: BuiltinSlashCommandRuntime,
|
|
): Promise<string | boolean> {
|
|
const parsed = parseSlashCommand(text);
|
|
if (!parsed) return false;
|
|
|
|
const command = BUILTIN_SLASH_COMMAND_LOOKUP.get(parsed.name);
|
|
if (!command) return false;
|
|
if (parsed.args.length > 0 && !command.allowArgs) {
|
|
return false;
|
|
}
|
|
// Collab guests run a read-mostly replica: session-mutating builtins are
|
|
// host-only; the allowlist covers purely local/read-only commands.
|
|
if (runtime.ctx.collabGuest && !COLLAB_GUEST_ALLOWED_COMMANDS[command.name]) {
|
|
runtime.ctx.showStatus(`/${command.name} is host-only during a collab session`);
|
|
runtime.ctx.editor.setText("");
|
|
return true;
|
|
}
|
|
if (command.handleTui) {
|
|
const result = await command.handleTui(parsed, runtime);
|
|
if (result && typeof result === "object" && "prompt" in result) return result.prompt;
|
|
return true;
|
|
}
|
|
if (command.handle) {
|
|
// No TUI-specific override → adapt the ACP/text-mode `handle` to the
|
|
// TUI by routing `runtime.output` through `ctx.showStatus`, clearing
|
|
// the editor after the call, and reusing the active session's plugin
|
|
// reload pipeline. Spec authors get a single body usable from either
|
|
// dispatcher without forcing every TUI test to construct the full
|
|
// `SlashCommandRuntime` shape.
|
|
const ctx = runtime.ctx;
|
|
const adapted: SlashCommandRuntime = {
|
|
session: ctx.session,
|
|
sessionManager: ctx.sessionManager,
|
|
settings: ctx.settings,
|
|
cwd: ctx.sessionManager.getCwd(),
|
|
output: (text: string) => {
|
|
ctx.showStatus(text);
|
|
},
|
|
refreshCommands: () => ctx.refreshSlashCommandState(),
|
|
reloadPlugins: () => reloadTuiPluginState(ctx),
|
|
};
|
|
const result = await command.handle(parsed, adapted);
|
|
ctx.editor.setText("");
|
|
if (result && typeof result === "object" && "prompt" in result) return result.prompt;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Look up a unified spec by name or alias. Used by the ACP dispatcher. */
|
|
export function lookupBuiltinSlashCommand(name: string): SlashCommandSpec | undefined {
|
|
return BUILTIN_SLASH_COMMAND_LOOKUP.get(name);
|
|
}
|
|
|
|
export type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime, SlashCommandSpec, TuiSlashCommandRuntime };
|