pi-cc-extensions 0.9.5 → 0.9.6
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 +2 -2
- package/README.md +2 -2
- package/extensions/feature/compact-thinking.ts +1 -1
- package/extensions/renderer/compact-mode.ts +174 -62
- package/extensions/renderer/default-mode.ts +23 -14
- package/extensions/renderer/mouse/interaction.ts +180 -79
- package/extensions/renderer/mouse/layout.ts +14 -5
- package/extensions/renderer/tool/grouping.ts +3 -2
- package/extensions/renderer/tool/show-more-hint.ts +10 -0
- package/package.json +2 -1
package/README.en.md
CHANGED
|
@@ -29,7 +29,7 @@ Run `/reload` after installation.
|
|
|
29
29
|
| --------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
30
30
|
| Claude Code UI | Tool summaries, expand/collapse, rich edit/write diffs, and `on` / `compact` / `off` modes | `/ccstyle` |
|
|
31
31
|
| Markdown enhancements | Mermaid diagrams, admonitions, URL linking, and more | Automatic |
|
|
32
|
-
| Fullscreen mode | Tool card/group expand and collapse, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
|
|
32
|
+
| Fullscreen mode | Tool card/group expand and collapse on click, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
|
|
33
33
|
| Settings panel | `Style / Features / UI / Diff / Thinking / Footer` tabs | `/ccstyle` |
|
|
34
34
|
| Context inspection | Usage breakdown and previews for the system prompt, memory, skills, tools definition, and messages | `/context` |
|
|
35
35
|
| Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
|
|
@@ -89,7 +89,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
> [!TIP]
|
|
92
|
-
> **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. An expanded diff always shows every line; when expanded Input/Output exceeds the line cap, the footer `… +N more lines • click to show more` opens a full preview.
|
|
92
|
+
> **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. An expanded diff always shows every line; when expanded Input/Output exceeds the line cap, the footer `… +N more lines • click to show more` opens a full preview. Click to collapse (dragging inside the card selects text).
|
|
93
93
|
|
|
94
94
|
> [!NOTE]
|
|
95
95
|
> **Mermaid rendering**: set `markdown.mermaid` to `final` via `~/.pi/agent/settings.json` or the Mermaid diagrams option in `/settings`. Default `streaming` redraws per frame; `final` renders once at completion.
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
29
29
|
| --------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
30
30
|
| Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `compact` / `off` 三种模式 | `/ccstyle` |
|
|
31
31
|
| Markdown 增强 | Mermaid 图、提示框、URL 链接化等 | 自动生效 |
|
|
32
|
-
| Fullscreen mode | 工具卡/group
|
|
32
|
+
| Fullscreen mode | 工具卡/group 单击展开与收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
|
|
33
33
|
| 配置面板 | `Style / Features / UI / Diff / Thinking / Footer` 六页签 | `/ccstyle` |
|
|
34
34
|
| 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools definition 和消息内容 | `/context` |
|
|
35
35
|
| Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
|
|
@@ -89,7 +89,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
> [!TIP]
|
|
92
|
-
> **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;工具卡展开态 diff 全量显示,Input/Output 超行时末行 `… +N more lines • click to show more`
|
|
92
|
+
> **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;工具卡展开态 diff 全量显示,Input/Output 超行时末行 `… +N more lines • click to show more` 打开全量预览,单击收起(卡内拖动为选中文本)。
|
|
93
93
|
|
|
94
94
|
> [!NOTE]
|
|
95
95
|
> **Mermaid 渲染**:建议把 `markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
|
|
@@ -323,7 +323,7 @@ function hiddenPreviewHint(
|
|
|
323
323
|
|
|
324
324
|
const expandedThinking = new Set<number>();
|
|
325
325
|
|
|
326
|
-
/** 折叠预览 + 展开全文。fullscreen 点击 hint
|
|
326
|
+
/** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、展开卡单击整块收起(拖动选择文本时不收起)。 */
|
|
327
327
|
export class ThinkingPreviewBlock implements Component {
|
|
328
328
|
private heading: string;
|
|
329
329
|
private text: string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compact mode:每条含 toolCall 的 assistant message 折叠为一条逐步累加的摘要行
|
|
3
3
|
* (`Ran for 8s, bash×2, read×2`),edit/write 独立标题行(`✓ write <path> (+25 -0)`),
|
|
4
|
-
* 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen
|
|
5
|
-
* compact-thinking/Pi 原生或专用
|
|
4
|
+
* 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen 点击)时助手文本按原生渲染,
|
|
5
|
+
* thinking 与工具卡装进 userMessageBg 面板(卡内恢复 compact-thinking/Pi 原生或专用
|
|
6
|
+
* renderer)。edit/write 复用 mode=on 的 rich diff
|
|
6
7
|
* 与 `editDiffCollapsedLines` / `writeDiffCollapsedLines` / `expandedPreviewMaxLines`。
|
|
7
8
|
* Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
|
|
8
9
|
* 底部 Agents/Tasks 面板由 pi-subagents/pi-tasks 独立 widget 负责,不经 tool 卡外置。
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
import { renderRichToolResult } from "./tool/diff/index.ts";
|
|
36
37
|
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
37
38
|
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
39
|
+
import { getToolMouseTui } from "./mouse/scroll.ts";
|
|
38
40
|
import { insetComponent, renderExpandedToolResult, scheduleAnimation } from "./tool/result.ts";
|
|
39
41
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
40
42
|
import { formatDisplayPath } from "./tool/names.ts";
|
|
@@ -50,6 +52,11 @@ import {
|
|
|
50
52
|
PROTOTYPE_ORIGINAL_KEY,
|
|
51
53
|
} from "../utils/patch-keys.ts";
|
|
52
54
|
|
|
55
|
+
/** Pi 的 native 组件与扩展可能各持一份 pi-tui,Spacer 不能只靠 instanceof 判。 */
|
|
56
|
+
function isSpacerComponent(value: any): boolean {
|
|
57
|
+
return value instanceof Spacer || value?.constructor?.name === "Spacer";
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
/** compact 渲染层对 compact-thinking 的只读查询面(不建第二套计时器)。 */
|
|
54
61
|
export type CompactThinkingQuery = {
|
|
55
62
|
getMessageThinkingDurationMs(messageTimestamp: number): number | undefined;
|
|
@@ -320,14 +327,18 @@ function editWriteExpandedCard(theme: any): any {
|
|
|
320
327
|
);
|
|
321
328
|
}
|
|
322
329
|
|
|
323
|
-
/** compact
|
|
324
|
-
*
|
|
325
|
-
*
|
|
330
|
+
/** compact 展开面板:外卡片保持 userMessageBg;展开的 thinking 另包一层更深的
|
|
331
|
+
* 内卡(自身 Box 背景与外卡同色,不区分会糊在一起),工具卡直接铺在外卡背景上。
|
|
332
|
+
* 每个子卡前面补 1 行分隔(上一条已经是空行时不再补),子卡自带的首尾空行会被裁掉,
|
|
333
|
+
* 避免与外卡/内卡 padding 叠出双空行。
|
|
334
|
+
* hits:子卡的行区间与命中目标(thinking 命中自身,工具命中 tool 组件),
|
|
335
|
+
* 供展开后点击 hint。未开启 toolHits 时不映射工具,点击仍归外层面板。 */
|
|
326
336
|
function layoutExpandedToolCard(
|
|
327
337
|
theme: any,
|
|
328
338
|
children: any[],
|
|
329
339
|
width: number,
|
|
330
340
|
paints?: string[][],
|
|
341
|
+
toolHits = false,
|
|
331
342
|
): { lines: string[]; hits: Array<{ child: any; start: number; end: number }> } {
|
|
332
343
|
const slot = "userMessageBg";
|
|
333
344
|
const toolBgAnsi = darkenBgAnsi(theme, slot);
|
|
@@ -335,31 +346,18 @@ function layoutExpandedToolCard(
|
|
|
335
346
|
const lines: string[] = [];
|
|
336
347
|
const hits: Array<{ child: any; start: number; end: number }> = [];
|
|
337
348
|
const isThinkingPreview = (child: any) => typeof child?.setHintHovered === "function";
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
349
|
+
let lastBlank = false;
|
|
350
|
+
const pushBlank = () => {
|
|
351
|
+
if (lastBlank) return;
|
|
352
|
+
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
353
|
+
lastBlank = true;
|
|
354
|
+
};
|
|
341
355
|
for (let childIndex = 0; childIndex < children.length; childIndex++) {
|
|
342
356
|
const child = children[childIndex];
|
|
343
357
|
const painted = paints?.[childIndex];
|
|
344
358
|
const childLines = Array.isArray(painted) ? painted : child.render(innerWidth);
|
|
345
|
-
// 展开的 thinking
|
|
346
|
-
const
|
|
347
|
-
if (!nest) {
|
|
348
|
-
let start = 0;
|
|
349
|
-
if (skipLeadingBlank) {
|
|
350
|
-
while (start < childLines.length && !hasVisibleText(childLines[start])) start++;
|
|
351
|
-
skipLeadingBlank = false;
|
|
352
|
-
}
|
|
353
|
-
const rangeStart = lines.length;
|
|
354
|
-
for (let i = start; i < childLines.length; i++) {
|
|
355
|
-
lines.push(paddedBackgroundRow(theme, slot, childLines[i], width));
|
|
356
|
-
}
|
|
357
|
-
if (lines.length > rangeStart) hits.push({ child, start: rangeStart, end: lines.length });
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
// 工具卡:前导 1 空行 + 首尾文本行之间的内容区(深色);不保留尾随,
|
|
361
|
-
// 相邻工具卡之间正好 1 空行,底部由卡片 padding 收尾。
|
|
362
|
-
skipLeadingBlank = false;
|
|
359
|
+
// 展开的 thinking 带深色内卡(自带上下 padding,不再补外卡分隔行)。
|
|
360
|
+
const innerCard = isThinkingPreview(child) && child.expanded === true;
|
|
363
361
|
let first = -1;
|
|
364
362
|
let last = -1;
|
|
365
363
|
for (let i = 0; i < childLines.length; i++) {
|
|
@@ -371,26 +369,36 @@ function layoutExpandedToolCard(
|
|
|
371
369
|
if (first < 0) {
|
|
372
370
|
for (const line of childLines) {
|
|
373
371
|
lines.push(paddedBackgroundRow(theme, slot, line, width));
|
|
372
|
+
lastBlank = !hasVisibleText(line);
|
|
374
373
|
}
|
|
375
374
|
continue;
|
|
376
375
|
}
|
|
377
|
-
|
|
376
|
+
if (!innerCard) pushBlank();
|
|
378
377
|
const rangeStart = lines.length;
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
lines.push(
|
|
383
|
-
|
|
384
|
-
|
|
378
|
+
// 展开的 thinking 走深色内卡(上下各 1 行 padding),工具卡直接铺在外卡上。
|
|
379
|
+
const paintRow = innerCard
|
|
380
|
+
? (line: string) => lines.push(toolCardBgRow(theme, slot, toolBgAnsi, line, width))
|
|
381
|
+
: (line: string) => lines.push(paddedBackgroundRow(theme, slot, line, width));
|
|
382
|
+
if (innerCard) paintRow("");
|
|
383
|
+
for (let i = first; i <= last; i++) paintRow(childLines[i]);
|
|
384
|
+
if (innerCard) paintRow("");
|
|
385
|
+
// 内卡的下 padding 不能当分隔:后面的子卡要再留 1 行外卡空行。
|
|
386
|
+
lastBlank = false;
|
|
387
|
+
// thinking 命中自身;工具在展开卡内需要能二次展开时才映射回 tool 组件。
|
|
385
388
|
if (isThinkingPreview(child)) hits.push({ child, start: rangeStart, end: lines.length });
|
|
389
|
+
else if (toolHits && child.__ccTool) {
|
|
390
|
+
hits.push({ child: child.__ccTool, start: rangeStart, end: lines.length });
|
|
391
|
+
}
|
|
386
392
|
}
|
|
387
|
-
|
|
393
|
+
pushBlank();
|
|
388
394
|
return { lines, hits };
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
function compactRoundCard(
|
|
392
398
|
cardItems: Array<{ child?: any; tool?: any }>,
|
|
393
399
|
toolRender: (tool: any, width: number) => string[],
|
|
400
|
+
/** true 时卡内工具行归 tool 组件,可点击二次展开(整回合展开卡)。 */
|
|
401
|
+
toolHits = false,
|
|
394
402
|
): any {
|
|
395
403
|
const children: any[] = [];
|
|
396
404
|
for (const item of cardItems) {
|
|
@@ -399,6 +407,7 @@ function compactRoundCard(
|
|
|
399
407
|
const tool = item.tool;
|
|
400
408
|
children.push({
|
|
401
409
|
__ccToolCard: true,
|
|
410
|
+
__ccTool: tool,
|
|
402
411
|
render: (innerWidth: number) => toolRender(tool, innerWidth),
|
|
403
412
|
invalidate: () => tool.invalidate?.(),
|
|
404
413
|
});
|
|
@@ -429,12 +438,12 @@ function compactRoundCard(
|
|
|
429
438
|
) {
|
|
430
439
|
return paint;
|
|
431
440
|
}
|
|
432
|
-
const laid = layoutExpandedToolCard(theme, children, width, paints);
|
|
441
|
+
const laid = layoutExpandedToolCard(theme, children, width, paints, toolHits);
|
|
433
442
|
paint = {
|
|
434
443
|
width,
|
|
435
444
|
theme,
|
|
436
445
|
paints,
|
|
437
|
-
lines:
|
|
446
|
+
lines: laid.lines,
|
|
438
447
|
hits: laid.hits,
|
|
439
448
|
};
|
|
440
449
|
return paint;
|
|
@@ -445,10 +454,8 @@ function compactRoundCard(
|
|
|
445
454
|
return layout(width).lines;
|
|
446
455
|
},
|
|
447
456
|
childAtRow(localRow: number, width: number) {
|
|
448
|
-
if (localRow < 1) return null;
|
|
449
|
-
const row = localRow - 1;
|
|
450
457
|
for (const hit of layout(width).hits) {
|
|
451
|
-
if (
|
|
458
|
+
if (localRow >= hit.start && localRow < hit.end) return hit.child;
|
|
452
459
|
}
|
|
453
460
|
return null;
|
|
454
461
|
},
|
|
@@ -531,6 +538,43 @@ export function setHoveredCompactAssistant(component: any): boolean {
|
|
|
531
538
|
return true;
|
|
532
539
|
}
|
|
533
540
|
|
|
541
|
+
/**
|
|
542
|
+
* 整回合展开卡内的工具被点击展开时调用:让 compact 的强制折叠放行这一个,
|
|
543
|
+
* 并把同一回合里其他展开的工具收回去(保持单开)。非 round 内工具是空操作。
|
|
544
|
+
*/
|
|
545
|
+
let roundToolExpansion: { markUserExpanded(tool: any): void } | undefined;
|
|
546
|
+
|
|
547
|
+
export function markCompactRoundToolExpanded(tool: any): void {
|
|
548
|
+
roundToolExpansion?.markUserExpanded(tool);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** 面板展开前的视口位置:收起时还原,避免面板撑高后收起导致视口错位。 */
|
|
552
|
+
const panelViewportBeforeExpand = new WeakMap<
|
|
553
|
+
object,
|
|
554
|
+
{ scrollTop: number; followingEnd: boolean }
|
|
555
|
+
>();
|
|
556
|
+
|
|
557
|
+
function capturePanelViewport(card: any): void {
|
|
558
|
+
if (!card || panelViewportBeforeExpand.has(card)) return;
|
|
559
|
+
const view = getToolMouseTui()?.getPrimaryScrollView?.();
|
|
560
|
+
if (!view || typeof view.scrollTop !== "number" || typeof view.scrollTo !== "function") return;
|
|
561
|
+
panelViewportBeforeExpand.set(card, {
|
|
562
|
+
scrollTop: view.scrollTop,
|
|
563
|
+
followingEnd: view.isFollowingEnd !== false,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function restorePanelViewport(card: any): void {
|
|
568
|
+
const snapshot = panelViewportBeforeExpand.get(card);
|
|
569
|
+
if (!snapshot) return;
|
|
570
|
+
panelViewportBeforeExpand.delete(card);
|
|
571
|
+
const view = getToolMouseTui()?.getPrimaryScrollView?.();
|
|
572
|
+
if (!view) return;
|
|
573
|
+
// 展开前就在底部:交回官方 follow;否则回到展开前的滚动偏移。
|
|
574
|
+
if (snapshot.followingEnd && typeof view.scrollToEnd === "function") view.scrollToEnd();
|
|
575
|
+
else if (typeof view.scrollTo === "function") view.scrollTo(snapshot.scrollTop);
|
|
576
|
+
}
|
|
577
|
+
|
|
534
578
|
function compactEditWriteLine(
|
|
535
579
|
component: any,
|
|
536
580
|
width: number,
|
|
@@ -968,6 +1012,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
968
1012
|
let activeRound: CompactRound | undefined;
|
|
969
1013
|
let roundByComponent = new WeakMap<object, CompactRound>();
|
|
970
1014
|
const expandedRoundToolIds = new Set<string>();
|
|
1015
|
+
/** 用户显式点开的 round 内工具;强制折叠不压回这些 id。 */
|
|
1016
|
+
const explicitRoundToolIds = new Set<string>();
|
|
971
1017
|
let uiRef: { requestRender?: (force?: boolean) => void } | undefined;
|
|
972
1018
|
let roundTickTimer: ReturnType<typeof setInterval> | undefined;
|
|
973
1019
|
|
|
@@ -1037,6 +1083,23 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1037
1083
|
return result;
|
|
1038
1084
|
};
|
|
1039
1085
|
|
|
1086
|
+
const markRoundToolUserExpanded = (tool: any): void => {
|
|
1087
|
+
const id = String(tool?.toolCallId ?? "");
|
|
1088
|
+
if (!id || !expandedRoundToolIds.has(id)) return;
|
|
1089
|
+
for (const other of trackedToolComponents) {
|
|
1090
|
+
const otherId = String(other?.toolCallId ?? "");
|
|
1091
|
+
if (other === tool || !expandedRoundToolIds.has(otherId) || other.expanded !== true) continue;
|
|
1092
|
+
explicitRoundToolIds.delete(otherId);
|
|
1093
|
+
if (typeof other.setExpanded === "function") other.setExpanded(false);
|
|
1094
|
+
else {
|
|
1095
|
+
other.expanded = false;
|
|
1096
|
+
other.updateDisplay?.();
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
explicitRoundToolIds.add(id);
|
|
1100
|
+
};
|
|
1101
|
+
roundToolExpansion = { markUserExpanded: markRoundToolUserExpanded };
|
|
1102
|
+
|
|
1040
1103
|
const roundMessages = (round: CompactRound): any[] => [
|
|
1041
1104
|
...round.messages.values(),
|
|
1042
1105
|
...round.detachedMessages,
|
|
@@ -1061,17 +1124,24 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1061
1124
|
const summary = getSummary();
|
|
1062
1125
|
for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
|
|
1063
1126
|
round.suppressedToolIds.clear();
|
|
1127
|
+
// 回合收起(或本回合不在展开态)时忘掉用户的单开选择。
|
|
1128
|
+
if (round.anchor.expanded !== true) {
|
|
1129
|
+
for (const id of roundToolCallIds(round)) explicitRoundToolIds.delete(id);
|
|
1130
|
+
}
|
|
1064
1131
|
|
|
1065
1132
|
for (const [component, message] of round.messages) {
|
|
1066
1133
|
component.lastMessage = message;
|
|
1067
1134
|
ensureAssistantSetExpanded(component);
|
|
1068
1135
|
component[ASSISTANT_TOGGLE_ROUND_KEY] = (expanded: boolean) => {
|
|
1136
|
+
const wasExpanded = round.anchor.expanded === true;
|
|
1137
|
+
if (expanded && !wasExpanded) capturePanelViewport(round.anchor);
|
|
1069
1138
|
for (const member of round.messages.keys()) member.expanded = expanded;
|
|
1070
1139
|
if (!expanded) {
|
|
1071
1140
|
for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
|
|
1072
1141
|
round.suppressedToolIds.clear();
|
|
1073
1142
|
}
|
|
1074
1143
|
renderRound(round);
|
|
1144
|
+
if (!expanded && wasExpanded) restorePanelViewport(round.anchor);
|
|
1075
1145
|
};
|
|
1076
1146
|
}
|
|
1077
1147
|
|
|
@@ -1080,14 +1150,39 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1080
1150
|
for (const tool of trackedToolComponents) {
|
|
1081
1151
|
if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
|
|
1082
1152
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1153
|
+
// 助手文本按原生渲染、不进面板(面板只留 thinking 与工具卡)。
|
|
1154
|
+
// thinking/工具卡按连续段各包一张面板;先出文本再出面板,
|
|
1155
|
+
// 否则消息内容里的 thinking([thinking, text, toolCall])会盖到文本上面。
|
|
1156
|
+
const anchorChildren: any[] = [];
|
|
1157
|
+
let cardItems: Array<{ child?: any; tool?: any }> = [];
|
|
1158
|
+
const flushPanel = () => {
|
|
1159
|
+
if (cardItems.length === 0) return;
|
|
1160
|
+
if (cardItems.every((item) => !item.tool && isSpacerComponent(item.child))) {
|
|
1161
|
+
for (const item of cardItems) anchorChildren.push(item.child);
|
|
1162
|
+
} else {
|
|
1163
|
+
// 面板上沿留 1 行卡外空行:面板底色与 userMessage 同色,
|
|
1164
|
+
// 贴在一起会看成一个块;卡内顶部再加 1 行 padding 保持上下对称。
|
|
1165
|
+
anchorChildren.push(new Spacer(1));
|
|
1166
|
+
anchorChildren.push(
|
|
1167
|
+
compactRoundCard(
|
|
1168
|
+
cardItems,
|
|
1169
|
+
(tool, innerWidth) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
1170
|
+
// 整回合展开卡:卡内工具行归 tool 组件,可单击二次展开。
|
|
1171
|
+
true,
|
|
1172
|
+
),
|
|
1173
|
+
);
|
|
1088
1174
|
}
|
|
1089
|
-
|
|
1175
|
+
cardItems = [];
|
|
1090
1176
|
};
|
|
1177
|
+
const takeChild = (child: any, outside: boolean) => {
|
|
1178
|
+
if (!outside) {
|
|
1179
|
+
cardItems.push({ child });
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
flushPanel();
|
|
1183
|
+
anchorChildren.push(child);
|
|
1184
|
+
};
|
|
1185
|
+
const placedToolIds = new Set<string>();
|
|
1091
1186
|
const placeTool = (id: string, fallbackName?: string) => {
|
|
1092
1187
|
const tool = toolsById.get(id);
|
|
1093
1188
|
if (!tool || EDIT_WRITE_TOOLS.has(String(tool.toolName ?? fallbackName ?? ""))) return;
|
|
@@ -1095,6 +1190,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1095
1190
|
placedToolIds.add(id);
|
|
1096
1191
|
cardItems.push({ tool });
|
|
1097
1192
|
};
|
|
1193
|
+
type LocalEntry = { outside: boolean; child?: any; toolId?: string; toolName?: string };
|
|
1098
1194
|
for (const [component, message] of round.messages) {
|
|
1099
1195
|
passThroughAssistant(component, message);
|
|
1100
1196
|
const kids = Array.isArray(component.contentContainer?.children)
|
|
@@ -1103,25 +1199,39 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1103
1199
|
component.contentContainer?.clear?.();
|
|
1104
1200
|
const cursor = { i: 0 };
|
|
1105
1201
|
let inThinkingRun = false;
|
|
1202
|
+
// 本消息产出先收集再发射:文本(含其前导 Spacer)统一排在面板前面。
|
|
1203
|
+
const local: LocalEntry[] = [];
|
|
1204
|
+
const takeRun = (outside: boolean) => {
|
|
1205
|
+
while (cursor.i < kids.length && isSpacerComponent(kids[cursor.i])) {
|
|
1206
|
+
local.push({ outside, child: kids[cursor.i++] });
|
|
1207
|
+
}
|
|
1208
|
+
if (cursor.i < kids.length) local.push({ outside, child: kids[cursor.i++] });
|
|
1209
|
+
};
|
|
1106
1210
|
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1107
1211
|
if (item?.type === "thinking") {
|
|
1108
1212
|
if (!String(item.thinking ?? "").trim()) continue;
|
|
1109
1213
|
if (!inThinkingRun) {
|
|
1110
|
-
|
|
1214
|
+
takeRun(false);
|
|
1111
1215
|
inThinkingRun = true;
|
|
1112
1216
|
}
|
|
1113
1217
|
continue;
|
|
1114
1218
|
}
|
|
1115
1219
|
inThinkingRun = false;
|
|
1116
1220
|
if (item?.type === "text" && String(item.text ?? "").trim()) {
|
|
1117
|
-
|
|
1221
|
+
takeRun(true);
|
|
1118
1222
|
continue;
|
|
1119
1223
|
}
|
|
1120
1224
|
if (item?.type === "toolCall" && typeof item.id === "string") {
|
|
1121
|
-
|
|
1225
|
+
local.push({ outside: false, toolId: item.id, toolName: item.name });
|
|
1122
1226
|
}
|
|
1123
1227
|
}
|
|
1124
|
-
while (cursor.i < kids.length)
|
|
1228
|
+
while (cursor.i < kids.length) local.push({ outside: false, child: kids[cursor.i++] });
|
|
1229
|
+
for (const entry of local) if (entry.outside) takeChild(entry.child, true);
|
|
1230
|
+
for (const entry of local) {
|
|
1231
|
+
if (entry.outside) continue;
|
|
1232
|
+
if (entry.toolId) placeTool(entry.toolId, entry.toolName);
|
|
1233
|
+
else takeChild(entry.child, false);
|
|
1234
|
+
}
|
|
1125
1235
|
}
|
|
1126
1236
|
for (const message of round.detachedMessages) {
|
|
1127
1237
|
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
@@ -1136,12 +1246,14 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1136
1246
|
expandedRoundToolIds.add(id);
|
|
1137
1247
|
if (!placedToolIds.has(id)) placeTool(id);
|
|
1138
1248
|
}
|
|
1139
|
-
// Round
|
|
1249
|
+
// Round 展开只打开外层卡片。普通工具保持折叠,避免长输出递归撑满屏幕;
|
|
1250
|
+
// 用户单独点开的(explicitRoundToolIds)不压回去。
|
|
1140
1251
|
for (const tool of trackedToolComponents) {
|
|
1141
1252
|
if (
|
|
1142
1253
|
!ids.has(tool.toolCallId) ||
|
|
1143
1254
|
EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")) ||
|
|
1144
|
-
tool.expanded !== true
|
|
1255
|
+
tool.expanded !== true ||
|
|
1256
|
+
explicitRoundToolIds.has(String(tool.toolCallId ?? ""))
|
|
1145
1257
|
) {
|
|
1146
1258
|
continue;
|
|
1147
1259
|
}
|
|
@@ -1151,11 +1263,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1151
1263
|
tool.updateDisplay?.();
|
|
1152
1264
|
}
|
|
1153
1265
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
patch.toolOriginalRender.call(tool, innerWidth),
|
|
1157
|
-
),
|
|
1158
|
-
);
|
|
1266
|
+
flushPanel();
|
|
1267
|
+
for (const child of anchorChildren) round.anchor.contentContainer.addChild(child);
|
|
1159
1268
|
// 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
|
|
1160
1269
|
appendStopStatus(round.anchor, stopStatus);
|
|
1161
1270
|
return;
|
|
@@ -1189,6 +1298,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1189
1298
|
activeRound = undefined;
|
|
1190
1299
|
roundByComponent = new WeakMap();
|
|
1191
1300
|
expandedRoundToolIds.clear();
|
|
1301
|
+
explicitRoundToolIds.clear();
|
|
1192
1302
|
deps.query?.setCompactSummaryActive?.(false);
|
|
1193
1303
|
stopRoundTick();
|
|
1194
1304
|
};
|
|
@@ -1322,12 +1432,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1322
1432
|
};
|
|
1323
1433
|
|
|
1324
1434
|
patch.toolInstalledUpdateDisplay = function (this: any) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
this.expanded = false;
|
|
1435
|
+
const id = String(this.toolCallId ?? "");
|
|
1436
|
+
if (patch.active && config.mode === "compact" && expandedRoundToolIds.has(id)) {
|
|
1437
|
+
// 用户点开的保留展开;其余(含全局展开被收回的)继续强制折叠。
|
|
1438
|
+
if (this.expanded !== true) explicitRoundToolIds.delete(id);
|
|
1439
|
+
else if (!explicitRoundToolIds.has(id)) this.expanded = false;
|
|
1331
1440
|
}
|
|
1332
1441
|
const result = patch.toolOriginalUpdateDisplay.call(this);
|
|
1333
1442
|
if (!patch.active) return result;
|
|
@@ -1355,6 +1464,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1355
1464
|
patch.dispose = () => {
|
|
1356
1465
|
if (!patch.active) return;
|
|
1357
1466
|
patch.active = false;
|
|
1467
|
+
if (roundToolExpansion?.markUserExpanded === markRoundToolUserExpanded) {
|
|
1468
|
+
roundToolExpansion = undefined;
|
|
1469
|
+
}
|
|
1358
1470
|
if (assistantPrototype.updateContent === patch.assistantInstalled) {
|
|
1359
1471
|
assistantPrototype.updateContent = patch.assistantOriginal;
|
|
1360
1472
|
}
|
|
@@ -221,7 +221,7 @@ function createCcstyleTool(
|
|
|
221
221
|
...originalTool,
|
|
222
222
|
renderShell: "self",
|
|
223
223
|
renderCall(args: any, theme: any, context: any) {
|
|
224
|
-
if (config.mode
|
|
224
|
+
if (config.mode === "off") {
|
|
225
225
|
return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -293,7 +293,7 @@ function createCcstyleTool(
|
|
|
293
293
|
};
|
|
294
294
|
},
|
|
295
295
|
renderResult(result: any, options: any, theme: any, context: any) {
|
|
296
|
-
if (config.mode
|
|
296
|
+
if (config.mode === "off") {
|
|
297
297
|
return renderDefault(
|
|
298
298
|
originalTool,
|
|
299
299
|
"renderResult",
|
|
@@ -416,8 +416,9 @@ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch):
|
|
|
416
416
|
const builtInDefinition = component.builtInToolDefinition;
|
|
417
417
|
const definition = extensionDefinition ?? builtInDefinition;
|
|
418
418
|
const toolName = String(component.toolName || definition?.name || "");
|
|
419
|
+
// compact 也复用同一套 ccstyle call/result,避免折叠态工具卡回落到 Pi 原生样式。
|
|
419
420
|
const useCcstyle =
|
|
420
|
-
patch.mode()
|
|
421
|
+
patch.mode() !== "off" &&
|
|
421
422
|
!DEDICATED_RENDERER_TOOLS.has(toolName) &&
|
|
422
423
|
!preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
|
|
423
424
|
component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
|
|
@@ -585,19 +586,27 @@ function installGlobalToolRendering(
|
|
|
585
586
|
return originalRender.call(this, width);
|
|
586
587
|
}
|
|
587
588
|
const cache = patch.paintCache;
|
|
588
|
-
|
|
589
|
+
// A running partial tool owns a time-varying spinner. Light animation ticks
|
|
590
|
+
// request a repaint without invalidating the component, so settled-content
|
|
591
|
+
// caching must not intercept those paints.
|
|
592
|
+
const cacheable = !(this.executionStarted && this.isPartial);
|
|
593
|
+
const hit = cacheable ? cache?.get(this) : undefined;
|
|
589
594
|
if (hit && toolPaintMatches(hit, this, width)) return hit.lines;
|
|
590
595
|
const lines = originalRender.call(this, width);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
596
|
+
if (cacheable) {
|
|
597
|
+
cache?.set(this, {
|
|
598
|
+
width,
|
|
599
|
+
expanded: Boolean(this.expanded),
|
|
600
|
+
isPartial: Boolean(this.isPartial),
|
|
601
|
+
result: this.result,
|
|
602
|
+
args: this.args,
|
|
603
|
+
callHover: isToolCallHovered(this.toolCallId),
|
|
604
|
+
ioHover: ioHoverOf(this),
|
|
605
|
+
lines,
|
|
606
|
+
});
|
|
607
|
+
} else {
|
|
608
|
+
cache?.delete(this);
|
|
609
|
+
}
|
|
601
610
|
return lines;
|
|
602
611
|
};
|
|
603
612
|
patch.invalidatePaint = function (this: any): void {
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { hasActiveTextPreview, showTextPreview } from "../../feature/context.ts";
|
|
3
3
|
import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
patchRegistry,
|
|
6
|
+
TOOL_GROUPING_PARENT_KEY,
|
|
7
|
+
TOOL_MOUSE_OWNER_KEY,
|
|
8
|
+
} from "../../utils/patch-keys.ts";
|
|
5
9
|
import { ToolGroupComponent } from "../tool/grouping.ts";
|
|
6
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
isCompactAssistantComponent,
|
|
12
|
+
markCompactRoundToolExpanded,
|
|
13
|
+
setHoveredCompactAssistant,
|
|
14
|
+
} from "../compact-mode.ts";
|
|
7
15
|
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
8
16
|
import { config } from "../../config/config.ts";
|
|
9
17
|
import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
|
|
10
|
-
import { setToolTuiFullscreen } from "../tool/show-more-hint.ts";
|
|
18
|
+
import { setToolTuiFullscreen, isCollapseHintLine } from "../tool/show-more-hint.ts";
|
|
11
19
|
import {
|
|
12
20
|
type ExpandedToolIoView,
|
|
13
21
|
getActiveIoViewFrame,
|
|
@@ -76,7 +84,7 @@ type FrameToolRender = {
|
|
|
76
84
|
};
|
|
77
85
|
|
|
78
86
|
type InteractionRegion = {
|
|
79
|
-
kind: "collapsed-hint" | "expanded-card" | "show-more" | "scroll-bottom";
|
|
87
|
+
kind: "collapsed-hint" | "collapse-hint" | "expanded-card" | "show-more" | "scroll-bottom";
|
|
80
88
|
row: number;
|
|
81
89
|
startCol: number;
|
|
82
90
|
endCol: number;
|
|
@@ -119,6 +127,7 @@ function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
|
|
|
119
127
|
matches.find((region) => region.kind === "show-more") ??
|
|
120
128
|
matches.find((region) => region.kind === "scroll-bottom") ??
|
|
121
129
|
matches.find((region) => region.kind === "collapsed-hint") ??
|
|
130
|
+
matches.find((region) => region.kind === "collapse-hint") ??
|
|
122
131
|
matches.find((region) => region.kind === "expanded-card") ??
|
|
123
132
|
null
|
|
124
133
|
);
|
|
@@ -146,7 +155,12 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
146
155
|
const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
|
|
147
156
|
const component = region?.component;
|
|
148
157
|
const nextToolCallId = region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
|
|
149
|
-
|
|
158
|
+
// group 只在提示文字上高亮(展开卡整行都是 expanded-card,不能算 hint)。
|
|
159
|
+
const nextGroup =
|
|
160
|
+
(region?.kind === "collapsed-hint" || region?.kind === "collapse-hint") &&
|
|
161
|
+
component instanceof ToolGroupComponent
|
|
162
|
+
? component
|
|
163
|
+
: null;
|
|
150
164
|
const nextIoView = region?.kind === "show-more" ? (region.view ?? null) : null;
|
|
151
165
|
const nextIoSection = region?.kind === "show-more" ? (region.section ?? null) : null;
|
|
152
166
|
const changed = nextToolCallId !== sharedToolHoverState().toolCallId;
|
|
@@ -160,75 +174,92 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
160
174
|
tui.requestRender?.();
|
|
161
175
|
}
|
|
162
176
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
/** 单击判定:按下与松开落在同一格(renderer 合成 click 的条件),位移与时限各留容差。 */
|
|
178
|
+
const COLLAPSE_CLICK_MOVE_TOLERANCE = 1;
|
|
179
|
+
const COLLAPSE_CLICK_MAX_MS = 600;
|
|
180
|
+
/** 扩展收起自有卡后,官方 click 可能对同一次点击再 toggle 内部工具卡,短暂吞掉。 */
|
|
181
|
+
const SUPPRESS_OFFICIAL_CARD_CLICK_MS = 300;
|
|
182
|
+
/** 官方 fullscreen 的焦点事件串:发给原 handler 可清掉选区锚点(见 tui-alt-screen)。 */
|
|
183
|
+
const FULLSCREEN_FOCUS_OUT = "\x1b[O";
|
|
169
184
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
let pendingCollapsePress: {
|
|
186
|
+
card: any;
|
|
187
|
+
/** 命中的最内层组件:官方工具卡由 MouseRegion 的 click 收起。 */
|
|
188
|
+
component: any;
|
|
189
|
+
col: number;
|
|
190
|
+
row: number;
|
|
191
|
+
at: number;
|
|
192
|
+
} | null = null;
|
|
193
|
+
let suppressOfficialCardClickUntil = 0;
|
|
175
194
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
const id = expandPanelIdentity(card);
|
|
179
|
-
const prev = lastExpandClick;
|
|
180
|
-
pendingExpandPress = { id };
|
|
181
|
-
if (prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS) {
|
|
182
|
-
pendingExpandPress = null;
|
|
183
|
-
lastExpandClick = null;
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
return false;
|
|
195
|
+
function clearPendingCollapsePress(): void {
|
|
196
|
+
pendingCollapsePress = null;
|
|
187
197
|
}
|
|
188
198
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// 50ms 内又按下:仍是同一次单击,不能武装成双击。
|
|
197
|
-
if (pendingExpandPress) return;
|
|
198
|
-
lastExpandClick = click;
|
|
199
|
-
}, EXPAND_PANEL_CLICK_ARM_MS);
|
|
200
|
-
if (
|
|
201
|
-
typeof armExpandClickTimer === "object" &&
|
|
202
|
-
armExpandClickTimer &&
|
|
203
|
-
"unref" in armExpandClickTimer
|
|
204
|
-
) {
|
|
205
|
-
armExpandClickTimer.unref();
|
|
199
|
+
/** 带键拖动或滚轮:位置真的变了(超出抖动容差)就不再是单击。 */
|
|
200
|
+
function clearPendingCollapsePressOnMove(packet: SgrMousePacket): void {
|
|
201
|
+
const press = pendingCollapsePress;
|
|
202
|
+
if (!press) return;
|
|
203
|
+
if ((packet.code & 64) !== 0) {
|
|
204
|
+
pendingCollapsePress = null;
|
|
205
|
+
return;
|
|
206
206
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
function clearExpandPanelDoubleClick(): void {
|
|
210
|
-
pendingExpandPress = null;
|
|
211
|
-
lastExpandClick = null;
|
|
212
|
-
if (armExpandClickTimer) {
|
|
213
|
-
clearTimeout(armExpandClickTimer);
|
|
214
|
-
armExpandClickTimer = null;
|
|
207
|
+
if (Math.abs(packet.col - press.col) <= COLLAPSE_CLICK_MOVE_TOLERANCE) {
|
|
208
|
+
if (Math.abs(packet.row - press.row) <= COLLAPSE_CLICK_MOVE_TOLERANCE) return;
|
|
215
209
|
}
|
|
210
|
+
pendingCollapsePress = null;
|
|
216
211
|
}
|
|
217
212
|
|
|
218
|
-
function
|
|
219
|
-
// 单击也 consume,避免官方链再合成一次 press 把单击当成双击。
|
|
220
|
-
if (!isExpandPanelDoubleClick(card)) return true;
|
|
221
|
-
card.setExpanded(false);
|
|
213
|
+
function clearHoverState(): void {
|
|
222
214
|
setHoveredToolCallId(null);
|
|
223
215
|
setHoveredToolGroup(null);
|
|
224
216
|
setHoveredThinking(null);
|
|
225
217
|
setHoveredMessageDisplay(null);
|
|
226
218
|
setHoveredToolIo(null, null);
|
|
227
219
|
setHoveredCompactAssistant(null);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 收起一张展开卡(group 的 setExpanded 会传播到内部工具)。 */
|
|
223
|
+
function collapseExpandedCard(tui: any, card: any): void {
|
|
224
|
+
card.setExpanded(false);
|
|
225
|
+
clearHoverState();
|
|
228
226
|
card.invalidate?.();
|
|
229
|
-
tui
|
|
230
|
-
|
|
231
|
-
|
|
227
|
+
tui?.requestRender?.();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function rememberCollapsePress(card: any, component: any, packet: SgrMousePacket): void {
|
|
231
|
+
pendingCollapsePress = { card, component, col: packet.col, row: packet.row, at: Date.now() };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 松手结算:只有"按下与松开在同一格"的完整单击才收起,拖动选择文本时保持展开。
|
|
236
|
+
* 官方工具卡(result 区的 MouseRegion)由 guard 在官方 click 里收起,这里必须跳过,
|
|
237
|
+
* 否则先收起会被官方 click 的 setExpanded(!expanded) 翻回来。
|
|
238
|
+
*/
|
|
239
|
+
function resolveCollapsePress(
|
|
240
|
+
tui: any,
|
|
241
|
+
packet: SgrMousePacket,
|
|
242
|
+
options: { skipOfficialCards: boolean },
|
|
243
|
+
): any {
|
|
244
|
+
const press = pendingCollapsePress;
|
|
245
|
+
pendingCollapsePress = null;
|
|
246
|
+
if (!press || packet.final !== "m") return undefined;
|
|
247
|
+
if (Math.abs(packet.col - press.col) > COLLAPSE_CLICK_MOVE_TOLERANCE) return undefined;
|
|
248
|
+
if (Math.abs(packet.row - press.row) > COLLAPSE_CLICK_MOVE_TOLERANCE) return undefined;
|
|
249
|
+
if (Date.now() - press.at > COLLAPSE_CLICK_MAX_MS) return undefined;
|
|
250
|
+
if (options.skipOfficialCards && press.component instanceof ToolExecutionComponent)
|
|
251
|
+
return undefined;
|
|
252
|
+
const card = press.card;
|
|
253
|
+
if (
|
|
254
|
+
(card instanceof ToolGroupComponent || isCompactAssistantComponent(card)) &&
|
|
255
|
+
press.component !== card
|
|
256
|
+
) {
|
|
257
|
+
// 点击落在卡内工具行:官方 click 会对同一次点击再次 toggle,吞掉它。
|
|
258
|
+
suppressOfficialCardClickUntil = Date.now() + SUPPRESS_OFFICIAL_CARD_CLICK_MS;
|
|
259
|
+
}
|
|
260
|
+
collapseExpandedCard(tui, card);
|
|
261
|
+
// 返回收起的卡:调用方只对 compact 面板清官方选区。
|
|
262
|
+
return card;
|
|
232
263
|
}
|
|
233
264
|
|
|
234
265
|
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
@@ -238,9 +269,13 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
238
269
|
if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
|
|
239
270
|
const component = region.component;
|
|
240
271
|
if (!component) return false;
|
|
241
|
-
if (region.kind === "expanded-card"
|
|
272
|
+
if (region.kind === "expanded-card" || region.kind === "collapse-hint") {
|
|
273
|
+
// regular 模式没有官方 click 合成,按下先记账,松手结算是否收起。
|
|
274
|
+
rememberCollapsePress(component, component, packet);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
242
277
|
component.setExpanded(true);
|
|
243
|
-
|
|
278
|
+
clearPendingCollapsePress();
|
|
244
279
|
setHoveredToolCallId(null);
|
|
245
280
|
setHoveredToolGroup(null);
|
|
246
281
|
setHoveredToolIo(null, null);
|
|
@@ -339,9 +374,10 @@ function blocksOfficialCardToggle(component: any, event: any): boolean {
|
|
|
339
374
|
|
|
340
375
|
/**
|
|
341
376
|
* 官方 fullscreen 工具卡点击:collapsed hint 单击展开
|
|
342
|
-
* (有且仅保持一个展开:展开前收起其他工具卡),expanded
|
|
343
|
-
*
|
|
344
|
-
*
|
|
377
|
+
* (有且仅保持一个展开:展开前收起其他工具卡),expanded 卡放行官方 press,
|
|
378
|
+
* 位置不变的松手才收起(官方卡经 guard,思考/group/compact 自有卡由扩展结算),
|
|
379
|
+
* 拖动选择文本时不收起;截断头 show-more 单击打开全量预览;回到底部按钮 scrollToBottom。
|
|
380
|
+
* 滚动条列、含 OSC8 链接行、非工具区域、折叠卡正文放行官方。
|
|
345
381
|
*/
|
|
346
382
|
function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
347
383
|
const layout = tui.currentLayout;
|
|
@@ -359,7 +395,10 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
359
395
|
const target = componentAtLocalRow(hit.box.component, hit.localRow, contentWidth);
|
|
360
396
|
if (!target) return false;
|
|
361
397
|
const component = target.component;
|
|
362
|
-
|
|
398
|
+
// compact 展开面板内命中时 owner 是面板:收起对象与记账对象都归面板,
|
|
399
|
+
// 否则 skipOfficialCards 会把内部工具当成官方卡跳过。
|
|
400
|
+
const card = target.group ?? target.owner ?? component;
|
|
401
|
+
const pressComponent = target.owner ?? component;
|
|
363
402
|
// 回到底部按钮:按组件引用命中,不依赖渲染行缓存。
|
|
364
403
|
if (getScrollButtonVisible() && component === getScrollButtonWidget()) {
|
|
365
404
|
tui.scrollToBottom?.();
|
|
@@ -377,21 +416,32 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
377
416
|
if (!component.expanded) {
|
|
378
417
|
// collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
|
|
379
418
|
const hint = collapsedHintHitbox(line);
|
|
380
|
-
|
|
381
|
-
|
|
419
|
+
const onHint = Boolean(
|
|
420
|
+
hint &&
|
|
421
|
+
packet.col >= hint.startCol &&
|
|
422
|
+
packet.col <= hint.endCol &&
|
|
423
|
+
isCollapsedHintRow(component, line),
|
|
424
|
+
);
|
|
425
|
+
if (!onHint) {
|
|
426
|
+
// 面板内非提示区(工具卡标题/摘要行、thinking 预览正文):单击收起整块面板。
|
|
427
|
+
if (target.owner) rememberCollapsePress(target.owner, target.owner, packet);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
382
430
|
// single-expand:展开前收起其他已展开工具卡/group。
|
|
383
431
|
const others: any[] = [];
|
|
384
432
|
collectFullscreenToolCards(hit.box.component, others);
|
|
385
433
|
for (const other of others) {
|
|
386
434
|
if (other !== component && other.expanded) {
|
|
387
|
-
// 展开 round
|
|
388
|
-
if (isThinking && isCompactAssistantComponent(other)) continue;
|
|
435
|
+
// 展开 round 卡内 thinking/工具时,外层 compact 卡是它的容器,不能收起。
|
|
436
|
+
if ((isThinking || isTool) && isCompactAssistantComponent(other)) continue;
|
|
389
437
|
other.setExpanded(false);
|
|
390
438
|
other.invalidate?.();
|
|
391
439
|
}
|
|
392
440
|
}
|
|
441
|
+
// 展开 round 卡内工具时,让 compact 的强制折叠放行它(非 round 内工具为空操作)。
|
|
442
|
+
if (isTool) markCompactRoundToolExpanded(component);
|
|
393
443
|
component.setExpanded(true);
|
|
394
|
-
|
|
444
|
+
clearPendingCollapsePress();
|
|
395
445
|
} else {
|
|
396
446
|
// 普通工具截断头 show-more:打开全量预览(不收起)。
|
|
397
447
|
const view = isTool ? component.resultRendererComponent : null;
|
|
@@ -399,7 +449,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
399
449
|
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
400
450
|
const section = view.matchShowMoreLine(plain);
|
|
401
451
|
if (section) {
|
|
402
|
-
|
|
452
|
+
clearPendingCollapsePress();
|
|
403
453
|
const box = view.showMoreHitbox(plain);
|
|
404
454
|
return tryOpenToolIoShowMore({
|
|
405
455
|
kind: "show-more",
|
|
@@ -412,17 +462,20 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
412
462
|
});
|
|
413
463
|
}
|
|
414
464
|
}
|
|
415
|
-
//
|
|
416
|
-
|
|
465
|
+
// 其余放行官方:选区、OSC8 链接与 click 合成都交给 renderer。
|
|
466
|
+
// 完整单击的收起:官方卡由 guard 处理,自有卡在松手时结算。
|
|
467
|
+
rememberCollapsePress(card, pressComponent, packet);
|
|
468
|
+
return false;
|
|
417
469
|
}
|
|
418
|
-
// 点击后清 hover
|
|
470
|
+
// 点击后清 hover 高亮。只 invalidate 点中的组件(或它所在的 group),
|
|
471
|
+
// 不要动 owner 面板:面板 invalidate 会连带重跑 updateContent,重建 thinking 实例。
|
|
419
472
|
setHoveredToolCallId(null);
|
|
420
473
|
setHoveredToolGroup(null);
|
|
421
474
|
setHoveredThinking(null);
|
|
422
475
|
setHoveredMessageDisplay(null);
|
|
423
476
|
setHoveredToolIo(null, null);
|
|
424
477
|
setHoveredCompactAssistant(null);
|
|
425
|
-
|
|
478
|
+
(target.group ?? component).invalidate?.();
|
|
426
479
|
tui.requestRender?.();
|
|
427
480
|
return true;
|
|
428
481
|
}
|
|
@@ -526,7 +579,22 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
526
579
|
if (packets && tui.hasOverlay?.() && hasActiveTextPreview()) return undefined;
|
|
527
580
|
if (packets && !tui.hasOverlay?.()) {
|
|
528
581
|
for (const packet of packets) {
|
|
529
|
-
if (isSgrLeftRelease(packet))
|
|
582
|
+
if (isSgrLeftRelease(packet)) {
|
|
583
|
+
// 只给 compact 面板清选区:自有面板收起会重排布局,官方选区锚点
|
|
584
|
+
// 还停在旧布局上,同一次 release 会结算出多行高亮。
|
|
585
|
+
if (
|
|
586
|
+
isCompactAssistantComponent(
|
|
587
|
+
resolveCollapsePress(tui, packet, { skipOfficialCards: true }),
|
|
588
|
+
)
|
|
589
|
+
) {
|
|
590
|
+
try {
|
|
591
|
+
Reflect.apply(original, this, [FULLSCREEN_FOCUS_OUT]);
|
|
592
|
+
} catch {
|
|
593
|
+
/* 老版本没有选区状态 */
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
} else if (!isSgrLeftPress(packet) && !isSgrIdleMotion(packet))
|
|
597
|
+
clearPendingCollapsePressOnMove(packet);
|
|
530
598
|
if (isSgrLeftPress(packet) && handleFullscreenToolClick(tui, packet)) {
|
|
531
599
|
return { consume: true };
|
|
532
600
|
}
|
|
@@ -639,6 +707,11 @@ function buildInteractionFrame(
|
|
|
639
707
|
if (placement.componentRow < cardStart) continue;
|
|
640
708
|
const finalRow = lineIndexToScreenRow(placement.lineIndex);
|
|
641
709
|
if (finalRow >= 1 && finalRow <= visibleRows) {
|
|
710
|
+
// 展开态的 ↑ Collapse 提示单独成区:hover 高亮 hint,点击仍收起整卡。
|
|
711
|
+
const hintBox = collapsedHintHitbox(placement.finalLine);
|
|
712
|
+
if (hintBox && isCollapseHintLine(stripTerminalSequences(placement.finalLine))) {
|
|
713
|
+
regions.push({ kind: "collapse-hint", row: finalRow, ...hintBox, component });
|
|
714
|
+
}
|
|
642
715
|
regions.push({
|
|
643
716
|
kind: "expanded-card",
|
|
644
717
|
row: finalRow,
|
|
@@ -868,8 +941,15 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
868
941
|
let consumed = false;
|
|
869
942
|
for (const packet of packets) {
|
|
870
943
|
updateToolSummaryHover(getToolMouseTui(), packet);
|
|
871
|
-
if (isSgrLeftRelease(packet))
|
|
872
|
-
|
|
944
|
+
if (isSgrLeftRelease(packet)) {
|
|
945
|
+
resolveCollapsePress(getToolMouseTui(), packet, { skipOfficialCards: false });
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (!isSgrLeftPress(packet)) {
|
|
949
|
+
// 带键拖动或滚轮:位置变了就不再是单击。
|
|
950
|
+
if (!isSgrIdleMotion(packet)) clearPendingCollapsePressOnMove(packet);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
873
953
|
if (toggleToolAtMouseClick(getToolMouseTui(), packet)) {
|
|
874
954
|
consumed = true;
|
|
875
955
|
}
|
|
@@ -899,7 +979,8 @@ export function teardownToolMouseInteraction(
|
|
|
899
979
|
setHoveredToolIo(null, null);
|
|
900
980
|
setHoveredCompactAssistant(null);
|
|
901
981
|
releaseToolCardMouseGuard();
|
|
902
|
-
|
|
982
|
+
clearPendingCollapsePress();
|
|
983
|
+
suppressOfficialCardClickUntil = 0;
|
|
903
984
|
try {
|
|
904
985
|
if (isLazyProxyTui(getToolMouseTui())) releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
905
986
|
else getToolMouseTui()?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
@@ -947,6 +1028,13 @@ type ToolCardMouseGuard = {
|
|
|
947
1028
|
wrapper: (event: any) => any;
|
|
948
1029
|
};
|
|
949
1030
|
|
|
1031
|
+
/** 官方 click 落在展开卡上:同一 group 的任意内部工具行都收起整个 group。 */
|
|
1032
|
+
function collapseExpandedCardFromOfficialClick(component: any): void {
|
|
1033
|
+
if (typeof component?.setExpanded !== "function") return;
|
|
1034
|
+
const group = component?.[TOOL_GROUPING_PARENT_KEY];
|
|
1035
|
+
collapseExpandedCard(getToolMouseTui(), group instanceof ToolGroupComponent ? group : component);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
950
1038
|
function installToolCardMouseGuard(): void {
|
|
951
1039
|
const prototype = (ToolExecutionComponent as any)?.prototype;
|
|
952
1040
|
if (typeof prototype?.handleMouse !== "function") return;
|
|
@@ -956,6 +1044,19 @@ function installToolCardMouseGuard(): void {
|
|
|
956
1044
|
// 每次取当前值作为下游,可能是别的扩展的包装。
|
|
957
1045
|
const original = prototype.handleMouse;
|
|
958
1046
|
const wrapper = function (this: any, event: any) {
|
|
1047
|
+
if (event?.type === "click" && event.button === "left") {
|
|
1048
|
+
// 扩展刚收起过自有卡:这次 click 属于同一次单击,吞掉避免内部工具被翻回展开。
|
|
1049
|
+
if (suppressOfficialCardClickUntil && Date.now() <= suppressOfficialCardClickUntil) {
|
|
1050
|
+
suppressOfficialCardClickUntil = 0;
|
|
1051
|
+
if (this?.expanded) collapseExpandedCardFromOfficialClick(this);
|
|
1052
|
+
return { handled: true };
|
|
1053
|
+
}
|
|
1054
|
+
// 展开卡整卡 click:接管官方 toggle,改为收起(group 内部工具行收起整个 group)。
|
|
1055
|
+
if (this?.expanded && typeof this.setExpanded === "function") {
|
|
1056
|
+
collapseExpandedCardFromOfficialClick(this);
|
|
1057
|
+
return { handled: true };
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
959
1060
|
if (blocksOfficialCardToggle(this, event)) return { handled: true };
|
|
960
1061
|
return original.call(this, event);
|
|
961
1062
|
};
|
|
@@ -11,12 +11,17 @@ export type ComponentRowHit = {
|
|
|
11
11
|
row: number;
|
|
12
12
|
/** 内部工具命中时所属的展开 group;普通卡点击仍折叠整个 group。 */
|
|
13
13
|
group?: ToolGroupComponent;
|
|
14
|
+
/** compact 展开面板内的命中:外层面板,单击非提示区时收起它。 */
|
|
15
|
+
owner?: any;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
|
-
/** 行内 [click to show more] 提示的命中列区间(1-based,含两端)。 */
|
|
18
|
+
/** 行内 [click to show more] / [↑ Collapse] 提示的命中列区间(1-based,含两端)。 */
|
|
17
19
|
export function collapsedHintHitbox(line: string): { startCol: number; endCol: number } | null {
|
|
18
20
|
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
19
|
-
const match =
|
|
21
|
+
const match =
|
|
22
|
+
/(\([^()\n]* \/ click\)|click to show more|to show more|↑ Collapse|to collapse)(?=\)?\s*$)/.exec(
|
|
23
|
+
plain,
|
|
24
|
+
);
|
|
20
25
|
if (!match?.[1]) return null;
|
|
21
26
|
const startCol = visibleWidth(plain.slice(0, match.index)) + 1;
|
|
22
27
|
return { startCol, endCol: startCol + visibleWidth(match[1]) - 1 };
|
|
@@ -119,11 +124,15 @@ export function componentAtLocalRow(
|
|
|
119
124
|
return { component, row: localRow };
|
|
120
125
|
}
|
|
121
126
|
if (isCompactAssistantComponent(component)) {
|
|
122
|
-
//
|
|
127
|
+
// 折叠:整行摘要。展开:先命中面板内 thinking/工具卡/文本,其余(空行)仍归外层卡片。
|
|
123
128
|
if (component.expanded === true) {
|
|
124
129
|
const inner = nestedChildAtRow(component, localRow, width);
|
|
125
|
-
if (inner
|
|
126
|
-
|
|
130
|
+
if (inner) {
|
|
131
|
+
// 展开的 thinking 有自己的展开态:点它只收它自己,不收整块面板。
|
|
132
|
+
const ownExpandable = inner instanceof ThinkingPreviewBlock && inner.expanded === true;
|
|
133
|
+
return ownExpandable
|
|
134
|
+
? { component: inner, row: localRow }
|
|
135
|
+
: { component: inner, row: localRow, owner: component };
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
return { component, row: localRow };
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
10
10
|
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
11
11
|
import { mcpToolTitle } from "./mcp-title.ts";
|
|
12
|
-
import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
|
|
12
|
+
import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
|
|
13
13
|
import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
|
|
14
14
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
15
15
|
import {
|
|
@@ -372,7 +372,8 @@ export class ToolGroupComponent extends Container {
|
|
|
372
372
|
const overallColor = overall === "pending" ? "accent" : overall;
|
|
373
373
|
const nameList = names.size > 1 ? ` ${fg("dim", `• ${toolNameList(this.children)}`)}` : "";
|
|
374
374
|
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
375
|
-
const
|
|
375
|
+
const hintText = this._expanded ? collapseHintText() : showMoreHintText();
|
|
376
|
+
const hint = `${fg("dim", "•")} ${fg(this.hintHovered ? "text" : "dim", hintText)}`;
|
|
376
377
|
const lines = [
|
|
377
378
|
"",
|
|
378
379
|
truncateToWidth(
|
|
@@ -36,3 +36,13 @@ export function expandShortcutText(): string {
|
|
|
36
36
|
export function showMoreHintText(): string {
|
|
37
37
|
return isToolTuiFullscreen() ? "click to show more" : `${expandShortcutText()} to show more`;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** 展开卡的收起提示,与折叠态 showMoreHintText 同一位置对称。 */
|
|
41
|
+
export function collapseHintText(): string {
|
|
42
|
+
return isToolTuiFullscreen() ? "↑ Collapse" : `${expandShortcutText()} to collapse`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 该行是否带展开态收起提示(用于展开卡的 hint hover 命中)。 */
|
|
46
|
+
export function isCollapseHintLine(plainLine: string): boolean {
|
|
47
|
+
return plainLine.includes(collapseHintText());
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Claude Code-style TUI output with some personal touches and handy utilities.",
|
|
6
6
|
"type": "module",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"test": "node --test tests/**/*.test.ts",
|
|
41
|
+
"coverage": "node --test --experimental-test-coverage --test-coverage-include=extensions/**/*.ts tests/**/*.test.ts",
|
|
41
42
|
"typecheck": "tsc --noEmit",
|
|
42
43
|
"format": "biome format --write .",
|
|
43
44
|
"format:check": "biome format .",
|