pi-cc-extensions 0.8.54 → 0.8.56

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/README.en.md CHANGED
@@ -38,15 +38,15 @@ Run `/reload` after installation.
38
38
 
39
39
  ## Features
40
40
 
41
-
42
- | Feature | Description | Entry point |
43
- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
44
- | Claude Code Output | Tool summaries, expand/collapse, rich edit/write diffs, and`on` / `compact` / `off` modes | `/ccstyle` |
45
- | Fullscreen mode | Tool card/group click-to-toggle, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
46
- | Settings panel | `Style / Diff / Thinking / Feature` tabs: startup header toggle and wheel step | `/ccstyle` |
47
- | Context inspection | Usage breakdown and previews for the system prompt, tools, skills, and messages | `/context` |
48
- | Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
49
- | Theme | Bundled CC Dark and CC Light themes | `/theme` |
41
+ | Feature | Description | Entry point |
42
+ | --------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
43
+ | Claude Code Output | Tool summaries, expand/collapse, rich edit/write diffs, and`on` / `compact` / `off` modes | `/ccstyle` |
44
+ | Markdown enhancements | Mermaid art, admonitions, URL linking, and more | Automatic |
45
+ | Fullscreen mode | Tool card/group click-to-toggle, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
46
+ | Settings panel | `Style / Diff / Thinking / Feature` tabs: startup header toggle and wheel step | `/ccstyle` |
47
+ | Context inspection | Usage breakdown and previews for the system prompt, tools, skills, and messages | `/context` |
48
+ | Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
49
+ | Theme | Bundled CC Dark and CC Light themes | `/theme` |
50
50
 
51
51
  ## Configuration
52
52
 
@@ -70,6 +70,8 @@ Run `/reload` after installation.
70
70
  }
71
71
  ```
72
72
 
73
+ > **Tip**: set `markdown.mermaid` to `final` via `~/.pi/agent/settings.json` or the Mermaid diagrams option in `/settings`. Default `streaming` redraws per frame; `final` renders once at completion.
74
+
73
75
  ## Local development
74
76
 
75
77
  ```bash
@@ -84,15 +86,13 @@ npm run typecheck
84
86
 
85
87
  ## Recommended companions
86
88
 
87
-
88
89
  | Extension | Purpose |
89
- | ------------------------------------------ | -------------------------------------------------------------- |
90
+ | ---------------------------------------- | ------------------------------------------------------------ |
90
91
  | `npm:@tintinweb/pi-subagents` | Parallel SubAgents, background tasks, and worktree isolation |
91
92
  | `npm:@tintinweb/pi-tasks` | Claude Code-style task tracking and coordination |
92
93
  | `npm:pi-mcp-adapter` | On-demand MCP tool discovery with lower context usage |
93
94
  | `npm:@ff-labs/pi-fff` | FFF-powered fuzzy file and content search (fffind / ffgrep) |
94
95
  | `npm:pi-web-access` | Web search, URL fetching, GitHub cloning, PDF/video parsing |
95
- | `npm:pi-theme-picker` | Theme search and live preview |
96
96
  | `npm:@narumitw/pi-usage` | Current-account usage for Codex / Copilot / OpenRouter |
97
97
  | `git:github.com/DietrichGebert/ponytail` | Lazy-mode coding: forces the simplest working solution |
98
98
 
package/README.md CHANGED
@@ -38,15 +38,15 @@ pi install git:github.com/minuque/pi-cc-extensions
38
38
 
39
39
  ## 功能
40
40
 
41
-
42
- | 功能 | 说明 | 入口 |
43
- | ---------------------- | ---------------------------------------------------------------------------------------------------------- | ------------- |
44
- | Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `compact` / `off` 三种模式 | `/ccstyle` |
45
- | Fullscreen mode | 工具卡/group 点击展开与收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
46
- | 配置面板 | `Style / Diff / Thinking / Feature` 四页签,含启动头开关与滚轮步进 | `/ccstyle` |
47
- | 上下文检查 | 查看上下文占用,并预览 System prompt、Tools、Skills 和消息内容 | `/context` |
48
- | Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
49
- | 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/theme` |
41
+ | 功能 | 说明 | 入口 |
42
+ | --------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
43
+ | Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `compact` / `off` 三种模式 | `/ccstyle` |
44
+ | Markdown 增强 | Mermaid 图、提示框、URL 链接化等 | 自动生效 |
45
+ | Fullscreen mode | 工具卡/group 点击展开与收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
46
+ | 配置面板 | `Style / Diff / Thinking / Feature` 四页签,含启动头开关与滚轮步进 | `/ccstyle` |
47
+ | 上下文检查 | 查看上下文占用,并预览 System prompt、Tools、Skills 和消息内容 | `/context` |
48
+ | Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
49
+ | 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/theme` |
50
50
 
51
51
  ## 配置
52
52
 
@@ -66,10 +66,18 @@ pi install git:github.com/minuque/pi-cc-extensions
66
66
  "previewLines": 3, // thinking 预览行数,0 隐藏预览正文
67
67
  "animationIntervalMs": 90, // thinking 标题动画间隔(毫秒)
68
68
  "showStartupHeader": true, // 自定义启动头(logo + tips)开关
69
- "scrollStepLines": 3 // fullscreen 滚轮滚动步进行数
69
+ "scrollStepLines": 3, // fullscreen 滚轮滚动步进行数
70
+ "enableSessionReference": true, // @ session 引用(搜历史会话注入上下文)
71
+ "enableSubagentAutocomplete": true, // @ subagent 自动补全与委派提示
72
+ "enableContextCommand": true, // /context 上下文占用检查
73
+ "enableAgentSummary": true, // 每回合工具统计摘要
74
+ "enableWorkingMessage": true, // Working... 底部 token/耗时展示
75
+ "enableAliases": true // /clear、/exit 别名
70
76
  }
71
77
  ```
72
78
 
79
+ > **建议**:`markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
80
+
73
81
  ## 本地开发
74
82
 
75
83
  ```bash
@@ -78,22 +86,19 @@ npm run typecheck
78
86
  ./test.bat # or pi -e .
79
87
  ```
80
88
 
81
-
82
89
  ## 兼容性
83
90
 
84
91
  - Node.js `>=22.19.0`,Pi `^0.84.0`(通过根目录 `package.json` 的 `pi.extensions` 和 `pi.themes` 加载)
85
92
 
86
93
  ## 推荐搭配
87
94
 
88
-
89
95
  | 扩展 | 用途 |
90
- | ------------------------------------------ | -------------------------------------------------- |
96
+ | ---------------------------------------- | ------------------------------------------------ |
91
97
  | `npm:@tintinweb/pi-subagents` | 并行 SubAgent、后台任务与工作树隔离 |
92
98
  | `npm:@tintinweb/pi-tasks` | Claude Code 风格任务跟踪与协调 |
93
99
  | `npm:pi-mcp-adapter` | 按需发现 MCP 工具,减少上下文占用 |
94
100
  | `npm:@ff-labs/pi-fff` | 模糊文件与内容检索(fffind / ffgrep) |
