/** * 启动界面中文翻译扩展 * * 将 pi 的初始启动界面中的介绍和引导文字翻译为中文, * 快捷键和命令提示保持原样。 */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { keyHint, keyText, rawKeyHint, VERSION } from "@mariozechner/pi-coding-agent"; const SPROUTCLAW_LOGO = [ "███████╗██████╗ ██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗██╗ █████╗ ██╗ ██╗", "██╔════╝██╔══██╗██╔══██╗██╔═══██╗██║ ██║╚══██╔══╝██╔════╝██║ ██╔══██╗██║ ██║", "███████╗██████╔╝██████╔╝██║ ██║██║ ██║ ██║ ██║ ██║ ███████║██║ █╗ ██║", "╚════██║██╔═══╝ ██╔══██╗██║ ██║██║ ██║ ██║ ██║ ██║ ██╔══██║██║███╗██║", "███████║██║ ██║ ██║╚██████╔╝╚██████╔╝ ██║ ╚██████╗███████╗██║ ██║╚███╔███╔╝", "╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ", ]; const LOGO_PALETTE = [ [125, 255, 230], [154, 255, 140], [255, 245, 135], [255, 176, 235], [150, 210, 255], [190, 170, 255], ]; function rgb(text: string, color: number[]): string { return `\x1b[38;2;${color[0]};${color[1]};${color[2]}m${text}\x1b[39m`; } function lerp(a: number, b: number, t: number): number { return Math.round(a + (b - a) * t); } function gradientText(text: string, offset = 0): string { const chars = [...text]; const last = Math.max(1, chars.length - 1); return chars .map((char, index) => { if (char === " ") return char; const position = (index / last + offset) % 1; const scaled = position * (LOGO_PALETTE.length - 1); const startIndex = Math.floor(scaled); const endIndex = Math.min(LOGO_PALETTE.length - 1, startIndex + 1); const t = scaled - startIndex; const start = LOGO_PALETTE[startIndex]; const end = LOGO_PALETTE[endIndex]; return rgb(char, [ lerp(start[0], end[0], t), lerp(start[1], end[1], t), lerp(start[2], end[2], t), ]); }) .join(""); } function startupLine(label: string, text: string, color: number[]): string { return `${rgb(label, color)} ${rgb(text, [210, 235, 255])}`; } function keyTextOr(action: string, fallback: string): string { const text = keyText(action as any).trim(); return text || fallback; } function renderDivider(theme: { fg: (color: string, text: string) => string }, width: number): string { return theme.fg("dim", "─".repeat(Math.max(1, width))); } function renderSproutclawLogo(theme: any, width: number): string[] { if (width < 96) { return [ [ rgb("❯ ", [100, 200, 255]), theme.bold(gradientText("SproutClaw")), rgb(` v${VERSION}`, [170, 235, 255]), rgb(" · ", [80, 100, 130]), rgb("萌芽运维开发助手", [190, 170, 255]), ].join(""), ]; } const lines: string[] = []; for (let i = 0; i < SPROUTCLAW_LOGO.length; i++) { lines.push(theme.bold(gradientText(SPROUTCLAW_LOGO[i], i * 0.08))); } return lines; } 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", "中断当前任务"), keyHint("app.clear", "清空输入"), rawKeyHint(`${keyText("app.clear")} twice`, "退出"), keyHint("app.exit", "空输入退出"), keyHint("app.suspend", "挂起进程"), keyHint("tui.editor.deleteToLineEnd" as any, "删除到行尾"), keyHint("app.thinking.cycle", "切换思考强度"), rawKeyHint( `${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "切换模型", ), keyHint("app.model.select", "选择模型"), keyHint("app.tools.expand", "展开工具输出"), keyHint("app.thinking.toggle", "展开思考过程"), keyHint("app.editor.external", "外部编辑器"), rawKeyHint("/", "Agent 命令"), rawKeyHint("!", "执行 shell"), rawKeyHint("!!", "执行 shell 且不进上下文"), keyHint("app.message.followUp", "追加后续消息"), keyHint("app.message.dequeue", "编辑队列消息"), keyHint("app.clipboard.pasteImage", "粘贴图片"), rawKeyHint("drop files", "附加文件"), ].join("\n"); const compactOnboarding = startupLine( "工作台", `描述要处理的代码、部署或服务器问题,我会执行命令、修改文件并同步进度。按 ${keyTextOr("app.tools.expand", "Ctrl+O")} 展开详情。`, [150, 255, 210], ); const onboarding = [ startupLine("SproutClaw", "树萌芽运维开发助手,负责代码修改、服务部署、服务器操作、排障和知识检索。", [255, 232, 140]), rgb("直接输入任务即可开始。", [255, 190, 235]), ].join("\n"); let expanded = false; return { render(width: number): string[] { const divider = renderDivider(theme, width); const lines: string[] = [ divider, ...renderSproutclawLogo(theme, width), divider, rgb("萌芽运维开发Agent助手", [170, 235, 255]), ]; if (expanded) { lines.push(expandedInstructions); lines.push(onboarding); } else { lines.push(compactOnboarding); lines.push(onboarding); } return lines; }, invalidate() {}, setExpanded(v: boolean) { expanded = v; }, }; }); }); }