import { BUILTIN_SLASH_COMMANDS } from "../../../../../../packages/coding-agent/src/core/slash-commands.ts"; const BUILTIN_NAMES = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name)); /** Built-in slash commands that WebUI executes (not TUI-only hints). */ export const WEBUI_BUILTIN_SLASH_NAMES = new Set([ "compact", "new", "reload", "clone", "name", "model", "session", "export", "copy", ]); export function isWebUiSlashCommand(source: "builtin" | "extension" | "prompt" | "skill", name: string): boolean { if (source === "extension" || source === "prompt" || source === "skill") { return true; } return WEBUI_BUILTIN_SLASH_NAMES.has(name); } export interface SlashCommandEntry { name: string; description: string; source: "builtin" | "extension" | "prompt" | "skill"; } export function filterWebUiSlashCommands(commands: SlashCommandEntry[]): SlashCommandEntry[] { return commands.filter((command) => isWebUiSlashCommand(command.source, command.name)); } export interface SlashDispatchResult { handled: boolean; message?: string; action?: "new_session" | "reload_messages" | "reload_sessions" | "copy"; sessionFile?: string; } type SendCmd = (command: Record) => Promise; interface ParsedSlash { name: string; args: string; } function parseSlashInput(text: string): ParsedSlash | null { const trimmed = text.trim(); if (!trimmed.startsWith("/")) return null; const body = trimmed.slice(1); const spaceIndex = body.indexOf(" "); if (spaceIndex === -1) { return { name: body, args: "" }; } return { name: body.slice(0, spaceIndex), args: body.slice(spaceIndex + 1).trim(), }; } function webuiOnlyMessage(command: string): SlashDispatchResult { const hints: Record = { settings: "请使用 WebUI 顶栏或设置页修改配置。", "scoped-models": "该命令仅在终端交互模式可用。", changelog: "该命令仅在终端交互模式可用。", hotkeys: "该命令仅在终端交互模式可用。", fork: "该命令仅在终端交互模式可用(需选择消息节点)。", tree: "该命令仅在终端交互模式可用。", login: "请在 pi 终端模式中执行 /login 配置认证。", logout: "请在 pi 终端模式中执行 /logout 移除认证。", resume: "请使用左侧会话列表切换会话。", import: "请使用终端模式 /import,或通过会话列表管理历史会话。", share: "该命令仅在终端交互模式可用。", quit: "WebUI 不会退出 pi 进程。", }; return { handled: true, message: hints[command] || "该命令在 WebUI 中不可用,请在终端模式使用。", }; } async function findModelMatch(sendCmd: SendCmd, searchTerm: string): Promise<{ provider: string; modelId: string } | null> { const response = await sendCmd({ type: "get_available_models" }); if (!response.success) return null; const models = response.data?.models || []; const term = searchTerm.trim().toLowerCase(); if (!term) return null; if (term.includes("/")) { const slashIndex = term.indexOf("/"); const provider = term.slice(0, slashIndex); const modelId = term.slice(slashIndex + 1); const match = models.find( (model: any) => String(model.provider || "").toLowerCase() === provider && String(model.id || "").toLowerCase() === modelId, ); if (match) { return { provider: match.provider, modelId: match.id }; } } const byId = models.filter((model: any) => String(model.id || "").toLowerCase() === term); if (byId.length === 1) { return { provider: byId[0].provider, modelId: byId[0].id }; } return null; } export function isBuiltinSlashCommand(text: string): boolean { const parsed = parseSlashInput(text); return parsed ? BUILTIN_NAMES.has(parsed.name) : false; } export async function dispatchSlashCommand(text: string, sendCmd: SendCmd): Promise { const parsed = parseSlashInput(text); if (!parsed) return null; if (!BUILTIN_NAMES.has(parsed.name)) return null; const { name, args } = parsed; switch (name) { case "compact": { const response = await sendCmd({ type: "compact", customInstructions: args || undefined }); if (!response.success) throw new Error(response.error || "压缩失败"); return { handled: true, action: "reload_messages" }; } case "new": { const response = await sendCmd({ type: "new_session" }); if (!response.success) throw new Error(response.error || "新建会话失败"); if (response.data?.cancelled) { return { handled: true, message: "已取消新建会话" }; } const state = await sendCmd({ type: "get_state" }); if (!state.success) throw new Error(state.error || "读取会话状态失败"); return { handled: true, action: "new_session", sessionFile: state.data?.sessionFile, message: "已开始新会话", }; } case "reload": { const response = await sendCmd({ type: "reload" }); if (!response.success) throw new Error(response.error || "重新加载失败"); return { handled: true, action: "reload_sessions", message: "已重新加载配置与扩展" }; } case "clone": { const response = await sendCmd({ type: "clone" }); if (!response.success) throw new Error(response.error || "克隆会话失败"); const state = await sendCmd({ type: "get_state" }); return { handled: true, action: "new_session", sessionFile: state.success ? state.data?.sessionFile : undefined, message: "已克隆当前会话", }; } case "name": { if (!args) { const state = await sendCmd({ type: "get_state" }); if (!state.success) throw new Error(state.error || "读取会话状态失败"); const currentName = state.data?.sessionName; return { handled: true, message: currentName ? `当前会话名称:${currentName}` : "用法:/name <名称>", }; } const response = await sendCmd({ type: "set_session_name", name: args }); if (!response.success) throw new Error(response.error || "设置会话名称失败"); return { handled: true, message: `会话名称已设为:${args}` }; } case "model": { if (!args) { return { handled: true, message: "请使用顶栏模型选择器切换模型,或输入 /model provider/modelId。" }; } const match = await findModelMatch(sendCmd, args); if (!match) { throw new Error(`未找到模型:${args}`); } const response = await sendCmd({ type: "set_model", provider: match.provider, modelId: match.modelId }); if (!response.success) throw new Error(response.error || "切换模型失败"); return { handled: true, message: `已切换模型:${match.provider}/${match.modelId}` }; } case "session": { const response = await sendCmd({ type: "get_session_stats" }); if (!response.success) throw new Error(response.error || "读取会话信息失败"); const stats = response.data || {}; const state = await sendCmd({ type: "get_state" }); const sessionName = state.success ? state.data?.sessionName : undefined; const lines = [ sessionName ? `名称:${sessionName}` : null, stats.sessionFile ? `文件:${stats.sessionFile}` : null, stats.sessionId ? `ID:${stats.sessionId}` : null, `用户消息:${stats.userMessages ?? 0}`, `助手消息:${stats.assistantMessages ?? 0}`, `工具调用:${stats.toolCalls ?? 0}`, `总计:${stats.totalMessages ?? 0}`, ].filter(Boolean); return { handled: true, message: lines.join("\n") }; } case "export": { const response = await sendCmd({ type: "export_html", outputPath: args || undefined }); if (!response.success) throw new Error(response.error || "导出失败"); const outputPath = response.data?.path || response.data?.filePath || args || "默认路径"; return { handled: true, message: `会话已导出:${outputPath}` }; } case "copy": { const response = await sendCmd({ type: "get_last_assistant_text" }); if (!response.success) throw new Error(response.error || "读取助手消息失败"); const copyText = String(response.data?.text || "").trim(); if (!copyText) { return { handled: true, message: "暂无可复制的助手消息。" }; } return { handled: true, action: "copy", message: copyText }; } case "settings": case "scoped-models": case "changelog": case "hotkeys": case "fork": case "tree": case "login": case "logout": case "resume": case "import": case "share": case "quit": return webuiOnlyMessage(name); default: return null; } }