pi-cc-extensions 0.9.9 → 0.9.11
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 +49 -50
- package/README.md +7 -8
- package/extensions/config/config.ts +48 -8
- package/extensions/config/panel.ts +158 -30
- package/extensions/feature/compact-thinking.ts +19 -16
- package/extensions/feature/shell/footer-currency.ts +29 -0
- package/extensions/feature/shell/footer.ts +52 -7
- package/extensions/feature/shell/mcp-chip.ts +136 -0
- package/extensions/renderer/compact-mode.ts +78 -14
- package/extensions/renderer/default-mode.ts +51 -13
- package/extensions/renderer/mouse/interaction.ts +32 -21
- package/extensions/renderer/tool/codemode.ts +306 -0
- package/extensions/renderer/tool/diff/diff-header.ts +1 -1
- package/extensions/renderer/tool/diff/diff-highlight.ts +36 -0
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/diff/diff-write-render.ts +2 -2
- package/extensions/renderer/tool/diff/line-width-safety.ts +6 -5
- package/extensions/renderer/tool/grouping.ts +31 -6
- package/extensions/renderer/tool/message-display.ts +3 -3
- package/extensions/renderer/tool/names.ts +3 -0
- package/extensions/renderer/tool/result.ts +173 -36
- package/extensions/utils/ansi-text.ts +1 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 渲染副作用(applyStyleMode / refreshCurrentTranscript)由 renderer 经
|
|
5
5
|
* CcstylePanelHooks 注入,避免 config → renderer 循环依赖。
|
|
6
6
|
*/
|
|
7
|
-
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { getSettingsListTheme, type ThemeBg } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Input, SettingsList, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { CompactThinkingController } from "../feature/compact-thinking.ts";
|
|
10
10
|
import {
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
clearCustomFooter,
|
|
13
13
|
getFooterStatusSnapshot,
|
|
14
14
|
} from "../feature/shell/footer.ts";
|
|
15
|
+
import {
|
|
16
|
+
FOOTER_CURRENCY_PRESETS,
|
|
17
|
+
presetFooterCurrencyRate,
|
|
18
|
+
} from "../feature/shell/footer-currency.ts";
|
|
15
19
|
import {
|
|
16
20
|
footerChipDescription,
|
|
17
21
|
footerLineOfKey,
|
|
@@ -33,6 +37,7 @@ import {
|
|
|
33
37
|
DIFF_SPLIT_MIN_WIDTH_VALUES,
|
|
34
38
|
DIFF_VIEW_MODES,
|
|
35
39
|
EXCLUDE_RENDERER_CANDIDATES,
|
|
40
|
+
EXPANDED_CARD_BACKGROUND_SLOTS,
|
|
36
41
|
EXPANDED_INPUT_MAX_LINES_VALUES,
|
|
37
42
|
EXPANDED_OUTPUT_MAX_LINES_VALUES,
|
|
38
43
|
EXPANDED_PREVIEW_MAX_LINES_VALUES,
|
|
@@ -41,7 +46,6 @@ import {
|
|
|
41
46
|
pickInputClip,
|
|
42
47
|
pickPositiveInt,
|
|
43
48
|
pickPositiveNumber,
|
|
44
|
-
SCROLL_STEP_LINES_VALUES,
|
|
45
49
|
THINKING_ANIMATION_INTERVAL_VALUES,
|
|
46
50
|
THINKING_PREVIEW_LINES_VALUES,
|
|
47
51
|
INPUT_CLIP_VALUES,
|
|
@@ -201,6 +205,14 @@ function diffIndicatorDescription(mode: DiffIndicatorMode): string {
|
|
|
201
205
|
return "Vertical bar indicators on changed lines (default).";
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
function expandedCardBackgroundDescription(slot: ThemeBg): string {
|
|
209
|
+
if (slot === "userMessageBg") {
|
|
210
|
+
return "Expanded cards share the user message background. Pick another slot to tell them apart.";
|
|
211
|
+
}
|
|
212
|
+
if (slot === "toolPendingBg") return "Expanded cards use the theme's neutral tool background.";
|
|
213
|
+
return `Expanded cards use the theme's ${slot} background.`;
|
|
214
|
+
}
|
|
215
|
+
|
|
204
216
|
/** 额外功能开关项:on/off 二值,描述随状态切换;切换后需重启生效。 */
|
|
205
217
|
function featureToggleSetting(
|
|
206
218
|
id: string,
|
|
@@ -272,10 +284,22 @@ function buildExcludeRenderersSubmenu(
|
|
|
272
284
|
};
|
|
273
285
|
}
|
|
274
286
|
|
|
275
|
-
|
|
276
|
-
|
|
287
|
+
function footerCurrencyRateLabel(rate: number | null): string {
|
|
288
|
+
return rate === null ? "-" : String(rate);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 手动输入子面板。accept 返回字符串则提交,undefined 取消,null 用 invalid 提示。
|
|
293
|
+
*/
|
|
294
|
+
function buildValueInputSubmenu(
|
|
277
295
|
theme: any,
|
|
278
|
-
|
|
296
|
+
options: {
|
|
297
|
+
prompt: string;
|
|
298
|
+
initial: string;
|
|
299
|
+
hint: string;
|
|
300
|
+
accept: (raw: string) => string | null | undefined;
|
|
301
|
+
invalid?: (raw: string) => string;
|
|
302
|
+
},
|
|
279
303
|
closeSubmenu: (selected?: string) => void,
|
|
280
304
|
): {
|
|
281
305
|
render: (width: number) => string[];
|
|
@@ -284,27 +308,28 @@ function buildNumberInputSubmenu(
|
|
|
284
308
|
} {
|
|
285
309
|
const input = new Input();
|
|
286
310
|
let error = "";
|
|
287
|
-
input.setValue(
|
|
311
|
+
input.setValue(options.initial);
|
|
288
312
|
input.onSubmit = (value: string) => {
|
|
289
313
|
const raw = value.trim();
|
|
290
|
-
|
|
291
|
-
|
|
314
|
+
const next = options.accept(raw);
|
|
315
|
+
if (next === undefined) {
|
|
316
|
+
closeSubmenu();
|
|
292
317
|
return;
|
|
293
318
|
}
|
|
294
|
-
if (
|
|
295
|
-
error =
|
|
319
|
+
if (next === null) {
|
|
320
|
+
error = options.invalid?.(raw) ?? "Invalid value";
|
|
296
321
|
return;
|
|
297
322
|
}
|
|
298
|
-
closeSubmenu(
|
|
323
|
+
closeSubmenu(next);
|
|
299
324
|
};
|
|
300
325
|
input.onEscape = () => closeSubmenu();
|
|
301
326
|
return {
|
|
302
327
|
render: (width: number) => {
|
|
303
328
|
const safe = Math.max(0, Math.floor(width));
|
|
304
329
|
const lines = [
|
|
305
|
-
theme.fg("dim",
|
|
330
|
+
theme.fg("dim", options.prompt),
|
|
306
331
|
...input.render(safe),
|
|
307
|
-
truncateToWidth(theme.fg("dim",
|
|
332
|
+
truncateToWidth(theme.fg("dim", options.hint), safe),
|
|
308
333
|
];
|
|
309
334
|
if (error !== "") lines.push(theme.fg("dim", ` ${error}`));
|
|
310
335
|
return lines;
|
|
@@ -314,6 +339,25 @@ function buildNumberInputSubmenu(
|
|
|
314
339
|
};
|
|
315
340
|
}
|
|
316
341
|
|
|
342
|
+
/** 数值项:预填当前值,空输入取消,非数字留下错误。 */
|
|
343
|
+
function buildNumberInputSubmenu(
|
|
344
|
+
theme: any,
|
|
345
|
+
setting: { label: string; currentValue: string },
|
|
346
|
+
closeSubmenu: (selected?: string) => void,
|
|
347
|
+
) {
|
|
348
|
+
return buildValueInputSubmenu(
|
|
349
|
+
theme,
|
|
350
|
+
{
|
|
351
|
+
prompt: ` ${setting.label} — custom value:`,
|
|
352
|
+
initial: setting.currentValue,
|
|
353
|
+
hint: " Enter to apply · Esc to go back",
|
|
354
|
+
accept: (raw) => (raw === "" ? undefined : Number.isFinite(Number(raw)) ? raw : null),
|
|
355
|
+
invalid: (raw) => `Invalid number: "${raw}"`,
|
|
356
|
+
},
|
|
357
|
+
closeSubmenu,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
317
361
|
/** Section tabs for /ccstyle — matches Zentui-style "A / B / C" headers. */
|
|
318
362
|
type CcstyleSection = {
|
|
319
363
|
id: "style" | "diff" | "thinking" | "ui" | "feature" | "footer";
|
|
@@ -492,6 +536,13 @@ export async function showCcstylePanel(
|
|
|
492
536
|
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
493
537
|
buildNumberInputSubmenu(theme, expandedMaxSetting, closeSubmenu),
|
|
494
538
|
};
|
|
539
|
+
const expandedCardBgSetting = {
|
|
540
|
+
id: "expandedCardBackground",
|
|
541
|
+
label: "Expanded card bg",
|
|
542
|
+
description: expandedCardBackgroundDescription(config.expandedCardBackground),
|
|
543
|
+
currentValue: config.expandedCardBackground,
|
|
544
|
+
values: [...EXPANDED_CARD_BACKGROUND_SLOTS],
|
|
545
|
+
};
|
|
495
546
|
const thinkingTitleSetting = {
|
|
496
547
|
id: "useSummaryTitlesAsThinkingTitle",
|
|
497
548
|
label: "Summary title",
|
|
@@ -535,15 +586,6 @@ export async function showCcstylePanel(
|
|
|
535
586
|
currentValue: config.showStartupHeader ? "on" : "off",
|
|
536
587
|
values: ["on", "off"],
|
|
537
588
|
};
|
|
538
|
-
const scrollStepSetting = {
|
|
539
|
-
id: "scrollStepLines",
|
|
540
|
-
label: "Scroll step",
|
|
541
|
-
description: "Mouse wheel scroll lines in fullscreen mode.",
|
|
542
|
-
currentValue: String(config.scrollStepLines),
|
|
543
|
-
values: [...SCROLL_STEP_LINES_VALUES],
|
|
544
|
-
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
545
|
-
buildNumberInputSubmenu(theme, scrollStepSetting, closeSubmenu),
|
|
546
|
-
};
|
|
547
589
|
|
|
548
590
|
// 额外功能开关:注册于扩展加载期,切换后需重启(/reload)生效。
|
|
549
591
|
const sessionReferenceToggle = featureToggleSetting(
|
|
@@ -588,6 +630,65 @@ export async function showCcstylePanel(
|
|
|
588
630
|
"Aliases disabled.",
|
|
589
631
|
config.enableAliases,
|
|
590
632
|
);
|
|
633
|
+
const footerCurrencySetting = {
|
|
634
|
+
id: "footerCurrency",
|
|
635
|
+
label: "Cost currency",
|
|
636
|
+
description:
|
|
637
|
+
"ISO code for the status-bar cost. Switching currency fills its preset rate. USD clears the rate.",
|
|
638
|
+
currentValue: config.footerCurrency,
|
|
639
|
+
values: [...FOOTER_CURRENCY_PRESETS],
|
|
640
|
+
submenu: (_current: string, closeSubmenu: (selected?: string) => void) => {
|
|
641
|
+
nestedSubmenuOpen = true;
|
|
642
|
+
nestedHint = " Enter to apply · Esc back to Footer";
|
|
643
|
+
return buildValueInputSubmenu(
|
|
644
|
+
theme,
|
|
645
|
+
{
|
|
646
|
+
prompt: " Cost currency — ISO code:",
|
|
647
|
+
initial: footerCurrencySetting.currentValue,
|
|
648
|
+
hint: " Enter to apply · Esc to go back",
|
|
649
|
+
accept: (raw) => (raw === "" ? undefined : raw),
|
|
650
|
+
},
|
|
651
|
+
(selected) => {
|
|
652
|
+
nestedSubmenuOpen = false;
|
|
653
|
+
nestedHint = "";
|
|
654
|
+
closeSubmenu(selected);
|
|
655
|
+
},
|
|
656
|
+
);
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
const footerCurrencyRateSetting = {
|
|
660
|
+
id: "footerCurrencyRate",
|
|
661
|
+
label: "Cost rate",
|
|
662
|
+
description:
|
|
663
|
+
"Display units per 1 USD of cost. Switching currency replaces this. Empty keeps the number and only changes the symbol.",
|
|
664
|
+
currentValue: footerCurrencyRateLabel(config.footerCurrencyRate),
|
|
665
|
+
submenu: (_current: string, closeSubmenu: (selected?: string) => void) => {
|
|
666
|
+
nestedSubmenuOpen = true;
|
|
667
|
+
nestedHint = " Enter to apply · empty clears · Esc back to Footer";
|
|
668
|
+
return buildValueInputSubmenu(
|
|
669
|
+
theme,
|
|
670
|
+
{
|
|
671
|
+
prompt: " Cost rate — display units per 1 cost unit:",
|
|
672
|
+
initial:
|
|
673
|
+
footerCurrencyRateSetting.currentValue === "-"
|
|
674
|
+
? ""
|
|
675
|
+
: footerCurrencyRateSetting.currentValue,
|
|
676
|
+
hint: " Enter to apply · empty clears conversion · Esc to go back",
|
|
677
|
+
accept: (raw) => {
|
|
678
|
+
if (raw === "" || raw === "-") return "-";
|
|
679
|
+
const n = Number(raw);
|
|
680
|
+
return Number.isFinite(n) && n > 0 ? raw : null;
|
|
681
|
+
},
|
|
682
|
+
invalid: () => "Rate must be a positive number",
|
|
683
|
+
},
|
|
684
|
+
(selected) => {
|
|
685
|
+
nestedSubmenuOpen = false;
|
|
686
|
+
nestedHint = "";
|
|
687
|
+
closeSubmenu(selected);
|
|
688
|
+
},
|
|
689
|
+
);
|
|
690
|
+
},
|
|
691
|
+
};
|
|
591
692
|
const footerNerdIconsSetting = {
|
|
592
693
|
id: "footerNerdIcons",
|
|
593
694
|
label: "Nerd Font icons",
|
|
@@ -647,6 +748,27 @@ export async function showCcstylePanel(
|
|
|
647
748
|
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
648
749
|
return;
|
|
649
750
|
}
|
|
751
|
+
if (id === "footerCurrency") {
|
|
752
|
+
updateConfig({
|
|
753
|
+
footerCurrency: value,
|
|
754
|
+
footerCurrencyRate: presetFooterCurrencyRate(value),
|
|
755
|
+
});
|
|
756
|
+
footerCurrencySetting.currentValue = config.footerCurrency;
|
|
757
|
+
footerCurrencyRateSetting.currentValue = footerCurrencyRateLabel(config.footerCurrencyRate);
|
|
758
|
+
if (config.enableCustomFooter) applyCustomFooter(ctx);
|
|
759
|
+
ctx.ui.notify(
|
|
760
|
+
`Updated ${id}: ${config.footerCurrency} · ${footerCurrencyRateSetting.currentValue}`,
|
|
761
|
+
"info",
|
|
762
|
+
);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (id === "footerCurrencyRate") {
|
|
766
|
+
updateConfig({ footerCurrencyRate: value === "-" ? null : Number(value) });
|
|
767
|
+
footerCurrencyRateSetting.currentValue = footerCurrencyRateLabel(config.footerCurrencyRate);
|
|
768
|
+
if (config.enableCustomFooter) applyCustomFooter(ctx);
|
|
769
|
+
ctx.ui.notify(`Updated ${id}: ${footerCurrencyRateSetting.currentValue}`, "info");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
650
772
|
if (id === "footerNerdIcons") {
|
|
651
773
|
const enabled = value === "on";
|
|
652
774
|
updateConfig({ footerNerdIcons: enabled });
|
|
@@ -759,6 +881,12 @@ export async function showCcstylePanel(
|
|
|
759
881
|
});
|
|
760
882
|
expandedMaxSetting.currentValue = String(config.expandedPreviewMaxLines);
|
|
761
883
|
break;
|
|
884
|
+
case "expandedCardBackground":
|
|
885
|
+
updateConfig({ expandedCardBackground: value as ThemeBg });
|
|
886
|
+
expandedCardBgSetting.description = expandedCardBackgroundDescription(
|
|
887
|
+
config.expandedCardBackground,
|
|
888
|
+
);
|
|
889
|
+
break;
|
|
762
890
|
case "useSummaryTitlesAsThinkingTitle":
|
|
763
891
|
updateConfig({ useSummaryTitlesAsThinkingTitle: value === "on" });
|
|
764
892
|
break;
|
|
@@ -788,12 +916,6 @@ export async function showCcstylePanel(
|
|
|
788
916
|
else clearStartupHeader(ctx);
|
|
789
917
|
break;
|
|
790
918
|
}
|
|
791
|
-
case "scrollStepLines":
|
|
792
|
-
updateConfig({
|
|
793
|
-
scrollStepLines: pickPositiveInt(value, DEFAULT_CONFIG.scrollStepLines, 1, 50),
|
|
794
|
-
});
|
|
795
|
-
scrollStepSetting.currentValue = String(config.scrollStepLines);
|
|
796
|
-
break;
|
|
797
919
|
default:
|
|
798
920
|
return;
|
|
799
921
|
}
|
|
@@ -827,9 +949,9 @@ export async function showCcstylePanel(
|
|
|
827
949
|
expandedInputSetting,
|
|
828
950
|
expandedOutputSetting,
|
|
829
951
|
expandedMaxSetting,
|
|
952
|
+
expandedCardBgSetting,
|
|
830
953
|
inputClipSetting,
|
|
831
954
|
startupHeaderSetting,
|
|
832
|
-
scrollStepSetting,
|
|
833
955
|
],
|
|
834
956
|
},
|
|
835
957
|
{
|
|
@@ -857,7 +979,13 @@ export async function showCcstylePanel(
|
|
|
857
979
|
{
|
|
858
980
|
id: "footer",
|
|
859
981
|
label: "Footer",
|
|
860
|
-
items: [
|
|
982
|
+
items: [
|
|
983
|
+
customFooterSetting,
|
|
984
|
+
footerCurrencySetting,
|
|
985
|
+
footerCurrencyRateSetting,
|
|
986
|
+
footerNerdIconsSetting,
|
|
987
|
+
pluginChipsSetting,
|
|
988
|
+
],
|
|
861
989
|
},
|
|
862
990
|
];
|
|
863
991
|
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
wrapTextWithAnsi,
|
|
19
19
|
} from "@earendil-works/pi-tui";
|
|
20
20
|
import { Box, Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
21
|
+
// 展开卡背景是 ccstyle 的渲染约定,不随上游 CompactThinkingConfig 走,直接读全局配置。
|
|
22
|
+
import { config as styleConfig } from "../config/config.ts";
|
|
21
23
|
import { isToolTuiFullscreen } from "../renderer/tool/show-more-hint.ts";
|
|
22
24
|
import {
|
|
23
25
|
animateCompactThinkingText,
|
|
@@ -309,19 +311,20 @@ function hiddenPreviewHint(
|
|
|
309
311
|
const action = thinkingExpandAction() ?? "";
|
|
310
312
|
if (hiddenLines > 0) {
|
|
311
313
|
const noun = hiddenLines === 1 ? "line" : "lines";
|
|
314
|
+
// 与其它提示同一套写法:小圆点连接,不用逗号和括号
|
|
312
315
|
return {
|
|
313
|
-
prefix: `
|
|
316
|
+
prefix: ` · ${hiddenLines} more ${noun}${action ? " · " : ""}`,
|
|
314
317
|
action,
|
|
315
|
-
suffix: "
|
|
318
|
+
suffix: "",
|
|
316
319
|
};
|
|
317
320
|
}
|
|
318
321
|
if (forceExpandHint && isToolTuiFullscreen()) {
|
|
319
|
-
return { prefix: "
|
|
322
|
+
return { prefix: " · ", action: "click to show more", suffix: "" };
|
|
320
323
|
}
|
|
321
324
|
return undefined;
|
|
322
325
|
}
|
|
323
326
|
|
|
324
|
-
const expandedThinking = new Set<
|
|
327
|
+
const expandedThinking = new Set<string>();
|
|
325
328
|
|
|
326
329
|
/** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、展开卡单击整块收起(拖动选择文本时不收起)。 */
|
|
327
330
|
export class ThinkingPreviewBlock implements Component {
|
|
@@ -332,7 +335,6 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
332
335
|
readonly runStartIndex: number;
|
|
333
336
|
private style: (text: string) => string;
|
|
334
337
|
private theme: Theme | undefined;
|
|
335
|
-
private _expanded: boolean;
|
|
336
338
|
private hintHovered = false;
|
|
337
339
|
private collapsedMemo:
|
|
338
340
|
| {
|
|
@@ -360,7 +362,6 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
360
362
|
this.runStartIndex = runStartIndex;
|
|
361
363
|
this.style = style;
|
|
362
364
|
this.theme = theme;
|
|
363
|
-
this._expanded = expandedThinking.has(messageTimestamp);
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
private paint(color: string, text: string): string {
|
|
@@ -369,18 +370,20 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
369
370
|
: text;
|
|
370
371
|
}
|
|
371
372
|
|
|
373
|
+
/** 展开态按消息时间戳 + run 序号共享:面板重排/内容重建换掉实例后,
|
|
374
|
+
* 布局里的旧实例与 render 的新实例同键,点谁都能展开。 */
|
|
372
375
|
get expanded(): boolean {
|
|
373
|
-
return this.
|
|
376
|
+
return expandedThinking.has(`${this.messageTimestamp}:${this.runStartIndex}`);
|
|
374
377
|
}
|
|
375
378
|
|
|
376
379
|
setExpanded(expanded: boolean): void {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (expanded) expandedThinking.add(this.messageTimestamp);
|
|
380
|
+
const key = `${this.messageTimestamp}:${this.runStartIndex}`;
|
|
381
|
+
if (expanded) expandedThinking.add(key);
|
|
380
382
|
else {
|
|
381
|
-
expandedThinking.delete(
|
|
383
|
+
expandedThinking.delete(key);
|
|
382
384
|
evictExpandedWraps(this.messageTimestamp, this.runStartIndex);
|
|
383
385
|
}
|
|
386
|
+
this.collapsedMemo = undefined;
|
|
384
387
|
}
|
|
385
388
|
|
|
386
389
|
setHintHovered(hovered: boolean): void {
|
|
@@ -389,7 +392,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
389
392
|
}
|
|
390
393
|
|
|
391
394
|
private headingLines(width: number, hiddenLines: number, padding: number): string[] {
|
|
392
|
-
const hint = this.
|
|
395
|
+
const hint = this.expanded
|
|
393
396
|
? undefined
|
|
394
397
|
: hiddenPreviewHint(hiddenLines, Boolean(this.text) && config.previewLines <= 0);
|
|
395
398
|
if (!hint) return new Text(this.heading, padding, 0).render(width);
|
|
@@ -409,10 +412,10 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
409
412
|
}
|
|
410
413
|
|
|
411
414
|
private bodyLines(width: number, padding: number): { lines: string[]; hiddenLines: number } {
|
|
412
|
-
if (!this.text || (config.previewLines <= 0 && !this.
|
|
415
|
+
if (!this.text || (config.previewLines <= 0 && !this.expanded)) {
|
|
413
416
|
return { lines: [], hiddenLines: 0 };
|
|
414
417
|
}
|
|
415
|
-
if (this.
|
|
418
|
+
if (this.expanded) {
|
|
416
419
|
return {
|
|
417
420
|
lines: wrapExpandedThinking(this.messageTimestamp, this.runStartIndex, this.text, width),
|
|
418
421
|
hiddenLines: 0,
|
|
@@ -423,7 +426,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
423
426
|
}
|
|
424
427
|
|
|
425
428
|
render(width: number) {
|
|
426
|
-
if (this.
|
|
429
|
+
if (this.expanded) {
|
|
427
430
|
const innerWidth = Math.max(1, width - 2);
|
|
428
431
|
const body = this.bodyLines(innerWidth, 0);
|
|
429
432
|
const heading = this.headingLines(innerWidth, 0, 0);
|
|
@@ -432,7 +435,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
432
435
|
: heading;
|
|
433
436
|
const bgFn =
|
|
434
437
|
this.theme && typeof this.theme.bg === "function"
|
|
435
|
-
? (text: string) => this.theme!.bg(
|
|
438
|
+
? (text: string) => this.theme!.bg(styleConfig.expandedCardBackground, text)
|
|
436
439
|
: undefined;
|
|
437
440
|
const box = new Box(1, 1, bgFn);
|
|
438
441
|
box.addChild({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const KNOWN_CURRENCIES = new Set(Intl.supportedValuesOf("currency"));
|
|
2
|
+
|
|
3
|
+
/** 1 美元费用对应的显示币种数量。面板切换币种时写入。 */
|
|
4
|
+
export const FOOTER_CURRENCY_RATES = {
|
|
5
|
+
CNY: 7.12,
|
|
6
|
+
EUR: 0.92,
|
|
7
|
+
JPY: 150,
|
|
8
|
+
GBP: 0.76,
|
|
9
|
+
INR: 96.25,
|
|
10
|
+
HKD: 7.78,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export const FOOTER_CURRENCY_PRESETS = ["USD", ...Object.keys(FOOTER_CURRENCY_RATES)] as const;
|
|
14
|
+
|
|
15
|
+
/** 不在表里的币种,包括 USD,返回 null。 */
|
|
16
|
+
export function presetFooterCurrencyRate(currency: string): number | null {
|
|
17
|
+
const code = currency.trim().toUpperCase();
|
|
18
|
+
return FOOTER_CURRENCY_RATES[code as keyof typeof FOOTER_CURRENCY_RATES] ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 底栏费用文本。USD 或不支持的币种保持美元写法。没有倍率时只换符号。 */
|
|
22
|
+
export function formatFooterCost(cost: number, currency: string, rate: number | null): string {
|
|
23
|
+
const usd = `$${cost.toFixed(2)}`;
|
|
24
|
+
if (currency === "USD" || !KNOWN_CURRENCIES.has(currency)) return usd;
|
|
25
|
+
const formatted = new Intl.NumberFormat("en", { style: "currency", currency }).format(
|
|
26
|
+
rate === null ? cost : cost * rate,
|
|
27
|
+
);
|
|
28
|
+
return rate === null ? formatted : `≈${formatted}`;
|
|
29
|
+
}
|
|
@@ -21,6 +21,7 @@ import { join } from "node:path";
|
|
|
21
21
|
import { pathToFileURL } from "node:url";
|
|
22
22
|
import { config } from "../../config/config.ts";
|
|
23
23
|
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
24
|
+
import { formatFooterCost } from "./footer-currency.ts";
|
|
24
25
|
import {
|
|
25
26
|
PI_USAGE_KEY,
|
|
26
27
|
isSkippedFooterStatusKey,
|
|
@@ -28,18 +29,24 @@ import {
|
|
|
28
29
|
visibleFooterPluginTexts,
|
|
29
30
|
type FooterChipLayout,
|
|
30
31
|
} from "./footer-layout.ts";
|
|
32
|
+
import { MCP_STATUS_KEY, buildMcpChip } from "./mcp-chip.ts";
|
|
31
33
|
|
|
32
34
|
const GIT_REFRESH_INTERVAL_MS = 10_000;
|
|
33
35
|
const USAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
36
|
+
// 后台连接 MCP 服务器不会触发 footer 重绘,靠这个兜底轮询把芯片刷出来
|
|
37
|
+
const MCP_REFRESH_INTERVAL_MS = 5_000;
|
|
34
38
|
const USAGE_TIMEOUT_MS = 15_000;
|
|
35
39
|
// zentui NERD_DEFAULT_ICONS:git / cacheHit。无 Nerd Font 时留空,只保留文字。
|
|
36
40
|
export const FOOTER_NERD_ICON_GIT = "";
|
|
37
41
|
export const FOOTER_NERD_ICON_CACHE = "";
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
// md power-plug:MCP 服务器连接数
|
|
44
|
+
export const FOOTER_NERD_ICON_MCP = "\u{f06a5}";
|
|
45
|
+
|
|
46
|
+
export function footerGlyphs(nerdIcons: boolean): { git: string; cache: string; mcp: string } {
|
|
40
47
|
return nerdIcons
|
|
41
|
-
? { git: FOOTER_NERD_ICON_GIT, cache: FOOTER_NERD_ICON_CACHE }
|
|
42
|
-
: { git: "", cache: "" };
|
|
48
|
+
? { git: FOOTER_NERD_ICON_GIT, cache: FOOTER_NERD_ICON_CACHE, mcp: FOOTER_NERD_ICON_MCP }
|
|
49
|
+
: { git: "", cache: "", mcp: "" };
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
type GitStats = { add: number; del: number };
|
|
@@ -97,8 +104,15 @@ export function formatXaiFooterChip(report: XaiFooterReport): string | undefined
|
|
|
97
104
|
|
|
98
105
|
const cachedExtensionStatuses = new Map<string, string>();
|
|
99
106
|
let cachedLocalUsageText = "";
|
|
107
|
+
/** 本包推算的内置 MCP 芯片(含 nerd 图标);空字符串表示不显示。 */
|
|
108
|
+
let cachedMcpChip = "";
|
|
109
|
+
|
|
110
|
+
/** 别的扩展(pi-mcp-adapter)自己写了 mcp 状态时不画,避免两个芯片。 */
|
|
111
|
+
function ownMcpChip(): string {
|
|
112
|
+
return cachedMcpChip && !cachedExtensionStatuses.has(MCP_STATUS_KEY) ? cachedMcpChip : "";
|
|
113
|
+
}
|
|
100
114
|
|
|
101
|
-
/** 面板用:当前 setStatus 文案 + 本包 pi-usage(无文案时为空字符串)。 */
|
|
115
|
+
/** 面板用:当前 setStatus 文案 + 本包 pi-usage / MCP(无文案时为空字符串)。 */
|
|
102
116
|
export function getFooterStatusSnapshot(): Map<string, string> {
|
|
103
117
|
const out = new Map<string, string>();
|
|
104
118
|
for (const [key, text] of cachedExtensionStatuses) {
|
|
@@ -106,6 +120,8 @@ export function getFooterStatusSnapshot(): Map<string, string> {
|
|
|
106
120
|
out.set(key, text);
|
|
107
121
|
}
|
|
108
122
|
out.set(PI_USAGE_KEY, cachedLocalUsageText);
|
|
123
|
+
const mcpChip = ownMcpChip();
|
|
124
|
+
if (mcpChip) out.set(MCP_STATUS_KEY, mcpChip);
|
|
109
125
|
return out;
|
|
110
126
|
}
|
|
111
127
|
|
|
@@ -167,6 +183,9 @@ function colorUsageChip(theme: any, text: string): string {
|
|
|
167
183
|
let currentTui: any = undefined;
|
|
168
184
|
let refreshCurrentGitStats: (() => void) | undefined;
|
|
169
185
|
let refreshCurrentUsage: (() => void) | undefined;
|
|
186
|
+
let refreshCurrentMcpChip: (() => void) | undefined;
|
|
187
|
+
/** 工具/命令列表在 pi API 上,而 footer 工厂只拿得到 ctx,加载时记下来。 */
|
|
188
|
+
let extensionApi: Pick<ExtensionAPI, "getAllTools" | "getCommands"> | undefined;
|
|
170
189
|
|
|
171
190
|
const createCustomFooterFactory =
|
|
172
191
|
(ctx: ExtensionContext) => (tui: any, theme: any, footerData: any) => {
|
|
@@ -281,6 +300,21 @@ const createCustomFooterFactory =
|
|
|
281
300
|
const usageRefreshTimer = setInterval(refreshUsage, USAGE_REFRESH_INTERVAL_MS);
|
|
282
301
|
usageRefreshTimer.unref?.();
|
|
283
302
|
|
|
303
|
+
// 内置 MCP 的状态不经 setStatus 暴露,只能轮询 pi API 与 mcp.json 推算
|
|
304
|
+
const refreshMcp = () => {
|
|
305
|
+
if (disposed) return;
|
|
306
|
+
const chip = buildMcpChip(ctx, extensionApi);
|
|
307
|
+
const glyph = footerGlyphs(config.footerNerdIcons).mcp;
|
|
308
|
+
const next = chip ? `${glyph ? `${glyph} ` : ""}${chip}` : "";
|
|
309
|
+
if (cachedMcpChip === next) return;
|
|
310
|
+
cachedMcpChip = next;
|
|
311
|
+
tui.requestRender();
|
|
312
|
+
};
|
|
313
|
+
refreshCurrentMcpChip = refreshMcp;
|
|
314
|
+
refreshMcp();
|
|
315
|
+
const mcpRefreshTimer = setInterval(refreshMcp, MCP_REFRESH_INTERVAL_MS);
|
|
316
|
+
mcpRefreshTimer.unref?.();
|
|
317
|
+
|
|
284
318
|
// 分支变化时同步更新分支名和相对 HEAD 的统计。
|
|
285
319
|
const unsubBranch = footerData.onBranchChange(() => {
|
|
286
320
|
refreshGitStats();
|
|
@@ -359,6 +393,8 @@ const createCustomFooterFactory =
|
|
|
359
393
|
footerData.getExtensionStatuses().entries() as Iterable<[string, string]>,
|
|
360
394
|
);
|
|
361
395
|
const pluginTexts = pluginTextsForRender(localUsageChip);
|
|
396
|
+
const mcpChip = ownMcpChip();
|
|
397
|
+
if (mcpChip) pluginTexts.set(MCP_STATUS_KEY, mcpChip);
|
|
362
398
|
const layout = resolveFooterChipLayout(layoutFromConfig(), [...pluginTexts.keys()]);
|
|
363
399
|
const dimPlugin = (text: string) => colorUsageChip(theme, text);
|
|
364
400
|
const line1Plugins = visibleFooterPluginTexts(
|
|
@@ -384,8 +420,10 @@ const createCustomFooterFactory =
|
|
|
384
420
|
cachePct > 0 ? `${glyphs.cache ? `${glyphs.cache} ` : ""}${Math.floor(cachePct)}%` : "";
|
|
385
421
|
const costChip =
|
|
386
422
|
cost || usingSubscription
|
|
387
|
-
? theme.fg(
|
|
388
|
-
|
|
423
|
+
? theme.fg(
|
|
424
|
+
"dim",
|
|
425
|
+
formatFooterCost(cost, config.footerCurrency, config.footerCurrencyRate),
|
|
426
|
+
) + (usingSubscription ? theme.fg("warning", " sub") : "")
|
|
389
427
|
: "";
|
|
390
428
|
const line1 = joinChips([
|
|
391
429
|
theme.fg("accent", modelLabel),
|
|
@@ -436,11 +474,13 @@ const createCustomFooterFactory =
|
|
|
436
474
|
usageAbort = undefined;
|
|
437
475
|
clearInterval(gitRefreshTimer);
|
|
438
476
|
clearInterval(usageRefreshTimer);
|
|
477
|
+
clearInterval(mcpRefreshTimer);
|
|
439
478
|
unsubBranch();
|
|
440
479
|
if (currentTui === tui) {
|
|
441
480
|
currentTui = undefined;
|
|
442
481
|
refreshCurrentGitStats = undefined;
|
|
443
482
|
refreshCurrentUsage = undefined;
|
|
483
|
+
refreshCurrentMcpChip = undefined;
|
|
444
484
|
}
|
|
445
485
|
},
|
|
446
486
|
};
|
|
@@ -463,6 +503,7 @@ export function clearCustomFooter(ctx: ExtensionContext): void {
|
|
|
463
503
|
}
|
|
464
504
|
|
|
465
505
|
export default function (pi: ExtensionAPI) {
|
|
506
|
+
extensionApi = pi;
|
|
466
507
|
// 模型/思考级别变化时强制重渲染(自定义 footer 不会被内置 invalidate() 触达)
|
|
467
508
|
pi.on("model_select", () => {
|
|
468
509
|
currentTui?.requestRender();
|
|
@@ -470,7 +511,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
470
511
|
});
|
|
471
512
|
pi.on("thinking_level_select", () => currentTui?.requestRender());
|
|
472
513
|
// 工具执行完成后立即刷新;定时器只负责兜底捕获外部文件变化。
|
|
473
|
-
pi.on("tool_execution_end", () =>
|
|
514
|
+
pi.on("tool_execution_end", () => {
|
|
515
|
+
refreshCurrentGitStats?.();
|
|
516
|
+
// 懒连接的 MCP 服务器可能刚被调用过
|
|
517
|
+
refreshCurrentMcpChip?.();
|
|
518
|
+
});
|
|
474
519
|
|
|
475
520
|
// 启动 / /reload / 新建会话 时按配置恢复
|
|
476
521
|
pi.on("session_start", (_event, ctx) => {
|