pi-cc-extensions 1.0.13 → 1.0.14
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.
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
|
|
9
9
|
* edit/write 与 ask_user_question 留在外层:不进摘要计数,折叠态也单独成行。
|
|
10
10
|
* 外层行一旦在消息里出现,当前回合就地完结(不计静默期),后续消息另起回合:
|
|
11
|
-
*
|
|
11
|
+
* 摘要行因此只折叠连续的一段工具调用。收尾后本回合的外层行排在摘要行之下,
|
|
12
|
+
* 摘要行覆盖的是它们之前的工具段;回合还在跑时它们留在原位,摘要行钉在可写底缘。
|
|
12
13
|
* 用户消息是回合的硬边界:steer 消息插进回合中间、或上一回合没正文就收尾时,回合
|
|
13
14
|
* 就地收口、后续消息另起一回合。custom 通知/summary 类组件既不是成员也不算外来行,
|
|
14
15
|
* 原地留在两段之间,不打断统计也不被挪位。
|
|
@@ -1211,6 +1212,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1211
1212
|
liveSlotToolIds: Set<string>;
|
|
1212
1213
|
/** 收尾后的静默期截止时间戳;期间仍按围观态渲染,到点才收拢。 */
|
|
1213
1214
|
settleUntil?: number;
|
|
1215
|
+
/** 已请求收尾但回合还有活干(消息仍在流式、工具还在跑):活干完再兑现。 */
|
|
1216
|
+
endRequest?: { endTimestamp?: number };
|
|
1214
1217
|
/** 消息已有 toolCall、工具组件还没到:等它一 updateDisplay 就重画槽位。 */
|
|
1215
1218
|
pendingLiveTool?: boolean;
|
|
1216
1219
|
foldTimer?: ReturnType<typeof setTimeout>;
|
|
@@ -1235,6 +1238,19 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1235
1238
|
let idleRef: (() => boolean) | undefined = deps.isIdle;
|
|
1236
1239
|
let roundTickTimer: ReturnType<typeof setInterval> | undefined;
|
|
1237
1240
|
|
|
1241
|
+
/** 本进程见过的回合:活动回合、本次 pass 建起的、收尾静默期的、还挂着尾行的。 */
|
|
1242
|
+
const allRounds = (): CompactRound[] => {
|
|
1243
|
+
const rounds = new Set<CompactRound>();
|
|
1244
|
+
if (activeRound) rounds.add(activeRound);
|
|
1245
|
+
for (const round of liveRounds) rounds.add(round);
|
|
1246
|
+
for (const round of pendingFoldRounds) rounds.add(round);
|
|
1247
|
+
for (const tail of mountedTails) {
|
|
1248
|
+
const round = tail.round as CompactRound | undefined;
|
|
1249
|
+
if (round) rounds.add(round);
|
|
1250
|
+
}
|
|
1251
|
+
return [...rounds];
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1238
1254
|
/** 会话是否空闲:回合活性的事实来源,取不到就当没空闲(宁可保留 Running)。 */
|
|
1239
1255
|
const sessionIdle = (): boolean => {
|
|
1240
1256
|
try {
|
|
@@ -1263,14 +1279,23 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1263
1279
|
roundTickTimer = undefined;
|
|
1264
1280
|
};
|
|
1265
1281
|
|
|
1282
|
+
/** 还有回合要盯就得继续 tick:活动回合的时长要跳,挂起的收尾请求要等活干完。 */
|
|
1283
|
+
const roundTickNeeded = (): boolean => {
|
|
1284
|
+
for (const round of allRounds()) {
|
|
1285
|
+
if (round.active || round.endRequest !== undefined) return true;
|
|
1286
|
+
}
|
|
1287
|
+
return false;
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1266
1290
|
// 自备 tick:getter 在 render 重算挂钟;兼驱动 Running 扫光(setCompactSummaryActive)。
|
|
1267
1291
|
const ensureRoundTick = (): void => {
|
|
1268
1292
|
if (roundTickTimer) return;
|
|
1269
1293
|
roundTickTimer = setInterval(() => {
|
|
1270
|
-
if (!patch.active || !
|
|
1294
|
+
if (!patch.active || !roundTickNeeded()) {
|
|
1271
1295
|
stopRoundTick();
|
|
1272
1296
|
return;
|
|
1273
1297
|
}
|
|
1298
|
+
for (const round of allRounds()) retryDeferredFinalize(round);
|
|
1274
1299
|
settleStrandedRounds();
|
|
1275
1300
|
try {
|
|
1276
1301
|
// 非 force:保留 fullscreen 布局缓存和差分绘制。
|
|
@@ -1302,9 +1327,15 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1302
1327
|
value <= Date.now() + 60_000;
|
|
1303
1328
|
|
|
1304
1329
|
/** 结束回合活动态;不做任何重绘。更晚的消息 timestamp 用来保住回放时长;
|
|
1305
|
-
* 同一条消息的 timestamp 不动,活回合改用本进程挂钟,避免把历史 timestamp 收到「现在」。
|
|
1306
|
-
|
|
1330
|
+
* 同一条消息的 timestamp 不动,活回合改用本进程挂钟,避免把历史 timestamp 收到「现在」。
|
|
1331
|
+
* 回合还有活干时只记下收尾请求:提前翻成 Ran for 会把仍在跑的工具报成已结束。 */
|
|
1332
|
+
const finalizeRound = (round: CompactRound, endTimestamp?: number): boolean => {
|
|
1307
1333
|
clearFoldTimer(round);
|
|
1334
|
+
if (roundHasLiveWork(round)) {
|
|
1335
|
+
round.endRequest ??= { endTimestamp };
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
round.endRequest = undefined;
|
|
1308
1339
|
round.active = false;
|
|
1309
1340
|
if (round.endedAt === undefined) {
|
|
1310
1341
|
let fallback = 0;
|
|
@@ -1321,22 +1352,26 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1321
1352
|
}
|
|
1322
1353
|
if (activeRound === round) {
|
|
1323
1354
|
activeRound = undefined;
|
|
1324
|
-
|
|
1325
|
-
|
|
1355
|
+
// 还有回合在跑(另一个活动回合或挂起的收尾):扫光与 tick 不能跟着一起停。
|
|
1356
|
+
const stillLive = roundTickNeeded();
|
|
1357
|
+
deps.query?.setCompactSummaryActive?.(stillLive);
|
|
1358
|
+
if (!stillLive) stopRoundTick();
|
|
1326
1359
|
}
|
|
1360
|
+
return true;
|
|
1327
1361
|
};
|
|
1328
1362
|
|
|
1329
1363
|
/**
|
|
1330
1364
|
* 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。
|
|
1331
1365
|
* 分组语义不受影响:activeRound 立即让位,新消息不会被并进已收尾的回合。
|
|
1366
|
+
* 还有活干时整段跳过:回合保持 live,等活干完再收尾。
|
|
1332
1367
|
*/
|
|
1333
1368
|
const endRound = (round: CompactRound, render = false, endTimestamp?: number): void => {
|
|
1334
|
-
|
|
1335
|
-
|
|
1369
|
+
const wasActive = round.active;
|
|
1370
|
+
if (!finalizeRound(round, endTimestamp)) return;
|
|
1371
|
+
if (!render || !wasActive) {
|
|
1336
1372
|
if (render) renderRound(round);
|
|
1337
1373
|
return;
|
|
1338
1374
|
}
|
|
1339
|
-
finalizeRound(round, endTimestamp);
|
|
1340
1375
|
if (round.foldTimer !== undefined) return;
|
|
1341
1376
|
round.settleUntil = Date.now() + FOLD_SETTLE_MS;
|
|
1342
1377
|
pendingFoldRounds.add(round);
|
|
@@ -1348,10 +1383,23 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1348
1383
|
}, FOLD_SETTLE_MS);
|
|
1349
1384
|
};
|
|
1350
1385
|
|
|
1386
|
+
/**
|
|
1387
|
+
* 兑现挂起的收尾请求:活干完了当场收尾并重绘,摘要行从 Running 翻成 Ran for。
|
|
1388
|
+
* 挂在工具收尾与 tick 上,不另建定时器。
|
|
1389
|
+
*/
|
|
1390
|
+
const retryDeferredFinalize = (round: CompactRound): boolean => {
|
|
1391
|
+
const request = round.endRequest;
|
|
1392
|
+
if (!request || roundHasLiveWork(round)) return false;
|
|
1393
|
+
endRound(round, true, request.endTimestamp);
|
|
1394
|
+
renderRound(round);
|
|
1395
|
+
return true;
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1351
1398
|
/**
|
|
1352
1399
|
* 外层行(edit/write/ask)切出的段落封口:跳过静默期直接定稿。
|
|
1353
1400
|
* 静默期是为"正文 token 先于 toolCall 到达"的双重转场准备的;外层边界
|
|
1354
1401
|
* 出现时摘要段内容已完整,立即收拢让 Ran for 当场落位,槽位卡一并撤走。
|
|
1402
|
+
* 本回合还有工具在跑时只挂起收尾:摘要行继续 Running,活干完再定稿。
|
|
1355
1403
|
*/
|
|
1356
1404
|
const sealRound = (round: CompactRound, endTimestamp?: number): void => {
|
|
1357
1405
|
finalizeRound(round, endTimestamp);
|
|
@@ -1428,6 +1476,24 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1428
1476
|
return ids;
|
|
1429
1477
|
};
|
|
1430
1478
|
|
|
1479
|
+
/**
|
|
1480
|
+
* 回合还有活干:成员消息仍在流式,或本回合的工具还没拿到最终结果。
|
|
1481
|
+
* 收尾要么按事实等活干完,要么摘要行就会把仍在跑的工具报成 Ran for。
|
|
1482
|
+
*/
|
|
1483
|
+
const roundHasLiveWork = (round: CompactRound): boolean => {
|
|
1484
|
+
for (const component of round.messages.keys()) {
|
|
1485
|
+
if (component?.isStreaming === true) return true;
|
|
1486
|
+
}
|
|
1487
|
+
const ids = roundToolCallIds(round);
|
|
1488
|
+
if (ids.size === 0) return false;
|
|
1489
|
+
for (const tool of trackedToolComponents) {
|
|
1490
|
+
if (typeof tool?.toolCallId === "string" && ids.has(tool.toolCallId) && isRunningTool(tool)) {
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return false;
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1431
1497
|
/**
|
|
1432
1498
|
* 摘要行有东西可汇总才挂:普通工具(edit/write/ask 之外)或非空思考。
|
|
1433
1499
|
* 只剩外层卡的回合不挂——那些卡各自能展开,空摘要点开只会跳位。
|
|
@@ -1602,6 +1668,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1602
1668
|
return tail;
|
|
1603
1669
|
};
|
|
1604
1670
|
|
|
1671
|
+
/** 它们排在摘要行之下;定位时不把它们当摘要行之前的成员。 */
|
|
1672
|
+
const isOuterToolRow = (ids: Set<string>, value: any): boolean =>
|
|
1673
|
+
typeof value?.toolCallId === "string" &&
|
|
1674
|
+
ids.has(value.toolCallId) &&
|
|
1675
|
+
OUTER_TOOLS.has(String(value.toolName ?? ""));
|
|
1676
|
+
|
|
1605
1677
|
/**
|
|
1606
1678
|
* 尾行归位。目标位:活回合(运行中/收尾静默期)且未展开 → 容器最末,
|
|
1607
1679
|
* 计数行钉在 transcript 可写底缘,运行中到达的提示行/custom 条目只能落在
|
|
@@ -1621,22 +1693,33 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1621
1693
|
if (!container || !Array.isArray(container.children)) return;
|
|
1622
1694
|
tail.container = container;
|
|
1623
1695
|
const children = container.children as any[];
|
|
1696
|
+
const ids = roundToolCallIds(round);
|
|
1697
|
+
const live = roundLive(round);
|
|
1624
1698
|
const isMember = roundMemberMatcher(round);
|
|
1699
|
+
// 收尾后,本回合的外露行(edit/write/ask)一律排到摘要行之下;
|
|
1700
|
+
// 回合还在跑时它们留在原位,摘要行才能钉在可写底缘。
|
|
1701
|
+
const staysAbove = (value: any): boolean =>
|
|
1702
|
+
isMember(value) && (!isOuterToolRow(ids, value) || live);
|
|
1625
1703
|
const current = children.indexOf(tail.host);
|
|
1626
1704
|
// 摘尾行前先定目标位,已在位就整段跳过。
|
|
1627
1705
|
let last = -1;
|
|
1628
1706
|
for (let i = 0; i < children.length; i++) {
|
|
1629
|
-
if (children[i] === tail.host || !
|
|
1707
|
+
if (children[i] === tail.host || !staysAbove(children[i])) continue;
|
|
1630
1708
|
last = i;
|
|
1631
1709
|
}
|
|
1632
|
-
|
|
1710
|
+
// 外露行压在摘要行上方时仍要重排:尾行就算在目标位上也得让位。
|
|
1711
|
+
const outerAbove =
|
|
1712
|
+
!live &&
|
|
1713
|
+
current > 0 &&
|
|
1714
|
+
children.some((value: any, index: number) => index < current && isOuterToolRow(ids, value));
|
|
1715
|
+
const pinToEnd = live && round.anchor?.expanded !== true;
|
|
1633
1716
|
const desired = pinToEnd || last < 0 ? children.length - 1 : last + 1;
|
|
1634
|
-
if (current === desired) return;
|
|
1717
|
+
if (current === desired && !outerAbove) return;
|
|
1635
1718
|
if (current >= 0) children.splice(current, 1);
|
|
1636
1719
|
let first = -1;
|
|
1637
1720
|
last = -1;
|
|
1638
1721
|
for (let i = 0; i < children.length; i++) {
|
|
1639
|
-
if (!
|
|
1722
|
+
if (!staysAbove(children[i])) continue;
|
|
1640
1723
|
if (first < 0) first = i;
|
|
1641
1724
|
last = i;
|
|
1642
1725
|
}
|
|
@@ -1646,7 +1729,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1646
1729
|
// 用户消息永远留在原地:挪走会把别人的消息挪到用户消息下面去。
|
|
1647
1730
|
// custom 通知同理:它是回合内的透明行,不被统计也不挪位。
|
|
1648
1731
|
if (
|
|
1649
|
-
|
|
1732
|
+
staysAbove(children[i]) ||
|
|
1650
1733
|
isUserMessageComponent(children[i]) ||
|
|
1651
1734
|
isTransparentComponent(children[i])
|
|
1652
1735
|
) {
|
|
@@ -1681,8 +1764,10 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1681
1764
|
*/
|
|
1682
1765
|
const syncTailForTool = (toolCallId: string): void => {
|
|
1683
1766
|
if (!toolCallId) return;
|
|
1684
|
-
for (const round of
|
|
1685
|
-
if (!
|
|
1767
|
+
for (const round of allRounds()) {
|
|
1768
|
+
if (!roundToolCallIds(round).has(toolCallId)) continue;
|
|
1769
|
+
// 工具收尾后活干完了:挂起的收尾请求当场兑现,摘要行立刻翻成 Ran for。
|
|
1770
|
+
if (retryDeferredFinalize(round)) return;
|
|
1686
1771
|
// 槽位是在消息更新时画的,当时工具组件还不存在:组件到了就补上。
|
|
1687
1772
|
// 参数流式更新也走这里,槽位卡跟着换内容。
|
|
1688
1773
|
if (round.pendingLiveTool || roundLive(round)) renderRound(round);
|
|
@@ -2288,6 +2373,14 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2288
2373
|
if (hasText) {
|
|
2289
2374
|
const round = roundByComponent.get(this);
|
|
2290
2375
|
const previousMessage = round?.messages.get(this);
|
|
2376
|
+
// 消息还在流式时正文不是回合边界:同一条消息常常先出正文、再出 toolCall,
|
|
2377
|
+
// 这时收尾会在下一帧另起一条摘要行,一段工作被拆成两条。
|
|
2378
|
+
// 正文先按成员渲染,等消息收尾(isStreaming=false)再判。
|
|
2379
|
+
if (round && previousMessage && self.isStreaming === true) {
|
|
2380
|
+
round.messages.set(this, message);
|
|
2381
|
+
renderRound(round);
|
|
2382
|
+
return undefined;
|
|
2383
|
+
}
|
|
2291
2384
|
if (round && previousMessage && (round.anchor !== this || round.messages.size > 1)) {
|
|
2292
2385
|
// 最终回答开始后,当前组件恢复原生文本;它已完成的 thinking
|
|
2293
2386
|
// 留在上一轮摘要中,避免再次生成独立 Thought 行。
|
|
@@ -84,7 +84,7 @@ export async function executeWriteWithMetadata(
|
|
|
84
84
|
// 覆盖层只揽写前快照:路径解析(含 Windows 下 /mnt/<drive>、/cygdrive/<drive> 转换)、
|
|
85
85
|
// 目录创建、mutation queue 与错误文案全交回 pi 官方 write,参数与官方逐字一致。
|
|
86
86
|
const definition = createWriteToolDefinition(cwd, {
|
|
87
|
-
operations: writeSnapshotOperations(store, toolCallId),
|
|
87
|
+
operations: writeSnapshotOperations(store, toolCallId, signal),
|
|
88
88
|
}) as any;
|
|
89
89
|
try {
|
|
90
90
|
await definition.execute(toolCallId, params, signal, undefined, { cwd });
|
|
@@ -101,12 +101,16 @@ export async function executeWriteWithMetadata(
|
|
|
101
101
|
function writeSnapshotOperations(
|
|
102
102
|
store: WriteExecutionMetadataStore,
|
|
103
103
|
toolCallId: string,
|
|
104
|
+
signal: AbortSignal | undefined,
|
|
104
105
|
): WriteOperations {
|
|
105
106
|
return {
|
|
106
107
|
mkdir: (dir) => mkdir(dir, { recursive: true }).then(() => {}),
|
|
107
108
|
writeFile: async (absolutePath, content) => {
|
|
108
109
|
store.delete(toolCallId);
|
|
109
110
|
const metadata = await capturePreviousContent(absolutePath);
|
|
111
|
+
// 官方只在进 operations 之前查 abort(紧接着这次调用,中间没有 await),
|
|
112
|
+
// 快照读盘期间的取消只能在这里补:抛错,不写文件,也不留 metadata。
|
|
113
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
110
114
|
await writeFile(absolutePath, content, "utf8");
|
|
111
115
|
// 写已生效:后续 abort 不得把这次写报成失败,快照先落下。
|
|
112
116
|
store.set(toolCallId, metadata);
|