Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
12 kB · 341 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342import { APP_NAME } from "../config.js";import type { SourceInfo } from "./source-info.js";
export type SlashCommandSource = "extension" | "prompt" | "skill";
export interface SlashCommandInfo { name: string; description?: string; source: SlashCommandSource; sourceInfo: SourceInfo;}
export const SESSION_SLASH_COMMAND_NAMES = ["compact", "refine", "goal", "autonomous"] as const;
export type SessionSlashCommandName = (typeof SESSION_SLASH_COMMAND_NAMES)[number];
const SESSION_SLASH_COMMAND_NAME_SET: ReadonlySet<string> = new Set(SESSION_SLASH_COMMAND_NAMES);
export function isSessionSlashCommandName(value: unknown): value is SessionSlashCommandName { return typeof value === "string" && SESSION_SLASH_COMMAND_NAME_SET.has(value);}
export interface SessionSlashCommand { name: SessionSlashCommandName; args: string; text: string;}
export interface RefineCommandOptions { instructions?: string; rollbackId?: string; global?: boolean;}
export function parseRefineCommandOptions(args: string): RefineCommandOptions { let rest = args.trim(); let global = false; if (/^--global(?=\s|$)/.test(rest)) { global = true; rest = rest.replace(/^--global(?=\s|$)/, "").trim(); } if (rest === "rollback") throw new Error("Usage: /refine rollback <refinement-id>"); // Slash-command args keep their original separators (tabs, Unicode spaces); // match the subcommand with the same class parseSlashCommand splits on. const rollbackMatch = /^rollback[\t\p{Zs}]/u.exec(rest); if (rollbackMatch) { let rollbackId = rest.slice(rollbackMatch[0].length).trim(); if (rollbackId === "--global") { throw new Error("Usage: /refine rollback <refinement-id>"); } if (/\s--global$/.test(rollbackId)) { global = true; rollbackId = rollbackId.replace(/\s--global$/, "").trim(); } if (!rollbackId) throw new Error("Usage: /refine rollback <refinement-id>"); return { rollbackId, global }; } return { instructions: rest || undefined, global };}
export interface BuiltinSlashCommand { name: string; description: string; execution?: "client" | "session"; /** Shown in autocomplete before the description, e.g. "[instructions]" */ argumentHint?: string; /** Hidden names that resolve to this command without being shown as commands. */ aliases?: readonly string[]; takesArgument?: boolean;}
export interface ParsedSlashCommand { name: string; args: string;}
export interface ResolvedSlashCommand extends ParsedSlashCommand { originalName: string; isAlias: boolean;}
interface BuiltinSlashCommandAlias { name: string; aliasFor: string;}
const CANONICAL_BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [ { name: "settings", description: "Open settings menu" }, { name: "model", description: "Select model (opens selector UI)", argumentHint: "[search]" }, { name: "effort", description: "Select reasoning/thinking level (opens selector UI)", argumentHint: "[level]" }, { name: "fast", description: "Toggle OpenAI Fast mode" }, { name: "scoped-models", description: "Enable/disable models for Alt+M cycling" }, { name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)", argumentHint: "[path]", takesArgument: true, }, { name: "import", description: "Import and resume a session from a JSONL file", argumentHint: "<path.jsonl>", takesArgument: true, }, { name: "share", description: "Share session as a secret GitHub gist" }, { name: "copy", description: "Copy last agent message to clipboard" }, { name: "btw", description: "Ask a side question without adding it to the session; replies follow up, esc returns", argumentHint: "<question>", takesArgument: true, }, { name: "name", description: "Set or show the session display name", argumentHint: "[name]", takesArgument: true, }, { name: "session", description: "Show session info" }, { name: "system-prompt", description: "Show the exact system prompt sent to the model" }, { name: "logs", description: "Show where daemon and client logs are saved" }, { name: "traces", description: "Preview, upload, or configure Prime Agent traces", argumentHint: "[status|on|off|preview|upload|upload-current|upload-all|login]", }, { name: "context", description: "Show token, cost, and context usage for agent and sub-agents" }, { name: "usage", description: "Show provider quota and session usage" }, { name: "changelog", description: "Show changelog entries" }, { name: "update", description: `Update ${APP_NAME} and installed packages`, argumentHint: "[source|--self|--extensions|--nightly|--stable]", takesArgument: true, }, { name: "nightly", description: `Switch ${APP_NAME} updates to the nightly channel (unreleased builds, may be broken)`, argumentHint: "[on|off|status]", takesArgument: true, }, { name: "hotkeys", description: "Show all keyboard shortcuts" }, { name: "fork", description: "Create a new fork from a previous user message" }, { name: "clone", description: "Duplicate the current session at the current position" }, { name: "tree", description: "Navigate session tree (switch branches)" }, { name: "login", description: "Configure provider authentication" }, { name: "logout", description: "Remove provider authentication" }, { name: "mcp", description: "Browse external services or manage MCP integrations", argumentHint: "[add|list|get|remove|login|logout]", takesArgument: true, }, { name: "plugins", description: "Browse and connect external services", argumentHint: "[search]", takesArgument: true, }, { name: "new", description: "Start a new session, optionally named and/or with an initial prompt", argumentHint: '[--name "session name" --] [prompt]', takesArgument: true, }, { name: "compact", description: "Compact session context manually, or set an auto-compaction threshold (e.g. /compact at 600k)", argumentHint: "[instructions | at <tokens>]", takesArgument: true, }, { name: "refine", description: "Refine continual harness prompt notes, skills, subagents, and memory", }, { name: "goal", description: "Set or view a persistent goal; supports pause, resume, and clear", argumentHint: "[objective]", takesArgument: true, }, { name: "autonomous", description: "Set or view autonomous mode with an optional budget", argumentHint: "[status|off|on [--max-continuations <n>] [--max-turns <n>] [--max-tokens <n>] [--timeout-ms <n>] [--gate <command>]]", takesArgument: true, }, { name: "rlm-max-depth", description: "Set/view the per-chat persistent RLM max depth immediately; never interrupts or queues the running turn", argumentHint: "[<int> [--global]]", takesArgument: true, }, { name: "heartbeat", description: "Set or view a persistent heartbeat; delivery defaults to steer, use --follow-up to queue; supports pause, resume, stop, and clear", argumentHint: "[status|pause|resume|stop|[every <duration>] [--steer|--follow-up] <instruction>]", takesArgument: true, }, { name: "heartbeats", description: "View and manage all user and agent heartbeats" }, { name: "resume", description: "Open the agents view, or resume a session by id or path", argumentHint: "[id|path]", takesArgument: true, }, { name: "reload", description: "Reload keybindings, extensions, skills, prompts, themes, or models", argumentHint: "[models]", takesArgument: true, }, { name: "fullscreen", description: "Toggle fullscreen (alternate screen) rendering with scrollable transcript", argumentHint: "[on|off]", takesArgument: true, }, { name: "quit", description: `Quit ${APP_NAME}` },];
const BUILTIN_SLASH_COMMAND_ALIASES: ReadonlyArray<BuiltinSlashCommandAlias> = [ { name: "clear", aliasFor: "new" }, { name: "thinking", aliasFor: "effort" }, { name: "rename", aliasFor: "name" }, { name: "side", aliasFor: "btw" },];
function buildBuiltinSlashCommands(): ReadonlyArray<BuiltinSlashCommand> { const canonicalByName = new Map(CANONICAL_BUILTIN_SLASH_COMMANDS.map((command) => [command.name, command])); const aliasesByTarget = new Map<string, string[]>(); for (const alias of BUILTIN_SLASH_COMMAND_ALIASES) { const target = canonicalByName.get(alias.aliasFor); if (!target) { throw new Error(`Slash command alias '/${alias.name}' targets unknown command '/${alias.aliasFor}'`); } const targetAliases = aliasesByTarget.get(alias.aliasFor); if (targetAliases) { targetAliases.push(alias.name); } else { aliasesByTarget.set(alias.aliasFor, [alias.name]); } } return CANONICAL_BUILTIN_SLASH_COMMANDS.map((command) => ({ ...command, ...(isSessionSlashCommandName(command.name) ? { execution: "session" as const } : {}), ...(aliasesByTarget.has(command.name) ? { aliases: aliasesByTarget.get(command.name) } : {}), }));}
export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = buildBuiltinSlashCommands();
const BUILTIN_SLASH_COMMAND_BY_NAME = new Map(BUILTIN_SLASH_COMMANDS.map((command) => [command.name, command]));const BUILTIN_SLASH_COMMAND_ALIAS_TO_NAME = new Map( BUILTIN_SLASH_COMMANDS.flatMap((command) => command.aliases?.map((alias) => [alias, command.name] as const) ?? []),);
export function parseSlashCommand(text: string): ParsedSlashCommand | undefined { if (!text.startsWith("/")) return undefined; const match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(text); if (!match) return undefined; return { name: match[1], args: (match[2] ?? "").trim() };}
export function resolveBuiltinSlashCommandName(name: string): string { return BUILTIN_SLASH_COMMAND_ALIAS_TO_NAME.get(name) ?? name;}
export function isBuiltinSlashCommandName(name: string): boolean { return BUILTIN_SLASH_COMMAND_BY_NAME.has(name) || BUILTIN_SLASH_COMMAND_ALIAS_TO_NAME.has(name);}
export function builtinSlashCommandTakesArgument(name: string): boolean { // /clear remains the no-argument compatibility alias even though /new accepts arguments. if (name === "clear") return false; return BUILTIN_SLASH_COMMAND_BY_NAME.get(resolveBuiltinSlashCommandName(name))?.takesArgument === true;}
export function resolveSlashCommand(command: ParsedSlashCommand): ResolvedSlashCommand { const name = resolveBuiltinSlashCommandName(command.name); return { ...command, name, originalName: command.name, isAlias: name !== command.name, };}
export function parseSessionSlashCommand(text: string): SessionSlashCommand | undefined { if (/[\r\n\u2028\u2029]/u.test(text)) return undefined; const parsed = parseSlashCommand(text); if (!parsed) return undefined; const name = resolveBuiltinSlashCommandName(parsed.name); const command = BUILTIN_SLASH_COMMAND_BY_NAME.get(name); if (command?.execution !== "session" || !isSessionSlashCommandName(name)) return undefined; return { name, args: parsed.args, text };}
/** * Closest command-name suggestion for an unrecognized slash command, or * undefined when nothing is near enough. Shared by the CLI's unknown- * command notice and the session's typo guard. */export function findSlashCommandSuggestion(input: string, candidates: readonly string[]): string | undefined { let closest: { candidate: string; distance: number } | undefined; for (const candidate of candidates) { const distance = slashCommandEditDistance(input, candidate); if (!closest || distance < closest.distance) { closest = { candidate, distance }; } } // Very short tokens match only on a single-character typo: two-character // tolerance on a three-character token lets unrelated path-like words // (tmp vs mcp) masquerade as command typos. const threshold = input.length <= 3 ? 1 : Math.max(2, Math.floor(input.length / 3)); if (!closest || closest.distance > threshold) { return undefined; } return closest.candidate;}
function slashCommandEditDistance(left: string, right: string): number { const previous = new Array<number>(right.length + 1); const current = new Array<number>(right.length + 1); for (let j = 0; j <= right.length; j++) previous[j] = j; for (let i = 1; i <= left.length; i++) { current[0] = i; for (let j = 1; j <= right.length; j++) { current[j] = Math.min( previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1), ); } for (let j = 0; j <= right.length; j++) previous[j] = current[j]; } return previous[right.length];}