singularity-forge/web/lib/browser-slash-command-dispatch.ts
Mikael Hugo 22cbd83675 fix: update test snapshots for queryInstruction and complete /sf prefix Phase 2 deprecation
- Fix memory-embeddings-llm-gateway tests: add queryInstruction field to
  expected config objects after loadGatewayConfigFromEnv was updated to
  return it
- Add STYLEGUIDE.md: SF code standards adapted from ace-coder patterns
  (purpose doctrine, principles, anti-patterns STY001-012, thresholds,
  naming, patterns, documentation sections)
- Phase 2 /sf prefix removal: update all web components, browser dispatch,
  and tests to use direct commands (/autonomous, /stop, /next, /discuss,
  /init, /new-milestone) instead of /sf-prefixed forms
  - workflow-actions.ts: all command strings updated
  - chat-mode.tsx: SF_ACTIONS array updated
  - project-welcome.tsx: primaryCommand values updated
  - command-surface.tsx: fallback display updated
  - remaining-command-panels.tsx: usage examples updated
  - browser-slash-command-dispatch.ts: add stop/new-milestone/init to
    SF_PASSTHROUGH_COMMANDS so they route correctly to the extension
  - recovery-diagnostics-service.ts: suggestion commands updated
  - welcome-screen.ts: hint text updated
  - All affected tests updated to match new command strings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-09 00:17:47 +02:00

386 lines
8.2 KiB
TypeScript

