pi-cc-extensions 1.0.14 → 1.0.16
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 +5 -4
- package/README.md +5 -4
- package/extensions/config/panel.ts +4 -4
- package/extensions/feature/compact-thinking.ts +6 -0
- package/extensions/feature/shell/footer.ts +22 -4
- package/extensions/feature/shell/session-tokens.ts +3 -3
- package/extensions/renderer/compact-mode.ts +22 -2
- package/extensions/renderer/index.ts +5 -0
- package/extensions/renderer/mouse/interaction.ts +33 -5
- package/extensions/renderer/tool/ccstyle-tool.ts +80 -20
- package/extensions/renderer/tool/codemode.ts +2 -1
- package/extensions/renderer/tool/diff/diff-header.ts +4 -4
- package/extensions/renderer/tool/diff/diff-layout.ts +6 -6
- package/extensions/renderer/tool/diff/diff-palette.ts +4 -4
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/grouping.ts +10 -2
- package/extensions/renderer/tool/tool-io.ts +19 -9
- package/extensions/renderer/transcript-mode.ts +457 -0
- package/extensions/renderer/transcript-scope.ts +44 -0
- package/package.json +4 -2
- package/themes/cc-dark-hc.json +90 -0
- package/themes/cc-dark.json +3 -5
- package/themes/cc-light-hc.json +90 -0
- package/themes/cc-light.json +3 -5
package/README.en.md
CHANGED
|
@@ -32,9 +32,10 @@ Run `/reload` after installation.
|
|
|
32
32
|
| Fullscreen mode | Tool card/group expand and collapse on click, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
|
|
33
33
|
| Settings panel | `Style / Features / UI / Diff / Thinking / Footer` tabs | `/ccstyle` |
|
|
34
34
|
| Context inspection | Usage breakdown and previews for the system prompt, memory, skills, tools definition, and messages | `/context` |
|
|
35
|
+
| Transcript browsing | Full-screen transcript browsing with every tool output expanded | `Ctrl+O` |
|
|
35
36
|
| Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
|
|
36
|
-
| Status bar | Shows model, context, cache, cost, the conversation token total, and git; also works with `@narumitw/pi-usage` for live quota. Git/cache Nerd Font icons can be turned off | `/ccstyle` |
|
|
37
|
-
| Theme | Bundled CC Dark
|
|
37
|
+
| Status bar | Shows model, context, cache, cost, the conversation token total, and git; also works with `@narumitw/pi-usage` for live quota. Git/cache/token Nerd Font icons can be turned off | `/ccstyle` |
|
|
38
|
+
| Theme | Bundled themes: CC Dark, CC Light, CC Dark HC, CC Light HC | `/settings` → Theme |
|
|
38
39
|
|
|
39
40
|
Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-examples-default.md) · [compact](./docs/tool-render-examples-compact.md)
|
|
40
41
|
|
|
@@ -86,10 +87,10 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
86
87
|
"footerTokenRate": true, // output rate chip; false hides it
|
|
87
88
|
"footerTokenRateMode": "average", // average: whole-prompt avg; live: trailing 1s rate
|
|
88
89
|
"footerTokenRateTtft": true, // append TTFT to the rate chip; false shows tok/s only
|
|
89
|
-
"footerSessionTokens": false, // conversation token total chip (
|
|
90
|
+
"footerSessionTokens": false, // conversation token total chip (Nerd Font glyph, Σ in plain text); sums usage.totalTokens from session entries. Off by default
|
|
90
91
|
"footerCurrency": "USD", // display currency for the footer cost
|
|
91
92
|
"footerCurrencyRate": null, // display units per 1 cost unit; null changes the symbol only
|
|
92
|
-
"footerNerdIcons": true, // Nerd Font glyphs for git/cache; false = plain text
|
|
93
|
+
"footerNerdIcons": true, // Nerd Font glyphs for git/cache/tokens; false = plain text
|
|
93
94
|
"footerGitStatsMode": "working", // working (default): uncommitted; branch: cumulative net changes
|
|
94
95
|
"footerHiddenKeys": [], // hidden chip keys
|
|
95
96
|
"footerLine1Keys": ["model", "context", "cache", "rate", "cost", "tokens", "pi-usage"], // chip order per line; builtins and plugins share one layout
|
package/README.md
CHANGED
|
@@ -32,9 +32,10 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
32
32
|
| Fullscreen mode | 工具卡/group 单击展开与收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
|
|
33
33
|
| 配置面板 | `Style / Features / UI / Diff / Thinking / Footer` 六页签 | `/ccstyle` |
|
|
34
34
|
| 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools definition 和消息内容 | `/context` |
|
|
35
|
+
| 会话记录浏览 | 全屏浏览完整会话记录,工具输出全展开 | `Ctrl+O` |
|
|
35
36
|
| Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
|
|
36
|
-
| 状态栏 | 显示:模型、上下文、缓存、费用、本次对话累计 token、git并适配 @narumitw/pi-usage 实时显示额度;git
|
|
37
|
-
| 主题 |
|
|
37
|
+
| 状态栏 | 显示:模型、上下文、缓存、费用、本次对话累计 token、git并适配 @narumitw/pi-usage 实时显示额度;git/缓存/token 图标可关 Nerd Font | `/ccstyle` |
|
|
38
|
+
| 主题 | 随包提供 4 个主题:CC Dark、CC Light、CC Dark HC、CC Light HC | `/settings` → Theme |
|
|
38
39
|
|
|
39
40
|
`on` / `compact` 两种模式的渲染样例:[默认模式](./docs/tool-render-examples-default.md) · [紧凑模式](./docs/tool-render-examples-compact.md)
|
|
40
41
|
|
|
@@ -86,10 +87,10 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
86
87
|
"footerTokenRate": true, // 输出速率芯片;false 隐藏
|
|
87
88
|
"footerTokenRateMode": "average", // average:整段指令平均(avg);live:最近 1 秒瞬时(live)
|
|
88
89
|
"footerTokenRateTtft": true, // 速率芯片是否带 TTFT;false 只显示 tok/s
|
|
89
|
-
"footerSessionTokens": false, // 本次对话累计 token
|
|
90
|
+
"footerSessionTokens": false, // 本次对话累计 token 芯片(Nerd Font 图标,纯文本为 Σ);累加会话条目的 usage.totalTokens,默认关
|
|
90
91
|
"footerCurrency": "USD", // 底栏费用的显示币种
|
|
91
92
|
"footerCurrencyRate": null, // 1 单位费用换成多少显示币种;null 只换符号、不乘倍率
|
|
92
|
-
"footerNerdIcons": true, // git
|
|
93
|
+
"footerNerdIcons": true, // git/缓存/token 用 Nerd Font 图标;false 为纯文本
|
|
93
94
|
"footerGitStatsMode": "working", // working(默认):未提交改动;branch:分支累计净改动
|
|
94
95
|
"footerHiddenKeys": [], // 隐藏的芯片 key
|
|
95
96
|
"footerLine1Keys": ["model", "context", "cache", "rate", "cost", "tokens", "pi-usage"], // 每行芯片顺序;内置项与插件同一套排布
|
|
@@ -759,8 +759,8 @@ export async function showCcstylePanel(
|
|
|
759
759
|
id: "footerNerdIcons",
|
|
760
760
|
label: "Nerd Font icons",
|
|
761
761
|
description: config.footerNerdIcons
|
|
762
|
-
? "Git and
|
|
763
|
-
: "Git and
|
|
762
|
+
? "Git, cache and token chips use Nerd Font glyphs. Turn off for plain text."
|
|
763
|
+
: "Git, cache and token chips use plain text. No Nerd Font required.",
|
|
764
764
|
currentValue: config.footerNerdIcons ? "on" : "off",
|
|
765
765
|
values: ["on", "off"],
|
|
766
766
|
};
|
|
@@ -937,8 +937,8 @@ export async function showCcstylePanel(
|
|
|
937
937
|
updateConfig({ footerNerdIcons: enabled });
|
|
938
938
|
footerNerdIconsSetting.currentValue = enabled ? "on" : "off";
|
|
939
939
|
footerNerdIconsSetting.description = enabled
|
|
940
|
-
? "Git and
|
|
941
|
-
: "Git and
|
|
940
|
+
? "Git, cache and token chips use Nerd Font glyphs. Turn off for plain text."
|
|
941
|
+
: "Git, cache and token chips use plain text. No Nerd Font required.";
|
|
942
942
|
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
943
943
|
return;
|
|
944
944
|
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
styleCompactThinkingText,
|
|
27
27
|
} from "../renderer/compact-mode.ts";
|
|
28
28
|
import { refreshMountedTranscript } from "../renderer/transcript-refresh.ts";
|
|
29
|
+
import { isTranscriptComponent } from "../renderer/transcript-scope.ts";
|
|
29
30
|
import {
|
|
30
31
|
COMPACT_THINKING_OWNER,
|
|
31
32
|
COMPACT_THINKING_PATCH_KEY,
|
|
@@ -645,6 +646,11 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
645
646
|
message: AssistantMessage,
|
|
646
647
|
isStreaming?: boolean,
|
|
647
648
|
) {
|
|
649
|
+
// 浏览层副本:不改写思考块,也不当作最新消息跟踪。
|
|
650
|
+
if (isTranscriptComponent(this)) {
|
|
651
|
+
originalUpdateContent.call(this, message, isStreaming);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
648
654
|
const component = this as AssistantMessageComponentLike;
|
|
649
655
|
const self = this as unknown as AssistantInternals;
|
|
650
656
|
// isStreaming 丢失 → mermaid 流式误渲染来回闪。
|
|
@@ -59,10 +59,26 @@ export const FOOTER_NERD_ICON_CACHE = "";
|
|
|
59
59
|
// md power-plug:MCP 服务器连接数
|
|
60
60
|
export const FOOTER_NERD_ICON_MCP = "\u{f06a5}";
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
// md sigma:会话累计 token。与缓存、MCP 同属 md 图标,字高一致。
|
|
63
|
+
export const FOOTER_NERD_ICON_TOKENS = "\u{f04a0}";
|
|
64
|
+
// 纯文本用希腊字母 Σ:数学求和号 ∑ 在多数字体里比数字高,排在芯片里突兀。
|
|
65
|
+
// 不留空:只剩数字时看不出是 token 总数。
|
|
66
|
+
export const FOOTER_PLAIN_ICON_TOKENS = "Σ";
|
|
67
|
+
|
|
68
|
+
export function footerGlyphs(nerdIcons: boolean): {
|
|
69
|
+
git: string;
|
|
70
|
+
cache: string;
|
|
71
|
+
mcp: string;
|
|
72
|
+
tokens: string;
|
|
73
|
+
} {
|
|
63
74
|
return nerdIcons
|
|
64
|
-
? {
|
|
65
|
-
|
|
75
|
+
? {
|
|
76
|
+
git: FOOTER_NERD_ICON_GIT,
|
|
77
|
+
cache: FOOTER_NERD_ICON_CACHE,
|
|
78
|
+
mcp: FOOTER_NERD_ICON_MCP,
|
|
79
|
+
tokens: FOOTER_NERD_ICON_TOKENS,
|
|
80
|
+
}
|
|
81
|
+
: { git: "", cache: "", mcp: "", tokens: FOOTER_PLAIN_ICON_TOKENS };
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
/**
|
|
@@ -487,7 +503,9 @@ const createCustomFooterFactory =
|
|
|
487
503
|
const costChip = costLabel
|
|
488
504
|
? theme.fg("dim", costLabel) + (usingSubscription ? theme.fg("warning", " sub") : "")
|
|
489
505
|
: "";
|
|
490
|
-
const tokensLabel = config.footerSessionTokens
|
|
506
|
+
const tokensLabel = config.footerSessionTokens
|
|
507
|
+
? formatSessionTokenTotal(tokens, footerGlyphs(config.footerNerdIcons).tokens)
|
|
508
|
+
: "";
|
|
491
509
|
|
|
492
510
|
const gitColor = theme.getThinkingBorderColor("medium");
|
|
493
511
|
let place = theme.fg("accent", cwdLabel);
|
|
@@ -45,8 +45,8 @@ export function sessionEntryTokens(entry: unknown): number {
|
|
|
45
45
|
return 0;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
export function formatSessionTokenTotal(total: number): string {
|
|
48
|
+
/** 芯片文案:`<glyph> 402k`;没有数据返回空串(不占位置)。 */
|
|
49
|
+
export function formatSessionTokenTotal(total: number, glyph: string): string {
|
|
50
50
|
if (!Number.isFinite(total) || total <= 0) return "";
|
|
51
|
-
return
|
|
51
|
+
return `${glyph} ${formatTokenCount(Math.round(total))}`;
|
|
52
52
|
}
|
|
@@ -60,6 +60,14 @@ import { isToolCallHovered } from "./mouse/hover.ts";
|
|
|
60
60
|
import { getToolMouseTui } from "./mouse/scroll.ts";
|
|
61
61
|
import { expandedBodyComponent, insetComponent } from "./tool/result.ts";
|
|
62
62
|
import { isLiveToolCall, scheduleAnimation } from "./tool/tool-state.ts";
|
|
63
|
+
import { isTranscriptComponent } from "./transcript-scope.ts";
|
|
64
|
+
|
|
65
|
+
const COMPACT_ROUND_TAIL_KEY = Symbol.for("ccstyle.compactRoundTail");
|
|
66
|
+
|
|
67
|
+
/** compact 挂进对话区的回合尾行(摘要行与槽位卡),浏览层跳过它。 */
|
|
68
|
+
export function isCompactRoundTail(value: unknown): boolean {
|
|
69
|
+
return Boolean(value && typeof value === "object" && (value as any)[COMPACT_ROUND_TAIL_KEY]);
|
|
70
|
+
}
|
|
63
71
|
import { renderExpandedToolResult } from "./tool/tool-io.ts";
|
|
64
72
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
65
73
|
import { styleChangeCounts } from "./tool/ccstyle-tool.ts";
|
|
@@ -1479,15 +1487,22 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1479
1487
|
/**
|
|
1480
1488
|
* 回合还有活干:成员消息仍在流式,或本回合的工具还没拿到最终结果。
|
|
1481
1489
|
* 收尾要么按事实等活干完,要么摘要行就会把仍在跑的工具报成 Ran for。
|
|
1490
|
+
* pendingLiveTool 是「toolCall 已写进消息、组件还没挂上」的空窗:流停了也要等它合上。
|
|
1491
|
+
* 只在 renderRoundLive 里按缺组件重算;空闲回放不会再补组件,不因此停在 Running。
|
|
1482
1492
|
*/
|
|
1483
1493
|
const roundHasLiveWork = (round: CompactRound): boolean => {
|
|
1494
|
+
if (round.pendingLiveTool && !sessionIdle()) return true;
|
|
1484
1495
|
for (const component of round.messages.keys()) {
|
|
1485
1496
|
if (component?.isStreaming === true) return true;
|
|
1486
1497
|
}
|
|
1487
1498
|
const ids = roundToolCallIds(round);
|
|
1488
1499
|
if (ids.size === 0) return false;
|
|
1489
1500
|
for (const tool of trackedToolComponents) {
|
|
1490
|
-
|
|
1501
|
+
const id = tool?.toolCallId;
|
|
1502
|
+
if (typeof id !== "string" || !ids.has(id)) continue;
|
|
1503
|
+
if (isRunningTool(tool)) return true;
|
|
1504
|
+
// 组件已挂上、还没 markExecutionStarted:声明期 id 仍在等派发。
|
|
1505
|
+
if (!sessionIdle() && isLiveToolCall(id) && (!tool.result || tool.isPartial === true)) {
|
|
1491
1506
|
return true;
|
|
1492
1507
|
}
|
|
1493
1508
|
}
|
|
@@ -1625,6 +1640,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1625
1640
|
}
|
|
1626
1641
|
const tail: MountedRoundTail = { host: undefined, container: undefined, parts: [], round };
|
|
1627
1642
|
const host: any = {
|
|
1643
|
+
[COMPACT_ROUND_TAIL_KEY]: true,
|
|
1628
1644
|
outputPad: Number(round.anchor?.outputPad) || 0,
|
|
1629
1645
|
children: [],
|
|
1630
1646
|
render: (width: number) => tail.parts.flatMap((part: any) => part?.render?.(width) ?? []),
|
|
@@ -2287,6 +2303,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2287
2303
|
return patch.assistantNative.call(this, message, isStreaming);
|
|
2288
2304
|
}
|
|
2289
2305
|
self.lastMessage = message;
|
|
2306
|
+
// 浏览层副本:不进回合跟踪,按原生方式渲染完整消息。
|
|
2307
|
+
if (isTranscriptComponent(this)) return passThroughAssistant(this, message, isStreaming);
|
|
2290
2308
|
trackedAssistantComponents.add(this);
|
|
2291
2309
|
if (!patch.active || config.mode !== "compact") {
|
|
2292
2310
|
return passThroughAssistant(this, message, isStreaming);
|
|
@@ -2431,7 +2449,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2431
2449
|
};
|
|
2432
2450
|
|
|
2433
2451
|
patch.toolInstalledRender = function (this: any, width: number) {
|
|
2434
|
-
if (!patch.active || config.mode !== "compact") {
|
|
2452
|
+
if (!patch.active || config.mode !== "compact" || isTranscriptComponent(this)) {
|
|
2435
2453
|
return patch.toolOriginalRender.call(this, width);
|
|
2436
2454
|
}
|
|
2437
2455
|
const name = String(this.toolName ?? "");
|
|
@@ -2460,6 +2478,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2460
2478
|
};
|
|
2461
2479
|
|
|
2462
2480
|
patch.toolInstalledUpdateDisplay = function (this: any) {
|
|
2481
|
+
// 浏览层副本:不受回合展开规则约束,也不进组件跟踪。
|
|
2482
|
+
if (isTranscriptComponent(this)) return patch.toolOriginalUpdateDisplay.call(this);
|
|
2463
2483
|
const id = String(this.toolCallId ?? "");
|
|
2464
2484
|
if (
|
|
2465
2485
|
patch.active &&
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
TOOL_MOUSE_DISABLE,
|
|
32
32
|
} from "./mouse/interaction.ts";
|
|
33
33
|
import { getToolMouseTui, noteNewTranscriptItem } from "./mouse/scroll.ts";
|
|
34
|
+
import { installTranscriptMode, teardownTranscriptMode } from "./transcript-mode.ts";
|
|
34
35
|
import {
|
|
35
36
|
disposeStickyPromptPatches,
|
|
36
37
|
installStickyPromptPatches,
|
|
@@ -249,6 +250,8 @@ export default function (
|
|
|
249
250
|
// 鼠标交互独立于渲染层:fullscreen 渲染层让位(hooks undefined)但
|
|
250
251
|
// 工具点击/回到底部适配仍需安装;保持在渲染层安装之后以维持原顺序。
|
|
251
252
|
if (ctx?.mode === "tui" && ctx?.hasUI) installToolMouseInteraction(ctx, mouseOwner);
|
|
253
|
+
// 浏览模式与鼠标交互同为输入监听,跟着一起装。
|
|
254
|
+
installTranscriptMode(ctx);
|
|
252
255
|
if (!hooks) return;
|
|
253
256
|
hooks.toolGrouping.setTheme(ctx.ui.theme);
|
|
254
257
|
setMessageDisplayTheme(ctx.ui.theme);
|
|
@@ -273,6 +276,7 @@ export default function (
|
|
|
273
276
|
// Compaction rebuilds the transcript without session_start. Rebind after
|
|
274
277
|
// other TUI extensions may have replaced the root input dispatcher.
|
|
275
278
|
if (ctx?.mode === "tui" && ctx?.hasUI) installToolMouseInteraction(ctx, mouseOwner);
|
|
279
|
+
installTranscriptMode(ctx);
|
|
276
280
|
if (!hooks) return;
|
|
277
281
|
hooks.toolGrouping.setTheme(ctx.ui.theme);
|
|
278
282
|
setMessageDisplayTheme(ctx.ui.theme);
|
|
@@ -313,6 +317,7 @@ export default function (
|
|
|
313
317
|
// 鼠标交互独立于渲染层:fullscreen 下 installation 为 undefined,
|
|
314
318
|
// 但 onTerminalInput 监听与 handleViewportInput 包装仍需释放。
|
|
315
319
|
teardownToolMouseInteraction(mouseOwner);
|
|
320
|
+
teardownTranscriptMode();
|
|
316
321
|
const current = installation;
|
|
317
322
|
if (!current || !current.defaultMode.isOwner()) return;
|
|
318
323
|
current.defaultMode.shutdown();
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
fullscreenLazyTui,
|
|
50
50
|
hideScrollButton,
|
|
51
51
|
dockScrollButtonActive,
|
|
52
|
+
isFullscreenAtBottom,
|
|
52
53
|
restoreOfficialScrollToEnd,
|
|
53
54
|
restoreNativeScrollIndicatorText,
|
|
54
55
|
syncOfficialScrollToEnd,
|
|
@@ -457,9 +458,9 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
457
458
|
// compact 摘要行自己记展开前的视口(收起时还原跟随),所以它先展开再停跟随:
|
|
458
459
|
// 顺序反了快照会记下「未跟随」,收起时就交不回官方 follow。
|
|
459
460
|
component.setExpanded(true);
|
|
460
|
-
keepViewportOnExpand(tui);
|
|
461
|
+
keepViewportOnExpand(tui, hit);
|
|
461
462
|
} else {
|
|
462
|
-
keepViewportOnExpand(tui);
|
|
463
|
+
keepViewportOnExpand(tui, hit);
|
|
463
464
|
component.setExpanded(true);
|
|
464
465
|
}
|
|
465
466
|
clearPendingCollapsePress();
|
|
@@ -509,13 +510,40 @@ function isSameExpandedRound(other: any, owner: any): boolean {
|
|
|
509
510
|
}
|
|
510
511
|
|
|
511
512
|
/**
|
|
512
|
-
*
|
|
513
|
-
*
|
|
513
|
+
* 点击行在滚动视口里的位置:视口内行号与视口高度。布局树读不到时返回 null。
|
|
514
|
+
* leaf 不带 scrollView,视口是最近的带 scrollView 的祖先 box。
|
|
515
|
+
*/
|
|
516
|
+
function clickedRowInViewport(
|
|
517
|
+
hit: { box: any; localRow: number } | null | undefined,
|
|
518
|
+
): { y: number; height: number } | null {
|
|
519
|
+
if (!hit?.box) return null;
|
|
520
|
+
let scrollBox: any = null;
|
|
521
|
+
for (let current = hit.box; current; current = current.parent) {
|
|
522
|
+
if (current.scrollView) {
|
|
523
|
+
scrollBox = current;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const rect = scrollBox?.rect;
|
|
528
|
+
const height = Number(rect?.height);
|
|
529
|
+
const rowY = Number(hit.box.rect?.y);
|
|
530
|
+
if (!rect || !(height > 0) || !Number.isFinite(rowY)) return null;
|
|
531
|
+
return { y: rowY + hit.localRow - Number(rect.y), height };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* 原地展开时视口怎么走。transcript 跟随底部时,内容变高会把视口拖到底,
|
|
536
|
+
* 点击的行和它上方的外层面板被顶出屏幕:展开前关掉跟随,点击行留在原位。
|
|
514
537
|
* compact 摘要行同样适用:它的展开入口在 setExpanded 之后调用本函数(见调用点)。
|
|
538
|
+
*
|
|
539
|
+
* 例外是点击行已经落在视口下半屏:它下面没有足够空间,冻结只会把展开出来的
|
|
540
|
+
* 内容全推到屏幕外(贴着底部输入框时最常见)。这种位置保持跟随,让新内容顶上来。
|
|
515
541
|
*/
|
|
516
|
-
function keepViewportOnExpand(tui: any): void {
|
|
542
|
+
function keepViewportOnExpand(tui: any, hit?: { box: any; localRow: number } | null): void {
|
|
517
543
|
const view = tui.getPrimaryScrollView?.();
|
|
518
544
|
if (typeof view?.scrollTo !== "function" || typeof view.scrollTop !== "number") return;
|
|
545
|
+
const spot = clickedRowInViewport(hit);
|
|
546
|
+
if (spot && isFullscreenAtBottom(tui) && spot.y >= spot.height / 2) return;
|
|
519
547
|
view.scrollTo(view.scrollTop, { disableFollow: true });
|
|
520
548
|
// 已脱离底部跟随:dock 模式亮出回到底部按钮,新输出到来时仍有入口。
|
|
521
549
|
if (view.isFollowingEnd === false && dockScrollButtonActive()) setScrollButtonVisible(true);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import { config, getToolDisplayConfig, type CompactStyleMode } from "../../config/config.ts";
|
|
7
|
+
import { expandedLineLimit } from "../transcript-scope.ts";
|
|
7
8
|
import { isToolCallHovered } from "../mouse/hover.ts";
|
|
8
9
|
import {
|
|
9
10
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
@@ -126,7 +127,7 @@ function renderExpandedTaskResult(
|
|
|
126
127
|
if (toolName === "TaskList") {
|
|
127
128
|
const tasks = parseTaskList(text);
|
|
128
129
|
if (!tasks.length) return undefined;
|
|
129
|
-
const limit = Math.max(1, config.expandedPreviewMaxLines);
|
|
130
|
+
const limit = Math.max(1, expandedLineLimit(config.expandedPreviewMaxLines));
|
|
130
131
|
const rows = tasks.slice(0, limit).map((task) => {
|
|
131
132
|
const color =
|
|
132
133
|
task.status === "completed"
|
|
@@ -194,6 +195,69 @@ function shellDurationText(toolName: string, context: any, isPartial: boolean):
|
|
|
194
195
|
return elapsed === undefined ? "" : formatToolDuration(elapsed);
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
/**
|
|
199
|
+
* 运行中的展开视图:与结束后同一份 Input / Output。
|
|
200
|
+
* 还没输出时用 Pending… 占位:空正文会被视图画成 Done,像已经跑完了。
|
|
201
|
+
* 纯空白(空格、换行、tab)同样算还没输出;非空正文保留原始缩进,不整体 trim。
|
|
202
|
+
* shell 的已用时间挂到视图尾部;取值函数每帧重算,秒数才跟着动画 tick 走。
|
|
203
|
+
*/
|
|
204
|
+
function renderRunningExpandedResult(toolName: string, result: any, context: any, theme: any): any {
|
|
205
|
+
const text = textFromResult(result);
|
|
206
|
+
const view = renderExpandedToolResult(
|
|
207
|
+
text.trim() ? text : "Pending…",
|
|
208
|
+
theme,
|
|
209
|
+
false,
|
|
210
|
+
context?.lastComponent,
|
|
211
|
+
context?.args,
|
|
212
|
+
context,
|
|
213
|
+
true,
|
|
214
|
+
);
|
|
215
|
+
if (isExpandedToolIoView(view) && SHELL_TOOL_NAMES.has(toolName)) {
|
|
216
|
+
view.setFooter(() => shellDurationText(toolName, context, true) || undefined);
|
|
217
|
+
}
|
|
218
|
+
return view;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 运行中的折叠行:` ↳ Pending… · 12.4s · click to show more`。
|
|
223
|
+
* 提示必须留在行尾:collapsedHintHitbox 只认行尾,没有它这行既点不动也看不出能点。
|
|
224
|
+
* 已用时间在 render() 内取,动画 tick 才能带上新值。
|
|
225
|
+
*/
|
|
226
|
+
function renderRunningCollapsedResult(options: {
|
|
227
|
+
toolName: string;
|
|
228
|
+
context: any;
|
|
229
|
+
theme: any;
|
|
230
|
+
/** ask_user_question 的结果树用 1 格缩进,与它的 ├/└ 对齐。 */
|
|
231
|
+
prefix?: string;
|
|
232
|
+
}): any {
|
|
233
|
+
const { toolName, context, theme, prefix } = options;
|
|
234
|
+
const toolCallId = context?.toolCallId;
|
|
235
|
+
const hintPrefix = theme.fg("dim", " · ");
|
|
236
|
+
const hint = hintPrefix + theme.fg("dim", showMoreHintText());
|
|
237
|
+
const hoveredHint = hintPrefix + theme.fg("text", showMoreHintText());
|
|
238
|
+
let cachedWidth: number | undefined;
|
|
239
|
+
let cachedBody: string | undefined;
|
|
240
|
+
let cachedLine: string | undefined;
|
|
241
|
+
let cachedHoveredLine: string | undefined;
|
|
242
|
+
return {
|
|
243
|
+
render(width: number) {
|
|
244
|
+
const elapsed = shellDurationText(toolName, context, true);
|
|
245
|
+
const body = elapsed ? `Pending… · ${elapsed}` : "Pending…";
|
|
246
|
+
if (cachedLine === undefined || cachedWidth !== width || cachedBody !== body) {
|
|
247
|
+
cachedWidth = width;
|
|
248
|
+
cachedBody = body;
|
|
249
|
+
cachedLine = theme.fg("muted", renderCollapsedToolResultToWidth(body, hint, width, prefix));
|
|
250
|
+
cachedHoveredLine = theme.fg(
|
|
251
|
+
"muted",
|
|
252
|
+
renderCollapsedToolResultToWidth(body, hoveredHint, width, prefix),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return [isToolCallHovered(toolCallId) ? cachedHoveredLine! : cachedLine];
|
|
256
|
+
},
|
|
257
|
+
invalidate(): void {},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
197
261
|
/**
|
|
198
262
|
* Agent 的 renderResult:与 pi-subagents 保持同样的分支,但收敛成 ccstyle 的 ↳ 行。
|
|
199
263
|
* running/partial 转交原渲染器(只有它有 spinner frame 和 activity 文案);
|
|
@@ -389,8 +453,12 @@ export function createCcstyleTool(
|
|
|
389
453
|
cachedWidth = width;
|
|
390
454
|
cachedIcon = icon;
|
|
391
455
|
// 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
|
|
392
|
-
cachedLine =
|
|
393
|
-
|
|
456
|
+
cachedLine = truncateToWidth(
|
|
457
|
+
`${lead}${icon} ${renderToolSummary(summary, mainWidth, theme.fg.bind(theme))}${extraStyled}`,
|
|
458
|
+
viewportWidth,
|
|
459
|
+
"",
|
|
460
|
+
);
|
|
461
|
+
return [cachedLine];
|
|
394
462
|
},
|
|
395
463
|
invalidate() {
|
|
396
464
|
cachedLine = undefined;
|
|
@@ -454,9 +522,11 @@ export function createCcstyleTool(
|
|
|
454
522
|
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
455
523
|
if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");
|
|
456
524
|
if (options?.isPartial) {
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
525
|
+
// 等待回答时也能展开看问题原文(Input 即 args.questions);折叠态 1 格缩进。
|
|
526
|
+
if (expanded) return renderRunningExpandedResult(toolName, result, context, theme);
|
|
527
|
+
// 折叠后清掉展开视图,与通用 partial 折叠一致。
|
|
528
|
+
if (context?.state) context.state.ccstyleIoView = undefined;
|
|
529
|
+
return renderRunningCollapsedResult({ toolName, context, theme, prefix: " ↳ " });
|
|
460
530
|
}
|
|
461
531
|
if (expanded) {
|
|
462
532
|
return renderExpandedToolResult(
|
|
@@ -479,21 +549,11 @@ export function createCcstyleTool(
|
|
|
479
549
|
});
|
|
480
550
|
}
|
|
481
551
|
if (options?.isPartial) {
|
|
552
|
+
// 运行中也给和结束后同一份 Input / Output:命令全文与已产生的输出。
|
|
553
|
+
if (expanded) return renderRunningExpandedResult(toolName, result, context, theme);
|
|
482
554
|
// 折叠态保持 3 格:与完成后的 ` ↳ …` 同列,工具跑完那行不横跳。
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (!SHELL_TOOL_NAMES.has(toolName)) {
|
|
486
|
-
return new Text(theme.fg("muted", pending), 0, 0);
|
|
487
|
-
}
|
|
488
|
-
// shell 运行中报已用时间:数字在 render() 内取,动画 tick 才能带上新值。
|
|
489
|
-
return {
|
|
490
|
-
render(width: number) {
|
|
491
|
-
const elapsed = shellDurationText(toolName, context, true);
|
|
492
|
-
const suffix = elapsed ? theme.fg("dim", ` · ${elapsed}`) : "";
|
|
493
|
-
return [truncateToWidth(theme.fg("muted", pending) + suffix, width, "")];
|
|
494
|
-
},
|
|
495
|
-
invalidate(): void {},
|
|
496
|
-
};
|
|
555
|
+
if (context?.state) context.state.ccstyleIoView = undefined;
|
|
556
|
+
return renderRunningCollapsedResult({ toolName, context, theme });
|
|
497
557
|
}
|
|
498
558
|
|
|
499
559
|
// shell:结果定型时定一次结束时间,这一行之后不再变。
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { highlightCode } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
13
|
import { config } from "../../config/config.ts";
|
|
14
|
+
import { expandedLineLimit } from "../transcript-scope.ts";
|
|
14
15
|
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
15
16
|
import { oneLine } from "../../utils/format.ts";
|
|
16
17
|
import { toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
@@ -164,7 +165,7 @@ export function codemodeCodeLines(
|
|
|
164
165
|
if (text) statements.push(text);
|
|
165
166
|
}
|
|
166
167
|
if (statements.length === 0) return [];
|
|
167
|
-
const limit = Math.max(1, config.expandedInputMaxLines);
|
|
168
|
+
const limit = Math.max(1, expandedLineLimit(config.expandedInputMaxLines));
|
|
168
169
|
const visible = statements.slice(0, limit);
|
|
169
170
|
const rail = `${INDENT}${fg("dim", "│")} `;
|
|
170
171
|
const lines = visible.map((statement) =>
|
|
@@ -53,8 +53,8 @@ function renderDiffStatBar(stats: DiffStats, width: number, theme: DiffTheme): s
|
|
|
53
53
|
}
|
|
54
54
|
const removedSlots = Math.max(0, barSlots - addedSlots);
|
|
55
55
|
|
|
56
|
-
const addedBar = addedSlots > 0 ? theme.fg("
|
|
57
|
-
const removedBar = removedSlots > 0 ? theme.fg("
|
|
56
|
+
const addedBar = addedSlots > 0 ? theme.fg("success", "━".repeat(addedSlots)) : "";
|
|
57
|
+
const removedBar = removedSlots > 0 ? theme.fg("error", "━".repeat(removedSlots)) : "";
|
|
58
58
|
return stabilizeBackgroundResets(
|
|
59
59
|
`${theme.fg("dim", "[")}${addedBar}${removedBar}${theme.fg("dim", "]")}`,
|
|
60
60
|
);
|
|
@@ -63,8 +63,8 @@ function renderDiffStatBar(stats: DiffStats, width: number, theme: DiffTheme): s
|
|
|
63
63
|
function buildDiffSummaryBasePieces(stats: DiffStats, theme: DiffTheme): string[] {
|
|
64
64
|
return [
|
|
65
65
|
theme.fg("toolOutput", `↳ ${emphasis(theme, "diff")}`),
|
|
66
|
-
theme.fg("
|
|
67
|
-
theme.fg("
|
|
66
|
+
theme.fg("success", `+${stats.added}`),
|
|
67
|
+
theme.fg("error", `-${stats.removed}`),
|
|
68
68
|
];
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -92,10 +92,10 @@ function renderChangeMarker(
|
|
|
92
92
|
return rowBg ? `${rowBg} ${rowBg}` : " ";
|
|
93
93
|
}
|
|
94
94
|
if (kind === "add") {
|
|
95
|
-
return colorizeSegment(theme, "
|
|
95
|
+
return colorizeSegment(theme, "success", glyph, rowBg);
|
|
96
96
|
}
|
|
97
97
|
if (kind === "remove") {
|
|
98
|
-
return colorizeSegment(theme, "
|
|
98
|
+
return colorizeSegment(theme, "error", glyph, rowBg);
|
|
99
99
|
}
|
|
100
100
|
return colorizeSegment(theme, "dim", glyph, rowBg);
|
|
101
101
|
}
|
|
@@ -132,12 +132,12 @@ function renderCodeDivider(
|
|
|
132
132
|
);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function getLineNumberColor(kind: DiffLineKind): "dim" | "
|
|
135
|
+
function getLineNumberColor(kind: DiffLineKind): "dim" | "success" | "error" {
|
|
136
136
|
if (kind === "add") {
|
|
137
|
-
return "
|
|
137
|
+
return "success";
|
|
138
138
|
}
|
|
139
139
|
if (kind === "remove") {
|
|
140
|
-
return "
|
|
140
|
+
return "error";
|
|
141
141
|
}
|
|
142
142
|
return "dim";
|
|
143
143
|
}
|
|
@@ -179,7 +179,7 @@ function renderClassicContentPrefix(
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
const glyph = kind === "add" ? "+" : "-";
|
|
182
|
-
const glyphColor = kind === "add" ? "
|
|
182
|
+
const glyphColor = kind === "add" ? "success" : "error";
|
|
183
183
|
const spacer = rowBg ? `${rowBg} ` : " ";
|
|
184
184
|
return `${colorizeSegment(theme, glyphColor, glyph, rowBg)}${spacer}`;
|
|
185
185
|
}
|
|
@@ -115,7 +115,7 @@ export function keepBackgroundAcrossResets(text: string, rowBg: string): string
|
|
|
115
115
|
|
|
116
116
|
export function colorizeSegment(
|
|
117
117
|
theme: DiffTheme,
|
|
118
|
-
color: "dim" | "
|
|
118
|
+
color: "dim" | "success" | "error",
|
|
119
119
|
text: string,
|
|
120
120
|
rowBg: string | undefined,
|
|
121
121
|
): string {
|
|
@@ -276,15 +276,15 @@ export function readThemeAnsi(
|
|
|
276
276
|
export function resolveDiffPalette(theme: DiffTheme): DiffPalette {
|
|
277
277
|
const fallbackBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolPendingBg")) ??
|
|
278
278
|
parseAnsiColorCode(readThemeAnsi(theme, "bg", "userMessageBg")) ?? { r: 32, g: 35, b: 42 };
|
|
279
|
-
//
|
|
279
|
+
// 新增行/删除行的文字和底色统一取 success、error,与工具标题、footer 计数同源。
|
|
280
280
|
const addBaseBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolSuccessBg")) ?? fallbackBg;
|
|
281
281
|
const removeBaseBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolErrorBg")) ?? addBaseBg;
|
|
282
|
-
const addFg = parseAnsiColorCode(readThemeAnsi(theme, "fg", "
|
|
282
|
+
const addFg = parseAnsiColorCode(readThemeAnsi(theme, "fg", "success")) ?? {
|
|
283
283
|
r: 88,
|
|
284
284
|
g: 173,
|
|
285
285
|
b: 88,
|
|
286
286
|
};
|
|
287
|
-
const removeFg = parseAnsiColorCode(readThemeAnsi(theme, "fg", "
|
|
287
|
+
const removeFg = parseAnsiColorCode(readThemeAnsi(theme, "fg", "error")) ?? {
|
|
288
288
|
r: 196,
|
|
289
289
|
g: 98,
|
|
290
290
|
b: 98,
|
|
@@ -96,9 +96,9 @@ export function styleDiffSummaryLine(
|
|
|
96
96
|
};
|
|
97
97
|
if (countsIndex >= 0 && stats) {
|
|
98
98
|
pushMuted(countsIndex);
|
|
99
|
-
pieces.push(theme.fg("
|
|
99
|
+
pieces.push(theme.fg("success", `+${stats.added}`));
|
|
100
100
|
pieces.push(theme.fg("muted", " "));
|
|
101
|
-
pieces.push(theme.fg("
|
|
101
|
+
pieces.push(theme.fg("error", `-${stats.removed}`));
|
|
102
102
|
cursor = countsIndex + counts.length;
|
|
103
103
|
}
|
|
104
104
|
if (hintIndex >= cursor) {
|
|
@@ -26,6 +26,7 @@ import { mcpToolTitle } from "./mcp-title.ts";
|
|
|
26
26
|
import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./names.ts";
|
|
27
27
|
import {
|
|
28
28
|
contentRowRange,
|
|
29
|
+
hasVisibleText,
|
|
29
30
|
imagePlacement,
|
|
30
31
|
stripAnsi,
|
|
31
32
|
stripBackgroundAnsi,
|
|
@@ -364,10 +365,17 @@ function expandedToolRows(tool: any, width: number): string[] {
|
|
|
364
365
|
const cached = expandedToolRowsCache.get(paint);
|
|
365
366
|
if (cached) return cached;
|
|
366
367
|
const rendered = visibleLines(paint);
|
|
367
|
-
|
|
368
|
-
|
|
368
|
+
// 首行的状态图标由组前缀给,子行里要剥掉。
|
|
369
|
+
const stripIcon = (line: string) =>
|
|
370
|
+
stripLeadingStatusIcon(line)
|
|
369
371
|
.replace(/^ +/, "")
|
|
370
372
|
.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*) +/, "$1");
|
|
373
|
+
// 剥完什么都没剩下(子卡首行只剩一个图标)就丢掉这一行,再对新的首行剥一次:
|
|
374
|
+
// 留着会多出一行只有 `├ ✓` 的空行,标题行也会带着图标和前缀叠成两个。
|
|
375
|
+
while (rendered.length) {
|
|
376
|
+
rendered[0] = stripIcon(rendered[0]);
|
|
377
|
+
if (hasVisibleText(rendered[0])) break;
|
|
378
|
+
rendered.shift();
|
|
371
379
|
}
|
|
372
380
|
const rows = rendered.length
|
|
373
381
|
? // 续行只剥外层 Box 的 1 格 left pad,保留 Input/Output 相对缩进
|