pi-cc-extensions 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +35 -35
- package/README.md +3 -3
- package/extensions/config/config.ts +5 -0
- package/extensions/config/panel.ts +22 -0
- package/extensions/feature/context.ts +339 -149
- package/extensions/feature/reference/index.ts +2 -0
- package/extensions/feature/reference/subagent.ts +2 -0
- package/extensions/feature/shell/footer.ts +1 -0
- package/extensions/feature/shell/token-rate.ts +15 -3
- package/extensions/feature/shell/working-message.ts +0 -19
- package/extensions/renderer/compact-mode.ts +152 -49
- package/extensions/renderer/index.ts +3 -0
- package/extensions/renderer/mouse/interaction.ts +53 -26
- package/extensions/renderer/tool/diff/write-execution.ts +2 -2
- package/package.json +1 -1
|
@@ -432,6 +432,8 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
432
432
|
});
|
|
433
433
|
|
|
434
434
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
435
|
+
// Pi 1.2.0 custom events have no prompt. Older hosts omit messageType.
|
|
436
|
+
if (!("prompt" in event)) return;
|
|
435
437
|
const referenceIds = extractSessionReferenceIds(event.prompt);
|
|
436
438
|
if (referenceIds.length === 0) return;
|
|
437
439
|
|
|
@@ -208,6 +208,8 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
|
|
|
208
208
|
|
|
209
209
|
// Inject as a request message, not systemPrompt, so the cached prefix stays stable.
|
|
210
210
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
211
|
+
// Pi 1.2.0 custom events have no prompt. Older hosts omit messageType.
|
|
212
|
+
if (!("prompt" in event)) return;
|
|
211
213
|
const agents = getAgents();
|
|
212
214
|
if (agents.length === 0) return;
|
|
213
215
|
|
|
@@ -182,11 +182,23 @@ export function currentTtftMs(): number {
|
|
|
182
182
|
return Math.max(0, firstTokenAt - runStartTime);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/**
|
|
186
|
-
export function
|
|
185
|
+
/** 不足 1 秒用毫秒,否则用一位小数的秒。 */
|
|
186
|
+
export function formatTtft(ttftMs: number): string {
|
|
187
|
+
if (ttftMs < 1000) return `${Math.round(ttftMs)}ms`;
|
|
188
|
+
return `${(ttftMs / 1000).toFixed(1)}s`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `avg 1.2k tok/s (TTFT: 812ms)`;无数据显示空串,未出首 token 或 showTtft 为 false 时无括号段。 */
|
|
192
|
+
export function formatTokenRate(
|
|
193
|
+
rate: number,
|
|
194
|
+
ttftMs = 0,
|
|
195
|
+
mode: TokenRateMode = "average",
|
|
196
|
+
showTtft = true,
|
|
197
|
+
): string {
|
|
187
198
|
if (!Number.isFinite(rate) || rate <= 0) return "";
|
|
188
199
|
const value = rate >= 1000 ? `${(rate / 1000).toFixed(1)}k` : `${Math.round(rate)}`;
|
|
189
|
-
const ttft =
|
|
200
|
+
const ttft =
|
|
201
|
+
showTtft && Number.isFinite(ttftMs) && ttftMs > 0 ? ` (TTFT: ${formatTtft(ttftMs)})` : "";
|
|
190
202
|
const label = mode === "live" ? "live" : "avg";
|
|
191
203
|
return `${label} ${value} tok/s${ttft}`;
|
|
192
204
|
}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { getCompactRunStatusText } from "../../renderer/compact-mode.ts";
|
|
3
|
-
import {
|
|
4
|
-
fullscreenLazyTui,
|
|
5
|
-
getToolMouseTui,
|
|
6
|
-
isFullscreenAtBottom,
|
|
7
|
-
} from "../../renderer/mouse/scroll.ts";
|
|
8
2
|
import { formatDuration, formatTokenCount } from "../../utils/format.ts";
|
|
9
3
|
import {
|
|
10
4
|
resetTokenRate,
|
|
@@ -60,23 +54,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
60
54
|
if (output > 0) recordProviderUsage(output);
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
/**
|
|
64
|
-
* 摘要行滚出视口才镜像:仅 fullscreen 且已离开 transcript 底部。
|
|
65
|
-
* regular 没有“离开底部”信号(transcript 在终端回滚区),保持 Pi 默认文案。
|
|
66
|
-
*/
|
|
67
|
-
function compactMirrorText(): string | undefined {
|
|
68
|
-
const tui = getToolMouseTui();
|
|
69
|
-
if (!tui || !fullscreenLazyTui(tui) || isFullscreenAtBottom(tui)) return undefined;
|
|
70
|
-
return getCompactRunStatusText();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
57
|
function buildWorkingMessage(): string {
|
|
74
58
|
const parts: string[] = [];
|
|
75
59
|
const tokens = tokenCount();
|
|
76
60
|
if (tokens > 0) parts.push(`↓ ${formatTokenCount(tokens)} tokens`);
|
|
77
|
-
// compact 活动回合且已滚出摘要行:直接用摘要行文案(自带回合时长,不叠 agent 计时)。
|
|
78
|
-
const compactStatus = compactMirrorText();
|
|
79
|
-
if (compactStatus) return [compactStatus, ...parts].join(" · ");
|
|
80
61
|
const elapsed = Date.now() - (agentStartTime || turnStartTime);
|
|
81
62
|
if (elapsed >= SHOW_TIMER_AFTER_MS || tokens > 0) {
|
|
82
63
|
// formatDuration 低于 1 秒返回 "",此处回退 "0s" 保持计时器连续跳动。
|
|
@@ -592,6 +592,11 @@ export type CompactModeHooks = {
|
|
|
592
592
|
assertOwnership(): void;
|
|
593
593
|
/** 重新渲染包含指定 toolCallId 的 assistant 消息(思考收尾时刷新)。 */
|
|
594
594
|
refreshToolCallMessage(toolCallId: string | undefined): void;
|
|
595
|
+
/**
|
|
596
|
+
* 收尾 resume 重放留下的活动回合。重放是同步的,回合之间靠下一条消息收口,
|
|
597
|
+
* 只有最后一条没有后续消息,会停在 Running。
|
|
598
|
+
*/
|
|
599
|
+
settleReplayedRound(): void;
|
|
595
600
|
shutdown(): void;
|
|
596
601
|
};
|
|
597
602
|
|
|
@@ -618,17 +623,6 @@ const trackedAssistantComponents = new Set<any>();
|
|
|
618
623
|
const trackedToolComponents = new Set<any>();
|
|
619
624
|
let hoveredAssistantComponent: any;
|
|
620
625
|
|
|
621
|
-
/**
|
|
622
|
-
* 活动回合摘要 getter:常驻状态行(pi working row)镜像用。
|
|
623
|
-
* installCompactMode 注册、卸载时清空,直接读 activeRound,不持有回合闭包。
|
|
624
|
-
*/
|
|
625
|
-
let compactRunStatusGetter: (() => string | undefined) | undefined;
|
|
626
|
-
|
|
627
|
-
/** 活动回合的摘要文本(`Running... · 9s, bash×1`);无活动回合返回 undefined。 */
|
|
628
|
-
export function getCompactRunStatusText(): string | undefined {
|
|
629
|
-
return compactRunStatusGetter?.();
|
|
630
|
-
}
|
|
631
|
-
|
|
632
626
|
/**
|
|
633
627
|
* 回合尾行记录:摘要行(live 时连槽位卡)挂成 transcript 容器里回合末尾的
|
|
634
628
|
* 兄弟组件,而不是 anchor 内部。anchor 内部的可变内容一旦被工具/diff 顶出视口
|
|
@@ -1196,6 +1190,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1196
1190
|
liveSlotToolIds: Set<string>;
|
|
1197
1191
|
/** 收尾后的静默期截止时间戳;期间仍按围观态渲染,到点才收拢。 */
|
|
1198
1192
|
settleUntil?: number;
|
|
1193
|
+
/** 消息已有 toolCall、工具组件还没到:等它一 updateDisplay 就重画槽位。 */
|
|
1194
|
+
pendingLiveTool?: boolean;
|
|
1199
1195
|
foldTimer?: ReturnType<typeof setTimeout>;
|
|
1200
1196
|
/** 回合挂钟起点,保证 Running 时长连续递增。 */
|
|
1201
1197
|
startedAt: number;
|
|
@@ -1205,6 +1201,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1205
1201
|
};
|
|
1206
1202
|
let activeRound: CompactRound | undefined;
|
|
1207
1203
|
let roundByComponent = new WeakMap<object, CompactRound>();
|
|
1204
|
+
/** 本 pass 建起的回合(含被收编的):清扫孤儿尾行的在册名单。 */
|
|
1205
|
+
const liveRounds = new Set<CompactRound>();
|
|
1208
1206
|
const expandedRoundToolIds = new Set<string>();
|
|
1209
1207
|
/** 用户显式点开的 round 内工具;强制折叠不压回这些 id。 */
|
|
1210
1208
|
const explicitRoundToolIds = new Set<string>();
|
|
@@ -1263,11 +1261,30 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1263
1261
|
pendingFoldRounds.delete(round);
|
|
1264
1262
|
};
|
|
1265
1263
|
|
|
1266
|
-
/**
|
|
1267
|
-
const
|
|
1264
|
+
/** 供采纳为回合边界的 timestamp:必须是看起来像 epoch 毫秒的数(桩数据/秒级会被拒)。 */
|
|
1265
|
+
const isUsableTimestamp = (value: unknown): value is number =>
|
|
1266
|
+
typeof value === "number" &&
|
|
1267
|
+
Number.isFinite(value) &&
|
|
1268
|
+
value >= 1_600_000_000_000 &&
|
|
1269
|
+
value <= Date.now() + 60_000;
|
|
1270
|
+
|
|
1271
|
+
/** 结束回合活动态;不做任何重绘。endTimestamp 用后一段内容的真实时刻收边(回放里
|
|
1272
|
+
* 同一毫秒完成的 create/finalize 差不出真实耗时);缺省取回合内消息的最大 timestamp。 */
|
|
1273
|
+
const finalizeRound = (round: CompactRound, endTimestamp?: number): void => {
|
|
1268
1274
|
clearFoldTimer(round);
|
|
1269
1275
|
round.active = false;
|
|
1270
|
-
if (round.endedAt === undefined)
|
|
1276
|
+
if (round.endedAt === undefined) {
|
|
1277
|
+
let fallback = 0;
|
|
1278
|
+
for (const message of roundMessages(round)) {
|
|
1279
|
+
if (isUsableTimestamp(message?.timestamp) && message.timestamp > fallback) {
|
|
1280
|
+
fallback = message.timestamp;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
round.endedAt = Math.max(
|
|
1284
|
+
round.startedAt,
|
|
1285
|
+
isUsableTimestamp(endTimestamp) ? endTimestamp : fallback || Date.now(),
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1271
1288
|
if (activeRound === round) {
|
|
1272
1289
|
activeRound = undefined;
|
|
1273
1290
|
deps.query?.setCompactSummaryActive?.(false);
|
|
@@ -1279,13 +1296,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1279
1296
|
* 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。
|
|
1280
1297
|
* 分组语义不受影响:activeRound 立即让位,新消息不会被并进已收尾的回合。
|
|
1281
1298
|
*/
|
|
1282
|
-
const endRound = (round: CompactRound, render = false): void => {
|
|
1299
|
+
const endRound = (round: CompactRound, render = false, endTimestamp?: number): void => {
|
|
1283
1300
|
if (!render || !round.active) {
|
|
1284
|
-
finalizeRound(round);
|
|
1301
|
+
finalizeRound(round, endTimestamp);
|
|
1285
1302
|
if (render) renderRound(round);
|
|
1286
1303
|
return;
|
|
1287
1304
|
}
|
|
1288
|
-
finalizeRound(round);
|
|
1305
|
+
finalizeRound(round, endTimestamp);
|
|
1289
1306
|
if (round.foldTimer !== undefined) return;
|
|
1290
1307
|
round.settleUntil = Date.now() + FOLD_SETTLE_MS;
|
|
1291
1308
|
pendingFoldRounds.add(round);
|
|
@@ -1357,12 +1374,6 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1357
1374
|
...round.detachedMessages,
|
|
1358
1375
|
];
|
|
1359
1376
|
|
|
1360
|
-
/** 镜像给常驻状态行的文案:只认当前活动回合(摘要行被顶出视口时由 working-message 取用)。 */
|
|
1361
|
-
compactRunStatusGetter = () => {
|
|
1362
|
-
const round = activeRound;
|
|
1363
|
-
return round?.active ? summarize(roundMessages(round), true, round) || undefined : undefined;
|
|
1364
|
-
};
|
|
1365
|
-
|
|
1366
1377
|
const roundToolCallIds = (round: CompactRound): Set<string> => {
|
|
1367
1378
|
const ids = new Set<string>();
|
|
1368
1379
|
for (const message of roundMessages(round)) {
|
|
@@ -1373,6 +1384,26 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1373
1384
|
return ids;
|
|
1374
1385
|
};
|
|
1375
1386
|
|
|
1387
|
+
/**
|
|
1388
|
+
* 摘要行有东西可汇总才挂:普通工具(edit/write/ask 之外)或非空思考。
|
|
1389
|
+
* 只剩外层卡的回合不挂——那些卡各自能展开,空摘要点开只会跳位。
|
|
1390
|
+
*/
|
|
1391
|
+
const roundHasSummaryContent = (round: CompactRound): boolean => {
|
|
1392
|
+
for (const message of roundMessages(round)) {
|
|
1393
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1394
|
+
if (item?.type === "thinking" && String(item.thinking ?? "").trim()) return true;
|
|
1395
|
+
if (
|
|
1396
|
+
item?.type === "toolCall" &&
|
|
1397
|
+
typeof item.name === "string" &&
|
|
1398
|
+
!OUTER_TOOLS.has(item.name)
|
|
1399
|
+
) {
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return false;
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1376
1407
|
/**
|
|
1377
1408
|
* anchor 所在的 transcript 容器(pi chatContainer)。
|
|
1378
1409
|
* 优先走分组补丁在 addChild 时挂的父指针;没有则沿 tui 组件树找直属父容器。
|
|
@@ -1452,7 +1483,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1452
1483
|
/** 取并入目标;旧回合被用户消息隔开时先收尾(静默期一转场,新回合立即接手)。 */
|
|
1453
1484
|
const joinOrCreateRound = (component: any, message: any): CompactRound => {
|
|
1454
1485
|
const joinable = joinableActiveRound(component);
|
|
1455
|
-
if (!joinable && activeRound) finishRound(activeRound);
|
|
1486
|
+
if (!joinable && activeRound) finishRound(activeRound, message?.timestamp);
|
|
1456
1487
|
return joinable ?? createRound(component, message);
|
|
1457
1488
|
};
|
|
1458
1489
|
|
|
@@ -1521,10 +1552,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1521
1552
|
};
|
|
1522
1553
|
|
|
1523
1554
|
/**
|
|
1524
|
-
*
|
|
1525
|
-
* transcript
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1555
|
+
* 尾行归位。目标位:活回合(运行中/收尾静默期)且未展开 → 容器最末,
|
|
1556
|
+
* 计数行钉在 transcript 可写底缘,运行中到达的提示行/custom 条目只能落在
|
|
1557
|
+
* 它上方,不会被顶离底部截成两行;其余(收尾回合、展开的回合)→ 回合最后
|
|
1558
|
+
* 一个成员之后,外来行统一换到摘要行之下。展开的回合不按 live 推到底:
|
|
1559
|
+
* 展开卡属于回合原位,推进容器末尾会把它挪到别的消息之间。
|
|
1560
|
+
* 尾行已在目标位就不动容器:展开/收起只换 tail.parts,无谓的重摘重插
|
|
1561
|
+
* 本身就是摘要行挪位的来源。
|
|
1528
1562
|
*/
|
|
1529
1563
|
const syncTailPosition = (round: CompactRound): void => {
|
|
1530
1564
|
const tail = round.tail;
|
|
@@ -1536,11 +1570,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1536
1570
|
if (!container || !Array.isArray(container.children)) return;
|
|
1537
1571
|
tail.container = container;
|
|
1538
1572
|
const children = container.children as any[];
|
|
1573
|
+
const isMember = roundMemberMatcher(round);
|
|
1539
1574
|
const current = children.indexOf(tail.host);
|
|
1575
|
+
// 摘尾行前先定目标位,已在位就整段跳过。
|
|
1576
|
+
let last = -1;
|
|
1577
|
+
for (let i = 0; i < children.length; i++) {
|
|
1578
|
+
if (children[i] === tail.host || !isMember(children[i])) continue;
|
|
1579
|
+
last = i;
|
|
1580
|
+
}
|
|
1581
|
+
const pinToEnd = roundLive(round) && round.anchor?.expanded !== true;
|
|
1582
|
+
const desired = pinToEnd || last < 0 ? children.length - 1 : last + 1;
|
|
1583
|
+
if (current === desired) return;
|
|
1540
1584
|
if (current >= 0) children.splice(current, 1);
|
|
1541
|
-
const isMember = roundMemberMatcher(round);
|
|
1542
1585
|
let first = -1;
|
|
1543
|
-
|
|
1586
|
+
last = -1;
|
|
1544
1587
|
for (let i = 0; i < children.length; i++) {
|
|
1545
1588
|
if (!isMember(children[i])) continue;
|
|
1546
1589
|
if (first < 0) first = i;
|
|
@@ -1555,7 +1598,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1555
1598
|
}
|
|
1556
1599
|
if (lastMember) last = children.indexOf(lastMember);
|
|
1557
1600
|
const insertAt = last < 0 ? children.length : last + 1;
|
|
1558
|
-
if (
|
|
1601
|
+
if (pinToEnd) {
|
|
1559
1602
|
// 活回合:外来行压到最后成员之后,尾行占容器最末。
|
|
1560
1603
|
children.splice(insertAt, 0, ...intruders);
|
|
1561
1604
|
children.push(tail.host);
|
|
@@ -1581,10 +1624,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1581
1624
|
const syncTailForTool = (toolCallId: string): void => {
|
|
1582
1625
|
if (!toolCallId) return;
|
|
1583
1626
|
for (const round of [activeRound, ...pendingFoldRounds]) {
|
|
1584
|
-
if (round
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1627
|
+
if (!round || !roundToolCallIds(round).has(toolCallId)) continue;
|
|
1628
|
+
// 槽位是在消息更新时画的,当时工具组件还不存在:组件到了就补上。
|
|
1629
|
+
// 参数流式更新也走这里,槽位卡跟着换内容。
|
|
1630
|
+
if (round.pendingLiveTool || roundLive(round)) renderRound(round);
|
|
1631
|
+
else if (round.tail) syncTailPosition(round);
|
|
1632
|
+
return;
|
|
1588
1633
|
}
|
|
1589
1634
|
};
|
|
1590
1635
|
|
|
@@ -1628,7 +1673,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1628
1673
|
}
|
|
1629
1674
|
};
|
|
1630
1675
|
|
|
1631
|
-
/**
|
|
1676
|
+
/** 新建活回合:起点用消息真实时刻(回放里 Date.now 会把整轮挤在同一毫秒)。 */
|
|
1632
1677
|
const createRound = (anchor: any, message: any): CompactRound => {
|
|
1633
1678
|
const round: CompactRound = {
|
|
1634
1679
|
anchor,
|
|
@@ -1637,8 +1682,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1637
1682
|
active: true,
|
|
1638
1683
|
suppressedToolIds: new Set(),
|
|
1639
1684
|
liveSlotToolIds: new Set(),
|
|
1640
|
-
startedAt: Date.now(),
|
|
1685
|
+
startedAt: isUsableTimestamp(message?.timestamp) ? message.timestamp : Date.now(),
|
|
1641
1686
|
};
|
|
1687
|
+
liveRounds.add(round);
|
|
1642
1688
|
activateRound(round);
|
|
1643
1689
|
adoptPriorRound(round, message);
|
|
1644
1690
|
return round;
|
|
@@ -1728,6 +1774,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1728
1774
|
}
|
|
1729
1775
|
let runningTool: any;
|
|
1730
1776
|
let lastTool: any;
|
|
1777
|
+
let missingTool = false;
|
|
1731
1778
|
for (const [, message] of round.messages) {
|
|
1732
1779
|
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1733
1780
|
if (item?.type !== "toolCall" || typeof item.id !== "string") continue;
|
|
@@ -1736,11 +1783,17 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1736
1783
|
if (OUTER_TOOLS.has(String(tool?.toolName ?? item.name ?? ""))) continue;
|
|
1737
1784
|
round.liveSlotToolIds.add(item.id);
|
|
1738
1785
|
liveSlotToolIds.add(item.id);
|
|
1739
|
-
|
|
1786
|
+
// pi 的 message_update 先 updateContent 再建工具组件:这一帧计数已经算上,
|
|
1787
|
+
// 组件要等 updateDisplay 才能进槽位。记下来,组件一到就补画。
|
|
1788
|
+
if (!tool) {
|
|
1789
|
+
missingTool = true;
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1740
1792
|
lastTool = tool;
|
|
1741
1793
|
if (isRunningTool(tool)) runningTool = tool;
|
|
1742
1794
|
}
|
|
1743
1795
|
}
|
|
1796
|
+
round.pendingLiveTool = missingTool;
|
|
1744
1797
|
// 槽位单块:活动思考最新;否则本回合最近的工具(运行中原位增长,完成也留着)。
|
|
1745
1798
|
const slotTool = thinkingKids.length > 0 ? undefined : (runningTool ?? lastTool);
|
|
1746
1799
|
const cardItems: Array<{ child?: any; tool?: any }> = slotTool
|
|
@@ -1795,6 +1848,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1795
1848
|
}
|
|
1796
1849
|
for (const id of round.liveSlotToolIds) liveSlotToolIds.delete(id);
|
|
1797
1850
|
round.liveSlotToolIds.clear();
|
|
1851
|
+
// pendingLiveTool 留给 renderRoundLive 重算:这里清掉的话,组件刚到的那次
|
|
1852
|
+
// 补画会把自己的触发条件抹掉,槽位仍是空的。
|
|
1798
1853
|
|
|
1799
1854
|
for (const [component, message] of round.messages) {
|
|
1800
1855
|
component.lastMessage = message;
|
|
@@ -1816,6 +1871,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1816
1871
|
};
|
|
1817
1872
|
}
|
|
1818
1873
|
|
|
1874
|
+
if (round.anchor.expanded === true) {
|
|
1875
|
+
// 没有可汇总内容的回合不进展开态:摘要行不存在,展开只会把正文搬回回合开头。
|
|
1876
|
+
if (!roundHasSummaryContent(round)) {
|
|
1877
|
+
for (const member of round.messages.keys()) member.expanded = false;
|
|
1878
|
+
round.anchor.expanded = false;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1819
1881
|
if (round.anchor.expanded === true) {
|
|
1820
1882
|
// 展开内容留在摘要行那个位置:回合最后一个成员(含 write/edit)之后。
|
|
1821
1883
|
// 放进 anchor 内部会跳到回合起点,盖到中间的 write/edit 上面。
|
|
@@ -1959,8 +2021,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1959
2021
|
for (const [component, message] of round.messages) {
|
|
1960
2022
|
if (component === round.anchor) {
|
|
1961
2023
|
renderAssistantWithoutThinking(component, message);
|
|
1962
|
-
// 空摘要不挂行(getter 在 Running
|
|
1963
|
-
|
|
2024
|
+
// 空摘要不挂行(getter 在 Running 启动瞬间也可能短暂为空)。
|
|
2025
|
+
// 只剩外层卡的回合同样不挂:摘要行没有可汇总的内容。
|
|
2026
|
+
if ((summary || round.active) && (round.active || roundHasSummaryContent(round))) {
|
|
1964
2027
|
mountRoundTail(round, (identity) => [
|
|
1965
2028
|
compactAssistantLineComponent(identity, getSummary, deps.query),
|
|
1966
2029
|
]);
|
|
@@ -1985,11 +2048,32 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1985
2048
|
ensureRoundTick();
|
|
1986
2049
|
};
|
|
1987
2050
|
|
|
1988
|
-
|
|
2051
|
+
/** 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。 */
|
|
2052
|
+
const finishRound = (round: CompactRound, endTimestamp?: number): void =>
|
|
2053
|
+
endRound(round, true, endTimestamp);
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* 重放/重建结束后的对账:mountedTails 里凡 tail.round 不在本 pass 回合名单的
|
|
2057
|
+
* 都是没被收编的孤儿(anchor 组件没了、消息换成了新对象、回放顺序错位),
|
|
2058
|
+
* 就地摘除。收编过的尾行 round 已过户到新 round,不在此列。
|
|
2059
|
+
*/
|
|
2060
|
+
const sweepOrphanTails = (): void => {
|
|
2061
|
+
for (const tail of [...mountedTails]) {
|
|
2062
|
+
const round = tail.round as CompactRound | undefined;
|
|
2063
|
+
if (round && liveRounds.has(round)) continue;
|
|
2064
|
+
removeTailHost(tail);
|
|
2065
|
+
mountedTails.delete(tail);
|
|
2066
|
+
if (round?.anchor && tailByAnchor.get(round.anchor) === tail) {
|
|
2067
|
+
tailByAnchor.delete(round.anchor);
|
|
2068
|
+
}
|
|
2069
|
+
if (round?.tail === tail) round.tail = undefined;
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
1989
2072
|
|
|
1990
2073
|
const resetRounds = (): void => {
|
|
1991
2074
|
for (const round of [...pendingFoldRounds]) clearFoldTimer(round);
|
|
1992
2075
|
activeRound = undefined;
|
|
2076
|
+
liveRounds.clear();
|
|
1993
2077
|
roundByComponent = new WeakMap();
|
|
1994
2078
|
expandedRoundToolIds.clear();
|
|
1995
2079
|
explicitRoundToolIds.clear();
|
|
@@ -2059,7 +2143,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2059
2143
|
round.messages.delete(this);
|
|
2060
2144
|
clearRoundAnchor(this);
|
|
2061
2145
|
}
|
|
2062
|
-
if (activeRound) finishRound(activeRound);
|
|
2146
|
+
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2063
2147
|
round = createRound(this, message);
|
|
2064
2148
|
roundByComponent.set(this, round);
|
|
2065
2149
|
} else if (!round) {
|
|
@@ -2068,6 +2152,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2068
2152
|
roundByComponent.set(this, round);
|
|
2069
2153
|
}
|
|
2070
2154
|
round.messages.set(this, message);
|
|
2155
|
+
if (isUsableTimestamp(message.timestamp) && message.timestamp < round.startedAt) {
|
|
2156
|
+
round.startedAt = message.timestamp;
|
|
2157
|
+
}
|
|
2071
2158
|
renderRound(round);
|
|
2072
2159
|
return undefined;
|
|
2073
2160
|
}
|
|
@@ -2082,17 +2169,17 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2082
2169
|
clearRoundAnchor(this);
|
|
2083
2170
|
round.detachedMessages.push(previousMessage);
|
|
2084
2171
|
roundByComponent.delete(this);
|
|
2085
|
-
finishRound(round);
|
|
2172
|
+
finishRound(round, message.timestamp);
|
|
2086
2173
|
return renderAssistantWithoutThinking(this, message);
|
|
2087
2174
|
}
|
|
2088
2175
|
if (round) {
|
|
2089
2176
|
// 围观态下 anchor 还挂着槽位卡,必须重绘才能收回。
|
|
2090
|
-
endRound(round, true);
|
|
2177
|
+
endRound(round, true, message.timestamp);
|
|
2091
2178
|
roundByComponent.delete(this);
|
|
2092
2179
|
clearRoundAnchor(this);
|
|
2093
2180
|
return passThroughAssistant(this, message);
|
|
2094
2181
|
}
|
|
2095
|
-
if (activeRound) finishRound(activeRound);
|
|
2182
|
+
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2096
2183
|
return renderAssistantWithoutThinking(this, message);
|
|
2097
2184
|
}
|
|
2098
2185
|
|
|
@@ -2104,6 +2191,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2104
2191
|
roundByComponent.set(this, round);
|
|
2105
2192
|
}
|
|
2106
2193
|
round.messages.set(this, message);
|
|
2194
|
+
if (isUsableTimestamp(message.timestamp) && message.timestamp < round.startedAt) {
|
|
2195
|
+
round.startedAt = message.timestamp;
|
|
2196
|
+
}
|
|
2107
2197
|
renderRound(round);
|
|
2108
2198
|
return undefined;
|
|
2109
2199
|
}
|
|
@@ -2111,7 +2201,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2111
2201
|
// 无可见内容的 abort/error/length:直接外层状态行,并结束进行中的回合。
|
|
2112
2202
|
const loneStatus = messageStopStatus(message);
|
|
2113
2203
|
if (loneStatus) {
|
|
2114
|
-
if (activeRound) finishRound(activeRound);
|
|
2204
|
+
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2115
2205
|
self.contentContainer.clear();
|
|
2116
2206
|
appendStopStatus(this, loneStatus);
|
|
2117
2207
|
return undefined;
|
|
@@ -2152,8 +2242,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2152
2242
|
|
|
2153
2243
|
patch.toolInstalledUpdateDisplay = function (this: any) {
|
|
2154
2244
|
const id = String(this.toolCallId ?? "");
|
|
2155
|
-
if (
|
|
2156
|
-
|
|
2245
|
+
if (
|
|
2246
|
+
patch.active &&
|
|
2247
|
+
config.mode === "compact" &&
|
|
2248
|
+
(expandedRoundToolIds.has(id) || liveSlotToolIds.has(id))
|
|
2249
|
+
) {
|
|
2250
|
+
// 用户点开的保留展开;其余(含全局展开、live 槽位接管的)继续强制折叠。
|
|
2251
|
+
// 槽位卡在消息更新时就接管了 id,组件后到时 setExpanded 会把它翻开,外层多出一张卡。
|
|
2157
2252
|
if (this.expanded !== true) explicitRoundToolIds.delete(id);
|
|
2158
2253
|
else if (!explicitRoundToolIds.has(id)) this.expanded = false;
|
|
2159
2254
|
}
|
|
@@ -2202,7 +2297,6 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2202
2297
|
trackedToolComponents.clear();
|
|
2203
2298
|
hoveredAssistantComponent = undefined;
|
|
2204
2299
|
resetRounds();
|
|
2205
|
-
compactRunStatusGetter = undefined;
|
|
2206
2300
|
};
|
|
2207
2301
|
|
|
2208
2302
|
assistantPrototype.updateContent = patch.assistantInstalled;
|
|
@@ -2232,6 +2326,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2232
2326
|
if (config.mode === "compact") syncGlobalExpanded(ctx);
|
|
2233
2327
|
else hoveredAssistantComponent = undefined;
|
|
2234
2328
|
refreshTrackedComponents();
|
|
2329
|
+
sweepOrphanTails();
|
|
2235
2330
|
},
|
|
2236
2331
|
refresh() {
|
|
2237
2332
|
if (!patch.active) return;
|
|
@@ -2239,6 +2334,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2239
2334
|
patch.assertAssistantOwnership();
|
|
2240
2335
|
if (config.mode !== "compact") hoveredAssistantComponent = undefined;
|
|
2241
2336
|
refreshTrackedComponents();
|
|
2337
|
+
sweepOrphanTails();
|
|
2242
2338
|
},
|
|
2243
2339
|
assertOwnership() {
|
|
2244
2340
|
if (!patch.active) return;
|
|
@@ -2259,6 +2355,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2259
2355
|
}
|
|
2260
2356
|
}
|
|
2261
2357
|
},
|
|
2358
|
+
settleReplayedRound() {
|
|
2359
|
+
if (!patch.active || config.mode !== "compact") return;
|
|
2360
|
+
const round = activeRound;
|
|
2361
|
+
if (!round) return;
|
|
2362
|
+
finalizeRound(round); // clears activeRound — capture it first
|
|
2363
|
+
renderRound(round);
|
|
2364
|
+
},
|
|
2262
2365
|
shutdown() {
|
|
2263
2366
|
patch.dispose();
|
|
2264
2367
|
},
|
|
@@ -250,6 +250,9 @@ export default function (
|
|
|
250
250
|
// compact-thinking 的 session_start 处理在本 handler 之后执行(在其之上再装
|
|
251
251
|
// 一层 updateContent);延迟再同步一次,保证 compact 补丁最终位于外层。
|
|
252
252
|
setTimeout(() => syncCompactMode(ctx), 0);
|
|
253
|
+
// 历史回放发生在 session_start 之后,延迟到回放结束再收尾最后一个回合,
|
|
254
|
+
// 否则它没有后续消息,摘要行会停在 Running。
|
|
255
|
+
setTimeout(() => compactModeHooks?.settleReplayedRound(), 0);
|
|
253
256
|
scheduleSessionRender(() => hooks.toolGrouping.refresh(getToolMouseTui()));
|
|
254
257
|
});
|
|
255
258
|
|