pi-cc-extensions 0.8.43 → 0.8.45
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 +17 -13
- package/README.md +19 -13
- package/extensions/compact-thinking.ts +650 -130
- package/extensions/fixed-editor.ts +127 -67
- package/package.json +8 -5
- package/themes/cc-dark.json +96 -0
package/README.en.md
CHANGED
|
@@ -42,13 +42,13 @@ Run `/reload` after installation, then try:
|
|
|
42
42
|
## Features
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
| Feature
|
|
46
|
-
|
|
|
47
|
-
| Claude Code-style output | Tool summaries, expand/collapse, rich edit/write diffs, and
|
|
48
|
-
| Fixed-editor interaction | Powered by
|
|
49
|
-
| Context inspection
|
|
50
|
-
| Session references
|
|
51
|
-
| Theme
|
|
45
|
+
| Feature | Description | Entry point |
|
|
46
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------- |
|
|
47
|
+
| Claude Code-style output | Tool summaries, expand/collapse, rich edit/write diffs, and`on` / `off` / `compact` modes | `/ccstyle` |
|
|
48
|
+
| Fixed-editor interaction | Powered by`@tifan/pi-fixed-editor`, with runtime toggling, five-row wheel scrolling, tool clicks, and back-to-bottom control | `/ccstyle` |
|
|
49
|
+
| Context inspection | Usage breakdown and previews for the system prompt, tools, skills, and messages | `/context` |
|
|
50
|
+
| Session references | Search and inject effective context from previous Sessions or existing SubAgents | `@session:` |
|
|
51
|
+
| Theme | Bundled GitHub Dark Default and CC Dark themes | `/theme` |
|
|
52
52
|
|
|
53
53
|
### Output modes
|
|
54
54
|
|
|
@@ -126,12 +126,16 @@ Run `/reload` after changing extensions.
|
|
|
126
126
|
## Recommended companions
|
|
127
127
|
|
|
128
128
|
|
|
129
|
-
| Extension
|
|
130
|
-
|
|
|
131
|
-
| `npm:@tintinweb/pi-subagents`
|
|
132
|
-
| `npm
|
|
133
|
-
| `npm:pi-
|
|
134
|
-
| `npm:@
|
|
129
|
+
| Extension | Purpose |
|
|
130
|
+
| ------------------------------------------ | -------------------------------------------------------------- |
|
|
131
|
+
| `npm:@tintinweb/pi-subagents` | Parallel SubAgents, background tasks, and worktree isolation |
|
|
132
|
+
| `npm:@tintinweb/pi-tasks` | Claude Code-style task tracking and coordination |
|
|
133
|
+
| `npm:pi-mcp-adapter` | On-demand MCP tool discovery with lower context usage |
|
|
134
|
+
| `npm:@ff-labs/pi-fff` | FFF-powered fuzzy file and content search (fffind / ffgrep) |
|
|
135
|
+
| `npm:pi-web-access` | Web search, URL fetching, GitHub cloning, PDF/video parsing |
|
|
136
|
+
| `npm:pi-theme-picker` | Theme search and live preview |
|
|
137
|
+
| `npm:@narumitw/pi-usage` | Current-account usage for Codex / Copilot / OpenRouter |
|
|
138
|
+
| `git:github.com/DietrichGebert/ponytail` | Lazy-mode coding: forces the simplest working solution |
|
|
135
139
|
|
|
136
140
|
## Credits
|
|
137
141
|
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
类 Claude Code
|
|
13
|
+
类 Claude Code TUI输出风格、固定编辑器、上下文检查,以及 Agent / Session 引用。
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
@@ -38,13 +38,14 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
38
38
|
|
|
39
39
|
## 功能
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
|
41
|
+
|
|
42
|
+
| 功能 | 说明 | 入口 |
|
|
43
|
+
| ---------------------- | --------------------------------------------------------------------------------- | ------------- |
|
|
43
44
|
| Claude Code 风格输出 | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `off` / `compact` 三种模式 | `/ccstyle` |
|
|
44
|
-
| Fixed editor 交互 | 基于`@tifan/pi-fixed-editor`,支持动态开关、每刻度 5 行滚动、工具点击与回到底部
|
|
45
|
-
| 上下文检查 | 查看上下文占用,并预览 System prompt、Tools、Skills 和消息内容
|
|
46
|
-
| Session 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文
|
|
47
|
-
| 主题 | 随包提供内置 GitHub Dark Default 主题
|
|
45
|
+
| Fixed editor 交互 | 基于`@tifan/pi-fixed-editor`,支持动态开关、每刻度 5 行滚动、工具点击与回到底部 | `/ccstyle` |
|
|
46
|
+
| 上下文检查 | 查看上下文占用,并预览 System prompt、Tools、Skills 和消息内容 | `/context` |
|
|
47
|
+
| Session 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@session:` |
|
|
48
|
+
| 主题 | 随包提供内置 GitHub Dark Default、CC Dark 主题 | `/theme` |
|
|
48
49
|
|
|
49
50
|
## 本地开发
|
|
50
51
|
|
|
@@ -73,12 +74,17 @@ pi -e .
|
|
|
73
74
|
|
|
74
75
|
## 推荐搭配
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| `npm
|
|
80
|
-
| `npm
|
|
81
|
-
| `npm
|
|
77
|
+
|
|
78
|
+
| 扩展 | 用途 |
|
|
79
|
+
| ------------------------------------------ | -------------------------------------------------- |
|
|
80
|
+
| `npm:@tintinweb/pi-subagents` | 并行 SubAgent、后台任务与工作树隔离 |
|
|
81
|
+
| `npm:@tintinweb/pi-tasks` | Claude Code 风格任务跟踪与协调 |
|
|
82
|
+
| `npm:pi-mcp-adapter` | 按需发现 MCP 工具,减少上下文占用 |
|
|
83
|
+
| `npm:@ff-labs/pi-fff` | 模糊文件与内容检索(fffind / ffgrep) |
|
|
84
|
+
| `npm:pi-web-access` | 网页搜索、URL 抓取、GitHub 克隆、PDF/视频解析 |
|
|
85
|
+
| `npm:pi-theme-picker` | 主题搜索和实时预览 |
|
|
86
|
+
| `npm:@narumitw/pi-usage` | 查看当前账号用量(Codex / Copilot / OpenRouter) |
|
|
87
|
+
| `git:github.com/DietrichGebert/ponytail` | 极简编码:强制最懒但有效的方案 |
|
|
82
88
|
|
|
83
89
|
## 致谢
|
|
84
90
|
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
// compact-thinking 本地内联实现。fork 自 pi-compact-thinking(MIT,
|
|
2
|
+
// https://github.com/tifandotme/pi-extensions/tree/master/packages/pi-compact-thinking)。
|
|
3
|
+
// 差异:
|
|
4
|
+
// 1. 不再依赖上游包 —— 配置解耦,由 claude-code-style 经 installCompactThinking
|
|
5
|
+
// /updateConfig 管控(模块级 config 对象,不再读写 compact-thinking.json)。
|
|
6
|
+
// 2. 合并了上游 fork patch:subagent 工具(Agent/Agents)执行期间保持思考动画,
|
|
7
|
+
// 直到 tool_execution_end 或下一个 text/thinking 边界才收尾。
|
|
8
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
9
|
+
import {
|
|
10
|
+
AssistantMessageComponent,
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
type Theme,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
// @ts-expect-error pi-tui 类型声明的 .ts 后缀 re-export 解析失败;运行时存在
|
|
15
|
+
import { getKeybindings } from "@earendil-works/pi-tui";
|
|
16
|
+
import { Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
17
|
+
|
|
18
|
+
// pi-tui 类型声明中 TUI 的 re-export 解析失败,本地用最小结构化类型(只用到 requestRender)。
|
|
19
|
+
type RenderTui = { requestRender(force?: boolean): void };
|
|
20
|
+
|
|
21
|
+
// pi-coding-agent 的类型声明中 AssistantMessageComponent 仅能以 value 形式使用。
|
|
22
|
+
type AssistantMessageComponentLike = InstanceType<typeof AssistantMessageComponent>;
|
|
10
23
|
|
|
11
24
|
export type CompactThinkingConfig = {
|
|
12
25
|
useSummaryTitlesAsThinkingTitle: boolean;
|
|
@@ -18,120 +31,639 @@ export type CompactThinkingController = {
|
|
|
18
31
|
updateConfig(next: CompactThinkingConfig): void;
|
|
19
32
|
};
|
|
20
33
|
|
|
21
|
-
type
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
type DurationEntryData = {
|
|
35
|
+
messageTimestamp: number;
|
|
36
|
+
contentIndex: number;
|
|
37
|
+
durationMs: number;
|
|
24
38
|
};
|
|
25
39
|
|
|
26
|
-
type
|
|
40
|
+
type SummaryPart = {
|
|
41
|
+
title: string;
|
|
42
|
+
body: string;
|
|
43
|
+
};
|
|
27
44
|
|
|
28
|
-
|
|
45
|
+
type ActiveThinking = {
|
|
46
|
+
messageTimestamp: number;
|
|
47
|
+
contentIndex: number;
|
|
48
|
+
startedAt: number;
|
|
49
|
+
};
|
|
29
50
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}`;
|
|
93
|
-
const messageEndOriginal = ` pi.on("message_end", (event) => {
|
|
94
|
-
if (event.message.role === "assistant") finishThinking();
|
|
95
|
-
});`;
|
|
96
|
-
const messageEndPatched = ` pi.on("message_end", (event) => {
|
|
97
|
-
if (event.message.role !== "assistant") return;
|
|
98
|
-
// Agent tool runs after message_end; keep the ticker until tool_execution_end
|
|
99
|
-
// or the next text/thinking boundary.
|
|
100
|
-
if (messageHasAgentTool(event.message)) return;
|
|
101
|
-
finishThinking();
|
|
102
|
-
});`;
|
|
103
|
-
|
|
104
|
-
let upstreamIndexSource = readFileSync(upstreamIndexPath, "utf8");
|
|
105
|
-
if (
|
|
106
|
-
upstreamIndexSource.includes(toolStartOriginal) &&
|
|
107
|
-
upstreamIndexSource.includes(updateBoundaryOriginal) &&
|
|
108
|
-
upstreamIndexSource.includes(messageEndOriginal)
|
|
109
|
-
) {
|
|
110
|
-
upstreamIndexSource = upstreamIndexSource
|
|
111
|
-
.replace(toolStartOriginal, toolStartPatched)
|
|
112
|
-
.replace(updateBoundaryOriginal, updateBoundaryPatched)
|
|
113
|
-
.replace(messageEndOriginal, messageEndPatched);
|
|
51
|
+
type AssistantInternals = {
|
|
52
|
+
contentContainer: {
|
|
53
|
+
clear(): void;
|
|
54
|
+
addChild(component: Component): void;
|
|
55
|
+
};
|
|
56
|
+
hideThinkingBlock: boolean;
|
|
57
|
+
markdownTheme: ConstructorParameters<typeof Markdown>[3];
|
|
58
|
+
hiddenThinkingLabel: string;
|
|
59
|
+
outputPad: number;
|
|
60
|
+
lastMessage?: AssistantMessage;
|
|
61
|
+
hasToolCalls: boolean;
|
|
62
|
+
updateContent(message: AssistantMessage): void;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type PatchedPrototype = typeof AssistantMessageComponent.prototype & {
|
|
66
|
+
updateContent: (message: AssistantMessage) => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// 配置由 claude-code-style 管控(installCompactThinking 注入初始值,
|
|
70
|
+
// updateConfig 运行时更新),上游的配置文件读写已移除。
|
|
71
|
+
const config: CompactThinkingConfig = {
|
|
72
|
+
useSummaryTitlesAsThinkingTitle: true,
|
|
73
|
+
previewLines: 3,
|
|
74
|
+
animationIntervalMs: 90,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const DURATION_ENTRY_TYPE = "compact-thinking-duration";
|
|
78
|
+
|
|
79
|
+
function restoreDurationEntries(
|
|
80
|
+
entries: Array<{ type: string; customType?: string; data?: unknown }>,
|
|
81
|
+
completedDurations: Map<number, Map<number, number>>,
|
|
82
|
+
) {
|
|
83
|
+
completedDurations.clear();
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (
|
|
86
|
+
entry.type !== "custom" ||
|
|
87
|
+
entry.customType !== DURATION_ENTRY_TYPE ||
|
|
88
|
+
!entry.data ||
|
|
89
|
+
typeof entry.data !== "object"
|
|
90
|
+
) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const data = entry.data as Partial<DurationEntryData>;
|
|
95
|
+
if (
|
|
96
|
+
typeof data.messageTimestamp !== "number" ||
|
|
97
|
+
!Number.isFinite(data.messageTimestamp) ||
|
|
98
|
+
typeof data.contentIndex !== "number" ||
|
|
99
|
+
!Number.isInteger(data.contentIndex) ||
|
|
100
|
+
typeof data.durationMs !== "number" ||
|
|
101
|
+
!Number.isFinite(data.durationMs) ||
|
|
102
|
+
data.durationMs < 1
|
|
103
|
+
) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let durations = completedDurations.get(data.messageTimestamp);
|
|
108
|
+
if (!durations) {
|
|
109
|
+
durations = new Map();
|
|
110
|
+
completedDurations.set(data.messageTimestamp, durations);
|
|
111
|
+
}
|
|
112
|
+
durations.set(data.contentIndex, data.durationMs);
|
|
114
113
|
}
|
|
114
|
+
}
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
function formatThoughtDuration(durationMs: number) {
|
|
117
|
+
if (durationMs < 1_000) {
|
|
118
|
+
return `${Math.max(1, Math.round(durationMs))}ms`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const totalSeconds = Math.max(1, Math.round(durationMs / 1_000));
|
|
122
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
123
|
+
const seconds = totalSeconds % 60;
|
|
124
|
+
if (minutes === 0) return `${seconds}s`;
|
|
125
|
+
return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getThinkingToggleHint() {
|
|
129
|
+
const keys = getKeybindings().getKeys("app.thinking.toggle");
|
|
130
|
+
return keys.length > 0 ? `${keys.join("/")} to expand` : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class StrictThinkingPreview implements Component {
|
|
134
|
+
private text: string;
|
|
135
|
+
private padding: number;
|
|
136
|
+
private style: (text: string) => string;
|
|
137
|
+
|
|
138
|
+
constructor(text: string, padding: number, style: (text: string) => string) {
|
|
139
|
+
this.text = text;
|
|
140
|
+
this.padding = padding;
|
|
141
|
+
this.style = style;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
render(width: number) {
|
|
145
|
+
const lines = new Text(this.style(this.text), this.padding, 0).render(width);
|
|
146
|
+
if (lines.length <= config.previewLines) return lines;
|
|
147
|
+
|
|
148
|
+
const hiddenLines = lines.length - config.previewLines;
|
|
149
|
+
const noun = hiddenLines === 1 ? "line" : "lines";
|
|
150
|
+
const toggleHint = getThinkingToggleHint();
|
|
151
|
+
const hint = `... (${hiddenLines} more ${noun}${toggleHint ? `, ${toggleHint}` : ""})`;
|
|
152
|
+
const hintLines = new Text(this.style(hint), this.padding, 0).render(width);
|
|
153
|
+
return [...hintLines, ...lines.slice(-config.previewLines)];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
invalidate() {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseSummaryPart(text: string): SummaryPart | undefined {
|
|
160
|
+
const match = /^\s*\*\*([^\n]+?)\*\*[ \t]*(?:\r?\n(?:\r?\n)?([\s\S]*))?\s*$/.exec(text);
|
|
161
|
+
if (!match) return undefined;
|
|
162
|
+
return { title: match[1].trim(), body: (match[2] ?? "").trim() };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseLatestStreamingSummary(text: string): SummaryPart | undefined {
|
|
166
|
+
// Providers do not consistently insert a blank line between streamed
|
|
167
|
+
// summary parts, so accept a bold title at the start of any source line.
|
|
168
|
+
const titlePattern = /(?:^|\r?\n)\s*\*\*([^\n*]+?)\*\*[ \t]*(?:\r?\n)?/g;
|
|
169
|
+
let latest: RegExpExecArray | undefined;
|
|
170
|
+
let match: RegExpExecArray | null;
|
|
171
|
+
while ((match = titlePattern.exec(text))) latest = match;
|
|
172
|
+
if (!latest) return parseSummaryPart(text);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
title: latest[1].trim(),
|
|
176
|
+
body: text.slice(latest.index + latest[0].length).trim(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isOpenAiResponsesMessage(message: AssistantMessage) {
|
|
181
|
+
return (
|
|
182
|
+
message.api === "openai-responses" ||
|
|
183
|
+
message.api === "openai-codex-responses" ||
|
|
184
|
+
message.api === "azure-openai-responses"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getLatestOpenAiSummary(thinkingSignature: string | undefined): SummaryPart | undefined {
|
|
189
|
+
if (!thinkingSignature) return undefined;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const item = JSON.parse(thinkingSignature) as {
|
|
193
|
+
type?: unknown;
|
|
194
|
+
summary?: Array<{ type?: unknown; text?: unknown }>;
|
|
195
|
+
};
|
|
196
|
+
if (item.type !== "reasoning" || !Array.isArray(item.summary)) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (let i = item.summary.length - 1; i >= 0; i--) {
|
|
201
|
+
const part = item.summary[i];
|
|
202
|
+
if (part.type !== "summary_text" || typeof part.text !== "string") {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const parsed = parseSummaryPart(part.text);
|
|
206
|
+
if (parsed) return parsed;
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
// Invalid or provider-specific signatures use the generic fallback.
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const WIDGET_ID = "compact-thinking-render-loop";
|
|
215
|
+
|
|
216
|
+
// 上游 index.ts 内联(含 subagent fork patch)。
|
|
217
|
+
function compactThinking(pi: ExtensionAPI) {
|
|
218
|
+
const prototype = AssistantMessageComponent.prototype as PatchedPrototype;
|
|
219
|
+
const originalUpdateContent = prototype.updateContent;
|
|
220
|
+
|
|
221
|
+
const completedDurations = new Map<number, Map<number, number>>();
|
|
222
|
+
const renderedComponents = new Set<AssistantMessageComponentLike>();
|
|
223
|
+
const streamingComponents = new Set<AssistantMessageComponentLike>();
|
|
224
|
+
let activeThinking: ActiveThinking | undefined;
|
|
225
|
+
let activeTheme: Theme | undefined;
|
|
226
|
+
let activeTui: RenderTui | undefined;
|
|
227
|
+
let latestComponent: AssistantMessageComponentLike | undefined;
|
|
228
|
+
let latestComponentTimestamp: number | undefined;
|
|
229
|
+
let animationTimer: ReturnType<typeof setInterval> | undefined;
|
|
230
|
+
let animationFrame = 0;
|
|
231
|
+
let patchInstalled = true;
|
|
232
|
+
|
|
233
|
+
function refreshRenderedComponents() {
|
|
234
|
+
for (const component of renderedComponents) {
|
|
235
|
+
const self = component as unknown as AssistantInternals;
|
|
236
|
+
if (self.lastMessage) self.updateContent(self.lastMessage);
|
|
237
|
+
}
|
|
238
|
+
activeTui?.requestRender(true);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function thinkingStyle(text: string) {
|
|
242
|
+
return activeTheme ? activeTheme.italic(activeTheme.fg("thinkingText", text)) : text;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function summaryTitleStyle(text: string) {
|
|
246
|
+
return activeTheme
|
|
247
|
+
? activeTheme.italic(activeTheme.bold(activeTheme.fg("thinkingText", text)))
|
|
248
|
+
: text;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function animatedText(text: string, baseStyle: (value: string) => string, animate: boolean) {
|
|
252
|
+
if (!animate || !activeTheme) return baseStyle(text);
|
|
253
|
+
|
|
254
|
+
const characters = Array.from(text);
|
|
255
|
+
if (characters.length === 0) return "";
|
|
256
|
+
const highlightWidth = Math.max(1, Math.min(5, Math.ceil(characters.length * 0.28)));
|
|
257
|
+
const start = (animationFrame % (characters.length + highlightWidth)) - highlightWidth;
|
|
258
|
+
const end = start + highlightWidth;
|
|
259
|
+
const before = characters.slice(0, Math.max(0, start)).join("");
|
|
260
|
+
const highlighted = characters
|
|
261
|
+
.slice(Math.max(0, start), Math.min(characters.length, end))
|
|
262
|
+
.join("");
|
|
263
|
+
const after = characters.slice(Math.max(0, end)).join("");
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
baseStyle(before) +
|
|
267
|
+
(highlighted
|
|
268
|
+
? activeTheme.italic(activeTheme.bold(activeTheme.fg("text", highlighted)))
|
|
269
|
+
: "") +
|
|
270
|
+
baseStyle(after)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getCompletedDuration(messageTimestamp: number, startIndex: number, endIndex: number) {
|
|
275
|
+
const durations = completedDurations.get(messageTimestamp);
|
|
276
|
+
if (!durations) return undefined;
|
|
277
|
+
for (let index = endIndex; index >= startIndex; index--) {
|
|
278
|
+
const duration = durations.get(index);
|
|
279
|
+
if (duration !== undefined) return duration;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isActiveRun(message: AssistantMessage, startIndex: number, endIndex: number) {
|
|
285
|
+
return (
|
|
286
|
+
activeThinking?.messageTimestamp === message.timestamp &&
|
|
287
|
+
activeThinking.contentIndex >= startIndex &&
|
|
288
|
+
activeThinking.contentIndex <= endIndex
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
prototype.updateContent = function patchedUpdateContent(message: AssistantMessage) {
|
|
293
|
+
const component = this as AssistantMessageComponentLike;
|
|
294
|
+
const self = this as unknown as AssistantInternals;
|
|
295
|
+
self.lastMessage = message;
|
|
296
|
+
renderedComponents.add(component);
|
|
297
|
+
latestComponent = component;
|
|
298
|
+
latestComponentTimestamp = message.timestamp;
|
|
299
|
+
|
|
300
|
+
// Visible mode is intentionally untouched: Shift+Tab restores Pi's exact
|
|
301
|
+
// built-in Thinking Markdown renderer, including every OpenAI summary stage.
|
|
302
|
+
if (!self.hideThinkingBlock) {
|
|
303
|
+
originalUpdateContent.call(this, message);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (activeThinking?.messageTimestamp === message.timestamp) {
|
|
308
|
+
streamingComponents.add(component);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
self.contentContainer.clear();
|
|
312
|
+
const hasActiveThinking =
|
|
313
|
+
activeThinking?.messageTimestamp === message.timestamp &&
|
|
314
|
+
message.content.some((content) => content.type === "thinking");
|
|
315
|
+
const hasVisibleContent =
|
|
316
|
+
hasActiveThinking ||
|
|
317
|
+
message.content.some(
|
|
318
|
+
(content) =>
|
|
319
|
+
(content.type === "text" && content.text.trim()) ||
|
|
320
|
+
(content.type === "thinking" && content.thinking.trim()),
|
|
321
|
+
);
|
|
322
|
+
// Reserve Pi's normal leading spacer even before the first thinking token.
|
|
323
|
+
// This prevents the placeholder heading from jumping down one row when
|
|
324
|
+
// summary/body content begins streaming.
|
|
325
|
+
if (hasVisibleContent) self.contentContainer.addChild(new Spacer(1));
|
|
326
|
+
|
|
327
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
328
|
+
const content = message.content[i];
|
|
329
|
+
|
|
330
|
+
if (content.type === "text" && content.text.trim()) {
|
|
331
|
+
self.contentContainer.addChild(
|
|
332
|
+
new Markdown(content.text.trim(), self.outputPad, 0, self.markdownTheme),
|
|
333
|
+
);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (content.type !== "thinking") continue;
|
|
338
|
+
|
|
339
|
+
const runStartIndex = i;
|
|
340
|
+
const hasVisibleContentBefore = message.content
|
|
341
|
+
.slice(0, runStartIndex)
|
|
342
|
+
.some(
|
|
343
|
+
(previous) =>
|
|
344
|
+
(previous.type === "text" && previous.text.trim()) ||
|
|
345
|
+
(previous.type === "thinking" && previous.thinking.trim()),
|
|
346
|
+
);
|
|
347
|
+
const thinkingBlocks: string[] = [];
|
|
348
|
+
let latestSummary: SummaryPart | undefined;
|
|
349
|
+
for (; i < message.content.length; i++) {
|
|
350
|
+
const thinkingContent = message.content[i];
|
|
351
|
+
if (thinkingContent.type !== "thinking") break;
|
|
352
|
+
const thinking = thinkingContent.thinking.trim();
|
|
353
|
+
if (!thinking) continue;
|
|
354
|
+
thinkingBlocks.push(thinking);
|
|
355
|
+
|
|
356
|
+
if (config.useSummaryTitlesAsThinkingTitle && isOpenAiResponsesMessage(message)) {
|
|
357
|
+
const contentIsActive =
|
|
358
|
+
activeThinking?.messageTimestamp === message.timestamp &&
|
|
359
|
+
activeThinking.contentIndex === i;
|
|
360
|
+
// During streaming, thinkingSignature may contain only the previously
|
|
361
|
+
// completed summary parts. Prefer the live text so a newly arriving
|
|
362
|
+
// title immediately replaces the old one instead of appearing in its
|
|
363
|
+
// preview body. Once complete, the structured signature is canonical.
|
|
364
|
+
latestSummary = contentIsActive
|
|
365
|
+
? (parseLatestStreamingSummary(thinking) ??
|
|
366
|
+
getLatestOpenAiSummary(thinkingContent.thinkingSignature) ??
|
|
367
|
+
latestSummary)
|
|
368
|
+
: (getLatestOpenAiSummary(thinkingContent.thinkingSignature) ??
|
|
369
|
+
parseLatestStreamingSummary(thinking) ??
|
|
370
|
+
latestSummary);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const runEndIndex = i - 1;
|
|
374
|
+
i--;
|
|
375
|
+
const active = isActiveRun(message, runStartIndex, runEndIndex);
|
|
376
|
+
// OpenAI can spend several seconds reasoning before it emits the first
|
|
377
|
+
// summary token. Keep an empty active block visible as animated
|
|
378
|
+
// "Thinking..." during that otherwise silent interval.
|
|
379
|
+
if (thinkingBlocks.length === 0 && !active) continue;
|
|
380
|
+
if (hasVisibleContentBefore) {
|
|
381
|
+
self.contentContainer.addChild(new Spacer(1));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const elapsedMs = activeThinking
|
|
385
|
+
? Math.max(1, Date.now() - activeThinking.startedAt)
|
|
386
|
+
: undefined;
|
|
387
|
+
const completedMs = getCompletedDuration(message.timestamp, runStartIndex, runEndIndex);
|
|
388
|
+
const durationMs = active ? elapsedMs : completedMs;
|
|
389
|
+
const durationText = durationMs === undefined ? undefined : formatThoughtDuration(durationMs);
|
|
390
|
+
|
|
391
|
+
let heading: string;
|
|
392
|
+
if (active && latestSummary) {
|
|
393
|
+
heading =
|
|
394
|
+
animatedText(latestSummary.title, summaryTitleStyle, true) +
|
|
395
|
+
(durationText ? thinkingStyle(` · ${durationText}`) : "");
|
|
396
|
+
} else if (active) {
|
|
397
|
+
const label = self.hiddenThinkingLabel || "Thinking...";
|
|
398
|
+
heading =
|
|
399
|
+
animatedText(label, thinkingStyle, true) +
|
|
400
|
+
(durationText ? thinkingStyle(` · ${durationText}`) : "");
|
|
401
|
+
} else {
|
|
402
|
+
// Completed compact blocks use one provider-independent status line.
|
|
403
|
+
heading = thinkingStyle(
|
|
404
|
+
durationText ? `Thought for ${durationText}` : self.hiddenThinkingLabel || "Thinking...",
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
self.contentContainer.addChild(new Text(heading, self.outputPad, 0));
|
|
408
|
+
|
|
409
|
+
const previewSource = latestSummary?.body ?? thinkingBlocks.join("\n\n");
|
|
410
|
+
if (config.previewLines > 0 && previewSource.trim()) {
|
|
411
|
+
self.contentContainer.addChild(
|
|
412
|
+
new StrictThinkingPreview(previewSource.trim(), self.outputPad, thinkingStyle),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const hasVisibleContentAfter = message.content
|
|
417
|
+
.slice(i + 1)
|
|
418
|
+
.some(
|
|
419
|
+
(next) =>
|
|
420
|
+
(next.type === "text" && next.text.trim()) ||
|
|
421
|
+
(next.type === "thinking" && next.thinking.trim()),
|
|
422
|
+
);
|
|
423
|
+
if (hasVisibleContentAfter) self.contentContainer.addChild(new Spacer(1));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const hasToolCalls = message.content.some((content) => content.type === "toolCall");
|
|
427
|
+
self.hasToolCalls = hasToolCalls;
|
|
428
|
+
|
|
429
|
+
if (message.stopReason === "length") {
|
|
430
|
+
self.contentContainer.addChild(new Spacer(1));
|
|
431
|
+
self.contentContainer.addChild(
|
|
432
|
+
new Text(
|
|
433
|
+
activeTheme
|
|
434
|
+
? activeTheme.fg(
|
|
435
|
+
"error",
|
|
436
|
+
"Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.",
|
|
437
|
+
)
|
|
438
|
+
: "Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.",
|
|
439
|
+
self.outputPad,
|
|
440
|
+
0,
|
|
441
|
+
),
|
|
442
|
+
);
|
|
443
|
+
} else if (!hasToolCalls) {
|
|
444
|
+
if (message.stopReason === "aborted") {
|
|
445
|
+
const abortMessage =
|
|
446
|
+
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
447
|
+
? message.errorMessage
|
|
448
|
+
: "Operation aborted";
|
|
449
|
+
self.contentContainer.addChild(new Spacer(1));
|
|
450
|
+
self.contentContainer.addChild(
|
|
451
|
+
new Text(
|
|
452
|
+
activeTheme ? activeTheme.fg("error", abortMessage) : abortMessage,
|
|
453
|
+
self.outputPad,
|
|
454
|
+
0,
|
|
455
|
+
),
|
|
456
|
+
);
|
|
457
|
+
} else if (message.stopReason === "error") {
|
|
458
|
+
const errorMessage = message.errorMessage || "Unknown error";
|
|
459
|
+
self.contentContainer.addChild(new Spacer(1));
|
|
460
|
+
self.contentContainer.addChild(
|
|
461
|
+
new Text(
|
|
462
|
+
activeTheme
|
|
463
|
+
? activeTheme.fg("error", `Error: ${errorMessage}`)
|
|
464
|
+
: `Error: ${errorMessage}`,
|
|
465
|
+
self.outputPad,
|
|
466
|
+
0,
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
function startThinking(message: AssistantMessage, contentIndex: number) {
|
|
474
|
+
activeThinking = {
|
|
475
|
+
messageTimestamp: message.timestamp,
|
|
476
|
+
contentIndex,
|
|
477
|
+
startedAt: Date.now(),
|
|
478
|
+
};
|
|
479
|
+
streamingComponents.clear();
|
|
480
|
+
animationFrame = 0;
|
|
481
|
+
|
|
482
|
+
// Depending on event-listener order, Pi may have rendered the empty
|
|
483
|
+
// thinking_start partial just before this extension receives the event.
|
|
484
|
+
// Re-render that component immediately so users do not wait for the first
|
|
485
|
+
// summary delta before seeing the animation.
|
|
486
|
+
if (latestComponent && latestComponentTimestamp === message.timestamp) {
|
|
487
|
+
streamingComponents.add(latestComponent);
|
|
488
|
+
const self = latestComponent as unknown as AssistantInternals;
|
|
489
|
+
self.updateContent(message);
|
|
490
|
+
activeTui?.requestRender();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (animationTimer) clearInterval(animationTimer);
|
|
494
|
+
animationTimer = setInterval(() => {
|
|
495
|
+
animationFrame++;
|
|
496
|
+
for (const component of streamingComponents) {
|
|
497
|
+
const self = component as unknown as AssistantInternals;
|
|
498
|
+
if (self.lastMessage) self.updateContent(self.lastMessage);
|
|
499
|
+
}
|
|
500
|
+
activeTui?.requestRender();
|
|
501
|
+
}, config.animationIntervalMs);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function finishThinking() {
|
|
505
|
+
if (!activeThinking) return;
|
|
506
|
+
const finished = activeThinking;
|
|
507
|
+
const durationMs = Math.max(1, Date.now() - finished.startedAt);
|
|
508
|
+
let durations = completedDurations.get(finished.messageTimestamp);
|
|
509
|
+
if (!durations) {
|
|
510
|
+
durations = new Map();
|
|
511
|
+
completedDurations.set(finished.messageTimestamp, durations);
|
|
512
|
+
}
|
|
513
|
+
durations.set(finished.contentIndex, durationMs);
|
|
514
|
+
pi.appendEntry(DURATION_ENTRY_TYPE, {
|
|
515
|
+
messageTimestamp: finished.messageTimestamp,
|
|
516
|
+
contentIndex: finished.contentIndex,
|
|
517
|
+
durationMs,
|
|
518
|
+
} as DurationEntryData);
|
|
519
|
+
|
|
520
|
+
activeThinking = undefined;
|
|
521
|
+
if (animationTimer) clearInterval(animationTimer);
|
|
522
|
+
animationTimer = undefined;
|
|
523
|
+
|
|
524
|
+
const components = [...streamingComponents];
|
|
525
|
+
streamingComponents.clear();
|
|
526
|
+
for (const component of components) {
|
|
527
|
+
const self = component as unknown as AssistantInternals;
|
|
528
|
+
if (self.lastMessage) self.updateContent(self.lastMessage);
|
|
529
|
+
}
|
|
530
|
+
activeTui?.requestRender();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ---- fork patch:subagent 工具执行期间保持思考动画 ----
|
|
534
|
+
|
|
535
|
+
// Subagent tools can run for minutes: keep the thinking loading animation
|
|
536
|
+
// alive for the whole execution and only finalize once the tool ends or the
|
|
537
|
+
// model emits the next text/thinking boundary.
|
|
538
|
+
function resumeAgentThinking(message: AssistantMessage | undefined) {
|
|
539
|
+
if (activeThinking) return;
|
|
540
|
+
const content = message?.content;
|
|
541
|
+
if (!Array.isArray(content)) return;
|
|
542
|
+
const index = content.findIndex((item) => item?.type === "thinking");
|
|
543
|
+
if (index < 0) return;
|
|
544
|
+
startThinking(message as AssistantMessage, index);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function messageHasAgentTool(message: AssistantMessage | undefined) {
|
|
548
|
+
return (
|
|
549
|
+
Array.isArray(message?.content) &&
|
|
550
|
+
message.content.some(
|
|
551
|
+
(content) =>
|
|
552
|
+
content?.type === "toolCall" &&
|
|
553
|
+
(content.name === "Agent" ||
|
|
554
|
+
content.name === "Agents" ||
|
|
555
|
+
content.args?.subagent_type != null),
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
pi.on("message_update", (event) => {
|
|
561
|
+
if (event.message.role !== "assistant") return;
|
|
562
|
+
const update = event.assistantMessageEvent;
|
|
563
|
+
|
|
564
|
+
if (update.type === "thinking_start") {
|
|
565
|
+
if (activeThinking?.messageTimestamp === event.message.timestamp) {
|
|
566
|
+
// Some Responses-compatible providers emit a fresh thinking_start for
|
|
567
|
+
// each summary/reasoning item even though no text or tool boundary has
|
|
568
|
+
// ended the visible Thinking run. Follow the new content block without
|
|
569
|
+
// resetting the run's original start time.
|
|
570
|
+
activeThinking.contentIndex = update.contentIndex;
|
|
571
|
+
} else {
|
|
572
|
+
if (activeThinking) finishThinking();
|
|
573
|
+
startThinking(event.message, update.contentIndex);
|
|
574
|
+
}
|
|
575
|
+
} else if (update.type === "thinking_delta") {
|
|
576
|
+
if (!activeThinking) {
|
|
577
|
+
startThinking(event.message, update.contentIndex);
|
|
578
|
+
} else if (activeThinking.messageTimestamp === event.message.timestamp) {
|
|
579
|
+
activeThinking.contentIndex = update.contentIndex;
|
|
580
|
+
}
|
|
581
|
+
} else if (update.type === "text_start") {
|
|
582
|
+
finishThinking();
|
|
583
|
+
} else if (update.type === "toolcall_start" || update.type === "toolcall_delta") {
|
|
584
|
+
if (messageHasAgentTool(event.message)) {
|
|
585
|
+
resumeAgentThinking(event.message);
|
|
586
|
+
} else {
|
|
587
|
+
finishThinking();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Do not finalize on thinking_end alone. OpenAI Responses providers can
|
|
591
|
+
// close one reasoning item and immediately open another for the next
|
|
592
|
+
// summary while Pi still renders both as one contiguous Thinking run.
|
|
593
|
+
// A text/tool transition or message_end is the actual visible boundary.
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// OpenAI-compatible providers may not close reasoning until the response ends.
|
|
597
|
+
pi.on("tool_execution_start", (event: any) => {
|
|
598
|
+
if (event.toolName === "Agent" || event.toolName === "Agents") {
|
|
599
|
+
resumeAgentThinking(latestComponent?.lastMessage ?? undefined);
|
|
600
|
+
} else {
|
|
601
|
+
finishThinking();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
pi.on("tool_execution_end", (event: any) => {
|
|
605
|
+
if (event.toolName === "Agent" || event.toolName === "Agents") finishThinking();
|
|
606
|
+
});
|
|
607
|
+
pi.on("message_end", (event) => {
|
|
608
|
+
if (event.message.role !== "assistant") return;
|
|
609
|
+
// Agent tool runs after message_end; keep the ticker until tool_execution_end
|
|
610
|
+
// or the next text/thinking boundary.
|
|
611
|
+
if (messageHasAgentTool(event.message)) return;
|
|
612
|
+
finishThinking();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
pi.on("session_start", (_event, ctx) => {
|
|
616
|
+
restoreDurationEntries(ctx.sessionManager.getBranch(), completedDurations);
|
|
617
|
+
activeTheme = ctx.ui.theme;
|
|
618
|
+
if (ctx.mode !== "tui") return;
|
|
619
|
+
|
|
620
|
+
// An empty widget gives the animation loop access to requestRender without
|
|
621
|
+
// enabling terminal mouse reporting or intercepting native scrollback input.
|
|
622
|
+
ctx.ui.setWidget(WIDGET_ID, (tui) => {
|
|
623
|
+
activeTui = tui;
|
|
624
|
+
return { render: () => [], invalidate() {} };
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// On resume, Pi may construct the chat before session_start is emitted.
|
|
628
|
+
// Rebuild those already-rendered components now that persisted durations
|
|
629
|
+
// and the active theme are available.
|
|
630
|
+
refreshRenderedComponents();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
634
|
+
restoreDurationEntries(ctx.sessionManager.getBranch(), completedDurations);
|
|
635
|
+
refreshRenderedComponents();
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
639
|
+
finishThinking();
|
|
640
|
+
if (animationTimer) clearInterval(animationTimer);
|
|
641
|
+
animationTimer = undefined;
|
|
642
|
+
activeTui = undefined;
|
|
643
|
+
activeTheme = undefined;
|
|
644
|
+
latestComponent = undefined;
|
|
645
|
+
latestComponentTimestamp = undefined;
|
|
646
|
+
completedDurations.clear();
|
|
647
|
+
renderedComponents.clear();
|
|
648
|
+
streamingComponents.clear();
|
|
649
|
+
if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
650
|
+
|
|
651
|
+
if (patchInstalled) {
|
|
652
|
+
prototype.updateContent = originalUpdateContent;
|
|
653
|
+
patchInstalled = false;
|
|
654
|
+
}
|
|
123
655
|
});
|
|
124
|
-
const upstreamConfig = (
|
|
125
|
-
jiti("pi-compact-thinking/lib/config.ts") as { config: CompactThinkingConfig }
|
|
126
|
-
).config;
|
|
127
|
-
const compactThinking = (
|
|
128
|
-
jiti.evalModule(upstreamIndexSource, { filename: upstreamIndexPath }) as {
|
|
129
|
-
default: (api: ExtensionAPI) => void;
|
|
130
|
-
}
|
|
131
|
-
).default;
|
|
132
|
-
return { config: upstreamConfig, compactThinking };
|
|
133
656
|
}
|
|
134
657
|
|
|
658
|
+
type CompactThinkingOwner = {
|
|
659
|
+
owner: object;
|
|
660
|
+
stop(event?: any, ctx?: any): void;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const COMPACT_THINKING_OWNER = Symbol.for("pi.ccstyle.compact-thinking-owner");
|
|
664
|
+
|
|
665
|
+
type UpstreamHandler = (event: any, ctx: any) => void;
|
|
666
|
+
|
|
135
667
|
export function installCompactThinking(
|
|
136
668
|
pi: ExtensionAPI,
|
|
137
669
|
initialConfig: CompactThinkingConfig,
|
|
@@ -141,7 +673,6 @@ export function installCompactThinking(
|
|
|
141
673
|
[COMPACT_THINKING_OWNER]?: CompactThinkingOwner;
|
|
142
674
|
};
|
|
143
675
|
|
|
144
|
-
let upstreamConfig: CompactThinkingConfig | undefined;
|
|
145
676
|
let session: { event: any; ctx: any } | undefined;
|
|
146
677
|
let active = false;
|
|
147
678
|
// Stable pi.on wrappers delegate here so activate/reload never double-binds.
|
|
@@ -188,22 +719,11 @@ export function installCompactThinking(
|
|
|
188
719
|
host[COMPACT_THINKING_OWNER]?.stop(event, ctx);
|
|
189
720
|
session = { event, ctx };
|
|
190
721
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
// 库首次加载会在 .pi/agent 下自建默认文件,加载完成后立即删除,不留残留。
|
|
194
|
-
const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
195
|
-
const configPath = join(agentDir, "compact-thinking.json");
|
|
196
|
-
let loaded: ReturnType<typeof loadPatchedUpstream>;
|
|
197
|
-
try {
|
|
198
|
-
loaded = loadPatchedUpstream();
|
|
199
|
-
} finally {
|
|
200
|
-
rmSync(configPath, { force: true });
|
|
201
|
-
}
|
|
202
|
-
upstreamConfig = loaded.config;
|
|
203
|
-
Object.assign(upstreamConfig, initialConfig);
|
|
722
|
+
// 配置统一由 claude-code-style 管控,加载时覆盖库默认值。
|
|
723
|
+
Object.assign(config, initialConfig);
|
|
204
724
|
delegates.clear();
|
|
205
725
|
|
|
206
|
-
|
|
726
|
+
compactThinking({
|
|
207
727
|
on(eventName: string, handler: UpstreamHandler) {
|
|
208
728
|
if (eventName === "session_start") {
|
|
209
729
|
// Already inside session_start — run immediately.
|
|
@@ -236,7 +756,7 @@ export function installCompactThinking(
|
|
|
236
756
|
return {
|
|
237
757
|
updateConfig(next) {
|
|
238
758
|
Object.assign(initialConfig, next);
|
|
239
|
-
|
|
759
|
+
Object.assign(config, next);
|
|
240
760
|
},
|
|
241
761
|
};
|
|
242
762
|
}
|
|
@@ -8,18 +8,6 @@ import { createJiti } from "jiti";
|
|
|
8
8
|
type LifecycleHandler = (event: any, ctx: any) => void;
|
|
9
9
|
type FixedEditorExtension = (pi: ExtensionAPI) => void;
|
|
10
10
|
|
|
11
|
-
// Upstream publishes TS only. Nested jiti must reuse Pi's live modules — peer
|
|
12
|
-
// deps are not resolvable from ~/.pi/agent/npm/node_modules/pi-cc-extensions.
|
|
13
|
-
const jiti = createJiti(import.meta.url, {
|
|
14
|
-
virtualModules: {
|
|
15
|
-
"@earendil-works/pi-coding-agent": piCodingAgent,
|
|
16
|
-
"@earendil-works/pi-tui": piTui,
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
// Match pi-fixed-editor's own `.js` import specifier; Jiti caches `.ts` separately.
|
|
20
|
-
const terminalSplit = jiti("@tifan/pi-fixed-editor/src/terminal-split.js") as {
|
|
21
|
-
TerminalSplitCompositor?: { prototype: Record<PropertyKey, any> };
|
|
22
|
-
};
|
|
23
11
|
export type FixedEditorHitbox = { row: number; startCol: number; endCol: number };
|
|
24
12
|
export type FixedEditorViewportSnapshot = {
|
|
25
13
|
tui: object;
|
|
@@ -61,48 +49,104 @@ function clearFixedEditorViewport(): void {
|
|
|
61
49
|
fixedEditorState.viewports = new WeakMap();
|
|
62
50
|
}
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
52
|
+
type UpstreamFixedEditor = {
|
|
53
|
+
start?: LifecycleHandler;
|
|
54
|
+
shutdown?: LifecycleHandler;
|
|
67
55
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
56
|
+
|
|
57
|
+
// 上游按需加载:0.84+ 的 tui 是惰性 Proxy(installFixedEditor 用
|
|
58
|
+
// isLazyProxySession 探测),fixed-editor 整体停用 —— 连上游包都不解析,
|
|
59
|
+
// jiti 失败或包缺失都不影响主流程。
|
|
60
|
+
let upstreamLoaded = false;
|
|
61
|
+
let upstream: UpstreamFixedEditor | undefined;
|
|
62
|
+
function loadUpstream(): UpstreamFixedEditor | undefined {
|
|
63
|
+
if (upstreamLoaded) return upstream;
|
|
64
|
+
upstreamLoaded = true;
|
|
65
|
+
try {
|
|
66
|
+
// Upstream publishes TS only. Nested jiti must reuse Pi's live modules — peer
|
|
67
|
+
// deps are not resolvable from ~/.pi/agent/npm/node_modules/pi-cc-extensions.
|
|
68
|
+
const jiti = createJiti(import.meta.url, {
|
|
69
|
+
virtualModules: {
|
|
70
|
+
"@earendil-works/pi-coding-agent": piCodingAgent,
|
|
71
|
+
"@earendil-works/pi-tui": piTui,
|
|
72
|
+
},
|
|
82
73
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
74
|
+
// Match pi-fixed-editor's own `.js` import specifier; Jiti caches `.ts` separately.
|
|
75
|
+
const terminalSplit = jiti("@tifan/pi-fixed-editor/src/terminal-split.js") as {
|
|
76
|
+
TerminalSplitCompositor?: { prototype: Record<PropertyKey, any> };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const FIXED_EDITOR_SPACING_PATCH = Symbol.for("pi.ccstyle.fixed-editor-spacing-patch");
|
|
80
|
+
const clusterModule = jiti("@tifan/pi-fixed-editor/src/cluster.js") as {
|
|
81
|
+
renderFixedEditorCluster?: (input: any) => any;
|
|
82
|
+
};
|
|
83
|
+
if (
|
|
84
|
+
!(clusterModule as any)[FIXED_EDITOR_SPACING_PATCH] &&
|
|
85
|
+
clusterModule.renderFixedEditorCluster
|
|
86
|
+
) {
|
|
87
|
+
const renderCluster = clusterModule.renderFixedEditorCluster;
|
|
88
|
+
const padWidgets = (lines: string[] | undefined) =>
|
|
89
|
+
lines?.map((line) => (line.length > 0 && !line.includes("Ctrl+End") ? ` ${line}` : line));
|
|
90
|
+
clusterModule.renderFixedEditorCluster = (input) => {
|
|
91
|
+
const aboveWidgetLines = padWidgets(input.aboveWidgetLines);
|
|
92
|
+
const result = renderCluster({
|
|
93
|
+
...input,
|
|
94
|
+
// pi-fixed-editor strips Pi's leading widget spacer; restore it below the spinner.
|
|
95
|
+
aboveWidgetLines:
|
|
96
|
+
input.statusLines?.length && aboveWidgetLines?.length
|
|
97
|
+
? ["", ...aboveWidgetLines]
|
|
98
|
+
: aboveWidgetLines,
|
|
99
|
+
belowWidgetLines: padWidgets(input.belowWidgetLines),
|
|
100
|
+
});
|
|
101
|
+
const buttonRow = result.lines.findIndex((line: string) => line.includes("Ctrl+End"));
|
|
102
|
+
if (buttonRow < 0) {
|
|
103
|
+
fixedEditorState.scrollButtonHitbox = null;
|
|
104
|
+
} else {
|
|
105
|
+
const plain = result.lines[buttonRow]
|
|
106
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
107
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
108
|
+
const start = plain.indexOf("[");
|
|
109
|
+
fixedEditorState.scrollButtonHitbox = {
|
|
110
|
+
row: input.terminalRows - result.lines.length + buttonRow + 1,
|
|
111
|
+
startCol: visibleWidth(plain.slice(0, Math.max(0, start))) + 1,
|
|
112
|
+
endCol: visibleWidth(plain.trimEnd()),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
95
116
|
};
|
|
117
|
+
(clusterModule as any)[FIXED_EDITOR_SPACING_PATCH] = true;
|
|
96
118
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
119
|
+
const loaded = jiti("@tifan/pi-fixed-editor/src/index.ts") as {
|
|
120
|
+
default?: FixedEditorExtension;
|
|
121
|
+
};
|
|
122
|
+
const fixedEditor = loaded.default ?? (loaded as unknown as FixedEditorExtension);
|
|
123
|
+
|
|
124
|
+
if (terminalSplit.TerminalSplitCompositor) {
|
|
125
|
+
installFixedEditorImePatch(terminalSplit.TerminalSplitCompositor);
|
|
126
|
+
// 0.84+ 的 tui 是惰性 Proxy:compositor 构造时捕获的 doRender/render 是
|
|
127
|
+
// 每次重新解析的包装,install 后执行会解析到 compositor 自身(无限递归)。
|
|
128
|
+
// 检测到惰性 Proxy 时跳过安装,渲染管线完全交给官方。
|
|
129
|
+
installProxyGuard(terminalSplit.TerminalSplitCompositor);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let start: LifecycleHandler | undefined;
|
|
133
|
+
let shutdown: LifecycleHandler | undefined;
|
|
134
|
+
fixedEditor({
|
|
135
|
+
on(event: string, handler: LifecycleHandler) {
|
|
136
|
+
if (event === "session_start") start = handler;
|
|
137
|
+
if (event === "session_shutdown") shutdown = handler;
|
|
138
|
+
},
|
|
139
|
+
} as unknown as ExtensionAPI);
|
|
140
|
+
|
|
141
|
+
upstream = { start, shutdown };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error("pi-cc-extensions: failed to load @tifan/pi-fixed-editor", error);
|
|
144
|
+
upstream = undefined;
|
|
145
|
+
}
|
|
146
|
+
return upstream;
|
|
100
147
|
}
|
|
101
|
-
const loaded = jiti("@tifan/pi-fixed-editor/src/index.ts") as {
|
|
102
|
-
default?: FixedEditorExtension;
|
|
103
|
-
};
|
|
104
|
-
const fixedEditor = loaded.default ?? (loaded as unknown as FixedEditorExtension);
|
|
105
148
|
|
|
149
|
+
const COMPOSITOR_PROXY_GUARD = Symbol.for("pi.ccstyle.compositor-proxy-guard");
|
|
106
150
|
const IME_CURSOR_PATCH = Symbol.for("pi.ccstyle.fixed-editor-ime-cursor-patch");
|
|
107
151
|
const VIEWPORT_PATCH = Symbol.for("pi.ccstyle.fixed-editor-viewport-patch");
|
|
108
152
|
const MOUSE_WRITE_PATCH = Symbol.for("pi.ccstyle.fixed-editor-mouse-write-patch");
|
|
@@ -197,16 +241,6 @@ export function installFixedEditorImePatch(compositorClass: {
|
|
|
197
241
|
}
|
|
198
242
|
}
|
|
199
243
|
|
|
200
|
-
const COMPOSITOR_PROXY_GUARD = Symbol.for("pi.ccstyle.compositor-proxy-guard");
|
|
201
|
-
|
|
202
|
-
if (terminalSplit.TerminalSplitCompositor) {
|
|
203
|
-
installFixedEditorImePatch(terminalSplit.TerminalSplitCompositor);
|
|
204
|
-
// 0.84+ 的 tui 是惰性 Proxy:compositor 构造时捕获的 doRender/render 是
|
|
205
|
-
// 每次重新解析的包装,install 后执行会解析到 compositor 自身(无限递归)。
|
|
206
|
-
// 检测到惰性 Proxy 时跳过安装,渲染管线完全交给官方。
|
|
207
|
-
installProxyGuard(terminalSplit.TerminalSplitCompositor);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
244
|
function installProxyGuard(compositorClass: { prototype: Record<PropertyKey, any> }): void {
|
|
211
245
|
const prototype = compositorClass.prototype;
|
|
212
246
|
const originalInstall = prototype.install;
|
|
@@ -277,20 +311,41 @@ function observeFooterReplacement(ui: any, listener: () => void): () => void {
|
|
|
277
311
|
};
|
|
278
312
|
}
|
|
279
313
|
|
|
314
|
+
const LAZY_PROXY_PROBE_KEY = "cc-lazy-proxy-probe";
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 测试钩子:会话未激活时直接加载上游。懒加载下 patch 仅在 activate 时执行,
|
|
318
|
+
* 直接 new 上游 compositor / 调用上游 cluster 的测试需要显式触发。
|
|
319
|
+
*/
|
|
320
|
+
export function loadFixedEditorUpstreamForTests(): void {
|
|
321
|
+
loadUpstream();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 探测当前会话的 TUI 是否为 0.84+ 惰性 Proxy(与 isFullscreenUi 同模式)。 */
|
|
325
|
+
function isLazyProxySession(ctx: any): boolean {
|
|
326
|
+
if (typeof ctx?.ui?.setWidget !== "function") return false;
|
|
327
|
+
let proxyTui = false;
|
|
328
|
+
try {
|
|
329
|
+
ctx.ui.setWidget(LAZY_PROXY_PROBE_KEY, (tui: any) => {
|
|
330
|
+
proxyTui = isLazyProxyTui(tui);
|
|
331
|
+
return { render: () => [], invalidate() {} };
|
|
332
|
+
});
|
|
333
|
+
} catch {
|
|
334
|
+
// 探测失败按 regular 处理,避免误伤
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
ctx.ui.setWidget(LAZY_PROXY_PROBE_KEY, undefined);
|
|
338
|
+
} catch {
|
|
339
|
+
// 忽略
|
|
340
|
+
}
|
|
341
|
+
return proxyTui;
|
|
342
|
+
}
|
|
343
|
+
|
|
280
344
|
/** Adds runtime on/off control around pi-fixed-editor's session lifecycle. */
|
|
281
345
|
export function installFixedEditor(
|
|
282
346
|
pi: ExtensionAPI,
|
|
283
347
|
initiallyEnabled: boolean,
|
|
284
348
|
): FixedEditorController {
|
|
285
|
-
let start: LifecycleHandler | undefined;
|
|
286
|
-
let shutdown: LifecycleHandler | undefined;
|
|
287
|
-
fixedEditor({
|
|
288
|
-
on(event: string, handler: LifecycleHandler) {
|
|
289
|
-
if (event === "session_start") start = handler;
|
|
290
|
-
if (event === "session_shutdown") shutdown = handler;
|
|
291
|
-
},
|
|
292
|
-
} as unknown as ExtensionAPI);
|
|
293
|
-
|
|
294
349
|
const owner = {};
|
|
295
350
|
let enabled = initiallyEnabled;
|
|
296
351
|
let active = false;
|
|
@@ -340,6 +395,11 @@ export function installFixedEditor(
|
|
|
340
395
|
if (!session || session.ctx?.mode !== "tui" || active) return;
|
|
341
396
|
// fullscreen 复用官方的 sticky editor,固定编辑器让位
|
|
342
397
|
if (isFullscreenUi(session.ctx)) return;
|
|
398
|
+
// 0.84+ 的 tui 是惰性 Proxy:fixed-editor 整体停用,不加载上游包。
|
|
399
|
+
if (isLazyProxySession(session.ctx)) return;
|
|
400
|
+
const loadedUpstream = loadUpstream();
|
|
401
|
+
if (!loadedUpstream?.start || !loadedUpstream.shutdown) return;
|
|
402
|
+
const { start, shutdown } = loadedUpstream;
|
|
343
403
|
const previous = (globalThis as any)[FIXED_EDITOR_OWNER] as FixedEditorOwner | undefined;
|
|
344
404
|
if (previous?.owner !== owner) previous?.stop();
|
|
345
405
|
const currentSession = session;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.45",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "A Pi productivity suite with Claude Code-style UI, fixed editor, context inspection, and agent/session references",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"./extensions/index.ts"
|
|
32
32
|
],
|
|
33
33
|
"themes": [
|
|
34
|
-
"./themes/github-dark-default.json"
|
|
34
|
+
"./themes/github-dark-default.json",
|
|
35
|
+
"./themes/cc-dark.json"
|
|
35
36
|
],
|
|
36
37
|
"image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/welcome.webp"
|
|
37
38
|
},
|
|
@@ -48,8 +49,7 @@
|
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@shikijs/cli": "^4.0.2",
|
|
50
51
|
"@tifan/pi-fixed-editor": "^0.2.5",
|
|
51
|
-
"jiti": "^2.7.0"
|
|
52
|
-
"pi-compact-thinking": "^0.2.0"
|
|
52
|
+
"jiti": "^2.7.0"
|
|
53
53
|
},
|
|
54
54
|
"bundledDependencies": [
|
|
55
55
|
"@tifan/pi-fixed-editor"
|
|
@@ -65,5 +65,8 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@biomejs/biome": "2.5.5",
|
|
67
67
|
"@types/node": "^26.1.1"
|
|
68
|
-
}
|
|
68
|
+
},
|
|
69
|
+
"bundleDependencies": [
|
|
70
|
+
"@tifan/pi-fixed-editor"
|
|
71
|
+
]
|
|
69
72
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
|
+
"name": "cc-dark",
|
|
4
|
+
"vars": {
|
|
5
|
+
"bg": "#131217",
|
|
6
|
+
"panel": "#1b1922",
|
|
7
|
+
"surface": "#211e2b",
|
|
8
|
+
"surfaceRaised": "#292533",
|
|
9
|
+
"border": "#36313f",
|
|
10
|
+
"borderMuted": "#292631",
|
|
11
|
+
"accent": "#5e9cff",
|
|
12
|
+
"accentBright": "#8ab4ff",
|
|
13
|
+
"purple": "#c084fc",
|
|
14
|
+
"pink": "#f472b6",
|
|
15
|
+
"cyan": "#67e8f9",
|
|
16
|
+
"blue": "#60a5fa",
|
|
17
|
+
"green": "#4ade80",
|
|
18
|
+
"red": "#fb7185",
|
|
19
|
+
"yellow": "#facc15",
|
|
20
|
+
"orange": "#fb923c",
|
|
21
|
+
"text": "#f5f5f5",
|
|
22
|
+
"muted": "#a1a1aa",
|
|
23
|
+
"dim": "#71717a",
|
|
24
|
+
"selectedBg": "#252035",
|
|
25
|
+
"userMessageBg": "#1d1a27",
|
|
26
|
+
"toolPendingBg": "#1b1924",
|
|
27
|
+
"toolSuccessBg": "#18251f",
|
|
28
|
+
"toolErrorBg": "#2a1b22",
|
|
29
|
+
"customMessageBg": "#211c30"
|
|
30
|
+
},
|
|
31
|
+
"colors": {
|
|
32
|
+
"accent": "accent",
|
|
33
|
+
"border": "border",
|
|
34
|
+
"borderAccent": "accent",
|
|
35
|
+
"borderMuted": "borderMuted",
|
|
36
|
+
"success": "green",
|
|
37
|
+
"error": "red",
|
|
38
|
+
"warning": "yellow",
|
|
39
|
+
"muted": "muted",
|
|
40
|
+
"dim": "dim",
|
|
41
|
+
"text": "text",
|
|
42
|
+
"thinkingText": "muted",
|
|
43
|
+
|
|
44
|
+
"selectedBg": "selectedBg",
|
|
45
|
+
"userMessageBg": "userMessageBg",
|
|
46
|
+
"userMessageText": "text",
|
|
47
|
+
"customMessageBg": "customMessageBg",
|
|
48
|
+
"customMessageText": "text",
|
|
49
|
+
"customMessageLabel": "purple",
|
|
50
|
+
"toolPendingBg": "toolPendingBg",
|
|
51
|
+
"toolSuccessBg": "toolSuccessBg",
|
|
52
|
+
"toolErrorBg": "toolErrorBg",
|
|
53
|
+
"toolTitle": "accentBright",
|
|
54
|
+
"toolOutput": "muted",
|
|
55
|
+
|
|
56
|
+
"mdHeading": "accentBright",
|
|
57
|
+
"mdLink": "cyan",
|
|
58
|
+
"mdLinkUrl": "muted",
|
|
59
|
+
"mdCode": "accentBright",
|
|
60
|
+
"mdCodeBlock": "text",
|
|
61
|
+
"mdCodeBlockBorder": "border",
|
|
62
|
+
"mdQuote": "muted",
|
|
63
|
+
"mdQuoteBorder": "accent",
|
|
64
|
+
"mdHr": "borderMuted",
|
|
65
|
+
"mdListBullet": "accent",
|
|
66
|
+
|
|
67
|
+
"toolDiffAdded": "green",
|
|
68
|
+
"toolDiffRemoved": "red",
|
|
69
|
+
"toolDiffContext": "muted",
|
|
70
|
+
|
|
71
|
+
"syntaxComment": "#6b7280",
|
|
72
|
+
"syntaxKeyword": "pink",
|
|
73
|
+
"syntaxFunction": "accentBright",
|
|
74
|
+
"syntaxVariable": "blue",
|
|
75
|
+
"syntaxString": "green",
|
|
76
|
+
"syntaxNumber": "orange",
|
|
77
|
+
"syntaxType": "cyan",
|
|
78
|
+
"syntaxOperator": "text",
|
|
79
|
+
"syntaxPunctuation": "muted",
|
|
80
|
+
|
|
81
|
+
"thinkingOff": "muted",
|
|
82
|
+
"thinkingMinimal": "dim",
|
|
83
|
+
"thinkingLow": "blue",
|
|
84
|
+
"thinkingMedium": "accent",
|
|
85
|
+
"thinkingHigh": "accentBright",
|
|
86
|
+
"thinkingXhigh": "cyan",
|
|
87
|
+
"thinkingMax": "purple",
|
|
88
|
+
|
|
89
|
+
"bashMode": "accent"
|
|
90
|
+
},
|
|
91
|
+
"export": {
|
|
92
|
+
"pageBg": "bg",
|
|
93
|
+
"cardBg": "panel",
|
|
94
|
+
"infoBg": "surface"
|
|
95
|
+
}
|
|
96
|
+
}
|