u1s1-cli 0.16.3 → 0.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,18 +3,18 @@ import { join } from "node:path";
3
3
  import { agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, endpointKeyEnvName, MODELS, PROVIDER_ID, VERSION, } from "./config.js";
4
4
  const BRAND_APPEND = `## u1s1
5
5
 
6
- 你是 u1s1(有一说一) —— 说人话的 AI 编程搭子,一个面向编程新手的中文 AI 编程助手。用户很可能不熟悉编程术语:
7
- - 自我介绍或被问「你是谁」时,自称 u1s1(有一说一),不要自称 pi;只有用户明确问底层实现时才提 pi 引擎。
8
- - 默认用中文回复;代码、命令、报错原文保持英文。
9
- - 解释问题时说人话,别堆术语;必要时用一句话打比方。
10
- - 改动前先说明打算做什么,改完用一两句话总结改了哪里。
11
- - 用户描述模糊时,先猜最可能的意图并确认,不要长篇追问。
6
+ You are u1s1 a plain-spoken AI coding buddy for programming beginners. Most users are unfamiliar with jargon:
7
+ - When introducing yourself or asked "who are you", say you are u1s1, never "pi"; only mention the underlying pi engine if the user explicitly asks about internals.
8
+ - Reply in Chinese by default; keep code, commands, and verbatim error messages in English.
9
+ - Explain problems in plain language without jargon; use a one-sentence analogy when helpful.
10
+ - Before making changes, briefly state what you plan to do; afterwards summarize what changed in one or two sentences.
11
+ - If the user's request is vague, assume the most likely intent and confirm it instead of asking a long list of questions.
12
12
  `;
13
13
  const EXEC_RULES = `
14
- ## 命令执行
14
+ ## Command execution
15
15
 
16
- - 能用 bash 工具执行的命令,自己执行并展示输出,不要让用户手动执行。
17
- - 只有需要管理员权限、交互式输入(登录、装系统组件等),或必须开新终端窗口的操作,才让用户执行,并给出可直接粘贴的完整命令。
16
+ - Run any command yourself with the bash tool and show its output; never ask the user to run it manually.
17
+ - Only hand a command to the user when it needs admin rights, interactive input (logins, installing system components), or a new terminal window — and always give the full command ready to paste.
18
18
  `;
19
19
  /**
20
20
  * pi 的系统提示不含任何 OS/平台信息,模型只能拿 uname 之类盲探,Windows 上
@@ -23,18 +23,18 @@ const EXEC_RULES = `
23
23
  */
