pi-cc-extensions 1.0.2 → 1.0.3
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 +35 -35
- package/README.md +3 -3
- package/extensions/config/config.ts +5 -0
- package/extensions/config/panel.ts +22 -0
- package/extensions/feature/context.ts +131 -28
- package/extensions/feature/reference/index.ts +2 -0
- package/extensions/feature/reference/subagent.ts +2 -0
- package/extensions/feature/shell/footer.ts +1 -0
- package/extensions/feature/shell/token-rate.ts +15 -3
- package/extensions/feature/shell/working-message.ts +0 -19
- package/extensions/renderer/compact-mode.ts +67 -27
- package/extensions/renderer/index.ts +3 -0
- package/extensions/renderer/tool/diff/write-execution.ts +2 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -45,52 +45,53 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
45
45
|
```jsonc
|
|
46
46
|
{
|
|
47
47
|
// style
|
|
48
|
-
"mode": "on",
|
|
49
|
-
"excludeRenderers": [],
|
|
48
|
+
"mode": "on", // on / compact / off
|
|
49
|
+
"excludeRenderers": [], // tools keeping the native renderer; Agent always keeps its dedicated renderer
|
|
50
50
|
|
|
51
51
|
// features
|
|
52
|
-
"enableSessionReference": true,
|
|
53
|
-
"enableSubagentAutocomplete": true,
|
|
54
|
-
"enableContextCommand": true,
|
|
55
|
-
"enableAgentSummary": true,
|
|
56
|
-
"enableWorkingMessage": true,
|
|
57
|
-
"enableAliases": true,
|
|
52
|
+
"enableSessionReference": true, // @ session references
|
|
53
|
+
"enableSubagentAutocomplete": true, // @subagent:[name] completion and delegation hints
|
|
54
|
+
"enableContextCommand": true, // /context usage check
|
|
55
|
+
"enableAgentSummary": true, // per-turn tool summary
|
|
56
|
+
"enableWorkingMessage": true, // Working... bottom token/elapsed
|
|
57
|
+
"enableAliases": true, // /clear, /exit aliases
|
|
58
58
|
|
|
59
59
|
// ui
|
|
60
|
-
"expandedInputMaxLines": 5,
|
|
61
|
-
"expandedOutputMaxLines": 10,
|
|
62
|
-
"expandedPreviewMaxLines": 40,
|
|
60
|
+
"expandedInputMaxLines": 5, // expanded tool Input lines; overflow shows a footer hint
|
|
61
|
+
"expandedOutputMaxLines": 10, // expanded tool Output lines; overflow shows a footer hint
|
|
62
|
+
"expandedPreviewMaxLines": 40, // max lines for expanded TaskList bodies (expanded diffs always show all)
|
|
63
63
|
"expandedCardBackground": "userMessageBg", // expanded card background slot: userMessageBg (same as user prompts) / toolPendingBg (neutral) etc.
|
|
64
|
-
"toolLabelClip": 0,
|
|
65
|
-
"showStartupHeader": true,
|
|
66
|
-
"stickyUserMessage": true,
|
|
64
|
+
"toolLabelClip": 0, // tool summary path/command clip length; 0 = fit width
|
|
65
|
+
"showStartupHeader": true, // startup header (logo + tips) toggle
|
|
66
|
+
"stickyUserMessage": true, // fullscreen: pin a user message's first line once it scrolls off the top edge, until the next one takes over
|
|
67
67
|
|
|
68
68
|
// diff
|
|
69
|
-
"diffViewMode": "auto",
|
|
70
|
-
"diffIndicatorMode": "bars",
|
|
71
|
-
"diffSplitMinWidth": 120,
|
|
72
|
-
"editDiffCollapsedLines": 24,
|
|
73
|
-
"writeDiffCollapsedLines": 0,
|
|
74
|
-
"diffWordWrap": true,
|
|
69
|
+
"diffViewMode": "auto", // layout: auto / split / unified
|
|
70
|
+
"diffIndicatorMode": "bars", // change indicators: bars / classic / none
|
|
71
|
+
"diffSplitMinWidth": 120, // min terminal width for side-by-side columns
|
|
72
|
+
"editDiffCollapsedLines": 24, // edit collapse lines; 0 = change-count summary only
|
|
73
|
+
"writeDiffCollapsedLines": 0, // write collapse lines; 0 = creation summary only
|
|
74
|
+
"diffWordWrap": true, // wrap long diff lines
|
|
75
75
|
|
|
76
76
|
// thinking
|
|
77
77
|
"useSummaryTitlesAsThinkingTitle": true, // use latest summary as thinking title
|
|
78
|
-
"previewLines": 3,
|
|
79
|
-
"animationIntervalMs": 90,
|
|
80
|
-
"dimThinkingText": false,
|
|
78
|
+
"previewLines": 3, // preview lines; 0 hides
|
|
79
|
+
"animationIntervalMs": 90, // title animation interval (ms)
|
|
80
|
+
"dimThinkingText": false, // dim thinking body text
|
|
81
81
|
|
|
82
82
|
// footer
|
|
83
|
-
"enableCustomFooter": true,
|
|
84
|
-
"footerTokenRate": true,
|
|
85
|
-
"footerTokenRateMode": "average",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
83
|
+
"enableCustomFooter": true, // custom status bar
|
|
84
|
+
"footerTokenRate": true, // output rate chip; false hides it
|
|
85
|
+
"footerTokenRateMode": "average", // average: whole-prompt avg; live: trailing 1s rate
|
|
86
|
+
"footerTokenRateTtft": true, // append TTFT to the rate chip; false shows tok/s only
|
|
87
|
+
"footerCurrency": "USD", // display currency for the footer cost
|
|
88
|
+
"footerCurrencyRate": null, // display units per 1 cost unit; null changes the symbol only
|
|
89
|
+
"footerNerdIcons": true, // Nerd Font glyphs for git/cache; false = plain text
|
|
90
|
+
"footerGitStatsMode": "working", // working (default): uncommitted; branch: cumulative net changes
|
|
91
|
+
"footerHiddenKeys": [], // hidden plugin chip keys
|
|
92
|
+
"footerLine1Keys": ["pi-usage"], // line1 plugin chip order; pi-usage is shown by default, data from @narumitw/pi-usage
|
|
93
|
+
"footerLine2Keys": [], // line2 plugin chip order (after cwd/git)
|
|
94
|
+
"footerLine3Keys": [], // line3 overflow slot; painted only when a chip is visible
|
|
94
95
|
}
|
|
95
96
|
```
|
|
96
97
|
|
|
@@ -116,7 +117,6 @@ pi -e . # one run, does not edit settings.json
|
|
|
116
117
|
|
|
117
118
|
| Extension | Purpose |
|
|
118
119
|
| ------------------------ | ----------------------------------------------------------- |
|
|
119
|
-
| `npm:@ff-labs/pi-fff` | FFF-powered fuzzy file and content search (fffind / ffgrep) |
|
|
120
120
|
| `npm:pi-web-access` | Web search, URL fetching, GitHub cloning, PDF/video parsing |
|
|
121
121
|
| `npm:@narumitw/pi-usage` | Current-account usage for Codex / Copilot / OpenRouter |
|
|
122
122
|
|
package/README.md
CHANGED
|
@@ -81,8 +81,9 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
81
81
|
|
|
82
82
|
// footer
|
|
83
83
|
"enableCustomFooter": true, // 自定义状态栏
|
|
84
|
-
"footerTokenRate": true,
|
|
85
|
-
"footerTokenRateMode": "average",
|
|
84
|
+
"footerTokenRate": true, // 输出速率芯片;false 隐藏
|
|
85
|
+
"footerTokenRateMode": "average", // average:整段指令平均(avg);live:最近 1 秒瞬时(live)
|
|
86
|
+
"footerTokenRateTtft": true, // 速率芯片是否带 TTFT;false 只显示 tok/s
|
|
86
87
|
"footerCurrency": "USD", // 底栏费用的显示币种
|
|
87
88
|
"footerCurrencyRate": null, // 1 单位费用换成多少显示币种;null 只换符号、不乘倍率
|
|
88
89
|
"footerNerdIcons": true, // git/缓存用 Nerd Font 图标;false 为纯文本
|
|
@@ -116,7 +117,6 @@ pi -e . # 只跑一次,不改 settings.json
|
|
|
116
117
|
|
|
117
118
|
| 扩展 | 用途 |
|
|
118
119
|
| ---------------------------------------- | ------------------------------------------------ |
|
|
119
|
-
| `npm:@ff-labs/pi-fff` | 模糊文件与内容检索(fffind / ffgrep) |
|
|
120
120
|
| `npm:pi-web-access` | 网页搜索、URL 抓取、GitHub 克隆、PDF/视频解析 |
|
|
121
121
|
| `npm:@narumitw/pi-usage` | 查看当前账号用量(Codex / Copilot / OpenRouter) |
|
|
122
122
|
|
|
@@ -79,6 +79,8 @@ export type Config = {
|
|
|
79
79
|
footerTokenRate: boolean;
|
|
80
80
|
/** average:整段指令墙钟平均;live:最近 1 秒瞬时速率,空档回退平均。 */
|
|
81
81
|
footerTokenRateMode: FooterTokenRateMode;
|
|
82
|
+
/** 速率芯片是否带 TTFT。关则只显示速率。 */
|
|
83
|
+
footerTokenRateTtft: boolean;
|
|
82
84
|
/** ISO 4217 显示币种。USD 时不换算。 */
|
|
83
85
|
footerCurrency: string;
|
|
84
86
|
/** 1 单位费用对应的显示币种数量。null 只换符号、不乘倍率。 */
|
|
@@ -171,6 +173,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
171
173
|
enableCustomFooter: true,
|
|
172
174
|
footerTokenRate: true,
|
|
173
175
|
footerTokenRateMode: "average",
|
|
176
|
+
footerTokenRateTtft: true,
|
|
174
177
|
footerCurrency: "USD",
|
|
175
178
|
footerCurrencyRate: null,
|
|
176
179
|
footerNerdIcons: true,
|
|
@@ -307,6 +310,7 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
307
310
|
FOOTER_TOKEN_RATE_MODES,
|
|
308
311
|
DEFAULT_CONFIG.footerTokenRateMode,
|
|
309
312
|
),
|
|
313
|
+
footerTokenRateTtft: source.footerTokenRateTtft !== false,
|
|
310
314
|
footerCurrency: normalizeCurrency(source.footerCurrency),
|
|
311
315
|
footerCurrencyRate: normalizeFooterCurrencyRate(source.footerCurrencyRate),
|
|
312
316
|
footerNerdIcons: source.footerNerdIcons !== false,
|
|
@@ -372,6 +376,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
372
376
|
`footer=${source.enableCustomFooter ? "on" : "off"}`,
|
|
373
377
|
`footerTokenRate=${source.footerTokenRate ? "on" : "off"}`,
|
|
374
378
|
`footerTokenRateMode=${source.footerTokenRateMode}`,
|
|
379
|
+
`footerTokenRateTtft=${source.footerTokenRateTtft ? "on" : "off"}`,
|
|
375
380
|
`footerCurrency=${source.footerCurrency}`,
|
|
376
381
|
`footerRate=${source.footerCurrencyRate ?? "-"}`,
|
|
377
382
|
`footerIcons=${source.footerNerdIcons ? "nerd" : "plain"}`,
|
|
@@ -101,6 +101,12 @@ function footerTokenRateModeDescription(mode: FooterTokenRateMode): string {
|
|
|
101
101
|
: "Average over the whole user prompt (avg), including tool waits. Stays until the next prompt.";
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function footerTokenRateTtftDescription(enabled: boolean): string {
|
|
105
|
+
return enabled
|
|
106
|
+
? "Append time to first token, e.g. (TTFT: 812ms) or (TTFT: 1.2s). Turn off to show the rate only."
|
|
107
|
+
: "Time to first token hidden. The rate chip shows tok/s only.";
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
function footerGitStatsDescription(mode: FooterGitStatsMode): string {
|
|
105
111
|
return mode === "branch"
|
|
106
112
|
? "Net changes since the merge base, including uncommitted edits. Base: origin/HEAD, then local main/master. Untracked files excluded; unavailable stats hidden."
|
|
@@ -758,6 +764,13 @@ export async function showCcstylePanel(
|
|
|
758
764
|
currentValue: config.footerTokenRateMode,
|
|
759
765
|
values: FOOTER_TOKEN_RATE_MODES,
|
|
760
766
|
};
|
|
767
|
+
const footerTokenRateTtftSetting = {
|
|
768
|
+
id: "footerTokenRateTtft",
|
|
769
|
+
label: "Token rate TTFT",
|
|
770
|
+
description: footerTokenRateTtftDescription(config.footerTokenRateTtft),
|
|
771
|
+
currentValue: config.footerTokenRateTtft ? "on" : "off",
|
|
772
|
+
values: ["on", "off"],
|
|
773
|
+
};
|
|
761
774
|
const pluginChipsSetting = {
|
|
762
775
|
id: "footerPluginChips",
|
|
763
776
|
label: "Plugin chips",
|
|
@@ -847,6 +860,14 @@ export async function showCcstylePanel(
|
|
|
847
860
|
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
848
861
|
return;
|
|
849
862
|
}
|
|
863
|
+
if (id === "footerTokenRateTtft") {
|
|
864
|
+
const enabled = value === "on";
|
|
865
|
+
updateConfig({ footerTokenRateTtft: enabled });
|
|
866
|
+
footerTokenRateTtftSetting.currentValue = enabled ? "on" : "off";
|
|
867
|
+
footerTokenRateTtftSetting.description = footerTokenRateTtftDescription(enabled);
|
|
868
|
+
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
850
871
|
if (id === "footerNerdIcons") {
|
|
851
872
|
const enabled = value === "on";
|
|
852
873
|
updateConfig({ footerNerdIcons: enabled });
|
|
@@ -1067,6 +1088,7 @@ export async function showCcstylePanel(
|
|
|
1067
1088
|
customFooterSetting,
|
|
1068
1089
|
footerTokenRateSetting,
|
|
1069
1090
|
footerTokenRateModeSetting,
|
|
1091
|
+
footerTokenRateTtftSetting,
|
|
1070
1092
|
footerCurrencySetting,
|
|
1071
1093
|
footerCurrencyRateSetting,
|
|
1072
1094
|
footerNerdIconsSetting,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ExtensionAPI,
|
|
4
4
|
type ExtensionCommandContext,
|
|
5
5
|
type ToolInfo,
|
|
6
|
+
copyToClipboard,
|
|
6
7
|
estimateTokens,
|
|
7
8
|
formatSkillsForPrompt,
|
|
8
9
|
getMarkdownTheme,
|
|
@@ -54,6 +55,30 @@ export function escCloseHitbox(bounds: DialogBounds): {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
const COPY_LABEL = "[copy]";
|
|
59
|
+
const ESC_LABEL = "[esc]";
|
|
60
|
+
|
|
61
|
+
/** 测试可替换;生产使用 pi 的剪贴板实现。 */
|
|
62
|
+
let copyTextImpl: (text: string) => Promise<void> = (text) => copyToClipboard(text);
|
|
63
|
+
|
|
64
|
+
export function setPreviewCopyImpl(impl: ((text: string) => Promise<void>) | null): void {
|
|
65
|
+
copyTextImpl = impl ?? ((text) => copyToClipboard(text));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 1-based terminal hitbox of the [copy] button, one space left of [esc] on the same title row. */
|
|
69
|
+
export function copyButtonHitbox(bounds: DialogBounds): {
|
|
70
|
+
row: number;
|
|
71
|
+
startCol: number;
|
|
72
|
+
endCol: number;
|
|
73
|
+
} {
|
|
74
|
+
const esc = escCloseHitbox(bounds);
|
|
75
|
+
return {
|
|
76
|
+
row: esc.row,
|
|
77
|
+
startCol: esc.startCol - visibleWidth(COPY_LABEL) - 1,
|
|
78
|
+
endCol: esc.startCol - 2,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
let activeContextOverlays = 0;
|
|
58
83
|
|
|
59
84
|
/** fullscreen 输入包装用于把鼠标事件继续传给当前 context 主弹框或文本预览 overlay。 */
|
|
@@ -81,7 +106,9 @@ export async function showTextPreview(
|
|
|
81
106
|
let pageSize = 1;
|
|
82
107
|
let totalLines = 1;
|
|
83
108
|
let escHovered = false;
|
|
109
|
+
let copyHovered = false;
|
|
84
110
|
let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
111
|
+
let copyHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
85
112
|
let scrollbarHitbox:
|
|
86
113
|
| {
|
|
87
114
|
col: number;
|
|
@@ -108,6 +135,24 @@ export async function showTextPreview(
|
|
|
108
135
|
tui.requestRender();
|
|
109
136
|
};
|
|
110
137
|
|
|
138
|
+
const setCopyHovered = (hovered: boolean): void => {
|
|
139
|
+
if (hovered === copyHovered) return;
|
|
140
|
+
copyHovered = hovered;
|
|
141
|
+
tui.requestRender();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const copyPreview = (): void => {
|
|
145
|
+
void copyTextImpl(content)
|
|
146
|
+
.then(() => {
|
|
147
|
+
const flash = (tui as { flash?: (message: string) => void }).flash;
|
|
148
|
+
if (typeof flash === "function") flash("Copied!");
|
|
149
|
+
else ctx.ui.notify("Copied preview to clipboard", "info");
|
|
150
|
+
})
|
|
151
|
+
.catch((error: unknown) => {
|
|
152
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Copy failed", "error");
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
111
156
|
const dragScrollbarTo = (mouseRow: number): void => {
|
|
112
157
|
if (!scrollbarHitbox || scrollbarDragOffset === null) return;
|
|
113
158
|
const trackSize = scrollbarHitbox.endRow - scrollbarHitbox.startRow + 1;
|
|
@@ -151,7 +196,14 @@ export async function showTextPreview(
|
|
|
151
196
|
mouse.col >= escHitbox.startCol &&
|
|
152
197
|
mouse.col <= escHitbox.endCol,
|
|
153
198
|
);
|
|
199
|
+
const overCopy = Boolean(
|
|
200
|
+
copyHitbox &&
|
|
201
|
+
mouse.row === copyHitbox.row &&
|
|
202
|
+
mouse.col >= copyHitbox.startCol &&
|
|
203
|
+
mouse.col <= copyHitbox.endCol,
|
|
204
|
+
);
|
|
154
205
|
setEscHovered(overEsc);
|
|
206
|
+
setCopyHovered(overCopy);
|
|
155
207
|
const button = mouseBaseButton(mouse.code);
|
|
156
208
|
const motion = (mouse.code & 32) !== 0;
|
|
157
209
|
if (button === 0 && !motion) {
|
|
@@ -159,6 +211,10 @@ export async function showTextPreview(
|
|
|
159
211
|
done(undefined);
|
|
160
212
|
return;
|
|
161
213
|
}
|
|
214
|
+
if (overCopy) {
|
|
215
|
+
copyPreview();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
162
218
|
if (
|
|
163
219
|
scrollbarHitbox &&
|
|
164
220
|
mouse.col === scrollbarHitbox.col &&
|
|
@@ -187,7 +243,7 @@ export async function showTextPreview(
|
|
|
187
243
|
|
|
188
244
|
render(width: number) {
|
|
189
245
|
const inner = Math.max(1, width - 2);
|
|
190
|
-
const
|
|
246
|
+
const actionsWidth = visibleWidth(`${COPY_LABEL} ${ESC_LABEL}`);
|
|
191
247
|
const bodyInner = Math.max(1, inner - 1);
|
|
192
248
|
const bodyWidth = Math.max(1, bodyInner - 1);
|
|
193
249
|
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
@@ -205,6 +261,7 @@ export async function showTextPreview(
|
|
|
205
261
|
const overlayTop = 2 + Math.floor((availableHeight - viewportHeight) / 2);
|
|
206
262
|
const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
|
|
207
263
|
escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
|
|
264
|
+
copyHitbox = copyButtonHitbox({ left: overlayLeft, top: overlayTop, width });
|
|
208
265
|
const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
|
|
209
266
|
const border = (text: string) => theme.fg("border", text);
|
|
210
267
|
const scrollable = totalLines > pageSize;
|
|
@@ -237,11 +294,11 @@ export async function showTextPreview(
|
|
|
237
294
|
});
|
|
238
295
|
const start = totalLines === 0 ? 0 : scrollOffset + 1;
|
|
239
296
|
const end = Math.min(totalLines, scrollOffset + pageSize);
|
|
240
|
-
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [esc] close`;
|
|
297
|
+
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [copy] · [esc] close`;
|
|
241
298
|
|
|
242
299
|
return [
|
|
243
300
|
border(`╭${"─".repeat(inner)}╮`),
|
|
244
|
-
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner -
|
|
301
|
+
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner - actionsWidth)}${theme.fg(copyHovered ? "text" : "muted", COPY_LABEL)} ${theme.fg(escHovered ? "text" : "muted", ESC_LABEL)}${border("│")}`,
|
|
245
302
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
246
303
|
...bodyRows,
|
|
247
304
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
@@ -273,6 +330,27 @@ const tokenEstimate = (value: unknown): number => {
|
|
|
273
330
|
return Math.max(0, Math.ceil(text.length / 4));
|
|
274
331
|
};
|
|
275
332
|
|
|
333
|
+
/** system 消息对请求的贡献:正文 + 各 section 文本 + 声明的工具 schema,与 pi 的 estimateTokens 同口径。 */
|
|
334
|
+
function systemMessageTokens(message: {
|
|
335
|
+
content?: unknown;
|
|
336
|
+
sections?: Record<string, string | null> | null;
|
|
337
|
+
toolsAdded?: unknown;
|
|
338
|
+
toolsRemoved?: unknown;
|
|
339
|
+
}): number {
|
|
340
|
+
let chars = typeof message.content === "string" ? message.content.length : 0;
|
|
341
|
+
for (const section of Object.values(message.sections ?? {})) {
|
|
342
|
+
if (typeof section === "string") chars += section.length;
|
|
343
|
+
}
|
|
344
|
+
if (message.toolsAdded) chars += JSON.stringify(message.toolsAdded).length;
|
|
345
|
+
if (message.toolsRemoved) chars += JSON.stringify(message.toolsRemoved).length;
|
|
346
|
+
return Math.ceil(chars / 4);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** prompt 组装会把 CRLF 归一成 LF,匹配前对齐行尾。 */
|
|
350
|
+
function normalizeLineEndings(text: string): string {
|
|
351
|
+
return text.replace(/\r\n?/g, "\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
276
354
|
/** 只统计确实嵌进 system prompt 的片段,避免源文件预览把占用加两遍。 */
|
|
277
355
|
function embeddedTokens(prompt: string, chunk: string): number {
|
|
278
356
|
if (!chunk || !prompt.includes(chunk)) return 0;
|
|
@@ -348,24 +426,24 @@ export function collectContextBreakdown(
|
|
|
348
426
|
allTools: ToolInfo[],
|
|
349
427
|
): ContextBreakdown {
|
|
350
428
|
const options = (ctx.getSystemPromptOptions?.() ?? {}) as BuildSystemPromptOptions;
|
|
351
|
-
const systemPrompt = typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "";
|
|
352
429
|
const selectedTools = new Set(options.selectedTools ?? ["read", "bash", "edit", "write"]);
|
|
353
430
|
const toolDefinitionPreview: string[] = [];
|
|
354
431
|
const toolResultPreview: string[] = [];
|
|
355
432
|
const contextPreview: string[] = [];
|
|
433
|
+
const systemPromptPreview: string[] = [];
|
|
356
434
|
let toolDefinitionTokens = 0;
|
|
357
435
|
let toolResultTokens = 0;
|
|
358
436
|
let contextTokens = 0;
|
|
437
|
+
let systemTokens = 0;
|
|
359
438
|
|
|
360
439
|
const memoryPreview: string[] = [];
|
|
361
|
-
|
|
362
|
-
for (const file of
|
|
363
|
-
memoryTokens += embeddedTokens(systemPrompt, file.content);
|
|
440
|
+
const contextFiles = options.contextFiles ?? [];
|
|
441
|
+
for (const file of contextFiles) {
|
|
364
442
|
memoryPreview.push(`## ${file.path}\n\n${previewValue(file.content)}`);
|
|
365
443
|
}
|
|
366
444
|
|
|
367
|
-
const
|
|
368
|
-
const
|
|
445
|
+
const skills = options.skills ?? [];
|
|
446
|
+
const skillsText = formatSkillsForPrompt(skills).trim();
|
|
369
447
|
|
|
370
448
|
for (const tool of allTools) {
|
|
371
449
|
if (!selectedTools.has(tool.name)) continue;
|
|
@@ -378,21 +456,51 @@ export function collectContextBreakdown(
|
|
|
378
456
|
toolDefinitionPreview.push(`## Definition: ${tool.name}\n\n${previewValue(definition)}`);
|
|
379
457
|
}
|
|
380
458
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
459
|
+
let memoryTokens = 0;
|
|
460
|
+
let skillsTokens = 0;
|
|
461
|
+
for (const projected of ctx.sessionManager.buildSessionProjection().entries) {
|
|
462
|
+
const source = projected.sourceEntry;
|
|
463
|
+
for (const message of projected.messages) {
|
|
464
|
+
if (message.role === "system") {
|
|
465
|
+
systemTokens += systemMessageTokens(message);
|
|
466
|
+
// Memory / skills 按投影中的实际 section 文本切分,与 system prompt 增量互补、不重不漏。
|
|
467
|
+
const sections = (message.sections ?? {}) as Record<string, string | null>;
|
|
468
|
+
const projectContext =
|
|
469
|
+
typeof sections.project_context === "string"
|
|
470
|
+
? normalizeLineEndings(sections.project_context)
|
|
471
|
+
: "";
|
|
472
|
+
for (const file of contextFiles) {
|
|
473
|
+
const chunk = `<project_instructions path="${file.path}">\n${normalizeLineEndings(file.content)}\n</project_instructions>`;
|
|
474
|
+
memoryTokens += embeddedTokens(projectContext, chunk);
|
|
475
|
+
systemTokens -= embeddedTokens(projectContext, chunk);
|
|
476
|
+
}
|
|
477
|
+
const skillsSection =
|
|
478
|
+
typeof sections.skills === "string" ? normalizeLineEndings(sections.skills) : "";
|
|
479
|
+
skillsTokens += embeddedTokens(skillsSection, skillsText);
|
|
480
|
+
systemTokens -= embeddedTokens(skillsSection, skillsText);
|
|
481
|
+
systemPromptPreview.push(
|
|
482
|
+
(source.type === "compaction"
|
|
483
|
+
? "## System prompt snapshot at compaction\n\n"
|
|
484
|
+
: "## System prompt update\n\n") +
|
|
485
|
+
previewValue({
|
|
486
|
+
content: message.content,
|
|
487
|
+
sections: message.sections,
|
|
488
|
+
toolsAdded: (message.toolsAdded ?? []).map((tool: { name?: string }) => tool?.name),
|
|
489
|
+
toolsRemoved: (message.toolsRemoved ?? []).map(
|
|
490
|
+
(tool: { name?: string }) => tool?.name,
|
|
491
|
+
),
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
} else if (message.role === "assistant") {
|
|
495
|
+
contextTokens += estimateTokens(message);
|
|
385
496
|
for (const block of message.content) {
|
|
386
497
|
if (block.type === "toolCall") {
|
|
387
|
-
contextTokens += tokenEstimate(block.name) + tokenEstimate(block.arguments);
|
|
388
498
|
contextPreview.push(
|
|
389
499
|
`## Assistant tool call: ${block.name}\n\n${previewValue(block.arguments)}`,
|
|
390
500
|
);
|
|
391
501
|
} else if (block.type === "text") {
|
|
392
|
-
contextTokens += tokenEstimate(block.text);
|
|
393
502
|
contextPreview.push(`## Assistant\n\n${block.text}`);
|
|
394
503
|
} else if (block.type === "thinking") {
|
|
395
|
-
contextTokens += tokenEstimate(block.thinking);
|
|
396
504
|
contextPreview.push(`## Assistant thinking\n\n${block.thinking}`);
|
|
397
505
|
}
|
|
398
506
|
}
|
|
@@ -402,8 +510,8 @@ export function collectContextBreakdown(
|
|
|
402
510
|
`## Result: ${message.toolName}\n\n${previewValue(message.content)}`,
|
|
403
511
|
);
|
|
404
512
|
} else if (message.role === "bashExecution") {
|
|
405
|
-
|
|
406
|
-
|
|
513
|
+
contextTokens += estimateTokens(message);
|
|
514
|
+
contextPreview.push(
|
|
407
515
|
`## Bash\n\nCommand:\n\n${previewValue(message.command)}\n\nOutput:\n\n${previewValue(message.output)}`,
|
|
408
516
|
);
|
|
409
517
|
} else if (message.role === "branchSummary" || message.role === "compactionSummary") {
|
|
@@ -413,18 +521,10 @@ export function collectContextBreakdown(
|
|
|
413
521
|
contextTokens += estimateTokens(message);
|
|
414
522
|
contextPreview.push(`## ${message.role}\n\n${previewValue(message.content)}`);
|
|
415
523
|
}
|
|
416
|
-
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
417
|
-
contextTokens += tokenEstimate(entry.summary);
|
|
418
|
-
contextPreview.push(
|
|
419
|
-
`## ${entry.type === "compaction" ? "Compaction" : "Branch summary"}\n\n${entry.summary}`,
|
|
420
|
-
);
|
|
421
|
-
} else if (entry.type === "custom_message") {
|
|
422
|
-
contextTokens += tokenEstimate(entry.content);
|
|
423
|
-
contextPreview.push(`## Custom: ${entry.customType}\n\n${previewValue(entry.content)}`);
|
|
424
524
|
}
|
|
425
525
|
}
|
|
426
526
|
|
|
427
|
-
|
|
527
|
+
systemTokens = Math.max(0, systemTokens);
|
|
428
528
|
return {
|
|
429
529
|
parts: [
|
|
430
530
|
{ label: "System prompt", tokens: systemTokens, color: "accent" },
|
|
@@ -435,7 +535,10 @@ export function collectContextBreakdown(
|
|
|
435
535
|
{ label: "Context", tokens: contextTokens, color: "warning" },
|
|
436
536
|
] satisfies ContextPart[],
|
|
437
537
|
previews: {
|
|
438
|
-
systemPrompt:
|
|
538
|
+
systemPrompt:
|
|
539
|
+
systemPromptPreview.join("\n\n") ||
|
|
540
|
+
(typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "") ||
|
|
541
|
+
"No system prompt.",
|
|
439
542
|
memoryFiles: memoryPreview.join("\n\n") || "No memory files in context.",
|
|
440
543
|
skills: skillsText || "No skills in context.",
|
|
441
544
|
tools: toolDefinitionPreview.join("\n\n") || "No active tool definitions.",
|
|
@@ -432,6 +432,8 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
432
432
|
});
|
|
433
433
|
|
|
434
434
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
435
|
+
// Pi 1.2.0 custom events have no prompt. Older hosts omit messageType.
|
|
436
|
+
if (!("prompt" in event)) return;
|
|
435
437
|
const referenceIds = extractSessionReferenceIds(event.prompt);
|
|
436
438
|
if (referenceIds.length === 0) return;
|
|
437
439
|
|
|
@@ -208,6 +208,8 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
|
|
|
208
208
|
|
|
209
209
|
// Inject as a request message, not systemPrompt, so the cached prefix stays stable.
|
|
210
210
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
211
|
+
// Pi 1.2.0 custom events have no prompt. Older hosts omit messageType.
|
|
212
|
+
if (!("prompt" in event)) return;
|
|
211
213
|
const agents = getAgents();
|
|
212
214
|
if (agents.length === 0) return;
|
|
213
215
|
|
|
@@ -182,11 +182,23 @@ export function currentTtftMs(): number {
|
|
|
182
182
|
return Math.max(0, firstTokenAt - runStartTime);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/**
|
|
186
|
-
export function
|
|
185
|
+
/** 不足 1 秒用毫秒,否则用一位小数的秒。 */
|
|
186
|
+
export function formatTtft(ttftMs: number): string {
|
|
187
|
+
if (ttftMs < 1000) return `${Math.round(ttftMs)}ms`;
|
|
188
|
+
return `${(ttftMs / 1000).toFixed(1)}s`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `avg 1.2k tok/s (TTFT: 812ms)`;无数据显示空串,未出首 token 或 showTtft 为 false 时无括号段。 */
|
|
192
|
+
export function formatTokenRate(
|
|
193
|
+
rate: number,
|
|
194
|
+
ttftMs = 0,
|
|
195
|
+
mode: TokenRateMode = "average",
|
|
196
|
+
showTtft = true,
|
|
197
|
+
): string {
|
|
187
198
|
if (!Number.isFinite(rate) || rate <= 0) return "";
|
|
188
199
|
const value = rate >= 1000 ? `${(rate / 1000).toFixed(1)}k` : `${Math.round(rate)}`;
|
|
189
|
-
const ttft =
|
|
200
|
+
const ttft =
|
|
201
|
+
showTtft && Number.isFinite(ttftMs) && ttftMs > 0 ? ` (TTFT: ${formatTtft(ttftMs)})` : "";
|
|
190
202
|
const label = mode === "live" ? "live" : "avg";
|
|
191
203
|
return `${label} ${value} tok/s${ttft}`;
|
|
192
204
|
}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { getCompactRunStatusText } from "../../renderer/compact-mode.ts";
|
|
3
|
-
import {
|
|
4
|
-
fullscreenLazyTui,
|
|
5
|
-
getToolMouseTui,
|
|
6
|
-
isFullscreenAtBottom,
|
|
7
|
-
} from "../../renderer/mouse/scroll.ts";
|
|
8
2
|
import { formatDuration, formatTokenCount } from "../../utils/format.ts";
|
|
9
3
|
import {
|
|
10
4
|
resetTokenRate,
|
|
@@ -60,23 +54,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
60
54
|
if (output > 0) recordProviderUsage(output);
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
/**
|
|
64
|
-
* 摘要行滚出视口才镜像:仅 fullscreen 且已离开 transcript 底部。
|
|
65
|
-
* regular 没有“离开底部”信号(transcript 在终端回滚区),保持 Pi 默认文案。
|
|
66
|
-
*/
|
|
67
|
-
function compactMirrorText(): string | undefined {
|
|
68
|
-
const tui = getToolMouseTui();
|
|
69
|
-
if (!tui || !fullscreenLazyTui(tui) || isFullscreenAtBottom(tui)) return undefined;
|
|
70
|
-
return getCompactRunStatusText();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
57
|
function buildWorkingMessage(): string {
|
|
74
58
|
const parts: string[] = [];
|
|
75
59
|
const tokens = tokenCount();
|
|
76
60
|
if (tokens > 0) parts.push(`↓ ${formatTokenCount(tokens)} tokens`);
|
|
77
|
-
// compact 活动回合且已滚出摘要行:直接用摘要行文案(自带回合时长,不叠 agent 计时)。
|
|
78
|
-
const compactStatus = compactMirrorText();
|
|
79
|
-
if (compactStatus) return [compactStatus, ...parts].join(" · ");
|
|
80
61
|
const elapsed = Date.now() - (agentStartTime || turnStartTime);
|
|
81
62
|
if (elapsed >= SHOW_TIMER_AFTER_MS || tokens > 0) {
|
|
82
63
|
// formatDuration 低于 1 秒返回 "",此处回退 "0s" 保持计时器连续跳动。
|
|
@@ -592,6 +592,11 @@ export type CompactModeHooks = {
|
|
|
592
592
|
assertOwnership(): void;
|
|
593
593
|
/** 重新渲染包含指定 toolCallId 的 assistant 消息(思考收尾时刷新)。 */
|
|
594
594
|
refreshToolCallMessage(toolCallId: string | undefined): void;
|
|
595
|
+
/**
|
|
596
|
+
* 收尾 resume 重放留下的活动回合。重放是同步的,回合之间靠下一条消息收口,
|
|
597
|
+
* 只有最后一条没有后续消息,会停在 Running。
|
|
598
|
+
*/
|
|
599
|
+
settleReplayedRound(): void;
|
|
595
600
|
shutdown(): void;
|
|
596
601
|
};
|
|
597
602
|
|
|
@@ -618,17 +623,6 @@ const trackedAssistantComponents = new Set<any>();
|
|
|
618
623
|
const trackedToolComponents = new Set<any>();
|
|
619
624
|
let hoveredAssistantComponent: any;
|
|
620
625
|
|
|
621
|
-
/**
|
|
622
|
-
* 活动回合摘要 getter:常驻状态行(pi working row)镜像用。
|
|
623
|
-
* installCompactMode 注册、卸载时清空,直接读 activeRound,不持有回合闭包。
|
|
624
|
-
*/
|
|
625
|
-
let compactRunStatusGetter: (() => string | undefined) | undefined;
|
|
626
|
-
|
|
627
|
-
/** 活动回合的摘要文本(`Running... · 9s, bash×1`);无活动回合返回 undefined。 */
|
|
628
|
-
export function getCompactRunStatusText(): string | undefined {
|
|
629
|
-
return compactRunStatusGetter?.();
|
|
630
|
-
}
|
|
631
|
-
|
|
632
626
|
/**
|
|
633
627
|
* 回合尾行记录:摘要行(live 时连槽位卡)挂成 transcript 容器里回合末尾的
|
|
634
628
|
* 兄弟组件,而不是 anchor 内部。anchor 内部的可变内容一旦被工具/diff 顶出视口
|
|
@@ -1196,6 +1190,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1196
1190
|
liveSlotToolIds: Set<string>;
|
|
1197
1191
|
/** 收尾后的静默期截止时间戳;期间仍按围观态渲染,到点才收拢。 */
|
|
1198
1192
|
settleUntil?: number;
|
|
1193
|
+
/** 消息已有 toolCall、工具组件还没到:等它一 updateDisplay 就重画槽位。 */
|
|
1194
|
+
pendingLiveTool?: boolean;
|
|
1199
1195
|
foldTimer?: ReturnType<typeof setTimeout>;
|
|
1200
1196
|
/** 回合挂钟起点,保证 Running 时长连续递增。 */
|
|
1201
1197
|
startedAt: number;
|
|
@@ -1357,12 +1353,6 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1357
1353
|
...round.detachedMessages,
|
|
1358
1354
|
];
|
|
1359
1355
|
|
|
1360
|
-
/** 镜像给常驻状态行的文案:只认当前活动回合(摘要行被顶出视口时由 working-message 取用)。 */
|
|
1361
|
-
compactRunStatusGetter = () => {
|
|
1362
|
-
const round = activeRound;
|
|
1363
|
-
return round?.active ? summarize(roundMessages(round), true, round) || undefined : undefined;
|
|
1364
|
-
};
|
|
1365
|
-
|
|
1366
1356
|
const roundToolCallIds = (round: CompactRound): Set<string> => {
|
|
1367
1357
|
const ids = new Set<string>();
|
|
1368
1358
|
for (const message of roundMessages(round)) {
|
|
@@ -1373,6 +1363,26 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1373
1363
|
return ids;
|
|
1374
1364
|
};
|
|
1375
1365
|
|
|
1366
|
+
/**
|
|
1367
|
+
* 摘要行有东西可汇总才挂:普通工具(edit/write/ask 之外)或非空思考。
|
|
1368
|
+
* 只剩外层卡的回合不挂——那些卡各自能展开,空摘要点开只会跳位。
|
|
1369
|
+
*/
|
|
1370
|
+
const roundHasSummaryContent = (round: CompactRound): boolean => {
|
|
1371
|
+
for (const message of roundMessages(round)) {
|
|
1372
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1373
|
+
if (item?.type === "thinking" && String(item.thinking ?? "").trim()) return true;
|
|
1374
|
+
if (
|
|
1375
|
+
item?.type === "toolCall" &&
|
|
1376
|
+
typeof item.name === "string" &&
|
|
1377
|
+
!OUTER_TOOLS.has(item.name)
|
|
1378
|
+
) {
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return false;
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1376
1386
|
/**
|
|
1377
1387
|
* anchor 所在的 transcript 容器(pi chatContainer)。
|
|
1378
1388
|
* 优先走分组补丁在 addChild 时挂的父指针;没有则沿 tui 组件树找直属父容器。
|
|
@@ -1581,10 +1591,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1581
1591
|
const syncTailForTool = (toolCallId: string): void => {
|
|
1582
1592
|
if (!toolCallId) return;
|
|
1583
1593
|
for (const round of [activeRound, ...pendingFoldRounds]) {
|
|
1584
|
-
if (round
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1594
|
+
if (!round || !roundToolCallIds(round).has(toolCallId)) continue;
|
|
1595
|
+
// 槽位是在消息更新时画的,当时工具组件还不存在:组件到了就补上。
|
|
1596
|
+
// 参数流式更新也走这里,槽位卡跟着换内容。
|
|
1597
|
+
if (round.pendingLiveTool || roundLive(round)) renderRound(round);
|
|
1598
|
+
else if (round.tail) syncTailPosition(round);
|
|
1599
|
+
return;
|
|
1588
1600
|
}
|
|
1589
1601
|
};
|
|
1590
1602
|
|
|
@@ -1728,6 +1740,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1728
1740
|
}
|
|
1729
1741
|
let runningTool: any;
|
|
1730
1742
|
let lastTool: any;
|
|
1743
|
+
let missingTool = false;
|
|
1731
1744
|
for (const [, message] of round.messages) {
|
|
1732
1745
|
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1733
1746
|
if (item?.type !== "toolCall" || typeof item.id !== "string") continue;
|
|
@@ -1736,11 +1749,17 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1736
1749
|
if (OUTER_TOOLS.has(String(tool?.toolName ?? item.name ?? ""))) continue;
|
|
1737
1750
|
round.liveSlotToolIds.add(item.id);
|
|
1738
1751
|
liveSlotToolIds.add(item.id);
|
|
1739
|
-
|
|
1752
|
+
// pi 的 message_update 先 updateContent 再建工具组件:这一帧计数已经算上,
|
|
1753
|
+
// 组件要等 updateDisplay 才能进槽位。记下来,组件一到就补画。
|
|
1754
|
+
if (!tool) {
|
|
1755
|
+
missingTool = true;
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1740
1758
|
lastTool = tool;
|
|
1741
1759
|
if (isRunningTool(tool)) runningTool = tool;
|
|
1742
1760
|
}
|
|
1743
1761
|
}
|
|
1762
|
+
round.pendingLiveTool = missingTool;
|
|
1744
1763
|
// 槽位单块:活动思考最新;否则本回合最近的工具(运行中原位增长,完成也留着)。
|
|
1745
1764
|
const slotTool = thinkingKids.length > 0 ? undefined : (runningTool ?? lastTool);
|
|
1746
1765
|
const cardItems: Array<{ child?: any; tool?: any }> = slotTool
|
|
@@ -1795,6 +1814,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1795
1814
|
}
|
|
1796
1815
|
for (const id of round.liveSlotToolIds) liveSlotToolIds.delete(id);
|
|
1797
1816
|
round.liveSlotToolIds.clear();
|
|
1817
|
+
// pendingLiveTool 留给 renderRoundLive 重算:这里清掉的话,组件刚到的那次
|
|
1818
|
+
// 补画会把自己的触发条件抹掉,槽位仍是空的。
|
|
1798
1819
|
|
|
1799
1820
|
for (const [component, message] of round.messages) {
|
|
1800
1821
|
component.lastMessage = message;
|
|
@@ -1816,6 +1837,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1816
1837
|
};
|
|
1817
1838
|
}
|
|
1818
1839
|
|
|
1840
|
+
if (round.anchor.expanded === true) {
|
|
1841
|
+
// 没有可汇总内容的回合不进展开态:摘要行不存在,展开只会把正文搬回回合开头。
|
|
1842
|
+
if (!roundHasSummaryContent(round)) {
|
|
1843
|
+
for (const member of round.messages.keys()) member.expanded = false;
|
|
1844
|
+
round.anchor.expanded = false;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1819
1847
|
if (round.anchor.expanded === true) {
|
|
1820
1848
|
// 展开内容留在摘要行那个位置:回合最后一个成员(含 write/edit)之后。
|
|
1821
1849
|
// 放进 anchor 内部会跳到回合起点,盖到中间的 write/edit 上面。
|
|
@@ -1959,8 +1987,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1959
1987
|
for (const [component, message] of round.messages) {
|
|
1960
1988
|
if (component === round.anchor) {
|
|
1961
1989
|
renderAssistantWithoutThinking(component, message);
|
|
1962
|
-
// 空摘要不挂行(getter 在 Running
|
|
1963
|
-
|
|
1990
|
+
// 空摘要不挂行(getter 在 Running 启动瞬间也可能短暂为空)。
|
|
1991
|
+
// 只剩外层卡的回合同样不挂:摘要行没有可汇总的内容。
|
|
1992
|
+
if ((summary || round.active) && (round.active || roundHasSummaryContent(round))) {
|
|
1964
1993
|
mountRoundTail(round, (identity) => [
|
|
1965
1994
|
compactAssistantLineComponent(identity, getSummary, deps.query),
|
|
1966
1995
|
]);
|
|
@@ -2152,8 +2181,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2152
2181
|
|
|
2153
2182
|
patch.toolInstalledUpdateDisplay = function (this: any) {
|
|
2154
2183
|
const id = String(this.toolCallId ?? "");
|
|
2155
|
-
if (
|
|
2156
|
-
|
|
2184
|
+
if (
|
|
2185
|
+
patch.active &&
|
|
2186
|
+
config.mode === "compact" &&
|
|
2187
|
+
(expandedRoundToolIds.has(id) || liveSlotToolIds.has(id))
|
|
2188
|
+
) {
|
|
2189
|
+
// 用户点开的保留展开;其余(含全局展开、live 槽位接管的)继续强制折叠。
|
|
2190
|
+
// 槽位卡在消息更新时就接管了 id,组件后到时 setExpanded 会把它翻开,外层多出一张卡。
|
|
2157
2191
|
if (this.expanded !== true) explicitRoundToolIds.delete(id);
|
|
2158
2192
|
else if (!explicitRoundToolIds.has(id)) this.expanded = false;
|
|
2159
2193
|
}
|
|
@@ -2202,7 +2236,6 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2202
2236
|
trackedToolComponents.clear();
|
|
2203
2237
|
hoveredAssistantComponent = undefined;
|
|
2204
2238
|
resetRounds();
|
|
2205
|
-
compactRunStatusGetter = undefined;
|
|
2206
2239
|
};
|
|
2207
2240
|
|
|
2208
2241
|
assistantPrototype.updateContent = patch.assistantInstalled;
|
|
@@ -2259,6 +2292,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2259
2292
|
}
|
|
2260
2293
|
}
|
|
2261
2294
|
},
|
|
2295
|
+
settleReplayedRound() {
|
|
2296
|
+
if (!patch.active || config.mode !== "compact") return;
|
|
2297
|
+
const round = activeRound;
|
|
2298
|
+
if (!round) return;
|
|
2299
|
+
finalizeRound(round); // clears activeRound — capture it first
|
|
2300
|
+
renderRound(round);
|
|
2301
|
+
},
|
|
2262
2302
|
shutdown() {
|
|
2263
2303
|
patch.dispose();
|
|
2264
2304
|
},
|
|
@@ -250,6 +250,9 @@ export default function (
|
|
|
250
250
|
// compact-thinking 的 session_start 处理在本 handler 之后执行(在其之上再装
|
|
251
251
|
// 一层 updateContent);延迟再同步一次,保证 compact 补丁最终位于外层。
|
|
252
252
|
setTimeout(() => syncCompactMode(ctx), 0);
|
|
253
|
+
// 历史回放发生在 session_start 之后,延迟到回放结束再收尾最后一个回合,
|
|
254
|
+
// 否则它没有后续消息,摘要行会停在 Running。
|
|
255
|
+
setTimeout(() => compactModeHooks?.settleReplayedRound(), 0);
|
|
253
256
|
scheduleSessionRender(() => hooks.toolGrouping.refresh(getToolMouseTui()));
|
|
254
257
|
});
|
|
255
258
|
|
|
@@ -94,13 +94,13 @@ export async function executeWriteWithMetadata(
|
|
|
94
94
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
95
95
|
throwIfAborted();
|
|
96
96
|
await writeFile(absolutePath, params.content, "utf8");
|
|
97
|
-
|
|
97
|
+
// The file is already changed. A later abort must not report failure.
|
|
98
98
|
store.set(toolCallId, metadata);
|
|
99
99
|
return {
|
|
100
100
|
content: [
|
|
101
101
|
{
|
|
102
102
|
type: "text",
|
|
103
|
-
text: `Successfully wrote ${params.content
|
|
103
|
+
text: `Successfully wrote ${Buffer.byteLength(params.content, "utf8")} bytes to ${params.path}`,
|
|
104
104
|
},
|
|
105
105
|
],
|
|
106
106
|
details: undefined,
|