pi-cc-extensions 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -0
- package/README.md +3 -1
- package/extensions/config/config.ts +16 -0
- package/extensions/config/panel.ts +47 -0
- package/extensions/feature/compact-thinking.ts +1 -1
- package/extensions/feature/reference/subagent.ts +2 -1
- package/extensions/feature/shell/footer.ts +57 -10
- package/extensions/feature/shell/token-rate.ts +192 -0
- package/extensions/feature/shell/working-message.ts +52 -65
- package/extensions/renderer/compact-mode.ts +38 -28
- package/extensions/renderer/default-mode.ts +23 -463
- package/extensions/renderer/index.ts +3 -3
- package/extensions/renderer/markdown-enhance.ts +63 -22
- package/extensions/renderer/mouse/hover.ts +1 -1
- package/extensions/renderer/mouse/interaction.ts +2 -2
- package/extensions/renderer/mouse/packets.ts +1 -1
- package/extensions/renderer/mouse/sticky-prompt.ts +61 -9
- package/extensions/renderer/tool/ask-user-question.ts +1 -1
- package/extensions/renderer/tool/ccstyle-tool.ts +467 -0
- package/extensions/renderer/tool/codemode.ts +1 -1
- package/extensions/renderer/tool/diff/diff-edit-render.ts +1 -1
- package/extensions/renderer/tool/diff/diff-limits.ts +1 -1
- package/extensions/renderer/tool/diff/diff-write-render.ts +1 -1
- package/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
- package/extensions/renderer/tool/grouping.ts +56 -50
- package/extensions/renderer/tool/message-display.ts +1 -1
- package/extensions/renderer/tool/names.ts +41 -1
- package/extensions/renderer/tool/result.ts +2 -862
- package/extensions/renderer/tool/tool-io.ts +735 -0
- package/extensions/renderer/tool/tool-state.ts +138 -0
- package/extensions/utils/format.ts +9 -0
- package/package.json +1 -1
- package/themes/cc-light.json +1 -1
- package/extensions/renderer/tool/show-more-hint.ts +0 -48
|
@@ -5,43 +5,28 @@ import {
|
|
|
5
5
|
getToolMouseTui,
|
|
6
6
|
isFullscreenAtBottom,
|
|
7
7
|
} from "../../renderer/mouse/scroll.ts";
|
|
8
|
-
import { formatDuration } from "../../utils/format.ts";
|
|
8
|
+
import { formatDuration, formatTokenCount } from "../../utils/format.ts";
|
|
9
|
+
import {
|
|
10
|
+
resetTokenRate,
|
|
11
|
+
carryTokenRate,
|
|
12
|
+
pauseTokenRate,
|
|
13
|
+
resumeTokenRate,
|
|
14
|
+
markFirstToken,
|
|
15
|
+
sampleTokenDelta,
|
|
16
|
+
recordProviderUsage,
|
|
17
|
+
startMessageSegment,
|
|
18
|
+
totalTokens,
|
|
19
|
+
} from "./token-rate.ts";
|
|
9
20
|
|
|
10
21
|
const REFRESH_INTERVAL_MS = 1_000;
|
|
11
22
|
/** Elapsed time is only shown once the turn has run this long. */
|
|
12
23
|
const SHOW_TIMER_AFTER_MS = 3_000;
|
|
13
24
|
|
|
14
|
-
function formatCount(value: number): string {
|
|
15
|
-
return new Intl.NumberFormat("en-US").format(value);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type ContentBlock = {
|
|
19
|
-
type?: unknown;
|
|
20
|
-
text?: unknown;
|
|
21
|
-
thinkingSignature?: { body?: unknown };
|
|
22
|
-
};
|
|
23
|
-
|
|
24
25
|
type StreamMessage = {
|
|
25
26
|
content?: unknown;
|
|
26
27
|
usage?: { output?: unknown };
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
/** 每个 content index 的可见文本/思考长度;无对应块的 index 保持稀疏洞。 */
|
|
30
|
-
function textBlockLengths(message: StreamMessage): number[] {
|
|
31
|
-
const content = message.content;
|
|
32
|
-
if (!Array.isArray(content)) return [];
|
|
33
|
-
const lengths: number[] = [];
|
|
34
|
-
for (let index = 0; index < content.length; index++) {
|
|
35
|
-
const block = content[index] as ContentBlock;
|
|
36
|
-
if (block?.type === "text" && typeof block.text === "string") {
|
|
37
|
-
lengths[index] = block.text.length;
|
|
38
|
-
} else if (block?.type === "thinking" && block.thinkingSignature?.body) {
|
|
39
|
-
lengths[index] = (block.thinkingSignature.body as string).length;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return lengths;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
30
|
function outputUsage(message: StreamMessage): number {
|
|
46
31
|
const value = Number(message?.usage?.output);
|
|
47
32
|
return Number.isFinite(value) && value > 0 ? Math.round(value) : 0;
|
|
@@ -55,39 +40,24 @@ type WorkingUi = {
|
|
|
55
40
|
* Extend Pi's footer working row while preserving its spinner and "Working...":
|
|
56
41
|
* `⠋ Working... (↓ 1,234 tokens · 12s)`
|
|
57
42
|
*
|
|
58
|
-
*
|
|
59
|
-
* provider `usage.output` whenever the stream exposes an actual count.
|
|
60
|
-
*/
|
|
43
|
+
* token 数直接读 token-rate 的累计(估算 → provider usage 同源),不再本地重数。 */
|
|
61
44
|
export default function (pi: ExtensionAPI): void {
|
|
62
45
|
let turnActive = false;
|
|
63
46
|
let agentStartTime = 0;
|
|
64
47
|
let turnStartTime = 0;
|
|
65
|
-
|
|
66
|
-
let
|
|
67
|
-
let providerOutputTokens = 0;
|
|
48
|
+
/** 当前用户指令的起点;工具调用后的 turn 复用它,直到下一条指令。 */
|
|
49
|
+
let rateRunStartTime = 0;
|
|
68
50
|
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
69
51
|
let lastMessage: string | null = null;
|
|
70
52
|
let activeCtx: { ui: WorkingUi | undefined; hasUI: boolean } | null = null;
|
|
71
53
|
|
|
72
54
|
function tokenCount(): number {
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function setTextBlockLength(index: number, length: number): void {
|
|
77
|
-
const previous = responseTextBlockLengths[index] ?? 0;
|
|
78
|
-
responseTextBlockLengths[index] = Math.max(0, length);
|
|
79
|
-
responseLength = Math.max(0, responseLength + responseTextBlockLengths[index] - previous);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function resetResponseTracking(message?: StreamMessage): void {
|
|
83
|
-
responseTextBlockLengths = message ? textBlockLengths(message) : [];
|
|
84
|
-
responseLength = responseTextBlockLengths.reduce((sum, length) => sum + length, 0);
|
|
85
|
-
providerOutputTokens = message ? outputUsage(message) : 0;
|
|
55
|
+
return totalTokens();
|
|
86
56
|
}
|
|
87
57
|
|
|
88
58
|
function updateProviderUsage(message: StreamMessage): void {
|
|
89
59
|
const output = outputUsage(message);
|
|
90
|
-
if (output > 0)
|
|
60
|
+
if (output > 0) recordProviderUsage(output);
|
|
91
61
|
}
|
|
92
62
|
|
|
93
63
|
/**
|
|
@@ -103,7 +73,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
103
73
|
function buildWorkingMessage(): string {
|
|
104
74
|
const parts: string[] = [];
|
|
105
75
|
const tokens = tokenCount();
|
|
106
|
-
if (tokens > 0) parts.push(`↓ ${
|
|
76
|
+
if (tokens > 0) parts.push(`↓ ${formatTokenCount(tokens)} tokens`);
|
|
107
77
|
// compact 活动回合且已滚出摘要行:直接用摘要行文案(自带回合时长,不叠 agent 计时)。
|
|
108
78
|
const compactStatus = compactMirrorText();
|
|
109
79
|
if (compactStatus) return [compactStatus, ...parts].join(" · ");
|
|
@@ -179,7 +149,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
179
149
|
stopRefreshLoop();
|
|
180
150
|
agentStartTime = 0;
|
|
181
151
|
turnStartTime = 0;
|
|
182
|
-
|
|
152
|
+
// 不重置 token-rate:footer 定格显示上一回合速率
|
|
183
153
|
restoreDefaultWorkingMessage();
|
|
184
154
|
}
|
|
185
155
|
|
|
@@ -187,12 +157,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
187
157
|
if (!agentStartTime) agentStartTime = Date.now();
|
|
188
158
|
});
|
|
189
159
|
|
|
190
|
-
pi.on("turn_start", async (
|
|
160
|
+
pi.on("turn_start", async (event, ctx) => {
|
|
191
161
|
turnActive = true;
|
|
192
162
|
activeCtx = ctx;
|
|
193
|
-
|
|
163
|
+
// 事件时间戳在 handler 之前产生;用 Date.now() 会和紧随的采样落在同一毫秒,分母变成 0。
|
|
164
|
+
turnStartTime = event?.timestamp || Date.now();
|
|
194
165
|
if (!agentStartTime) agentStartTime = turnStartTime;
|
|
195
|
-
|
|
166
|
+
// 工具调用后的 turn 属于同一条用户指令:计时、TTFT 与累计 token 保持到下一条指令。
|
|
167
|
+
// 速率起点用本指令第一个 turn 的时间戳;before_agent_start 只当指令 id。
|
|
168
|
+
if (rateRunStartTime !== agentStartTime) {
|
|
169
|
+
rateRunStartTime = agentStartTime;
|
|
170
|
+
resetTokenRate(turnStartTime);
|
|
171
|
+
}
|
|
172
|
+
startMessageSegment(turnStartTime);
|
|
196
173
|
syncWorkingMessage(true);
|
|
197
174
|
scheduleRefreshTick();
|
|
198
175
|
});
|
|
@@ -203,24 +180,25 @@ export default function (pi: ExtensionAPI): void {
|
|
|
203
180
|
if (!evt) return;
|
|
204
181
|
|
|
205
182
|
if (evt.type === "start") {
|
|
206
|
-
|
|
207
|
-
} else if (
|
|
208
|
-
|
|
183
|
+
// 新 assistant 消息起点:速率分段已在 turn_start 处理。
|
|
184
|
+
} else if (
|
|
185
|
+
evt.type === "thinking_start" ||
|
|
186
|
+
evt.type === "text_start" ||
|
|
187
|
+
evt.type === "toolcall_start"
|
|
188
|
+
) {
|
|
189
|
+
// TTFT 的首块含 toolcall_start:模型直接发起 tool call(thinking/text 省略)时也算首 token。
|
|
190
|
+
// 分段锚点只在 turn_start 重置。这里再重置会把累计 usage.output 当成新的首报。
|
|
191
|
+
markFirstToken();
|
|
209
192
|
updateProviderUsage(evt.partial);
|
|
210
193
|
} else if (evt.type === "thinking_delta" || evt.type === "text_delta") {
|
|
211
|
-
|
|
212
|
-
setTextBlockLength(evt.contentIndex, (responseTextBlockLengths[evt.contentIndex] ?? 0) + add);
|
|
194
|
+
if (typeof evt.delta === "string") sampleTokenDelta(evt.delta);
|
|
213
195
|
updateProviderUsage(evt.partial);
|
|
214
|
-
} else if (evt.type === "text_end") {
|
|
215
|
-
setTextBlockLength(
|
|
216
|
-
evt.contentIndex,
|
|
217
|
-
typeof evt.content === "string" ? evt.content.length : 0,
|
|
218
|
-
);
|
|
196
|
+
} else if (evt.type === "text_end" || evt.type === "thinking_end") {
|
|
219
197
|
updateProviderUsage(evt.partial);
|
|
220
198
|
} else if (evt.type === "done") {
|
|
221
|
-
|
|
199
|
+
updateProviderUsage(evt.message);
|
|
222
200
|
} else if (evt.type === "error") {
|
|
223
|
-
|
|
201
|
+
updateProviderUsage(evt.error);
|
|
224
202
|
} else {
|
|
225
203
|
updateProviderUsage(evt.partial);
|
|
226
204
|
}
|
|
@@ -229,11 +207,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
229
207
|
scheduleRefreshTick();
|
|
230
208
|
});
|
|
231
209
|
|
|
210
|
+
pi.on("tool_execution_start", async () => {
|
|
211
|
+
pauseTokenRate();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
pi.on("tool_execution_end", async () => {
|
|
215
|
+
resumeTokenRate();
|
|
216
|
+
});
|
|
217
|
+
|
|
232
218
|
pi.on("turn_end", async (_event, ctx) => {
|
|
233
219
|
turnActive = false;
|
|
234
220
|
activeCtx = ctx;
|
|
235
221
|
stopRefreshLoop();
|
|
236
|
-
|
|
222
|
+
// 本段 token 结转进累计,下一段 turn 继续累加;此处不清速率。
|
|
223
|
+
carryTokenRate();
|
|
237
224
|
// No completion message: return immediately to Pi's default idle state.
|
|
238
225
|
restoreDefaultWorkingMessage();
|
|
239
226
|
});
|
|
@@ -42,7 +42,7 @@ import { toolLoadingIcon } from "../utils/tool-loading-icon.ts";
|
|
|
42
42
|
import { sanitizeToolResultText } from "../utils/tool-result-sanitize.ts";
|
|
43
43
|
import { refreshTranscriptComponent } from "./transcript-refresh.ts";
|
|
44
44
|
import { getMessageDisplayTheme } from "./tool/message-display.ts";
|
|
45
|
-
import { showMoreHintText } from "./tool/
|
|
45
|
+
import { showMoreHintText } from "./tool/names.ts";
|
|
46
46
|
import {
|
|
47
47
|
countEditDiffStats,
|
|
48
48
|
countWriteDiffStats,
|
|
@@ -53,13 +53,11 @@ import { displayConfigCacheKey } from "./tool/diff/diff-component.ts";
|
|
|
53
53
|
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
54
54
|
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
55
55
|
import { getToolMouseTui } from "./mouse/scroll.ts";
|
|
56
|
-
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
renderExpandedToolResult,
|
|
60
|
-
scheduleAnimation,
|
|
61
|
-
} from "./tool/result.ts";
|
|
56
|
+
import { insetComponent } from "./tool/result.ts";
|
|
57
|
+
import { isLiveToolCall, scheduleAnimation } from "./tool/tool-state.ts";
|
|
58
|
+
import { renderExpandedToolResult } from "./tool/tool-io.ts";
|
|
62
59
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
60
|
+
import { styleChangeCounts } from "./tool/ccstyle-tool.ts";
|
|
63
61
|
import { formatDisplayPath } from "./tool/names.ts";
|
|
64
62
|
import {
|
|
65
63
|
hasVisibleText,
|
|
@@ -663,31 +661,37 @@ export function markCompactRoundToolExpanded(tool: any): void {
|
|
|
663
661
|
roundToolExpansion?.markUserExpanded(tool);
|
|
664
662
|
}
|
|
665
663
|
|
|
666
|
-
/**
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
664
|
+
/**
|
|
665
|
+
* 展开面板前的分隔空行与面板绑成一体:挂在同一尾行上,才不会被拆到
|
|
666
|
+
* anchor 正文之后(面板挂回合尾行,空行却留在消息里)。
|
|
667
|
+
* 带 childAtRow 返回 null:拆分判据认它为面板成员,行命中落空时归外层面板。
|
|
668
|
+
*/
|
|
669
|
+
function panelBoundSpacer(): any {
|
|
670
|
+
return {
|
|
671
|
+
render: () => [""],
|
|
672
|
+
childAtRow: () => null,
|
|
673
|
+
invalidate() {},
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** 面板展开前的视口标记:收起时需要重钉视口的卡。 */
|
|
678
|
+
const panelViewportBeforeExpand = new WeakSet<object>();
|
|
671
679
|
|
|
672
680
|
function capturePanelViewport(card: any): void {
|
|
673
|
-
if (!card
|
|
674
|
-
|
|
675
|
-
if (!view || typeof view.scrollTop !== "number" || typeof view.scrollTo !== "function") return;
|
|
676
|
-
panelViewportBeforeExpand.set(card, {
|
|
677
|
-
scrollTop: view.scrollTop,
|
|
678
|
-
followingEnd: view.isFollowingEnd !== false,
|
|
679
|
-
});
|
|
681
|
+
if (!card) return;
|
|
682
|
+
panelViewportBeforeExpand.add(card);
|
|
680
683
|
}
|
|
681
684
|
|
|
682
685
|
function restorePanelViewport(card: any): void {
|
|
683
|
-
|
|
684
|
-
if (!snapshot) return;
|
|
685
|
-
panelViewportBeforeExpand.delete(card);
|
|
686
|
+
if (!panelViewportBeforeExpand.delete(card)) return;
|
|
686
687
|
const view = getToolMouseTui()?.getPrimaryScrollView?.();
|
|
687
688
|
if (!view) return;
|
|
688
|
-
//
|
|
689
|
-
|
|
690
|
-
|
|
689
|
+
// 不用展开前的快照:展开期间用户可能滚进面板里,回跳旧位置才是"跳"。
|
|
690
|
+
// 面板在摘要行之下,收起只删它以下的行,保持当前 scrollTop 摘要行原地不动;
|
|
691
|
+
// clamp 后落在新底时 scrollTo 会自动恢复 followingEnd(并清 followSuppressedAtEnd)。
|
|
692
|
+
if (typeof view.scrollTo === "function" && typeof view.scrollTop === "number") {
|
|
693
|
+
view.scrollTo(view.scrollTop);
|
|
694
|
+
}
|
|
691
695
|
}
|
|
692
696
|
|
|
693
697
|
function compactEditWriteLine(
|
|
@@ -724,8 +728,13 @@ function compactEditWriteLine(
|
|
|
724
728
|
)
|
|
725
729
|
: undefined;
|
|
726
730
|
if (stats) {
|
|
727
|
-
|
|
728
|
-
|
|
731
|
+
const counts = styleChangeCounts(
|
|
732
|
+
(color, text) => theme.fg(color, text),
|
|
733
|
+
stats.added,
|
|
734
|
+
stats.removed,
|
|
735
|
+
);
|
|
736
|
+
statsText = counts.text;
|
|
737
|
+
statsStyled = counts.styled;
|
|
729
738
|
}
|
|
730
739
|
}
|
|
731
740
|
// 展开卡 Box(1,1) 已 pad;折叠行自己留 1 格前导空格
|
|
@@ -1828,7 +1837,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1828
1837
|
} else {
|
|
1829
1838
|
// 面板上沿留 1 行卡外空行:面板底色与 userMessage 同色,
|
|
1830
1839
|
// 贴在一起会看成一个块;卡内顶部再加 1 行 padding 保持上下对称。
|
|
1831
|
-
|
|
1840
|
+
// 空行必须与面板同进退(childAtRow 参与面板拆分),用 panelBoundSpacer。
|
|
1841
|
+
anchorChildren.push(panelBoundSpacer());
|
|
1832
1842
|
anchorChildren.push(
|
|
1833
1843
|
compactRoundCard(
|
|
1834
1844
|
cardItems,
|