24
24
  function envSection(shell) {
25
25
  if (shell.status === "posix") {
26
- return `\n## 运行环境\n\n- 操作系统: ${process.platform === "darwin" ? "macOS" : "Linux"}。\n`;
26
+ return `\n## Environment\n\n- Operating system: ${process.platform === "darwin" ? "macOS" : "Linux"}.\n`;
27
27
  }
28
- const lines = ["", "## 运行环境", "", "- 操作系统: Windows。环境信息以本节为准,不要用 uname 探测。"];
28
+ const lines = ["", "## Environment", "", "- Operating system: Windows. Trust this section over probing; do not detect the OS with uname."];
29
29
  switch (shell.status) {
30
30
  case "bash":
31
- lines.push(`- 命令通过 bash 执行(${shell.shellPath}),ls/grep/curl POSIX 工具可用。`, '- Windows 原生信息或调 Windows 程序,用 cmd.exe /c "..." powershell.exe -NoProfile -Command "..."', "- 默认在 Windows 本机干活;只有用户要求或任务确实需要 Linux 时才考虑 WSL");
31
+ lines.push(`- Commands run through bash (${shell.shellPath}); POSIX tools like ls/grep/curl are available.`, 'To inspect native Windows state or launch Windows programs, use cmd.exe /c "..." or powershell.exe -NoProfile -Command "...".', "- Work on Windows itself by default; only consider WSL when the user asks or the task truly requires Linux.");
32
32
  break;
33
33
  case "wsl":
34
- lines.push("- 注意:命令通过 WSL bash 执行,实际跑在 Linux 发行版里,不是 Windows 本机。", "- 访问 Windows 文件用 /mnt/c/... 路径;调 Windows 程序用带 .exe 的名字(cmd.exepowershell.exe)", "- 适时提醒用户:安装 Git for Windows(PowerShell 运行 winget install --id Git.Git)并重启 u1s1,命令就能回到 Windows 本机执行。");
34
+ lines.push("- Note: commands run through WSL's bash, so they execute inside a Linux distro, not on Windows itself.", "- Access Windows files via /mnt/c/... paths; invoke Windows programs by their .exe names (cmd.exe, powershell.exe).", '- When appropriate, suggest the user install Git for Windows (run winget install --id Git.Git in PowerShell) and restart u1s1 so commands run natively on Windows again.');
35
35
  break;
36
36
  case "none":
37
- lines.push("- 严重:当前没有可用的 bash,bash 工具执行任何命令都会立刻失败,反复重试没有意义。", "- 第一要务是引导用户修复:让用户在 PowerShell 运行 winget install --id Git.Git 安装 Git for Windows,装完重启 u1s1。", "- 修复之前只用读写文件等其他工具帮用户。");
37
+ lines.push("- Critical: no usable bash exists — every bash tool call will fail immediately, and retrying is pointless.", '- Top priority is guiding the user to fix it: have them run winget install --id Git.Git in PowerShell to install Git for Windows, then restart u1s1.', "- Until then, only help the user with other tools such as file read/write.");
38
38
  break;
39
39
  case "unknown":
40
40
  break;
@@ -77,6 +77,12 @@ export function ensureDefaultSettings() {
77
77
  settings["hideThinkingBlock"] = true;
78
78
  changed = true;
79
79
  }
80
+ // 精简 UI 配套:组件变矮(状态栏消失、思考区伸缩)时清掉残留空行,
81
+ // 否则增量打印会在聊天流里越攒越多幽灵空行
82
+ if (!("clearOnShrink" in settings)) {
83
+ settings["clearOnShrink"] = true;
84
+ changed = true;
85
+ }
80
86
  // 启动时是否显示品牌横幅(logo + 版本号/目录/提示)
81
87
  if (!("showStartupBanner" in settings)) {
82
88
  settings["showStartupBanner"] = true;
package/dist/index.js CHANGED
@@ -8,7 +8,6 @@ import { registerLoopCommand } from "./loop.js";
8
8
  import { ensureSearchTools } from "./search-tools.js";
9
9
  import { ensureUsableShell } from "./shell-doctor.js";
10
10
  import { applyBrandUi, setUpdateNotice } from "./style.js";
11
- import { offerStarterTemplates } from "./templates.js";
12
11
  import { fetchModels, loadCustomEndpoints } from "./api.js";
13
12
  const PACKAGE_NAME = "u1s1-cli";
14
13
  /** 启动时检测到的可自动安装的新版;TUI 退出后才装(见 installPendingUpdate)。 */
@@ -215,7 +214,6 @@ async function runAgent(cfg, args) {
215
214
  name: "u1s1",
216
215
  factory: (pi) => {
217
216
  applyBrandUi(pi, VERSION);
218
- offerStarterTemplates(pi);
219
217
  // /loop: 会话级定时循环执行提示词(对标 Claude Code),实现见 loop.ts
220
218
  registerLoopCommand(pi);
221
219
  // 会话标题:拿用户第一条正经输入的前半截当名字(像 Claude Code 那样),
package/dist/loop.js CHANGED
@@ -8,8 +8,9 @@
8
8
  * /loop 20m /review-pr 1234 循环里也能跑其他斜杠命令
9
9
  * /loop stop 停止当前循环
10
10
  *
11
- * 语义与 Claude Code 对齐:任务只活在当前会话,/clear 或退出即消失。
12
- * 实现核心是 pi.sendUserMessage():定时器到点就"替用户打字"发一轮,
11
+ * 语义与 Claude Code 对齐:任务只活在当前会话,/clear 或退出即消失;
12
+ * 启动时立刻跑第一轮,之后每隔一个间隔再跑。
13
+ * 实现核心是 pi.sendUserMessage():到点就"替用户打字"发一轮,
13
14
  * deliverAs: "followUp" 保证上一轮没跑完时排队等它结束,不硬插打断;
14
15
  * expandPromptTemplates: true 让 prompt 里的斜杠命令也能被展开执行。
15
16
  */
@@ -116,7 +117,7 @@ export function registerLoopCommand(pi) {
116
117
  stopLoop(ctx.ui);
117
118
  activePrompt = parsed.prompt;
118
119
  activeIntervalMs = parsed.intervalMs;
119
- timer = setInterval(() => {
120
+ const fire = () => {
120
121
  try {
121
122
  pi.sendUserMessage(activePrompt, {
122
123
  deliverAs: "followUp",
@@ -126,9 +127,12 @@ export function registerLoopCommand(pi) {
126
127
  catch (e) {
127
128
  ctx.ui.notify(`loop 执行失败: ${e instanceof Error ? e.message : String(e)}`, "error");
128
129
  }
129
- }, activeIntervalMs);
130
+ };
131
+ // 第一轮立刻跑:用户写完命令就想看到结果,不该干等一个间隔
132
+ fire();
133
+ timer = setInterval(fire, activeIntervalMs);
130
134
  ctx.ui.setStatus(STATUS_KEY, `🔁 每${formatInterval(activeIntervalMs)} · ${preview(activePrompt)}`);
131
- ctx.ui.notify(`🔁 已开始:每${formatInterval(activeIntervalMs)}执行一次「${preview(activePrompt)}」`, "info");
135
+ ctx.ui.notify(`🔁 已开始:现在先跑第一轮,之后每${formatInterval(activeIntervalMs)}一次「${preview(activePrompt)}」`, "info");
132
136
  },
133
137
  });
134
138
  }
package/dist/tools.js CHANGED
@@ -81,7 +81,7 @@ export function createSearchTool(cfg) {
81
81
  name: "web_search",
82
82
  label: "联网搜索",
83
83
  description: "Search the web for current information. Use it for anything time-sensitive or outside the codebase: library docs, error messages, latest versions, recent events. Returns a short answer plus ranked results with URLs and snippets. Follow up with web_fetch to read a promising result in full.",
84
- promptSnippet: "搜索互联网获取最新信息(文档、报错、版本、时事)",
84
+ promptSnippet: "Search the internet for up-to-date information (docs, errors, versions, current events)",
85
85
  promptGuidelines: [
86
86
  "Use web_search when the answer depends on information newer than your knowledge or not present in the repository, instead of guessing.",
87
87
  "Prefer one focused web_search query over several near-duplicate queries; each search costs the user credits.",
@@ -160,9 +160,9 @@ export function createImageTool(cfg) {
160
160
  "Saves the result as a local image file and returns its path. " +
161
161
  "Pass local file paths or http(s) URLs in `images` to edit an image or use references (style transfer, adding elements, combining up to 10 images). " +
162
162
  "Prompts work in Chinese or English; describe content, style, composition, and any text to render.",
163
- promptSnippet: "AI 生成/修改图片(文生图、改图、多图合成)",
163
+ promptSnippet: "AI image generation/editing (text-to-image, editing, multi-image composition)",
164
164
  promptGuidelines: [
165
- "Use generate_image when the user wants a picture created or an existing image modified (logo, illustration, poster, placeholder art, 改图/换风格).",
165
+ "Use generate_image when the user wants a picture created or an existing image modified (logo, illustration, poster, placeholder art, style change).",
166
166
  "Each call generates one image and costs the user credits; refine the prompt first instead of regenerating repeatedly.",
167
167
  "To edit an existing image, pass its path in `images` and describe only the change in `prompt`.",
168
168
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "0.16.3",
3
+ "version": "0.16.5",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,8 @@
26
26
  "build": "tsc",
27
27
  "dev": "tsx src/index.ts",
28
28
  "typecheck": "tsc --noEmit",
29
- "prepublishOnly": "npm run build",
29
+ "prepublishOnly": "npm run build && npm test",
30
+ "test": "node scripts/render-regression.mjs",
30
31
  "postinstall": "node scripts/patch-pi.js",
31
32
  "package": "bash ../../scripts/package.sh",
32
33
  "upload-release": "bash ../../scripts/upload-release.sh",
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 渲染回归测试 —— 防止「修好的空行问题在发版后又回来」。
3
+ *
4
+ * 背景:精简 UI 依赖两处修复才能保持每轮回复只占必要的行数:
5
+ * 1. pi 的 AssistantMessageComponent 补丁(patches/*.patch + postinstall patch-pi.js)
6
+ * —— 隐藏的思考块不算可见内容、空标签跳过、隐形思考块后不加间隔空行
7
+ * 2. 扩展给内置工具的 renderShell:"self" + 空 Text —— 工具行零行渲染
8
+ *
9
+ * 本脚本用 pi 真实组件模拟一轮对话并断言行数。pi 升级后补丁没跟上、
10
+ * 或扩展渲染被改动导致行数膨胀时,这里直接失败,挡在发布前。
11
+ *
12
+ * 运行:node scripts/render-regression.mjs(prepublishOnly 会自动跑)
13
+ */
14
+
15
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
16
+ import { existsSync } from "node:fs";
17
+ import { join, dirname } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ // pi 的 exports 不暴露 package.json 和 main,直接向上找 node_modules 里的包目录
21
+ // (npm 全局安装、pnpm workspace 都会把包放在搜索路径上的 node_modules 里)
22
+ function findPiRoot() {
23
+ let dir = dirname(dirname(fileURLToPath(import.meta.url))); // packages/cli
24
+ while (dir !== dirname(dir)) {
25
+ const candidate = join(dir, "node_modules", "@earendil-works", "pi-coding-agent");
26
+ if (existsSync(candidate)) return candidate;
27
+ dir = dirname(dir);
28
+ }
29
+ throw new Error("找不到 @earendil-works/pi-coding-agent 包目录");
30
+ }
31
+ const piRoot = findPiRoot();
32
+
33
+ const { initTheme } = await import(join(piRoot, "dist/modes/interactive/theme/theme.js"));
34
+ const { AssistantMessageComponent } = await import(
35
+ join(piRoot, "dist/modes/interactive/components/assistant-message.js")
36
+ );
37
+ const { ToolExecutionComponent } = await import(
38
+ join(piRoot, "dist/modes/interactive/components/tool-execution.js")
39
+ );
40
+ const { createBashTool } = await import(join(piRoot, "dist/core/tools/bash.js"));
41
+
42
+ initTheme("dark");
43
+
44
+ let failed = 0;
45
+ function check(name, actual, expected) {
46
+ const ok = actual === expected;
47
+ if (!ok) failed++;
48
+ console.log(`${ok ? "✓" : "✗"} ${name}: ${actual} 行(期望 ${expected})`);
49
+ }
50
+
51
+ // 与 u1s1-compact-ui 扩展一致的隐藏渲染器
52
+ function hiddenToolDef(orig) {
53
+ return {
54
+ name: "bash",
55
+ label: "bash",
56
+ description: orig.description,
57
+ parameters: orig.parameters,
58
+ renderShell: "self",
59
+ async execute() {},
60
+ renderCall() {
61
+ return new Text("", 0, 0);
62
+ },
63
+ renderResult(result, { expanded }, theme, context) {
64
+ if (context.isError) {
65
+ const c = result.content.find((x) => x.type === "text");
66
+ return new Text(theme.fg("warning", "✗ ") + theme.fg("muted", c?.text ?? "error"), 0, 0);
67
+ }
68
+ if (!expanded) return new Text("", 0, 0);
69
+ const c = result.content.find((x) => x.type === "text");
70
+ return new Text(c?.text ?? "", 0, 0);
71
+ },
72
+ };
73
+ }
74
+
75
+ // ---- 1. 助手消息(思考+正文,思考隐藏、标签为空):只应有 间隔行+正文 = 2 行 ----
76
+ // pi 补丁失效时这里会变成 4 行(思考块前后各多一个空行)
77
+ const msg = {
78
+ role: "assistant",
79
+ content: [
80
+ { type: "thinking", thinking: "模型的一段思考" },
81
+ { type: "text", text: "回复正文" },
82
+ ],
83
+ };
84
+ const am = new AssistantMessageComponent(msg, true, undefined, "", 1, []);
85
+ const chat1 = new Container();
86
+ chat1.addChild(am);
87
+ check("助手消息(思考+正文,思考隐藏)", chat1.render(80).length, 2);
88
+
89
+ // ---- 2. 隐藏的工具行(带结果):0 行 ----
90
+ const origBash = createBashTool(process.cwd());
91
+ const comp = new ToolExecutionComponent(
92
+ "bash",
93
+ "t1",
94
+ { command: "ls" },
95
+ {},
96
+ hiddenToolDef(origBash),
97
+ null,
98
+ process.cwd(),
99
+ );
100
+ comp.updateResult({ content: [{ type: "text", text: "file1\nfile2\nexit code: 0" }] });
101
+ const chat2 = new Container();
102
+ chat2.addChild(comp);
103
+ check("隐藏的工具行(已完成)", chat2.render(80).length, 0);
104
+
105
+ // ---- 3. 完整一轮:用户消息 + 思考正文回复 + 隐藏工具行 + 第二条回复 ----
106
+ // 期望:间隔 + 用户 + (间隔+正文) + 0 + (间隔+正文) = 6 行
107
+ const chat3 = new Container();
108
+ chat3.addChild(new Spacer(1));
109
+ chat3.addChild(new Text("用户提问", 1, 0));
110
+ chat3.addChild(new AssistantMessageComponent(mkMsg("思考1", "我先看看。"), true, undefined, "", 1, []));
111
+ chat3.addChild(comp);
112
+ chat3.addChild(new AssistantMessageComponent(mkMsg("思考2", "我去查一下。"), true, undefined, "", 1, []));
113
+ function mkMsg(thinking, text) {
114
+ return {
115
+ role: "assistant",
116
+ content: [
117
+ ...(thinking ? [{ type: "thinking", thinking }] : []),
118
+ { type: "text", text },
119
+ ],
120
+ };
121
+ }
122
+ check("完整一轮对话", chat3.render(80).length, 6);
123
+
124
+ if (failed > 0) {
125
+ console.error(`\n渲染回归测试失败:${failed} 项。多半是 pi 升级后补丁没跟上,或扩展渲染被改动。`);
126
+ console.error("对照 patches/@earendil-works__pi-coding-agent*.patch 和 agent-setup.ts 里的 writeCompactUiExtension 检查。");
127
+ process.exit(1);
128
+ }
129
+ console.log("\n渲染回归测试全部通过 ✓");
@@ -1,2 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- export declare function offerStarterTemplates(pi: ExtensionAPI): void;
package/dist/templates.js DELETED
@@ -1,48 +0,0 @@
1
- import { readdirSync } from "node:fs";
2
- /**
3
- * 空目录首跑模板(roadmap-next #3):小白第一次在空文件夹里启动 u1s1,
4
- * 面对空白输入框往往想不出第一句说什么。弹一个选择器给几个现成的小项目,
5
- * 选中后把完整提示词放进输入框——不直接替用户发送,让他看到「原来就是
6
- * 说这样一句话」,想改就改,回车才开跑。选「我自己说」或按 Esc 都不打扰。
7
- */
8
- const TEMPLATES = [
9
- {
10
- label: "🏠 个人主页——介绍我自己的网页",
11
- prompt: "帮我做一个个人主页网页,介绍我自己,放上我的爱好和联系方式,风格简洁好看,手机和电脑打开都正常。做完教我怎么在浏览器里预览。",
12
- },
13
- {
14
- label: "📅 倒数日——距离某天还有多少天",
15
- prompt: "做一个倒数日网页,可以输入日期和事件名,大字显示距离那天还有多少天,设计得可爱一点。做完教我怎么在浏览器里预览。",
16
- },
17
- {
18
- label: "🖼️ 电子相册——可以翻页的照片墙",
19
- prompt: "帮我做一个电子相册网页,照片可以左右翻页,先放几张占位图,并告诉我之后怎么换成自己的照片。做完教我怎么在浏览器里预览。",
20
- },
21
- ];
22
- const SELF_DESCRIBE = "✍️ 不用了,我自己说";
23
- /** 只看可见条目:.git / .DS_Store 这类隐藏文件不算「有东西」。读不了就当非空,不打扰。 */
24
- function isEmptyDir(dir) {
25
- try {
26
- return readdirSync(dir).filter((name) => !name.startsWith(".")).length === 0;
27
- }
28
- catch {
29
- return false;
30
- }
31
- }
32
- let offered = false;
33
- export function offerStarterTemplates(pi) {
34
- pi.on("session_start", async (_event, ctx) => {
35
- // /clear 等新会话也会触发 session_start,每次启动只弹一次
36
- if (ctx.mode !== "tui" || offered)
37
- return;
38
- if (!isEmptyDir(process.cwd()))
39
- return;
40
- offered = true;
41
- const choice = await ctx.ui.select("这个文件夹还是空的,挑一个现成的小项目直接开跑?", [...TEMPLATES.map((t) => t.label), SELF_DESCRIBE]);
42
- const t = TEMPLATES.find((x) => x.label === choice);
43
- if (!t)
44
- return;
45
- ctx.ui.setEditorText(t.prompt);
46
- ctx.ui.notify("提示词放进输入框了:想改哪句直接改,按回车开跑", "info");
47
- });
48
- }