Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions ts/packages/cli/src/commands/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -52,6 +53,16 @@ async function getCompletionsData(
dispatcher: Dispatcher,
): Promise<CompletionData | null> {
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
Expand Down Expand Up @@ -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({
Expand All @@ -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
Expand Down
27 changes: 25 additions & 2 deletions ts/packages/cli/src/enhancedConsole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -1385,6 +1390,24 @@ export async function processCommandsEnhanced<T>(
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...");
Expand Down Expand Up @@ -1468,5 +1491,5 @@ function getNextInput(
* Returns a clean prompt regardless of status text
*/
export function getEnhancedConsolePrompt(_text: string): string {
return "> ";
return `${getVerboseIndicator()}> `;
}
180 changes: 180 additions & 0 deletions ts/packages/cli/src/slashCommands.ts
Original file line number Diff line number Diff line change
@@ -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<any>,
) => Promise<any>;

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<string, SlashCommand>();
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<any>,
): Promise<SlashCommandResult> {
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 };
}
Loading