diff --git a/.pi/extensions/prompt-url-widget.ts b/.pi/extensions/prompt-url-widget.ts deleted file mode 100644 index 835fce10..00000000 --- a/.pi/extensions/prompt-url-widget.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { DynamicBorder, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent"; -import { Container, Text } from "@earendil-works/pi-tui"; - -const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im; -const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im; - -type PromptMatch = { - kind: "pr" | "issue"; - url: string; -}; - -type GhMetadata = { - title?: string; - author?: { - login?: string; - name?: string | null; - }; -}; - -function extractPromptMatch(prompt: string): PromptMatch | undefined { - const prMatch = prompt.match(PR_PROMPT_PATTERN); - if (prMatch?.[1]) { - return { kind: "pr", url: prMatch[1].trim() }; - } - - const issueMatch = prompt.match(ISSUE_PROMPT_PATTERN); - if (issueMatch?.[1]) { - return { kind: "issue", url: issueMatch[1].trim() }; - } - - return undefined; -} - -async function fetchGhMetadata( - pi: ExtensionAPI, - kind: PromptMatch["kind"], - url: string, -): Promise { - const args = - kind === "pr" ? ["pr", "view", url, "--json", "title,author"] : ["issue", "view", url, "--json", "title,author"]; - - try { - const result = await pi.exec("gh", args); - if (result.code !== 0 || !result.stdout) return undefined; - return JSON.parse(result.stdout) as GhMetadata; - } catch { - return undefined; - } -} - -function formatAuthor(author?: GhMetadata["author"]): string | undefined { - if (!author) return undefined; - const name = author.name?.trim(); - const login = author.login?.trim(); - if (name && login) return `${name} (@${login})`; - if (login) return `@${login}`; - if (name) return name; - return undefined; -} - -export default function promptUrlWidgetExtension(pi: ExtensionAPI) { - const setWidget = (ctx: ExtensionContext, match: PromptMatch, title?: string, authorText?: string) => { - ctx.ui.setWidget("prompt-url", (_tui, thm) => { - const titleText = title ? thm.fg("accent", title) : thm.fg("accent", match.url); - const authorLine = authorText ? thm.fg("muted", authorText) : undefined; - const urlLine = thm.fg("dim", match.url); - - const lines = [titleText]; - if (authorLine) lines.push(authorLine); - lines.push(urlLine); - - const container = new Container(); - container.addChild(new DynamicBorder((s: string) => thm.fg("muted", s))); - container.addChild(new Text(lines.join("\n"), 1, 0)); - return container; - }); - }; - - const applySessionName = (ctx: ExtensionContext, match: PromptMatch, title?: string) => { - const label = match.kind === "pr" ? "PR" : "Issue"; - const trimmedTitle = title?.trim(); - const fallbackName = `${label}: ${match.url}`; - const desiredName = trimmedTitle ? `${label}: ${trimmedTitle} (${match.url})` : fallbackName; - const currentName = pi.getSessionName()?.trim(); - if (!currentName) { - pi.setSessionName(desiredName); - return; - } - if (currentName === match.url || currentName === fallbackName) { - pi.setSessionName(desiredName); - } - }; - - pi.on("before_agent_start", async (event, ctx) => { - if (!ctx.hasUI) return; - const match = extractPromptMatch(event.prompt); - if (!match) { - return; - } - - setWidget(ctx, match); - applySessionName(ctx, match); - void fetchGhMetadata(pi, match.kind, match.url).then((meta) => { - const title = meta?.title?.trim(); - const authorText = formatAuthor(meta?.author); - setWidget(ctx, match, title, authorText); - applySessionName(ctx, match, title); - }); - }); - - pi.on("session_switch", async (_event, ctx) => { - rebuildFromSession(ctx); - }); - - const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => { - if (!content) return ""; - if (typeof content === "string") return content; - return ( - content - .filter((block): block is { type: "text"; text: string } => block.type === "text") - .map((block) => block.text) - .join("\n") ?? "" - ); - }; - - const rebuildFromSession = (ctx: ExtensionContext) => { - if (!ctx.hasUI) return; - - const entries = ctx.sessionManager.getEntries(); - const lastMatch = [...entries].reverse().find((entry) => { - if (entry.type !== "message" || entry.message.role !== "user") return false; - const text = getUserText(entry.message.content); - return !!extractPromptMatch(text); - }); - - const content = - lastMatch?.type === "message" && lastMatch.message.role === "user" ? lastMatch.message.content : undefined; - const text = getUserText(content); - const match = text ? extractPromptMatch(text) : undefined; - if (!match) { - ctx.ui.setWidget("prompt-url", undefined); - return; - } - - setWidget(ctx, match); - applySessionName(ctx, match); - void fetchGhMetadata(pi, match.kind, match.url).then((meta) => { - const title = meta?.title?.trim(); - const authorText = formatAuthor(meta?.author); - setWidget(ctx, match, title, authorText); - applySessionName(ctx, match, title); - }); - }; - - pi.on("session_start", async (_event, ctx) => { - rebuildFromSession(ctx); - }); -} diff --git a/.pi/extensions/redraws.ts b/.pi/extensions/redraws.ts deleted file mode 100644 index 0f24a835..00000000 --- a/.pi/extensions/redraws.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Redraws Extension - * - * Exposes /tui to show TUI redraw stats. - */ - -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { Text } from "@earendil-works/pi-tui"; - -export default function (pi: ExtensionAPI) { - pi.registerCommand("tui", { - description: "Show TUI stats", - handler: async (_args, ctx) => { - if (!ctx.hasUI) return; - let redraws = 0; - await ctx.ui.custom((tui, _theme, _keybindings, done) => { - redraws = tui.fullRedraws; - done(undefined); - return new Text("", 0, 0); - }); - ctx.ui.notify(`TUI full redraws: ${redraws}`, "info"); - }, - }); -} diff --git a/.pi/extensions/sproutclaw-setup.ts b/.pi/extensions/sproutclaw-setup.ts deleted file mode 100644 index 451d9c94..00000000 --- a/.pi/extensions/sproutclaw-setup.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * sproutclaw / mengya 命令安装扩展 - * - * 启动后自动在 /usr/local/bin/ 下创建 mengya 和 sproutclaw 命令, - * 方便快速启动 sproutclaw(cd 到项目目录并执行 ./pi-test.sh)。 - * - * 命令 /install-commands 可随时手动重装。 - */ - -import { existsSync, writeFileSync, chmodSync, readFileSync } from "node:fs"; -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; - -const SPROUTCLAW_DIR = "/shumengya/project/agent/sproutclaw"; -const BIN_DIR = "/usr/local/bin"; -const COMMANDS = ["mengya", "sproutclaw"]; - -/** 创建或修复命令脚本 */ -function installCommand(name: string): boolean { - const path = `${BIN_DIR}/${name}`; - - const script = `#!/usr/bin/env bash -set -euo pipefail -cd ${SPROUTCLAW_DIR} -exec ./pi-test.sh "$@" -`; - if (existsSync(path)) { - try { - const current = readFileSync(path, "utf-8"); - if (current === script) return false; - } catch { - // Rewrite unreadable or invalid command files below. - } - } - writeFileSync(path, script, "utf-8"); - chmodSync(path, 0o755); - return true; -} - -export default function (pi: ExtensionAPI) { - // 启动时自动安装 - for (const name of COMMANDS) { - const created = installCommand(name); - if (created) { - console.log(`[sproutclaw-setup] Created /usr/local/bin/${name}`); - } - } - - // 命令:手动重装 - pi.registerCommand("install-commands", { - description: "Install mengya and sproutclaw commands to /usr/local/bin", - handler: async (_args, ctx) => { - let count = 0; - for (const name of COMMANDS) { - if (installCommand(name)) count++; - } - if (count > 0) { - ctx.ui.notify(`Installed ${count} command(s): mengya, sproutclaw`, "success"); - } else { - ctx.ui.notify("Both commands already exist", "info"); - } - }, - }); -} diff --git a/.pi/extensions/startup-chinese.ts b/.pi/extensions/startup-chinese.ts deleted file mode 100644 index 79c25637..00000000 --- a/.pi/extensions/startup-chinese.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * 启动界面中文翻译扩展 - * - * 将 pi 的初始启动界面中的介绍和引导文字翻译为中文, - * 快捷键和命令提示保持原样。 - */ - -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { keyHint, keyText, rawKeyHint, VERSION } from "@mariozechner/pi-coding-agent"; - -export default function (pi: ExtensionAPI) { - pi.on("session_start", async (_event, ctx) => { - if (!ctx.hasUI) return; - - ctx.ui.setHeader((_tui, theme) => { - const expandedInstructions = [ - keyHint("app.interrupt", "to interrupt"), - keyHint("app.clear", "to clear"), - rawKeyHint(`${keyText("app.clear")} twice`, "to exit"), - keyHint("app.exit", "to exit (empty)"), - keyHint("app.suspend", "to suspend"), - keyHint("tui.editor.deleteToLineEnd" as any, "to delete to end"), - keyHint("app.thinking.cycle", "to cycle thinking level"), - rawKeyHint( - `${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, - "to cycle models", - ), - keyHint("app.model.select", "to select model"), - keyHint("app.tools.expand", "to expand tools"), - keyHint("app.thinking.toggle", "to expand thinking"), - keyHint("app.editor.external", "for external editor"), - rawKeyHint("/", "for commands"), - rawKeyHint("!", "to run bash"), - rawKeyHint("!!", "to run bash (no context)"), - keyHint("app.message.followUp", "to queue follow-up"), - keyHint("app.message.dequeue", "to edit all queued messages"), - keyHint("app.clipboard.pasteImage", "to paste image"), - rawKeyHint("drop files", "to attach"), - ].join("\n"); - const compactInstructions = [ - keyHint("app.interrupt", "interrupt"), - rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"), - rawKeyHint("/", "commands"), - rawKeyHint("!", "bash"), - keyHint("app.tools.expand", "more"), - ].join(theme.fg("muted", " · ")); - const compactOnboarding = theme.fg( - "dim", - `按 ${keyText("app.tools.expand")} 查看完整帮助和已加载资源。`, - ); - const onboarding = theme.fg( - "dim", - "Pi 可以解释自身功能并查阅文档。询问它如何使用或扩展 Pi。", - ); - - let expanded = false; - - return { - render(_width: number): string[] { - const logo = - theme.bold(theme.fg("accent", "pi")) + theme.fg("dim", ` v${VERSION}`); - - const lines: string[] = [logo]; - if (expanded) { - lines.push(expandedInstructions); - lines.push(""); - lines.push(onboarding); - } else { - lines.push(compactInstructions); - lines.push(compactOnboarding); - lines.push(""); - lines.push(onboarding); - } - return lines; - }, - invalidate() {}, - setExpanded(v: boolean) { - expanded = v; - }, - }; - }); - }); -} diff --git a/.pi/extensions/tps.ts b/.pi/extensions/tps.ts deleted file mode 100644 index 6ce88868..00000000 --- a/.pi/extensions/tps.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; - -function isAssistantMessage(message: unknown): message is AssistantMessage { - if (!message || typeof message !== "object") return false; - const role = (message as { role?: unknown }).role; - return role === "assistant"; -} - -export default function (pi: ExtensionAPI) { - let agentStartMs: number | null = null; - - pi.on("agent_start", () => { - agentStartMs = Date.now(); - }); - - pi.on("agent_end", (event, ctx) => { - if (!ctx.hasUI) return; - if (agentStartMs === null) return; - - const elapsedMs = Date.now() - agentStartMs; - agentStartMs = null; - if (elapsedMs <= 0) return; - - let input = 0; - let output = 0; - let cacheRead = 0; - let cacheWrite = 0; - let totalTokens = 0; - - for (const message of event.messages) { - if (!isAssistantMessage(message)) continue; - input += message.usage.input || 0; - output += message.usage.output || 0; - cacheRead += message.usage.cacheRead || 0; - cacheWrite += message.usage.cacheWrite || 0; - totalTokens += message.usage.totalTokens || 0; - } - - if (output <= 0) return; - - const elapsedSeconds = elapsedMs / 1000; - const tokensPerSecond = output / elapsedSeconds; - const message = `TPS ${tokensPerSecond.toFixed(1)} tok/s. out ${output.toLocaleString()}, in ${input.toLocaleString()}, cache r/w ${cacheRead.toLocaleString()}/${cacheWrite.toLocaleString()}, total ${totalTokens.toLocaleString()}, ${elapsedSeconds.toFixed(1)}s`; - ctx.ui.notify(message, "info"); - }); -} diff --git a/.pi/extensions/webui/index.ts b/.pi/extensions/webui/index.ts deleted file mode 100644 index 01c545e6..00000000 --- a/.pi/extensions/webui/index.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * WebUI 扩展 —— 在浏览器中通过网页与 pi 对话 - * - * 安装: - * 将本文件放入 .pi/extensions/webui/index.ts - * - * 用法: - * /webui on → 启动网页服务(默认端口 19133) - * /webui off → 停止网页服务 - * /webui on 8080 → 指定端口启动 - */ - -import { spawn, type ChildProcess } from "node:child_process"; -import { join, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs"; -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const PID_FILE = join(__dirname, ".webui.pid"); - -let serverProcess: ChildProcess | null = null; -let serverPort = 19133; - -function findTsx(): string { - // 从扩展所在目录向上找 repo 根目录 - let dir = __dirname; - for (let i = 0; i < 10; i++) { - const candidate = join(dir, "node_modules", ".bin", "tsx"); - if (existsSync(candidate)) return candidate; - const parent = dirname(dir); - if (parent === dir) break; - dir = parent; - } - // fallback - return join(__dirname, "..", "..", "..", "node_modules", ".bin", "tsx"); -} - -function readPidFile(): number | null { - if (!existsSync(PID_FILE)) return null; - try { - const pid = parseInt(readFileSync(PID_FILE, "utf8").trim(), 10); - return Number.isFinite(pid) && pid > 0 ? pid : null; - } catch { - return null; - } -} - -function writePidFile(pid: number): void { - try { - writeFileSync(PID_FILE, String(pid), "utf8"); - } catch { - // 忽略:PID 文件只是辅助信息 - } -} - -function clearPidFile(): void { - try { - if (existsSync(PID_FILE)) unlinkSync(PID_FILE); - } catch { - // 忽略 - } -} - -function isProcessAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -function waitForStartup(port: number, child: ChildProcess): Promise { - return new Promise((resolve, reject) => { - let settled = false; - let timeout: NodeJS.Timeout | null = null; - - const finish = (err?: Error) => { - if (settled) return; - settled = true; - if (timeout) clearTimeout(timeout); - if (err) reject(err); - else resolve(); - }; - - const check = () => { - if (child.exitCode !== null || child.signalCode !== null) { - finish(new Error(`webui 子进程已退出,code=${child.exitCode ?? "null"}`)); - return; - } - - const http = require("node:http"); - const req = http.get( - { hostname: "127.0.0.1", port, path: "/api/sessions", timeout: 1500 }, - (res: any) => { - res.resume(); - if (res.statusCode === 200) finish(); - else setTimeout(check, 200); - }, - ); - req.on("error", () => setTimeout(check, 200)); - req.on("timeout", () => { - req.destroy(); - setTimeout(check, 200); - }); - }; - - timeout = setTimeout(() => finish(new Error("webui 启动超时")), 10000); - setTimeout(check, 300); - }); -} - -function probePort(port: number): Promise<"free" | "running" | "occupied"> { - return new Promise((resolve) => { - const http = require("node:http"); - const req = http.get( - { hostname: "127.0.0.1", port, path: "/api/sessions", timeout: 1200 }, - (res: any) => { - res.resume(); - resolve(res.statusCode === 200 ? "running" : "occupied"); - }, - ); - req.on("error", () => resolve("free")); - req.on("timeout", () => { - req.destroy(); - resolve("free"); - }); - }); -} - -async function startServer(port: number, ctx: any): Promise { - if (serverProcess) { - ctx.ui.notify(`网页服务已在端口 ${serverPort} 运行`, "error"); - return; - } - - const oldPid = readPidFile(); - if (oldPid && !isProcessAlive(oldPid)) { - clearPidFile(); - } - - const state = await probePort(port); - if (state === "running") { - serverPort = port; - ctx.ui.notify(`网页服务已在端口 ${port} 运行`, "info"); - return; - } - if (state === "occupied") { - ctx.ui.notify(`端口 ${port} 已被其他进程占用`, "error"); - return; - } - - const tsxBin = findTsx(); - const serverFile = join(__dirname, "server.ts"); - - if (!existsSync(serverFile)) { - ctx.ui.notify(`服务器文件未找到: ${serverFile}`, "error"); - return; - } - - serverPort = port; - serverProcess = spawn(tsxBin, [serverFile, "--port", String(port)], { - cwd: join(__dirname, "..", "..", ".."), // repo 根目录 - stdio: ["ignore", "inherit", "inherit"], - env: { ...process.env }, - }); - - serverProcess.on("exit", (code) => { - serverProcess = null; - clearPidFile(); - }); - - if (serverProcess.pid) { - writePidFile(serverProcess.pid); - } - - try { - await waitForStartup(port, serverProcess); - } catch (err: any) { - clearPidFile(); - serverProcess = null; - ctx.ui.notify(`网页服务启动失败: ${err.message}`, "error"); - return; - } - - ctx.ui.notify(`网页服务已启动 → http://smallmengya:${port}`, "info"); - - // 显示局域网地址 - const { networkInterfaces } = require("node:os"); - const nets = networkInterfaces(); - for (const name of Object.keys(nets)) { - for (const net of nets[name]) { - if (net.family === "IPv4" && !net.internal) { - ctx.ui.notify(` 局域网: http://${net.address}:${port}`, "info"); - } - } - } -} - -async function stopServer(ctx: any): Promise { - if (!serverProcess) { - const pid = readPidFile(); - if (!pid) { - const state = await probePort(serverPort || 19133); - if (state === "running") { - ctx.ui.notify( - `网页服务正在端口 ${serverPort || 19133} 运行,但当前没有 PID 记录,无法直接停止`, - "error", - ); - return; - } - - ctx.ui.notify("网页服务未运行", "info"); - return; - } - - try { - if (isProcessAlive(pid)) { - process.kill(pid, "SIGTERM"); - ctx.ui.notify(`网页服务已停止(PID ${pid})`, "info"); - } else { - ctx.ui.notify("网页服务未运行,已清理旧 PID", "info"); - } - clearPidFile(); - } catch { - clearPidFile(); - ctx.ui.notify("网页服务未运行", "info"); - } - return; - } - - serverProcess.kill("SIGTERM"); - serverProcess = null; - serverPort = 0; - clearPidFile(); - ctx.ui.notify("网页服务已停止", "info"); -} - -export default function (pi: ExtensionAPI) { - pi.registerCommand("webui", { - description: "通过 /webui on 启动、/webui off 停止 Web 聊天界面", - handler: async (args, ctx) => { - const trimmed = args.trim(); - const [command = "", value = ""] = trimmed.split(/\s+/, 2); - - if (command === "off" || command === "stop" || command === "0") { - await stopServer(ctx); - return; - } - - const portInput = command === "on" ? value : command; - const port = portInput ? parseInt(portInput, 10) : 19133; - if (isNaN(port) || port < 1 || port > 65535) { - ctx.ui.notify(`无效端口: ${trimmed || "(空)"},使用 19133`, "error"); - return; - } - - await startServer(port, ctx); - }, - }); -} diff --git a/.pi/extensions/webui/public/app.js b/.pi/extensions/webui/public/app.js deleted file mode 100644 index c86e6335..00000000 --- a/.pi/extensions/webui/public/app.js +++ /dev/null @@ -1,1115 +0,0 @@ -"use strict"; - -// ─── DOM 引用 ───────────────────────────────────────────────────────────── - -const chat = document.getElementById("chat"); -const input = document.getElementById("input"); -const sendBtn = document.getElementById("sendBtn"); -const abortBtn = document.getElementById("abortBtn"); -const statusDot = document.getElementById("statusDot"); -const statusText = document.getElementById("statusText"); -const statusModel = document.getElementById("statusModel"); -const headerTitle = document.getElementById("headerTitle"); -const headerMeta = document.getElementById("headerMeta"); -const sidebar = document.getElementById("sidebar"); -const overlay = document.getElementById("sidebarOverlay"); -const sessionList = document.getElementById("sessionList"); -const sessionContextBar = document.getElementById("sessionContextBar"); -const sessionContextPrimary = document.getElementById("sessionContextPrimary"); -const sessionContextSecondary = document.getElementById("sessionContextSecondary"); -const modelSelect = document.getElementById("modelSelect"); -const thinkingSelect = document.getElementById("thinkingSelect"); - -// ─── 状态 ───────────────────────────────────────────────────────────────── - -let currentAssistantEl = null; -let assistantBuffer = ""; -let isStreaming = false; -let eventSource = null; -let sessions = []; -let activeSessionPath = null; // 当前加载的会话文件路径 -let backendSessionPath = null; -let sessionActivationPromise = null; -let newSessionPromise = null; -const isTouchLike = window.matchMedia("(max-width: 768px)").matches || navigator.maxTouchPoints > 0; -const sessionCache = new Map(); -let availableModels = []; -let currentModelKey = ""; -let isApplyingModelSettings = false; -const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh"]; - -let sessionDashboardRaf = null; - -/** 与 interactive footer 相同的 token 缩写 */ -function formatFooterTokens(count) { - const n = Number(count) || 0; - if (n <= 0) return "0"; - if (n < 1000) return String(Math.round(n)); - if (n < 10000) return `${(n / 1000).toFixed(1)}k`; - if (n < 1_000_000) return `${Math.round(n / 1000)}k`; - if (n < 10_000_000) return `${(n / 1_000_000).toFixed(1)}M`; - return `${Math.round(n / 1_000_000)}M`; -} - -function renderSessionContextBar(data) { - if (!sessionContextBar || !sessionContextPrimary || !sessionContextSecondary) return; - if (!data || data.error || !data.stats || !data.model) { - sessionContextBar.hidden = true; - return; - } - - sessionContextBar.hidden = false; - - const tok = data.stats.tokens || {}; - const parts = []; - if (tok.input) parts.push(`↑${formatFooterTokens(tok.input)}`); - if (tok.output) parts.push(`↓${formatFooterTokens(tok.output)}`); - if (tok.cacheRead) parts.push(`R${formatFooterTokens(tok.cacheRead)}`); - if (tok.cacheWrite) parts.push(`W${formatFooterTokens(tok.cacheWrite)}`); - - const costNum = Number(data.stats.cost ?? 0); - parts.push(`$${costNum.toFixed(3)}`); - - const cx = data.stats.contextUsage; - const cw = cx?.contextWindow ?? data.model.contextWindow ?? 0; - const pctRaw = cx?.percent; - const pctStr = pctRaw != null ? Number(pctRaw).toFixed(1) : "?"; - const pctNum = pctRaw != null ? Number(pctRaw) : null; - const autoInd = data.autoCompactionEnabled ? " (auto)" : ""; - const ctxTxt = - pctStr === "?" && cw - ? `?/${formatFooterTokens(cw)}${autoInd}` - : `${pctStr}%/${formatFooterTokens(cw)}${autoInd}`; - - sessionContextPrimary.textContent = ""; - const row = document.createElement("div"); - row.className = "session-context-row"; - - const left = document.createElement("div"); - left.className = "session-context-left"; - left.appendChild(document.createTextNode(`${parts.join(" ")} `)); - const ctxSpan = document.createElement("span"); - ctxSpan.textContent = ctxTxt; - if (pctNum != null) { - if (pctNum > 90) ctxSpan.classList.add("ctx-danger"); - else if (pctNum > 70) ctxSpan.classList.add("ctx-warn"); - } - left.appendChild(ctxSpan); - - row.appendChild(left); - sessionContextPrimary.appendChild(row); - - const liveStreaming = isStreaming || data.isStreaming; - let sub = ""; - if (data.isCompacting) sub = "⚡ 正在整理上下文…"; - else if (liveStreaming) sub = "⋯ 回复中…"; - else if (data.turnIndex > 0) sub = `✓ Turn ${data.turnIndex} complete`; - sessionContextSecondary.textContent = sub; -} - -function modelKey(model) { - return model ? `${model.provider}/${model.id}` : ""; -} - -function modelLabel(model) { - if (!model) return ""; - return `${model.provider}/${model.id}`; -} - -function getAvailableThinkingLevels(model) { - if (!model?.reasoning) return ["off"]; - return thinkingLevels.filter((level) => { - const mapped = model.thinkingLevelMap?.[level]; - if (mapped === null) return false; - if (level === "xhigh") return mapped !== undefined; - return true; - }); -} - -function syncThinkingOptions(model, currentLevel) { - if (!thinkingSelect) return; - - const levels = getAvailableThinkingLevels(model); - const nextOptionsKey = levels.join("|"); - if (thinkingSelect.dataset.optionsKey !== nextOptionsKey) { - thinkingSelect.innerHTML = ""; - for (const level of levels) { - const option = document.createElement("option"); - option.value = level; - option.textContent = level; - thinkingSelect.appendChild(option); - } - thinkingSelect.dataset.optionsKey = nextOptionsKey; - } - - const effectiveLevel = levels.includes(currentLevel) ? currentLevel : levels[0] || "off"; - if (thinkingSelect.value !== effectiveLevel) { - thinkingSelect.value = effectiveLevel; - } -} - -function updateModelControlsFromState(data) { - if (!modelSelect || !thinkingSelect || !data || data.error) return; - - currentModelKey = modelKey(data.model); - if (currentModelKey && modelSelect.value !== currentModelKey) { - modelSelect.value = currentModelKey; - } - - const thinking = data.thinkingLevel || "off"; - syncThinkingOptions(data.model, thinking); - - const supportsThinking = !!data.model?.reasoning; - thinkingSelect.disabled = !supportsThinking || isStreaming || isApplyingModelSettings; - modelSelect.disabled = isStreaming || isApplyingModelSettings; -} - -async function loadAvailableModels() { - if (!modelSelect) return; - try { - const res = await fetch("/api/models"); - const data = await res.json(); - if (!res.ok || data.error) throw new Error(data.error || res.statusText); - availableModels = data.models || []; - modelSelect.innerHTML = ""; - for (const model of availableModels) { - const option = document.createElement("option"); - option.value = modelKey(model); - option.textContent = modelLabel(model); - modelSelect.appendChild(option); - } - if (currentModelKey) modelSelect.value = currentModelKey; - } catch (err) { - modelSelect.innerHTML = ``; - addMessage("system", `模型列表加载失败: ${err.message}`); - } -} - -async function applyModelSelection() { - if (!modelSelect || !modelSelect.value || isApplyingModelSettings) return; - const [provider, ...idParts] = modelSelect.value.split("/"); - const modelId = idParts.join("/"); - if (!provider || !modelId) return; - - isApplyingModelSettings = true; - modelSelect.disabled = true; - thinkingSelect.disabled = true; - setLoadingState("正在切换模型"); - try { - const res = await fetch("/api/model", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, modelId }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) throw new Error(data.error || res.statusText); - await fetchSessionState(); - setLoadingState("模型已切换"); - setTimeout(() => setLoadingState(""), 1000); - } catch (err) { - setLoadingState(""); - addMessage("system", `切换模型失败: ${err.message}`); - await fetchSessionState(); - } finally { - isApplyingModelSettings = false; - await fetchSessionState(); - } -} - -async function applyThinkingSelection() { - if (!thinkingSelect || isApplyingModelSettings) return; - isApplyingModelSettings = true; - modelSelect.disabled = true; - thinkingSelect.disabled = true; - setLoadingState("正在设置推理强度"); - try { - const res = await fetch("/api/thinking", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ level: thinkingSelect.value }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) throw new Error(data.error || res.statusText); - await fetchSessionState(); - setLoadingState("推理强度已设置"); - setTimeout(() => setLoadingState(""), 1000); - } catch (err) { - setLoadingState(""); - addMessage("system", `设置推理强度失败: ${err.message}`); - await fetchSessionState(); - } finally { - isApplyingModelSettings = false; - await fetchSessionState(); - } -} - -function scheduleSessionDashboardRefresh() { - if (sessionDashboardRaf != null) return; - sessionDashboardRaf = requestAnimationFrame(() => { - sessionDashboardRaf = null; - fetchSessionState(); - }); -} - -function syncViewportHeight() { - const viewportHeight = window.visualViewport?.height || window.innerHeight; - document.documentElement.style.setProperty("--app-height", `${viewportHeight}px`); -} - -function formatSessionTitle(title) { - const text = typeof title === "string" ? title.trim() : ""; - return text || "未命名"; -} - -/** 与服务端一致的机器标签(会话 id、纯 hex 段等),不写进界面标题 */ -function isMachineSessionLabel(text, headerId) { - const t = (text ?? "").trim(); - if (!t) return true; - if (headerId && t === headerId) return true; - if (/^[0-9a-f]{8,}$/i.test(t)) return true; - if (/^[0-9]{10,}$/.test(t)) return true; - return false; -} - -/** 首句截取(侧边栏兜底与顶栏回填,需与 server.ts 保持一致逻辑) */ -function titleFromFirstUserMessage(text, maxChars = 56) { - const cleaned = String(text ?? "").replace(/\s+/g, " ").trim(); - if (!cleaned) return ""; - const sentenceMatch = cleaned.match(/^(.+?[。!?.!?])(\s|$)/); - let candidate = - sentenceMatch && sentenceMatch[1] ? sentenceMatch[1].trim() : cleaned; - if (candidate.length > maxChars) { - candidate = `${candidate.slice(0, maxChars).trimEnd()}…`; - } - return candidate; -} - -async function activateSessionBackend(path) { - const activateRes = await fetch("/api/sessions/activate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path }), - }); - const activateData = await activateRes.json().catch(() => ({})); - - if (activateRes.ok && !activateData.error) { - backendSessionPath = path; - return; - } - - const activateError = activateData.error || activateRes.statusText || "Unknown error"; - if (activateRes.status !== 404 && !/not found/i.test(activateError)) { - throw new Error(activateError); - } - - const fallbackRes = await fetch("/api/sessions/load", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path }), - }); - const fallbackData = await fallbackRes.json().catch(() => ({})); - if (!fallbackRes.ok || fallbackData.error) { - throw new Error(fallbackData.error || fallbackRes.statusText); - } - - backendSessionPath = path; -} - -// ─── 输入框自动伸缩 ─────────────────────────────────────────────────── - -input.addEventListener("input", () => { - input.style.height = "auto"; - input.style.height = Math.min(input.scrollHeight, 150) + "px"; -}); - -input.addEventListener("keydown", (e) => { - if (e.isComposing) return; - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - send(); - } -}); - -input.addEventListener("beforeinput", (e) => { - if (isTouchLike && e.inputType === "insertLineBreak") { - e.preventDefault(); - send(); - } -}); - -// ─── 侧边栏切换 ──────────────────────────────────────────────────────── - -function toggleSidebar() { - const open = sidebar.classList.toggle("open"); - overlay.classList.toggle("open", open); -} - -window.addEventListener("resize", () => { - syncViewportHeight(); - if (window.innerWidth > 768) { - sidebar.classList.remove("open"); - overlay.classList.remove("open"); - } -}); - -if (window.visualViewport) { - window.visualViewport.addEventListener("resize", syncViewportHeight); - window.visualViewport.addEventListener("scroll", syncViewportHeight); -} - -window.addEventListener("orientationchange", syncViewportHeight); - -// ─── 状态更新 ──────────────────────────────────────────────────────────── - -function setStatus(connected, streaming) { - if (connected) { - statusDot.className = streaming ? "status-dot streaming" : "status-dot connected"; - statusText.textContent = streaming ? "输入中..." : "就绪"; - headerMeta.textContent = streaming ? "回复中" : ""; - } else { - statusDot.className = "status-dot disconnected"; - statusText.textContent = "未连接"; - headerMeta.textContent = "未连接"; - } - abortBtn.style.display = streaming ? "flex" : "none"; - sendBtn.disabled = streaming; -} - -// ─── Markdown 渲染 ────────────────────────────────────────────────────── - -function escapeMarkdownCode(raw) { - return String(raw) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); -} - -function renderAssistantCodeBlock(token) { - const langSlug = (((token.lang || "").match(/^\S+/) || [""])[0]).trim().toLowerCase(); - const codeRaw = String(token.text ?? "").replace(/\n+$/, ""); - /** marked:escaped 已为 HTML 字面量片段,Highlighter 跳过 */ - const skipHl = !!token.escaped; - - let innerHtml; - if (skipHl) { - innerHtml = codeRaw; - } else { - try { - const hl = - typeof globalThis.hljs !== "undefined" - ? globalThis.hljs - : typeof hljs !== "undefined" - ? hljs - : null; - if (hl && typeof hl.highlight === "function") { - if (langSlug && hl.getLanguage(langSlug)) { - innerHtml = hl.highlight(codeRaw, { language: langSlug }).value; - } else { - innerHtml = hl.highlightAuto(codeRaw).value; - } - } else { - innerHtml = escapeMarkdownCode(codeRaw); - } - } catch { - innerHtml = escapeMarkdownCode(codeRaw); - } - } - - const safeLangSlug = /^[a-z][a-z0-9_-]*$/i.test(langSlug) ? langSlug : ""; - const codeClass = - safeLangSlug ? `hljs language-${safeLangSlug}` : "hljs"; - return `
${innerHtml}
\n`; -} - -(function initMarkedCodeHighlight() { - if (typeof marked === "undefined" || typeof marked.use !== "function") return; - marked.use({ - renderer: { - code: renderAssistantCodeBlock, - }, - }); -})(); - -function renderMarkdown(text) { - if (!text) return ""; - return marked.parse(text, { breaks: true, gfm: true }); -} - -// ─── 消息渲染 ─────────────────────────────────────────────────────────── - -function removeEmptyState() { - const el = chat.querySelector(".empty-state"); - if (el) el.remove(); -} - -function scrollToBottom() { - chat.scrollTop = chat.scrollHeight; -} - -function findToolCallMessageEl(toolCallId) { - if (toolCallId == null || toolCallId === "") return null; - const id = String(toolCallId); - for (const el of chat.querySelectorAll(".msg.tool_call")) { - if (el.dataset.toolCallId === id) return el; - } - return null; -} - -/** summary 单行:图标 + 工具名 + 短 id(不展开整条 JSON) */ -function deriveToolCallSummary(fullText) { - const raw = String(fullText || "").trim(); - if (!raw) return "tool"; - let icon = ""; - if (raw.startsWith("🔧")) icon = "🔧"; - else if (raw.startsWith("✅")) icon = "✅"; - else if (raw.startsWith("❌")) icon = "❌"; - const rest = icon ? raw.slice(icon.length).trimStart() : raw; - const idLine = rest.split("\n").find((l) => l.trimStart().startsWith("# ")); - const idPart = idLine ? idLine.replace(/^\s*#\s*/, "").trim().split(/\s/)[0] : ""; - const oneLine = rest.replace(/\s+/g, " "); - const nameMatch = oneLine.match(/(\w+)\s*\(/); - const name = nameMatch ? nameMatch[1] : "tool"; - let label = idPart ? `${name} · ${idPart}` : name; - if (icon) label = `${icon} ${label}`; - return label; -} - -function buildToolCallCollapsible(fullText) { - const details = document.createElement("details"); - details.className = "tool-call-collapsible"; - const summary = document.createElement("summary"); - summary.className = "tool-call-summary"; - const sumSpan = document.createElement("span"); - sumSpan.className = "tool-call-summary-text"; - sumSpan.textContent = deriveToolCallSummary(fullText); - summary.appendChild(sumSpan); - const detail = document.createElement("div"); - detail.className = "tool-call-detail"; - const pre = document.createElement("pre"); - pre.className = "tool-call-detail-pre"; - pre.textContent = fullText; - detail.appendChild(pre); - details.appendChild(summary); - details.appendChild(detail); - return details; -} - -function getToolCallFullText(el) { - const pre = el.querySelector(".tool-call-detail-pre"); - return pre ? pre.textContent : el.textContent; -} - -function syncToolCallBody(el, fullText) { - const txt = fullText ?? ""; - if (!el.querySelector(".tool-call-collapsible")) { - el.textContent = ""; - el.appendChild(buildToolCallCollapsible(txt)); - return; - } - const pre = el.querySelector(".tool-call-detail-pre"); - const sum = el.querySelector(".tool-call-summary-text"); - if (pre) pre.textContent = txt; - if (sum) sum.textContent = deriveToolCallSummary(txt); -} - -function appendToolCallMessage(text, toolCallId) { - removeEmptyState(); - const el = document.createElement("div"); - el.className = "msg tool_call"; - if (toolCallId != null && toolCallId !== "") el.dataset.toolCallId = String(toolCallId); - el.appendChild(buildToolCallCollapsible(text || "")); - chat.appendChild(el); - scrollToBottom(); - return el; -} - -/** 助手消息落盘时的工具摘要;若该行已在 tool_execution 中更新则不再覆盖 */ -function upsertToolCallSummary(tc) { - const tid = tc.toolCallId || tc.id; - const text = formatToolCall(tc); - const existing = findToolCallMessageEl(tid); - if (existing) { - const cur = getToolCallFullText(existing).trimStart(); - const running = cur.startsWith("🔧") || cur.startsWith("✅") || cur.startsWith("❌"); - if (!running) syncToolCallBody(existing, text); - if (tid && !existing.dataset.toolCallId) existing.dataset.toolCallId = tid; - return; - } - appendToolCallMessage(text, tid); -} - -function addMessage(role, content, extraClass) { - removeEmptyState(); - - if (role === "assistant" && extraClass === "streaming") { - if (!currentAssistantEl) { - currentAssistantEl = document.createElement("div"); - currentAssistantEl.className = "msg assistant streaming"; - chat.appendChild(currentAssistantEl); - } - const raw = content || assistantBuffer; - currentAssistantEl.innerHTML = renderMarkdown(raw || ""); - scrollToBottom(); - return currentAssistantEl; - } - - const el = document.createElement("div"); - el.className = `msg ${role}`; - if (extraClass) el.classList.add(extraClass); - - if (role === "assistant") { - el.innerHTML = renderMarkdown(content || ""); - } else { - el.textContent = content; - } - - chat.appendChild(el); - if (role !== "system") scrollToBottom(); - return el; -} - -function setLoadingState(text) { - headerMeta.textContent = text || ""; -} - -function finalizeAssistantMessage(content) { - const trimmed = - typeof content === "string" - ? content.trim() - : content - ? String(content).trim() - : ""; - if (currentAssistantEl) { - currentAssistantEl.classList.remove("streaming"); - if (trimmed) { - currentAssistantEl.innerHTML = renderMarkdown(trimmed); - } else { - currentAssistantEl.remove(); - } - currentAssistantEl = null; - } else if (trimmed) { - addMessage("assistant", trimmed); - } - assistantBuffer = ""; -} - -function displayMessages(messages, isHistory = false) { - for (const m of messages) { - if (m.role === "user") { - addMessage("user", extractContent(m.content)); - } else if (m.role === "assistant") { - const text = extractContent(m.content); - const toolCalls = getToolCalls(m.content); - if (text) addMessage("assistant", text); - for (const tc of toolCalls) upsertToolCallSummary(tc); - } - } -} - -function extractContent(content) { - if (!content) return ""; - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content - .filter(c => c.type === "text") - .map(c => c.text) - .join(""); - } - return String(content); -} - -function getToolCalls(content) { - if (!Array.isArray(content)) return []; - return content.filter(c => c.type === "toolCall"); -} - -function formatToolCall(tc) { - const name = tc.toolName || tc.name || "tool"; - // pi 会话块为 { type, id, name, arguments };部分来源用 args / input - const args = tc.arguments ?? tc.args ?? tc.input ?? {}; - let text = `${name}(${JSON.stringify(args)})`; - const shortId = tc.toolCallId || tc.id; - if (shortId) text = `# ${String(shortId).slice(0, 8)}\n` + text; - return text; -} - -function formatToolResult(tr) { - let text = ""; - if (typeof tr.content === "string") text = tr.content; - else if (Array.isArray(tr.content)) { - text = tr.content.map(c => (c && c.text) || JSON.stringify(c)).join("\n"); - } else if (tr.content && tr.content.text) text = tr.content.text; - else text = JSON.stringify(tr.content || tr, null, 2); - return text.length > 800 ? text.slice(0, 800) + "\n… (已截断)" : text; -} - -function escHtml(s) { - const div = document.createElement("div"); - div.textContent = s; - return div.innerHTML; -} - -// ─── 会话列表 ─────────────────────────────────────────────────────────── - -async function loadSessions() { - sessionList.innerHTML = '
加载中...
'; - try { - const res = await fetch("/api/sessions"); - const data = await res.json(); - sessions = data.sessions || []; - renderSessionList(); - } catch (err) { - sessionList.innerHTML = `
加载失败: ${err.message}
`; - } -} - -function renderSessionList() { - if (!sessions.length) { - sessionList.innerHTML = '
暂无历史会话
'; - return; - } - - sessionList.innerHTML = sessions.map((s) => { - const timeAgo = formatTimeAgo(s.modified || s.created); - const isActive = activeSessionPath === s.path; - const pathAttr = escHtml(s.path); - const title = formatSessionTitle(s.name); - return `
- - -
`; - }).join(""); -} - -async function deleteSession(event, path) { - event?.stopPropagation(); - if (isStreaming) { - addMessage("system", "正在回复中,暂不能删除会话"); - return; - } - - const row = sessions.find((s) => s.path === path); - const title = formatSessionTitle(row?.name || row?.firstMessage || "该会话"); - if (!confirm(`删除会话「${title}」?此操作不可恢复。`)) return; - - try { - const res = await fetch("/api/sessions/delete", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) throw new Error(data.error || res.statusText); - - sessionCache.delete(path); - sessions = sessions.filter((s) => s.path !== path); - if (activeSessionPath === path) { - clearChat(); - backendSessionPath = null; - sessionActivationPromise = null; - } else { - renderSessionList(); - } - } catch (err) { - addMessage("system", `删除会话失败: ${err.message}`); - } -} - -function formatTimeAgo(isoStr) { - const d = new Date(isoStr); - const now = new Date(); - const diffMs = now - d; - const mins = Math.floor(diffMs / 60000); - if (mins < 1) return "刚刚"; - if (mins < 60) return `${mins} 分钟前`; - const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours} 小时前`; - const days = Math.floor(hours / 24); - if (days < 30) return `${days} 天前`; - return d.toLocaleDateString("zh-CN", { month: "short", day: "numeric" }); -} - -// ─── 加载会话 ─────────────────────────────────────────────────────────── - -async function loadSession(path) { - toggleSidebar(); // close sidebar on mobile - activeSessionPath = path; - renderSessionList(); - setLoadingState("加载中..."); - - try { - let payload = sessionCache.get(path); - const poisonName = (p) => - typeof p?.session?.name === "string" && - p.session.name.trim() && - isMachineSessionLabel(p.session.name.trim(), String(p.session?.id ?? "")); - if (payload && poisonName(payload)) { - sessionCache.delete(path); - payload = null; - } - if (!payload) { - const res = await fetch("/api/sessions/history", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path }), - }); - payload = await res.json(); - if (payload.error) throw new Error(payload.error); - sessionCache.set(path, payload); - } - - // 先把历史内容立即渲染出来,后台再同步会话上下文 - chat.innerHTML = ""; - currentAssistantEl = null; - assistantBuffer = ""; - const sid = payload.session?.id != null ? String(payload.session.id) : ""; - let headerLabel = ""; - if ( - typeof payload.session?.name === "string" && - payload.session.name.trim() && - !isMachineSessionLabel(payload.session.name.trim(), sid) - ) { - headerLabel = payload.session.name.trim(); - } - if (!headerLabel) { - for (const m of payload.messages || []) { - if (m.role === "user") { - const fb = titleFromFirstUserMessage(extractContent(m.content)); - if (fb) { - headerLabel = fb; - break; - } - } - } - } - headerTitle.textContent = formatSessionTitle(headerLabel); - displayMessages(payload.messages, true); - scrollToBottom(); - // 历史已可见,勿再占用顶栏「加载中…」(后台激活可能较慢) - setLoadingState(""); - scheduleSessionDashboardRefresh(); - - const activationPromise = (async () => { - try { - await activateSessionBackend(path); - } catch (err) { - addMessage("system", `会话同步失败: ${err.message}`); - throw err; - } finally { - if (sessionActivationPromise === activationPromise) { - sessionActivationPromise = null; - } - } - })(); - - sessionActivationPromise = activationPromise; - activationPromise.catch(() => {}); - } catch (err) { - setLoadingState(""); - addMessage("system", `加载会话失败: ${err.message}`); - } -} - -// ─── SSE 事件流 ──────────────────────────────────────────────────────── - -function connectSSE() { - if (eventSource) eventSource.close(); - - eventSource = new EventSource("/api/events"); - - eventSource.onopen = () => { - setStatus(true, isStreaming); - }; - - eventSource.onmessage = (e) => { - try { - const ev = JSON.parse(e.data); - handleEvent(ev); - } catch { /* ignore */ } - }; - - eventSource.onerror = () => { - setStatus(false, false); - setTimeout(connectSSE, 2000); - }; -} - -function handleEvent(ev) { - switch (ev.type) { - case "connected": - setStatus(true, false); - scheduleSessionDashboardRefresh(); - break; - - case "agent_start": - isStreaming = true; - setStatus(true, true); - assistantBuffer = ""; - currentAssistantEl = null; - scheduleSessionDashboardRefresh(); - break; - - case "agent_end": - isStreaming = false; - setStatus(true, false); - if (currentAssistantEl) { - currentAssistantEl.classList.remove("streaming"); - currentAssistantEl = null; - } - assistantBuffer = ""; - if (backendSessionPath) { - sessionCache.delete(backendSessionPath); - } - // 后台刷新会话列表(可能有新消息产生) - setTimeout(loadSessions, 1000); - // 获取当前会话状态以显示模型信息 - fetchSessionState(); - break; - - case "message_start": { - const m = ev.message; - if (m.role === "user") { - const text = extractContent(m.content); - addMessage("user", text); - } - break; - } - - case "message_update": { - const m = ev.message; - if (m.role === "assistant") { - const text = extractContent(m.content); - assistantBuffer = text; - addMessage("assistant", text, "streaming"); - } - break; - } - - case "message_end": { - const m = ev.message; - if (m.role === "assistant") { - const text = extractContent(m.content); - const toolCalls = getToolCalls(m.content); - for (const tc of toolCalls) upsertToolCallSummary(tc); - finalizeAssistantMessage(text); - scheduleSessionDashboardRefresh(); - } - break; - } - - case "tool_execution_start": { - const tid = ev.toolCallId; - const label = `🔧 ${ev.toolName}(${JSON.stringify(ev.args ?? {})})`; - const existing = findToolCallMessageEl(tid); - if (existing) { - syncToolCallBody(existing, label); - } else { - appendToolCallMessage(label, tid); - } - break; - } - - case "tool_execution_update": { - updateLastToolCall(ev); - break; - } - - case "tool_execution_end": { - updateLastToolCall(ev, ev.isError); - if (ev.isError) { - addMessage("system", `工具 ${ev.toolName} 执行出错`); - } - break; - } - } -} - -function updateLastToolCall(ev, isError) { - const els = chat.querySelectorAll(".msg.tool_call"); - const last = findToolCallMessageEl(ev.toolCallId) ?? els[els.length - 1]; - if (!last) return; - const icon = isError === undefined ? "🔧" : isError ? "❌" : "✅"; - let detail = ""; - if (ev.partialResult?.content) { - detail = "\n" + formatToolResult(ev.partialResult); - } - let full = ""; - if (isError !== undefined) { - const resultText = ev.result?.content - ? "\n" + formatToolResult(ev.result) - : ""; - full = `${icon} ${ev.toolName}${resultText || detail}`; - } else { - full = `${icon} ${ev.toolName}${detail}`; - } - syncToolCallBody(last, full); -} - -// ─── API 调用 ─────────────────────────────────────────────────────────── - -async function send() { - const text = input.value.trim(); - if (!text || isStreaming) return; - - if (newSessionPromise) { - try { - await newSessionPromise; - } catch { - sendBtn.disabled = false; - return; - } - } - - if (activeSessionPath && backendSessionPath !== activeSessionPath) { - if (sessionActivationPromise) { - try { - await sessionActivationPromise; - } catch { - return; - } - } else { - try { - await activateSessionBackend(activeSessionPath); - } catch (err) { - addMessage("system", `切换会话失败: ${err.message}`); - sendBtn.disabled = false; - return; - } - } - } - - input.value = ""; - input.style.height = "auto"; - sendBtn.disabled = true; - - try { - const res = await fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: text }), - }); - if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - addMessage("system", `发送失败: ${err.error}`); - sendBtn.disabled = false; - } - } catch (err) { - addMessage("system", `网络错误: ${err.message}`); - sendBtn.disabled = false; - } - - if (isTouchLike) { - input.blur(); - } -} - -async function abortStream() { - if (!isStreaming) return; - try { - await fetch("/api/abort", { method: "POST" }); - } catch { /* ignore */ } -} - -function clearChat(title = "聊天") { - chat.innerHTML = `
-
- -
-

发送一条消息开始对话

-

按 Enter 发送 · Shift+Enter 换行

-
`; - currentAssistantEl = null; - assistantBuffer = ""; - activeSessionPath = null; - headerTitle.textContent = title; - if (sessionContextBar) sessionContextBar.hidden = true; - renderSessionList(); -} - -function newSession() { - if (isStreaming) { - addMessage("system", "正在回复中,暂不能创建新会话"); - return; - } - if (newSessionPromise) return; - - statusText.textContent = "新建中..."; - setLoadingState("正在创建新会话"); - backendSessionPath = null; - activeSessionPath = null; - sessionActivationPromise = null; - clearChat("新会话"); - - newSessionPromise = (async () => { - const res = await fetch("/api/new-session", { method: "POST" }); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) throw new Error(data.error || res.statusText); - - backendSessionPath = data.sessionFile || null; - setStatus(true, false); - setLoadingState("已创建"); - setTimeout(() => setLoadingState(""), 1200); - await fetchSessionState(); - })(); - - newSessionPromise - .catch((err) => { - setStatus(true, false); - setLoadingState(""); - addMessage("system", `创建新会话失败: ${err.message}`); - }) - .finally(() => { - newSessionPromise = null; - }); -} - -async function fetchSessionState() { - try { - const res = await fetch("/api/session-state"); - if (!res.ok) return; - const data = await res.json(); - if (data.error) return; - - renderSessionContextBar(data); - updateModelControlsFromState(data); - if (statusModel) statusModel.textContent = ""; - - if (!(!activeSessionPath || backendSessionPath === activeSessionPath)) return; - - const sid = data.sessionId != null ? String(data.sessionId) : ""; - const pathKey = backendSessionPath || activeSessionPath; - const row = pathKey ? sessions.find((s) => s.path === pathKey) : null; - - if (data.sessionName && !isMachineSessionLabel(data.sessionName, sid)) { - headerTitle.textContent = formatSessionTitle(data.sessionName); - } else if (row) { - if (row.name && !isMachineSessionLabel(row.name, sid)) { - headerTitle.textContent = formatSessionTitle(row.name); - } else if (row.firstMessage && row.firstMessage !== "(空)") { - headerTitle.textContent = formatSessionTitle( - titleFromFirstUserMessage(row.firstMessage), - ); - } - } - } catch { /* ignore */ } -} - -// ─── 初始化 ───────────────────────────────────────────────────────────── - -syncViewportHeight(); -connectSSE(); -loadSessions(); -loadAvailableModels(); -if (modelSelect) modelSelect.addEventListener("change", applyModelSelection); -if (thinkingSelect) thinkingSelect.addEventListener("change", applyThinkingSelection); -requestAnimationFrame(() => { - requestAnimationFrame(() => { - fetchSessionState(); - }); -}); diff --git a/.pi/extensions/webui/public/index.html b/.pi/extensions/webui/public/index.html deleted file mode 100644 index 9d35ce45..00000000 --- a/.pi/extensions/webui/public/index.html +++ /dev/null @@ -1,125 +0,0 @@ - - - - - -萌小芽 - - - - - - - - - - - - -
- - - - -
- - - - -
-
-
- -
-

发送一条消息开始对话

-

按 Enter 发送 · Shift+Enter 换行

-
-
- - -
-
- - -
-
-
-
- - - - - - diff --git a/.pi/extensions/webui/public/logo.png b/.pi/extensions/webui/public/logo.png deleted file mode 100644 index 2914e6d6..00000000 Binary files a/.pi/extensions/webui/public/logo.png and /dev/null differ diff --git a/.pi/extensions/webui/public/marked.umd.js b/.pi/extensions/webui/public/marked.umd.js deleted file mode 100644 index 5ca252d9..00000000 --- a/.pi/extensions/webui/public/marked.umd.js +++ /dev/null @@ -1,2213 +0,0 @@ -/** - * marked v15.0.12 - a markdown parser - * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed) - * https://github.com/markedjs/marked - */ - -/** - * DO NOT EDIT THIS FILE - * The code in this file is generated from files in ./src/ - */ -(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; -"use strict"; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// src/marked.ts -var marked_exports = {}; -__export(marked_exports, { - Hooks: () => _Hooks, - Lexer: () => _Lexer, - Marked: () => Marked, - Parser: () => _Parser, - Renderer: () => _Renderer, - TextRenderer: () => _TextRenderer, - Tokenizer: () => _Tokenizer, - defaults: () => _defaults, - getDefaults: () => _getDefaults, - lexer: () => lexer, - marked: () => marked, - options: () => options, - parse: () => parse, - parseInline: () => parseInline, - parser: () => parser, - setOptions: () => setOptions, - use: () => use, - walkTokens: () => walkTokens -}); -module.exports = __toCommonJS(marked_exports); - -// src/defaults.ts -function _getDefaults() { - return { - async: false, - breaks: false, - extensions: null, - gfm: true, - hooks: null, - pedantic: false, - renderer: null, - silent: false, - tokenizer: null, - walkTokens: null - }; -} -var _defaults = _getDefaults(); -function changeDefaults(newDefaults) { - _defaults = newDefaults; -} - -// src/rules.ts -var noopTest = { exec: () => null }; -function edit(regex, opt = "") { - let source = typeof regex === "string" ? regex : regex.source; - const obj = { - replace: (name, val) => { - let valSource = typeof val === "string" ? val : val.source; - valSource = valSource.replace(other.caret, "$1"); - source = source.replace(name, valSource); - return obj; - }, - getRegex: () => { - return new RegExp(source, opt); - } - }; - return obj; -} -var other = { - codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, - outputLinkReplace: /\\([\[\]])/g, - indentCodeCompensation: /^(\s+)(?:```)/, - beginningSpace: /^\s+/, - endingHash: /#$/, - startingSpaceChar: /^ /, - endingSpaceChar: / $/, - nonSpaceChar: /[^ ]/, - newLineCharGlobal: /\n/g, - tabCharGlobal: /\t/g, - multipleSpaceGlobal: /\s+/g, - blankLine: /^[ \t]*$/, - doubleBlankLine: /\n[ \t]*\n[ \t]*$/, - blockquoteStart: /^ {0,3}>/, - blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, - blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, - listReplaceTabs: /^\t+/, - listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, - listIsTask: /^\[[ xX]\] /, - listReplaceTask: /^\[[ xX]\] +/, - anyLine: /\n.*\n/, - hrefBrackets: /^<(.*)>$/, - tableDelimiter: /[:|]/, - tableAlignChars: /^\||\| *$/g, - tableRowBlankLine: /\n[ \t]*$/, - tableAlignRight: /^ *-+: *$/, - tableAlignCenter: /^ *:-+: *$/, - tableAlignLeft: /^ *:-+ *$/, - startATag: /^/i, - startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, - endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, - startAngleBracket: /^$/, - pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, - unicodeAlphaNumeric: /[\p{L}\p{N}]/u, - escapeTest: /[&<>"']/, - escapeReplace: /[&<>"']/g, - escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, - escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, - unescapeTest: /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, - caret: /(^|[^\[])\^/g, - percentDecode: /%25/g, - findPipe: /\|/g, - splitPipe: / \|/, - slashPipe: /\\\|/g, - carriageReturn: /\r\n|\r/g, - spaceLine: /^ +$/gm, - notSpaceStart: /^\S*/, - endingNewline: /\n$/, - listItemRegex: (bull) => new RegExp(`^( {0,3}${bull})((?:[ ][^\\n]*)?(?:\\n|$))`), - nextBulletRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`), - hrRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`), - fencesBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`), - headingBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`), - htmlBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}<(?:[a-z].*>|!--)`, "i") -}; -var newline = /^(?:[ \t]*(?:\n|$))+/; -var blockCode = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/; -var fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; -var hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; -var heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; -var bullet = /(?:[*+-]|\d{1,9}[.)])/; -var lheadingCore = /^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/; -var lheading = edit(lheadingCore).replace(/bull/g, bullet).replace(/blockCode/g, /(?: {4}| {0,3}\t)/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}/).replace(/html/g, / {0,3}<[^\n>]+>\n/).replace(/\|table/g, "").getRegex(); -var lheadingGfm = edit(lheadingCore).replace(/bull/g, bullet).replace(/blockCode/g, /(?: {4}| {0,3}\t)/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}/).replace(/html/g, / {0,3}<[^\n>]+>\n/).replace(/table/g, / {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(); -var _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/; -var blockText = /^[^\n]+/; -var _blockLabel = /(?!\s*\])(?:\\.|[^\[\]\\])+/; -var def = edit(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label", _blockLabel).replace("title", /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(); -var list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g, bullet).getRegex(); -var _tag = "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul"; -var _comment = /|$))/; -var html = edit( - "^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))", - "i" -).replace("comment", _comment).replace("tag", _tag).replace("attribute", / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(); -var paragraph = edit(_paragraph).replace("hr", hr).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("|table", "").replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", _tag).getRegex(); -var blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph", paragraph).getRegex(); -var blockNormal = { - blockquote, - code: blockCode, - def, - fences, - heading, - hr, - html, - lheading, - list, - newline, - paragraph, - table: noopTest, - text: blockText -}; -var gfmTable = edit( - "^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)" -).replace("hr", hr).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("blockquote", " {0,3}>").replace("code", "(?: {4}| {0,3} )[^\\n]").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", _tag).getRegex(); -var blockGfm = { - ...blockNormal, - lheading: lheadingGfm, - table: gfmTable, - paragraph: edit(_paragraph).replace("hr", hr).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("table", gfmTable).replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", _tag).getRegex() -}; -var blockPedantic = { - ...blockNormal, - html: edit( - `^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))` - ).replace("comment", _comment).replace(/tag/g, "(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(), - def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, - heading: /^(#{1,6})(.*)(?:\n+|$)/, - fences: noopTest, - // fences not supported - lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, - paragraph: edit(_paragraph).replace("hr", hr).replace("heading", " *#{1,6} *[^\n]").replace("lheading", lheading).replace("|table", "").replace("blockquote", " {0,3}>").replace("|fences", "").replace("|list", "").replace("|html", "").replace("|tag", "").getRegex() -}; -var escape = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; -var inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; -var br = /^( {2,}|\\)\n(?!\s*$)/; -var inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g; -var emStrongLDelimCore = /^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/; -var emStrongLDelim = edit(emStrongLDelimCore, "u").replace(/punct/g, _punctuation).getRegex(); -var emStrongLDelimGfm = edit(emStrongLDelimCore, "u").replace(/punct/g, _punctuationGfmStrongEm).getRegex(); -var emStrongRDelimAstCore = "^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)"; -var emStrongRDelimAst = edit(emStrongRDelimAstCore, "gu").replace(/notPunctSpace/g, _notPunctuationOrSpace).replace(/punctSpace/g, _punctuationOrSpace).replace(/punct/g, _punctuation).getRegex(); -var emStrongRDelimAstGfm = edit(emStrongRDelimAstCore, "gu").replace(/notPunctSpace/g, _notPunctuationOrSpaceGfmStrongEm).replace(/punctSpace/g, _punctuationOrSpaceGfmStrongEm).replace(/punct/g, _punctuationGfmStrongEm).getRegex(); -var emStrongRDelimUnd = edit( - "^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)", - "gu" -).replace(/notPunctSpace/g, _notPunctuationOrSpace).replace(/punctSpace/g, _punctuationOrSpace).replace(/punct/g, _punctuation).getRegex(); -var anyPunctuation = edit(/\\(punct)/, "gu").replace(/punct/g, _punctuation).getRegex(); -var autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme", /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email", /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(); -var _inlineComment = edit(_comment).replace("(?:-->|$)", "-->").getRegex(); -var tag = edit( - "^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^" -).replace("comment", _inlineComment).replace("attribute", /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(); -var _inlineLabel = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; -var link = edit(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label", _inlineLabel).replace("href", /<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title", /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(); -var reflink = edit(/^!?\[(label)\]\[(ref)\]/).replace("label", _inlineLabel).replace("ref", _blockLabel).getRegex(); -var nolink = edit(/^!?\[(ref)\](?:\[\])?/).replace("ref", _blockLabel).getRegex(); -var reflinkSearch = edit("reflink|nolink(?!\\()", "g").replace("reflink", reflink).replace("nolink", nolink).getRegex(); -var inlineNormal = { - _backpedal: noopTest, - // only used for GFM url - anyPunctuation, - autolink, - blockSkip, - br, - code: inlineCode, - del: noopTest, - emStrongLDelim, - emStrongRDelimAst, - emStrongRDelimUnd, - escape, - link, - nolink, - punctuation, - reflink, - reflinkSearch, - tag, - text: inlineText, - url: noopTest -}; -var inlinePedantic = { - ...inlineNormal, - link: edit(/^!?\[(label)\]\((.*?)\)/).replace("label", _inlineLabel).getRegex(), - reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label", _inlineLabel).getRegex() -}; -var inlineGfm = { - ...inlineNormal, - emStrongRDelimAst: emStrongRDelimAstGfm, - emStrongLDelim: emStrongLDelimGfm, - url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, "i").replace("email", /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(), - _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, - del: /^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/, - text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\": ">", - '"': """, - "'": "'" -}; -var getEscapeReplacement = (ch) => escapeReplacements[ch]; -function escape2(html2, encode) { - if (encode) { - if (other.escapeTest.test(html2)) { - return html2.replace(other.escapeReplace, getEscapeReplacement); - } - } else { - if (other.escapeTestNoEncode.test(html2)) { - return html2.replace(other.escapeReplaceNoEncode, getEscapeReplacement); - } - } - return html2; -} -function cleanUrl(href) { - try { - href = encodeURI(href).replace(other.percentDecode, "%"); - } catch { - return null; - } - return href; -} -function splitCells(tableRow, count) { - const row = tableRow.replace(other.findPipe, (match, offset, str) => { - let escaped = false; - let curr = offset; - while (--curr >= 0 && str[curr] === "\\") escaped = !escaped; - if (escaped) { - return "|"; - } else { - return " |"; - } - }), cells = row.split(other.splitPipe); - let i = 0; - if (!cells[0].trim()) { - cells.shift(); - } - if (cells.length > 0 && !cells.at(-1)?.trim()) { - cells.pop(); - } - if (count) { - if (cells.length > count) { - cells.splice(count); - } else { - while (cells.length < count) cells.push(""); - } - } - for (; i < cells.length; i++) { - cells[i] = cells[i].trim().replace(other.slashPipe, "|"); - } - return cells; -} -function rtrim(str, c, invert) { - const l = str.length; - if (l === 0) { - return ""; - } - let suffLen = 0; - while (suffLen < l) { - const currChar = str.charAt(l - suffLen - 1); - if (currChar === c && !invert) { - suffLen++; - } else if (currChar !== c && invert) { - suffLen++; - } else { - break; - } - } - return str.slice(0, l - suffLen); -} -function findClosingBracket(str, b) { - if (str.indexOf(b[1]) === -1) { - return -1; - } - let level = 0; - for (let i = 0; i < str.length; i++) { - if (str[i] === "\\") { - i++; - } else if (str[i] === b[0]) { - level++; - } else if (str[i] === b[1]) { - level--; - if (level < 0) { - return i; - } - } - } - if (level > 0) { - return -2; - } - return -1; -} - -// src/Tokenizer.ts -function outputLink(cap, link2, raw, lexer2, rules) { - const href = link2.href; - const title = link2.title || null; - const text = cap[1].replace(rules.other.outputLinkReplace, "$1"); - lexer2.state.inLink = true; - const token = { - type: cap[0].charAt(0) === "!" ? "image" : "link", - raw, - href, - title, - text, - tokens: lexer2.inlineTokens(text) - }; - lexer2.state.inLink = false; - return token; -} -function indentCodeCompensation(raw, text, rules) { - const matchIndentToCode = raw.match(rules.other.indentCodeCompensation); - if (matchIndentToCode === null) { - return text; - } - const indentToCode = matchIndentToCode[1]; - return text.split("\n").map((node) => { - const matchIndentInNode = node.match(rules.other.beginningSpace); - if (matchIndentInNode === null) { - return node; - } - const [indentInNode] = matchIndentInNode; - if (indentInNode.length >= indentToCode.length) { - return node.slice(indentToCode.length); - } - return node; - }).join("\n"); -} -var _Tokenizer = class { - options; - rules; - // set by the lexer - lexer; - // set by the lexer - constructor(options2) { - this.options = options2 || _defaults; - } - space(src) { - const cap = this.rules.block.newline.exec(src); - if (cap && cap[0].length > 0) { - return { - type: "space", - raw: cap[0] - }; - } - } - code(src) { - const cap = this.rules.block.code.exec(src); - if (cap) { - const text = cap[0].replace(this.rules.other.codeRemoveIndent, ""); - return { - type: "code", - raw: cap[0], - codeBlockStyle: "indented", - text: !this.options.pedantic ? rtrim(text, "\n") : text - }; - } - } - fences(src) { - const cap = this.rules.block.fences.exec(src); - if (cap) { - const raw = cap[0]; - const text = indentCodeCompensation(raw, cap[3] || "", this.rules); - return { - type: "code", - raw, - lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, "$1") : cap[2], - text - }; - } - } - heading(src) { - const cap = this.rules.block.heading.exec(src); - if (cap) { - let text = cap[2].trim(); - if (this.rules.other.endingHash.test(text)) { - const trimmed = rtrim(text, "#"); - if (this.options.pedantic) { - text = trimmed.trim(); - } else if (!trimmed || this.rules.other.endingSpaceChar.test(trimmed)) { - text = trimmed.trim(); - } - } - return { - type: "heading", - raw: cap[0], - depth: cap[1].length, - text, - tokens: this.lexer.inline(text) - }; - } - } - hr(src) { - const cap = this.rules.block.hr.exec(src); - if (cap) { - return { - type: "hr", - raw: rtrim(cap[0], "\n") - }; - } - } - blockquote(src) { - const cap = this.rules.block.blockquote.exec(src); - if (cap) { - let lines = rtrim(cap[0], "\n").split("\n"); - let raw = ""; - let text = ""; - const tokens = []; - while (lines.length > 0) { - let inBlockquote = false; - const currentLines = []; - let i; - for (i = 0; i < lines.length; i++) { - if (this.rules.other.blockquoteStart.test(lines[i])) { - currentLines.push(lines[i]); - inBlockquote = true; - } else if (!inBlockquote) { - currentLines.push(lines[i]); - } else { - break; - } - } - lines = lines.slice(i); - const currentRaw = currentLines.join("\n"); - const currentText = currentRaw.replace(this.rules.other.blockquoteSetextReplace, "\n $1").replace(this.rules.other.blockquoteSetextReplace2, ""); - raw = raw ? `${raw} -${currentRaw}` : currentRaw; - text = text ? `${text} -${currentText}` : currentText; - const top = this.lexer.state.top; - this.lexer.state.top = true; - this.lexer.blockTokens(currentText, tokens, true); - this.lexer.state.top = top; - if (lines.length === 0) { - break; - } - const lastToken = tokens.at(-1); - if (lastToken?.type === "code") { - break; - } else if (lastToken?.type === "blockquote") { - const oldToken = lastToken; - const newText = oldToken.raw + "\n" + lines.join("\n"); - const newToken = this.blockquote(newText); - tokens[tokens.length - 1] = newToken; - raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw; - text = text.substring(0, text.length - oldToken.text.length) + newToken.text; - break; - } else if (lastToken?.type === "list") { - const oldToken = lastToken; - const newText = oldToken.raw + "\n" + lines.join("\n"); - const newToken = this.list(newText); - tokens[tokens.length - 1] = newToken; - raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw; - text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw; - lines = newText.substring(tokens.at(-1).raw.length).split("\n"); - continue; - } - } - return { - type: "blockquote", - raw, - tokens, - text - }; - } - } - list(src) { - let cap = this.rules.block.list.exec(src); - if (cap) { - let bull = cap[1].trim(); - const isordered = bull.length > 1; - const list2 = { - type: "list", - raw: "", - ordered: isordered, - start: isordered ? +bull.slice(0, -1) : "", - loose: false, - items: [] - }; - bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; - if (this.options.pedantic) { - bull = isordered ? bull : "[*+-]"; - } - const itemRegex = this.rules.other.listItemRegex(bull); - let endsWithBlankLine = false; - while (src) { - let endEarly = false; - let raw = ""; - let itemContents = ""; - if (!(cap = itemRegex.exec(src))) { - break; - } - if (this.rules.block.hr.test(src)) { - break; - } - raw = cap[0]; - src = src.substring(raw.length); - let line = cap[2].split("\n", 1)[0].replace(this.rules.other.listReplaceTabs, (t) => " ".repeat(3 * t.length)); - let nextLine = src.split("\n", 1)[0]; - let blankLine = !line.trim(); - let indent = 0; - if (this.options.pedantic) { - indent = 2; - itemContents = line.trimStart(); - } else if (blankLine) { - indent = cap[1].length + 1; - } else { - indent = cap[2].search(this.rules.other.nonSpaceChar); - indent = indent > 4 ? 1 : indent; - itemContents = line.slice(indent); - indent += cap[1].length; - } - if (blankLine && this.rules.other.blankLine.test(nextLine)) { - raw += nextLine + "\n"; - src = src.substring(nextLine.length + 1); - endEarly = true; - } - if (!endEarly) { - const nextBulletRegex = this.rules.other.nextBulletRegex(indent); - const hrRegex = this.rules.other.hrRegex(indent); - const fencesBeginRegex = this.rules.other.fencesBeginRegex(indent); - const headingBeginRegex = this.rules.other.headingBeginRegex(indent); - const htmlBeginRegex = this.rules.other.htmlBeginRegex(indent); - while (src) { - const rawLine = src.split("\n", 1)[0]; - let nextLineWithoutTabs; - nextLine = rawLine; - if (this.options.pedantic) { - nextLine = nextLine.replace(this.rules.other.listReplaceNesting, " "); - nextLineWithoutTabs = nextLine; - } else { - nextLineWithoutTabs = nextLine.replace(this.rules.other.tabCharGlobal, " "); - } - if (fencesBeginRegex.test(nextLine)) { - break; - } - if (headingBeginRegex.test(nextLine)) { - break; - } - if (htmlBeginRegex.test(nextLine)) { - break; - } - if (nextBulletRegex.test(nextLine)) { - break; - } - if (hrRegex.test(nextLine)) { - break; - } - if (nextLineWithoutTabs.search(this.rules.other.nonSpaceChar) >= indent || !nextLine.trim()) { - itemContents += "\n" + nextLineWithoutTabs.slice(indent); - } else { - if (blankLine) { - break; - } - if (line.replace(this.rules.other.tabCharGlobal, " ").search(this.rules.other.nonSpaceChar) >= 4) { - break; - } - if (fencesBeginRegex.test(line)) { - break; - } - if (headingBeginRegex.test(line)) { - break; - } - if (hrRegex.test(line)) { - break; - } - itemContents += "\n" + nextLine; - } - if (!blankLine && !nextLine.trim()) { - blankLine = true; - } - raw += rawLine + "\n"; - src = src.substring(rawLine.length + 1); - line = nextLineWithoutTabs.slice(indent); - } - } - if (!list2.loose) { - if (endsWithBlankLine) { - list2.loose = true; - } else if (this.rules.other.doubleBlankLine.test(raw)) { - endsWithBlankLine = true; - } - } - let istask = null; - let ischecked; - if (this.options.gfm) { - istask = this.rules.other.listIsTask.exec(itemContents); - if (istask) { - ischecked = istask[0] !== "[ ] "; - itemContents = itemContents.replace(this.rules.other.listReplaceTask, ""); - } - } - list2.items.push({ - type: "list_item", - raw, - task: !!istask, - checked: ischecked, - loose: false, - text: itemContents, - tokens: [] - }); - list2.raw += raw; - } - const lastItem = list2.items.at(-1); - if (lastItem) { - lastItem.raw = lastItem.raw.trimEnd(); - lastItem.text = lastItem.text.trimEnd(); - } else { - return; - } - list2.raw = list2.raw.trimEnd(); - for (let i = 0; i < list2.items.length; i++) { - this.lexer.state.top = false; - list2.items[i].tokens = this.lexer.blockTokens(list2.items[i].text, []); - if (!list2.loose) { - const spacers = list2.items[i].tokens.filter((t) => t.type === "space"); - const hasMultipleLineBreaks = spacers.length > 0 && spacers.some((t) => this.rules.other.anyLine.test(t.raw)); - list2.loose = hasMultipleLineBreaks; - } - } - if (list2.loose) { - for (let i = 0; i < list2.items.length; i++) { - list2.items[i].loose = true; - } - } - return list2; - } - } - html(src) { - const cap = this.rules.block.html.exec(src); - if (cap) { - const token = { - type: "html", - block: true, - raw: cap[0], - pre: cap[1] === "pre" || cap[1] === "script" || cap[1] === "style", - text: cap[0] - }; - return token; - } - } - def(src) { - const cap = this.rules.block.def.exec(src); - if (cap) { - const tag2 = cap[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, " "); - const href = cap[2] ? cap[2].replace(this.rules.other.hrefBrackets, "$1").replace(this.rules.inline.anyPunctuation, "$1") : ""; - const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, "$1") : cap[3]; - return { - type: "def", - tag: tag2, - raw: cap[0], - href, - title - }; - } - } - table(src) { - const cap = this.rules.block.table.exec(src); - if (!cap) { - return; - } - if (!this.rules.other.tableDelimiter.test(cap[2])) { - return; - } - const headers = splitCells(cap[1]); - const aligns = cap[2].replace(this.rules.other.tableAlignChars, "").split("|"); - const rows = cap[3]?.trim() ? cap[3].replace(this.rules.other.tableRowBlankLine, "").split("\n") : []; - const item = { - type: "table", - raw: cap[0], - header: [], - align: [], - rows: [] - }; - if (headers.length !== aligns.length) { - return; - } - for (const align of aligns) { - if (this.rules.other.tableAlignRight.test(align)) { - item.align.push("right"); - } else if (this.rules.other.tableAlignCenter.test(align)) { - item.align.push("center"); - } else if (this.rules.other.tableAlignLeft.test(align)) { - item.align.push("left"); - } else { - item.align.push(null); - } - } - for (let i = 0; i < headers.length; i++) { - item.header.push({ - text: headers[i], - tokens: this.lexer.inline(headers[i]), - header: true, - align: item.align[i] - }); - } - for (const row of rows) { - item.rows.push(splitCells(row, item.header.length).map((cell, i) => { - return { - text: cell, - tokens: this.lexer.inline(cell), - header: false, - align: item.align[i] - }; - })); - } - return item; - } - lheading(src) { - const cap = this.rules.block.lheading.exec(src); - if (cap) { - return { - type: "heading", - raw: cap[0], - depth: cap[2].charAt(0) === "=" ? 1 : 2, - text: cap[1], - tokens: this.lexer.inline(cap[1]) - }; - } - } - paragraph(src) { - const cap = this.rules.block.paragraph.exec(src); - if (cap) { - const text = cap[1].charAt(cap[1].length - 1) === "\n" ? cap[1].slice(0, -1) : cap[1]; - return { - type: "paragraph", - raw: cap[0], - text, - tokens: this.lexer.inline(text) - }; - } - } - text(src) { - const cap = this.rules.block.text.exec(src); - if (cap) { - return { - type: "text", - raw: cap[0], - text: cap[0], - tokens: this.lexer.inline(cap[0]) - }; - } - } - escape(src) { - const cap = this.rules.inline.escape.exec(src); - if (cap) { - return { - type: "escape", - raw: cap[0], - text: cap[1] - }; - } - } - tag(src) { - const cap = this.rules.inline.tag.exec(src); - if (cap) { - if (!this.lexer.state.inLink && this.rules.other.startATag.test(cap[0])) { - this.lexer.state.inLink = true; - } else if (this.lexer.state.inLink && this.rules.other.endATag.test(cap[0])) { - this.lexer.state.inLink = false; - } - if (!this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(cap[0])) { - this.lexer.state.inRawBlock = true; - } else if (this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(cap[0])) { - this.lexer.state.inRawBlock = false; - } - return { - type: "html", - raw: cap[0], - inLink: this.lexer.state.inLink, - inRawBlock: this.lexer.state.inRawBlock, - block: false, - text: cap[0] - }; - } - } - link(src) { - const cap = this.rules.inline.link.exec(src); - if (cap) { - const trimmedUrl = cap[2].trim(); - if (!this.options.pedantic && this.rules.other.startAngleBracket.test(trimmedUrl)) { - if (!this.rules.other.endAngleBracket.test(trimmedUrl)) { - return; - } - const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), "\\"); - if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { - return; - } - } else { - const lastParenIndex = findClosingBracket(cap[2], "()"); - if (lastParenIndex === -2) { - return; - } - if (lastParenIndex > -1) { - const start = cap[0].indexOf("!") === 0 ? 5 : 4; - const linkLen = start + cap[1].length + lastParenIndex; - cap[2] = cap[2].substring(0, lastParenIndex); - cap[0] = cap[0].substring(0, linkLen).trim(); - cap[3] = ""; - } - } - let href = cap[2]; - let title = ""; - if (this.options.pedantic) { - const link2 = this.rules.other.pedanticHrefTitle.exec(href); - if (link2) { - href = link2[1]; - title = link2[3]; - } - } else { - title = cap[3] ? cap[3].slice(1, -1) : ""; - } - href = href.trim(); - if (this.rules.other.startAngleBracket.test(href)) { - if (this.options.pedantic && !this.rules.other.endAngleBracket.test(trimmedUrl)) { - href = href.slice(1); - } else { - href = href.slice(1, -1); - } - } - return outputLink(cap, { - href: href ? href.replace(this.rules.inline.anyPunctuation, "$1") : href, - title: title ? title.replace(this.rules.inline.anyPunctuation, "$1") : title - }, cap[0], this.lexer, this.rules); - } - } - reflink(src, links) { - let cap; - if ((cap = this.rules.inline.reflink.exec(src)) || (cap = this.rules.inline.nolink.exec(src))) { - const linkString = (cap[2] || cap[1]).replace(this.rules.other.multipleSpaceGlobal, " "); - const link2 = links[linkString.toLowerCase()]; - if (!link2) { - const text = cap[0].charAt(0); - return { - type: "text", - raw: text, - text - }; - } - return outputLink(cap, link2, cap[0], this.lexer, this.rules); - } - } - emStrong(src, maskedSrc, prevChar = "") { - let match = this.rules.inline.emStrongLDelim.exec(src); - if (!match) return; - if (match[3] && prevChar.match(this.rules.other.unicodeAlphaNumeric)) return; - const nextChar = match[1] || match[2] || ""; - if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { - const lLength = [...match[0]].length - 1; - let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; - const endReg = match[0][0] === "*" ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; - endReg.lastIndex = 0; - maskedSrc = maskedSrc.slice(-1 * src.length + lLength); - while ((match = endReg.exec(maskedSrc)) != null) { - rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; - if (!rDelim) continue; - rLength = [...rDelim].length; - if (match[3] || match[4]) { - delimTotal += rLength; - continue; - } else if (match[5] || match[6]) { - if (lLength % 3 && !((lLength + rLength) % 3)) { - midDelimTotal += rLength; - continue; - } - } - delimTotal -= rLength; - if (delimTotal > 0) continue; - rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); - const lastCharLength = [...match[0]][0].length; - const raw = src.slice(0, lLength + match.index + lastCharLength + rLength); - if (Math.min(lLength, rLength) % 2) { - const text2 = raw.slice(1, -1); - return { - type: "em", - raw, - text: text2, - tokens: this.lexer.inlineTokens(text2) - }; - } - const text = raw.slice(2, -2); - return { - type: "strong", - raw, - text, - tokens: this.lexer.inlineTokens(text) - }; - } - } - } - codespan(src) { - const cap = this.rules.inline.code.exec(src); - if (cap) { - let text = cap[2].replace(this.rules.other.newLineCharGlobal, " "); - const hasNonSpaceChars = this.rules.other.nonSpaceChar.test(text); - const hasSpaceCharsOnBothEnds = this.rules.other.startingSpaceChar.test(text) && this.rules.other.endingSpaceChar.test(text); - if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { - text = text.substring(1, text.length - 1); - } - return { - type: "codespan", - raw: cap[0], - text - }; - } - } - br(src) { - const cap = this.rules.inline.br.exec(src); - if (cap) { - return { - type: "br", - raw: cap[0] - }; - } - } - del(src) { - const cap = this.rules.inline.del.exec(src); - if (cap) { - return { - type: "del", - raw: cap[0], - text: cap[2], - tokens: this.lexer.inlineTokens(cap[2]) - }; - } - } - autolink(src) { - const cap = this.rules.inline.autolink.exec(src); - if (cap) { - let text, href; - if (cap[2] === "@") { - text = cap[1]; - href = "mailto:" + text; - } else { - text = cap[1]; - href = text; - } - return { - type: "link", - raw: cap[0], - text, - href, - tokens: [ - { - type: "text", - raw: text, - text - } - ] - }; - } - } - url(src) { - let cap; - if (cap = this.rules.inline.url.exec(src)) { - let text, href; - if (cap[2] === "@") { - text = cap[0]; - href = "mailto:" + text; - } else { - let prevCapZero; - do { - prevCapZero = cap[0]; - cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? ""; - } while (prevCapZero !== cap[0]); - text = cap[0]; - if (cap[1] === "www.") { - href = "http://" + cap[0]; - } else { - href = cap[0]; - } - } - return { - type: "link", - raw: cap[0], - text, - href, - tokens: [ - { - type: "text", - raw: text, - text - } - ] - }; - } - } - inlineText(src) { - const cap = this.rules.inline.text.exec(src); - if (cap) { - const escaped = this.lexer.state.inRawBlock; - return { - type: "text", - raw: cap[0], - text: cap[0], - escaped - }; - } - } -}; - -// src/Lexer.ts -var _Lexer = class __Lexer { - tokens; - options; - state; - tokenizer; - inlineQueue; - constructor(options2) { - this.tokens = []; - this.tokens.links = /* @__PURE__ */ Object.create(null); - this.options = options2 || _defaults; - this.options.tokenizer = this.options.tokenizer || new _Tokenizer(); - this.tokenizer = this.options.tokenizer; - this.tokenizer.options = this.options; - this.tokenizer.lexer = this; - this.inlineQueue = []; - this.state = { - inLink: false, - inRawBlock: false, - top: true - }; - const rules = { - other, - block: block.normal, - inline: inline.normal - }; - if (this.options.pedantic) { - rules.block = block.pedantic; - rules.inline = inline.pedantic; - } else if (this.options.gfm) { - rules.block = block.gfm; - if (this.options.breaks) { - rules.inline = inline.breaks; - } else { - rules.inline = inline.gfm; - } - } - this.tokenizer.rules = rules; - } - /** - * Expose Rules - */ - static get rules() { - return { - block, - inline - }; - } - /** - * Static Lex Method - */ - static lex(src, options2) { - const lexer2 = new __Lexer(options2); - return lexer2.lex(src); - } - /** - * Static Lex Inline Method - */ - static lexInline(src, options2) { - const lexer2 = new __Lexer(options2); - return lexer2.inlineTokens(src); - } - /** - * Preprocessing - */ - lex(src) { - src = src.replace(other.carriageReturn, "\n"); - this.blockTokens(src, this.tokens); - for (let i = 0; i < this.inlineQueue.length; i++) { - const next = this.inlineQueue[i]; - this.inlineTokens(next.src, next.tokens); - } - this.inlineQueue = []; - return this.tokens; - } - blockTokens(src, tokens = [], lastParagraphClipped = false) { - if (this.options.pedantic) { - src = src.replace(other.tabCharGlobal, " ").replace(other.spaceLine, ""); - } - while (src) { - let token; - if (this.options.extensions?.block?.some((extTokenizer) => { - if (token = extTokenizer.call({ lexer: this }, src, tokens)) { - src = src.substring(token.raw.length); - tokens.push(token); - return true; - } - return false; - })) { - continue; - } - if (token = this.tokenizer.space(src)) { - src = src.substring(token.raw.length); - const lastToken = tokens.at(-1); - if (token.raw.length === 1 && lastToken !== void 0) { - lastToken.raw += "\n"; - } else { - tokens.push(token); - } - continue; - } - if (token = this.tokenizer.code(src)) { - src = src.substring(token.raw.length); - const lastToken = tokens.at(-1); - if (lastToken?.type === "paragraph" || lastToken?.type === "text") { - lastToken.raw += "\n" + token.raw; - lastToken.text += "\n" + token.text; - this.inlineQueue.at(-1).src = lastToken.text; - } else { - tokens.push(token); - } - continue; - } - if (token = this.tokenizer.fences(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.heading(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.hr(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.blockquote(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.list(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.html(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.def(src)) { - src = src.substring(token.raw.length); - const lastToken = tokens.at(-1); - if (lastToken?.type === "paragraph" || lastToken?.type === "text") { - lastToken.raw += "\n" + token.raw; - lastToken.text += "\n" + token.raw; - this.inlineQueue.at(-1).src = lastToken.text; - } else if (!this.tokens.links[token.tag]) { - this.tokens.links[token.tag] = { - href: token.href, - title: token.title - }; - } - continue; - } - if (token = this.tokenizer.table(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.lheading(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - let cutSrc = src; - if (this.options.extensions?.startBlock) { - let startIndex = Infinity; - const tempSrc = src.slice(1); - let tempStart; - this.options.extensions.startBlock.forEach((getStartIndex) => { - tempStart = getStartIndex.call({ lexer: this }, tempSrc); - if (typeof tempStart === "number" && tempStart >= 0) { - startIndex = Math.min(startIndex, tempStart); - } - }); - if (startIndex < Infinity && startIndex >= 0) { - cutSrc = src.substring(0, startIndex + 1); - } - } - if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { - const lastToken = tokens.at(-1); - if (lastParagraphClipped && lastToken?.type === "paragraph") { - lastToken.raw += "\n" + token.raw; - lastToken.text += "\n" + token.text; - this.inlineQueue.pop(); - this.inlineQueue.at(-1).src = lastToken.text; - } else { - tokens.push(token); - } - lastParagraphClipped = cutSrc.length !== src.length; - src = src.substring(token.raw.length); - continue; - } - if (token = this.tokenizer.text(src)) { - src = src.substring(token.raw.length); - const lastToken = tokens.at(-1); - if (lastToken?.type === "text") { - lastToken.raw += "\n" + token.raw; - lastToken.text += "\n" + token.text; - this.inlineQueue.pop(); - this.inlineQueue.at(-1).src = lastToken.text; - } else { - tokens.push(token); - } - continue; - } - if (src) { - const errMsg = "Infinite loop on byte: " + src.charCodeAt(0); - if (this.options.silent) { - console.error(errMsg); - break; - } else { - throw new Error(errMsg); - } - } - } - this.state.top = true; - return tokens; - } - inline(src, tokens = []) { - this.inlineQueue.push({ src, tokens }); - return tokens; - } - /** - * Lexing/Compiling - */ - inlineTokens(src, tokens = []) { - let maskedSrc = src; - let match = null; - if (this.tokens.links) { - const links = Object.keys(this.tokens.links); - if (links.length > 0) { - while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { - if (links.includes(match[0].slice(match[0].lastIndexOf("[") + 1, -1))) { - maskedSrc = maskedSrc.slice(0, match.index) + "[" + "a".repeat(match[0].length - 2) + "]" + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); - } - } - } - } - while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { - maskedSrc = maskedSrc.slice(0, match.index) + "++" + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); - } - while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { - maskedSrc = maskedSrc.slice(0, match.index) + "[" + "a".repeat(match[0].length - 2) + "]" + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); - } - let keepPrevChar = false; - let prevChar = ""; - while (src) { - if (!keepPrevChar) { - prevChar = ""; - } - keepPrevChar = false; - let token; - if (this.options.extensions?.inline?.some((extTokenizer) => { - if (token = extTokenizer.call({ lexer: this }, src, tokens)) { - src = src.substring(token.raw.length); - tokens.push(token); - return true; - } - return false; - })) { - continue; - } - if (token = this.tokenizer.escape(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.tag(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.link(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.reflink(src, this.tokens.links)) { - src = src.substring(token.raw.length); - const lastToken = tokens.at(-1); - if (token.type === "text" && lastToken?.type === "text") { - lastToken.raw += token.raw; - lastToken.text += token.text; - } else { - tokens.push(token); - } - continue; - } - if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.codespan(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.br(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.del(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (token = this.tokenizer.autolink(src)) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - if (!this.state.inLink && (token = this.tokenizer.url(src))) { - src = src.substring(token.raw.length); - tokens.push(token); - continue; - } - let cutSrc = src; - if (this.options.extensions?.startInline) { - let startIndex = Infinity; - const tempSrc = src.slice(1); - let tempStart; - this.options.extensions.startInline.forEach((getStartIndex) => { - tempStart = getStartIndex.call({ lexer: this }, tempSrc); - if (typeof tempStart === "number" && tempStart >= 0) { - startIndex = Math.min(startIndex, tempStart); - } - }); - if (startIndex < Infinity && startIndex >= 0) { - cutSrc = src.substring(0, startIndex + 1); - } - } - if (token = this.tokenizer.inlineText(cutSrc)) { - src = src.substring(token.raw.length); - if (token.raw.slice(-1) !== "_") { - prevChar = token.raw.slice(-1); - } - keepPrevChar = true; - const lastToken = tokens.at(-1); - if (lastToken?.type === "text") { - lastToken.raw += token.raw; - lastToken.text += token.text; - } else { - tokens.push(token); - } - continue; - } - if (src) { - const errMsg = "Infinite loop on byte: " + src.charCodeAt(0); - if (this.options.silent) { - console.error(errMsg); - break; - } else { - throw new Error(errMsg); - } - } - } - return tokens; - } -}; - -// src/Renderer.ts -var _Renderer = class { - options; - parser; - // set by the parser - constructor(options2) { - this.options = options2 || _defaults; - } - space(token) { - return ""; - } - code({ text, lang, escaped }) { - const langString = (lang || "").match(other.notSpaceStart)?.[0]; - const code = text.replace(other.endingNewline, "") + "\n"; - if (!langString) { - return "
" + (escaped ? code : escape2(code, true)) + "
\n"; - } - return '
' + (escaped ? code : escape2(code, true)) + "
\n"; - } - blockquote({ tokens }) { - const body = this.parser.parse(tokens); - return `
-${body}
-`; - } - html({ text }) { - return text; - } - heading({ tokens, depth }) { - return `${this.parser.parseInline(tokens)} -`; - } - hr(token) { - return "
\n"; - } - list(token) { - const ordered = token.ordered; - const start = token.start; - let body = ""; - for (let j = 0; j < token.items.length; j++) { - const item = token.items[j]; - body += this.listitem(item); - } - const type = ordered ? "ol" : "ul"; - const startAttr = ordered && start !== 1 ? ' start="' + start + '"' : ""; - return "<" + type + startAttr + ">\n" + body + "\n"; - } - listitem(item) { - let itemBody = ""; - if (item.task) { - const checkbox = this.checkbox({ checked: !!item.checked }); - if (item.loose) { - if (item.tokens[0]?.type === "paragraph") { - item.tokens[0].text = checkbox + " " + item.tokens[0].text; - if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === "text") { - item.tokens[0].tokens[0].text = checkbox + " " + escape2(item.tokens[0].tokens[0].text); - item.tokens[0].tokens[0].escaped = true; - } - } else { - item.tokens.unshift({ - type: "text", - raw: checkbox + " ", - text: checkbox + " ", - escaped: true - }); - } - } else { - itemBody += checkbox + " "; - } - } - itemBody += this.parser.parse(item.tokens, !!item.loose); - return `
  • ${itemBody}
  • -`; - } - checkbox({ checked }) { - return "'; - } - paragraph({ tokens }) { - return `

    ${this.parser.parseInline(tokens)}

    -`; - } - table(token) { - let header = ""; - let cell = ""; - for (let j = 0; j < token.header.length; j++) { - cell += this.tablecell(token.header[j]); - } - header += this.tablerow({ text: cell }); - let body = ""; - for (let j = 0; j < token.rows.length; j++) { - const row = token.rows[j]; - cell = ""; - for (let k = 0; k < row.length; k++) { - cell += this.tablecell(row[k]); - } - body += this.tablerow({ text: cell }); - } - if (body) body = `${body}`; - return "\n\n" + header + "\n" + body + "
    \n"; - } - tablerow({ text }) { - return ` -${text} -`; - } - tablecell(token) { - const content = this.parser.parseInline(token.tokens); - const type = token.header ? "th" : "td"; - const tag2 = token.align ? `<${type} align="${token.align}">` : `<${type}>`; - return tag2 + content + ` -`; - } - /** - * span level renderer - */ - strong({ tokens }) { - return `${this.parser.parseInline(tokens)}`; - } - em({ tokens }) { - return `${this.parser.parseInline(tokens)}`; - } - codespan({ text }) { - return `${escape2(text, true)}`; - } - br(token) { - return "
    "; - } - del({ tokens }) { - return `${this.parser.parseInline(tokens)}`; - } - link({ href, title, tokens }) { - const text = this.parser.parseInline(tokens); - const cleanHref = cleanUrl(href); - if (cleanHref === null) { - return text; - } - href = cleanHref; - let out = '
    "; - return out; - } - image({ href, title, text, tokens }) { - if (tokens) { - text = this.parser.parseInline(tokens, this.parser.textRenderer); - } - const cleanHref = cleanUrl(href); - if (cleanHref === null) { - return escape2(text); - } - href = cleanHref; - let out = `${text} { - const tokens2 = genericToken[childTokens].flat(Infinity); - values = values.concat(this.walkTokens(tokens2, callback)); - }); - } else if (genericToken.tokens) { - values = values.concat(this.walkTokens(genericToken.tokens, callback)); - } - } - } - } - return values; - } - use(...args) { - const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; - args.forEach((pack) => { - const opts = { ...pack }; - opts.async = this.defaults.async || opts.async || false; - if (pack.extensions) { - pack.extensions.forEach((ext) => { - if (!ext.name) { - throw new Error("extension name required"); - } - if ("renderer" in ext) { - const prevRenderer = extensions.renderers[ext.name]; - if (prevRenderer) { - extensions.renderers[ext.name] = function(...args2) { - let ret = ext.renderer.apply(this, args2); - if (ret === false) { - ret = prevRenderer.apply(this, args2); - } - return ret; - }; - } else { - extensions.renderers[ext.name] = ext.renderer; - } - } - if ("tokenizer" in ext) { - if (!ext.level || ext.level !== "block" && ext.level !== "inline") { - throw new Error("extension level must be 'block' or 'inline'"); - } - const extLevel = extensions[ext.level]; - if (extLevel) { - extLevel.unshift(ext.tokenizer); - } else { - extensions[ext.level] = [ext.tokenizer]; - } - if (ext.start) { - if (ext.level === "block") { - if (extensions.startBlock) { - extensions.startBlock.push(ext.start); - } else { - extensions.startBlock = [ext.start]; - } - } else if (ext.level === "inline") { - if (extensions.startInline) { - extensions.startInline.push(ext.start); - } else { - extensions.startInline = [ext.start]; - } - } - } - } - if ("childTokens" in ext && ext.childTokens) { - extensions.childTokens[ext.name] = ext.childTokens; - } - }); - opts.extensions = extensions; - } - if (pack.renderer) { - const renderer = this.defaults.renderer || new _Renderer(this.defaults); - for (const prop in pack.renderer) { - if (!(prop in renderer)) { - throw new Error(`renderer '${prop}' does not exist`); - } - if (["options", "parser"].includes(prop)) { - continue; - } - const rendererProp = prop; - const rendererFunc = pack.renderer[rendererProp]; - const prevRenderer = renderer[rendererProp]; - renderer[rendererProp] = (...args2) => { - let ret = rendererFunc.apply(renderer, args2); - if (ret === false) { - ret = prevRenderer.apply(renderer, args2); - } - return ret || ""; - }; - } - opts.renderer = renderer; - } - if (pack.tokenizer) { - const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); - for (const prop in pack.tokenizer) { - if (!(prop in tokenizer)) { - throw new Error(`tokenizer '${prop}' does not exist`); - } - if (["options", "rules", "lexer"].includes(prop)) { - continue; - } - const tokenizerProp = prop; - const tokenizerFunc = pack.tokenizer[tokenizerProp]; - const prevTokenizer = tokenizer[tokenizerProp]; - tokenizer[tokenizerProp] = (...args2) => { - let ret = tokenizerFunc.apply(tokenizer, args2); - if (ret === false) { - ret = prevTokenizer.apply(tokenizer, args2); - } - return ret; - }; - } - opts.tokenizer = tokenizer; - } - if (pack.hooks) { - const hooks = this.defaults.hooks || new _Hooks(); - for (const prop in pack.hooks) { - if (!(prop in hooks)) { - throw new Error(`hook '${prop}' does not exist`); - } - if (["options", "block"].includes(prop)) { - continue; - } - const hooksProp = prop; - const hooksFunc = pack.hooks[hooksProp]; - const prevHook = hooks[hooksProp]; - if (_Hooks.passThroughHooks.has(prop)) { - hooks[hooksProp] = (arg) => { - if (this.defaults.async) { - return Promise.resolve(hooksFunc.call(hooks, arg)).then((ret2) => { - return prevHook.call(hooks, ret2); - }); - } - const ret = hooksFunc.call(hooks, arg); - return prevHook.call(hooks, ret); - }; - } else { - hooks[hooksProp] = (...args2) => { - let ret = hooksFunc.apply(hooks, args2); - if (ret === false) { - ret = prevHook.apply(hooks, args2); - } - return ret; - }; - } - } - opts.hooks = hooks; - } - if (pack.walkTokens) { - const walkTokens2 = this.defaults.walkTokens; - const packWalktokens = pack.walkTokens; - opts.walkTokens = function(token) { - let values = []; - values.push(packWalktokens.call(this, token)); - if (walkTokens2) { - values = values.concat(walkTokens2.call(this, token)); - } - return values; - }; - } - this.defaults = { ...this.defaults, ...opts }; - }); - return this; - } - setOptions(opt) { - this.defaults = { ...this.defaults, ...opt }; - return this; - } - lexer(src, options2) { - return _Lexer.lex(src, options2 ?? this.defaults); - } - parser(tokens, options2) { - return _Parser.parse(tokens, options2 ?? this.defaults); - } - parseMarkdown(blockType) { - const parse2 = (src, options2) => { - const origOpt = { ...options2 }; - const opt = { ...this.defaults, ...origOpt }; - const throwError = this.onError(!!opt.silent, !!opt.async); - if (this.defaults.async === true && origOpt.async === false) { - return throwError(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.")); - } - if (typeof src === "undefined" || src === null) { - return throwError(new Error("marked(): input parameter is undefined or null")); - } - if (typeof src !== "string") { - return throwError(new Error("marked(): input parameter is of type " + Object.prototype.toString.call(src) + ", string expected")); - } - if (opt.hooks) { - opt.hooks.options = opt; - opt.hooks.block = blockType; - } - const lexer2 = opt.hooks ? opt.hooks.provideLexer() : blockType ? _Lexer.lex : _Lexer.lexInline; - const parser2 = opt.hooks ? opt.hooks.provideParser() : blockType ? _Parser.parse : _Parser.parseInline; - if (opt.async) { - return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src).then((src2) => lexer2(src2, opt)).then((tokens) => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens).then((tokens) => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens).then((tokens) => parser2(tokens, opt)).then((html2) => opt.hooks ? opt.hooks.postprocess(html2) : html2).catch(throwError); - } - try { - if (opt.hooks) { - src = opt.hooks.preprocess(src); - } - let tokens = lexer2(src, opt); - if (opt.hooks) { - tokens = opt.hooks.processAllTokens(tokens); - } - if (opt.walkTokens) { - this.walkTokens(tokens, opt.walkTokens); - } - let html2 = parser2(tokens, opt); - if (opt.hooks) { - html2 = opt.hooks.postprocess(html2); - } - return html2; - } catch (e) { - return throwError(e); - } - }; - return parse2; - } - onError(silent, async) { - return (e) => { - e.message += "\nPlease report this to https://github.com/markedjs/marked."; - if (silent) { - const msg = "

    An error occurred:

    " + escape2(e.message + "", true) + "
    "; - if (async) { - return Promise.resolve(msg); - } - return msg; - } - if (async) { - return Promise.reject(e); - } - throw e; - }; - } -}; - -// src/marked.ts -var markedInstance = new Marked(); -function marked(src, opt) { - return markedInstance.parse(src, opt); -} -marked.options = marked.setOptions = function(options2) { - markedInstance.setOptions(options2); - marked.defaults = markedInstance.defaults; - changeDefaults(marked.defaults); - return marked; -}; -marked.getDefaults = _getDefaults; -marked.defaults = _defaults; -marked.use = function(...args) { - markedInstance.use(...args); - marked.defaults = markedInstance.defaults; - changeDefaults(marked.defaults); - return marked; -}; -marked.walkTokens = function(tokens, callback) { - return markedInstance.walkTokens(tokens, callback); -}; -marked.parseInline = markedInstance.parseInline; -marked.Parser = _Parser; -marked.parser = _Parser.parse; -marked.Renderer = _Renderer; -marked.TextRenderer = _TextRenderer; -marked.Lexer = _Lexer; -marked.lexer = _Lexer.lex; -marked.Tokenizer = _Tokenizer; -marked.Hooks = _Hooks; -marked.parse = marked; -var options = marked.options; -var setOptions = marked.setOptions; -var use = marked.use; -var walkTokens = marked.walkTokens; -var parseInline = marked.parseInline; -var parse = marked; -var parser = _Parser.parse; -var lexer = _Lexer.lex; - -if(__exports != exports)module.exports = exports;return module.exports})); -//# sourceMappingURL=marked.umd.js.map diff --git a/.pi/extensions/webui/public/settings.html b/.pi/extensions/webui/public/settings.html deleted file mode 100644 index 98bde48e..00000000 --- a/.pi/extensions/webui/public/settings.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - -设置 - 萌小芽 - - - - - - - - -
    -
    -
    - -
    -

    设置

    -

    萌小芽 Agent

    -
    -
    -
    返回聊天 -
    - -
    -
    - - -
    -
    -
    -
    -

    系统提示词

    -

    -
    - -
    - -
    -
    - -
    -
    -
    -

    Skills

    -

    -
    - -
    -
    -
    加载中...
    -
    -
    - -
    -
    -
    -

    插件扩展

    -

    -
    - -
    -
    -
    加载中...
    -
    -
    -
    -
    -
    -
    - - - - diff --git a/.pi/extensions/webui/public/settings.js b/.pi/extensions/webui/public/settings.js deleted file mode 100644 index 25e8649c..00000000 --- a/.pi/extensions/webui/public/settings.js +++ /dev/null @@ -1,177 +0,0 @@ -"use strict"; - -const systemPromptInput = document.getElementById("systemPromptInput"); -const systemPromptPath = document.getElementById("systemPromptPath"); -const saveSystemPromptBtn = document.getElementById("saveSystemPromptBtn"); -const refreshSettingsBtn = document.getElementById("refreshSettingsBtn"); -const settingsStatus = document.getElementById("settingsStatus"); -const skillsList = document.getElementById("skillsList"); -const skillsCount = document.getElementById("skillsCount"); -const extensionsList = document.getElementById("extensionsList"); -const extensionsMeta = document.getElementById("extensionsMeta"); -const settingsNavBtns = document.querySelectorAll(".settings-nav-btn"); -const settingsPanes = document.querySelectorAll(".settings-pane"); - -const SETTINGS_PANE_KEY = "sproutclaw-settings-pane"; - -function escHtml(s) { - const div = document.createElement("div"); - div.textContent = s; - return div.innerHTML; -} - -function setSettingsStatus(text, kind = "") { - if (!settingsStatus) return; - settingsStatus.textContent = text || ""; - settingsStatus.className = `settings-status${kind ? ` ${kind}` : ""}`; -} - -function showSettingsPane(paneId) { - const id = ["prompt", "skills", "extensions"].includes(paneId) ? paneId : "prompt"; - settingsNavBtns.forEach((btn) => { - const active = btn.dataset.pane === id; - btn.classList.toggle("is-active", active); - btn.setAttribute("aria-selected", active ? "true" : "false"); - }); - settingsPanes.forEach((pane) => { - const active = pane.id === `pane-${id}`; - pane.classList.toggle("is-active", active); - pane.toggleAttribute("hidden", !active); - }); - try { - sessionStorage.setItem(SETTINGS_PANE_KEY, id); - } catch { - /* ignore */ - } -} - -function initSettingsNav() { - settingsNavBtns.forEach((btn) => { - btn.addEventListener("click", () => showSettingsPane(btn.dataset.pane)); - }); - let initial = "prompt"; - try { - const saved = sessionStorage.getItem(SETTINGS_PANE_KEY); - if (["prompt", "skills", "extensions"].includes(saved)) initial = saved; - } catch { - /* ignore */ - } - showSettingsPane(initial); -} - -async function loadSettings() { - if (skillsList) skillsList.innerHTML = '
    加载中...
    '; - if (extensionsList) extensionsList.innerHTML = '
    加载中...
    '; - setSettingsStatus(""); - try { - const res = await fetch("/api/settings"); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) throw new Error(data.error || res.statusText); - if (systemPromptInput) systemPromptInput.value = data.systemPrompt || ""; - if (systemPromptPath) systemPromptPath.textContent = data.systemPromptPath || ""; - renderSkills(data.skills || []); - renderExtensions(data.extensions || [], data.extensionsPath || ""); - } catch (err) { - if (skillsList) skillsList.innerHTML = `
    加载失败: ${escHtml(err.message)}
    `; - if (extensionsList) extensionsList.innerHTML = `
    加载失败: ${escHtml(err.message)}
    `; - setSettingsStatus(`加载设置失败: ${err.message}`, "error"); - } -} - -function renderSkills(skills) { - if (!skillsList) return; - if (skillsCount) skillsCount.textContent = `${skills.length} 个已加载`; - if (!skills.length) { - skillsList.innerHTML = '
    暂无已加载 skills
    '; - return; - } - - skillsList.innerHTML = skills.map((skill) => { - const meta = [skill.scope, skill.source].filter(Boolean).join(" · "); - const path = skill.path || ""; - return `
    -
    -
    ${escHtml(skill.name || skill.command || "未命名")}
    - ${meta ? `
    ${escHtml(meta)}
    ` : ""} -
    - ${skill.description ? `
    ${escHtml(skill.description)}
    ` : ""} - ${path ? `
    ${escHtml(path)}
    ` : ""} -
    `; - }).join(""); -} - -function renderExtensions(extensions, extensionsPath) { - if (!extensionsList) return; - if (extensionsMeta) { - extensionsMeta.textContent = extensionsPath - ? `${extensions.length} 个已加载 · ${extensionsPath}` - : `${extensions.length} 个已加载`; - } - if (!extensions.length) { - extensionsList.innerHTML = '
    暂无已加载插件扩展
    '; - return; - } - - extensionsList.innerHTML = extensions.map((extension) => { - const meta = extension.kind || [extension.scope, extension.source].filter(Boolean).join(" · "); - const counts = [ - [`命令`, extension.commands?.length || 0], - [`工具`, extension.tools?.length || 0], - [`事件`, extension.handlers?.length || 0], - [`Flags`, extension.flags?.length || 0], - [`快捷键`, extension.shortcuts?.length || 0], - ].filter(([, count]) => count > 0); - const hasDetails = ["commands", "tools", "handlers", "flags", "shortcuts"].some((key) => extension[key]?.length); - return `
    -
    -
    ${escHtml(extension.name || extension.path || "未命名")}
    - ${meta ? `
    ${escHtml(meta)}
    ` : ""} -
    - ${counts.length ? `
    ${counts.map(([label, count]) => `${escHtml(label)} ${count}`).join("")}
    ` : ""} - ${hasDetails ? `
    - 查看注册项 - ${renderNameList("命令", extension.commands)} - ${renderNameList("工具", extension.tools)} - ${renderNameList("事件", extension.handlers)} - ${renderNameList("Flags", extension.flags)} - ${renderNameList("快捷键", extension.shortcuts)} -
    ` : ""} - ${extension.location ? `
    ${escHtml(extension.location)}
    ` : ""} -
    `; - }).join(""); -} - -function renderNameList(label, items) { - if (!Array.isArray(items) || !items.length) return ""; - return `
    - ${escHtml(label)} -
    ${items.map((item) => `${escHtml(item)}`).join("")}
    -
    `; -} - -async function saveSystemPrompt() { - if (!systemPromptInput || !saveSystemPromptBtn) return; - saveSystemPromptBtn.disabled = true; - setSettingsStatus("保存中..."); - try { - const res = await fetch("/api/settings/system-prompt", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ systemPrompt: systemPromptInput.value }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) throw new Error(data.error || res.statusText); - if (systemPromptPath && data.systemPromptPath) systemPromptPath.textContent = data.systemPromptPath; - setSettingsStatus("已保存,Agent 已重新加载", "ok"); - } catch (err) { - setSettingsStatus(`保存失败: ${err.message}`, "error"); - } finally { - saveSystemPromptBtn.disabled = false; - } -} - -initSettingsNav(); -saveSystemPromptBtn?.addEventListener("click", saveSystemPrompt); -refreshSettingsBtn?.addEventListener("click", loadSettings); -document.getElementById("refreshExtensionsBtn")?.addEventListener("click", loadSettings); -loadSettings(); diff --git a/.pi/extensions/webui/public/style.css b/.pi/extensions/webui/public/style.css deleted file mode 100644 index 75bcd478..00000000 --- a/.pi/extensions/webui/public/style.css +++ /dev/null @@ -1,1500 +0,0 @@ -:root { - --app-height: 100vh; - /* - * 全站字体栈(改 font-family 族前须先明确需求)。 - * UI:有网时 index.html 外链 Inter + Noto Sans SC;外链失败则仅用后续系统无衬线。 - * 拉丁优先 Inter,中文缺字形由 Noto Sans SC 补。 - * 代码:行内/块 code 与 pre → Source Code Pro(Google 文件名 source-code-pro)+ Menlo … - */ - --font-ui: Inter, "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", - "Segoe UI", system-ui, sans-serif; - --font-mono: "Source Code Pro", Menlo, Monaco, Consolas, "Courier New", monospace; -} - -/* ─── 重置与基础 ──────────────────────────────────────────────────── */ - -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - -webkit-text-size-adjust: 100%; - font-family: var(--font-ui); -} - -body { - font-family: inherit; - background: - radial-gradient(circle at top left, rgba(37, 99, 235, 0.06), transparent 28%), - linear-gradient(180deg, #f7f8fb 0%, #f2f4f8 100%); - color: #1a1a1a; - height: var(--app-height); - overflow: hidden; -} - -body.settings-page { - display: flex; - flex-direction: column; - min-height: var(--app-height); - height: var(--app-height); - overflow: hidden; - background: #fff; -} - -button, -input, -textarea, -select, -optgroup { - font-family: inherit; -} - -[hidden] { - display: none !important; -} - -/* 全文等宽:
    (含 fenced 与非 Markdown 片段) */
    -code,
    -kbd,
    -samp,
    -pre {
    -  font-family: var(--font-mono);
    -}
    -
    -/* highlight.js github 主题会写 SFMono/ui-monospace;统一回站点等宽栈 */
    -.hljs,
    -pre code.hljs,
    -code.hljs {
    -  font-family: var(--font-mono) !important;
    -}
    -
    -/* ─── 布局 ─────────────────────────────────────────────────────────── */
    -
    -#app {
    -  display: flex;
    -  height: var(--app-height);
    -  min-height: 0;
    -}
    -
    -/* ─── 侧边栏 ───────────────────────────────────────────────────────── */
    -
    -#sidebar {
    -  width: 236px;
    -  background: rgba(255, 255, 255, 0.94);
    -  backdrop-filter: blur(10px);
    -  border-right: 1px solid #e5e5e5;
    -  display: flex;
    -  flex-direction: column;
    -  flex-shrink: 0;
    -  z-index: 100;
    -  transition: transform 0.25s ease;
    -}
    -
    -.sidebar-header {
    -  padding: 18px 16px 10px;
    -  border-bottom: 1px solid #f0f0f0;
    -}
    -
    -.sidebar-brand {
    -  display: flex;
    -  align-items: center;
    -  gap: 10px;
    -  min-width: 0;
    -}
    -
    -.sidebar-logo {
    -  width: 36px;
    -  height: 36px;
    -  object-fit: contain;
    -  flex-shrink: 0;
    -}
    -
    -.sidebar-title {
    -  font-size: 17px;
    -  font-weight: 700;
    -  color: #1a1a1a;
    -  letter-spacing: -0.3px;
    -  margin: 0;
    -  min-width: 0;
    -  overflow: hidden;
    -  text-overflow: ellipsis;
    -  white-space: nowrap;
    -}
    -
    -.sidebar-subtitle {
    -  font-size: 12px;
    -  color: #999;
    -  margin-top: 2px;
    -}
    -
    -/* ─── 会话列表 ─────────────────────────────────────────────────────── */
    -
    -.sidebar-section {
    -  flex: 1;
    -  display: flex;
    -  flex-direction: column;
    -  min-height: 0;
    -  overflow: hidden;
    -}
    -
    -.section-header {
    -  display: flex;
    -  align-items: center;
    -  justify-content: space-between;
    -  padding: 10px 16px 6px;
    -  font-size: 11px;
    -  font-weight: 600;
    -  color: #999;
    -  text-transform: uppercase;
    -  letter-spacing: 0.5px;
    -}
    -
    -.section-refresh {
    -  background: none;
    -  border: none;
    -  color: #bbb;
    -  cursor: pointer;
    -  padding: 2px;
    -  border-radius: 4px;
    -  display: flex;
    -  transition: color 0.15s, background 0.15s;
    -}
    -
    -.section-refresh:hover {
    -  color: #666;
    -  background: #f0f0f0;
    -}
    -
    -.session-list {
    -  flex: 1;
    -  overflow-y: auto;
    -  padding: 0 8px 6px;
    -}
    -
    -.session-list::-webkit-scrollbar { width: 4px; }
    -.session-list::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
    -
    -.session-empty {
    -  padding: 16px 10px;
    -  color: #bbb;
    -  font-size: 12px;
    -  text-align: center;
    -}
    -
    -/* 会话条目 */
    -.session-item {
    -  display: grid;
    -  grid-template-columns: minmax(0, 1fr) 28px;
    -  align-items: center;
    -  width: 100%;
    -  background: transparent;
    -  border: none;
    -  border-radius: 10px;
    -  transition: background 0.15s;
    -  margin-bottom: 3px;
    -}
    -
    -.session-item:hover { background: #f5f5f5; }
    -.session-item.active { background: #f0f7ff; }
    -
    -.session-item-main {
    -  min-width: 0;
    -  text-align: left;
    -  padding: 8px 4px 8px 10px;
    -  background: transparent;
    -  border: none;
    -  cursor: pointer;
    -}
    -
    -.session-item-name {
    -  font-size: 13px;
    -  font-weight: 500;
    -  color: #1a1a1a;
    -  white-space: nowrap;
    -  overflow: hidden;
    -  text-overflow: ellipsis;
    -}
    -
    -.session-item-meta {
    -  font-size: 10px;
    -  color: #bbb;
    -  white-space: nowrap;
    -  overflow: hidden;
    -  text-overflow: ellipsis;
    -  margin-top: 4px;
    -}
    -
    -.session-delete-btn {
    -  width: 24px;
    -  height: 24px;
    -  display: flex;
    -  align-items: center;
    -  justify-content: center;
    -  justify-self: end;
    -  margin-right: 4px;
    -  background: transparent;
    -  border: none;
    -  border-radius: 6px;
    -  color: #bbb;
    -  cursor: pointer;
    -  opacity: 0;
    -  transition: background 0.15s, color 0.15s, opacity 0.15s;
    -}
    -
    -.session-item:hover .session-delete-btn,
    -.session-item.active .session-delete-btn,
    -.session-delete-btn:focus-visible {
    -  opacity: 1;
    -}
    -
    -.session-delete-btn:hover {
    -  color: #dc2626;
    -  background: #fee2e2;
    -}
    -
    -/* ─── 侧边栏操作按钮 ─────────────────────────────────────────────── */
    -
    -.sidebar-actions {
    -  padding: 8px 10px 6px;
    -  display: flex;
    -  flex-direction: column;
    -  gap: 2px;
    -}
    -
    -.sidebar-actions button {
    -  display: flex;
    -  align-items: center;
    -  gap: 8px;
    -  width: 100%;
    -  padding: 8px 10px;
    -  background: transparent;
    -  border: none;
    -  border-radius: 10px;
    -  color: #555;
    -  font-size: 13px;
    -  cursor: pointer;
    -  transition: background 0.15s, color 0.15s;
    -}
    -
    -.sidebar-actions button:hover {
    -  background: #f0f0f0;
    -  color: #1a1a1a;
    -}
    -
    -.sidebar-actions button svg {
    -  flex-shrink: 0;
    -}
    -
    -/* ─── 侧边栏底部状态 ──────────────────────────────────────────────── */
    -
    -.sidebar-status {
    -  padding: 12px 16px;
    -  border-top: 1px solid #f0f0f0;
    -}
    -
    -.status-row {
    -  display: flex;
    -  align-items: center;
    -  gap: 8px;
    -  font-size: 12px;
    -  color: #888;
    -}
    -
    -.status-dot {
    -  width: 8px;
    -  height: 8px;
    -  border-radius: 50%;
    -  flex-shrink: 0;
    -}
    -
    -.status-dot.connected { background: #22c55e; }
    -.status-dot.streaming { background: #f59e0b; animation: pulse 1.2s ease-in-out infinite; }
    -.status-dot.disconnected { background: #ef4444; }
    -
    -.status-model {
    -  font-size: 10px;
    -  color: #bbb;
    -  margin-top: 4px;
    -  white-space: nowrap;
    -  overflow: hidden;
    -  text-overflow: ellipsis;
    -}
    -
    -@keyframes pulse {
    -  0%, 100% { opacity: 1; }
    -  50% { opacity: 0.4; }
    -}
    -
    -/* ─── 移动端遮罩层 ────────────────────────────────────────────────── */
    -
    -.sidebar-overlay {
    -  display: none;
    -  position: fixed;
    -  inset: 0;
    -  background: rgba(0,0,0,0.3);
    -  z-index: 99;
    -}
    -
    -/* ─── 主区域 ───────────────────────────────────────────────────────── */
    -
    -#main {
    -  flex: 1;
    -  display: flex;
    -  flex-direction: column;
    -  min-width: 0;
    -  min-height: 0;
    -  background: #fff;
    -}
    -
    -/* ─── 顶部栏 ───────────────────────────────────────────────────────── */
    -
    -#header {
    -  display: flex;
    -  align-items: center;
    -  gap: 10px;
    -  padding: 12px 16px;
    -  border-bottom: 1px solid #f0f0f0;
    -  flex-shrink: 0;
    -  background: rgba(255, 255, 255, 0.92);
    -  backdrop-filter: blur(10px);
    -  flex-wrap: wrap;
    -}
    -
    -.header-trailing {
    -  margin-left: auto;
    -  display: flex;
    -  flex-direction: row;
    -  align-items: center;
    -  justify-content: flex-end;
    -  gap: 8px;
    -  flex-shrink: 0;
    -  max-width: min(68%, 680px);
    -  min-width: 0;
    -}
    -
    -.menu-btn {
    -  display: none;
    -  width: 34px;
    -  height: 34px;
    -  align-items: center;
    -  justify-content: center;
    -  background: transparent;
    -  border: none;
    -  border-radius: 8px;
    -  color: #555;
    -  cursor: pointer;
    -  flex-shrink: 0;
    -}
    -
    -.menu-btn:hover { background: #f0f0f0; color: #1a1a1a; }
    -
    -.header-info {
    -  flex: 1 1 auto;
    -  min-width: 0;
    -}
    -
    -.header-info h1 {
    -  font-size: 15px;
    -  font-weight: 600;
    -  color: #1a1a1a;
    -}
    -
    -.header-meta {
    -  font-size: 11px;
    -  color: #999;
    -}
    -
    -.header-btn {
    -  display: flex;
    -  align-items: center;
    -  gap: 6px;
    -  padding: 7px 12px;
    -  background: #fef2f2;
    -  border: 1px solid #fecaca;
    -  border-radius: 8px;
    -  color: #dc2626;
    -  font-size: 12px;
    -  cursor: pointer;
    -  transition: background 0.15s;
    -  flex-shrink: 0;
    -}
    -
    -.header-btn:hover { background: #fee2e2; }
    -
    -.model-controls {
    -  display: flex;
    -  align-items: center;
    -  gap: 6px;
    -  min-width: 0;
    -}
    -
    -.model-select,
    -.thinking-select {
    -  height: 30px;
    -  border: 1px solid #e5e7eb;
    -  border-radius: 8px;
    -  background: #fff;
    -  color: #374151;
    -  font-size: 11px;
    -  outline: none;
    -}
    -
    -.model-select {
    -  width: min(260px, 28vw);
    -}
    -
    -.thinking-select {
    -  width: 86px;
    -}
    -
    -.model-select:focus,
    -.thinking-select:focus {
    -  border-color: #93c5fd;
    -  box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.22);
    -}
    -
    -.model-select:disabled,
    -.thinking-select:disabled {
    -  color: #9ca3af;
    -  background: #f9fafb;
    -  cursor: not-allowed;
    -}
    -
    -/* ─── 主区上下文条(与 TUI Footer 统计行同源:get_state.stats + model) ─ */
    -
    -.session-context-bar {
    -  flex-shrink: 0;
    -  padding: 6px 16px 7px;
    -  border-bottom: 1px solid #f0f0f0;
    -  background: rgba(250, 251, 253, 0.97);
    -  font-size: 11px;
    -  line-height: 1.42;
    -  color: #5c6578;
    -}
    -
    -/* 并入顶栏右上角:不占整宽独立一行 */
    -.session-context-bar.session-context-in-header {
    -  border: none;
    -  background: transparent;
    -  padding: 0;
    -  max-width: 100%;
    -  text-align: right;
    -}
    -
    -.session-context-in-header .session-context-row {
    -  justify-content: flex-end;
    -}
    -
    -.session-context-in-header.session-context-bar .session-context-primary-wrap .session-context-row {
    -  flex-direction: column;
    -  align-items: flex-end;
    -  gap: 2px;
    -}
    -
    -.session-context-row {
    -  display: flex;
    -  flex-wrap: wrap;
    -  align-items: baseline;
    -  justify-content: space-between;
    -  gap: 10px;
    -}
    -
    -.session-context-left {
    -  flex: 1;
    -  min-width: 0;
    -  font-variant-numeric: tabular-nums;
    -  word-break: break-word;
    -}
    -
    -.session-context-in-header .session-context-left,
    -.session-context-in-header .session-context-right {
    -  flex: 0 1 auto;
    -  max-width: 100%;
    -  text-align: right;
    -}
    -
    -.session-context-right {
    -  flex-shrink: 0;
    -  text-align: right;
    -  opacity: 0.95;
    -  max-width: 46%;
    -}
    -
    -#sessionContextSecondary {
    -  margin-top: 3px;
    -  font-size: 10px;
    -  color: #8b95a8;
    -}
    -
    -.session-context-in-header #sessionContextSecondary {
    -  margin-top: 2px;
    -}
    -
    -#sessionContextSecondary:empty {
    -  display: none;
    -}
    -
    -.session-context-bar .ctx-warn {
    -  color: #b45309;
    -  font-weight: 500;
    -}
    -
    -.session-context-bar .ctx-danger {
    -  color: #b91c1c;
    -  font-weight: 500;
    -}
    -
    -/* ─── 聊天区域 ─────────────────────────────────────────────────────── */
    -
    -#chat {
    -  flex: 1;
    -  overflow-y: auto;
    -  padding: 12px 16px 10px;
    -  display: flex;
    -  flex-direction: column;
    -  gap: 6px;
    -  min-height: 0;
    -}
    -
    -#chat::-webkit-scrollbar { width: 6px; }
    -#chat::-webkit-scrollbar-track { background: transparent; }
    -#chat::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; }
    -#chat::-webkit-scrollbar-thumb:hover { background: #ccc; }
    -
    -/* 空状态 */
    -.empty-state {
    -  display: flex;
    -  flex-direction: column;
    -  align-items: center;
    -  justify-content: center;
    -  flex: 1;
    -  color: #bbb;
    -  text-align: center;
    -  gap: 8px;
    -}
    -
    -.empty-icon { color: #ddd; }
    -.empty-state p { font-size: 13px; }
    -.empty-hint { font-size: 11px; color: #ccc; }
    -
    -/* ─── 消息 ─────────────────────────────────────────────────────────── */
    -
    -.msg {
    -  max-width: min(700px, 100%);
    -  padding: 10px 12px;
    -  line-height: 1.4;
    -  font-size: 16px;
    -  word-wrap: break-word;
    -  white-space: pre-wrap;
    -}
    -
    -.msg.user {
    -  align-self: flex-end;
    -  background: linear-gradient(180deg, #f0f7ff 0%, #eaf2ff 100%);
    -  border: 1px solid #dbeafe;
    -  border-radius: 14px 14px 4px 14px;
    -  margin: 0 0 2px;
    -  color: #1a1a1a;
    -}
    -
    -.msg.assistant {
    -  align-self: flex-start;
    -  margin: 0;
    -  padding: 0;
    -  color: #1a1a1a;
    -  line-height: 1.32;
    -}
    -
    -.msg.system {
    -  align-self: center;
    -  color: #aaa;
    -  font-size: 11px;
    -  text-align: center;
    -  padding: 6px 10px;
    -}
    -
    -.msg.thinking {
    -  align-self: flex-start;
    -  color: #888;
    -  font-size: 13px;
    -  font-style: italic;
    -  padding: 8px 0 8px 16px;
    -  border-left: 3px solid #e5e5e5;
    -  margin: 0;
    -}
    -
    -.msg.tool_call {
    -  align-self: flex-start;
    -  font-size: 13px;
    -  font-family: var(--font-mono);
    -  color: #666;
    -  padding: 0;
    -  margin: 0 0 4px 12px;
    -  background: #fafafa;
    -  border-radius: 10px;
    -  border: 1px solid #eee;
    -  line-height: 1.4;
    -  max-width: min(680px, 100%);
    -}
    -
    -.tool-call-collapsible {
    -  margin: 0;
    -}
    -
    -.tool-call-collapsible > .tool-call-summary {
    -  list-style: none;
    -  cursor: pointer;
    -  display: flex;
    -  align-items: center;
    -  gap: 6px;
    -  padding: 6px 10px;
    -  user-select: none;
    -  font-weight: 500;
    -  color: #555;
    -}
    -
    -.tool-call-collapsible > .tool-call-summary::-webkit-details-marker {
    -  display: none;
    -}
    -
    -.tool-call-summary::before {
    -  content: "";
    -  width: 0;
    -  height: 0;
    -  border-left: 5px solid #888;
    -  border-top: 3.5px solid transparent;
    -  border-bottom: 3.5px solid transparent;
    -  flex-shrink: 0;
    -  transform: rotate(0deg);
    -  transition: transform 0.15s ease;
    -}
    -
    -.tool-call-collapsible[open] > .tool-call-summary::before {
    -  transform: rotate(90deg);
    -}
    -
    -.tool-call-summary-text {
    -  min-width: 0;
    -  flex: 1;
    -  overflow: hidden;
    -  text-overflow: ellipsis;
    -  white-space: nowrap;
    -}
    -
    -.tool-call-detail {
    -  margin: 0;
    -  padding: 6px 10px 8px;
    -  border-top: 1px solid #eee;
    -}
    -
    -.tool-call-detail-pre {
    -  margin: 0;
    -  padding: 0;
    -  white-space: pre-wrap;
    -  word-break: break-word;
    -  font-size: 12px;
    -  line-height: 1.45;
    -}
    -
    -.msg.tool_result {
    -  align-self: flex-start;
    -  font-size: 12px;
    -  font-family: var(--font-mono);
    -  color: #666;
    -  padding: 8px 12px;
    -  margin: 0 0 2px 14px;
    -  background: #fafafa;
    -  border-radius: 10px;
    -  border: 1px solid #eee;
    -  max-height: 200px;
    -  overflow-y: auto;
    -}
    -
    -/* ─── Markdown 渲染内容 ──────────────────────────────────────────── */
    -
    -.msg.assistant h1, .msg.assistant h2, .msg.assistant h3,
    -.msg.assistant h4, .msg.assistant h5, .msg.assistant h6 {
    -  margin: 0.2em 0 0.04em;
    -  font-weight: 600;
    -  line-height: 1.25;
    -}
    -.msg.assistant h1 { font-size: 1.22em; }
    -.msg.assistant h2 { font-size: 1.12em; }
    -.msg.assistant h3 { font-size: 1.04em; }
    -.msg.assistant h4 { font-size: 1em; }
    -
    -.msg.assistant p { margin: 0.03em 0; }
    -.msg.assistant p:first-child { margin-top: 0; }
    -.msg.assistant p:last-child { margin-bottom: 0; }
    -
    -.msg.assistant strong { font-weight: 600; }
    -.msg.assistant em { font-style: italic; }
    -
    -.msg.assistant a {
    -  color: #2563eb;
    -  text-decoration: none;
    -}
    -.msg.assistant a:hover { text-decoration: underline; }
    -
    -.msg.assistant ul, .msg.assistant ol {
    -  margin: 0.02em 0;
    -  padding-left: 1.05em;
    -}
    -.msg.assistant li {
    -  margin: 0;
    -}
    -.msg.assistant li + li {
    -  margin-top: 0.04em;
    -}
    -
    -/* marked 常在 
  • 内包一层或多层

    ,会与 li 边距叠加出大块留白 */ -.msg.assistant li > p { - margin: 0; -} -.msg.assistant li > p + p { - margin-top: 0.06em; -} - -.msg.assistant li ul, -.msg.assistant li ol { - margin: 0.03em 0 0.06em; -} - -.msg.assistant blockquote { - margin: 0.08em 0; - padding: 2px 8px; - border-left: 2px solid #e5e5e5; - color: #666; -} - -.msg.assistant hr { - border: none; - border-top: 1px solid #eee; - margin: 0.2em 0; -} - -.msg.assistant table { - border-collapse: collapse; - margin: 0.06em 0; - font-size: 13px; - width: 100%; -} -.msg.assistant th, .msg.assistant td { - border: 1px solid #e5e5e5; - padding: 4px 7px; - text-align: left; -} -.msg.assistant th { - background: #fafafa; - font-weight: 600; -} - -.msg.assistant code:not(.hljs) { - font-size: 0.9em; - background: #f5f5f5; - padding: 2px 6px; - border-radius: 4px; - color: #d63384; - word-break: break-word; -} - -.msg.assistant pre.assistant-pre { - margin: 0.35em 0; - padding: 0; - background: transparent; - border: 1px solid #e8edf2; - border-radius: 8px; - overflow-x: auto; - line-height: 1.5; -} - -.msg.assistant pre.assistant-pre code.hljs { - display: block; - padding: 12px 14px; - background: none; - border: none; - color: inherit; - font-size: 14px; - line-height: 1.5; - word-break: normal; - tab-size: 2; -} - -.msg.assistant img { - max-width: 100%; - border-radius: 8px; - margin: 0.06em 0; -} - -/* 流式光标 */ -.msg.streaming::after { - content: "▊"; - animation: blink 0.8s step-end infinite; - color: #2563eb; - margin-left: 1px; -} - -@keyframes blink { 50% { opacity: 0; } } - -/* 消息分隔线 */ -.msg.assistant + .msg.assistant { - border-top: 1px solid #f0f0f0; - padding-top: 4px; -} - -/* ─── 设置页 ─────────────────────────────────────────────────────── */ - -.settings-view { - flex: 1; - min-height: 0; - overflow-y: auto; - background: #fff; - padding: 18px 18px 22px; -} - -.settings-page-main { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - width: 100%; - padding: 0; -} - -.settings-page-header { - flex-shrink: 0; - width: 100%; - margin: 0; - padding: max(14px, env(safe-area-inset-top)) max(18px, env(safe-area-inset-right)) 14px max(18px, env(safe-area-inset-left)); - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - border-bottom: 1px solid #eef0f3; - background: #fafbfc; -} - -.settings-page-title { - display: flex; - align-items: center; - gap: 10px; - min-width: 0; -} - -.settings-page-title h1 { - font-size: 18px; - font-weight: 700; - color: #111827; - line-height: 1.2; -} - -.settings-page-title p { - margin-top: 2px; - font-size: 12px; - color: #8b95a8; -} - -.settings-page-shell { - flex: 1; - min-height: 0; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; -} - -.settings-link-btn { - display: inline-flex; - align-items: center; - justify-content: center; - text-decoration: none; -} - -/* 设置页:侧栏 + 单栏内容(移动端侧栏改为顶部分段) */ -.settings-layout { - flex: 1; - min-height: 0; - width: 100%; - display: flex; - flex-direction: row; - align-items: stretch; - overflow: hidden; -} - -.settings-sidebar { - flex-shrink: 0; - width: 220px; - display: flex; - flex-direction: column; - gap: 6px; - padding: 14px 12px; - padding-left: max(12px, env(safe-area-inset-left)); - background: #f4f6f9; - border-right: 1px solid #e5e7eb; -} - -.settings-nav-btn { - display: flex; - align-items: center; - width: 100%; - margin: 0; - padding: 11px 14px; - border: 1px solid transparent; - border-radius: 10px; - background: transparent; - font-family: inherit; - font-size: 14px; - font-weight: 500; - color: #4b5563; - text-align: left; - cursor: pointer; - transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s; - touch-action: manipulation; -} - -.settings-nav-btn:hover { - background: rgba(255, 255, 255, 0.72); - color: #111827; -} - -.settings-nav-btn.is-active { - background: #fff; - color: #2563eb; - border-color: #e5e7eb; - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); -} - -.settings-nav-btn:focus-visible { - outline: 2px solid rgba(37, 99, 235, 0.45); - outline-offset: 2px; -} - -.settings-nav-btn-label { - min-width: 0; -} - -.settings-panes { - flex: 1; - min-width: 0; - min-height: 0; - display: flex; - flex-direction: column; - padding: 14px max(14px, env(safe-area-inset-right)) max(14px, env(safe-area-inset-bottom)) 14px; -} - -.settings-pane:not(.is-active) { - display: none !important; -} - -.settings-pane.is-active { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; -} - -.settings-panel { - border: 1px solid #e5e7eb; - border-radius: 8px; - background: #fff; - min-width: 0; - min-height: 0; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.settings-panel-header { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; - row-gap: 8px; - padding: 12px 14px; - border-bottom: 1px solid #eef0f3; - background: #fafbfc; -} - -.settings-panel-header h2 { - font-size: 14px; - line-height: 1.3; - font-weight: 600; - color: #111827; -} - -.settings-panel-header p { - margin-top: 3px; - font-size: 11px; - color: #8b95a8; - word-break: break-all; -} - -.settings-primary-btn, -.settings-secondary-btn { - height: 30px; - padding: 0 12px; - border-radius: 7px; - font-size: 12px; - font-weight: 500; - cursor: pointer; - flex-shrink: 0; - transition: background 0.15s, border-color 0.15s, color 0.15s, opacity 0.15s; -} - -.settings-primary-btn { - color: #fff; - background: #2563eb; - border: 1px solid #2563eb; -} - -.settings-primary-btn:hover { - background: #1d4ed8; - border-color: #1d4ed8; -} - -.settings-primary-btn:disabled { - opacity: 0.55; - cursor: not-allowed; -} - -.settings-secondary-btn { - color: #374151; - background: #fff; - border: 1px solid #d1d5db; -} - -.settings-secondary-btn:hover { - background: #f3f4f6; -} - -.settings-textarea { - display: block; - width: 100%; - flex: 1 1 auto; - min-height: 0; - padding: 14px; - border: none; - outline: none; - resize: none; - color: #111827; - background: #fff; - font-family: var(--font-mono); - font-size: 13px; - line-height: 1.55; - white-space: pre; - overflow: auto; -} - -.settings-textarea:focus { - box-shadow: inset 0 0 0 2px rgba(37, 99, 235, 0.18); -} - -.settings-status { - flex-shrink: 0; - min-height: 30px; - padding: 7px 14px 9px; - border-top: 1px solid #eef0f3; - color: #8b95a8; - font-size: 12px; -} - -.settings-status.ok { - color: #15803d; -} - -.settings-status.error { - color: #b91c1c; -} - -.skills-list, -.extensions-list { - flex: 1 1 auto; - min-height: 0; - overflow-y: auto; - padding: 8px; -} - -.skill-item, -.extension-item { - padding: 10px 10px 9px; - border: 1px solid #edf0f4; - border-radius: 8px; - background: #fff; -} - -.skill-item + .skill-item, -.extension-item + .extension-item { - margin-top: 7px; -} - -.skill-title-row, -.extension-title-row { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 8px; - min-width: 0; -} - -.skill-name, -.extension-name { - min-width: 0; - color: #111827; - font-size: 13px; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.skill-meta, -.extension-meta { - flex-shrink: 0; - color: #9ca3af; - font-size: 10px; -} - -.skill-desc { - margin-top: 5px; - color: #4b5563; - font-size: 12px; - line-height: 1.45; -} - -.skill-path { - margin-top: 6px; - color: #9ca3af; - font-family: var(--font-mono); - font-size: 10px; - line-height: 1.4; - word-break: break-all; -} - -.extension-counts { - display: flex; - flex-wrap: wrap; - gap: 5px; - margin-top: 7px; -} - -.extension-counts span { - display: inline-flex; - align-items: center; - height: 20px; - padding: 0 7px; - border-radius: 999px; - background: #f3f4f6; - color: #4b5563; - font-size: 11px; - line-height: 1; -} - -.extension-details { - margin-top: 8px; - border-top: 1px solid #f1f3f6; - padding-top: 7px; -} - -.extension-details > summary { - cursor: pointer; - color: #6b7280; - font-size: 11px; - user-select: none; -} - -.extension-details > summary:hover { - color: #2563eb; -} - -.extension-name-list { - margin-top: 8px; - display: grid; - grid-template-columns: 42px minmax(0, 1fr); - gap: 8px; - align-items: start; - font-size: 11px; - color: #8b95a8; -} - -.extension-name-list > div { - display: flex; - flex-wrap: wrap; - gap: 5px; - min-width: 0; -} - -.extension-name-list code { - padding: 2px 6px; - border-radius: 5px; - background: #f8fafc; - border: 1px solid #edf0f4; - color: #374151; - font-family: var(--font-mono); - font-size: 10px; - word-break: break-all; -} - -.extension-path { - margin-top: 8px; - color: #9ca3af; - font-family: var(--font-mono); - font-size: 10px; - line-height: 1.4; - word-break: break-all; -} - -.settings-empty { - padding: 20px 10px; - color: #9ca3af; - font-size: 12px; - text-align: center; -} - -/* ─── 输入区域 ─────────────────────────────────────────────────────── */ - -#inputArea { - padding: 10px 16px 12px; - border-top: 1px solid #f0f0f0; - flex-shrink: 0; - background: rgba(255, 255, 255, 0.92); - backdrop-filter: blur(10px); -} - -.input-wrapper { - display: flex; - gap: 8px; - align-items: flex-end; - max-width: 720px; - margin: 0 auto; - background: #f6f7fb; - border: 1px solid #e5e5e5; - border-radius: 14px; - padding: 4px 4px 4px 12px; - transition: border-color 0.15s; -} - -.input-wrapper:focus-within { - border-color: #2563eb; - background: #fff; -} - -#input { - flex: 1; - background: transparent; - border: none; - color: #1a1a1a; - padding: 8px 0; - font-size: 16px; - font-family: inherit; - resize: none; - outline: none; - min-height: 24px; - max-height: 120px; - line-height: 1.45; -} - -#input::placeholder { color: #bbb; } - -#sendBtn { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - background: #2563eb; - border: none; - border-radius: 11px; - color: #fff; - cursor: pointer; - flex-shrink: 0; - transition: background 0.15s, opacity 0.15s; - touch-action: manipulation; -} - -#sendBtn:hover { background: #1d4ed8; } -#sendBtn:disabled { background: #d1d5db; cursor: not-allowed; } - -/* ─── 移动端适配 ──────────────────────────────────────────────────── */ - -@media (max-width: 768px) { - body, - #app { - height: var(--app-height); - } - - #sidebar { - position: fixed; - top: 0; - left: 0; - bottom: 0; - transform: translateX(-100%); - } - - #sidebar.open { - transform: translateX(0); - } - - .sidebar-overlay.open { - display: block; - } - - .menu-btn { - display: flex; - } - - #header { - padding: 10px 12px; - align-items: flex-start; - } - - .header-trailing { - max-width: 100%; - width: 100%; - flex-basis: 100%; - margin-left: 0; - justify-content: flex-start; - padding-left: 44px; - box-sizing: border-box; - flex-wrap: wrap; - } - - .model-controls { - width: 100%; - } - - .model-select { - width: min(100%, 260px); - flex: 1 1 180px; - } - - .session-context-bar:not(.session-context-in-header) { - padding: 5px 12px 6px; - font-size: 10px; - } - - .session-context-in-header { - font-size: 10px; - } - - #sessionContextSecondary { - font-size: 9px; - } - - #chat { - padding: 10px 12px 8px; - } - - .settings-view { - padding: 12px; - } - - .settings-page-main { - padding: 0; - } - - .settings-page-header { - align-items: flex-start; - padding-top: max(12px, env(safe-area-inset-top)); - padding-bottom: 12px; - padding-left: max(12px, env(safe-area-inset-left)); - padding-right: max(12px, env(safe-area-inset-right)); - } - - .settings-layout { - flex-direction: column; - } - - .settings-sidebar { - width: 100%; - flex-direction: row; - align-items: stretch; - gap: 8px; - padding: 10px max(12px, env(safe-area-inset-left)) 10px max(12px, env(safe-area-inset-right)); - border-right: none; - border-bottom: 1px solid #e5e7eb; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - } - - .settings-sidebar::-webkit-scrollbar { - height: 0; - width: 0; - } - - .settings-nav-btn { - flex: 1 1 0; - min-width: 0; - justify-content: center; - text-align: center; - padding: 12px 8px; - font-size: 13px; - } - - .settings-panes { - flex: 1; - min-height: 0; - padding: 10px max(10px, env(safe-area-inset-left)) max(12px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-right)); - } - - .settings-textarea { - font-size: 12px; - } - - .msg { - padding: 11px 12px; - font-size: 16px; - } - - .msg.user { - max-width: 88%; - } - - .msg.assistant { - padding-left: 0; - } - - .msg.tool_call, - .msg.tool_result { - margin-left: 0; - font-size: 12px; - } - - #inputArea { - position: sticky; - bottom: 0; - z-index: 5; - padding: 8px 10px calc(10px + env(safe-area-inset-bottom)); - } - - .input-wrapper { - padding: 2px 2px 2px 10px; - } -} - -@media (max-width: 480px) { - .msg.user { - max-width: 92%; - padding: 10px 12px; - font-size: 16px; - } - - .msg.assistant { - font-size: 16px; - padding: 10px 0; - } -} - -/* ─── 动效偏好 ───────────────────────────────────────────────────── */ - -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - transition-duration: 0.01ms !important; - animation-duration: 0.01ms !important; - } -} diff --git a/.pi/extensions/webui/server.ts b/.pi/extensions/webui/server.ts deleted file mode 100644 index d03a13f1..00000000 --- a/.pi/extensions/webui/server.ts +++ /dev/null @@ -1,552 +0,0 @@ -#!/usr/bin/env node - -/** - * pi-mono WebUI Server - * - * 被 webui 扩展启动,作为独立子进程运行。 - * 内嵌 HTTP 服务 + pi RPC 子进程,提供浏览器聊天界面。 - * - * 用法(由扩展自动调用): - * tsx server.ts --port 19133 - * - * 前端静态文件位于同目录 public/ 下。 - */ - -import { spawn } from "node:child_process"; -import { createServer } from "node:http"; -import { readFileSync, readdirSync, statSync, existsSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs"; -import { join, dirname, resolve, basename } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const PUBLIC_DIR = join(__dirname, "public"); - -// 从 CWD 确定 repo 根目录(启动时 CWD 为 repo 根目录) -const REPO_ROOT = process.cwd(); -const SESSIONS_DIR = join(REPO_ROOT, ".pi", "sessions"); -const PROJECT_EXTENSIONS_DIR = join(REPO_ROOT, ".pi", "extensions"); -const SYSTEM_PROMPT_FILE = join(REPO_ROOT, ".pi", "agent", "AGENTS.md"); -const TSX_BIN = join(REPO_ROOT, "node_modules", ".bin", "tsx"); - -// ─── 解析 CLI 参数 ───────────────────────────────────────────────── - -const args = process.argv.slice(2); -function getArg(flag: string): string | undefined { - const idx = args.indexOf(flag); - return idx !== -1 ? args[idx + 1] : undefined; -} -const PORT = parseInt(getArg("--port") || "19133", 10); - -// ─── 启动 pi RPC 子进程 ──────────────────────────────────────────── - -const piArgs = [ - join(REPO_ROOT, "packages", "coding-agent", "src", "cli.ts"), - "--mode", "rpc", -]; - -console.log(`[webui] 启动 pi RPC: ${TSX_BIN} ${piArgs.join(" ")}`); - -const pi = spawn(TSX_BIN, piArgs, { - cwd: REPO_ROOT, - stdio: ["pipe", "pipe", "pipe"], - env: { - ...process.env, - PI_CODING_AGENT_DIR: join(REPO_ROOT, ".pi", "agent"), - }, -}); - -pi.stderr.on("data", (data) => process.stderr.write(`[pi] ${data}`)); -pi.on("exit", (code) => { - console.log(`[webui] pi 退出, code=${code}`); - process.exit(1); -}); - -// ─── JSONL 行协议 ────────────────────────────────────────────────── - -let buffer = ""; -const pending = new Map void; reject: (e: Error) => void }>(); -const sseClients = new Set(); -let reqId = 0; - -function onLine(line: string) { - if (!line.trim()) return; - try { - const msg = JSON.parse(line); - if (msg.type === "response" && msg.id && pending.has(msg.id)) { - const p = pending.get(msg.id)!; - pending.delete(msg.id); - p.resolve(msg); - return; - } - const data = `data: ${JSON.stringify(msg)}\n\n`; - for (const res of sseClients) res.write(data); - } catch { - // 忽略非 JSON 行 - } -} - -pi.stdout.on("data", (chunk: Buffer) => { - buffer += chunk.toString(); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - for (const line of lines) onLine(line); -}); - -function sendCmd(command: Record): Promise { - return new Promise((resolve, reject) => { - const id = `req_${++reqId}`; - const line = JSON.stringify({ ...command, id }) + "\n"; - pending.set(id, { resolve, reject }); - pi.stdin.write(line); - setTimeout(() => { - if (pending.has(id)) { - pending.delete(id); - reject(new Error(`命令超时: ${command.type}`)); - } - }, 60000); - }); -} - -// ─── 会话文件读取 ────────────────────────────────────────────────── - -function listSessionFiles(): string[] { - if (!existsSync(SESSIONS_DIR)) return []; - return readdirSync(SESSIONS_DIR) - .filter((f) => f.endsWith(".jsonl")) - .sort() - .reverse() - .map((f) => join(SESSIONS_DIR, f)); -} - -function readSessionSummary(filePath: string): Record | null { - try { - const content = readFileSync(filePath, "utf8"); - const lines = content.trim().split("\n"); - if (!lines.length) return null; - const header = JSON.parse(lines[0]); - if (header.type !== "session") return null; - - let nameFromInfo = ""; - let messageCount = 0; - let firstMessage = ""; - const stats = statSync(filePath); - - for (const line of lines) { - if (!line.trim()) continue; - try { - const entry = JSON.parse(line); - if (entry.type === "session_info" && entry.name) { - const n = String(entry.name).trim(); - if (n && !isMachineSessionLabel(n, header.id)) { - nameFromInfo = n; - } - } - if (entry.type === "message") { - messageCount++; - if (!firstMessage && entry.message?.role === "user") { - firstMessage = extractPreview(entry.message); - } - } - } catch { /* skip */ } - } - - const fromFirstUser = titleFromFirstUserMessage(firstMessage); - let name = nameFromInfo; - if (!name || isMachineSessionLabel(name, header.id)) { - name = fromFirstUser || ""; - } - - return { - path: filePath, - id: header.id, - name, - created: header.timestamp, - modified: stats.mtime.toISOString(), - messageCount, - firstMessage: firstMessage || "(空)", - }; - } catch { - return null; - } -} - -function isMachineSessionLabel(text: string, sessionHeaderId: string): boolean { - const t = (text ?? "").trim(); - if (!t) return true; - if (sessionHeaderId && t === sessionHeaderId) return true; - if (/^[0-9a-f]{8,}$/i.test(t)) return true; - if (/^[0-9]{10,}$/.test(t)) return true; - return false; -} - -/** 侧边栏标题:优先用户首句(遇句号等截断),否则整段截取 */ -function titleFromFirstUserMessage(text: string, maxChars = 56): string { - const cleaned = String(text ?? "").replace(/\s+/g, " ").trim(); - if (!cleaned) return ""; - const sentenceMatch = cleaned.match(/^(.+?[。!?.!?])(\s|$)/); - let candidate = - sentenceMatch && sentenceMatch[1] ? sentenceMatch[1].trim() : cleaned; - if (candidate.length > maxChars) { - candidate = `${candidate.slice(0, maxChars).trimEnd()}…`; - } - return candidate; -} - -function readSessionMessages(filePath: string): unknown[] { - try { - const content = readFileSync(filePath, "utf8"); - return content - .trim() - .split("\n") - .map((l) => { try { const e = JSON.parse(l); return e.type === "message" ? e.message : null; } catch { return null; } }) - .filter(Boolean); - } catch { - return []; - } -} - -function extractPreview(msg: any): string { - const c = msg.content; - if (!c) return ""; - if (typeof c === "string") return c.slice(0, 200); - if (Array.isArray(c)) return c.filter((x: any) => x.type === "text").map((x: any) => x.text).join("").slice(0, 200); - return ""; -} - -function resolveSessionFile(filePath: string): string { - const resolved = resolve(filePath); - const sessionsRoot = resolve(SESSIONS_DIR); - if (!resolved.startsWith(`${sessionsRoot}/`) || !resolved.endsWith(".jsonl")) { - throw new Error("无效会话路径"); - } - return resolved; -} - -function readSystemPrompt(): string { - if (!existsSync(SYSTEM_PROMPT_FILE)) return ""; - return readFileSync(SYSTEM_PROMPT_FILE, "utf8"); -} - -function writeSystemPrompt(content: string): void { - mkdirSync(dirname(SYSTEM_PROMPT_FILE), { recursive: true }); - writeFileSync(SYSTEM_PROMPT_FILE, content, "utf8"); -} - -function normalizeSkillCommand(command: any): Record { - const rawName = String(command.name || ""); - const sourceInfo = command.sourceInfo || {}; - return { - name: rawName.replace(/^skill:/, ""), - command: rawName, - description: command.description || "", - scope: sourceInfo.scope || "", - source: sourceInfo.source || "", - path: sourceInfo.path || "", - }; -} - -async function listLoadedSkills(): Promise[]> { - const response = await sendCmd({ type: "get_commands" }); - if (!response.success) throw new Error(response.error || "读取 skills 失败"); - const commands = response.data?.commands || []; - return commands - .filter((command: any) => command?.source === "skill") - .map(normalizeSkillCommand) - .sort((a: any, b: any) => String(a.name).localeCompare(String(b.name))); -} - -function normalizeExtension(extension: any): Record { - const sourceInfo = extension.sourceInfo || {}; - const pathValue = String(extension.path || ""); - const source = String(sourceInfo.source || ""); - const resolvedPath = String(extension.resolvedPath || ""); - return { - name: displayExtensionName(pathValue, source), - rawName: extension.name || "", - path: pathValue, - resolvedPath, - scope: sourceInfo.scope || "", - source, - sourcePath: sourceInfo.path || "", - location: displayExtensionLocation(pathValue, resolvedPath), - kind: displayExtensionKind(sourceInfo.scope, source), - commands: extension.commands || [], - tools: extension.tools || [], - flags: extension.flags || [], - shortcuts: extension.shortcuts || [], - handlers: extension.handlers || [], - }; -} - -function isProjectExtension(extension: any): boolean { - const pathValue = String(extension.path || extension.resolvedPath || ""); - if (!pathValue) return false; - const resolved = resolve(pathValue); - const projectExtensionsRoot = resolve(PROJECT_EXTENSIONS_DIR); - return resolved === projectExtensionsRoot || resolved.startsWith(`${projectExtensionsRoot}/`); -} - -function displayExtensionName(extensionPath: string, source: string): string { - const sourceMatch = source.match(/^npm:(.+)$/); - if (sourceMatch?.[1]) return sourceMatch[1]; - - const normalized = extensionPath.replace(/\\/g, "/"); - const parts = normalized.split("/").filter(Boolean); - const file = parts[parts.length - 1] || normalized; - if (/^index\.[tj]s$/i.test(file) && parts.length >= 2) { - return cleanExtensionName(parts[parts.length - 2]); - } - return cleanExtensionName(file); -} - -function cleanExtensionName(name: string): string { - return basename(name).replace(/\.[cm]?[tj]s$/i, ""); -} - -function displayExtensionKind(scope: string, source: string): string { - const scopeText = scope === "project" ? "项目" : scope === "user" ? "用户" : scope || "未知"; - if (source.startsWith("npm:")) return `${scopeText} NPM`; - if (source === "auto") return `${scopeText}本地`; - return source ? `${scopeText} · ${source}` : scopeText; -} - -function displayExtensionLocation(extensionPath: string, resolvedPath: string): string { - const pathValue = extensionPath || resolvedPath; - if (!pathValue) return ""; - return pathValue.replace(REPO_ROOT, "."); -} - -async function listLoadedExtensions(): Promise[]> { - const response = await sendCmd({ type: "get_extensions" }); - if (!response.success) throw new Error(response.error || "读取扩展失败"); - return (response.data?.extensions || []) - .filter(isProjectExtension) - .map(normalizeExtension) - .sort((a: any, b: any) => String(a.name).localeCompare(String(b.name))); -} - -// ─── HTTP 服务 ───────────────────────────────────────────────────── - -const MIME: Record = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".png": "image/png", - ".svg": "image/svg+xml", -}; - -function serveStatic(urlPath: string, res: any): void { - const file = urlPath === "/" ? "/index.html" : urlPath; - const full = join(PUBLIC_DIR, file); - if (!full.startsWith(PUBLIC_DIR)) { res.writeHead(403); res.end("Forbidden"); return; } - if (!existsSync(full)) { res.writeHead(404); res.end("Not found"); return; } - const ext = file.match(/\.\w+$/)?.[0] || ".html"; - res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" }); - res.end(readFileSync(full)); -} - -function json(res: any, data: unknown, status = 200): void { - res.writeHead(status, { "Content-Type": "application/json" }); - res.end(JSON.stringify(data)); -} - -function readBody(req: any): Promise> { - return new Promise((resolve, reject) => { - let body = ""; - req.on("data", (c: Buffer) => (body += c.toString())); - req.on("end", () => { try { resolve(JSON.parse(body)); } catch { reject(new Error("无效 JSON")); } }); - req.on("error", reject); - }); -} - -const server = createServer((req, res) => { - const url = new URL(req.url!, `http://localhost:${PORT}`); - - // 静态文件 - if (req.method === "GET" && !url.pathname.startsWith("/api/")) { - return serveStatic(url.pathname, res); - } - - // POST /api/chat - if (req.method === "POST" && url.pathname === "/api/chat") { - return readBody(req) - .then(({ message }) => sendCmd({ type: "prompt", message }).then(() => json(res, { ok: true }))) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // GET /api/events (SSE) - if (req.method === "GET" && url.pathname === "/api/events") { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }); - res.write('data: {"type":"connected"}\n\n'); - sseClients.add(res); - req.on("close", () => sseClients.delete(res)); - return; - } - - // POST /api/messages - if (req.method === "POST" && url.pathname === "/api/messages") { - return sendCmd({ type: "get_messages" }) - .then((r: any) => (r.success ? json(res, r.data) : json(res, { error: r.error }, 500))) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/new-session - if (req.method === "POST" && url.pathname === "/api/new-session") { - return sendCmd({ type: "new_session" }) - .then((r: any) => (r.success ? json(res, r.data) : json(res, { error: r.error }, 500))) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/abort - if (req.method === "POST" && url.pathname === "/api/abort") { - return sendCmd({ type: "abort" }) - .then(() => json(res, { ok: true })) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // GET /api/models - if (req.method === "GET" && url.pathname === "/api/models") { - return sendCmd({ type: "get_available_models" }) - .then((r: any) => (r.success ? json(res, r.data) : json(res, { error: r.error }, 500))) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/model - if (req.method === "POST" && url.pathname === "/api/model") { - return readBody(req) - .then(({ provider, modelId }) => - sendCmd({ type: "set_model", provider, modelId }).then((r: any) => - r.success ? json(res, { model: r.data }) : json(res, { error: r.error }, 500), - ), - ) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/thinking - if (req.method === "POST" && url.pathname === "/api/thinking") { - return readBody(req) - .then(({ level }) => - sendCmd({ type: "set_thinking_level", level }).then((r: any) => - r.success ? json(res, { ok: true }) : json(res, { error: r.error }, 500), - ), - ) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // GET /api/settings - if (req.method === "GET" && url.pathname === "/api/settings") { - return Promise.all([listLoadedSkills(), listLoadedExtensions()]) - .then(([skills, extensions]) => json(res, { - systemPrompt: readSystemPrompt(), - systemPromptPath: SYSTEM_PROMPT_FILE, - extensionsPath: PROJECT_EXTENSIONS_DIR, - skills, - extensions, - })) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/settings/system-prompt - if (req.method === "POST" && url.pathname === "/api/settings/system-prompt") { - return readBody(req) - .then(async ({ systemPrompt }) => { - if (typeof systemPrompt !== "string") { - throw new Error("systemPrompt 必须是字符串"); - } - writeSystemPrompt(systemPrompt); - const reload = await sendCmd({ type: "reload" }); - if (!reload.success) throw new Error(reload.error || "Agent 重新加载失败"); - json(res, { ok: true, systemPromptPath: SYSTEM_PROMPT_FILE }); - }) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // GET /api/sessions - if (req.method === "GET" && url.pathname === "/api/sessions") { - const summaries = listSessionFiles().map(readSessionSummary).filter(Boolean); - return json(res, { sessions: summaries }); - } - - // POST /api/sessions/history - if (req.method === "POST" && url.pathname === "/api/sessions/history") { - return readBody(req) - .then(({ path: sp }) => { - const messages = readSessionMessages(sp as string); - const summary = readSessionSummary(sp as string); - json(res, { messages, session: summary }); - }) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/sessions/delete - if (req.method === "POST" && url.pathname === "/api/sessions/delete") { - return readBody(req) - .then(({ path: sp }) => { - const sessionPath = resolveSessionFile(sp as string); - if (!existsSync(sessionPath)) throw new Error("会话不存在"); - unlinkSync(sessionPath); - json(res, { ok: true }); - }) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/sessions/load - if (req.method === "POST" && url.pathname === "/api/sessions/load") { - return readBody(req) - .then(async ({ path: sp }) => { - const sw = await sendCmd({ type: "switch_session", sessionPath: sp, cwdOverride: REPO_ROOT }); - if (!sw.success) throw new Error(sw.error); - const mr = await sendCmd({ type: "get_messages" }); - if (!mr.success) throw new Error(mr.error); - const summary = readSessionSummary(sp as string); - json(res, { messages: mr.data.messages, session: summary }); - }) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/sessions/activate - if (req.method === "POST" && url.pathname === "/api/sessions/activate") { - return readBody(req) - .then(async ({ path: sp }) => { - const sw = await sendCmd({ type: "switch_session", sessionPath: sp, cwdOverride: REPO_ROOT }); - if (!sw.success) throw new Error(sw.error); - json(res, { ok: true }); - }) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // POST /api/sessions/name - if (req.method === "POST" && url.pathname === "/api/sessions/name") { - return readBody(req) - .then(({ name }) => sendCmd({ type: "set_session_name", name }).then(() => json(res, { ok: true }))) - .catch((err) => json(res, { error: err.message }, 500)); - } - - // GET /api/session-state - if (req.method === "GET" && url.pathname === "/api/session-state") { - return sendCmd({ type: "get_state" }) - .then((r: any) => (r.success ? json(res, r.data) : json(res, { error: r.error }, 500))) - .catch((err) => json(res, { error: err.message }, 500)); - } - - res.writeHead(404); - res.end("Not found"); -}); - -server.on("error", (err: NodeJS.ErrnoException) => { - if (err.code === "EADDRINUSE") { - console.error(`[webui] 端口 ${PORT} 已被占用`); - } else { - console.error(`[webui] HTTP 服务启动失败: ${err.message}`); - } - process.exit(1); -}); - -server.listen(PORT, "0.0.0.0", () => { - console.log(`[webui] HTTP 服务已启动: http://localhost:${PORT}`); - console.log(`[webui] 局域网访问: http://smallmengya:${PORT}`); -}); diff --git a/.pi/git/.gitignore b/.pi/git/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/.pi/git/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/.pi/npm/.gitignore b/.pi/npm/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/.pi/npm/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/.pi/prompts/cl.md b/.pi/prompts/cl.md deleted file mode 100644 index 592d2d67..00000000 --- a/.pi/prompts/cl.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -description: Audit changelog entries before release ---- -Audit changelog entries for all commits since the last release. - -## Process - -1. **Find the last release tag:** - ```bash - git tag --sort=-version:refname | head -1 - ``` - -2. **List all commits since that tag:** - ```bash - git log ..HEAD --oneline - ``` - -3. **Read each package's [Unreleased] section:** - - packages/ai/CHANGELOG.md - - packages/tui/CHANGELOG.md - - packages/coding-agent/CHANGELOG.md - -4. **For each commit, check:** - - Skip: changelog updates, doc-only changes, release housekeeping - - Skip: changes to generated model catalogs (for example `packages/ai/src/models.generated.ts`) unless accompanied by an intentional product-facing change in non-generated source/docs. - - Determine which package(s) the commit affects (use `git show --stat`) - - Verify a changelog entry exists in the affected package(s) - - For external contributions (PRs), verify format: `Description ([#N](url) by [@user](url))` - -5. **Cross-package duplication rule:** - Changes in `ai`, `agent` or `tui` that affect end users should be duplicated to `coding-agent` changelog, since coding-agent is the user-facing package that depends on them. - -6. **Add New Features section after changelog fixes:** - - Insert a `### New Features` section at the start of `## [Unreleased]` in `packages/coding-agent/CHANGELOG.md`. - - Propose the top new features to the user for confirmation before writing them. - - Link to relevant docs and sections whenever possible. - -7. **Report:** - - List commits with missing entries - - List entries that need cross-package duplication - - Add any missing entries directly - -## Changelog Format Reference - -Sections (in order): -- `### Breaking Changes` - API changes requiring migration -- `### Added` - New features -- `### Changed` - Changes to existing functionality -- `### Fixed` - Bug fixes -- `### Removed` - Removed features - -Attribution: -- Internal: `Fixed foo ([#123](https://github.com/earendil-works/pi-mono/issues/123))` -- External: `Added bar ([#456](https://github.com/earendil-works/pi-mono/pull/456) by [@user](https://github.com/user))` diff --git a/.pi/prompts/is.md b/.pi/prompts/is.md deleted file mode 100644 index 3dee06a5..00000000 --- a/.pi/prompts/is.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -description: Analyze GitHub issues (bugs or feature requests) -argument-hint: "" ---- -Analyze GitHub issue(s): $ARGUMENTS - -For each issue: - -1. Add the `inprogress` label to the issue via GitHub CLI before analysis starts. If adding the label fails, report that explicitly and continue. -2. Read the issue in full, including all comments and linked issues/PRs. -3. Do not trust analysis written in the issue. Independently verify behavior and derive your own analysis from the code and execution path. - -4. **For bugs**: - - Ignore any root cause analysis in the issue (likely wrong) - - Read all related code files in full (no truncation) - - Trace the code path and identify the actual root cause - - Propose a fix - -5. **For feature requests**: - - Do not trust implementation proposals in the issue without verification - - Read all related code files in full (no truncation) - - Propose the most concise implementation approach - - List affected files and changes needed - -Do NOT implement unless explicitly asked. Analyze and propose only. diff --git a/.pi/prompts/pr.md b/.pi/prompts/pr.md deleted file mode 100644 index 800d4524..00000000 --- a/.pi/prompts/pr.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -description: Review PRs from URLs with structured issue and code analysis -argument-hint: "" ---- -You are given one or more GitHub PR URLs: $@ - -For each PR URL, do the following in order: -1. Add the `inprogress` label to the PR via GitHub CLI before analysis starts. If adding the label fails, report that explicitly and continue. -2. Read the PR page in full. Include description, all comments, all commits, and all changed files. -3. Identify any linked issues referenced in the PR body, comments, commit messages, or cross links. Read each issue in full, including all comments. -4. Analyze the PR diff. Read all relevant code files in full with no truncation from the current main branch and compare against the diff. Do not fetch PR file blobs unless a file is missing on main or the diff context is insufficient. Include related code paths that are not in the diff but are required to validate behavior. -5. Check for a changelog entry in the relevant `packages/*/CHANGELOG.md` files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify: - - Entry uses correct section (`### Breaking Changes`, `### Added`, `### Fixed`, etc.) - - External contributions include PR link and author: `Fixed foo ([#123](https://github.com/earendil-works/pi-mono/pull/123) by [@user](https://github.com/user))` - - Breaking changes are in `### Breaking Changes`, not just `### Fixed` -6. Check if packages/coding-agent/README.md, packages/coding-agent/docs/*.md, packages/coding-agent/examples/**/*.md require modification. This is usually the case when existing features have been changed, or new features have been added. -7. Provide a structured review with these sections: - - Good: solid choices or improvements - - Bad: concrete issues, regressions, missing tests, or risks - - Ugly: subtle or high impact problems -8. Add Questions or Assumptions if anything is unclear. -9. Add Change summary and Tests. - -Output format per PR: -PR: -Changelog: -- ... -Good: -- ... -Bad: -- ... -Ugly: -- ... -Questions or Assumptions: -- ... -Change summary: -- ... -Tests: -- ... - -If no issues are found, say so under Bad and Ugly. \ No newline at end of file diff --git a/.pi/prompts/wr.md b/.pi/prompts/wr.md deleted file mode 100644 index 65108e9c..00000000 --- a/.pi/prompts/wr.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -description: Finish the current task end-to-end with changelog, commit, and push -argument-hint: "[instructions]" ---- -Wrap it. - -Additional instructions: $ARGUMENTS - -Determine context from the conversation history first. - -Rules for context detection: -- If the conversation already mentions a GitHub issue or PR, use that existing context. -- If the work came from `/is` or `/pr`, assume the issue or PR context is already known from the conversation and from the analysis work already done. -- If there is no GitHub issue or PR in the conversation history, treat this as non-GitHub work. - -Unless I explicitly override something in this request, do the following in order: - -1. Add or update the relevant package changelog entry under `## [Unreleased]` using the repo changelog rules. -2. If this task is tied to a GitHub issue or PR and a final issue or PR comment has not already been posted in this session, draft it in my tone, preview it, and post exactly one final comment. -3. Commit only files you changed in this session. -4. If this task is tied to exactly one GitHub issue, include `closes #` in the commit message. If it is tied to multiple issues, stop and ask which one to use. If it is not tied to any issue, do not include `closes #` or `fixes #` in the commit message. -5. Check the current git branch. If it is not `main`, stop and ask what to do. Do not push from another branch unless I explicitly say so. -6. Push the current branch. - -Constraints: -- Never stage unrelated files. -- Never use `git add .` or `git add -A`. -- Run required checks before committing if code changed. -- Do not open a PR unless I explicitly ask. -- If this is not GitHub issue or PR work, do not post a GitHub comment. -- If a final issue or PR comment was already posted in this session, do not post another one unless I explicitly ask. diff --git a/context.md b/context.md new file mode 100644 index 00000000..564a9b0a --- /dev/null +++ b/context.md @@ -0,0 +1,142 @@ +# Code Context + +## Top-Level Structure + +``` +.sproutclaw/ +├── .claude/ # Claude configuration +├── .github/ # Issue templates, workflows (issue-gate, pr-gate, approve-contributor) +├── .husky/ # Git hooks +├── .pi/ # pi/pi-mono agent configuration (AGENTS.md) +├── node_modules/ +├── packages/ # 5 monorepo workspaces +│ ├── ai/ # @earendil-works/pi-ai v0.74.0 +│ ├── agent/ # @earendil-works/pi-agent-core v0.74.0 +│ ├── coding-agent/ # @earendil-works/pi-coding-agent v0.74.0 +│ ├── tui/ # @earendil-works/pi-tui v0.74.0 +│ └── web-ui/ # @earendil-works/pi-web-ui v0.74.0 +├── scripts/ # CI/release/utility scripts +├── tmp/ # Temp files +├── pi-test.sh # Interactive TUI test script +├── README.md +├── CONTRIBUTING.md +├── AGENTS.md +├── biome.json # Linting/formatting config +├── tsconfig.base.json # Shared TS config +├── tsconfig.json # Root TS config +├── package.json # Workspace root (npm workspaces) +└── bun.lock / package-lock.json +``` + +## Packages + +### 1. `packages/ai` — `@earendil-works/pi-ai` +**Unified LLM API** — automatic model discovery, provider configuration, streaming. + +- `src/index.ts` — package entry +- `src/types.ts` — core types (`Api`, `StreamOptions`, `Model`, `KnownProvider`) +- `src/stream.ts` — main streaming implementation +- `src/models.ts` / `src/models.generated.ts` — model registry + auto-generated +- `src/providers/` — individual provider implementations (Anthropic, Google, OpenAI, Mistral, etc.) +- `src/cli.ts` — CLI for model listing etc. +- `src/env-api-keys.ts` — credential detection from env vars +- `src/oauth.ts` — OAuth support +- `src/api-registry.ts` — API/stream provider registration +- `scripts/generate-models.ts` — model list generation script +- Subpath exports per provider (e.g. `./anthropic`, `./google`, etc.) + +### 2. `packages/agent` — `@earendil-works/pi-agent-core` +**General-purpose agent** — transport abstraction, state management, attachment support. + +- `src/index.ts` — package entry +- `src/types.ts` — core types (agent state, attachments, transports) +- `src/agent.ts` — agent implementation +- `src/agent-loop.ts` — agent main loop +- `src/proxy.ts` — proxy support +- `src/harness/` — test harness utilities + +### 3. `packages/coding-agent` — `@earendil-works/pi-coding-agent` +**Coding agent CLI** — the `pi` CLI binary with read, bash, edit, write tools and session management. + +- `src/cli.ts` / `src/cli/` — CLI argument parsing / entry point +- `src/main.ts` — main agent runner +- `src/index.ts` — package entry +- `src/config.ts` — configuration management +- `src/core/` — core logic (model resolution, hooks, tool execution, session management) +- `src/modes/` — operational modes +- `src/bun/` — Bun-specific support +- `src/migrations.ts` — data migration utilities +- `src/package-manager-cli.ts` — package manager interaction +- `docs/` — provider setup docs +- `examples/` — extension examples (custom provider, sandbox) +- Binary name: `pi` + +### 4. `packages/tui` — `@earendil-works/pi-tui` +**Terminal UI library** — differential rendering for text-based terminal applications. + +- `src/index.ts` — package entry +- `src/tui.ts` — core TUI rendering engine +- `src/terminal.ts` — low-level terminal I/O +- `src/keys.ts` / `src/keybindings.ts` — key input handling +- `src/components/` — reusable UI components +- `src/editor-component.ts` — text editor component +- `src/autocomplete.ts` — autocomplete support +- `src/fuzzy.ts` — fuzzy matching +- `src/terminal-image.ts` — image rendering in terminal +- `src/kill-ring.ts` / `src/undo-stack.ts` — editing helpers +- `src/stdin-buffer.ts` / `src/utils.ts` — utilities + +### 5. `packages/web-ui` — `@earendil-works/pi-web-ui` +**Reusable web UI components** — AI chat interfaces powered by pi-ai. + +- `src/index.ts` — package entry +- `src/app.css` — Tailwind CSS stylesheet +- `src/ChatPanel.ts` — main chat panel component +- `src/components/` — reusable React components +- `src/dialogs/` — dialog components +- `src/prompts/` — prompt templates/management +- `src/storage/` — client-side storage abstraction +- `src/tools/` — tool integration +- `src/utils/` — utilities +- Build: TypeScript + Tailwind CSS + +## Root Scripts (`scripts/`) + +- `release.mjs` — automated release workflow (version + changelog + publish) +- `sync-versions.js` — lockstep version sync across all packages +- `cost.ts` / `stats.ts` — analytics/telemetry stats +- `tool-stats.ts` / `edit-tool-stats.mjs` / `read-tool-stats.mjs` — tool usage statistics +- `profile-coding-agent-node.mjs` — Node.js profiling for coding agent +- `build-binaries.sh` — binary build script +- `browser-smoke-entry.ts` / `check-browser-smoke.mjs` — browser smoke tests +- `session-transcripts.ts` / `session-context-stats.mjs` — session logging + +## Architecture Summary + +``` +pi-tui (terminal UI) ─── pi-coding-agent (CLI + tools) ─── pi-agent-core (agent loop) + │ + pi-web-ui (web UI) ────────────┤ + │ + pi-ai (LLM provider abstraction) +``` + +- **pi-ai** is the foundation layer providing unified LLM API access (Anthropic, Google, OpenAI, Azure, Mistral, Bedrock, etc.) +- **pi-agent-core** builds on pi-ai with agent loop, transport abstraction, and state management +- **pi-coding-agent** is the main product: a coding agent CLI (`pi` command) with read/bash/edit/write tools and session management +- **pi-tui** is a standalone TUI rendering library used by pi-coding-agent +- **pi-web-ui** provides reusable chat UI components (React) powered by pi-ai + +## Key Facts + +- **Lockstep versioning**: all packages share the same version (currently 0.74.0) +- **Release automation**: `npm run release:patch` / `release:minor` via `scripts/release.mjs` +- **TypeScript with tsgo**: uses `tsgo` (TypeScript native preview) for fast builds; web-ui uses `tsc` +- **Linting**: Biome (v2.3.5) for formatting and linting (`npm run check`) +- **Testing**: vitest for ai/agent/coding-agent; node --test for tui +- **No `any` types allowed**, standard top-level imports only (no inline `import()`) +- **GitHub CI**: issue-gate, pr-gate, approve-contributor workflows for contributor management + +## Start Here + +Open `package.json` (root) for workspace structure and available scripts. For a specific package, start with its `package.json` then `src/index.ts` for the public API surface. diff --git a/package-lock.json b/package-lock.json index 824b8d98..2796779d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5515,6 +5515,7 @@ "node_modules/lit": { "version": "3.3.2", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", @@ -6639,6 +6640,7 @@ "node_modules/tailwind-merge": { "version": "3.5.0", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -6663,7 +6665,8 @@ }, "node_modules/tailwindcss": { "version": "4.2.4", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.3", @@ -6761,6 +6764,7 @@ "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.4", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6839,6 +6843,7 @@ "version": "4.21.0", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -6926,6 +6931,7 @@ "node_modules/vite": { "version": "7.3.3", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -7034,6 +7040,7 @@ "node_modules/vite/node_modules/picomatch": { "version": "4.0.4", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7281,6 +7288,7 @@ "node_modules/zod": { "version": "4.4.3", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts index 9bbee4c8..e71c7ba1 100644 --- a/packages/ai/src/image-models.generated.ts +++ b/packages/ai/src/image-models.generated.ts @@ -4,21 +4,21 @@ import type { ImagesApi, ImagesModel } from "./types.js"; export const IMAGE_MODELS = { - openrouter: { + "openrouter": { "black-forest-labs/flux.2-flex": { id: "black-forest-labs/flux.2-flex", name: "Black Forest Labs: FLUX.2 Flex", api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "black-forest-labs/flux.2-klein-4b": { id: "black-forest-labs/flux.2-klein-4b", @@ -26,14 +26,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "black-forest-labs/flux.2-max": { id: "black-forest-labs/flux.2-max", @@ -41,14 +41,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "black-forest-labs/flux.2-pro": { id: "black-forest-labs/flux.2-pro", @@ -56,14 +56,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "bytedance-seed/seedream-4.5": { id: "bytedance-seed/seedream-4.5", @@ -71,14 +71,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["image", "text"], + input: ["image","text"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "google/gemini-2.5-flash-image": { id: "google/gemini-2.5-flash-image", @@ -86,14 +86,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["image", "text"], - output: ["image", "text"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0.08333333333333334, - }, + input: ["image","text"], + output: ["image","text"], + cost: { + "input": 0.3, + "output": 2.5, + "cacheRead": 0.03, + "cacheWrite": 0.08333333333333334 + } } satisfies ImagesModel<"openrouter-images">, "google/gemini-3-pro-image-preview": { id: "google/gemini-3-pro-image-preview", @@ -101,14 +101,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["image", "text"], - output: ["image", "text"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0.375, - }, + input: ["image","text"], + output: ["image","text"], + cost: { + "input": 2, + "output": 12, + "cacheRead": 0.19999999999999998, + "cacheWrite": 0.375 + } } satisfies ImagesModel<"openrouter-images">, "google/gemini-3.1-flash-image-preview": { id: "google/gemini-3.1-flash-image-preview", @@ -116,14 +116,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["image", "text"], - output: ["image", "text"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, + input: ["image","text"], + output: ["image","text"], + cost: { + "input": 0.5, + "output": 3, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "openai/gpt-5-image": { id: "openai/gpt-5-image", @@ -131,14 +131,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["image", "text"], - output: ["image", "text"], - cost: { - input: 10, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, + input: ["image","text"], + output: ["image","text"], + cost: { + "input": 10, + "output": 10, + "cacheRead": 1.25, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "openai/gpt-5-image-mini": { id: "openai/gpt-5-image-mini", @@ -146,14 +146,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["image", "text"], - output: ["image", "text"], - cost: { - input: 2.5, - output: 2, - cacheRead: 0.25, - cacheWrite: 0, - }, + input: ["image","text"], + output: ["image","text"], + cost: { + "input": 2.5, + "output": 2, + "cacheRead": 0.25, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "openai/gpt-5.4-image-2": { id: "openai/gpt-5.4-image-2", @@ -161,14 +161,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["image", "text"], - output: ["image", "text"], - cost: { - input: 8, - output: 15, - cacheRead: 2, - cacheWrite: 0, - }, + input: ["image","text"], + output: ["image","text"], + cost: { + "input": 8, + "output": 15, + "cacheRead": 2, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "openrouter/auto": { id: "openrouter/auto", @@ -176,14 +176,59 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], - output: ["text", "image"], - cost: { - input: -1000000, - output: -1000000, - cacheRead: 0, - cacheWrite: 0, - }, + input: ["text","image"], + output: ["text","image"], + cost: { + "input": -1000000, + "output": -1000000, + "cacheRead": 0, + "cacheWrite": 0 + } + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v3": { + id: "recraft/recraft-v3", + name: "Recraft: Recraft V3", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text","image"], + output: ["image"], + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4": { + id: "recraft/recraft-v4", + name: "Recraft: Recraft V4", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text","image"], + output: ["image"], + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + } satisfies ImagesModel<"openrouter-images">, + "recraft/recraft-v4-pro": { + id: "recraft/recraft-v4-pro", + name: "Recraft: Recraft V4 Pro", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text","image"], + output: ["image"], + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "sourceful/riverflow-v2-fast": { id: "sourceful/riverflow-v2-fast", @@ -191,14 +236,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "sourceful/riverflow-v2-fast-preview": { id: "sourceful/riverflow-v2-fast-preview", @@ -206,14 +251,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "sourceful/riverflow-v2-max-preview": { id: "sourceful/riverflow-v2-max-preview", @@ -221,14 +266,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "sourceful/riverflow-v2-pro": { id: "sourceful/riverflow-v2-pro", @@ -236,14 +281,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, "sourceful/riverflow-v2-standard-preview": { id: "sourceful/riverflow-v2-standard-preview", @@ -251,14 +296,14 @@ export const IMAGE_MODELS = { api: "openrouter-images", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - input: ["text", "image"], + input: ["text","image"], output: ["image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, + cost: { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } } satisfies ImagesModel<"openrouter-images">, }, } as const satisfies Record>>; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index e700a56c..445a7e38 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -56,23 +56,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 8192, } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-premier-v1:0": { - id: "amazon.nova-premier-v1:0", - name: "Nova Premier", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 12.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, "amazon.nova-pro-v1:0": { id: "amazon.nova-pro-v1:0", name: "Nova Pro", @@ -90,91 +73,6 @@ export const MODELS = { contextWindow: 300000, maxTokens: 8192, } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-haiku-20241022-v1:0": { - id: "anthropic.claude-3-5-haiku-20241022-v1:0", - name: "Claude Haiku 3.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-sonnet-20240620-v1:0": { - id: "anthropic.claude-3-5-sonnet-20240620-v1:0", - name: "Claude Sonnet 3.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-sonnet-20241022-v2:0": { - id: "anthropic.claude-3-5-sonnet-20241022-v2:0", - name: "Claude Sonnet 3.5 v2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-7-sonnet-20250219-v1:0": { - id: "anthropic.claude-3-7-sonnet-20250219-v1:0", - name: "Claude Sonnet 3.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-haiku-20240307-v1:0": { - id: "anthropic.claude-3-haiku-20240307-v1:0", - name: "Claude Haiku 3", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, "anthropic.claude-haiku-4-5-20251001-v1:0": { id: "anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5", @@ -209,23 +107,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 32000, } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-20250514-v1:0": { - id: "anthropic.claude-opus-4-20250514-v1:0", - name: "Claude Opus 4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, "anthropic.claude-opus-4-5-20251101-v1:0": { id: "anthropic.claude-opus-4-5-20251101-v1:0", name: "Claude Opus 4.5", @@ -279,23 +160,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-20250514-v1:0": { - id: "anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, "anthropic.claude-sonnet-4-5-20250929-v1:0": { id: "anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5", @@ -330,6 +194,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 64000, } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-haiku-4-5-20251001-v1:0": { + id: "au.anthropic.claude-haiku-4-5-20251001-v1:0", + name: "Claude Haiku 4.5 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, "au.anthropic.claude-opus-4-6-v1": { id: "au.anthropic.claude-opus-4-6-v1", name: "AU Anthropic Claude Opus 4.6", @@ -348,6 +229,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "au.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (AU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, "au.anthropic.claude-sonnet-4-6": { id: "au.anthropic.claude-sonnet-4-6", name: "AU Anthropic Claude Sonnet 4.6", @@ -486,23 +384,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "eu.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5 (EU)", @@ -607,23 +488,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "global.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5 (Global)", @@ -692,22 +556,57 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-405b-instruct-v1:0": { - id: "meta.llama3-1-405b-instruct-v1:0", - name: "Llama 3.1 405B Instruct", + "jp.anthropic.claude-opus-4-7": { + id: "jp.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (JP)", api: "bedrock-converse-stream", provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], cost: { - input: 2.4, - output: 2.4, - cacheRead: 0, - cacheWrite: 0, + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, }, - contextWindow: 128000, - maxTokens: 4096, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { + id: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", + name: "Claude Sonnet 4.5 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"bedrock-converse-stream">, + "jp.anthropic.claude-sonnet-4-6": { + id: "jp.anthropic.claude-sonnet-4-6", + name: "Claude Sonnet 4.6 (JP)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, } satisfies Model<"bedrock-converse-stream">, "meta.llama3-1-70b-instruct-v1:0": { id: "meta.llama3-1-70b-instruct-v1:0", @@ -743,74 +642,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-11b-instruct-v1:0": { - id: "meta.llama3-2-11b-instruct-v1:0", - name: "Llama 3.2 11B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.16, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-1b-instruct-v1:0": { - id: "meta.llama3-2-1b-instruct-v1:0", - name: "Llama 3.2 1B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-3b-instruct-v1:0": { - id: "meta.llama3-2-3b-instruct-v1:0", - name: "Llama 3.2 3B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-90b-instruct-v1:0": { - id: "meta.llama3-2-90b-instruct-v1:0", - name: "Llama 3.2 90B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, "meta.llama3-3-70b-instruct-v1:0": { id: "meta.llama3-3-70b-instruct-v1:0", name: "Llama 3.3 70B Instruct", @@ -1389,23 +1220,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 32000, } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-20250514-v1:0": { - id: "us.anthropic.claude-opus-4-20250514-v1:0", - name: "Claude Opus 4 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, "us.anthropic.claude-opus-4-5-20251101-v1:0": { id: "us.anthropic.claude-opus-4-5-20251101-v1:0", name: "Claude Opus 4.5 (US)", @@ -1459,23 +1273,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "us.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", name: "Claude Sonnet 4.5 (US)", @@ -1510,6 +1307,57 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 64000, } satisfies Model<"bedrock-converse-stream">, + "us.deepseek.r1-v1:0": { + id: "us.deepseek.r1-v1:0", + name: "DeepSeek-R1 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { + input: 1.35, + output: 5.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 32768, + } satisfies Model<"bedrock-converse-stream">, + "us.meta.llama4-maverick-17b-instruct-v1:0": { + id: "us.meta.llama4-maverick-17b-instruct-v1:0", + name: "Llama 4 Maverick 17B Instruct (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.24, + output: 0.97, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, + "us.meta.llama4-scout-17b-instruct-v1:0": { + id: "us.meta.llama4-scout-17b-instruct-v1:0", + name: "Llama 4 Scout 17B Instruct (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.66, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 3500000, + maxTokens: 16384, + } satisfies Model<"bedrock-converse-stream">, "writer.palmyra-x4-v1:0": { id: "writer.palmyra-x4-v1:0", name: "Palmyra X4", @@ -3998,25 +3846,6 @@ export const MODELS = { contextWindow: 144000, maxTokens: 64000, } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsEagerToolInputStreaming":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 216000, - maxTokens: 16000, - } satisfies Model<"anthropic-messages">, "claude-sonnet-4.5": { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", @@ -4092,25 +3921,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 64000, } satisfies Model<"openai-completions">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, "gemini-3.1-pro-preview": { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", @@ -4168,25 +3978,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"openai-completions">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, "gpt-5-mini": { id: "gpt-5-mini", name: "GPT-5-mini", @@ -4206,82 +3997,6 @@ export const MODELS = { contextWindow: 264000, maxTokens: 64000, } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 264000, - maxTokens: 64000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1-Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1-Codex-max", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - reasoning: true, - thinkingLevelMap: {"off":null}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, "gpt-5.2": { id: "gpt-5.2", name: "GPT-5.2", @@ -7756,6 +7471,25 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 64000, } satisfies Model<"anthropic-messages">, + "deepseek-v4-flash-free": { + id: "deepseek-v4-flash-free", + name: "DeepSeek V4 Flash Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, "gemini-3-flash": { id: "gemini-3-flash", name: "Gemini 3 Flash", @@ -8114,23 +7848,6 @@ export const MODELS = { contextWindow: 1050000, maxTokens: 128000, } satisfies Model<"openai-responses">, - "hy3-preview-free": { - id: "hy3-preview-free", - name: "Hy3 preview Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, "kimi-k2.5": { id: "kimi-k2.5", name: "Kimi K2.5", @@ -8267,6 +7984,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 65536, } satisfies Model<"anthropic-messages">, + "ring-2.6-1t-free": { + id: "ring-2.6-1t-free", + name: "Ring 2.6 1T Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 66000, + } satisfies Model<"openai-completions">, }, "opencode-go": { "deepseek-v4-flash": { @@ -8634,40 +8368,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"openai-completions">, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Anthropic: Claude 3.7 Sonnet", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.7-sonnet:thinking": { - id: "anthropic/claude-3.7-sonnet:thinking", - name: "Anthropic: Claude 3.7 Sonnet (thinking)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, "anthropic/claude-haiku-4.5": { id: "anthropic/claude-haiku-4.5", name: "Anthropic: Claude Haiku 4.5", @@ -8875,6 +8575,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, + "arcee-ai/trinity-large-thinking:free": { + id: "arcee-ai/trinity-large-thinking:free", + name: "Arcee AI: Trinity Large Thinking (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 80000, + } satisfies Model<"openai-completions">, "arcee-ai/trinity-mini": { id: "arcee-ai/trinity-mini", name: "Arcee AI: Trinity Mini", @@ -9122,13 +8839,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.15, - output: 0.75, - cacheRead: 0, + input: 0.21, + output: 0.7899999999999999, + cacheRead: 0.13, cacheWrite: 0, }, - contextWindow: 32768, - maxTokens: 7168, + contextWindow: 163840, + maxTokens: 32768, } satisfies Model<"openai-completions">, "deepseek/deepseek-r1": { id: "deepseek/deepseek-r1", @@ -9568,8 +9285,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.13, - output: 0.38, + input: 0.12, + output: 0.37, cacheRead: 0, cacheWrite: 0, }, @@ -9661,6 +9378,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, + "inclusionai/ring-2.6-1t:free": { + id: "inclusionai/ring-2.6-1t:free", + name: "inclusionAI: Ring-2.6-1T (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "kwaipilot/kat-coder-pro-v2": { id: "kwaipilot/kat-coder-pro-v2", name: "Kwaipilot: KAT-Coder-Pro V2", @@ -9825,11 +9559,11 @@ export const MODELS = { cost: { input: 0.15, output: 1.15, - cacheRead: 0.03, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 196608, - maxTokens: 131072, + maxTokens: 196608, } satisfies Model<"openai-completions">, "minimax/minimax-m2.5:free": { id: "minimax/minimax-m2.5:free", @@ -9857,13 +9591,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.3, + input: 0.29900000000000004, output: 1.2, - cacheRead: 0.059, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 196608, - maxTokens: 4096, + maxTokens: 131072, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -10299,13 +10033,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.75, + input: 0.74, output: 3.5, - cacheRead: 0.15, + cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 16384, + contextWindow: 32768, + maxTokens: 32768, } satisfies Model<"openai-completions">, "nex-agi/deepseek-v3.1-nex-n1": { id: "nex-agi/deepseek-v3.1-nex-n1", @@ -11825,12 +11559,12 @@ export const MODELS = { input: ["text"], cost: { input: 0.08, - output: 0.24, - cacheRead: 0.04, + output: 0.28, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 40960, - maxTokens: 40960, + maxTokens: 16384, } satisfies Model<"openai-completions">, "qwen/qwen3-8b": { id: "qwen/qwen3-8b", @@ -12461,18 +12195,18 @@ export const MODELS = { contextWindow: 262144, maxTokens: 65536, } satisfies Model<"openai-completions">, - "tencent/hy3-preview:free": { - id: "tencent/hy3-preview:free", - name: "Tencent: Hy3 preview (free)", + "tencent/hy3-preview": { + id: "tencent/hy3-preview", + name: "Tencent: Hy3 preview", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, input: ["text"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 0.06599999999999999, + output: 0.26, + cacheRead: 0.029, cacheWrite: 0, }, contextWindow: 262144, @@ -12512,23 +12246,6 @@ export const MODELS = { contextWindow: 32768, maxTokens: 32768, } satisfies Model<"openai-completions">, - "tngtech/deepseek-r1t2-chimera": { - id: "tngtech/deepseek-r1t2-chimera", - name: "TNG: DeepSeek R1T2 Chimera", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.1, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"openai-completions">, "upstage/solar-pro-3": { id: "upstage/solar-pro-3", name: "Upstage: Solar Pro 3", @@ -12725,9 +12442,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09, - output: 0.29, - cacheRead: 0.045, + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 262144, @@ -12799,7 +12516,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 131072, + maxTokens: 16384, } satisfies Model<"openai-completions">, "z-ai/glm-4-32b": { id: "z-ai/glm-4-32b", @@ -12997,13 +12714,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 1.0499999999999998, - output: 3.5, - cacheRead: 0.5249999999999999, + input: 0.98, + output: 3.08, + cacheRead: 0.182, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 65535, + maxTokens: 4096, } satisfies Model<"openai-completions">, "z-ai/glm-5v-turbo": { id: "z-ai/glm-5v-turbo", @@ -13116,13 +12833,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.75, + input: 0.74, output: 3.5, - cacheRead: 0.15, + cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 16384, + contextWindow: 32768, + maxTokens: 32768, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", @@ -13822,23 +13539,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"anthropic-messages">, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude 3.7 Sonnet", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, "anthropic/claude-haiku-4.5": { id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5", diff --git a/pi-test.sh b/pi-test.sh index b036b8fa..f2119c5f 100755 --- a/pi-test.sh +++ b/pi-test.sh @@ -6,7 +6,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Default to the repository-local pi config so this source checkout uses the # copied learning config instead of ~/.pi. export PI_CODING_AGENT_DIR="${PI_CODING_AGENT_DIR:-$SCRIPT_DIR/.pi/agent}" -export PI_CODING_AGENT_SESSION_DIR="${PI_CODING_AGENT_SESSION_DIR:-$SCRIPT_DIR/.pi/sessions}" # Check for --no-env flag NO_ENV=false