95
101
  | `npm:pi-web-access` | 网页搜索、URL 抓取、GitHub 克隆、PDF/视频解析 |
96
- | `npm:pi-theme-picker` | 主题搜索和实时预览 |
97
102
  | `npm:@narumitw/pi-usage` | 查看当前账号用量(Codex / Copilot / OpenRouter) |
98
103
  | `git:github.com/DietrichGebert/ponytail` | 极简编码:强制最懒但有效的方案 |
99
104
 
@@ -25,6 +25,12 @@ export type Config = {
25
25
  animationIntervalMs: number;
26
26
  showStartupHeader: boolean;
27
27
  scrollStepLines: number;
28
+ enableSessionReference: boolean;
29
+ enableSubagentAutocomplete: boolean;
30
+ enableContextCommand: boolean;
31
+ enableAgentSummary: boolean;
32
+ enableWorkingMessage: boolean;
33
+ enableAliases: boolean;
28
34
  };
29
35
 
30
36
  const AGENT_DIR = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
@@ -67,6 +73,12 @@ export const DEFAULT_CONFIG: Config = {
67
73
  animationIntervalMs: 90,
68
74
  showStartupHeader: true,
69
75
  scrollStepLines: 3,
76
+ enableSessionReference: true,
77
+ enableSubagentAutocomplete: true,
78
+ enableContextCommand: true,
79
+ enableAgentSummary: true,
80
+ enableWorkingMessage: true,
81
+ enableAliases: true,
70
82
  };
71
83
 
72
84
  function pickEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
@@ -148,6 +160,12 @@ export function normalizeConfig(input: unknown): Config {
148
160
  ),
149
161
  showStartupHeader: source.showStartupHeader !== false,
150
162
  scrollStepLines: pickPositiveInt(source.scrollStepLines, DEFAULT_CONFIG.scrollStepLines, 1, 50),
163
+ enableSessionReference: source.enableSessionReference !== false,
164
+ enableSubagentAutocomplete: source.enableSubagentAutocomplete !== false,
165
+ enableContextCommand: source.enableContextCommand !== false,
166
+ enableAgentSummary: source.enableAgentSummary !== false,
167
+ enableWorkingMessage: source.enableWorkingMessage !== false,
168
+ enableAliases: source.enableAliases !== false,
151
169
  };
152
170
  }
153
171
 
@@ -189,6 +207,12 @@ export function formatConfigStatus(source: Config = config): string {
189
207
  `thinkingAnimation=${source.animationIntervalMs}ms`,
190
208
  `startupHeader=${source.showStartupHeader ? "on" : "off"}`,
191
209
  `scrollStep=${source.scrollStepLines}`,
210
+ `sessionRef=${source.enableSessionReference ? "on" : "off"}`,
211
+ `subagentAuto=${source.enableSubagentAutocomplete ? "on" : "off"}`,
212
+ `context=${source.enableContextCommand ? "on" : "off"}`,
213
+ `agentSummary=${source.enableAgentSummary ? "on" : "off"}`,
214
+ `workingMsg=${source.enableWorkingMessage ? "on" : "off"}`,
215
+ `aliases=${source.enableAliases ? "on" : "off"}`,
192
216
  ].join(" · ");
193
217
  }
194
218
 
@@ -64,6 +64,30 @@ function diffIndicatorDescription(mode: DiffIndicatorMode): string {
64
64
  return "Vertical bar indicators on changed lines (default).";
65
65
  }
66
66
 
