@trim21/personal-pi-extensions 0.1.586 → 0.1.588

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.586",
3
+ "version": "0.1.588",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
package/src/bwrap/core.ts CHANGED
@@ -134,6 +134,15 @@ export function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
134
134
  }
135
135
  }
136
136
 
137
+ /**
138
+ * 把配置文件形状(可缺省、容忍未知字段)补全成完整 BwrapConfig:合并默认值后走
139
+ * 同一套 schema 校验。与 loadBwrapConfig 的文件路径共用同一条补全语义,供
140
+ * frontmatter 等非文件来源的配置使用(配置的加载/构造与沙箱创建解耦)。
141
+ */
142
+ export function completeBwrapConfig(file: BwrapConfigFile): BwrapConfig {
143
+ return Value.Parse(bwrapConfigSchema, deepMerge(DEFAULT_CONFIG, file));
144
+ }
145
+
137
146
  function deepMerge(base: BwrapConfig, overrides: Partial<BwrapConfig>): BwrapConfig {
138
147
  return {
139
148
  mode: overrides.mode ?? base.mode,
@@ -23,6 +23,7 @@ import { type SelectAction, selectMultiple, selectWithOptionalInput } from "../l
23
23
  import { type ApprovalRule, evaluateBashApproval, matchRule } from "./approval-rules.js";
24
24
  import { commandPatternsFor } from "./approval-suggest.js";
25
25
  import {
26
+ type BwrapConfig,
26
27
  type BwrapMode,
27
28
  findBwrap,
28
29
  findMihomo,
@@ -308,12 +309,17 @@ const SANDBOX_ESCAPE_HATCH =
308
309
  * 命令没经沙箱(allow-all、审批通过的全权限、Windows)时返回 undefined:
309
310
  * 没有沙箱就没什么可提示的。
310
311
  */
311
- export function describeSandbox(resolved: ResolvedBwrap, unsandboxed: boolean): string | undefined {
312
+ export function describeSandbox(
313
+ resolved: ResolvedBwrap,
314
+ unsandboxed: boolean,
315
+ fixed = false,
316
+ ): string | undefined {
312
317
  if (unsandboxed) return undefined;
313
318
  return [
314
319
  "<system-reminder>",
315
320
  `This command ran in a sandbox: ${describeLimits(resolved)}.`,
316
- SANDBOX_ESCAPE_HATCH,
321
+ // 固定沙箱的提权请求会被直接拒绝,再提示 escape hatch 只会诱导无效请求
322
+ ...(fixed ? [] : [SANDBOX_ESCAPE_HATCH]),
317
323
  "</system-reminder>",
318
324
  ].join("\n");
319
325
  }
@@ -347,9 +353,16 @@ export class BwrapRuntime {
347
353
  * 独立入口各持一份、经 pi.events 同步。
348
354
  */
349
355
  private readonly policy: RequestPolicy;
356
+ /**
357
+ * 创建时传入的完整配置:加载配置与创建解耦——有它就不再读盘,且沙箱视为固定
358
+ * (不注册 /bwrap-* 命令、非沙盒请求一律拒绝)。undefined 表示从全局/项目
359
+ * bwrap.json 加载(主会话入口的现状行为)。
360
+ */
361
+ private readonly config: BwrapConfig | undefined;
350
362
 
351
- constructor(policy: RequestPolicy) {
363
+ constructor(policy: RequestPolicy, config?: BwrapConfig) {
352
364
  this.policy = policy;
365
+ this.config = config;
353
366
  }
354
367
 
355
368
  setup(pi: ExtensionAPI): void {
@@ -394,11 +407,18 @@ export class BwrapRuntime {
394
407
  pi.on("before_agent_start", (event, ctx) => {
395
408
  const runtime = this.resolve(ctx);
396
409
  const isWindows = process.platform === "win32";
397
- const modeText = ctx.hasUI
398
- ? isWindows
399
- ? "Every bash command requires user approval before it runs."
400
- : `Current bwrap mode: **${runtime.mode}**. The bwrap runtime selects sandboxing and, when requested, user approval for unsandboxed execution.`
401
- : `Current bwrap mode: **${runtime.mode}**. This headless session cannot ask for approval: commands that require user approval are denied.`;
410
+ let modeText: string;
411
+ if (ctx.hasUI) {
412
+ if (isWindows) {
413
+ modeText = "Every bash command requires user approval before it runs.";
414
+ } else if (this.config === undefined) {
415
+ modeText = `Current bwrap mode: **${runtime.mode}**. The bwrap runtime selects sandboxing and, when requested, user approval for unsandboxed execution.`;
416
+ } else {
417
+ modeText = `Current bwrap mode: **${runtime.mode}**, fixed by the agent's declared sandbox config. Unsandboxed execution requests are refused without approval.`;
418
+ }
419
+ } else {
420
+ modeText = `Current bwrap mode: **${runtime.mode}**. This headless session cannot ask for approval: commands that require user approval are denied.`;
421
+ }
402
422
  const unavailableText =
403
423
  !isWindows && this.bwrapUnavailable
404
424
  ? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
@@ -414,11 +434,16 @@ export class BwrapRuntime {
414
434
  };
415
435
  });
416
436
 
417
- this.registerCommands(pi);
437
+ // 固定沙箱(创建时显式传入配置)不注册 /bwrap-* 命令:切模式与恢复提权审批
438
+ // 都可能放宽声明的沙箱
439
+ if (this.config === undefined) this.registerCommands(pi);
418
440
  }
419
441
 
420
442
  setMode(cwd: string, mode: BwrapMode): ResolvedBwrap {
421
- this.resolved = loadSandboxConfig({ workspace: cwd, mode });
443
+ this.resolved =
444
+ this.config === undefined
445
+ ? loadSandboxConfig({ workspace: cwd, mode })
446
+ : resolveBwrap({ ...this.config, mode });
422
447
  return this.resolved;
423
448
  }
424
449
 
@@ -453,9 +478,13 @@ export class BwrapRuntime {
453
478
  // 用户在审批框选「Run this in sandbox」拒绝提权:命令降级为沙盒内执行
454
479
  let userChoseSandbox = false;
455
480
  if (needsApproval && runtime.bwrapEnabled) {
456
- // /bwrap-deny-request:非沙盒请求直接拒绝——审批规则与审批框都不再参与,
457
- // 拒绝文案与用户点 Deny 相同,直到用户用 /bwrap-allow-request 恢复审批。
458
- if (request.requestFullAccess === true && this.policy.deniesRequests()) {
481
+ // /bwrap-deny-request 与固定沙箱(创建时显式传入配置):非沙盒请求直接
482
+ // 拒绝——审批规则与审批框都不再参与,拒绝文案与用户点 Deny 相同
483
+ // (deny-request 可用 /bwrap-allow-request 恢复审批,固定沙箱不可恢复)。
484
+ if (
485
+ request.requestFullAccess === true &&
486
+ (this.config !== undefined || this.policy.deniesRequests())
487
+ ) {
459
488
  throw new Error(UNSANDBOXED_DENIED);
460
489
  }
461
490
  // 先按 approvalRules 自动判定:allow 直接放行,deny 直接拒绝,未命中才弹框
@@ -484,7 +513,9 @@ export class BwrapRuntime {
484
513
  }
485
514
  // 命令失败时附带的沙箱状态(写边界 + 网络层级),由上层拼进错误文本。
486
515
  // 用户选「Run this in sandbox」时不附:沙箱作用域改由 sandboxReminder 说明
487
- const sandboxHint = userChoseSandbox ? undefined : describeSandbox(runtime, local);
516
+ const sandboxHint = userChoseSandbox
517
+ ? undefined
518
+ : describeSandbox(runtime, local, this.config !== undefined);
488
519
  // 提权被拒、命令在沙盒内执行的系统提醒:成功与失败都由上层附上
489
520
  const sandboxReminder = userChoseSandbox ? describeSandboxChoice(runtime) : undefined;
490
521
  // 计时起点放在审批之后:审批弹窗的等待时长属于用户 UI 操作,不是命令运行时间
@@ -592,8 +623,7 @@ export class BwrapRuntime {
592
623
  }
593
624
 
594
625
  private resolve(ctx: Pick<ExtensionContext, "cwd">): ResolvedBwrap {
595
- const config = loadBwrapConfig(ctx.cwd);
596
- if (!this.resolved) this.resolved = resolveBwrap(config);
626
+ if (!this.resolved) this.resolved = resolveBwrap(this.config ?? loadBwrapConfig(ctx.cwd));
597
627
  return this.resolved;
598
628
  }
599
629
 
@@ -960,7 +990,15 @@ export class BwrapRuntime {
960
990
  }
961
991
  }
962
992
 
963
- /** 缺省自建一份不跨入口同步的策略;入口通常显式传入 `createRequestPolicy(pi.events)`。 */
964
- export function createBwrapRuntime(policy: RequestPolicy = createRequestPolicy()): BwrapRuntime {
965
- return new BwrapRuntime(policy);
993
+ /**
994
+ * 缺省自建一份不跨入口同步的策略;入口通常显式传入 `createRequestPolicy(pi.events)`。
995
+ * config 为完整配置(completeBwrapConfig 的产物):加载配置与创建解耦,传入后不再
996
+ * 读盘,且沙箱固定(不注册 /bwrap-* 命令、非沙盒请求直接拒绝)。缺省从全局/项目
997
+ * bwrap.json 加载。
998
+ */
999
+ export function createBwrapRuntime(
1000
+ policy: RequestPolicy = createRequestPolicy(),
1001
+ config?: BwrapConfig,
1002
+ ): BwrapRuntime {
1003
+ return new BwrapRuntime(policy, config);
966
1004
  }
@@ -16,6 +16,10 @@
16
16
  * provider: openai # optional; overrides the global default
17
17
  * model: claude-haiku-4-5 # optional; overrides the global default
18
18
  * thinkingLevel: high # optional; overrides the global default
19
+ * sandbox: # optional; fixed bwrap sandbox for the bash tool
20
+ * mode: readonly
21
+ * extraWritablePaths:
22
+ * - /tmp
19
23
  * ---
20
24
  * System prompt for the agent goes here.
21
25
  *
@@ -24,6 +28,16 @@
24
28
  * omitted, the subagent runs with the read-only default toolset from the
25
29
  * spawn-agent config (read/grep/find/ls) unless overridden there.
26
30
  *
31
+ * `sandbox` is a complete bwrap config (same shape as bwrap.json) that becomes
32
+ * the bash tool's fixed sandbox: it is parsed here and passed to the subagent's
33
+ * bwrap runtime directly (config loading is decoupled from sandbox creation),
34
+ * so the user's bwrap.json is not consulted for that agent. Unsandboxed
35
+ * execution requests are refused and /bwrap-* mode commands are not registered.
36
+ * Fields are optional and fall back to the standard bwrap defaults (e.g.
37
+ * `sandbox: {mode: readonly}` alone is enough for a read-only shell);
38
+ * `extraWritablePaths` is the writable escape hatch under a read-only mode.
39
+ * Without `sandbox`, the subagent's bash follows the user bwrap config.
40
+ *
27
41
  * Global defaults for provider/model/thinkingLevel come from
28
42
  * `~/.pi/agent/spawn-agent.json` (see loadSpawnAgentConfig); frontmatter
29
43
  * fields take precedence over them, which in turn take precedence over the
@@ -38,6 +52,8 @@ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
38
52
  import { type Static, Type } from "typebox";
39
53
  import { Value } from "typebox/value";
40
54
 
55
+ import { type BwrapConfig, bwrapConfigFileSchema, completeBwrapConfig } from "./bwrap/core.js";
56
+
41
57
  /**
42
58
  * Valid thinking levels, mirroring pi's CLI --thinking validation
43
59
  * (VALID_THINKING_LEVELS). "off" disables thinking; "max" is deliberately
@@ -52,6 +68,7 @@ const agentFrontmatterSchema = Type.Object({
52
68
  provider: Type.Optional(Type.String()),
53
69
  model: Type.Optional(Type.String()),
54
70
  thinkingLevel: Type.Optional(Type.Union(THINKING_LEVELS.map((level) => Type.Literal(level)))),
71
+ sandbox: Type.Optional(bwrapConfigFileSchema),
55
72
  });
56
73
 
57
74
  function parseAgentFrontmatter(frontmatter: unknown) {
@@ -71,6 +88,8 @@ export interface AgentConfig {
71
88
  model?: string;
72
89
  /** Thinking level, applied via --thinking. */
73
90
  thinkingLevel?: (typeof THINKING_LEVELS)[number];
91
+ /** bash 的完整沙箱配置(bwrap.json 同构);undefined 表示跟随用户 bwrap 配置。 */
92
+ sandbox?: BwrapConfig;
74
93
  systemPrompt: string;
75
94
  filePath: string;
76
95
  }
@@ -118,6 +137,7 @@ export function discoverAgents(dir = join(getAgentDir(), "agents")): AgentConfig
118
137
  provider: fm.provider,
119
138
  model: fm.model,
120
139
  thinkingLevel: fm.thinkingLevel,
140
+ sandbox: fm.sandbox ? completeBwrapConfig(fm.sandbox) : undefined,
121
141
  systemPrompt: body,
122
142
  filePath,
123
143
  });
@@ -26,6 +26,10 @@
26
26
  *
27
27
  * Security default: without an explicit `tools:` in the frontmatter, the
28
28
  * subagent only gets read-only tools (read/grep/find/ls) — no bash/write/edit.
29
+ * A frontmatter `sandbox:` gives the bash tool a fixed bwrap sandbox config
30
+ * (bwrap.json shape, e.g. mode readonly): it is passed to the subagent's bwrap
31
+ * runtime as the complete config, unsandboxed execution requests are refused,
32
+ * and /bwrap-* mode commands are not registered (see spawn-agent-agents.ts).
29
33
  */
30
34
 
31
35
  import { existsSync } from "node:fs";
@@ -42,16 +46,20 @@ import {
42
46
  type ExtensionAPI,
43
47
  type ExtensionUIContext,
44
48
  getAgentDir,
49
+ type InlineExtension,
45
50
  ModelRuntime,
46
51
  type PromptOptions,
47
52
  SessionManager,
48
53
  SettingsManager,
49
54
  truncateTail,
50
- truncateToVisualLines,
51
55
  } from "@earendil-works/pi-coding-agent";
52
56
  import { Type } from "typebox";
53
57
 
58
+ import { type BwrapRuntime, createBwrapRuntime } from "./bwrap/runtime.js";
59
+ import { registerShellTools } from "./claude-code/shell.js";
54
60
  import { type ToolPendant } from "./lib/pendant.js";
61
+ import { createRequestPolicy } from "./lib/request-policy.js";
62
+ import opencodeBash from "./opencode/bash.js";
55
63
  import {
56
64
  type AgentConfig,
57
65
  applyAgentDefaults,
@@ -87,10 +95,11 @@ const SETTINGS_PATH = join(getAgentDir(), "settings.json");
87
95
  * `additionalExtensionPaths` so the subagent uses the enhanced implementation
88
96
  * instead of the built-in one.
89
97
  *
90
- * The bash override also carries the bwrap sandbox: opencode/bash.ts creates
91
- * its own bwrap runtime instance and runs commands through runtime.execute(),
92
- * so agents that declare the bash tool get sandboxing automatically. Agents
93
- * without bash need no bwrap setup (there are no commands to sandbox).
98
+ * `bash` (opencode) and `Bash` (claude-code) are deliberately not in this map:
99
+ * they are injected via an inline extension factory (subagentShellExtension)
100
+ * instead, because their bwrap runtime carries the agent's declared sandbox
101
+ * config — a path-based override has no channel to pass per-agent config
102
+ * (see createSubagentSession).
94
103
  * (Workspace write protection is embedded in the opencode write/edit tools.)
95
104
  *
96
105
  * Claude Code style tools (capitalized names) map to their claude-code
@@ -111,7 +120,6 @@ const TOOL_EXTENSION_OVERRIDES: Record<string, string> = {
111
120
  read: "opencode/files.ts",
112
121
  edit: "opencode/files.ts",
113
122
  write: "opencode/files.ts",
114
- bash: "opencode/bash.ts",
115
123
  grep: "opencode/grep.ts",
116
124
  glob: "opencode/glob.ts",
117
125
  Grep: "claude-code/grep.ts",
@@ -139,7 +147,7 @@ interface UsageStats {
139
147
  turns: number;
140
148
  }
141
149
 
142
- /** 子 agent 的一次运行结果:runAgent 的返回值,也是 details.result 的内容。 */
150
+ /** 子 agent 的一次运行结果:runAgent 的返回值。 */
143
151
  interface SubagentResult {
144
152
  agent: string;
145
153
  task: string;
@@ -153,8 +161,6 @@ interface SubagentResult {
153
161
  }
154
162
 
155
163
  interface SubagentDetails {
156
- /** 子 agent 运行结果(除 pendant 外的字段都收在这里)。 */
157
- result: SubagentResult;
158
164
  /** 折叠 markdown 面板:父 agent 的 prompt 与父 agent 看到的子 agent 结果。 */
159
165
  pendant?: ToolPendant;
160
166
  }
@@ -271,6 +277,20 @@ export function overrideExtensionPaths(tools: string[]): string[] {
271
277
  return paths;
272
278
  }
273
279
 
280
+ /**
281
+ * bash 类工具(opencode `bash` / cc `Bash`)的内联扩展工厂:bwrap runtime 随闭包
282
+ * 携带该 agent 的完整沙箱配置(frontmatter `sandbox`,已在 discoverAgents 补全成
283
+ * BwrapConfig)。路径式 override(additionalExtensionPaths)没有 per-agent 配置
284
+ * 通道,extensionFactories 的工厂闭包是 SDK 提供的唯一注入点。agent.sandbox 为
285
+ * undefined 时与各注册函数的默认构造完全一致(跟随用户 bwrap 配置)。
286
+ */
287
+ export function subagentShellExtension(
288
+ agent: AgentConfig,
289
+ register: (pi: ExtensionAPI, runtime: BwrapRuntime) => void,
290
+ ): InlineExtension {
291
+ return (pi) => register(pi, createBwrapRuntime(createRequestPolicy(pi.events), agent.sandbox));
292
+ }
293
+
274
294
  /**
275
295
  * Resolve the frontmatter model to a runtime Model. A "provider/model" string
276
296
  * carries its own provider; a bare model id uses the declared provider, then
@@ -328,13 +348,18 @@ export async function createSubagentSession(
328
348
  cwd: string,
329
349
  parentUI: ExtensionUIContext | undefined,
330
350
  ): Promise<AgentSession> {
351
+ const tools = agent.tools ?? DEFAULT_TOOLS;
331
352
  const settingsManager = SettingsManager.create(cwd, getAgentDir());
332
353
  const loader = new DefaultResourceLoader({
333
354
  cwd,
334
355
  agentDir: getAgentDir(),
335
356
  settingsManager,
336
357
  noExtensions: true,
337
- additionalExtensionPaths: overrideExtensionPaths(agent.tools ?? DEFAULT_TOOLS),
358
+ additionalExtensionPaths: overrideExtensionPaths(tools),
359
+ extensionFactories: [
360
+ ...(tools.includes("bash") ? [subagentShellExtension(agent, opencodeBash)] : []),
361
+ ...(tools.includes("Bash") ? [subagentShellExtension(agent, registerShellTools)] : []),
362
+ ],
338
363
  appendSystemPrompt: agent.systemPrompt ? [agent.systemPrompt] : undefined,
339
364
  });
340
365
  await loader.reload();
@@ -344,7 +369,7 @@ export async function createSubagentSession(
344
369
  cwd,
345
370
  model: resolveModel(modelRuntime, agent, settingsManager),
346
371
  thinkingLevel: agent.thinkingLevel,
347
- tools: agent.tools ?? DEFAULT_TOOLS,
372
+ tools,
348
373
  sessionManager: SessionManager.inMemory(cwd),
349
374
  settingsManager,
350
375
  resourceLoader: loader,
@@ -437,7 +462,7 @@ export async function runAgent(
437
462
  const footer = usageLine ? `\`${name}\` ${usageLine}` : `\`${name}\``;
438
463
  onUpdate?.({
439
464
  content: [{ type: "text", text: [...logLines, footer].join("\n") }],
440
- details: { result: { ...result } },
465
+ details: {},
441
466
  });
442
467
  };
443
468
 
@@ -563,20 +588,7 @@ export default function spawnAgent(pi: ExtensionAPI) {
563
588
  text: `Unknown agent "${params.agent}". Available agents: ${formatAgentList(agents)}`,
564
589
  },
565
590
  ],
566
- details: {
567
- result: {
568
- agent: params.agent,
569
- task: params.task,
570
- exitCode: 1,
571
- messages: [],
572
- stderr: "",
573
- usage: {
574
- cost: 0,
575
- contextTokens: 0,
576
- turns: 0,
577
- },
578
- },
579
- },
591
+ details: {},
580
592
  isError: true,
581
593
  };
582
594
  }
@@ -599,7 +611,6 @@ export default function spawnAgent(pi: ExtensionAPI) {
599
611
  { type: "text", text: `Subagent "${result.agent}" failed (${reason}):\n${message}` },
600
612
  ],
601
613
  details: {
602
- result,
603
614
  pendant: {
604
615
  subtitle: result.agent,
605
616
  markdown: formatPendantMarkdown(params.task, message),
@@ -612,12 +623,11 @@ export default function spawnAgent(pi: ExtensionAPI) {
612
623
  const output = getFinalOutput(result.messages) || "(no output)";
613
624
  const truncation = truncateTail(output, { maxBytes: MAX_OUTPUT_BYTES });
614
625
  const text = truncation.truncated
615
- ? `${truncation.content}\n\n[Output truncated to ${formatTokens(truncation.content.length)} bytes. Full result preserved in tool details.]`
626
+ ? `${truncation.content}\n\n[Output truncated to ${formatTokens(truncation.content.length)} bytes.]`
616
627
  : output;
617
628
  return {
618
629
  content: [{ type: "text", text }],
619
630
  details: {
620
- result,
621
631
  pendant: {
622
632
  subtitle: result.agent,
623
633
  markdown: formatPendantMarkdown(params.task, text),
@@ -625,52 +635,5 @@ export default function spawnAgent(pi: ExtensionAPI) {
625
635
  },
626
636
  };
627
637
  },
628
-
629
- renderCall(args, theme) {
630
- const preview = args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task;
631
- const line =
632
- theme.fg("toolTitle", theme.bold("spawn_agent ")) + theme.fg("accent", args.agent);
633
- const detail = ` ${theme.fg("dim", preview)}`;
634
- return {
635
- render: (width: number) =>
636
- truncateToVisualLines(`${line}\n${detail}`, 2, width).visualLines,
637
- invalidate: (): void => undefined,
638
- };
639
- },
640
-
641
- renderResult(result, { expanded }, theme) {
642
- const details = result.details.result;
643
- const isError =
644
- details.exitCode !== 0 ||
645
- details.stopReason === "error" ||
646
- details.stopReason === "aborted";
647
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
648
- const finalOutput = getFinalOutput(details.messages);
649
- const usageStr = formatUsageStats(details.usage, details.model);
650
-
651
- const lines: string[] = [];
652
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(details.agent))}`;
653
- if (details.stopReason) header += ` ${theme.fg("error", `[${details.stopReason}]`)}`;
654
- lines.push(header);
655
- if (isError && details.errorMessage) {
656
- lines.push(theme.fg("error", `Error: ${details.errorMessage}`));
657
- }
658
- if (expanded) {
659
- lines.push("", theme.fg("muted", "─── Task ───"), theme.fg("dim", details.task));
660
- if (finalOutput) {
661
- lines.push("", theme.fg("muted", "─── Output ───"), finalOutput.trim());
662
- }
663
- } else if (finalOutput) {
664
- lines.push(theme.fg("toolOutput", finalOutput.split("\n").slice(0, 5).join("\n")));
665
- } else {
666
- lines.push(theme.fg("muted", "(no output)"));
667
- }
668
- if (usageStr) lines.push(theme.fg("dim", usageStr));
669
- return {
670
- render: (width: number) =>
671
- truncateToVisualLines(lines.join("\n"), Infinity, width).visualLines,
672
- invalidate: (): void => undefined,
673
- };
674
- },
675
638
  });
676
639
  }