import { BUILTIN_SLASH_COMMANDS } from "../../packages/pi-coding-agent/src/core/slash-commands.ts";
export type BrowserSlashCommandSurface =
| "settings"
| "model"
| "thinking"
| "git"
| "resume"
| "name"
| "fork"
| "compact"
| "login"
| "logout"
| "session"
| "export"
// SF command surfaces
| "sf-status"
| "sf-visualize"
| "sf-forensics"
| "sf-doctor"
| "sf-skill-health"
| "sf-knowledge"
| "sf-capture"
| "sf-triage"
| "sf-quick"
| "sf-history"
| "sf-undo"
| "sf-inspect"
| "sf-prefs"
| "sf-config"
| "sf-hooks"
| "sf-mode"
| "sf-steer"
| "sf-export"
| "sf-cleanup"
| "sf-queue";
export type BrowserSlashCommandLocalAction =
| "clear_terminal"
| "refresh_workspace"
| "sf_help";
export type BrowserSlashPromptCommandType = "prompt" | "follow_up";
export interface BrowserSlashCommandDispatchOptions {
isStreaming?: boolean;
}
export type BrowserSlashCommandDispatchResult =
| {
kind: "prompt";
input: string;
slashCommandName: string | null;
command: {
type: BrowserSlashPromptCommandType;
message: string;
};
}
| {
kind: "rpc";
input: string;
commandName: string;
command: { type: "get_state" } | { type: "new_session" };
}
| {
kind: "surface";
input: string;
commandName: string;
surface: BrowserSlashCommandSurface;
args: string;
}
| {
kind: "local";
input: string;
commandName: string;
action: BrowserSlashCommandLocalAction;
}
| {
kind: "reject";
input: string;
commandName: string;
reason: string;
guidance: string;
}
| {
kind: "view-navigate";
input: string;
commandName: string;
view: string;
};
export interface BrowserSlashCommandTerminalNotice {
type: "system" | "error";
message: string;
}
const BUILTIN_COMMAND_DESCRIPTIONS = new Map(
BUILTIN_SLASH_COMMANDS.map((command) => [command.name, command.description]),
);
const BUILTIN_COMMAND_NAMES = new Set(BUILTIN_COMMAND_DESCRIPTIONS.keys());
const SURFACE_COMMANDS = new Map<string, BrowserSlashCommandSurface>([
["settings", "settings"],
["model", "model"],
["thinking", "thinking"],
["git", "git"],
["resume", "resume"],
["name", "name"],
["fork", "fork"],
["compact", "compact"],
["login", "login"],
["logout", "logout"],
["session", "session"],
["export", "export"],
]);
// --- SF command dispatch ---
const SF_SURFACE_COMMANDS = new Map<string, BrowserSlashCommandSurface>([
["status", "sf-status"],
["visualize", "sf-visualize"],
["forensics", "sf-forensics"],
["doctor", "sf-doctor"],
["skill-health", "sf-skill-health"],
["knowledge", "sf-knowledge"],
["capture", "sf-capture"],
["triage", "sf-triage"],
["quick", "sf-quick"],
["history", "sf-history"],
["undo", "sf-undo"],
["inspect", "sf-inspect"],
["prefs", "sf-prefs"],
["config", "sf-config"],
["hooks", "sf-hooks"],
["mode", "sf-mode"],
["steer", "sf-steer"],
["cleanup", "sf-cleanup"],
["queue", "sf-queue"],
]);
const SF_PASSTHROUGH_COMMANDS = new Set<string>([
"autonomous",
"next",
"stop",
"pause",
"skip",
"discuss",
"run-hook",
"migrate",
"remote",
"new-milestone",
"init",
]);
export const SF_HELP_TEXT = `Available SF commands:
Workflow: next · autonomous · autonomous stop · pause · skip · queue · quick · capture · triage
Diagnostics: status · visualize · forensics · doctor · skill-health · inspect
Context: knowledge · history · undo · discuss
Settings: model · prefs · config · hooks · mode · steer
Advanced: export · cleanup · run-hook · migrate · remote
Type /<command> to run. Use /help for this message.`;
function dispatchSFCommand(
input: string,
commandName: string,
args: string,
options: BrowserSlashCommandDispatchOptions,
): BrowserSlashCommandDispatchResult {
// `/help` — render inline help locally
if (commandName === "help") {
return {
kind: "local",
input,
commandName,
action: "sf_help",
};
}
// `/visualize` — navigate to the visualizer view directly
if (commandName === "visualize") {
return {
kind: "view-navigate",
input,
commandName,
view: "visualize",
};
}
// Surface-routed commands — open browser-native UI
const surface = SF_SURFACE_COMMANDS.get(commandName);
if (surface) {
return {
kind: "surface",
input,
commandName,
surface,
args,
};
}
// Bridge-passthrough commands — let the extension handle them
if (SF_PASSTHROUGH_COMMANDS.has(commandName)) {
return {
kind: "prompt",
input,
slashCommandName: commandName,
command: {
type: getPromptCommandType(options),
message: input,
},
};
}
return buildDeferredBuiltinReject(input, commandName);
}
function parseSlashCommand(
input: string,
): { name: string; args: string } | null {
if (!input.startsWith("/")) return null;
const body = input.slice(1).trim();
if (!body) return null;
const firstSpaceIndex = body.search(/\s/);
if (firstSpaceIndex === -1) {
return { name: body, args: "" };
}
return {
name: body.slice(0, firstSpaceIndex),
args: body.slice(firstSpaceIndex + 1).trim(),
};
}
function getPromptCommandType(
options: BrowserSlashCommandDispatchOptions,
): BrowserSlashPromptCommandType {
return options.isStreaming ? "follow_up" : "prompt";
}
function formatBuiltinDescription(commandName: string): string {
return (
BUILTIN_COMMAND_DESCRIPTIONS.get(commandName) ??
"Browser handling is reserved for this built-in command."
);
}
function buildDeferredBuiltinReject(
input: string,
commandName: string,
): BrowserSlashCommandDispatchResult {
const description = formatBuiltinDescription(commandName);
return {
kind: "reject",
input,
commandName,
reason: `/${commandName} is a built-in pi command (${description}) that is not available in the browser yet.`,
guidance: "It was blocked instead of falling through to the model.",
};
}
export function isAuthoritativeBuiltinSlashCommand(
commandName: string,
): boolean {
return BUILTIN_COMMAND_NAMES.has(commandName);
}
export function dispatchBrowserSlashCommand(
input: string,
options: BrowserSlashCommandDispatchOptions = {},
): BrowserSlashCommandDispatchResult {
const trimmed = input.trim();
const parsed = parseSlashCommand(trimmed);
if (trimmed === "/clear") {
return {
kind: "local",
input: trimmed,
commandName: "clear",
action: "clear_terminal",
};
}
if (trimmed === "/refresh") {
return {
kind: "local",
input: trimmed,
commandName: "refresh",
action: "refresh_workspace",
};
}
if (trimmed === "/state") {
return {
kind: "rpc",
input: trimmed,
commandName: "state",
command: { type: "get_state" },
};
}
if (trimmed === "/new-session") {
return {
kind: "rpc",
input: trimmed,
commandName: "new",
command: { type: "new_session" },
};
}
if (!parsed) {
return {
kind: "prompt",
input: trimmed,
slashCommandName: null,
command: {
type: getPromptCommandType(options),
message: trimmed,
},
};
}
if (parsed.name === "new") {
return {
kind: "rpc",
input: trimmed,
commandName: "new",
command: { type: "new_session" },
};
}
if (
parsed.name === "help" ||
parsed.name === "visualize" ||
SF_SURFACE_COMMANDS.has(parsed.name) ||
SF_PASSTHROUGH_COMMANDS.has(parsed.name)
) {
return dispatchSFCommand(trimmed, parsed.name, parsed.args, options);
}
const browserSurface = SURFACE_COMMANDS.get(parsed.name);
if (browserSurface) {
return {
kind: "surface",
input: trimmed,
commandName: parsed.name,
surface: browserSurface,
args: parsed.args,
};
}
if (BUILTIN_COMMAND_NAMES.has(parsed.name)) {
return buildDeferredBuiltinReject(trimmed, parsed.name);
}
return {
kind: "prompt",
input: trimmed,
slashCommandName: parsed.name,
command: {
type: getPromptCommandType(options),
message: trimmed,
},
};
}
export function getBrowserSlashCommandTerminalNotice(
outcome: BrowserSlashCommandDispatchResult,
): BrowserSlashCommandTerminalNotice | null {
switch (outcome.kind) {
case "surface":
return {
type: "system",
message: `/${outcome.commandName} is reserved for browser-native handling and was not sent to the model.`,
};
case "reject":
return {
type: "error",
message: `${outcome.reason} ${outcome.guidance}`.trim(),
};
default:
return null;
}
}