67
+ /** 额外功能开关项:on/off 二值,描述随状态切换;切换后需重启生效。 */
68
+ function featureToggleSetting(
69
+ id: string,
70
+ label: string,
71
+ onDescription: string,
72
+ offDescription: string,
73
+ current: boolean,
74
+ ) {
75
+ const setting = {
76
+ id,
77
+ label,
78
+ description: current ? onDescription : offDescription,
79
+ currentValue: current ? "on" : "off",
80
+ values: ["on", "off"],
81
+ };
82
+ return {
83
+ setting,
84
+ apply(on: boolean): void {
85
+ setting.currentValue = on ? "on" : "off";
86
+ setting.description = on ? onDescription : offDescription;
87
+ },
88
+ };
89
+ }
90
+
67
91
  function buildExcludeRenderersSubmenu(
68
92
  onClose: () => void,
69
93
  onLiveChange: () => void,
@@ -334,7 +358,68 @@ export async function showCcstylePanel(
334
358
  buildNumberInputSubmenu(theme, scrollStepSetting, closeSubmenu),
335
359
  };
336
360
 
361
+ // 额外功能开关:注册于扩展加载期,切换后需重启(/reload)生效。
362
+ const sessionReferenceToggle = featureToggleSetting(
363
+ "enableSessionReference",
364
+ "Session reference",
365
+ "@ session mentions search & inject referenced session context. Next restart applies.",
366
+ "Session reference disabled.",
367
+ config.enableSessionReference,
368
+ );
369
+ const subagentAutocompleteToggle = featureToggleSetting(
370
+ "enableSubagentAutocomplete",
371
+ "Subagent autocomplete",
372
+ "@ subagent mentions suggest agents and inject delegation instructions. Next restart applies.",
373
+ "Subagent autocomplete disabled.",
374
+ config.enableSubagentAutocomplete,
375
+ );
376
+ const contextCommandToggle = featureToggleSetting(
377
+ "enableContextCommand",
378
+ "Context usage",
379
+ "/context shows context-window distribution with previews. Next restart applies.",
380
+ "Context command disabled.",
381
+ config.enableContextCommand,
382
+ );
383
+ const agentSummaryToggle = featureToggleSetting(
384
+ "enableAgentSummary",
385
+ "Agent summary",
386
+ "Append per-round tool stats after each agent turn. Next restart applies.",
387
+ "Agent summary disabled.",
388
+ config.enableAgentSummary,
389
+ );
390
+ const workingMessageToggle = featureToggleSetting(
391
+ "enableWorkingMessage",
392
+ "Working message",
393
+ "Extend Working... footer with token count and elapsed time. Next restart applies.",
394
+ "Native Working... footer only.",
395
+ config.enableWorkingMessage,
396
+ );
397
+ const aliasesToggle = featureToggleSetting(
398
+ "enableAliases",
399
+ "Aliases",
400
+ "/clear and /exit aliases enabled. Next restart applies.",
401
+ "Aliases disabled.",
402
+ config.enableAliases,
403
+ );
404
+ const featureToggles: Record<string, { apply: (on: boolean) => void }> = {
405
+ enableSessionReference: sessionReferenceToggle,
406
+ enableSubagentAutocomplete: subagentAutocompleteToggle,
407
+ enableContextCommand: contextCommandToggle,
408
+ enableAgentSummary: agentSummaryToggle,
409
+ enableWorkingMessage: workingMessageToggle,
410
+ enableAliases: aliasesToggle,
411
+ };
412
+
337
413
  const onSettingChange = (id: string, value: string) => {
414
+ // 额外功能开关:字段名与配置布尔字段一一对应,切换后重启生效。
415
+ const featureToggle = featureToggles[id];
416
+ if (featureToggle) {
417
+ (config as unknown as Record<string, boolean>)[id] = value === "on";
418
+ featureToggle.apply(value === "on");
419
+ saveConfig();
420
+ ctx.ui.notify(`Updated ${id}: ${value} (next restart)`, "info");
421
+ return;
422
+ }
338
423
  switch (id) {
339
424
  case "mode": {
340
425
  // 选项值带 Experimental 标记,选择后还原为真实 mode 值。
@@ -450,7 +535,16 @@ export async function showCcstylePanel(
450
535
  {
451
536
  id: "feature",
452
537
  label: "Feature",
453
- items: [startupHeaderSetting, scrollStepSetting],
538
+ items: [
539
+ startupHeaderSetting,
540
+ scrollStepSetting,
541
+ sessionReferenceToggle.setting,
542
+ subagentAutocompleteToggle.setting,
543
+ contextCommandToggle.setting,
544
+ agentSummaryToggle.setting,
545
+ workingMessageToggle.setting,
546
+ aliasesToggle.setting,
547
+ ],
454
548
  },
455
549
  ];
456
550
 
@@ -9,6 +9,8 @@
9
9
  */
10
10
 
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import { formatDuration } from "../../utils/format.ts";
13
+ export { formatDuration };
12
14
 
13
15
  /** 工具分类。 */
14
16
  export type AgentToolCategory = "bash" | "read" | "edit" | "write" | "other";
@@ -98,18 +100,6 @@ export class AgentRunSummary {
98
100
  }
99
101
  }
100
102
 
101
- /** 毫秒 → "1h 2m 3s"/"2m 3s"/"3s";低于 1 秒返回 ""(省略)。 */
102
- export function formatDuration(ms: number): string {
103
- const totalSec = Math.floor(ms / 1000);
104
- if (totalSec < 1) return "";
105
- const hours = Math.floor(totalSec / 3600);
106
- const minutes = Math.floor((totalSec % 3600) / 60);
107
- const seconds = totalSec % 60;
108
- if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
109
- if (minutes > 0) return `${minutes}m ${seconds}s`;
110
- return `${seconds}s`;
111
- }
112
-
113
103
  const plural = (count: number) => (count === 1 ? "" : "s");
114
104
 
115
105
  /** 输出顺序:bash → read → edit → write → other → failed。 */
@@ -94,7 +94,7 @@ type AssistantInternals = {
94
94
  };
95
95
 
96
96
  type PatchedPrototype = typeof AssistantMessageComponent.prototype & {
97
- updateContent: (message: AssistantMessage) => void;
97
+ updateContent: (message: AssistantMessage, isStreaming?: boolean) => void;
98
98
  };
99
99
 
100
100
  // 配置由 claude-code-style 管控(installCompactThinking 注入初始值,
@@ -334,9 +334,14 @@ function compactThinking(pi: ExtensionAPI) {
334
334
  );
335
335
  }
336
336
 
337
- prototype.updateContent = function patchedUpdateContent(message: AssistantMessage) {
337
+ prototype.updateContent = function patchedUpdateContent(
338
+ message: AssistantMessage,
339
+ isStreaming?: boolean,
340
+ ) {
338
341
  const component = this as AssistantMessageComponentLike;
339
342
  const self = this as unknown as AssistantInternals;
343
+ // isStreaming 丢失 → mermaid 流式误渲染来回闪。
344
+ self.isStreaming = isStreaming ?? self.isStreaming;
340
345
  self.lastMessage = message;
341
346
  latestComponent = component;
342
347
  latestComponentTimestamp = message.timestamp;
@@ -344,7 +349,7 @@ function compactThinking(pi: ExtensionAPI) {
344
349
  // Visible mode is intentionally untouched: Shift+Tab restores Pi's exact
345
350
  // built-in Thinking Markdown renderer, including every OpenAI summary stage.
346
351
  if (!self.hideThinkingBlock) {
347
- originalUpdateContent.call(this, message);
352
+ originalUpdateContent.call(this, message, self.isStreaming);
348
353
  return;
349
354
  }
350
355
 
@@ -184,7 +184,6 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
184
184
 
185
185
  const agentMap = new Map(agents.map((a) => [a.name, a]));
186
186
  const agentList = mentions.map((n) => `"${n}" (${agentMap.get(n)!.displayName})`).join(", ");
187
- const agentTypes = mentions.map((n) => `"${n}"`).join(" | ");
188
187
 
189
188
  return {
190
189
  systemPrompt:
@@ -2,7 +2,7 @@ import { VERSION, type AppKeybinding } from "@earendil-works/pi-coding-agent";
2
2
  import { getKeybindings } from "@earendil-works/pi-tui";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { config } from "../../config/config.ts";
5
-
5
+ import { ansi16ToRgb, ansi256ToRgb } from "../../utils/ansi-color.ts";
6
6
  type Rgb = [number, number, number];
7
7
  type StyledPart = {
8
8
  raw: string;
@@ -40,25 +40,6 @@ const PALETTE_SPAN = 0.25;
40
40
  // 行间相位偏移:小步累加 → 整体左上暗→右下亮的对角渐变
41
41
  const LOGO_ROW_PHASE_STEP = 0.08;
42
42
 
43
- const ANSI_16_RGB_TABLE: Rgb[] = [
44
- [0, 0, 0],
45
- [128, 0, 0],
46
- [0, 128, 0],
47
- [128, 128, 0],
48
- [0, 0, 128],
49
- [128, 0, 128],
50
- [0, 128, 128],
51
- [192, 192, 192],
52
- [128, 128, 128],
53
- [255, 0, 0],
54
- [0, 255, 0],
55
- [255, 255, 0],
56
- [0, 0, 255],
57
- [255, 0, 255],
58
- [0, 255, 255],
59
- [255, 255, 255],
60
- ];
61
-
62
43
  function stripAnsi(text: string): string {
63
44
  return text.replace(ANSI_PATTERN, "");
64
45
  }
@@ -104,27 +85,6 @@ function applyTruecolor(rgb: Rgb, text: string): string {
104
85
  return `\x1b[38;2;${red};${green};${blue}m${text}${ANSI_RESET}`;
105
86
  }
106
87
 
107
- function ansi16ToRgb(index: number): Rgb {
108
- return ANSI_16_RGB_TABLE[index] ?? [255, 255, 255];
109
- }
110
-
111
- function ansi256ToRgb(index: number): Rgb {
112
- if (index < 16) return ansi16ToRgb(index);
113
-
114
- if (index >= 232) {
115
- const gray = 8 + (index - 232) * 10;
116
- return [gray, gray, gray];
117
- }
118
-
119
- const cubeIndex = index - 16;
120
- const redIndex = Math.floor(cubeIndex / 36);
121
- const greenIndex = Math.floor((cubeIndex % 36) / 6);
122
- const blueIndex = cubeIndex % 6;
123
- const values = [0, 95, 135, 175, 215, 255];
124
-
125
- return [values[redIndex]!, values[greenIndex]!, values[blueIndex]!];
126
- }
127
-
128
88
  function parseTruecolorAnsi(ansi: string): Rgb | undefined {
129
89
  const match = ansi.match(/38;2;(\d+);(\d+);(\d+)/);
130
90
  if (!match) return undefined;
@@ -1,19 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { formatDuration } from "../../utils/format.ts";
2
3
 
3
4
  const REFRESH_INTERVAL_MS = 1_000;
4
5
  /** Elapsed time is only shown once the turn has run this long. */
5
6
  const SHOW_TIMER_AFTER_MS = 3_000;
6
7
 
7
- function formatDuration(ms: number): string {
8
- const totalSec = Math.floor(ms / 1000);
9
- const hours = Math.floor(totalSec / 3600);
10
- const minutes = Math.floor((totalSec % 3600) / 60);
11
- const seconds = totalSec % 60;
12
- if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
13
- if (minutes > 0) return `${minutes}m ${seconds}s`;
14
- return `${seconds}s`;
15
- }
16
-
17
8
  function formatCount(value: number): string {
18
9
  return new Intl.NumberFormat("en-US").format(value);
19
10
  }
@@ -91,7 +82,10 @@ export default function (pi: ExtensionAPI): void {
91
82
  const tokens = tokenCount();
92
83
  const parts: string[] = [];
93
84
  if (tokens > 0) parts.push(`↓ ${formatCount(tokens)} tokens`);
94
- if (elapsed >= SHOW_TIMER_AFTER_MS || tokens > 0) parts.push(formatDuration(elapsed));
85
+ if (elapsed >= SHOW_TIMER_AFTER_MS || tokens > 0) {
86
+ // formatDuration 低于 1 秒返回 "",此处回退 "0s" 保持计时器连续跳动。
87
+ parts.push(formatDuration(elapsed) || "0s");
88
+ }
95
89
  return parts.length ? `Working... (${parts.join(" · ")})` : "";
96
90
  }
97
91
 
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { config } from "./config/config.ts";
2
3
 
3
4
  // shell
4
5
  import piAliases from "./feature/shell/aliases.ts";
@@ -18,17 +19,17 @@ import markdownEnhance from "./renderer/markdown-enhance.ts";
18
19
 
19
20
  export default function (pi: ExtensionAPI): void {
20
21
  // shell chrome
21
- piAliases(pi);
22
+ if (config.enableAliases) piAliases(pi);
22
23
  piStartupHeader(pi);
23
- workingMessage(pi);
24
+ if (config.enableWorkingMessage) workingMessage(pi);
24
25
 
25
26
  // render stack:thinking controller 直接交给 style 作 query
26
27
  markdownEnhance(pi);
27
28
  claudeCodeStyle(pi, undefined, installCompactThinking(pi, getCompactThinkingConfig()));
28
29
 
29
30
  // features
30
- context(pi);
31
- sessionReference(pi);
32
- agentAutocomplete(pi);
33
- agentSummary(pi);
31
+ if (config.enableContextCommand) context(pi);
32
+ if (config.enableSessionReference) sessionReference(pi);
33
+ if (config.enableSubagentAutocomplete) agentAutocomplete(pi);
34
+ if (config.enableAgentSummary) agentSummary(pi);
34
35
  }
@@ -33,12 +33,8 @@ import {
33
33
  } from "./tool/diff/diff-renderer.ts";
34
34
  import { renderRichToolResult } from "./tool/diff/index.ts";
35
35
  import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
36
- import {
37
- insetComponent,
38
- oneLine,
39
- renderExpandedToolResult,
40
- scheduleAnimation,
41
- } from "./tool/result.ts";
36
+ import { insetComponent, renderExpandedToolResult, scheduleAnimation } from "./tool/result.ts";
37
+ import { oneLine } from "../utils/format.ts";
42
38
  import { paddedBackgroundRow, stripBackgroundAnsi } from "./tool/grouping.ts";
43
39
 
44
40
  /** compact 渲染层对 compact-thinking 的只读查询面(不建第二套计时器)。 */
@@ -763,13 +759,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
763
759
  dispose: () => {},
764
760
  };
765
761
 
766
- const passThroughAssistant = (component: any, message: any): any => {
762
+ const passThroughAssistant = (component: any, message: any, isStreaming?: boolean): any => {
767
763
  const self = component as any;
768
764
  if (self[ASSISTANT_REENTRY_KEY] === patch)
769
- return patch.assistantNative.call(component, message);
765
+ return patch.assistantNative.call(component, message, isStreaming);
770
766
  self[ASSISTANT_REENTRY_KEY] = patch;
771
767
  try {
772
- return patch.assistantOriginal.call(component, message);
768
+ return patch.assistantOriginal.call(component, message, isStreaming);
773
769
  } finally {
774
770
  delete self[ASSISTANT_REENTRY_KEY];
775
771
  }
@@ -838,12 +834,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
838
834
  if (render) renderRound(round);
839
835
  };
840
836
 
841
- const renderAssistantWithoutThinking = (component: any, message: any): any => {
837
+ const renderAssistantWithoutThinking = (
838
+ component: any,
839
+ message: any,
840
+ isStreaming?: boolean,
841
+ ): any => {
842
842
  const content = Array.isArray(message?.content) ? message.content : [];
843
- const result = passThroughAssistant(component, {
844
- ...message,
845
- content: content.filter((item: any) => item?.type !== "thinking"),
846
- });
843
+ const result = passThroughAssistant(
844
+ component,
845
+ {
846
+ ...message,
847
+ content: content.filter((item: any) => item?.type !== "thinking"),
848
+ },
849
+ isStreaming,
850
+ );
847
851
  component.lastMessage = message;
848
852
  return result;
849
853
  };
@@ -973,18 +977,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
973
977
  stopRoundTick();
974
978
  };
975
979
 
976
- patch.assistantInstalled = function (this: any, message: any) {
980
+ patch.assistantInstalled = function (this: any, message: any, isStreaming?: boolean) {
977
981
  const self = this as any;
982
+ // 同 compact-thinking:isStreaming 丢失 → mermaid 流式误渲染来回闪。
983
+ if (isStreaming !== undefined) self.isStreaming = isStreaming;
978
984
  if (self[ASSISTANT_REENTRY_KEY] === patch) {
979
- return patch.assistantNative.call(this, message);
985
+ return patch.assistantNative.call(this, message, isStreaming);
980
986
  }
981
987
  self.lastMessage = message;
982
988
  trackedAssistantComponents.add(this);
983
989
  if (!patch.active || config.mode !== "compact") {
984
- return passThroughAssistant(this, message);
990
+ return passThroughAssistant(this, message, isStreaming);
985
991
  }
986
992
  if (!self.contentContainer || typeof self.contentContainer.clear !== "function") {
987
- return passThroughAssistant(this, message);
993
+ return passThroughAssistant(this, message, isStreaming);
988
994
  }
989
995
 
990
996
  const content = Array.isArray(message?.content) ? message.content : [];
@@ -7,14 +7,13 @@
7
7
  import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
8
8
  import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
9
  import { config, getToolDisplayConfig, type CompactStyleMode } from "../config/config.ts";
10
- import { isToolCallHovered } from "./mouse/interaction.ts";
10
+ import { isToolCallHovered } from "./mouse/hover.ts";
11
11
  import {
12
12
  countLines,
13
13
  hasExpandableDetail,
14
14
  headTruncateToWidth,
15
15
  insetComponent,
16
16
  isToolExpanded,
17
- oneLine,
18
17
  outputLineCount,
19
18
  pendingIcon,
20
19
  renderCollapsedToolResultToWidth,
@@ -27,6 +26,7 @@ import {
27
26
  toolIconColor,
28
27
  toolViewportWidth,
29
28
  } from "./tool/result.ts";
29
+ import { oneLine } from "../utils/format.ts";
30
30
  import { showMoreHintText } from "./show-more-hint.ts";
31
31
  import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
32
32
  import { getMessageDisplayTheme } from "./message-display.ts";
@@ -272,11 +272,16 @@ function renderExpandedTaskResult(
272
272
  : task.status === "in_progress"
273
273
  ? "warning"
274
274
  : "muted";
275
- return ` ${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`;
275
+ return `${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`;
276
276
  });
277
277
  if (tasks.length > rows.length)
278
- rows.push(theme.fg("muted", ` … ${tasks.length - rows.length} more tasks`));
279
- return new Text(` ↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.join("\n")}`, 0, 0);
278
+ rows.push(theme.fg("muted", `… ${tasks.length - rows.length} more tasks`));
279
+ // 贴左:外层展开卡片 Box(1,1) 提供 1 格 padding
280
+ return new Text(
281
+ `↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.map((r) => ` ${r}`).join("\n")}`,
282
+ 0,
283
+ 0,
284
+ );
280
285
  }
281
286
  const line = text.trim();
282
287
  if (!line || line.includes("\n")) return undefined;
@@ -294,7 +299,7 @@ function renderExpandedTaskResult(
294
299
  } else if (toolName === "TaskStop") {
295
300
  formatted = `${theme.fg("success", "Stopped")} ${theme.fg("muted", line)}`;
296
301
  }
297
- return formatted ? new Text(` ↳ ${formatted}`, 0, 0) : undefined;
302
+ return formatted ? new Text(`↳ ${formatted}`, 0, 0) : undefined;
298
303
  }
299
304
 
300
305
  /** 用 ccstyle call/result 包装任意工具定义。 */
@@ -328,15 +333,21 @@ function createCcstyleTool(
328
333
  const summary = singleToolCallSummary(toolName, label, args);
329
334
  let cachedWidth: number | undefined;
330
335
  let cachedLine: string | undefined;
336
+ const expanded = Boolean(context?.expanded);
331
337
  return {
332
338
  render(width: number) {
333
339
  if (cachedLine !== undefined && cachedWidth === width) return [cachedLine];
334
340
  const viewportWidth = toolViewportWidth(width);
335
- const callWidth = Math.max(0, viewportWidth - visibleWidth(icon) - 2);
341
+ // 展开态贴左(外层 Box 已 pad 1);折叠 self-shell 保留 1 格前导空格
342
+ const lead = expanded ? "" : " ";
343
+ const callWidth = Math.max(
344
+ 0,
345
+ viewportWidth - visibleWidth(icon) - 1 - (expanded ? 0 : 1),
346
+ );
336
347
  const mainWidth = Math.max(0, callWidth - visibleWidth(summary.detail));
337
348
  cachedWidth = width;
338
349
  // 纯文本先截断再着色(省略号不带 ANSI);从头截断,与多 tool 一致
339
- cachedLine = ` ${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${theme.fg("dim", summary.detail)}`;
350
+ cachedLine = `${lead}${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${theme.fg("dim", summary.detail)}`;
340
351
  return [truncateToWidth(cachedLine, viewportWidth, "")];
341
352
  },
342
353
  invalidate() {},
@@ -352,13 +363,15 @@ function createCcstyleTool(
352
363
  );
353
364
  }
354
365
 
366
+ const expanded = isToolExpanded(options, context);
355
367
  if (options?.isPartial) {
356
- return new Text(theme.fg("muted", " ↳ Pending…"), 0, 0);
368
+ // 展开态贴左(Box 提供 1 格 pad);折叠态保持 3 格对齐标题
369
+ const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
370
+ return new Text(theme.fg("muted", pending), 0, 0);
357
371
  }
358
372
 
359
373
  const isError = options?.isError || context?.isError;
360
374
  setToolVisualState(context, isError ? "error" : "success");
361
- const expanded = isToolExpanded(options, context);
362
375
  const toolCallId = context?.toolCallId;
363
376
  if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
364
377
  // getter 保证 Diff indicator / wrap / limits 下次绘制即更新
@@ -376,7 +389,8 @@ function createCcstyleTool(
376
389
  writeExecutionMetadata,
377
390
  getToolDisplayConfig,
378
391
  );
379
- if (richResult) return insetComponent(richResult);
392
+ // 展开态由 Box(1,1) 提供内边距;折叠态 inset 一级缩进
393
+ if (richResult) return expanded ? richResult : insetComponent(richResult);
380
394
  }
381
395
 
382
396
  const text = textFromResult(result, expanded);
@@ -393,7 +407,8 @@ function createCcstyleTool(
393
407
  ? taskListSummary(tasks)
394
408
  : isError
395
409
  ? text
396
- ? oneLine(text)
410
+ ? // 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
411
+ oneLine(text, 72)
397
412
  : "Failed"
398
413
  : outputLines
399
414
  ? `${outputLines} ${lineWord} ${action}`
@@ -411,6 +426,7 @@ function createCcstyleTool(
411
426
  context?.lastComponent,
412
427
  args,
413
428
  context,
429
+ true, // mode=on:贴左,由外层 Box(1,1) 提供 1 格 padding
414
430
  );
415
431
  }
416
432
  if (context?.state) context.state.ccstyleIoView = undefined;
@@ -689,7 +705,12 @@ export function installToolExpandedBackground(): () => void {
689
705
  const theme = getMessageDisplayTheme();
690
706
  if (!theme?.bg) return;
691
707
  const box = this.contentBox;
692
- if (box?.setBgFn) box.setBgFn((text: string) => theme.bg("userMessageBg", text));
708
+ // 展开最外层卡片:上下左右内间距 1 格
709
+ if (box) {
710
+ box.paddingX = 1;
711
+ box.paddingY = 1;
712
+ if (box.setBgFn) box.setBgFn((text: string) => theme.bg("userMessageBg", text));
713
+ }
693
714
  },
694
715
  original,
695
716
  dispose: () => {
@@ -27,12 +27,11 @@ import {
27
27
  installToolMouseInteraction,
28
28
  resetToolHoverState,
29
29
  scheduleSessionRender,
30
- setHoveredToolGroup,
31
- setHoveredToolIo,
32
30
  teardownToolMouseInteraction,
33
31
  TOOL_MOUSE_DISABLE,
34
32
  } from "./mouse/interaction.ts";
35
33
  import { getToolMouseTui } from "./mouse/scroll.ts";
34
+ import { setHoveredToolGroup, setHoveredToolIo } from "./mouse/hover.ts";
36
35
  import { clearAllAnimations } from "./tool/result.ts";
37
36
  import { installWriteOverride, WriteExecutionMetadataStore } from "./tool/diff/index.ts";
38
37
  import {
@@ -55,9 +55,8 @@ const ADMONITION_STYLE: Record<string, { icon: string; label: string }> = {
55
55
  };
56
56
 
57
57
  /**
58
- * 把 > [!TYPE] 及其后续 > 行转成两列表格(图标标签列 + 内容列),视觉上成提示框。
59
- * 表格 cell 内不支持换行(<br> 显示为字面量),多行内容用空格合并。
60
- * 返回 null 表示该行不是提示框。
58
+ * 把 > [!TYPE] 及其后续 > 行转成带加粗标签的引用块,视觉上成提示框。
59
+ * 多行内容用空格合并为一行。返回 null 表示该行不是提示框。
61
60
  */
62
61
  function renderAdmonition(lines: string[], i: number): { output: string[]; next: number } | null {
63
62
  const m = lines[i].match(ADMONITION);
@@ -70,11 +69,11 @@ function renderAdmonition(lines: string[], i: number): { output: string[]; next:
70
69
  j++;
71
70
  }
72
71
  while (body.length > 0 && body[body.length - 1].trim() === "") body.pop();
73
- // 内容里的 | 转义,避免拆出额外表格列
74
- const content = body.join(" ").trim().replace(/\|/g, "\\|");
75
- // 表格后补空行,防止紧接的段落被解析为表格数据行
72
+ // 内容合并为一行,作为引用块正文
73
+ const content = body.join(" ").trim();
74
+ // 保留引用块形态,标签改为加粗图标+类型(pi 渲染为左侧竖线,比表格轻量)
76
75
  return {
77
- output: [`| ${style.icon} ${style.label} | ${content} |`, "|---|---|", ""],
76
+ output: [`> **${style.icon} ${style.label}**${content ? ` ${content}` : ""}`, ""],
78
77
  next: j,
79
78
  };
80
79
  }
@@ -222,7 +221,7 @@ function renderDiagrams(
222
221
  return out.join("\n");
223
222
  }
224
223
 
225
- /** GitHub 风格提示框 → 两列表格(跳过代码块)。 */
224
+ /** GitHub 风格提示框 → 加粗标签引用块(跳过代码块)。 */
226
225
  function renderAdmonitions(markdown: string): string {
227
226
  const lines = markdown.split("\n");
228
227
  const out: string[] = [];
@@ -22,11 +22,13 @@ export function sharedToolHoverState(): SharedToolHoverState {
22
22
  return (host[TOOL_HOVER_STATE_KEY] ??= { toolCallId: null });
23
23
  }
24
24
 
25
+ // 状态单源在 globalThis 槽;hoveredToolCallId 变量仅为兼容旧 deep import 保留
26
+ // (外部扩展可能直接从 interaction.ts re-export 读取),内部读取一律走槽。
25
27
  export let hoveredToolCallId: string | null = sharedToolHoverState().toolCallId;
26
28
 
27
29
  export function setHoveredToolCallId(toolCallId: string | null): void {
28
- hoveredToolCallId = toolCallId;
29
30
  sharedToolHoverState().toolCallId = toolCallId;
31
+ hoveredToolCallId = toolCallId;
30
32
  }
31
33
 
32
34
  export function isToolCallHovered(toolCallId: string | null | undefined): boolean {
@@ -158,7 +158,6 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
158
158
  if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
159
159
  const component = region.component;
160
160
  if (!component) return false;
161
- const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
162
161
  if (region.kind === "expanded-card") {
163
162
  component.setExpanded(false);
164
163
  setHoveredToolCallId(null);
@@ -74,10 +74,10 @@ export function resetScrollButtonState(): void {
74
74
 
75
75
  let scrollButtonSyncScheduled = false;
76
76
 
77
+ // 交互开关只取决于配置模式:原实现按 isLazyProxyTui(toolMouseTui) 分两分支,
78
+ // 两分支恒真(0.84+ 惰性 Proxy 下判定不再影响开关),折叠为单条件。
77
79
  export function toolMouseInteractionActive(): boolean {
78
- if (config.mode === "off") return false;
79
- if (isLazyProxyTui(toolMouseTui)) return true;
80
- return true;
80
+ return config.mode !== "off";
81
81
  }
82
82
 
83
83
  /** 惰性 Proxy 官方 fullscreen(TuiAltScreen)判定。 */
@@ -19,6 +19,7 @@ import {
19
19
  isFiniteSgrParam,
20
20
  expandSgrReset,
21
21
  } from "./ansi-utils.ts";
22
+ import { ansi256ToRgb } from "../../../utils/ansi-color.ts";
22
23
  import {
23
24
  buildCollapsedDiffHintText,
24
25
  clampRenderedLineToWidth,
@@ -1076,46 +1077,10 @@ function buildInlineHighlightMap(rows: SplitDiffRow[]): WeakMap<DiffLineEntry, D
1076
1077
  return highlights;
1077
1078
  }
1078
1079
 
1079
- function ansi256ToRgb(code: number): RgbColor {
1080
- if (code < 0) {
1081
- return { r: 0, g: 0, b: 0 };
1082
- }
1083
- if (code <= 15) {
1084
- const base16: RgbColor[] = [
1085
- { r: 0, g: 0, b: 0 },
1086
- { r: 128, g: 0, b: 0 },
1087
- { r: 0, g: 128, b: 0 },
1088
- { r: 128, g: 128, b: 0 },
1089
- { r: 0, g: 0, b: 128 },
1090
- { r: 128, g: 0, b: 128 },
1091
- { r: 0, g: 128, b: 128 },
1092
- { r: 192, g: 192, b: 192 },
1093
- { r: 128, g: 128, b: 128 },
1094
- { r: 255, g: 0, b: 0 },
1095
- { r: 0, g: 255, b: 0 },
1096
- { r: 255, g: 255, b: 0 },
1097
- { r: 0, g: 0, b: 255 },
1098
- { r: 255, g: 0, b: 255 },
1099
- { r: 0, g: 255, b: 255 },
1100
- { r: 255, g: 255, b: 255 },
1101
- ];
1102
- return base16[code] ?? { r: 255, g: 255, b: 255 };
1103
- }
1104
- if (code >= 232) {
1105
- const value = Math.max(0, Math.min(255, 8 + (code - 232) * 10));
1106
- return { r: value, g: value, b: value };
1107
- }
1108
-
1109
- const cube = code - 16;
1110
- const levels = [0, 95, 135, 175, 215, 255];
1111
- const blue = cube % 6;
1112
- const green = Math.floor(cube / 6) % 6;
1113
- const red = Math.floor(cube / 36) % 6;
1114
- return {
1115
- r: levels[red] ?? 0,
1116
- g: levels[green] ?? 0,
1117
- b: levels[blue] ?? 0,
1118
- };
1080
+ /** utils 版返回 tuple,此处适配为本文件使用的 RgbColor 形状。 */
1081
+ function ansi256ToRgbColor(code: number): RgbColor {
1082
+ const [r, g, b] = ansi256ToRgb(code);
1083
+ return { r, g, b };
1119
1084
  }
1120
1085
 
1121
1086
  function parseAnsiColorCode(ansi: string | undefined): RgbColor | null {
@@ -1140,14 +1105,14 @@ function parseAnsiColorCode(ansi: string | undefined): RgbColor | null {
1140
1105
  if (bitMatch) {
1141
1106
  const code = Number.parseInt(bitMatch[1] ?? "0", 10);
1142
1107
  if (Number.isFinite(code)) {
1143
- return ansi256ToRgb(code);
1108
+ return ansi256ToRgbColor(code);
1144
1109
  }
1145
1110
  }
1146
1111
 
1147
1112
  const basicMatch = /\x1b\[(?:3|4)([0-7])m/.exec(ansi);
1148
- if (basicMatch) return ansi256ToRgb(Number(basicMatch[1]));
1113
+ if (basicMatch) return ansi256ToRgbColor(Number(basicMatch[1]));
1149
1114
  const brightMatch = /\x1b\[(?:9|10)([0-7])m/.exec(ansi);
1150
- if (brightMatch) return ansi256ToRgb(Number(brightMatch[1]) + 8);
1115
+ if (brightMatch) return ansi256ToRgbColor(Number(brightMatch[1]) + 8);
1151
1116
  return null;
1152
1117
  }
1153
1118
 
@@ -7,7 +7,7 @@ import {
7
7
  type Component,
8
8
  } from "@earendil-works/pi-tui";
9
9
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
10
- import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
10
+ import { oneLine } from "../../utils/format.ts";
11
11
  import { showMoreHintText } from "../show-more-hint.ts";
12
12
 
13
13
  const PATCH_KEY = Symbol.for("pi.ccstyle.tool-grouping-patch");
@@ -147,13 +147,6 @@ export function paddedBackgroundRow(
147
147
  return `${bgAnsi}${stable}\x1b[49m`;
148
148
  }
149
149
 
150
- function oneLine(value: unknown, max = 96): string {
151
- const text = sanitizeToolResultText(String(value ?? ""), 4096)
152
- .replace(/\s+/g, " ")
153
- .trim();
154
- return text.length > max ? `${text.slice(0, max - 1)}…` : text;
155
- }
156
-
157
150
  function humanizeToolName(name: string): string {
158
151
  return name
159
152
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
@@ -395,7 +388,8 @@ export class ToolGroupComponent extends Container {
395
388
  const childLines = rendered.length ? rendered : [toolSummary(tool).main];
396
389
  for (let lineIndex = 0; lineIndex < childLines.length; lineIndex++) {
397
390
  const content =
398
- lineIndex === 0 ? childLines[lineIndex] : stripLeadingSpaces(childLines[lineIndex], 2);
391
+ // 续行只剥外层 Box 的 1 格 left pad,保留 Input/Output 相对缩进
392
+ lineIndex === 0 ? childLines[lineIndex] : stripLeadingSpaces(childLines[lineIndex], 1);
399
393
  const prefix =
400
394
  lineIndex === 0
401
395
  ? `${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `
@@ -23,13 +23,6 @@ export function insetComponent(component: any): any {
23
23
  };
24
24
  }
25
25
 
26
- export function oneLine(value: unknown, max = 72): string {
27
- const text = sanitizeToolResultText(String(value ?? ""))
28
- .replace(/\s+/g, " ")
29
- .trim();
30
- return text.length > max ? `${text.slice(0, max - 1)}…` : text;
31
- }
32
-
33
26
  function rawTextFromResult(result: any): string {
34
27
  return Array.isArray(result?.content)
35
28
  ? result.content
@@ -233,6 +226,8 @@ const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view")
233
226
  * └ Output click to show more
234
227
  * result line…
235
228
  *
229
+ * flushLeft=true(仅 mode=on 展开卡):去掉树线前导空格,由外层 Box(1,1) 提供 1 格 padding。
230
+ *
236
231
  * Reused across re-renders via context.lastComponent when possible.
237
232
  */
238
233
  export class ExpandedToolIoView {
@@ -251,6 +246,9 @@ export class ExpandedToolIoView {
251
246
  /** 0-based header line indexes that carry show-more after last render. */
252
247
  private showMoreHeaderRows: { input?: number; output?: number } = {};
253
248
 
249
+ /** flushLeft:贴左渲染(mode=on 展开卡);默认 false 保留前导空格(compact 等共用路径)。 */
250
+ private flushLeft: boolean;
251
+
254
252
  constructor(
255
253
  theme: any,
256
254
  inputBody: string,
@@ -258,6 +256,7 @@ export class ExpandedToolIoView {
258
256
  isError: boolean,
259
257
  maxOutputLines = config.expandedPreviewMaxLines,
260
258
  maxInputLines = config.expandedPreviewMaxLines,
259
+ flushLeft = false,
261
260
  ) {
262
261
  this.theme = theme;
263
262
  this.inputBody = inputBody;
@@ -265,6 +264,7 @@ export class ExpandedToolIoView {
265
264
  this.isError = isError;
266
265
  this.maxOutputLines = Math.max(1, maxOutputLines);
267
266
  this.maxInputLines = Math.max(1, maxInputLines);
267
+ this.flushLeft = flushLeft;
268
268
  }
269
269
 
270
270
  setContent(
@@ -273,16 +273,19 @@ export class ExpandedToolIoView {
273
273
  isError: boolean,
274
274
  maxOutputLines?: number,
275
275
  maxInputLines?: number,
276
+ flushLeft?: boolean,
276
277
  ): void {
277
278
  const nextOut =
278
279
  maxOutputLines !== undefined ? Math.max(1, maxOutputLines) : this.maxOutputLines;
279
280
  const nextIn = maxInputLines !== undefined ? Math.max(1, maxInputLines) : this.maxInputLines;
281
+ const nextFlush = flushLeft !== undefined ? flushLeft : this.flushLeft;
280
282
  if (
281
283
  this.inputBody === inputBody &&
282
284
  this.outputBody === outputBody &&
283
285
  this.isError === isError &&
284
286
  this.maxOutputLines === nextOut &&
285
- this.maxInputLines === nextIn
287
+ this.maxInputLines === nextIn &&
288
+ this.flushLeft === nextFlush
286
289
  ) {
287
290
  return;
288
291
  }
@@ -291,6 +294,7 @@ export class ExpandedToolIoView {
291
294
  this.isError = isError;
292
295
  this.maxOutputLines = nextOut;
293
296
  this.maxInputLines = nextIn;
297
+ this.flushLeft = nextFlush;
294
298
  this.invalidate();
295
299
  }
296
300
 
@@ -348,7 +352,9 @@ export class ExpandedToolIoView {
348
352
 
349
353
  const theme = this.theme;
350
354
  const safeWidth = Math.max(1, Math.floor(width));
351
- const rail = " │ ";
355
+ // flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1 格前导空格(compact 共用)
356
+ const lead = this.flushLeft ? "" : " ";
357
+ const rail = `${lead}│ `;
352
358
  const railWidth = visibleWidth(rail);
353
359
  const bodyWidth = toolViewportWidth(safeWidth);
354
360
  const contentWidth = Math.max(1, bodyWidth - railWidth);
@@ -363,7 +369,7 @@ export class ExpandedToolIoView {
363
369
  section: ToolIoSection,
364
370
  showMore: boolean,
365
371
  ) => {
366
- const mark = theme.fg("dim", ` ${corner} `);
372
+ const mark = theme.fg("dim", `${lead}${corner} `);
367
373
  const title = theme.fg(
368
374
  "accent",
369
375
  typeof theme.bold === "function" ? theme.bold(label) : label,
@@ -378,12 +384,13 @@ export class ExpandedToolIoView {
378
384
  };
379
385
 
380
386
  const pushRailLine = (styledContent: string, continued = true) => {
381
- const prefix = continued ? rail : " ";
387
+ // 续行 rail;Output 正文相对 └ 缩进 2 格(+ 可选 lead)
388
+ const prefix = continued ? rail : `${lead} `;
382
389
  lines.push(truncateToWidth(theme.fg("dim", prefix) + styledContent, safeWidth, ""));
383
390
  };
384
391
 
385
392
  const pushBlankRail = () => {
386
- lines.push(truncateToWidth(theme.fg("dim", " │"), safeWidth, ""));
393
+ lines.push(truncateToWidth(theme.fg("dim", `${lead}│`), safeWidth, ""));
387
394
  };
388
395
 
389
396
  /** Style `key: value` input rows — dim keys, readable values. */
@@ -654,6 +661,8 @@ export function renderExpandedToolResult(
654
661
  lastComponent?: unknown,
655
662
  args?: unknown,
656
663
  context?: any,
664
+ /** mode=on 展开卡贴左;compact 等保持默认前导空格 */
665
+ flushLeft = false,
657
666
  ): ExpandedToolIoView | ExpandedToolResultText | Text {
658
667
  const inputBody = formatToolInputArgs(args);
659
668
  const outputBody = body;
@@ -663,10 +672,18 @@ export function renderExpandedToolResult(
663
672
  if (inputBody.trim() || outputBody.trim()) {
664
673
  let view: ExpandedToolIoView;
665
674
  if (isExpandedToolIoView(lastComponent)) {
666
- lastComponent.setContent(inputBody, outputBody, isError, maxLines, maxLines);
675
+ lastComponent.setContent(inputBody, outputBody, isError, maxLines, maxLines, flushLeft);
667
676
  view = lastComponent;
668
677
  } else {
669
- view = new ExpandedToolIoView(theme, inputBody, outputBody, isError, maxLines, maxLines);
678
+ view = new ExpandedToolIoView(
679
+ theme,
680
+ inputBody,
681
+ outputBody,
682
+ isError,
683
+ maxLines,
684
+ maxLines,
685
+ flushLeft,
686
+ );
670
687
  }
671
688
  if (context) rememberIoView(context, view);
672
689
  return view;
@@ -0,0 +1,44 @@
1
+ /** ANSI 16 色 → RGB(标准 xterm 色序)。 */
2
+ const ANSI_16_RGB: readonly [number, number, number][] = [
3
+ [0, 0, 0],
4
+ [128, 0, 0],
5
+ [0, 128, 0],
6
+ [128, 128, 0],
7
+ [0, 0, 128],
8
+ [128, 0, 128],
9
+ [0, 128, 128],
10
+ [192, 192, 192],
11
+ [128, 128, 128],
12
+ [255, 0, 0],
13
+ [0, 255, 0],
14
+ [255, 255, 0],
15
+ [0, 0, 255],
16
+ [255, 0, 255],
17
+ [0, 255, 255],
18
+ [255, 255, 255],
19
+ ];
20
+
21
+ /** ANSI 16 色码(0-15)→ RGB;越界回退白。 */
22
+ export function ansi16ToRgb(index: number): [number, number, number] {
23
+ return ANSI_16_RGB[index] ?? [255, 255, 255];
24
+ }
25
+
26
+ /** ANSI 256 色码 → RGB(0-15 标准色 + 232-255 灰阶 + 16-231 六阶立方体)。 */
27
+ export function ansi256ToRgb(code: number): [number, number, number] {
28
+ // SGR 正则可解析任意位数字,入口先规范:取整并钳制到合法区间,
29
+ // 否则灰阶分支对 code > 255 会算出超出 0-255 的非法 RGB。
30
+ if (!Number.isFinite(code)) return [0, 0, 0];
31
+ code = Math.min(255, Math.max(0, Math.trunc(code)));
32
+ if (code < 16) return ansi16ToRgb(code);
33
+ if (code >= 232) {
34
+ const gray = 8 + (code - 232) * 10;
35
+ return [gray, gray, gray];
36
+ }
37
+ const cubeIndex = code - 16;
38
+ const levels = [0, 95, 135, 175, 215, 255];
39
+ return [
40
+ levels[Math.floor(cubeIndex / 36)]!,
41
+ levels[Math.floor((cubeIndex % 36) / 6)]!,
42
+ levels[cubeIndex % 6]!,
43
+ ];
44
+ }
@@ -0,0 +1,21 @@
1
+ import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
2
+
3
+ /** 毫秒 → "1h 2m 3s"/"2m 3s"/"3s";低于 1 秒返回 ""(省略)。 */
4
+ export function formatDuration(ms: number): string {
5
+ const totalSec = Math.floor(ms / 1000);
6
+ if (totalSec < 1) return "";
7
+ const hours = Math.floor(totalSec / 3600);
8
+ const minutes = Math.floor((totalSec % 3600) / 60);
9
+ const seconds = totalSec % 60;
10
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
11
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
12
+ return `${seconds}s`;
13
+ }
14
+
15
+ /** 文本单行化:去换行空白,超宽截断加省略号。 */
16
+ export function oneLine(value: unknown, max = 96): string {
17
+ const text = sanitizeToolResultText(String(value ?? ""), 4096)
18
+ .replace(/\s+/g, " ")
19
+ .trim();
20
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.54",
3
+ "version": "0.8.56",
4
4
  "license": "MIT",
5
5
  "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",
@@ -42,7 +42,8 @@
42
42
  "format": "biome format --write .",
43
43
  "format:check": "biome format .",
44
44
  "docs:tool-render": "node scripts/generate-tool-render-examples.mjs",
45
- "preversion": "npm run format && npm run typecheck && npm test && npm run docs:tool-render",
45
+ "lint": "biome check .",
46
+ "preversion": "npm run format && npm run lint && npm run typecheck && npm test && npm run docs:tool-render",
46
47
  "release": "npm version patch && npm publish",
47
48
  "release:minor": "npm version minor && npm publish",
48
49
  "release:major": "npm version major && npm publish"