u1s1-cli 1.14.0 → 1.14.2

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.
@@ -35,6 +35,14 @@ export declare function normalizeDefaultTools(settings: Record<string, unknown>)
35
35
  * 用户在 extensions 里自己写过 builtin:llama.cpp 的任何开关就不动;标记写下后同样不再碰。
36
36
  */
37
37
  export declare function applyLlamaDisabledDefault(settings: Record<string, unknown>): boolean;
38
+ /**
39
+ * 恢复 Pi 默认界面:早先版本替用户写下 hideThinkingBlock=true(隐藏思考块)和
40
+ * clearOnShrink=true(精简 UI 配套),并用扩展把内置工具行渲染成 0 行。现在不再覆盖,
41
+ * 老用户 settings.json 里残留的这两个值要清一次,否则升级后仍看不到思考块。
42
+ * 无法区分「u1s1 写的」和「用户后来自己选的」,所以全员只重置这一次;标记写下后
43
+ * 用户再在 /settings 里选的值就不会被动了。
44
+ */
45
+ export declare function applyNativeUiRestore(settings: Record<string, unknown>): boolean;
38
46
  /** Defaults that don't overwrite values the user already set. */
39
47
  export declare function ensureDefaultSettings(models?: ModelDef[]): void;
40
48
  /** ≤0.4.0 wrote u1s1-dark/u1s1-light into the pi themes dir; remove them. */
@@ -93,12 +101,11 @@ export declare function writeMcpExtension(): void;
93
101
  */
94
102
  export declare function writeAttributionExtension(site?: Site): void;
95
103
  /**
96
- * 生成「精简 UI」扩展到 <agentDir>/extensions/u1s1-compact-ui.js:
97
- * - 隐藏内置工具调用(read/bash/edit/write/grep/find/ls),出错才显示一行;ctrl+o 可展开
98
- * - 每轮回复结束追加一条汇总行,如 ⚙ 9 tools · bash×4 · read×3 · ✗ 1
99
- * - 思考块折叠标签设为空,配合 pi 补丁(见 patches/)消除多余空行
104
+ * 「精简 UI」扩展(u1s1-compact-ui.js)已下线:界面回到 Pi 默认——内置工具调用、思考块都按原样显示。
105
+ * 旧版本每次启动都把它写进 <agentDir>/extensions,Pi 会照单加载并继续隐藏工具行,
106
+ * 所以启动时要把残留文件删掉(Desktop App 共用同一个 agentDir,同样受益)。
100
107
  */
101
- export declare function writeCompactUiExtension(outputDir?: string): void;
108
+ export declare function removeLegacyCompactUiExtension(outputDir?: string): void;
102
109
  /**
103
110
  * pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
104
111
  * 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
@@ -88,6 +88,7 @@ function bundledPiVersion() {
88
88
  const THINKING_DEFAULTS_MARKER = "u1s1ModelThinkingDefaults";
89
89
  const CODEMODE_DEFAULT_MARKER = "u1s1CodemodeDefault";
90
90
  const LLAMA_DISABLED_MARKER = "u1s1LlamaDisabledDefault";
91
+ const NATIVE_UI_MARKER = "u1s1NativeUiRestored";
91
92
  function recordValue(value) {
92
93
  return typeof value === "object" && value !== null && !Array.isArray(value)
93
94
  ? value
@@ -202,6 +203,27 @@ export function applyLlamaDisabledDefault(settings) {
202
203
  settings[LLAMA_DISABLED_MARKER] = true;
203
204
  return true;
204
205
  }
206
+ /**
207
+ * 恢复 Pi 默认界面:早先版本替用户写下 hideThinkingBlock=true(隐藏思考块)和
208
+ * clearOnShrink=true(精简 UI 配套),并用扩展把内置工具行渲染成 0 行。现在不再覆盖,
209
+ * 老用户 settings.json 里残留的这两个值要清一次,否则升级后仍看不到思考块。
210
+ * 无法区分「u1s1 写的」和「用户后来自己选的」,所以全员只重置这一次;标记写下后
211
+ * 用户再在 /settings 里选的值就不会被动了。
212
+ */
213
+ export function applyNativeUiRestore(settings) {
214
+ if (settings[NATIVE_UI_MARKER] === true)
215
+ return false;
216
+ delete settings["hideThinkingBlock"];
217
+ delete settings["clearOnShrink"];
218
+ const terminal = recordValue(settings["terminal"]);
219
+ if (terminal && "clearOnShrink" in terminal) {
220
+ delete terminal["clearOnShrink"];
221
+ if (Object.keys(terminal).length === 0)
222
+ delete settings["terminal"];
223
+ }
224
+ settings[NATIVE_UI_MARKER] = true;
225
+ return true;
226
+ }
205
227
  /** Defaults that don't overwrite values the user already set. */
206
228
  export function ensureDefaultSettings(models = []) {
207
229
  mkdirSync(agentDir, { recursive: true });
@@ -225,16 +247,6 @@ export function ensureDefaultSettings(models = []) {
225
247
  changed = true;
226
248
  }
227
249
  }
228
- if (!("hideThinkingBlock" in settings)) {
229
- settings["hideThinkingBlock"] = true;
230
- changed = true;
231
- }
232
- // 精简 UI 配套:组件变矮(状态栏消失、思考区伸缩)时清掉残留空行,
233
- // 否则增量打印会在聊天流里越攒越多幽灵空行
234
- if (!("clearOnShrink" in settings)) {
235
- settings["clearOnShrink"] = true;
236
- changed = true;
237
- }
238
250
  // 启动时是否显示品牌横幅(logo + 版本号/目录/提示)
239
251
  if (!("showStartupBanner" in settings)) {
240
252
  settings["showStartupBanner"] = true;
@@ -255,6 +267,8 @@ export function ensureDefaultSettings(models = []) {
255
267
  settings["tuiMode"] = "regular";
256
268
  changed = true;
257
269
  }
270
+ if (applyNativeUiRestore(settings))
271
+ changed = true;
258
272
  if (applyCodemodeDefault(settings))
259
273
  changed = true;
260
274
  if (normalizeDefaultTools(settings))
@@ -493,121 +507,16 @@ export function writeAttributionExtension(site = activeSite()) {
493
507
  `}\n`);
494
508
  }
495
509
  /**
496
- * 生成「精简 UI」扩展到 <agentDir>/extensions/u1s1-compact-ui.js:
497
- * - 隐藏内置工具调用(read/bash/edit/write/grep/find/ls),出错才显示一行;ctrl+o 可展开
498
- * - 每轮回复结束追加一条汇总行,如 ⚙ 9 tools · bash×4 · read×3 · ✗ 1
499
- * - 思考块折叠标签设为空,配合 pi 补丁(见 patches/)消除多余空行
510
+ * 「精简 UI」扩展(u1s1-compact-ui.js)已下线:界面回到 Pi 默认——内置工具调用、思考块都按原样显示。
511
+ * 旧版本每次启动都把它写进 <agentDir>/extensions,Pi 会照单加载并继续隐藏工具行,
512
+ * 所以启动时要把残留文件删掉(Desktop App 共用同一个 agentDir,同样受益)。
500
513
  */
