- 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>
386 lines
8.2 KiB
TypeScript
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;
|
|
}
|
|
}
|