pi-cc-extensions 1.0.4 → 1.0.5
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 +6 -6
- package/README.md +6 -6
- package/extensions/config/config.ts +16 -27
- package/extensions/config/panel.ts +49 -48
- package/extensions/feature/compact-thinking.ts +2 -3
- package/extensions/feature/context.ts +70 -2
- package/extensions/feature/shell/footer-layout.ts +108 -23
- package/extensions/feature/shell/footer.ts +59 -43
- package/extensions/feature/shell/prompt-editor.ts +77 -0
- package/extensions/index.ts +6 -0
- package/extensions/renderer/compact-mode.ts +47 -40
- package/extensions/renderer/default-mode.ts +7 -4
- package/extensions/renderer/mouse/interaction.ts +18 -4
- package/extensions/renderer/mouse/scroll.ts +23 -8
- package/extensions/renderer/mouse/sticky-prompt.ts +2 -2
- package/extensions/renderer/tool/ccstyle-tool.ts +24 -12
- package/extensions/renderer/tool/codemode.ts +121 -6
- package/extensions/renderer/tool/diff/diff-limits.ts +3 -2
- package/extensions/renderer/tool/diff/diff-palette.ts +39 -8
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/grouping.ts +13 -13
- package/extensions/renderer/tool/message-display.ts +3 -2
- package/extensions/renderer/tool/names.ts +49 -49
- package/extensions/renderer/tool/result.ts +24 -0
- package/extensions/renderer/tool/tool-state.ts +51 -2
- package/package.json +1 -1
- package/themes/cc-dark.json +72 -71
- package/themes/cc-light.json +72 -71
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Compact mode:每条含 toolCall 的 assistant message 折叠为一条逐步累加的摘要行
|
|
3
3
|
* (`Ran for 8s, bash×2, read×2`),edit/write 独立标题行(`✓ write <path> (+25 -0)`),
|
|
4
4
|
* 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen 点击)时助手文本按原生渲染,
|
|
5
|
-
* thinking
|
|
5
|
+
* thinking 与工具卡装进按状态着色的面板(卡内恢复 compact-thinking/Pi 原生或专用
|
|
6
6
|
* renderer)。edit/write 复用 mode=on 的 rich diff
|
|
7
7
|
* 与 `editDiffCollapsedLines` / `writeDiffCollapsedLines` / `expandedPreviewMaxLines`。
|
|
8
8
|
* Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "@earendil-works/pi-coding-agent";
|
|
38
38
|
import { truncateToWidth, visibleWidth, Box, Spacer } from "@earendil-works/pi-tui";
|
|
39
39
|
import { config, getToolDisplayConfig } from "../config/config.ts";
|
|
40
|
+
import { aggregateToolCardBgSlot, toolCardBgSlot } from "./tool/tool-state.ts";
|
|
40
41
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./tool/ask-user-question.ts";
|
|
41
42
|
import { toolLoadingIcon } from "../utils/tool-loading-icon.ts";
|
|
42
43
|
import { sanitizeToolResultText } from "../utils/tool-result-sanitize.ts";
|
|
@@ -53,7 +54,7 @@ import { displayConfigCacheKey } from "./tool/diff/diff-component.ts";
|
|
|
53
54
|
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
54
55
|
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
55
56
|
import { getToolMouseTui } from "./mouse/scroll.ts";
|
|
56
|
-
import { insetComponent } from "./tool/result.ts";
|
|
57
|
+
import { expandedBodyComponent, insetComponent } from "./tool/result.ts";
|
|
57
58
|
import { isLiveToolCall, scheduleAnimation } from "./tool/tool-state.ts";
|
|
58
59
|
import { renderExpandedToolResult } from "./tool/tool-io.ts";
|
|
59
60
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
@@ -114,8 +115,7 @@ export function styleCompactThinkingText(
|
|
|
114
115
|
bold = false,
|
|
115
116
|
): string {
|
|
116
117
|
if (!theme) return text;
|
|
117
|
-
const
|
|
118
|
-
const colored = typeof theme.fg === "function" ? theme.fg(color, text) : text;
|
|
118
|
+
const colored = typeof theme.fg === "function" ? theme.fg("dim", text) : text;
|
|
119
119
|
const weighted = bold && typeof theme.bold === "function" ? theme.bold(colored) : colored;
|
|
120
120
|
return typeof theme.italic === "function" ? theme.italic(weighted) : weighted;
|
|
121
121
|
}
|
|
@@ -333,7 +333,7 @@ function darkenBgAnsi(theme: any, slot: string): string {
|
|
|
333
333
|
return `\x1b[48;2;${r};${g};${b}m`;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
/**
|
|
336
|
+
/** 工具卡深色行:左右各缩 1 格(含工具卡自身 padding),背景只到内容区,
|
|
337
337
|
* 修复行尾 reset 截断。 */
|
|
338
338
|
function toolCardBgRow(
|
|
339
339
|
theme: any,
|
|
@@ -342,8 +342,8 @@ function toolCardBgRow(
|
|
|
342
342
|
line: string,
|
|
343
343
|
width: number,
|
|
344
344
|
): string {
|
|
345
|
-
const leftInset =
|
|
346
|
-
const rightInset =
|
|
345
|
+
const leftInset = 1;
|
|
346
|
+
const rightInset = 1;
|
|
347
347
|
const contentWidth = Math.max(0, width - leftInset - rightInset);
|
|
348
348
|
// 深色块内左右各 1 格内部 padding;去掉行首原有空格后重新对齐
|
|
349
349
|
const text = stripBackgroundAnsi(line).replace(/^ +/, "");
|
|
@@ -356,14 +356,13 @@ function toolCardBgRow(
|
|
|
356
356
|
const inset = (n: number) => (outerBg ? `${outerBg}${" ".repeat(n)}\x1b[49m` : " ".repeat(n));
|
|
357
357
|
return `${inset(leftInset)}${bgAnsi}${stable}\x1b[49m${inset(rightInset)}`;
|
|
358
358
|
}
|
|
359
|
-
/** edit/write
|
|
360
|
-
function editWriteExpandedCard(theme: any): any {
|
|
359
|
+
/** edit/write 展开卡:按这张工具卡自己的状态取底色,不应用工具卡深色/间距改动。 */
|
|
360
|
+
function editWriteExpandedCard(theme: any, component: any): any {
|
|
361
|
+
const slot = toolCardBgSlot(component);
|
|
361
362
|
return new Box(
|
|
362
363
|
1,
|
|
363
364
|
1,
|
|
364
|
-
typeof theme.bg === "function"
|
|
365
|
-
? (text: string) => theme.bg(config.expandedCardBackground, text)
|
|
366
|
-
: undefined,
|
|
365
|
+
typeof theme.bg === "function" ? (text: string) => theme.bg(slot, text) : undefined,
|
|
367
366
|
);
|
|
368
367
|
}
|
|
369
368
|
|
|
@@ -384,7 +383,7 @@ function layoutExpandedToolCard(
|
|
|
384
383
|
toolHits = false,
|
|
385
384
|
live = false,
|
|
386
385
|
): { lines: string[]; hits: Array<{ child: any; start: number; end: number }> } {
|
|
387
|
-
const slot =
|
|
386
|
+
const slot = aggregateToolCardBgSlot(children.map((child) => child?.__ccTool).filter(Boolean));
|
|
388
387
|
const toolBgAnsi = darkenBgAnsi(theme, slot);
|
|
389
388
|
/** live 每行前面的标记宽度(首行 `↳ `,其余对齐空格)。 */
|
|
390
389
|
const markerWidth = live ? 2 : 0;
|
|
@@ -397,7 +396,10 @@ function layoutExpandedToolCard(
|
|
|
397
396
|
let liveLead = 0;
|
|
398
397
|
const pushRow = (raw: string) => {
|
|
399
398
|
if (!live) {
|
|
400
|
-
|
|
399
|
+
// 面板 pad 1 是唯一左边距:剥掉子行 collapsed 态自带的 1 格 lead,
|
|
400
|
+
// 否则标题行偏到 2、↳ 行偏到 4,左右不再对称。
|
|
401
|
+
const line = raw.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*) /, "$1");
|
|
402
|
+
lines.push(paddedBackgroundRow(theme, slot, line, width));
|
|
401
403
|
return;
|
|
402
404
|
}
|
|
403
405
|
// 槽位里不重复展开入口(见 SLOT_HINT_PATTERNS)。
|
|
@@ -500,7 +502,9 @@ function compactRoundCard(
|
|
|
500
502
|
| undefined;
|
|
501
503
|
const layout = (width: number) => {
|
|
502
504
|
const theme = themeOf();
|
|
503
|
-
const bgSlot =
|
|
505
|
+
const bgSlot = aggregateToolCardBgSlot(
|
|
506
|
+
children.map((child) => child?.__ccTool).filter(Boolean),
|
|
507
|
+
);
|
|
504
508
|
const innerWidth = contentWidth(width);
|
|
505
509
|
const paints = children.map((child) => {
|
|
506
510
|
const lines = child.render?.(innerWidth);
|
|
@@ -706,7 +710,7 @@ function compactEditWriteLine(
|
|
|
706
710
|
const isError = component.result?.isError === true;
|
|
707
711
|
const isPending = !component.result || component.isPartial === true;
|
|
708
712
|
const icon = isError ? "✗" : isPending ? toolLoadingIcon() : "✓";
|
|
709
|
-
const iconColor = isError ? "error" : isPending ? "
|
|
713
|
+
const iconColor = isError ? "error" : isPending ? "dim" : "success";
|
|
710
714
|
let statsText = "";
|
|
711
715
|
let statsStyled = "";
|
|
712
716
|
// 统计只显示一处:正文自带 +A -D 时标题不再重复(showStats=false)。
|
|
@@ -749,7 +753,7 @@ function compactEditWriteLine(
|
|
|
749
753
|
const hintPart = clickable
|
|
750
754
|
? `${theme.fg("dim", " · ")}${theme.fg(isToolCallHovered(component.toolCallId) ? "text" : "dim", clickText)}`
|
|
751
755
|
: "";
|
|
752
|
-
const line = `${iconPart}${namePart}${theme.fg("
|
|
756
|
+
const line = `${iconPart}${namePart}${theme.fg("muted", pathPart)}${statsStyled}${hintPart}`;
|
|
753
757
|
return ["", truncateToWidth(line, width, "")];
|
|
754
758
|
}
|
|
755
759
|
|
|
@@ -809,7 +813,7 @@ function compactEditWriteLines(
|
|
|
809
813
|
hit &&
|
|
810
814
|
hit.width === width &&
|
|
811
815
|
hit.theme === theme &&
|
|
812
|
-
hit.bgSlot ===
|
|
816
|
+
hit.bgSlot === toolCardBgSlot(component) &&
|
|
813
817
|
hit.diffConfigKey === diffConfigKey &&
|
|
814
818
|
hit.expanded === expanded &&
|
|
815
819
|
hit.result === component.result &&
|
|
@@ -826,7 +830,7 @@ function compactEditWriteLines(
|
|
|
826
830
|
compactEditPaintCache.set(component, {
|
|
827
831
|
width,
|
|
828
832
|
theme,
|
|
829
|
-
bgSlot:
|
|
833
|
+
bgSlot: toolCardBgSlot(component),
|
|
830
834
|
diffConfigKey,
|
|
831
835
|
expanded,
|
|
832
836
|
result: component.result,
|
|
@@ -894,7 +898,7 @@ function paintCompactEditWrite(
|
|
|
894
898
|
|
|
895
899
|
let detail: any;
|
|
896
900
|
if (hasRich) {
|
|
897
|
-
detail = expanded ? candidate : insetComponent(candidate as any);
|
|
901
|
+
detail = expanded ? expandedBodyComponent(candidate) : insetComponent(candidate as any);
|
|
898
902
|
} else {
|
|
899
903
|
const outputText = sanitizeToolResultText(
|
|
900
904
|
Array.isArray(result?.content)
|
|
@@ -921,7 +925,7 @@ function paintCompactEditWrite(
|
|
|
921
925
|
return [...title, ...detail.render(width)];
|
|
922
926
|
}
|
|
923
927
|
|
|
924
|
-
const box = editWriteExpandedCard(theme);
|
|
928
|
+
const box = editWriteExpandedCard(theme, component);
|
|
925
929
|
box.addChild({
|
|
926
930
|
render(innerWidth: number): string[] {
|
|
927
931
|
return compactEditWriteLine(component, innerWidth, writeMetadata, {
|
|
@@ -985,8 +989,7 @@ function compactAssistantLineComponent(
|
|
|
985
989
|
return paint.lines;
|
|
986
990
|
}
|
|
987
991
|
const plainText = truncateToWidth(resolved, summaryWidth, "…");
|
|
988
|
-
|
|
989
|
-
const plain = (value: string) => theme.fg(config.dimThinkingText ? "dim" : "muted", value);
|
|
992
|
+
const plain = (value: string) => theme.fg("dim", value);
|
|
990
993
|
let text = plain(plainText);
|
|
991
994
|
if (runningActive || plainText.startsWith("Ran for ")) {
|
|
992
995
|
const separator = plainText.indexOf(", ");
|
|
@@ -1195,6 +1198,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1195
1198
|
foldTimer?: ReturnType<typeof setTimeout>;
|
|
1196
1199
|
/** 回合挂钟起点,保证 Running 时长连续递增。 */
|
|
1197
1200
|
startedAt: number;
|
|
1201
|
+
/** 本进程看见回合的挂钟起点。消息 timestamp 不随工具执行前进。 */
|
|
1202
|
+
wallStartedAt: number;
|
|
1198
1203
|
endedAt?: number;
|
|
1199
1204
|
/** 回合尾行(摘要 + live 槽位),挂在 transcript 容器的回合末尾。 */
|
|
1200
1205
|
tail?: MountedRoundTail;
|
|
@@ -1268,8 +1273,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1268
1273
|
value >= 1_600_000_000_000 &&
|
|
1269
1274
|
value <= Date.now() + 60_000;
|
|
1270
1275
|
|
|
1271
|
-
/**
|
|
1272
|
-
*
|
|
1276
|
+
/** 结束回合活动态;不做任何重绘。更晚的消息 timestamp 用来保住回放时长;
|
|
1277
|
+
* 同一条消息的 timestamp 不动,活回合改用本进程挂钟,避免把历史 timestamp 收到「现在」。 */
|
|
1273
1278
|
const finalizeRound = (round: CompactRound, endTimestamp?: number): void => {
|
|
1274
1279
|
clearFoldTimer(round);
|
|
1275
1280
|
round.active = false;
|
|
@@ -1280,10 +1285,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1280
1285
|
fallback = message.timestamp;
|
|
1281
1286
|
}
|
|
1282
1287
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1288
|
+
const stamped = isUsableTimestamp(endTimestamp) ? endTimestamp : fallback;
|
|
1289
|
+
if (stamped > round.startedAt) round.endedAt = stamped;
|
|
1290
|
+
else if (typeof round.wallStartedAt === "number") {
|
|
1291
|
+
round.endedAt = round.startedAt + Math.max(0, Date.now() - round.wallStartedAt);
|
|
1292
|
+
} else round.endedAt = Math.max(round.startedAt, Date.now());
|
|
1287
1293
|
}
|
|
1288
1294
|
if (activeRound === round) {
|
|
1289
1295
|
activeRound = undefined;
|
|
@@ -1658,6 +1664,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1658
1664
|
}
|
|
1659
1665
|
if (!prior) return;
|
|
1660
1666
|
round.startedAt = prior.startedAt;
|
|
1667
|
+
if (typeof prior.wallStartedAt === "number") round.wallStartedAt = prior.wallStartedAt;
|
|
1661
1668
|
if (prior.detachedMessages.length) round.detachedMessages.push(...prior.detachedMessages);
|
|
1662
1669
|
if (!prior.active) {
|
|
1663
1670
|
round.active = false;
|
|
@@ -1683,6 +1690,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1683
1690
|
suppressedToolIds: new Set(),
|
|
1684
1691
|
liveSlotToolIds: new Set(),
|
|
1685
1692
|
startedAt: isUsableTimestamp(message?.timestamp) ? message.timestamp : Date.now(),
|
|
1693
|
+
wallStartedAt: Date.now(),
|
|
1686
1694
|
};
|
|
1687
1695
|
liveRounds.add(round);
|
|
1688
1696
|
activateRound(round);
|
|
@@ -2060,7 +2068,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2060
2068
|
const sweepOrphanTails = (): void => {
|
|
2061
2069
|
for (const tail of [...mountedTails]) {
|
|
2062
2070
|
const round = tail.round as CompactRound | undefined;
|
|
2063
|
-
|
|
2071
|
+
// 回合还在本 pass 名单里、且没有同 anchor 的更新 tail 顶位时保留。
|
|
2072
|
+
if (round && liveRounds.has(round)) {
|
|
2073
|
+
const anchor = round.anchor;
|
|
2074
|
+
if (!anchor || tailByAnchor.get(anchor) === tail) continue;
|
|
2075
|
+
}
|
|
2064
2076
|
removeTailHost(tail);
|
|
2065
2077
|
mountedTails.delete(tail);
|
|
2066
2078
|
if (round?.anchor && tailByAnchor.get(round.anchor) === tail) {
|
|
@@ -2138,16 +2150,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2138
2150
|
|
|
2139
2151
|
if (hasToolCalls) {
|
|
2140
2152
|
let round = roundByComponent.get(this);
|
|
2141
|
-
if (
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2147
|
-
round = createRound(this, message);
|
|
2148
|
-
roundByComponent.set(this, round);
|
|
2149
|
-
} else if (!round) {
|
|
2150
|
-
// 中间隔着用户消息就另起一回合,不并进上一个回合。
|
|
2153
|
+
if (!round) {
|
|
2154
|
+
// 本条消息是否带 text 不是回合边界:pi 流式协议里同一条
|
|
2155
|
+
// assistant 消息会以 text+toolCall、toolCall-only 反复到达。
|
|
2156
|
+
// 只在「组件未挂进任何回合」时并入 activeRound;被用户消息
|
|
2157
|
+
// 隔开(steer/上回合没正文)时由 joinOrCreateRound 收尾另起。
|
|
2151
2158
|
round = joinOrCreateRound(this, message);
|
|
2152
2159
|
roundByComponent.set(this, round);
|
|
2153
2160
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
TOOL_EXPANDED_BACKGROUND_PATCH,
|
|
15
15
|
} from "../utils/patch-keys.ts";
|
|
16
16
|
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
17
|
-
import { isLiveToolCall } from "./tool/tool-state.ts";
|
|
17
|
+
import { isLiveToolCall, toolCardBgSlot } from "./tool/tool-state.ts";
|
|
18
18
|
import { getMessageDisplayTheme } from "./tool/message-display.ts";
|
|
19
19
|
import { createCcstyleTool } from "./tool/ccstyle-tool.ts";
|
|
20
20
|
import type { WriteExecutionMetadataStore } from "./tool/diff/index.ts";
|
|
@@ -373,7 +373,9 @@ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
/**
|
|
376
|
+
/** 展开面板背景按工具状态取槽位;折叠行保持原生状态色。
|
|
377
|
+
* compact 下 ask_user_question 等普通工具的展开也走原生 contentBox,
|
|
378
|
+
* 这里按同一套状态覆盖它的底色。
|
|
377
379
|
* 必须在 compact-mode 之后安装,shutdown 时先于 compact-mode 释放。 */
|
|
378
380
|
export function installToolExpandedBackground(): () => void {
|
|
379
381
|
const previous = patchRegistry.get<ToolExpandedBackgroundPatch>(TOOL_EXPANDED_BACKGROUND_PATCH);
|
|
@@ -385,7 +387,7 @@ export function installToolExpandedBackground(): () => void {
|
|
|
385
387
|
prototype,
|
|
386
388
|
installed: function (this: any) {
|
|
387
389
|
original.call(this);
|
|
388
|
-
if (!patch.active || config.mode
|
|
390
|
+
if (!patch.active || config.mode === "off" || !this.expanded) return;
|
|
389
391
|
const theme = getMessageDisplayTheme();
|
|
390
392
|
if (!theme?.bg) return;
|
|
391
393
|
const box = this.contentBox;
|
|
@@ -394,7 +396,8 @@ export function installToolExpandedBackground(): () => void {
|
|
|
394
396
|
box.paddingX = 1;
|
|
395
397
|
box.paddingY = 1;
|
|
396
398
|
if (box.setBgFn) {
|
|
397
|
-
|
|
399
|
+
const slot = toolCardBgSlot(this);
|
|
400
|
+
box.setBgFn((text: string) => theme.bg(slot, text));
|
|
398
401
|
}
|
|
399
402
|
}
|
|
400
403
|
},
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
import {
|
|
49
49
|
fullscreenLazyTui,
|
|
50
50
|
hideScrollButton,
|
|
51
|
+
dockScrollButtonActive,
|
|
51
52
|
restoreOfficialScrollToEnd,
|
|
52
53
|
syncOfficialScrollToEnd,
|
|
53
54
|
isScrollBottomInput,
|
|
@@ -503,8 +504,8 @@ function keepViewportOnExpand(tui: any): void {
|
|
|
503
504
|
const view = tui.getPrimaryScrollView?.();
|
|
504
505
|
if (typeof view?.scrollTo !== "function" || typeof view.scrollTop !== "number") return;
|
|
505
506
|
view.scrollTo(view.scrollTop, { disableFollow: true });
|
|
506
|
-
//
|
|
507
|
-
if (view.isFollowingEnd === false) setScrollButtonVisible(true);
|
|
507
|
+
// 已脱离底部跟随:dock 模式亮出回到底部按钮,新输出到来时仍有入口。
|
|
508
|
+
if (view.isFollowingEnd === false && dockScrollButtonActive()) setScrollButtonVisible(true);
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
/**
|
|
@@ -579,6 +580,18 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
579
580
|
applyFullscreenHover(tui, target);
|
|
580
581
|
}
|
|
581
582
|
|
|
583
|
+
/** 布局缺失,或焦点 overlay 还不在上一帧列表里:这份矩形不能拿来判点击。 */
|
|
584
|
+
function overlayLayoutsCurrent(tui: any): boolean {
|
|
585
|
+
const layouts = tui?.renderedOverlayLayouts;
|
|
586
|
+
if (!Array.isArray(layouts) || layouts.length === 0) return false;
|
|
587
|
+
if (typeof tui.isOverlayFocused === "function" && tui.isOverlayFocused()) {
|
|
588
|
+
const focused = tui.focusedComponent;
|
|
589
|
+
if (focused && !layouts.some((layout: any) => layout?.entry?.component === focused))
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
|
|
582
595
|
/**
|
|
583
596
|
* 鼠标包(1-based 屏幕坐标)是否落在某个已渲染 overlay 的矩形内。
|
|
584
597
|
* renderedOverlayLayouts 与官方 dispatchMouseToOverlay 同源:只含上一帧画出的可见 overlay。
|
|
@@ -623,8 +636,9 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
623
636
|
// dispatchMouseToOverlay → overlay.handleMouse;overlay 外的包仍
|
|
624
637
|
// 走扩展收起/展开/hover。两条路径各管各的。
|
|
625
638
|
if (packets) {
|
|
626
|
-
//
|
|
627
|
-
//
|
|
639
|
+
// 布局还没出来或仍是上一帧:不要把点击收成工具展开。返回 undefined
|
|
640
|
+
// 让焦点 overlay 的 handleInput 拿到原始 SGR;官方链也靠这份布局,调它会消费且打到 transcript。
|
|
641
|
+
if (tui.hasOverlay?.() && !overlayLayoutsCurrent(tui)) return undefined;
|
|
628
642
|
const overlayPackets = tui.hasOverlay?.()
|
|
629
643
|
? packets.filter((packet) => packetInsideOverlay(tui, packet))
|
|
630
644
|
: undefined;
|
|
@@ -68,7 +68,7 @@ export function setScrollButtonVisible(visible: boolean): void {
|
|
|
68
68
|
export function noteNewTranscriptItem(): void {
|
|
69
69
|
const state = scrollButtonState();
|
|
70
70
|
const tui = getToolMouseTui();
|
|
71
|
-
if (!state.visible || !tui) return;
|
|
71
|
+
if (!dockScrollButtonActive() || !state.visible || !tui) return;
|
|
72
72
|
// 提交新消息时官方可能直接跳回底部(不经过滚动输入):先按实时跟随状态校正,
|
|
73
73
|
// 否则跟随期间的内容会被当成“离开底部时的新消息”计入。
|
|
74
74
|
if (isAtTranscriptBottom(tui)) {
|
|
@@ -107,6 +107,11 @@ export function toolMouseInteractionActive(): boolean {
|
|
|
107
107
|
return config.mode !== "off";
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** dock 模式才用扩展的独占行;native 或 ccstyle off 时交还 Pi 原生浮层。 */
|
|
111
|
+
export function dockScrollButtonActive(): boolean {
|
|
112
|
+
return toolMouseInteractionActive() && config.scrollButtonMode === "dock";
|
|
113
|
+
}
|
|
114
|
+
|
|
110
115
|
/** 惰性 Proxy 官方 fullscreen(TuiAltScreen)判定。 */
|
|
111
116
|
export function fullscreenLazyTui(tui: any): boolean {
|
|
112
117
|
return isLazyProxyTui(tui) && tui.mode === "fullscreen";
|
|
@@ -132,10 +137,15 @@ export function restoreOfficialScrollToEnd(tui: any): void {
|
|
|
132
137
|
tui[OFFICIAL_SCROLL_TO_END_KEY] = undefined;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
/**
|
|
140
|
+
/** dock 模式替换官方浮层;native 或 ccstyle off 时保留 Pi 原生 Jump to latest。 */
|
|
136
141
|
export function syncOfficialScrollToEnd(tui: any): void {
|
|
137
|
-
if (
|
|
138
|
-
|
|
142
|
+
if (dockScrollButtonActive()) {
|
|
143
|
+
disableOfficialScrollToEnd(tui);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
restoreOfficialScrollToEnd(tui);
|
|
147
|
+
setScrollButtonVisible(false);
|
|
148
|
+
setScrollButtonHovered(false);
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
/** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
|
|
@@ -202,7 +212,7 @@ export function hideScrollButton(tui: any): void {
|
|
|
202
212
|
export function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
203
213
|
if (
|
|
204
214
|
!fullscreenLazyTui(tui) ||
|
|
205
|
-
!
|
|
215
|
+
!dockScrollButtonActive() ||
|
|
206
216
|
!isScrollNavigationInput(data) ||
|
|
207
217
|
scrollButtonSyncScheduled
|
|
208
218
|
)
|
|
@@ -211,7 +221,7 @@ export function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
|
211
221
|
const previousLines = tui.previousLines;
|
|
212
222
|
const check = (attempt: number) => {
|
|
213
223
|
scrollButtonSyncScheduled = false;
|
|
214
|
-
if (getToolMouseTui() !== tui) return;
|
|
224
|
+
if (getToolMouseTui() !== tui || !dockScrollButtonActive()) return;
|
|
215
225
|
// Pi renders on its own frame timer. Inspect the resulting viewport before
|
|
216
226
|
// showing the button so empty or non-scrollable transcripts never flash it.
|
|
217
227
|
const rendered = tui.previousLines !== previousLines;
|
|
@@ -234,7 +244,7 @@ export function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
export function updateScrollButtonFromInput(tui: any, data: string): void {
|
|
237
|
-
if (!fullscreenLazyTui(tui) || !
|
|
247
|
+
if (!fullscreenLazyTui(tui) || !dockScrollButtonActive()) return;
|
|
238
248
|
if (matchesKey(data, "enter") || matchesKey(data, "return")) hideScrollButton(tui);
|
|
239
249
|
}
|
|
240
250
|
|
|
@@ -247,7 +257,12 @@ function scrollButtonText(): string {
|
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
export function renderScrollButton(width: number, theme: any): string[] {
|
|
250
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
!dockScrollButtonActive() ||
|
|
262
|
+
!getScrollButtonVisible() ||
|
|
263
|
+
!fullscreenLazyTui(getToolMouseTui())
|
|
264
|
+
)
|
|
265
|
+
return [];
|
|
251
266
|
const label = theme.fg(
|
|
252
267
|
getScrollButtonHovered() ? "text" : "accent",
|
|
253
268
|
`[ ↓ ${scrollButtonText()} ]`,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "../../utils/patch-keys.ts";
|
|
28
28
|
import { isToolTuiFullscreen } from "../tool/names.ts";
|
|
29
29
|
import type { SgrMousePacket } from "./packets.ts";
|
|
30
|
-
import { setScrollButtonVisible } from "./scroll.ts";
|
|
30
|
+
import { dockScrollButtonActive, setScrollButtonVisible } from "./scroll.ts";
|
|
31
31
|
|
|
32
32
|
/** 消息正文首行/末行的零宽 APC 标记;终端忽略,合成前剥离。 */
|
|
33
33
|
const MESSAGE_START = "\x1b_cc:p\x07";
|
|
@@ -338,7 +338,7 @@ export function handleStickyPromptClick(tui: any, packet: SgrMousePacket): boole
|
|
|
338
338
|
const view = tui.getPrimaryScrollView?.();
|
|
339
339
|
if (typeof view?.scrollTo === "function") {
|
|
340
340
|
view.scrollTo(state.jumpRow, { disableFollow: true });
|
|
341
|
-
if (view.isFollowingEnd === false) setScrollButtonVisible(true);
|
|
341
|
+
if (view.isFollowingEnd === false && dockScrollButtonActive()) setScrollButtonVisible(true);
|
|
342
342
|
}
|
|
343
343
|
// 落点已到消息头,这一帧的粘性行立即失效;等下一帧按新位置重算。
|
|
344
344
|
state.visible = false;
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./names.ts";
|
|
28
28
|
import {
|
|
29
29
|
collapsedResultSummary,
|
|
30
|
+
expandedBodyComponent,
|
|
30
31
|
formatToolDuration,
|
|
31
32
|
insetComponent,
|
|
32
33
|
renderCollapsedToolResultToWidth,
|
|
@@ -154,6 +155,23 @@ function renderExpandedTaskResult(
|
|
|
154
155
|
return formatted ? new Text(`↳ ${formatted}`, 0, 0) : undefined;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
/**
|
|
159
|
+
* 工具耗时(毫秒)。结束时优先用 pi 记在结果上的 durationMs(单调,restore 出来的行也带得到);
|
|
160
|
+
* 运行中 pi 不给(partial 期间渲染上下文里恒为 undefined),退回 renderCall 记下的时钟。
|
|
161
|
+
* 拿不到返回 undefined。时钟回拨时钳到 0。
|
|
162
|
+
*/
|
|
163
|
+
function toolElapsedMs(context: any, isPartial: boolean): number | undefined {
|
|
164
|
+
const recorded = context?.durationMs;
|
|
165
|
+
if (!isPartial && typeof recorded === "number" && Number.isFinite(recorded) && recorded >= 0) {
|
|
166
|
+
return recorded;
|
|
167
|
+
}
|
|
168
|
+
const startedAt = context?.state?.ccstyleShellStartedAt;
|
|
169
|
+
if (typeof startedAt !== "number") return undefined;
|
|
170
|
+
const endedAt = context?.state?.ccstyleShellEndedAt;
|
|
171
|
+
const end = typeof endedAt === "number" ? endedAt : Date.now();
|
|
172
|
+
return Math.max(0, end - startedAt);
|
|
173
|
+
}
|
|
174
|
+
|
|
157
175
|
/**
|
|
158
176
|
* shell 的耗时文本。Pi 原生 shell 渲染器会在结果底部报一行,ccstyle 的通用折叠行把它盖掉了。
|
|
159
177
|
*
|
|
@@ -163,16 +181,8 @@ function renderExpandedTaskResult(
|
|
|
163
181
|
*/
|
|
164
182
|
function shellDurationText(toolName: string, context: any, isPartial: boolean): string {
|
|
165
183
|
if (!SHELL_TOOL_NAMES.has(toolName)) return "";
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
return formatToolDuration(recorded);
|
|
169
|
-
}
|
|
170
|
-
const startedAt = context?.state?.ccstyleShellStartedAt;
|
|
171
|
-
if (typeof startedAt !== "number") return "";
|
|
172
|
-
const endedAt = context?.state?.ccstyleShellEndedAt;
|
|
173
|
-
const end = typeof endedAt === "number" ? endedAt : Date.now();
|
|
174
|
-
// 时钟回拨时别吐负数;快照固定时钟也会走到这。
|
|
175
|
-
return formatToolDuration(Math.max(0, end - startedAt));
|
|
184
|
+
const elapsed = toolElapsedMs(context, isPartial);
|
|
185
|
+
return elapsed === undefined ? "" : formatToolDuration(elapsed);
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
/** 用 ccstyle call/result 包装任意工具定义。 */
|
|
@@ -328,6 +338,7 @@ export function createCcstyleTool(
|
|
|
328
338
|
running: Boolean(options?.isPartial),
|
|
329
339
|
isError,
|
|
330
340
|
isHovered: () => isToolCallHovered(toolCallId),
|
|
341
|
+
code: context?.args?.code,
|
|
331
342
|
});
|
|
332
343
|
}
|
|
333
344
|
// ask_user_question:折叠态摊成问答树,展开态按问题逐条列出选择。
|
|
@@ -405,8 +416,9 @@ export function createCcstyleTool(
|
|
|
405
416
|
writeExecutionMetadata,
|
|
406
417
|
getToolDisplayConfig,
|
|
407
418
|
);
|
|
408
|
-
// 展开态由 Box(1,1)
|
|
409
|
-
if (richResult)
|
|
419
|
+
// 展开态由 Box(1,1) 提供内边距,首行再缩 2 格与标题文字列对齐;折叠态 inset 一级缩进
|
|
420
|
+
if (richResult)
|
|
421
|
+
return expanded ? expandedBodyComponent(richResult) : insetComponent(richResult);
|
|
410
422
|
}
|
|
411
423
|
|
|
412
424
|
const text = textFromResult(result, expanded);
|