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 +12 -12
- package/README.md +19 -14
- package/extensions/config/config.ts +24 -0
- package/extensions/config/panel.ts +95 -1
- package/extensions/feature/agent-summary/core.ts +2 -12
- package/extensions/feature/compact-thinking.ts +8 -3
- package/extensions/feature/reference/subagent.ts +0 -1
- package/extensions/feature/shell/startup-header.ts +1 -41
- package/extensions/feature/shell/working-message.ts +5 -11
- package/extensions/index.ts +7 -6
- package/extensions/renderer/compact-mode.ts +24 -18
- package/extensions/renderer/default-mode.ts +34 -13
- package/extensions/renderer/index.ts +1 -2
- package/extensions/renderer/markdown-enhance.ts +7 -8
- package/extensions/renderer/mouse/hover.ts +3 -1
- package/extensions/renderer/mouse/interaction.ts +0 -1
- package/extensions/renderer/mouse/scroll.ts +3 -3
- package/extensions/renderer/tool/diff/diff-renderer.ts +8 -43
- package/extensions/renderer/tool/grouping.ts +3 -9
- package/extensions/renderer/tool/result.ts +31 -14
- package/extensions/utils/ansi-color.ts +44 -0
- package/extensions/utils/format.ts +21 -0
- package/package.json +3 -2
package/README.en.md
CHANGED
|
@@ -38,15 +38,15 @@ Run `/reload` after installation.
|
|
|
38
38
|
|
|
39
39
|
## Features
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
| Fullscreen mode
|
|
46
|
-
| Settings panel
|
|
47
|
-
| Context inspection
|
|
48
|
-
| Session/Subagent references
|
|
49
|
-
| 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
|
-
|
|
|
45
|
-
| Fullscreen mode
|
|
46
|
-
| 配置面板
|
|
47
|
-
| 上下文检查
|
|
48
|
-
| Session/Subagent 引用
|
|
49
|
-
| 主题
|
|
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: [
|
|
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(
|
|
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)
|
|
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
|
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
-
|
|
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 = (
|
|
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(
|
|
844
|
-
|
|
845
|
-
|
|
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/
|
|
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
|
|
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",
|
|
279
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
*
|
|
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()
|
|
75
|
-
//
|
|
72
|
+
// 内容合并为一行,作为引用块正文
|
|
73
|
+
const content = body.join(" ").trim();
|
|
74
|
+
// 保留引用块形态,标签改为加粗图标+类型(pi 渲染为左侧竖线,比表格轻量)
|
|
76
75
|
return {
|
|
77
|
-
output: [
|
|
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
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
|
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
|
|
1113
|
+
if (basicMatch) return ansi256ToRgbColor(Number(basicMatch[1]));
|
|
1149
1114
|
const brightMatch = /\x1b\[(?:9|10)([0-7])m/.exec(ansi);
|
|
1150
|
-
if (brightMatch) return
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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"
|