diff --git a/ts/packages/cli/src/commands/interactive.ts b/ts/packages/cli/src/commands/interactive.ts index a4f130dd2..270174b48 100644 --- a/ts/packages/cli/src/commands/interactive.ts +++ b/ts/packages/cli/src/commands/interactive.ts @@ -26,6 +26,7 @@ import { processCommandsEnhanced, withEnhancedConsoleClientIO, } from "../enhancedConsole.js"; +import { isSlashCommand, getSlashCompletions } from "../slashCommands.js"; import { getStatusSummary } from "agent-dispatcher/helpers/status"; import { getFsStorageProvider } from "dispatcher-node-providers"; import { createInterface } from "readline/promises"; @@ -52,6 +53,16 @@ async function getCompletionsData( dispatcher: Dispatcher, ): Promise { try { + // Handle slash command completions + if (isSlashCommand(line)) { + const completions = getSlashCompletions(line); + if (completions.length === 0) return null; + return { + allCompletions: completions, + filterStartIndex: 0, + prefix: "", + }; + } // Token-boundary logic: for non-@ input, only send complete tokens // to the backend. The NFA can only match whole tokens, so sending a // partial word like "p" fails. Instead, send up to the last token @@ -140,6 +151,11 @@ export default class Interactive extends Command { "Enable enhanced terminal UI with spinners and visual prompts", default: false, }), + verbose: Flags.string({ + description: + "Enable verbose debug output (optional: comma-separated debug namespaces, default: typeagent:*)", + required: false, + }), }; static args = { input: Args.file({ @@ -155,6 +171,18 @@ export default class Interactive extends Command { inspector.open(undefined, undefined, true); } + if (flags.verbose !== undefined) { + const { default: registerDebug } = await import("debug"); + const namespaces = flags.verbose || "typeagent:*"; + registerDebug.enable(namespaces); + process.env.DEBUG = namespaces; + // Also set internal verbose state for prompt indicator + const { enableVerboseFromFlag } = await import( + "../slashCommands.js" + ); + enableVerboseFromFlag(namespaces); + } + // Choose between standard and enhanced UI const withClientIO = flags.testUI ? withEnhancedConsoleClientIO diff --git a/ts/packages/cli/src/enhancedConsole.ts b/ts/packages/cli/src/enhancedConsole.ts index 759d70f91..1b3030295 100644 --- a/ts/packages/cli/src/enhancedConsole.ts +++ b/ts/packages/cli/src/enhancedConsole.ts @@ -35,6 +35,11 @@ import readline from "readline"; import { convert } from "html-to-text"; import { marked } from "marked"; import { markedTerminal } from "marked-terminal"; +import { + isSlashCommand, + handleSlashCommand, + getVerboseIndicator, +} from "./slashCommands.js"; // Track current processing state let currentSpinner: EnhancedSpinner | null = null; @@ -915,7 +920,7 @@ async function questionWithCompletion( } else if (input.length > 0) { hint = "↑↓ history · esc clear"; } else { - hint = "↑↓ history"; + hint = "↑↓ history · /help commands"; } output += "\n " + chalk.dim(hint) + "\x1b[K"; @@ -1385,6 +1390,24 @@ export async function processCommandsEnhanced( break; } + // Handle slash commands before sending to dispatcher + if (isSlashCommand(request)) { + try { + const slashResult = await handleSlashCommand(request, (cmd) => + processCommand(cmd, context), + ); + if (slashResult.exit) { + process.stdout.write("\x1b[s\n\x1b[K\n\x1b[K\x1b[u"); + break; + } + } catch (error) { + console.log(chalk.red(`Error: ${error}`)); + } + history.push(request); + console.log(""); + continue; + } + try { // Start spinner for processing startProcessingSpinner("Processing request..."); @@ -1468,5 +1491,5 @@ function getNextInput( * Returns a clean prompt regardless of status text */ export function getEnhancedConsolePrompt(_text: string): string { - return "> "; + return `${getVerboseIndicator()}> `; } diff --git a/ts/packages/cli/src/slashCommands.ts b/ts/packages/cli/src/slashCommands.ts new file mode 100644 index 000000000..432b2d540 --- /dev/null +++ b/ts/packages/cli/src/slashCommands.ts @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import registerDebug from "debug"; +import chalk from "chalk"; + +export type SlashCommandHandler = ( + args: string, + processCommand: (command: string) => Promise, +) => Promise; + +interface SlashCommand { + name: string; + description: string; + handler: SlashCommandHandler; +} + +// Verbose mode state +let verboseEnabled = false; +let activeNamespaces = ""; + +export function isVerboseEnabled(): boolean { + return verboseEnabled; +} + +export function enableVerboseFromFlag(namespaces: string): void { + verboseEnabled = true; + activeNamespaces = namespaces; +} + +export function getVerboseIndicator(): string { + if (!verboseEnabled) return ""; + if (activeNamespaces === "typeagent:*") + return chalk.yellow("[verbose]") + " "; + const parts = activeNamespaces.split(","); + if (parts.length === 1) { + const short = parts[0].replace(/^typeagent:/, "").replace(/:\*$/, ""); + return chalk.yellow(`[verbose:${short}]`) + " "; + } + return chalk.yellow(`[verbose:${parts.length} scopes]`) + " "; +} + +function handleVerbose(args: string): void { + if (args === "off" || (args === "" && verboseEnabled)) { + registerDebug.disable(); + verboseEnabled = false; + activeNamespaces = ""; + console.log(chalk.dim("Verbose mode disabled.")); + } else { + const namespaces = args || "typeagent:*"; + registerDebug.enable(namespaces); + process.env.DEBUG = namespaces; + verboseEnabled = true; + activeNamespaces = namespaces; + console.log(chalk.dim(`Verbose mode enabled: ${namespaces}`)); + } +} + +const slashCommands: SlashCommand[] = [ + { + name: "help", + description: "Show available commands", + handler: async (_args, processCommand) => { + // Print slash commands first, then delegate to @system help + console.log(chalk.bold("\nSlash Commands:")); + for (const cmd of slashCommands) { + console.log( + ` ${chalk.cyanBright("/" + cmd.name.padEnd(12))} ${chalk.dim(cmd.description)}`, + ); + } + console.log(""); + return processCommand("@system help"); + }, + }, + { + name: "clear", + description: "Clear the terminal screen", + handler: async () => { + process.stdout.write("\x1b[2J\x1b[H"); + }, + }, + { + name: "verbose", + description: "Toggle verbose debug output", + handler: async (args) => { + handleVerbose(args); + }, + }, + { + name: "trace", + description: "Manage debug trace namespaces", + handler: async (args, processCommand) => { + return processCommand(`@system trace ${args}`); + }, + }, + { + name: "history", + description: "Show conversation history", + handler: async (args, processCommand) => { + return processCommand(`@system history ${args}`); + }, + }, + { + name: "session", + description: "Session management", + handler: async (args, processCommand) => { + return processCommand(`@system session ${args}`); + }, + }, + { + name: "config", + description: "View or edit configuration", + handler: async (args, processCommand) => { + return processCommand(`@system config ${args}`); + }, + }, + { + name: "agents", + description: "List available agents", + handler: async (_args, processCommand) => { + return processCommand("@system config agents"); + }, + }, + { + name: "exit", + description: "Exit the CLI", + handler: async () => { + // Return a sentinel value that the main loop recognizes + return { exit: true }; + }, + }, +]; + +const commandMap = new Map(); +for (const cmd of slashCommands) { + commandMap.set(cmd.name, cmd); +} + +export function isSlashCommand(input: string): boolean { + return input.startsWith("/"); +} + +export function getSlashCompletions(input: string): string[] { + const prefix = input.substring(1).toLowerCase(); + return slashCommands + .filter((cmd) => cmd.name.startsWith(prefix)) + .map((cmd) => "/" + cmd.name); +} + +export interface SlashCommandResult { + handled: boolean; + exit?: boolean; + result?: any; +} + +export async function handleSlashCommand( + input: string, + processCommand: (command: string) => Promise, +): Promise { + const trimmed = input.substring(1).trim(); + const spaceIdx = trimmed.indexOf(" "); + const name = + spaceIdx === -1 + ? trimmed.toLowerCase() + : trimmed.substring(0, spaceIdx).toLowerCase(); + const args = spaceIdx === -1 ? "" : trimmed.substring(spaceIdx + 1).trim(); + + const cmd = commandMap.get(name); + if (!cmd) { + console.log(chalk.yellow(`Unknown command: ${input}`)); + console.log(chalk.dim("Type /help to see available commands.")); + return { handled: true }; + } + + const result = await cmd.handler(args, processCommand); + if (result?.exit) { + return { handled: true, exit: true }; + } + return { handled: true, result }; +}