501
- export function writeCompactUiExtension(outputDir) {
514
+ export function removeLegacyCompactUiExtension(outputDir) {
502
515
  const dir = outputDir ?? join(agentDir, "extensions");
503
- mkdirSync(dir, { recursive: true });
504
- // 汇总行文案按生成时的界面语言写死进扩展;英文版面向开发者,直接用工具原名
505
- const toolSummaryLabels = tr(`read: "读文件", write: "写文件", edit: "改文件", bash: "跑命令", grep: "搜内容", find: "找文件", ls: "列目录",`, `read: "read", write: "write", edit: "edit", bash: "bash", grep: "grep", find: "find", ls: "ls",`);
506
- const toolCallsLabel = tr(`d.total + " 次工具调用"`, `d.total + (d.total === 1 ? " tool call" : " tool calls")`);
507
- writeFileSync(join(dir, "u1s1-compact-ui.js"), `// 由 u1s1 每次启动自动生成,请勿手改
508
- function oneLine(text, max = 160) {
509
- const flat = String(text).replace(/\\s*\\n\\s*/g, " ; ").trim();
510
- return flat.length > max ? flat.slice(0, max - 1) + "…" : flat;
511
- }
512
-
513
- export default async function (pi) {
514
- // ---- 隐藏思考块折叠标签 ----
515
- pi.on("session_start", async (_event, ctx) => {
516
- if (ctx.hasUI) ctx.ui.setHiddenThinkingLabel("");
517
- });
518
-
519
- // ---- 汇总统计 ----
520
- let counts = {};
521
- let errors = 0;
522
- // 工具调用被折叠后,这一行摘要是新手了解「AI 刚才做了什么」的唯一入口,内置工具名给人话
523
- const TOOL_SUMMARY_LABELS = {
524
- ${toolSummaryLabels}
525
- };
526
- pi.on("agent_start", async () => {
527
- counts = {};
528
- errors = 0;
529
- });
530
- pi.on("tool_call", async (event) => {
531
- counts[event.toolName] = (counts[event.toolName] ?? 0) + 1;
532
- });
533
- pi.on("tool_result", async (event) => {
534
- if (event.isError) errors++;
535
- });
536
- pi.on("agent_end", async () => {
537
- const total = Object.values(counts).reduce((a, b) => a + b, 0);
538
- if (total === 0) return;
539
- const parts = Object.entries(counts)
540
- .sort((a, b) => b[1] - a[1])
541
- .map(([name, n]) => (TOOL_SUMMARY_LABELS[name] ?? name) + "×" + n);
542
- if (errors > 0) parts.push("✗ " + errors);
543
- pi.appendEntry("tool-summary", { total, summary: parts.join(" · ") });
544
- });
545
- pi.registerEntryRenderer("tool-summary", (entry, _opts, theme) => {
546
- const d = entry.data;
547
- let text = theme.fg("muted", "⚙ ");
548
- text += theme.fg("toolTitle", theme.bold(${toolCallsLabel}));
549
- text += theme.fg("dim", " · " + d.summary);
550
- return new Text(text, 0, 0);
551
- });
552
-
553
- // ---- 隐藏各内置工具 ----
554
- const { createReadTool, createBashTool, createEditTool, createWriteTool, createGrepTool, createFindTool, createLsTool, SettingsManager } = await import("@earendil-works/pi-coding-agent");
555
- const { Text } = await import("@earendil-works/pi-tui");
556
-
557
- // pick(ctx) 可按会话上下文换一个同 schema 的实际执行工具(bash 按 shellPath 选)
558
- function hideTool(name, orig, pick = () => orig) {
559
- pi.registerTool({
560
- name,
561
- label: name,
562
- description: orig.description,
563
- parameters: orig.parameters,
564
- // 自己管外壳:不用默认的 Box 包装,空内容才不会产生空行
565
- renderShell: "self",
566
- async execute(toolCallId, params, signal, onUpdate, ctx) {
567
- return pick(ctx).execute(toolCallId, params, signal, onUpdate);
568
- },
569
- renderCall() {
570
- return new Text("", 0, 0);
571
- },
572
- renderResult(result, { expanded }, theme, context) {
573
- if (context.isError) {
574
- const c = result.content.find((x) => x.type === "text");
575
- const msg = oneLine(c && c.type === "text" ? c.text : "error");
576
- // 低调处理:✗ 用警示色,内容用灰色弱化,避免满屏安静时红字过于扎眼
577
- return new Text(theme.fg("warning", "✗ ") + theme.fg("muted", msg), 0, 0);
578
- }
579
- if (!expanded) return new Text("", 0, 0);
580
- const c = result.content.find((x) => x.type === "text");
581
- if (!c || c.type !== "text") return new Text("", 0, 0);
582
- return new Text(c.text, 0, 0);
583
- },
584
- });
585
- }
586
-
587
- const cwd = process.cwd();
588
- // 模型跑的 shell 命令拿不到 U1S1_API_KEY / U1S1_EP_KEY_* 等密钥(进程级守卫之外的显式一层)
589
- const { stripSecretEnv } = await import(${JSON.stringify(new URL("./secret-env.js", import.meta.url).href)});
590
- hideTool("read", createReadTool(cwd));
591
- // 覆盖内置 bash 后 pi 不再替我们传 shellPath:不传就走 pi 的兜底,Git 不在 Program Files 时
592
- // 会命中 System32 的 WSL 转发器,全部命令失败。与内置同源读 settings.json 的 shellPath
593
- // (Windows 上由 shell-doctor 验活写入);项目级设置要看信任,故首次执行时按 ctx 解析一次
594
- const bashOptions = { spawnHook: (spawnContext) => ({ ...spawnContext, env: stripSecretEnv(spawnContext.env) }) };
595
- let bash;
596
- const bashFor = (ctx) => {
597
- if (!bash) {
598
- const shellPath = SettingsManager.create(ctx?.cwd ?? cwd, undefined, { projectTrusted: ctx?.isProjectTrusted() ?? false }).getShellPath();
599
- bash = createBashTool(cwd, { ...bashOptions, shellPath });
516
+ try {
517
+ rmSync(join(dir, "u1s1-compact-ui.js"), { force: true });
600
518
  }
601
- return bash;
602
- };
603
- hideTool("bash", createBashTool(cwd, bashOptions), bashFor);
604
- hideTool("edit", createEditTool(cwd));
605
- hideTool("write", createWriteTool(cwd));
606
- hideTool("grep", createGrepTool(cwd));
607
- hideTool("find", createFindTool(cwd));
608
- hideTool("ls", createLsTool(cwd));
609
- }
610
- `);
519
+ catch { }
611
520
  }
612
521
  /**
613
522
  * pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
@@ -54,6 +54,22 @@ interface SigningProxy {
54
54
  /** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
55
55
  export declare function attachAttestationHeader(holder: AttestationHolder, headers: Headers): Promise<void>;
56
56
  export type ClientSurface = "terminal" | "web" | "desktop" | "cloud";
57
+ /** What a failed gateway fetch looked like at the socket level; never carries request content. */
58
+ export interface GatewayFetchFailure {
59
+ /** Node/undici error code such as ECONNRESET or UND_ERR_CONNECT_TIMEOUT; "unknown" when absent. */
60
+ code: string;
61
+ /** One-line message for the user and the retry classifier, e.g. "fetch failed (ECONNRESET: read ECONNRESET)". */
62
+ detail: string;
63
+ }
64
+ /**
65
+ * undici reports every transport failure as a bare "fetch failed" and puts the
66
+ * real reason (reset, DNS, TLS, connect timeout) in `cause`, sometimes as an
67
+ * AggregateError from happy eyeballs. Keep the original message first so the
68
+ * agent's retry patterns still match.
69
+ */
70
+ export declare function describeFetchFailure(error: unknown): GatewayFetchFailure;
71
+ /** One listener per process (telemetry); failures are reported only when the local caller is still waiting. */
72
+ export declare function onGatewayFetchFailure(listener: ((failure: GatewayFetchFailure) => void) | undefined): void;
57
73
  /**
58
74
  * pi accepts static provider headers only. Keep it behind a loopback proxy that
59
75
  * replaces the local bearer credential with a fresh DPoP proof per request.
@@ -209,6 +209,36 @@ function clientSurface(fallback) {
209
209
  }
210
210
  return process.versions.electron ? "desktop" : fallback;
211
211
  }
212
+ /**
213
+ * undici reports every transport failure as a bare "fetch failed" and puts the
214
+ * real reason (reset, DNS, TLS, connect timeout) in `cause`, sometimes as an
215
+ * AggregateError from happy eyeballs. Keep the original message first so the
216
+ * agent's retry patterns still match.
217
+ */
218
+ export function describeFetchFailure(error) {
219
+ const message = error instanceof Error ? error.message : "signing proxy failed";
220
+ let cause = error instanceof Error ? error.cause : undefined;
221
+ if (cause instanceof AggregateError && cause.errors.length)
222
+ cause = cause.errors[0];
223
+ const causeRecord = typeof cause === "object" && cause !== null ? cause : {};
224
+ const code = typeof causeRecord.code === "string" && causeRecord.code
225
+ ? causeRecord.code
226
+ : typeof error?.code === "string"
227
+ ? error.code
228
+ : "unknown";
229
+ const causeMessage = typeof causeRecord.message === "string" ? causeRecord.message.split("\n")[0].slice(0, 200) : "";
230
+ if (!causeMessage && code === "unknown")
231
+ return { code, detail: message };
232
+ const reason = causeMessage && !causeMessage.includes(code) && code !== "unknown"
233
+ ? `${code}: ${causeMessage}`
234
+ : causeMessage || code;
235
+ return { code, detail: `${message} (${reason})` };
236
+ }
237
+ let gatewayFetchFailureListener;
238
+ /** One listener per process (telemetry); failures are reported only when the local caller is still waiting. */
239
+ export function onGatewayFetchFailure(listener) {
240
+ gatewayFetchFailureListener = listener;
241
+ }
212
242
  /**
213
243
  * pi accepts static provider headers only. Keep it behind a loopback proxy that
214
244
  * replaces the local bearer credential with a fresh DPoP proof per request.
@@ -289,7 +319,14 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
289
319
  res.writeHead(502, { "content-type": "application/json" });
290
320
  // 说明放前面给用户看;原始英文错误保留在括号里,pi 的重试分类正则
291
321
  // (fetch failed / timeout 等)和排查都还认得
292
- const raw = error instanceof Error ? error.message : "signing proxy failed";
322
+ const failure = describeFetchFailure(error);
323
+ try {
324
+ gatewayFetchFailureListener?.(failure);
325
+ }
326
+ catch {
327
+ // Diagnostics must never change the response the agent sees.
328
+ }
329
+ const raw = failure.detail;
293
330
  const message = tr(`连不上 u1s1 服务器,检查一下网络?(${raw})`, `Can't reach the u1s1 server. Check your network? (${raw})`);
294
331
  res.end(JSON.stringify({ error: { message } }));
295
332
  }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync, spawnSync } from "node:child_process";
3
3
  import { writeFileSync } from "node:fs";
4
- import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, removeLegacyWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAnnouncementsExtension, writeAttributionExtension, writeMcpExtension, writeCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
4
+ import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, removeLegacyWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAnnouncementsExtension, writeAttributionExtension, writeMcpExtension, removeLegacyCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { printConsoleBanner } from "./brand.js";
7
7
  import { activeSite, agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readCompactionBudgetSetting, readCompactionReserveTokens, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
@@ -10,7 +10,7 @@ import { ensureSearchTools } from "./search-tools.js";
10
10
  import { ensureUsableShell } from "./shell-doctor.js";
11
11
  import { applyBrandUi, setAnnouncement, setUpdateNotice } from "./style.js";
12
12
  import { AccessDeniedError, AuthError, fetchModels, loadCustomEndpoints, readJsonResponseCapped } from "./api.js";
13
- import { ensureSigningProxy } from "./device-auth.js";
13
+ import { ensureSigningProxy, onGatewayFetchFailure } from "./device-auth.js";
14
14
  import { installChildEnvGuard } from "./secret-env.js";
15
15
  import { deployHintText, markNudge, shouldShowDeployHint } from "./nudges.js";
16
16
  import { createGatewayTelemetry } from "./telemetry.js";
@@ -252,8 +252,8 @@ async function runAgent(cfg, args) {
252
252
  webFetchRender: webFetchRenderEnabled,
253
253
  imageGen: imageGenEnabled,
254
254
  });
255
- // 精简 UI:隐藏工具调用 + 汇总行 + 隐藏思考标签(同样投影到 agentDir/extensions)
256
- writeCompactUiExtension();
255
+ // 精简 UI 已下线,界面回到 Pi 默认;清掉旧版写进 agentDir/extensions 的扩展,否则 Pi 仍会加载它
256
+ removeLegacyCompactUiExtension();
257
257
  // 模型调用错误人话化(同样投影,Desktop App 共享)
258
258
  writeErrorHumanizeExtension();
259
259
  // 会话内公告触达:5 分钟轮询,公告变化时推进会话(同样投影,Desktop 共享)
@@ -282,6 +282,8 @@ async function runAgent(cfg, args) {
282
282
  let firstTaskDoneSent = false;
283
283
  let sessionEndSent = false;
284
284
  telemetry.track("session_start", { surface: "terminal" });
285
+ // 「连不上 u1s1 服务器」只在用户本机可见;记下底层错误码,网络恢复后随批次上报
286
+ onGatewayFetchFailure((failure) => telemetry.track("gateway_unreachable", { code: failure.code, surface: "terminal" }));
285
287
  const trackSessionEnd = () => {
286
288
  if (sessionEndSent)
287
289
  return;
package/dist/subagent.js CHANGED
@@ -133,8 +133,8 @@ export async function runSubagent(opts) {
133
133
  else {
134
134
  model = undefined;
135
135
  }
136
- // 子会话不加载精简 UI 扩展:它用 process.cwd() 重建内置工具,会破坏自定义
137
- // cwd(worktree 隔离)下的路径解析;且 UI 美化对无界面的子 agent 毫无意义
136
+ // 精简 UI 扩展已下线,但旧版 Desktop/并行会话可能还往共用的 agentDir 写它;它用
137
+ // process.cwd() 重建内置工具,会破坏自定义 cwd(worktree 隔离)下的路径解析,子会话一律不加载
138
138
  const cwd = opts.cwd ?? process.cwd();
139
139
  const agentDir = getAgentDir();
140
140
  // 项目信任跟随主会话:SettingsManager 缺省 projectTrusted=true,会把主会话
@@ -1,14 +1,14 @@
1
1
  import { type CliConfig } from "./config.js";
2
2
  /**
3
3
  * 客户端事件上报(运营清单 C1):只报「会话开始/结束、首次工具成功、首次任务
4
- * 完成、部署完成」几个节点,不采任何内容。事件先在内存排队,每 30 秒或退出时
4
+ * 完成、部署完成、连不上网关(只带错误码)」几个节点,不采任何内容。事件先在内存排队,每 30 秒或退出时
5
5
  * 批量 POST /v1/telemetry(每批最多 20 条),失败静默丢弃,绝不阻塞界面、
6
6
  * 绝不拖慢退出(退出冲刷最多等 1 秒)。
7
7
  *
8
8
  * 关闭方式:环境变量 U1S1_TELEMETRY=0,或 ~/.u1s1/agent/settings.json 里
9
9
  * "telemetry": false。关闭后 track 直接丢,不打印任何东西。
10
10
  */
11
- export type TelemetryEventName = "session_start" | "session_end" | "first_tool_success" | "first_task_done" | "deploy_done";
11
+ export type TelemetryEventName = "session_start" | "session_end" | "first_tool_success" | "first_task_done" | "deploy_done" | "gateway_unreachable";
12
12
  export interface TelemetryEvent {
13
13
  name: TelemetryEventName;
14
14
  /** epoch ms */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,8 +42,8 @@
42
42
  "license": "MIT",
43
43
  "homepage": "https://u1s1app.com",
44
44
  "dependencies": {
45
- "@earendil-works/pi-coding-agent": "1.0.0",
46
- "@earendil-works/pi-tui": "1.0.0",
45
+ "@earendil-works/pi-coding-agent": "1.2.0",
46
+ "@earendil-works/pi-tui": "1.2.0",
47
47
  "typebox": "1.3.7"
48
48
  },
49
49
  "devDependencies": {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The repository applies patches/*.patch through pnpm for development, while
5
5
  * global `npm i -g u1s1-cli` installations do not understand pnpm patches.
6
- * This script applies the same compact-UI, Windows TUI and UI-text (zh/en by
6
+ * This script applies the same Windows TUI and UI-text (zh/en by
7
7
  * U1S1_LANG) fixes to installed dependencies so both installation paths behave identically.
8
8
  *
9
9
  * Properties:
@@ -60,55 +60,6 @@ function findTuiDirs() {
60
60
  return [...found];
61
61
  }
62
62
 
63
- // Each tuple is [original, replacement], matching the pnpm coding-agent patch.
64
- const COMPACT_UI_REPLACEMENTS = [
65
- // Hidden thinking does not count as visible content.
66
- [
67
- '|| (c.type === "thinking" && c.thinking.trim())',
68
- '|| (c.type === "thinking" && !this.hideThinkingBlock && c.thinking.trim())',
69
- ],
70
- // Skip a hidden thinking run with an empty label entirely: no ANSI-only row, no trailing spacer.
71
- [
72
- "const hidden = this.thinkingVisibilityOverrides.get(runIndex) ?? this.hideThinkingBlock;",
73
- 'const hidden = this.thinkingVisibilityOverrides.get(runIndex) ?? this.hideThinkingBlock;\nif (hidden && this.hiddenThinkingLabel === "") { continue; }',
74
- ],
75
- ];
76
-
77
- function patchCompactUi() {
78
- const piDirs = findPackageDirs("@earendil-works/pi-coding-agent");
79
- if (piDirs.length === 0) {
80
- console.log("[u1s1] 提示: 未找到 @earendil-works/pi-coding-agent,精简 UI 空行补丁未应用(不影响使用)");
81
- return;
82
- }
83
- for (const piDir of piDirs) {
84
- const target = join(piDir, "dist", "modes", "interactive", "components", "assistant-message.js");
85
- try {
86
- let text = readFileSync(target, "utf8");
87
- if (text.includes("!this.hideThinkingBlock && c.thinking.trim()")) continue;
88
-
89
- let applied = 0;
90
- for (const [from, to] of COMPACT_UI_REPLACEMENTS) {
91
- if (text.includes(to)) {
92
- applied++;
93
- continue;
94
- }
95
- if (!text.includes(from)) continue;
96
- text = text.split(from).join(to);
97
- applied++;
98
- }
99
-
100
- if (applied < COMPACT_UI_REPLACEMENTS.length) {
101
- console.log(
102
- `[u1s1] 提示: pi 版本可能已更新,精简 UI 空行补丁只应用了 ${applied}/${COMPACT_UI_REPLACEMENTS.length} 处(不影响使用)`,
103
- );
104
- }
105
- writeFileSync(target, text);
106
- } catch {
107
- continue;
108
- }
109
- }
110
- }
111
-
112
63
  // ---- TUI 文案(与 pnpm coding-agent patch 同步,patch-pi.test.ts 校验双轨一致) ----
113
64
  // 中英双语:u1s1 CLI 拉起 pi 前写 process.env.U1S1_LANG(国际站 "en",中国站 "zh"),
114
65
  // 补丁注入的文案一律写成 `process.env.U1S1_LANG === "en" ? "English" : "中文"`,
@@ -343,81 +294,85 @@ const LOGIN_PROVIDER_REPLACEMENTS = [
343
294
  // 只换给人看的文案;mcp_servers 系统提示段、工具描述等给模型看的保持英文。
344
295
  // "Sign-in cancelled." 既是返回值又被 === 比较,两处必须一起换。
345
296
  const MCP_TEXT_REPLACEMENTS = [
346
- ['codemode: "called from codemode scripts, which find them with searchTools()",', 'codemode: process.env.U1S1_LANG === "en" ? "called from codemode scripts, which find them with searchTools()" : "由 codemode 脚本调用(脚本用 searchTools() 查找)",'],
347
- ['deferred: "not declared until tool_search loads them, then called directly; no codemode needed",', 'deferred: process.env.U1S1_LANG === "en" ? "not declared until tool_search loads them, then called directly; no codemode needed" : "先不告诉模型,tool_search 找到后再直接调用;不需要 codemode",'],
348
- ['direct: "declared to the model like built-in tools",', 'direct: process.env.U1S1_LANG === "en" ? "declared to the model like built-in tools" : "像内置工具一样直接提供给模型",'],
349
- ['return "disabled";', 'return process.env.U1S1_LANG === "en" ? "disabled" : "已停用";'],
350
- ['return "starting";', 'return process.env.U1S1_LANG === "en" ? "starting" : "启动中";'],
351
- ['return "needs sign-in";', 'return process.env.U1S1_LANG === "en" ? "needs sign-in" : "需要登录";'],
352
- ['return withError ? `failed: ${firstLine(connection.error ?? "unknown error")}` : "failed";', 'return withError ? (process.env.U1S1_LANG === "en" ? `failed: ${firstLine(connection.error ?? "unknown error")}` : `连接失败:${firstLine(connection.error ?? "未知错误")}`) : (process.env.U1S1_LANG === "en" ? "failed" : "连接失败");'],
353
- ['const resourceCount = count > 0 ? ` · ${count} resource${count === 1 ? "" : "s"}` : "";', 'const resourceCount = count > 0 ? (process.env.U1S1_LANG === "en" ? ` · ${count} resource${count === 1 ? "" : "s"}` : ` · ${count} 个资源`) : "";'],
354
- ['return `connected · ${tools.length} tool${tools.length === 1 ? "" : "s"}${resourceCount}`;', 'return process.env.U1S1_LANG === "en" ? `connected · ${tools.length} tool${tools.length === 1 ? "" : "s"}${resourceCount}` : `已连接 · ${tools.length} 个工具${resourceCount}`;'],
355
- ['return "connecting…";', 'return process.env.U1S1_LANG === "en" ? "connecting…" : "连接中…";'],
356
- ['const MCP_USAGE = "Usage: /mcp, /mcp login [server], /mcp logout [server], /mcp reconnect [server]";', 'const MCP_USAGE = process.env.U1S1_LANG === "en" ? "Usage: /mcp, /mcp login [server], /mcp logout [server], /mcp reconnect [server]" : "用法:/mcp、/mcp login [服务名]、/mcp logout [服务名]、/mcp reconnect [服务名]";'],
357
- ['ctx.ui.notify(`MCP tools are only reachable from the codemode or tool_search tool, but neither is active${reason}; they cannot be called.`, "warning");', 'ctx.ui.notify(process.env.U1S1_LANG === "en" ? `MCP tools are only reachable from the codemode or tool_search tool, but neither is active${reason}; they cannot be called.` : `MCP 工具只能经 codemode 或 tool_search 调用,但两者都没开启${reason},暂时用不了。`, "warning");'],
358
- { from: '`config: ${error}`', to: '(process.env.U1S1_LANG === "en" ? `config: ${error}` : `配置错误:${error}`)', expected: 2 },
359
- ['ctx.ui.notify(`MCP servers need attention:\\n${lines.map((line) => ` ${line}`).join("\\n")}\\nRun /mcp to fix.`, "warning");', 'ctx.ui.notify(process.env.U1S1_LANG === "en" ? `MCP servers need attention:\\n${lines.map((line) => ` ${line}`).join("\\n")}\\nRun /mcp to fix.` : `MCP 服务需要处理:\\n${lines.map((line) => ` ${line}`).join("\\n")}\\n输入 /mcp 处理。`, "warning");'],
360
- ['return `Could not update ${server.entry.source}: ${errorMessage(error)}`;', 'return process.env.U1S1_LANG === "en" ? `Could not update ${server.entry.source}: ${errorMessage(error)}` : `无法写入 ${server.entry.source}:${errorMessage(error)}`;'],
361
- ['return `MCP server "${server.entry.name}" does not use OAuth.`;', 'return process.env.U1S1_LANG === "en" ? `MCP server "${server.entry.name}" does not use OAuth.` : `MCP 服务「${server.entry.name}」不用 OAuth 登录。`;'],
362
- ['return "Sign-in cancelled.";', 'return process.env.U1S1_LANG === "en" ? "Sign-in cancelled." : "已取消登录。";'],
363
- ['failure === "Sign-in cancelled." ? "info" : "error"', 'failure === (process.env.U1S1_LANG === "en" ? "Sign-in cancelled." : "已取消登录。") ? "info" : "error"'],
364
- ['return `Sign-in failed: ${errorMessage(error)}`;', 'return process.env.U1S1_LANG === "en" ? `Sign-in failed: ${errorMessage(error)}` : `登录失败:${errorMessage(error)}`;'],
365
- ['return `Signed in, but ${errorMessage(error)}`;', 'return process.env.U1S1_LANG === "en" ? `Signed in, but ${errorMessage(error)}` : `已登录,但连接失败:${errorMessage(error)}`;'],
366
- ['return `MCP server "${server.entry.name}" is disabled.`;', 'return process.env.U1S1_LANG === "en" ? `MCP server "${server.entry.name}" is disabled.` : `MCP 服务「${server.entry.name}」已停用。`;'],
367
- { from: '`overridden: ${line}`', to: '(process.env.U1S1_LANG === "en" ? `overridden: ${line}` : `被覆盖:${line}`)', expected: 2 },
368
- // 面板脚注经 keyHint 走按键脚注的对照表;close/select/save 已在表里,这两个表里没有,就地换
369
- ['confirmLabel: "manage",', 'confirmLabel: process.env.U1S1_LANG === "en" ? "manage" : "管理",'],
370
- { from: 'Label: "back",', to: 'Label: process.env.U1S1_LANG === "en" ? "back" : "返回",', expected: 5 },
371
- ['title: "MCP servers",', 'title: process.env.U1S1_LANG === "en" ? "MCP servers" : "MCP 服务",'],
372
- // 空列表引导到 u1s1 mcp add(CLI 命令),英文同义
373
- { from: '`No MCP servers configured. Add them to ${resolve(getAgentDir(), "mcp.json")} or .pi/mcp.json.`', to: '(process.env.U1S1_LANG === "en" ? `No MCP servers configured. Add one with u1s1 mcp add, or edit ${resolve(getAgentDir(), "mcp.json")}.` : `还没有配置 MCP 服务。用 u1s1 mcp add 添加,或直接编辑 ${resolve(getAgentDir(), "mcp.json")}。`)', expected: 2 },
374
- ['empty: "This server is no longer configured.",', 'empty: process.env.U1S1_LANG === "en" ? "This server is no longer configured." : "这个服务已不在配置里了。",'],
375
- ['? "for this session"', '? (process.env.U1S1_LANG === "en" ? "for this session" : "仅本次会话")'],
376
- ['? `saved to the ${entry.scope} mcp.json`', '? (process.env.U1S1_LANG === "en" ? `saved to the ${entry.scope} mcp.json` : `保存到 mcp.json(${entry.scope})`)'],
377
- [': "saved to mcp.json";', ': (process.env.U1S1_LANG === "en" ? "saved to mcp.json" : "保存到 mcp.json");'],
378
- ['label: "Enable", description: saved', 'label: process.env.U1S1_LANG === "en" ? "Enable" : "启用", description: saved'],
379
- ['label: "Sign in", description: "opens the browser"', 'label: process.env.U1S1_LANG === "en" ? "Sign in" : "登录", description: process.env.U1S1_LANG === "en" ? "opens the browser" : "会打开浏览器"'],
380
- ['label: "Tools", description: `${connection.tools.length} offered`', 'label: process.env.U1S1_LANG === "en" ? "Tools" : "工具", description: process.env.U1S1_LANG === "en" ? `${connection.tools.length} offered` : `共 ${connection.tools.length} 个`'],
381
- ['label: "Reconnect" }', 'label: process.env.U1S1_LANG === "en" ? "Reconnect" : "重新连接" }'],
382
- ['label: "Sign out", description: "deletes the stored credentials"', 'label: process.env.U1S1_LANG === "en" ? "Sign out" : "退出登录", description: process.env.U1S1_LANG === "en" ? "deletes the stored credentials" : "删除已保存的登录凭据"'],
383
- ['label: "Exposure", description: exposureOf(entry)', 'label: process.env.U1S1_LANG === "en" ? "Exposure" : "暴露方式", description: exposureOf(entry)'],
384
- ['label: "Disable", description: saved', 'label: process.env.U1S1_LANG === "en" ? "Disable" : "停用", description: saved'],
385
- ['`State: ${describeState(server, false)}`', '(process.env.U1S1_LANG === "en" ? `State: ${describeState(server, false)}` : `状态:${describeState(server, false)}`)'],
386
- { from: '`MCP server ${name}`', to: '(process.env.U1S1_LANG === "en" ? `MCP server ${name}` : `MCP 服务 ${name}`)', expected: 3 },
387
- ['title: `Tools of ${server.entry.name}`,', 'title: process.env.U1S1_LANG === "en" ? `Tools of ${server.entry.name}` : `${server.entry.name} 的工具`,'],
388
- ['details: `Exposure ${exposure}: ${exposure === "hidden" ? "unreachable" : EXPOSURE_DESCRIPTIONS[exposure]}${overridden ? "\\nSome tools override it with toolExposure." : ""}`,', 'details: process.env.U1S1_LANG === "en" ? `Exposure ${exposure}: ${exposure === "hidden" ? "unreachable" : EXPOSURE_DESCRIPTIONS[exposure]}${overridden ? "\\nSome tools override it with toolExposure." : ""}` : `暴露方式 ${exposure}:${exposure === "hidden" ? "不可调用" : EXPOSURE_DESCRIPTIONS[exposure]}${overridden ? "\\n部分工具用 toolExposure 单独设置了。" : ""}`,'],
389
- ['empty: "The server offers no tools.",', 'empty: process.env.U1S1_LANG === "en" ? "The server offers no tools." : "这个服务没有提供工具。",'],
390
- ['title: `Exposure of ${server.entry.name}`,', 'title: process.env.U1S1_LANG === "en" ? `Exposure of ${server.entry.name}` : `${server.entry.name} 的暴露方式`,'],
391
- ['? `Applies to this session; the server is registered by ${server.entry.source}.`', '? (process.env.U1S1_LANG === "en" ? `Applies to this session; the server is registered by ${server.entry.source}.` : `仅本次会话生效;这个服务由 ${server.entry.source} 注册。`)'],
392
- [': `Saved to ${server.entry.source}.`,', ': (process.env.U1S1_LANG === "en" ? `Saved to ${server.entry.source}.` : `保存到 ${server.entry.source}。`),'],
393
- ['const title = `Sign in to ${name}`;', 'const title = process.env.U1S1_LANG === "en" ? `Sign in to ${name}` : `登录 ${name}`;'],
394
- ['ui.status(title, "Contacting the authorization server…");', 'ui.status(title, process.env.U1S1_LANG === "en" ? "Contacting the authorization server…" : "正在联系授权服务器…");'],
395
- ['ui.status(title, "Connecting…");', 'ui.status(title, process.env.U1S1_LANG === "en" ? "Connecting…" : "连接中…");'],
396
- ['"Reconnecting…"', '(process.env.U1S1_LANG === "en" ? "Reconnecting…" : "重新连接中…")'],
397
- ['action === "enable" ? "Connecting…" : "Disconnecting…"', 'action === "enable" ? (process.env.U1S1_LANG === "en" ? "Connecting…" : "连接中…") : (process.env.U1S1_LANG === "en" ? "Disconnecting…" : "断开中…")'],
398
- ['ctx.ui.notify(`No MCP server named "${name}".`, "error");', 'ctx.ui.notify(process.env.U1S1_LANG === "en" ? `No MCP server named "${name}".` : `没有叫「${name}」的 MCP 服务。`, "error");'],
399
- ['ctx.ui.select("MCP server", ', 'ctx.ui.select(process.env.U1S1_LANG === "en" ? "MCP server" : "选择 MCP 服务", '],
400
- ['none: "No enabled MCP server uses OAuth. Only HTTP servers without an Authorization header do.",', 'none: process.env.U1S1_LANG === "en" ? "No enabled MCP server uses OAuth. Only HTTP servers without an Authorization header do." : "没有用 OAuth 登录的已启用 MCP 服务(只有没配 Authorization 头的 HTTP 服务才用)。",'],
401
- ['ctx.ui.notify(`Signing in to MCP server "${name}" requires interactive mode.`, "error");', 'ctx.ui.notify(process.env.U1S1_LANG === "en" ? `Signing in to MCP server "${name}" requires interactive mode.` : `登录 MCP 服务「${name}」需要在交互界面里进行。`, "error");'],
402
- ['process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open"', 'process.platform === "darwin" ? (process.env.U1S1_LANG === "en" ? "Cmd+click to open" : "Cmd+点击打开") : (process.env.U1S1_LANG === "en" ? "Ctrl+click to open" : "Ctrl+点击打开")'],
403
- ['ctx.ui.notify(`Sign in to MCP server "${name}" in your browser:\\n${lines}`, "info");', 'ctx.ui.notify(process.env.U1S1_LANG === "en" ? `Sign in to MCP server "${name}" in your browser:\\n${lines}` : `请在浏览器里登录 MCP 服务「${name}」:\\n${lines}`, "info");'],
404
- ['ctx.ui.input(`Waiting for sign-in to "${name}". If the browser cannot reach this machine, paste the URL it was redirected to.`,', 'ctx.ui.input(process.env.U1S1_LANG === "en" ? `Waiting for sign-in to "${name}". If the browser cannot reach this machine, paste the URL it was redirected to.` : `等待登录「${name}」。浏览器连不到这台机器的话,把它跳转到的网址粘贴进来。`,'],
405
- ['ctx.ui.notify(`Signed in to MCP server "${name}" (${server.connection?.tools.length ?? 0} tools).`, "info");', 'ctx.ui.notify(process.env.U1S1_LANG === "en" ? `Signed in to MCP server "${name}" (${server.connection?.tools.length ?? 0} tools).` : `已登录 MCP 服务「${name}」(${server.connection?.tools.length ?? 0} 个工具)。`, "info");'],
406
- { from: '`MCP failed to load: ${errorMessage(error)}`', to: '(process.env.U1S1_LANG === "en" ? `MCP failed to load: ${errorMessage(error)}` : `MCP 加载失败:${errorMessage(error)}`)', expected: 2 },
407
- ['ctx.ui.notify("MCP servers are still connecting; their tools become available once connected.", "info");', 'ctx.ui.notify(process.env.U1S1_LANG === "en" ? "MCP servers are still connecting; their tools become available once connected." : "MCP 服务还在连接,连上后工具就能用了。", "info");'],
408
- ['description: "Manage MCP servers: sign in, reconnect, enable or disable, and change exposure",', 'description: process.env.U1S1_LANG === "en" ? "Manage MCP servers: sign in, reconnect, enable or disable, and change exposure" : "管理 MCP 服务:登录、重连、启用/停用、调整暴露方式",'],
409
- ['? `Signed out of MCP server "${server.entry.name}".`', '? (process.env.U1S1_LANG === "en" ? `Signed out of MCP server "${server.entry.name}".` : `已退出 MCP 服务「${server.entry.name}」的登录。`)'],
410
- [': `No stored credentials for MCP server "${server.entry.name}".`, "info");', ': (process.env.U1S1_LANG === "en" ? `No stored credentials for MCP server "${server.entry.name}".` : `MCP 服务「${server.entry.name}」没有保存的登录凭据。`), "info");'],
411
- ['none: "No enabled MCP server to reconnect.",', 'none: process.env.U1S1_LANG === "en" ? "No enabled MCP server to reconnect." : "没有可重连的已启用 MCP 服务。",'],
412
- ['ctx.ui.notify(`Reconnected to MCP server "${server.entry.name}" (${describeState(server)}).`, "info");', 'ctx.ui.notify(process.env.U1S1_LANG === "en" ? `Reconnected to MCP server "${server.entry.name}" (${describeState(server)}).` : `已重新连接 MCP 服务「${server.entry.name}」(${describeState(server)})。`, "info");'],
297
+ ["codemode: \"called from codemode scripts, which find them with searchTools()\",", "codemode: process.env.U1S1_LANG === \"en\" ? \"called from codemode scripts, which find them with searchTools()\" : \"由 codemode 脚本调用(脚本用 searchTools() 查找)\","],
298
+ ["deferred: \"not declared until tool_search loads them, then called directly; no codemode needed\",", "deferred: process.env.U1S1_LANG === \"en\" ? \"not declared until tool_search loads them, then called directly; no codemode needed\" : \"先不告诉模型,tool_search 找到后再直接调用;不需要 codemode\","],
299
+ ["direct: \"declared to the model like built-in tools\",", "direct: process.env.U1S1_LANG === \"en\" ? \"declared to the model like built-in tools\" : \"像内置工具一样直接提供给模型\","],
300
+ ["return \"disabled\";", "return process.env.U1S1_LANG === \"en\" ? \"disabled\" : \"已停用\";"],
301
+ ["return \"starting\";", "return process.env.U1S1_LANG === \"en\" ? \"starting\" : \"启动中\";"],
302
+ ["return \"needs sign-in\";", "return process.env.U1S1_LANG === \"en\" ? \"needs sign-in\" : \"需要登录\";"],
303
+ ["return withError ? `failed: ${firstLine(connection.error ?? \"unknown error\")}` : \"failed\";", "return withError ? (process.env.U1S1_LANG === \"en\" ? `failed: ${firstLine(connection.error ?? \"unknown error\")}` : `连接失败:${firstLine(connection.error ?? \"未知错误\")}`) : (process.env.U1S1_LANG === \"en\" ? \"failed\" : \"连接失败\");"],
304
+ ["return `connected · ${tools.length} tool${tools.length === 1 ? \"\" : \"s\"}${hasResources ? \" · resources\" : \"\"}`;", "return process.env.U1S1_LANG === \"en\" ? `connected · ${tools.length} tool${tools.length === 1 ? \"\" : \"s\"}${hasResources ? \" · resources\" : \"\"}` : `已连接 · ${tools.length} 个工具${hasResources ? \" · 含资源\" : \"\"}`;"],
305
+ ["return \"connecting…\";", "return process.env.U1S1_LANG === \"en\" ? \"connecting…\" : \"连接中…\";"],
306
+ ["const MCP_USAGE = \"Usage: /mcp, /mcp login [server], /mcp logout [server], /mcp reconnect [server]\";", "const MCP_USAGE = process.env.U1S1_LANG === \"en\" ? \"Usage: /mcp, /mcp login [server], /mcp logout [server], /mcp reconnect [server]\" : \"用法:/mcp、/mcp login [服务名]、/mcp logout [服务名]、/mcp reconnect [服务名]\";"],
307
+ ["ctx.ui.notify(`MCP tools are only reachable from the codemode or tool_search tool, but neither is active${reason}; they cannot be called.`, \"warning\");", "ctx.ui.notify(process.env.U1S1_LANG === \"en\" ? `MCP tools are only reachable from the codemode or tool_search tool, but neither is active${reason}; they cannot be called.` : `MCP 工具只能经 codemode 或 tool_search 调用,但两者都没开启${reason},暂时用不了。`, \"warning\");"],
308
+ ["const lines = only ? [] : configErrors.map((error) => `config: ${error}`);", "const lines = only ? [] : configErrors.map((error) => (process.env.U1S1_LANG === \"en\" ? `config: ${error}` : `配置错误:${error}`));"],
309
+ ["ctx.ui.notify(`MCP servers need attention:\\n${lines.map((line) => ` ${line}`).join(\"\\n\")}\\nRun /mcp to fix.`, \"warning\");", "ctx.ui.notify(process.env.U1S1_LANG === \"en\" ? `MCP servers need attention:\\n${lines.map((line) => ` ${line}`).join(\"\\n\")}\\nRun /mcp to fix.` : `MCP 服务需要处理:\\n${lines.map((line) => ` ${line}`).join(\"\\n\")}\\n输入 /mcp 处理。`, \"warning\");"],
310
+ ["return `Could not update ${entry.override ?? entry.source}: ${errorMessage(error)}`;", "return process.env.U1S1_LANG === \"en\" ? `Could not update ${entry.override ?? entry.source}: ${errorMessage(error)}` : `无法写入 ${entry.override ?? entry.source}:${errorMessage(error)}`;"],
311
+ ["return `MCP server \"${server.entry.name}\" does not use OAuth.`;", "return process.env.U1S1_LANG === \"en\" ? `MCP server \"${server.entry.name}\" does not use OAuth.` : `MCP 服务「${server.entry.name}」不用 OAuth 登录。`;"],
312
+ ["return \"Sign-in cancelled.\";", "return process.env.U1S1_LANG === \"en\" ? \"Sign-in cancelled.\" : \"已取消登录。\";"],
313
+ ["return `Sign-in failed: ${errorMessage(error)}`;", "return process.env.U1S1_LANG === \"en\" ? `Sign-in failed: ${errorMessage(error)}` : `登录失败:${errorMessage(error)}`;"],
314
+ ["return `Signed in, but ${errorMessage(error)}`;", "return process.env.U1S1_LANG === \"en\" ? `Signed in, but ${errorMessage(error)}` : `已登录,但连接失败:${errorMessage(error)}`;"],
315
+ ["return `MCP server \"${server.entry.name}\" is disabled.`;", "return process.env.U1S1_LANG === \"en\" ? `MCP server \"${server.entry.name}\" is disabled.` : `MCP 服务「${server.entry.name}」已停用。`;"],
316
+ ["...configErrors.map((error) => `config: ${error}`),", "...configErrors.map((error) => (process.env.U1S1_LANG === \"en\" ? `config: ${error}` : `配置错误:${error}`)),"],
317
+ ["...overridden.map((line) => `overridden: ${line}`),", "...overridden.map((line) => (process.env.U1S1_LANG === \"en\" ? `overridden: ${line}` : `被覆盖:${line}`)),"],
318
+ ["title: \"MCP servers\",", "title: process.env.U1S1_LANG === \"en\" ? \"MCP servers\" : \"MCP 服务\","],
319
+ ["description: `${describeState(server)} · ${exposureOf(server.entry)} · ${server.entry.override ? \"global, project override\" : (server.entry.scope ?? server.entry.source)}`,", "description: `${describeState(server)} · ${exposureOf(server.entry)} · ${server.entry.override ? process.env.U1S1_LANG === \"en\" ? \"global, project override\" : \"全局,项目覆盖\" : (server.entry.scope ?? server.entry.source)}`,"],
320
+ ["empty: `No MCP servers configured. Add them to ${resolve(getAgentDir(), \"mcp.json\")} or .pi/mcp.json.`,", "empty: (process.env.U1S1_LANG === \"en\" ? `No MCP servers configured. Add one with u1s1 mcp add, or edit ${resolve(getAgentDir(), \"mcp.json\")}.` : `还没有配置 MCP 服务。用 u1s1 mcp add 添加,或直接编辑 ${resolve(getAgentDir(), \"mcp.json\")}。`),"],
321
+ ["confirmLabel: \"manage\",", "confirmLabel: process.env.U1S1_LANG === \"en\" ? \"manage\" : \"管理\","],
322
+ ["cancelLabel: \"close\",", "cancelLabel: process.env.U1S1_LANG === \"en\" ? \"close\" : \"关闭\","],
323
+ ["empty: \"This server is no longer configured.\",", "empty: process.env.U1S1_LANG === \"en\" ? \"This server is no longer configured.\" : \"这个服务已不在配置里了。\","],
324
+ { from: "cancelLabel: \"back\",", to: "cancelLabel: process.env.U1S1_LANG === \"en\" ? \"back\" : \"返回\",", expected: 4 },
325
+ ["? \"for this session\"", "? (process.env.U1S1_LANG === \"en\" ? \"for this session\" : \"仅本次会话\")"],
326
+ ["? \"saved to the project mcp.json\"", "? (process.env.U1S1_LANG === \"en\" ? \"saved to the project mcp.json\" : \"保存到项目 mcp.json\")"],
327
+ ["? `saved to the ${entry.scope} mcp.json`", "? (process.env.U1S1_LANG === \"en\" ? `saved to the ${entry.scope} mcp.json` : `保存到 mcp.json(${entry.scope})`)"],
328
+ [": \"saved to mcp.json\";", ": (process.env.U1S1_LANG === \"en\" ? \"saved to mcp.json\" : \"保存到 mcp.json\");"],
329
+ ["const inProjectSaved = \"saved to the project mcp.json\";", "const inProjectSaved = process.env.U1S1_LANG === \"en\" ? \"saved to the project mcp.json\" : \"保存到项目 mcp.json\";"],
330
+ ["items.push({ value: \"enable\", label: \"Enable\", description: saved });", "items.push({ value: \"enable\", label: process.env.U1S1_LANG === \"en\" ? \"Enable\" : \"启用\", description: saved });"],
331
+ ["items.push({ value: \"enable-project\", label: \"Enable in this project\", description: inProjectSaved });", "items.push({ value: \"enable-project\", label: process.env.U1S1_LANG === \"en\" ? \"Enable in this project\" : \"在本项目启用\", description: inProjectSaved });"],
332
+ ["items.push({ value: \"signin\", label: \"Sign in\", description: \"opens the browser\" });", "items.push({ value: \"signin\", label: process.env.U1S1_LANG === \"en\" ? \"Sign in\" : \"登录\", description: process.env.U1S1_LANG === \"en\" ? \"opens the browser\" : \"会打开浏览器\" });"],
333
+ ["items.push({ value: \"tools\", label: \"Tools\", description: `${connection.tools.length} offered` });", "items.push({ value: \"tools\", label: process.env.U1S1_LANG === \"en\" ? \"Tools\" : \"工具\", description: process.env.U1S1_LANG === \"en\" ? `${connection.tools.length} offered` : `共 ${connection.tools.length} 个` });"],
334
+ ["items.push({ value: \"reconnect\", label: \"Reconnect\" });", "items.push({ value: \"reconnect\", label: process.env.U1S1_LANG === \"en\" ? \"Reconnect\" : \"重新连接\" });"],
335
+ ["items.push({ value: \"signout\", label: \"Sign out\", description: \"deletes the stored credentials\" });", "items.push({ value: \"signout\", label: process.env.U1S1_LANG === \"en\" ? \"Sign out\" : \"退出登录\", description: process.env.U1S1_LANG === \"en\" ? \"deletes the stored credentials\" : \"删除已保存的登录凭据\" });"],
336
+ ["items.push({ value: \"exposure\", label: \"Exposure\", description: exposureOf(entry) });", "items.push({ value: \"exposure\", label: process.env.U1S1_LANG === \"en\" ? \"Exposure\" : \"暴露方式\", description: exposureOf(entry) });"],
337
+ ["items.push({ value: \"disable\", label: \"Disable\", description: saved });", "items.push({ value: \"disable\", label: process.env.U1S1_LANG === \"en\" ? \"Disable\" : \"停用\", description: saved });"],
338
+ ["items.push({ value: \"disable-project\", label: \"Disable in this project\", description: inProjectSaved });", "items.push({ value: \"disable-project\", label: process.env.U1S1_LANG === \"en\" ? \"Disable in this project\" : \"在本项目停用\", description: inProjectSaved });"],
339
+ ["...(entry.override ? [`project override: ${entry.override}`] : []),", "...(entry.override ? [process.env.U1S1_LANG === \"en\" ? `project override: ${entry.override}` : `项目覆盖:${entry.override}`] : []),"],
340
+ ["title: `MCP server ${name}`,", "title: (process.env.U1S1_LANG === \"en\" ? `MCP server ${name}` : `MCP 服务 ${name}`),"],
341
+ ["confirmLabel: \"select\",", "confirmLabel: process.env.U1S1_LANG === \"en\" ? \"select\" : \"选择\","],
342
+ ["title: `Tools of ${server.entry.name}`,", "title: process.env.U1S1_LANG === \"en\" ? `Tools of ${server.entry.name}` : `${server.entry.name} 的工具`,"],
343
+ ["details: `Exposure ${exposure}: ${exposure === \"hidden\" ? \"unreachable\" : EXPOSURE_DESCRIPTIONS[exposure]}${overridden ? \"\\nSome tools override it with toolExposure.\" : \"\"}`,", "details: process.env.U1S1_LANG === \"en\" ? `Exposure ${exposure}: ${exposure === \"hidden\" ? \"unreachable\" : EXPOSURE_DESCRIPTIONS[exposure]}${overridden ? \"\\nSome tools override it with toolExposure.\" : \"\"}` : `暴露方式 ${exposure}:${exposure === \"hidden\" ? \"不可调用\" : EXPOSURE_DESCRIPTIONS[exposure]}${overridden ? \"\\n部分工具用 toolExposure 单独设置了。\" : \"\"}`,"],
344
+ ["empty: \"The server offers no tools.\",", "empty: process.env.U1S1_LANG === \"en\" ? \"The server offers no tools.\" : \"这个服务没有提供工具。\","],
345
+ ["confirmLabel: \"back\",", "confirmLabel: process.env.U1S1_LANG === \"en\" ? \"back\" : \"返回\","],
346
+ ["title: `Exposure of ${server.entry.name}`,", "title: process.env.U1S1_LANG === \"en\" ? `Exposure of ${server.entry.name}` : `${server.entry.name} 的暴露方式`,"],
347
+ [": `Saved to ${server.entry.override ?? server.entry.source}.`,", ": (process.env.U1S1_LANG === \"en\" ? `Saved to ${server.entry.override ?? server.entry.source}.` : `保存到 ${server.entry.override ?? server.entry.source}。`),"],
348
+ ["confirmLabel: \"save\",", "confirmLabel: process.env.U1S1_LANG === \"en\" ? \"save\" : \"保存\","],
349
+ ["const title = `Sign in to ${server.entry.name}`;", "const title = process.env.U1S1_LANG === \"en\" ? `Sign in to ${server.entry.name}` : `登录 ${server.entry.name}`;"],
350
+ ["status(\"Contacting the authorization server…\");", "status(process.env.U1S1_LANG === \"en\" ? \"Contacting the authorization server…\" : \"正在联系授权服务器…\");"],
351
+ ["status(\"Connecting…\");", "status(process.env.U1S1_LANG === \"en\" ? \"Connecting…\" : \"连接中…\");"],
352
+ ["return `No MCP servers configured. Add them to ${resolve(getAgentDir(), \"mcp.json\")} or .pi/mcp.json.`;", "return (process.env.U1S1_LANG === \"en\" ? `No MCP servers configured. Add one with u1s1 mcp add, or edit ${resolve(getAgentDir(), \"mcp.json\")}.` : `还没有配置 MCP 服务。用 u1s1 mcp add 添加,或直接编辑 ${resolve(getAgentDir(), \"mcp.json\")}。`);"],
353
+ ["lines.push(`overridden: ${line}`);", "lines.push((process.env.U1S1_LANG === \"en\" ? `overridden: ${line}` : `被覆盖:${line}`));"],
354
+ ["ctx.ui.notify(`No MCP server named \"${name}\".`, \"error\");", "ctx.ui.notify(process.env.U1S1_LANG === \"en\" ? `No MCP server named \"${name}\".` : `没有叫「${name}」的 MCP 服务。`, \"error\");"],
355
+ ["const choice = await ctx.ui.select(\"MCP server\", candidates.map((server) => server.entry.name));", "const choice = await ctx.ui.select(process.env.U1S1_LANG === \"en\" ? \"MCP server\" : \"选择 MCP 服务\", candidates.map((server) => server.entry.name));"],
356
+ ["none: \"No enabled MCP server uses OAuth. Only HTTP servers without an Authorization header do.\",", "none: process.env.U1S1_LANG === \"en\" ? \"No enabled MCP server uses OAuth. Only HTTP servers without an Authorization header do.\" : \"没有用 OAuth 登录的已启用 MCP 服务(只有没配 Authorization 头的 HTTP 服务才用)。\","],
357
+ ["ctx.ui.notify(`Signing in to MCP server \"${name}\" requires interactive mode.`, \"error\");", "ctx.ui.notify(process.env.U1S1_LANG === \"en\" ? `Signing in to MCP server \"${name}\" requires interactive mode.` : `登录 MCP 服务「${name}」需要在交互界面里进行。`, \"error\");"],
358
+ ["ctx.ui.notify(`Sign in to MCP server \"${name}\" in your browser:\\n${url.href}`, \"info\");", "ctx.ui.notify(process.env.U1S1_LANG === \"en\" ? `Sign in to MCP server \"${name}\" in your browser:\\n${url.href}` : `请在浏览器里登录 MCP 服务「${name}」:\\n${url.href}`, \"info\");"],
359
+ ["promptForRedirectUrl: (signal) => ctx.ui.input(`Waiting for sign-in to \"${name}\". If the browser cannot reach this machine, paste the URL it was redirected to.`, \"http://127.0.0.1:.../callback?code=...\", { signal }),", "promptForRedirectUrl: (signal) => ctx.ui.input(process.env.U1S1_LANG === \"en\" ? `Waiting for sign-in to \"${name}\". If the browser cannot reach this machine, paste the URL it was redirected to.` : `等待登录「${name}」。浏览器连不到这台机器的话,把它跳转到的网址粘贴进来。`, \"http://127.0.0.1:.../callback?code=...\", { signal }),"],
360
+ ["ctx.ui.notify(failure, failure === \"Sign-in cancelled.\" ? \"info\" : \"error\");", "ctx.ui.notify(failure, failure === (process.env.U1S1_LANG === \"en\" ? \"Sign-in cancelled.\" : \"已取消登录。\") ? \"info\" : \"error\");"],
361
+ ["ctx.ui.notify(`Signed in to MCP server \"${name}\" (${server.connection?.tools.length ?? 0} tools).`, \"info\");", "ctx.ui.notify(process.env.U1S1_LANG === \"en\" ? `Signed in to MCP server \"${name}\" (${server.connection?.tools.length ?? 0} tools).` : `已登录 MCP 服务「${name}」(${server.connection?.tools.length ?? 0} 个工具)。`, \"info\");"],
362
+ { from: "ctx.ui.notify(`MCP failed to load: ${errorMessage(error)}`, \"error\");", to: "ctx.ui.notify((process.env.U1S1_LANG === \"en\" ? `MCP failed to load: ${errorMessage(error)}` : `MCP 加载失败:${errorMessage(error)}`), \"error\");", expected: 2 },
363
+ ["ctx.ui.notify(\"MCP servers are still connecting; their tools become available once connected.\", \"info\");", "ctx.ui.notify(process.env.U1S1_LANG === \"en\" ? \"MCP servers are still connecting; their tools become available once connected.\" : \"MCP 服务还在连接,连上后工具就能用了。\", \"info\");"],
364
+ ["description: \"Manage MCP servers: sign in, reconnect, enable or disable, and change exposure\",", "description: process.env.U1S1_LANG === \"en\" ? \"Manage MCP servers: sign in, reconnect, enable or disable, and change exposure\" : \"管理 MCP 服务:登录、重连、启用/停用、调整暴露方式\","],
365
+ ["? `Signed out of MCP server \"${server.entry.name}\".`", "? (process.env.U1S1_LANG === \"en\" ? `Signed out of MCP server \"${server.entry.name}\".` : `已退出 MCP 服务「${server.entry.name}」的登录。`)"],
366
+ [": `No stored credentials for MCP server \"${server.entry.name}\".`, \"info\");", ": (process.env.U1S1_LANG === \"en\" ? `No stored credentials for MCP server \"${server.entry.name}\".` : `MCP 服务「${server.entry.name}」没有保存的登录凭据。`), \"info\");"],
367
+ ["none: \"No enabled MCP server to reconnect.\",", "none: process.env.U1S1_LANG === \"en\" ? \"No enabled MCP server to reconnect.\" : \"没有可重连的已启用 MCP 服务。\","],
368
+ ["ctx.ui.notify(`Reconnected to MCP server \"${server.entry.name}\" (${describeState(server)}).`, \"info\");", "ctx.ui.notify(process.env.U1S1_LANG === \"en\" ? `Reconnected to MCP server \"${server.entry.name}\" (${describeState(server)}).` : `已重新连接 MCP 服务「${server.entry.name}」(${describeState(server)})。`, \"info\");"],
413
369
  ];
414
370
  // /mcp 面板外框与登录页(dist/extensions/mcp/ui.js)
415
371
  const MCP_UI_REPLACEMENTS = [
416
- ['frame(theme, "MCP servers", [new Text(theme.fg("muted", "Loading…"), 1, 1)])', 'frame(theme, process.env.U1S1_LANG === "en" ? "MCP servers" : "MCP 服务", [new Text(theme.fg("muted", process.env.U1S1_LANG === "en" ? "Loading…" : "加载中…"), 1, 1)])'],
417
- ['menu.empty ?? "Nothing to show."', 'menu.empty ?? (process.env.U1S1_LANG === "en" ? "Nothing to show." : "没有内容。")'],
418
- ['process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open"', 'process.platform === "darwin" ? (process.env.U1S1_LANG === "en" ? "Cmd+click to open" : "Cmd+点击打开") : (process.env.U1S1_LANG === "en" ? "Ctrl+click to open" : "Ctrl+点击打开")'],
419
- ['theme.fg("muted", "Approve access in your browser. If it did not open, visit:")', 'theme.fg("muted", process.env.U1S1_LANG === "en" ? "Approve access in your browser. If it did not open, visit:" : "请在浏览器里同意授权。浏览器没自动打开的话,访问:")'],
420
- ['theme.fg("muted", "If the browser runs on another machine, paste the URL it was redirected to:")', 'theme.fg("muted", process.env.U1S1_LANG === "en" ? "If the browser runs on another machine, paste the URL it was redirected to:" : "浏览器在另一台机器上的话,把它跳转到的网址粘贴到这里:")'],
372
+ ["this.content = frame(theme, \"MCP servers\", [new Text(theme.fg(\"muted\", \"Loading…\"), 1, 1)]);", "this.content = frame(theme, process.env.U1S1_LANG === \"en\" ? \"MCP servers\" : \"MCP 服务\", [new Text(theme.fg(\"muted\", process.env.U1S1_LANG === \"en\" ? \"Loading…\" : \"加载中…\"), 1, 1)]);"],
373
+ ["body.push(new Text(this.theme.fg(\"muted\", menu.empty ?? \"Nothing to show.\"), 1, 0));", "body.push(new Text(this.theme.fg(\"muted\", menu.empty ?? (process.env.U1S1_LANG === \"en\" ? \"Nothing to show.\" : \"没有内容。\")), 1, 0));"],
374
+ ["new Text(this.theme.fg(\"muted\", \"Approve access in your browser. If it did not open, visit:\"), 1, 0),", "new Text(this.theme.fg(\"muted\", process.env.U1S1_LANG === \"en\" ? \"Approve access in your browser. If it did not open, visit:\" : \"请在浏览器里同意授权。浏览器没自动打开的话,访问:\"), 1, 0),"],
375
+ ["new Text(this.theme.fg(\"muted\", \"If the browser runs on another machine, paste the URL it was redirected to:\"), 1, 0),", "new Text(this.theme.fg(\"muted\", process.env.U1S1_LANG === \"en\" ? \"If the browser runs on another machine, paste the URL it was redirected to:\" : \"浏览器在另一台机器上的话,把它跳转到的网址粘贴到这里:\"), 1, 0),"],
421
376
  ];
422
377
  // codemode 工具结果(dist/extensions/codemode/renderer.js)的固定文案
423
378
  const CODEMODE_RENDER_REPLACEMENTS = [
@@ -563,7 +518,6 @@ const ENABLE_AUTOWRAP = "\\x1b[?7h";`,
563
518
  }
564
519
 
565
520
  try {
566
- patchCompactUi();
567
521
  patchTuiText();
568
522
  patchMainScreenAutowrap();
569
523
  } catch (err) {
@@ -1,18 +1,17 @@
1
1
  /**
2
- * 渲染回归测试 —— 防止「修好的空行问题在发版后又回来」。
2
+ * 渲染回归测试 —— 守住「界面回到 Pi 默认」和「中英文案补丁」。
3
3
  *
4
- * 背景:精简 UI 依赖两处修复才能保持每轮回复只占必要的行数:
5
- * 1. pi 的 AssistantMessageComponent 补丁(patches/*.patch + postinstall patch-pi.js)
6
- * —— 隐藏的思考块不算可见内容、空标签跳过、隐形思考块后不加间隔空行
7
- * 2. 扩展给内置工具的 renderShell:"self" + 空 Text —— 工具行零行渲染
4
+ * 背景:u1s1 曾用扩展隐藏内置工具行、补丁隐藏思考块来做「精简 UI」,现已下线,
5
+ * 工具调用和思考块都按 Pi 原样显示。本脚本用 pi 真实组件断言:
6
+ * 1. 内置工具行(bash)没被任何东西覆盖成 0 行——有调用、有结果就必须有可见行
7
+ * 2. 思考块未隐藏时必须出现在助手消息里(没有被补丁吞掉)
8
+ * 3. Windows ConPTY 的 autowrap 保护、补丁文案跟 U1S1_LANG 切中英
9
+ * pi 升级后补丁没跟上、或又有人把工具行/思考块藏起来时,这里直接失败,挡在发布前。
8
10
  *
9
- * 本脚本用 pi 真实组件模拟一轮对话并断言行数;另外核对补丁文案跟 U1S1_LANG 切中英。
10
- * pi 升级后补丁没跟上、或扩展渲染被改动导致行数膨胀时,这里直接失败,挡在发布前。
11
- *
12
- * 运行:node scripts/render-regression.mjs(prepublishOnly 会自动跑)
11
+ * 运行:node scripts/render-regression.mjs(prepublishOnly 会自动跑)
13
12
  */
14
13
 
15
- import { Container, Spacer, Text, TuiMainScreen } from "@earendil-works/pi-tui";
14
+ import { Container, TuiMainScreen } from "@earendil-works/pi-tui";
16
15
  import { existsSync, readFileSync } from "node:fs";
17
16
  import { join, dirname } from "node:path";
18
17
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -37,99 +36,47 @@ const { AssistantMessageComponent } = await import(
37
36
  const { ToolExecutionComponent } = await import(
38
37
  join(piRoot, "dist/modes/interactive/components/tool-execution.js")
39
38
  );
40
- const { createBashTool } = await import(join(piRoot, "dist/core/tools/bash.js"));
39
+ const { createBashToolDefinition } = await import(join(piRoot, "dist/core/tools/bash.js"));
41
40
 
42
41
  initTheme("dark");
43
42
 
44
43
  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
44
  function checkTrue(name, actual) {
52
45
  const ok = actual === true;
53
46
  if (!ok) failed++;
54
47
  console.log(`${ok ? "✓" : "✗"} ${name}`);
55
48
  }
56
49
 
57
- // 与 u1s1-compact-ui 扩展一致的隐藏渲染器
58
- function hiddenToolDef(orig) {
59
- return {
60
- name: "bash",
61
- label: "bash",
62
- description: orig.description,
63
- parameters: orig.parameters,
64
- renderShell: "self",
65
- async execute() {},
66
- renderCall() {
67
- return new Text("", 0, 0);
68
- },
69
- // pi owns this positional callback contract; keep the adapter narrow here.
70
- renderResult(...args) {
71
- const [result, { expanded }, theme, context] = args;
72
- if (context.isError) {
73
- const c = result.content.find((x) => x.type === "text");
74
- return new Text(theme.fg("warning", "✗ ") + theme.fg("muted", c?.text ?? "error"), 0, 0);
75
- }
76
- if (!expanded) return new Text("", 0, 0);
77
- const c = result.content.find((x) => x.type === "text");
78
- return new Text(c?.text ?? "", 0, 0);
79
- },
80
- };
81
- }
50
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
82
51
 
83
- // ---- 1. 助手消息(思考+正文,思考隐藏、标签为空):只应有 间隔行+正文 = 2 行 ----
84
- // pi 补丁失效时这里会变成 4 行(思考块前后各多一个空行)
85
- const msg = {
52
+ // ---- 1. 内置工具行保持 Pi 原样:调用和结果都可见 ----
53
+ const bashDef = createBashToolDefinition(process.cwd());
54
+ const toolComp = new ToolExecutionComponent("bash", "t1", { command: "echo native-tool-line" }, {}, bashDef, null, process.cwd());
55
+ toolComp.updateResult({ content: [{ type: "text", text: "native-tool-output\nexit code: 0" }] }, false);
56
+ const chatTool = new Container();
57
+ chatTool.addChild(toolComp);
58
+ const toolText = stripAnsi(chatTool.render(80).join("\n"));
59
+ checkTrue("内置工具调用行可见(含命令)", toolText.includes("native-tool-line"));
60
+ checkTrue("内置工具结果可见", toolText.includes("native-tool-output"));
61
+
62
+ // ---- 2. 思考块未隐藏时可见,隐藏时只留标签(Pi 默认行为,没被补丁吞掉) ----
63
+ const thinkingMsg = {
86
64
  role: "assistant",
87
65
  content: [
88
66
  { type: "thinking", thinking: "模型的一段思考" },
89
67
  { type: "text", text: "回复正文" },
90
68
  ],
91
69
  };
92
- const am = new AssistantMessageComponent(msg, true, undefined, "", 1, []);
93
- const chat1 = new Container();
94
- chat1.addChild(am);
95
- check("助手消息(思考+正文,思考隐藏)", chat1.render(80).length, 2);
96
-
97
- // ---- 2. 隐藏的工具行(带结果):0 行 ----
98
- const origBash = createBashTool(process.cwd());
99
- const comp = new ToolExecutionComponent(
100
- "bash",
101
- "t1",
102
- { command: "ls" },
103
- {},
104
- hiddenToolDef(origBash),
105
- null,
106
- process.cwd(),
107
- );
108
- comp.updateResult({ content: [{ type: "text", text: "file1\nfile2\nexit code: 0" }] });
109
- const chat2 = new Container();
110
- chat2.addChild(comp);
111
- check("隐藏的工具行(已完成)", chat2.render(80).length, 0);
112
-
113
- // ---- 3. 完整一轮:用户消息 + 思考正文回复 + 隐藏工具行 + 第二条回复 ----
114
- // 期望:间隔 + 用户 + (间隔+正文) + 0 + (间隔+正文) = 6 行
115
- const chat3 = new Container();
116
- chat3.addChild(new Spacer(1));
117
- chat3.addChild(new Text("用户提问", 1, 0));
118
- chat3.addChild(new AssistantMessageComponent(mkMsg("思考1", "我先看看。"), true, undefined, "", 1, []));
119
- chat3.addChild(comp);
120
- chat3.addChild(new AssistantMessageComponent(mkMsg("思考2", "我去查一下。"), true, undefined, "", 1, []));
121
- function mkMsg(thinking, text) {
122
- return {
123
- role: "assistant",
124
- content: [
125
- ...(thinking ? [{ type: "thinking", thinking }] : []),
126
- { type: "text", text },
127
- ],
128
- };
129
- }
130
- check("完整一轮对话", chat3.render(80).length, 6);
131
-
132
- // ---- 4. Regular-screen frames guard full-width lines against ConPTY eager autowrap ----
70
+ const shown = new Container();
71
+ shown.addChild(new AssistantMessageComponent(thinkingMsg, false, undefined, "Thinking...", 1, []));
72
+ const shownText = stripAnsi(shown.render(80).join("\n"));
73
+ checkTrue("思考块未隐藏时显示思考内容", shownText.includes("模型的一段思考") && shownText.includes("回复正文"));
74
+ const folded = new Container();
75
+ folded.addChild(new AssistantMessageComponent(thinkingMsg, true, undefined, "Thinking...", 1, []));
76
+ const foldedText = stripAnsi(folded.render(80).join("\n"));
77
+ checkTrue("思考块隐藏时显示折叠标签而不是整块消失", foldedText.includes("Thinking...") && foldedText.includes("回复正文") && !foldedText.includes("模型的一段思考"));
78
+
79
+ // ---- 3. Regular-screen frames guard full-width lines against ConPTY eager autowrap ----
133
80
  class RecordingTerminal {
134
81
  columns = 40;
135
82
  rows = 10;
@@ -177,11 +124,10 @@ tui.renderNow();
177
124
  const deletionFrame = terminal.writes.join("");
178
125
  checkTrue("Windows 删除行绘制关闭并恢复 autowrap", deletionFrame.includes("\x1b[?2026h\x1b[?7l") && deletionFrame.includes("\x1b[?7h\x1b[?2026l"));
179
126
 
180
- // ---- 5. 补丁文案跟 U1S1_LANG 走:en(国际站)不能漏出中文,其余保持中文 ----
127
+ // ---- 4. 补丁文案跟 U1S1_LANG 走:en(国际站)不能漏出中文,其余保持中文 ----
181
128
  // 斜杠命令描述在模块加载时求值,换语言要带 query 重新 import 一份新实例;
182
129
  // /hotkeys 表格是 handleHotkeysCommand 里现拼的模板串,从源码抠出来用桩变量求值。
183
130
  const CJK = /[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]/;
184
- const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
185
131
  const prevLang = process.env.U1S1_LANG;
186
132
  const interactiveSrc = readFileSync(join(piRoot, "dist/modes/interactive/interactive-mode.js"), "utf8");
187
133
  const hotkeysSrc = interactiveSrc.slice(
@@ -229,8 +175,8 @@ try {
229
175
  }
230
176
 
231
177
  if (failed > 0) {
232
- console.error(`\n渲染回归测试失败:${failed} 项。多半是 pi 升级后补丁没跟上,或扩展渲染被改动。`);
233
- console.error("对照 patches/@earendil-works__pi-*.patch、patch-pi.js 和 agent-setup.ts 里的 writeCompactUiExtension 检查。");
178
+ console.error(`\n渲染回归测试失败:${failed} 项。多半是 pi 升级后补丁没跟上,或界面又被改成隐藏工具行/思考块。`);
179
+ console.error("对照 patches/@earendil-works__pi-*.patch 与 patch-pi.js 检查。");
234
180
  process.exit(1);
235
181
  }
236
182
  console.log("\n渲染回归测试全部通过 ✓");