pi-cc-extensions 1.0.3 → 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 +291 -136
- 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 +124 -54
- package/extensions/renderer/default-mode.ts +7 -4
- package/extensions/renderer/mouse/interaction.ts +69 -28
- 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,12 +1198,16 @@ 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;
|
|
1201
1206
|
};
|
|
1202
1207
|
let activeRound: CompactRound | undefined;
|
|
1203
1208
|
let roundByComponent = new WeakMap<object, CompactRound>();
|
|
1209
|
+
/** 本 pass 建起的回合(含被收编的):清扫孤儿尾行的在册名单。 */
|
|
1210
|
+
const liveRounds = new Set<CompactRound>();
|
|
1204
1211
|
const expandedRoundToolIds = new Set<string>();
|
|
1205
1212
|
/** 用户显式点开的 round 内工具;强制折叠不压回这些 id。 */
|
|
1206
1213
|
const explicitRoundToolIds = new Set<string>();
|
|
@@ -1259,11 +1266,31 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1259
1266
|
pendingFoldRounds.delete(round);
|
|
1260
1267
|
};
|
|
1261
1268
|
|
|
1262
|
-
/**
|
|
1263
|
-
const
|
|
1269
|
+
/** 供采纳为回合边界的 timestamp:必须是看起来像 epoch 毫秒的数(桩数据/秒级会被拒)。 */
|
|
1270
|
+
const isUsableTimestamp = (value: unknown): value is number =>
|
|
1271
|
+
typeof value === "number" &&
|
|
1272
|
+
Number.isFinite(value) &&
|
|
1273
|
+
value >= 1_600_000_000_000 &&
|
|
1274
|
+
value <= Date.now() + 60_000;
|
|
1275
|
+
|
|
1276
|
+
/** 结束回合活动态;不做任何重绘。更晚的消息 timestamp 用来保住回放时长;
|
|
1277
|
+
* 同一条消息的 timestamp 不动,活回合改用本进程挂钟,避免把历史 timestamp 收到「现在」。 */
|
|
1278
|
+
const finalizeRound = (round: CompactRound, endTimestamp?: number): void => {
|
|
1264
1279
|
clearFoldTimer(round);
|
|
1265
1280
|
round.active = false;
|
|
1266
|
-
if (round.endedAt === undefined)
|
|
1281
|
+
if (round.endedAt === undefined) {
|
|
1282
|
+
let fallback = 0;
|
|
1283
|
+
for (const message of roundMessages(round)) {
|
|
1284
|
+
if (isUsableTimestamp(message?.timestamp) && message.timestamp > fallback) {
|
|
1285
|
+
fallback = message.timestamp;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
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());
|
|
1293
|
+
}
|
|
1267
1294
|
if (activeRound === round) {
|
|
1268
1295
|
activeRound = undefined;
|
|
1269
1296
|
deps.query?.setCompactSummaryActive?.(false);
|
|
@@ -1275,13 +1302,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1275
1302
|
* 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。
|
|
1276
1303
|
* 分组语义不受影响:activeRound 立即让位,新消息不会被并进已收尾的回合。
|
|
1277
1304
|
*/
|
|
1278
|
-
const endRound = (round: CompactRound, render = false): void => {
|
|
1305
|
+
const endRound = (round: CompactRound, render = false, endTimestamp?: number): void => {
|
|
1279
1306
|
if (!render || !round.active) {
|
|
1280
|
-
finalizeRound(round);
|
|
1307
|
+
finalizeRound(round, endTimestamp);
|
|
1281
1308
|
if (render) renderRound(round);
|
|
1282
1309
|
return;
|
|
1283
1310
|
}
|
|
1284
|
-
finalizeRound(round);
|
|
1311
|
+
finalizeRound(round, endTimestamp);
|
|
1285
1312
|
if (round.foldTimer !== undefined) return;
|
|
1286
1313
|
round.settleUntil = Date.now() + FOLD_SETTLE_MS;
|
|
1287
1314
|
pendingFoldRounds.add(round);
|
|
@@ -1462,7 +1489,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1462
1489
|
/** 取并入目标;旧回合被用户消息隔开时先收尾(静默期一转场,新回合立即接手)。 */
|
|
1463
1490
|
const joinOrCreateRound = (component: any, message: any): CompactRound => {
|
|
1464
1491
|
const joinable = joinableActiveRound(component);
|
|
1465
|
-
if (!joinable && activeRound) finishRound(activeRound);
|
|
1492
|
+
if (!joinable && activeRound) finishRound(activeRound, message?.timestamp);
|
|
1466
1493
|
return joinable ?? createRound(component, message);
|
|
1467
1494
|
};
|
|
1468
1495
|
|
|
@@ -1531,10 +1558,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1531
1558
|
};
|
|
1532
1559
|
|
|
1533
1560
|
/**
|
|
1534
|
-
*
|
|
1535
|
-
* transcript
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1561
|
+
* 尾行归位。目标位:活回合(运行中/收尾静默期)且未展开 → 容器最末,
|
|
1562
|
+
* 计数行钉在 transcript 可写底缘,运行中到达的提示行/custom 条目只能落在
|
|
1563
|
+
* 它上方,不会被顶离底部截成两行;其余(收尾回合、展开的回合)→ 回合最后
|
|
1564
|
+
* 一个成员之后,外来行统一换到摘要行之下。展开的回合不按 live 推到底:
|
|
1565
|
+
* 展开卡属于回合原位,推进容器末尾会把它挪到别的消息之间。
|
|
1566
|
+
* 尾行已在目标位就不动容器:展开/收起只换 tail.parts,无谓的重摘重插
|
|
1567
|
+
* 本身就是摘要行挪位的来源。
|
|
1538
1568
|
*/
|
|
1539
1569
|
const syncTailPosition = (round: CompactRound): void => {
|
|
1540
1570
|
const tail = round.tail;
|
|
@@ -1546,11 +1576,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1546
1576
|
if (!container || !Array.isArray(container.children)) return;
|
|
1547
1577
|
tail.container = container;
|
|
1548
1578
|
const children = container.children as any[];
|
|
1579
|
+
const isMember = roundMemberMatcher(round);
|
|
1549
1580
|
const current = children.indexOf(tail.host);
|
|
1581
|
+
// 摘尾行前先定目标位,已在位就整段跳过。
|
|
1582
|
+
let last = -1;
|
|
1583
|
+
for (let i = 0; i < children.length; i++) {
|
|
1584
|
+
if (children[i] === tail.host || !isMember(children[i])) continue;
|
|
1585
|
+
last = i;
|
|
1586
|
+
}
|
|
1587
|
+
const pinToEnd = roundLive(round) && round.anchor?.expanded !== true;
|
|
1588
|
+
const desired = pinToEnd || last < 0 ? children.length - 1 : last + 1;
|
|
1589
|
+
if (current === desired) return;
|
|
1550
1590
|
if (current >= 0) children.splice(current, 1);
|
|
1551
|
-
const isMember = roundMemberMatcher(round);
|
|
1552
1591
|
let first = -1;
|
|
1553
|
-
|
|
1592
|
+
last = -1;
|
|
1554
1593
|
for (let i = 0; i < children.length; i++) {
|
|
1555
1594
|
if (!isMember(children[i])) continue;
|
|
1556
1595
|
if (first < 0) first = i;
|
|
@@ -1565,7 +1604,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1565
1604
|
}
|
|
1566
1605
|
if (lastMember) last = children.indexOf(lastMember);
|
|
1567
1606
|
const insertAt = last < 0 ? children.length : last + 1;
|
|
1568
|
-
if (
|
|
1607
|
+
if (pinToEnd) {
|
|
1569
1608
|
// 活回合:外来行压到最后成员之后,尾行占容器最末。
|
|
1570
1609
|
children.splice(insertAt, 0, ...intruders);
|
|
1571
1610
|
children.push(tail.host);
|
|
@@ -1625,6 +1664,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1625
1664
|
}
|
|
1626
1665
|
if (!prior) return;
|
|
1627
1666
|
round.startedAt = prior.startedAt;
|
|
1667
|
+
if (typeof prior.wallStartedAt === "number") round.wallStartedAt = prior.wallStartedAt;
|
|
1628
1668
|
if (prior.detachedMessages.length) round.detachedMessages.push(...prior.detachedMessages);
|
|
1629
1669
|
if (!prior.active) {
|
|
1630
1670
|
round.active = false;
|
|
@@ -1640,7 +1680,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1640
1680
|
}
|
|
1641
1681
|
};
|
|
1642
1682
|
|
|
1643
|
-
/**
|
|
1683
|
+
/** 新建活回合:起点用消息真实时刻(回放里 Date.now 会把整轮挤在同一毫秒)。 */
|
|
1644
1684
|
const createRound = (anchor: any, message: any): CompactRound => {
|
|
1645
1685
|
const round: CompactRound = {
|
|
1646
1686
|
anchor,
|
|
@@ -1649,8 +1689,10 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1649
1689
|
active: true,
|
|
1650
1690
|
suppressedToolIds: new Set(),
|
|
1651
1691
|
liveSlotToolIds: new Set(),
|
|
1652
|
-
startedAt: Date.now(),
|
|
1692
|
+
startedAt: isUsableTimestamp(message?.timestamp) ? message.timestamp : Date.now(),
|
|
1693
|
+
wallStartedAt: Date.now(),
|
|
1653
1694
|
};
|
|
1695
|
+
liveRounds.add(round);
|
|
1654
1696
|
activateRound(round);
|
|
1655
1697
|
adoptPriorRound(round, message);
|
|
1656
1698
|
return round;
|
|
@@ -2014,11 +2056,36 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2014
2056
|
ensureRoundTick();
|
|
2015
2057
|
};
|
|
2016
2058
|
|
|
2017
|
-
|
|
2059
|
+
/** 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。 */
|
|
2060
|
+
const finishRound = (round: CompactRound, endTimestamp?: number): void =>
|
|
2061
|
+
endRound(round, true, endTimestamp);
|
|
2062
|
+
|
|
2063
|
+
/**
|
|
2064
|
+
* 重放/重建结束后的对账:mountedTails 里凡 tail.round 不在本 pass 回合名单的
|
|
2065
|
+
* 都是没被收编的孤儿(anchor 组件没了、消息换成了新对象、回放顺序错位),
|
|
2066
|
+
* 就地摘除。收编过的尾行 round 已过户到新 round,不在此列。
|
|
2067
|
+
*/
|
|
2068
|
+
const sweepOrphanTails = (): void => {
|
|
2069
|
+
for (const tail of [...mountedTails]) {
|
|
2070
|
+
const round = tail.round as CompactRound | undefined;
|
|
2071
|
+
// 回合还在本 pass 名单里、且没有同 anchor 的更新 tail 顶位时保留。
|
|
2072
|
+
if (round && liveRounds.has(round)) {
|
|
2073
|
+
const anchor = round.anchor;
|
|
2074
|
+
if (!anchor || tailByAnchor.get(anchor) === tail) continue;
|
|
2075
|
+
}
|
|
2076
|
+
removeTailHost(tail);
|
|
2077
|
+
mountedTails.delete(tail);
|
|
2078
|
+
if (round?.anchor && tailByAnchor.get(round.anchor) === tail) {
|
|
2079
|
+
tailByAnchor.delete(round.anchor);
|
|
2080
|
+
}
|
|
2081
|
+
if (round?.tail === tail) round.tail = undefined;
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2018
2084
|
|
|
2019
2085
|
const resetRounds = (): void => {
|
|
2020
2086
|
for (const round of [...pendingFoldRounds]) clearFoldTimer(round);
|
|
2021
2087
|
activeRound = undefined;
|
|
2088
|
+
liveRounds.clear();
|
|
2022
2089
|
roundByComponent = new WeakMap();
|
|
2023
2090
|
expandedRoundToolIds.clear();
|
|
2024
2091
|
explicitRoundToolIds.clear();
|
|
@@ -2083,20 +2150,18 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2083
2150
|
|
|
2084
2151
|
if (hasToolCalls) {
|
|
2085
2152
|
let round = roundByComponent.get(this);
|
|
2086
|
-
if (
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
if (activeRound) finishRound(activeRound);
|
|
2092
|
-
round = createRound(this, message);
|
|
2093
|
-
roundByComponent.set(this, round);
|
|
2094
|
-
} else if (!round) {
|
|
2095
|
-
// 中间隔着用户消息就另起一回合,不并进上一个回合。
|
|
2153
|
+
if (!round) {
|
|
2154
|
+
// 本条消息是否带 text 不是回合边界:pi 流式协议里同一条
|
|
2155
|
+
// assistant 消息会以 text+toolCall、toolCall-only 反复到达。
|
|
2156
|
+
// 只在「组件未挂进任何回合」时并入 activeRound;被用户消息
|
|
2157
|
+
// 隔开(steer/上回合没正文)时由 joinOrCreateRound 收尾另起。
|
|
2096
2158
|
round = joinOrCreateRound(this, message);
|
|
2097
2159
|
roundByComponent.set(this, round);
|
|
2098
2160
|
}
|
|
2099
2161
|
round.messages.set(this, message);
|
|
2162
|
+
if (isUsableTimestamp(message.timestamp) && message.timestamp < round.startedAt) {
|
|
2163
|
+
round.startedAt = message.timestamp;
|
|
2164
|
+
}
|
|
2100
2165
|
renderRound(round);
|
|
2101
2166
|
return undefined;
|
|
2102
2167
|
}
|
|
@@ -2111,17 +2176,17 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2111
2176
|
clearRoundAnchor(this);
|
|
2112
2177
|
round.detachedMessages.push(previousMessage);
|
|
2113
2178
|
roundByComponent.delete(this);
|
|
2114
|
-
finishRound(round);
|
|
2179
|
+
finishRound(round, message.timestamp);
|
|
2115
2180
|
return renderAssistantWithoutThinking(this, message);
|
|
2116
2181
|
}
|
|
2117
2182
|
if (round) {
|
|
2118
2183
|
// 围观态下 anchor 还挂着槽位卡,必须重绘才能收回。
|
|
2119
|
-
endRound(round, true);
|
|
2184
|
+
endRound(round, true, message.timestamp);
|
|
2120
2185
|
roundByComponent.delete(this);
|
|
2121
2186
|
clearRoundAnchor(this);
|
|
2122
2187
|
return passThroughAssistant(this, message);
|
|
2123
2188
|
}
|
|
2124
|
-
if (activeRound) finishRound(activeRound);
|
|
2189
|
+
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2125
2190
|
return renderAssistantWithoutThinking(this, message);
|
|
2126
2191
|
}
|
|
2127
2192
|
|
|
@@ -2133,6 +2198,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2133
2198
|
roundByComponent.set(this, round);
|
|
2134
2199
|
}
|
|
2135
2200
|
round.messages.set(this, message);
|
|
2201
|
+
if (isUsableTimestamp(message.timestamp) && message.timestamp < round.startedAt) {
|
|
2202
|
+
round.startedAt = message.timestamp;
|
|
2203
|
+
}
|
|
2136
2204
|
renderRound(round);
|
|
2137
2205
|
return undefined;
|
|
2138
2206
|
}
|
|
@@ -2140,7 +2208,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2140
2208
|
// 无可见内容的 abort/error/length:直接外层状态行,并结束进行中的回合。
|
|
2141
2209
|
const loneStatus = messageStopStatus(message);
|
|
2142
2210
|
if (loneStatus) {
|
|
2143
|
-
if (activeRound) finishRound(activeRound);
|
|
2211
|
+
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2144
2212
|
self.contentContainer.clear();
|
|
2145
2213
|
appendStopStatus(this, loneStatus);
|
|
2146
2214
|
return undefined;
|
|
@@ -2265,6 +2333,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2265
2333
|
if (config.mode === "compact") syncGlobalExpanded(ctx);
|
|
2266
2334
|
else hoveredAssistantComponent = undefined;
|
|
2267
2335
|
refreshTrackedComponents();
|
|
2336
|
+
sweepOrphanTails();
|
|
2268
2337
|
},
|
|
2269
2338
|
refresh() {
|
|
2270
2339
|
if (!patch.active) return;
|
|
@@ -2272,6 +2341,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2272
2341
|
patch.assertAssistantOwnership();
|
|
2273
2342
|
if (config.mode !== "compact") hoveredAssistantComponent = undefined;
|
|
2274
2343
|
refreshTrackedComponents();
|
|
2344
|
+
sweepOrphanTails();
|
|
2275
2345
|
},
|
|
2276
2346
|
assertOwnership() {
|
|
2277
2347
|
if (!patch.active) return;
|
|
@@ -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,
|
|
@@ -191,7 +192,7 @@ const FULLSCREEN_FOCUS_OUT = "\x1b[O";
|
|
|
191
192
|
|
|
192
193
|
let pendingCollapsePress: {
|
|
193
194
|
card: any;
|
|
194
|
-
/**
|
|
195
|
+
/** 命中的最内层组件:卡内工具行命中时用于触发官方 click 抑制窗口。 */
|
|
195
196
|
component: any;
|
|
196
197
|
col: number;
|
|
197
198
|
row: number;
|
|
@@ -240,22 +241,17 @@ function rememberCollapsePress(card: any, component: any, packet: SgrMousePacket
|
|
|
240
241
|
|
|
241
242
|
/**
|
|
242
243
|
* 松手结算:只有"按下与松开在同一格"的完整单击才收起,拖动选择文本时保持展开。
|
|
243
|
-
*
|
|
244
|
-
*
|
|
244
|
+
* 官方卡与自有卡统一在这里结算:fullscreen 下结算成功的 release 被 consume,
|
|
245
|
+
* 官方不会再合成 click(合成 click 要求按下/松开坐标完全一致,手抖一格就不发,
|
|
246
|
+
* 还会把刚收起的卡 toggle 回去、或误触卡内其他 MouseRegion)。
|
|
245
247
|
*/
|
|
246
|
-
function resolveCollapsePress(
|
|
247
|
-
tui: any,
|
|
248
|
-
packet: SgrMousePacket,
|
|
249
|
-
options: { skipOfficialCards: boolean },
|
|
250
|
-
): any {
|
|
248
|
+
function resolveCollapsePress(tui: any, packet: SgrMousePacket): any {
|
|
251
249
|
const press = pendingCollapsePress;
|
|
252
250
|
pendingCollapsePress = null;
|
|
253
251
|
if (!press || packet.final !== "m") return undefined;
|
|
254
252
|
if (Math.abs(packet.col - press.col) > COLLAPSE_CLICK_MOVE_TOLERANCE) return undefined;
|
|
255
253
|
if (Math.abs(packet.row - press.row) > COLLAPSE_CLICK_MOVE_TOLERANCE) return undefined;
|
|
256
254
|
if (Date.now() - press.at > COLLAPSE_CLICK_MAX_MS) return undefined;
|
|
257
|
-
if (options.skipOfficialCards && press.component instanceof ToolExecutionComponent)
|
|
258
|
-
return undefined;
|
|
259
255
|
const card = press.card;
|
|
260
256
|
if (
|
|
261
257
|
(card instanceof ToolGroupComponent || isCompactAssistantComponent(card)) &&
|
|
@@ -265,7 +261,7 @@ function resolveCollapsePress(
|
|
|
265
261
|
suppressOfficialCardClickUntil = Date.now() + SUPPRESS_OFFICIAL_CARD_CLICK_MS;
|
|
266
262
|
}
|
|
267
263
|
collapseExpandedCard(tui, card);
|
|
268
|
-
//
|
|
264
|
+
// 返回收起的卡:fullscreen 调用方据此吞掉这次 release 并清官方按下/选区状态。
|
|
269
265
|
return card;
|
|
270
266
|
}
|
|
271
267
|
|
|
@@ -398,7 +394,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
398
394
|
if (!target) return false;
|
|
399
395
|
const component = target.component;
|
|
400
396
|
// compact 展开面板内命中时 owner 是面板:收起对象与记账对象都归面板,
|
|
401
|
-
//
|
|
397
|
+
// 面板内工具行命中时 release 统一由扩展结算。
|
|
402
398
|
const card = target.group ?? target.owner ?? component;
|
|
403
399
|
const pressComponent = target.owner ?? component;
|
|
404
400
|
// 回到底部按钮:按组件引用命中,不依赖渲染行缓存。
|
|
@@ -474,8 +470,8 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
474
470
|
});
|
|
475
471
|
}
|
|
476
472
|
}
|
|
477
|
-
// 其余放行官方:选区、OSC8
|
|
478
|
-
//
|
|
473
|
+
// 其余放行官方:选区、OSC8 链接与官方按下登记都交给 renderer。
|
|
474
|
+
// 完整单击的收起统一在松手结算(release 被 consume,官方不会再合成 click)。
|
|
479
475
|
rememberCollapsePress(card, pressComponent, packet);
|
|
480
476
|
return false;
|
|
481
477
|
}
|
|
@@ -508,8 +504,8 @@ function keepViewportOnExpand(tui: any): void {
|
|
|
508
504
|
const view = tui.getPrimaryScrollView?.();
|
|
509
505
|
if (typeof view?.scrollTo !== "function" || typeof view.scrollTop !== "number") return;
|
|
510
506
|
view.scrollTo(view.scrollTop, { disableFollow: true });
|
|
511
|
-
//
|
|
512
|
-
if (view.isFollowingEnd === false) setScrollButtonVisible(true);
|
|
507
|
+
// 已脱离底部跟随:dock 模式亮出回到底部按钮,新输出到来时仍有入口。
|
|
508
|
+
if (view.isFollowingEnd === false && dockScrollButtonActive()) setScrollButtonVisible(true);
|
|
513
509
|
}
|
|
514
510
|
|
|
515
511
|
/**
|
|
@@ -584,6 +580,39 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
584
580
|
applyFullscreenHover(tui, target);
|
|
585
581
|
}
|
|
586
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
|
+
|
|
595
|
+
/**
|
|
596
|
+
* 鼠标包(1-based 屏幕坐标)是否落在某个已渲染 overlay 的矩形内。
|
|
597
|
+
* renderedOverlayLayouts 与官方 dispatchMouseToOverlay 同源:只含上一帧画出的可见 overlay。
|
|
598
|
+
*/
|
|
599
|
+
function packetInsideOverlay(tui: any, packet: SgrMousePacket): boolean {
|
|
600
|
+
const layouts = tui?.renderedOverlayLayouts;
|
|
601
|
+
if (!Array.isArray(layouts)) return false;
|
|
602
|
+
const x = packet.col - 1;
|
|
603
|
+
const y = packet.row - 1;
|
|
604
|
+
for (const layout of layouts) {
|
|
605
|
+
if (
|
|
606
|
+
x >= layout.col &&
|
|
607
|
+
x < layout.col + layout.width &&
|
|
608
|
+
y >= layout.row &&
|
|
609
|
+
y < layout.row + layout.height
|
|
610
|
+
)
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
587
616
|
/**
|
|
588
617
|
* 实例级包装 TuiAltScreen.handleViewportInput(惰性 Proxy 安全):
|
|
589
618
|
* 原型方法取 original(绕开 proxy 函数包装),实例 own property 装 wrapper
|
|
@@ -602,24 +631,33 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
602
631
|
// 官方 viewport 会消费键盘,扩展监听器无法补偿,必须在这里调度。
|
|
603
632
|
scheduleScrollButtonSync(tui, data);
|
|
604
633
|
const packets = parseSgrMousePackets(data);
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
634
|
+
// overlay(文本预览/context 弹框,都是真 overlay)可见时:落在
|
|
635
|
+
// overlay 矩形内的包经 packetInsideOverlay 放行官方
|
|
636
|
+
// dispatchMouseToOverlay → overlay.handleMouse;overlay 外的包仍
|
|
637
|
+
// 走扩展收起/展开/hover。两条路径各管各的。
|
|
638
|
+
if (packets) {
|
|
639
|
+
// 布局还没出来或仍是上一帧:不要把点击收成工具展开。返回 undefined
|
|
640
|
+
// 让焦点 overlay 的 handleInput 拿到原始 SGR;官方链也靠这份布局,调它会消费且打到 transcript。
|
|
641
|
+
if (tui.hasOverlay?.() && !overlayLayoutsCurrent(tui)) return undefined;
|
|
642
|
+
const overlayPackets = tui.hasOverlay?.()
|
|
643
|
+
? packets.filter((packet) => packetInsideOverlay(tui, packet))
|
|
644
|
+
: undefined;
|
|
645
|
+
const insideOverlay = (packet: SgrMousePacket) => overlayPackets?.includes(packet) === true;
|
|
609
646
|
for (const packet of packets) {
|
|
647
|
+
if (insideOverlay(packet)) continue;
|
|
610
648
|
if (isSgrLeftRelease(packet)) {
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
) {
|
|
649
|
+
// 结算单击收起:成功时吞掉 release,官方不会再对同一次点击合成
|
|
650
|
+
// click(官方 click 要求按下/松开坐标严格一致,还会把刚收起的卡
|
|
651
|
+
// toggle 回去,或误触卡内其他 MouseRegion 区)。
|
|
652
|
+
if (resolveCollapsePress(tui, packet) !== undefined) {
|
|
653
|
+
// 收起会重排布局:官方还留着按下时登记的 press 目标与选区锚点,
|
|
654
|
+
// 发 FOCUS_OUT 一次清干净,避免旧坐标结算出误选/误发 click。
|
|
618
655
|
try {
|
|
619
656
|
Reflect.apply(original, this, [FULLSCREEN_FOCUS_OUT]);
|
|
620
657
|
} catch {
|
|
621
658
|
/* 老版本没有选区状态 */
|
|
622
659
|
}
|
|
660
|
+
return { consume: true };
|
|
623
661
|
}
|
|
624
662
|
} else if (!isSgrLeftPress(packet) && !isSgrIdleMotion(packet))
|
|
625
663
|
clearPendingCollapsePressOnMove(packet);
|
|
@@ -965,12 +1003,15 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
965
1003
|
scheduleScrollButtonSync(getToolMouseTui(), data);
|
|
966
1004
|
return undefined;
|
|
967
1005
|
}
|
|
1006
|
+
// 文本预览活动时,点击交给预览自己的 handleInput(esc/copy/滚动条),
|
|
1007
|
+
// 不被残留的工具 region 吞掉;预览关闭后恢复。
|
|
1008
|
+
if (hasActiveTextPreview()) return undefined;
|
|
968
1009
|
|
|
969
1010
|
let consumed = false;
|
|
970
1011
|
for (const packet of packets) {
|
|
971
1012
|
updateToolSummaryHover(getToolMouseTui(), packet);
|
|
972
1013
|
if (isSgrLeftRelease(packet)) {
|
|
973
|
-
resolveCollapsePress(getToolMouseTui(), packet
|
|
1014
|
+
resolveCollapsePress(getToolMouseTui(), packet);
|
|
974
1015
|
continue;
|
|
975
1016
|
}
|
|
976
1017
|
if (!isSgrLeftPress(packet)) {
|