pi-cc-extensions 0.8.62 → 0.8.64
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 +3 -1
- package/README.md +3 -1
- package/extensions/feature/compact-thinking.ts +57 -12
- package/extensions/index.ts +0 -2
- package/extensions/renderer/compact-mode.ts +104 -79
- package/extensions/renderer/mouse/hover.ts +13 -0
- package/extensions/renderer/mouse/interaction.ts +57 -16
- package/extensions/renderer/mouse/layout.ts +36 -2
- package/extensions/renderer/tool/message-display.ts +19 -2
- package/package.json +1 -1
- package/extensions/feature/shell/model-status.ts +0 -19
package/README.en.md
CHANGED
|
@@ -39,7 +39,7 @@ Run `/reload` after installation.
|
|
|
39
39
|
| --------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
40
40
|
| Claude Code Output | Tool summaries, expand/collapse, rich edit/write diffs, and`on` / `compact` / `off` modes | `/ccstyle` |
|
|
41
41
|
| Markdown enhancements | Mermaid art, admonitions, URL linking, and more | Automatic |
|
|
42
|
-
| Fullscreen mode | Tool card/group click-to
|
|
42
|
+
| Fullscreen mode | Tool card/group click to expand, double-click to collapse, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
|
|
43
43
|
| Settings panel | `Style / Diff / Thinking / Feature` tabs: startup header toggle and wheel step | `/ccstyle` |
|
|
44
44
|
| Context inspection | Usage breakdown and previews for the system prompt, tools, skills, and messages | `/context` |
|
|
45
45
|
| Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
|
|
@@ -68,6 +68,8 @@ Run `/reload` after installation.
|
|
|
68
68
|
}
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
> **Fullscreen clicks**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. Click a truncated Input/Output header to open the full preview. Double-click an expanded panel to collapse it. Single clicks on an expanded card still select text and open links.
|
|
72
|
+
>
|
|
71
73
|
> **Tip**: 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.
|
|
72
74
|
|
|
73
75
|
## Local development
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
39
39
|
| --------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
40
40
|
| Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `compact` / `off` 三种模式 | `/ccstyle` |
|
|
41
41
|
| Markdown 增强 | Mermaid 图、提示框、URL 链接化等 | 自动生效 |
|
|
42
|
-
| Fullscreen mode | 工具卡/group
|
|
42
|
+
| Fullscreen mode | 工具卡/group 单击展开、双击收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
|
|
43
43
|
| 配置面板 | `Style / Diff / Thinking / Feature` 四页签,含启动头开关与滚轮步进 | `/ccstyle` |
|
|
44
44
|
| 上下文检查 | 查看上下文占用,并预览 System prompt、Tools、Skills 和消息内容 | `/context` |
|
|
45
45
|
| Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
|
|
@@ -74,6 +74,8 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
> **Fullscreen 点击**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要。单击截断的 Input/Output 打开全量预览。双击展开面板收起。展开卡上的单击仍可选择文本、打开链接。
|
|
78
|
+
>
|
|
77
79
|
> **建议**:`markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
|
|
78
80
|
|
|
79
81
|
## 本地开发
|
|
@@ -200,12 +200,58 @@ function cachePreview(key: string, entry: PreviewCacheEntry): void {
|
|
|
200
200
|
|
|
201
201
|
export function clearThinkingPreviewCache(): void {
|
|
202
202
|
previewCache.clear();
|
|
203
|
+
expandedWrapCache.clear();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type ExpandedWrapEntry = { text: string; lines: string[] };
|
|
207
|
+
const expandedWrapCache = new Map<string, ExpandedWrapEntry>();
|
|
208
|
+
export const THINKING_EXPANDED_WRAP_CACHE_MAX = 64;
|
|
209
|
+
|
|
210
|
+
function expandedWrapKey(messageTimestamp: number, runStartIndex: number, width: number): string {
|
|
211
|
+
return `${messageTimestamp}:${runStartIndex}:${width}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function wrapExpandedThinking(
|
|
215
|
+
messageTimestamp: number,
|
|
216
|
+
runStartIndex: number,
|
|
217
|
+
text: string,
|
|
218
|
+
width: number,
|
|
219
|
+
): string[] {
|
|
220
|
+
const key = expandedWrapKey(messageTimestamp, runStartIndex, width);
|
|
221
|
+
const hit = expandedWrapCache.get(key);
|
|
222
|
+
if (hit && hit.text === text) {
|
|
223
|
+
expandedWrapCache.delete(key);
|
|
224
|
+
expandedWrapCache.set(key, hit);
|
|
225
|
+
return hit.lines;
|
|
226
|
+
}
|
|
227
|
+
const lines = wrapTextWithAnsi(text.replace(/\t/g, " "), Math.max(1, width));
|
|
228
|
+
expandedWrapCache.delete(key);
|
|
229
|
+
while (expandedWrapCache.size >= THINKING_EXPANDED_WRAP_CACHE_MAX) {
|
|
230
|
+
const oldest = expandedWrapCache.keys().next().value;
|
|
231
|
+
if (oldest === undefined) break;
|
|
232
|
+
expandedWrapCache.delete(oldest);
|
|
233
|
+
}
|
|
234
|
+
expandedWrapCache.set(key, { text, lines });
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function evictExpandedWraps(messageTimestamp: number, runStartIndex: number): void {
|
|
239
|
+
for (const key of expandedWrapCache.keys()) {
|
|
240
|
+
const [ts, run] = key.split(":");
|
|
241
|
+
if (Number(ts) === messageTimestamp && Number(run) === runStartIndex) {
|
|
242
|
+
expandedWrapCache.delete(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
203
245
|
}
|
|
204
246
|
|
|
205
247
|
export function thinkingPreviewCacheSize(): number {
|
|
206
248
|
return previewCache.size;
|
|
207
249
|
}
|
|
208
250
|
|
|
251
|
+
export function thinkingExpandedWrapCacheSize(): number {
|
|
252
|
+
return expandedWrapCache.size;
|
|
253
|
+
}
|
|
254
|
+
|
|
209
255
|
/** 按可见宽度估算折行数,不走 Text 全量 wrap。无换行长段也能计到隐藏行。 */
|
|
210
256
|
function countWrappedLines(text: string, contentWidth: number): number {
|
|
211
257
|
if (!text) return 0;
|
|
@@ -280,17 +326,17 @@ function hiddenPreviewHint(
|
|
|
280
326
|
|
|
281
327
|
const expandedThinking = new Set<number>();
|
|
282
328
|
|
|
283
|
-
/** 折叠预览 + 展开全文。fullscreen 点击 hint
|
|
329
|
+
/** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、双击整块收起,对齐工具卡。 */
|
|
284
330
|
export class ThinkingPreviewBlock implements Component {
|
|
285
331
|
private heading: string;
|
|
286
332
|
private text: string;
|
|
287
333
|
private padding: number;
|
|
288
|
-
|
|
334
|
+
readonly messageTimestamp: number;
|
|
335
|
+
readonly runStartIndex: number;
|
|
289
336
|
private style: (text: string) => string;
|
|
290
337
|
private theme: Theme | undefined;
|
|
291
338
|
private _expanded: boolean;
|
|
292
339
|
private hintHovered = false;
|
|
293
|
-
private expandedBody: { width: number; lines: string[] } | undefined;
|
|
294
340
|
private collapsedMemo:
|
|
295
341
|
| {
|
|
296
342
|
width: number;
|
|
@@ -308,11 +354,13 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
308
354
|
messageTimestamp: number,
|
|
309
355
|
style: (text: string) => string,
|
|
310
356
|
theme?: Theme,
|
|
357
|
+
runStartIndex = 0,
|
|
311
358
|
) {
|
|
312
359
|
this.heading = heading;
|
|
313
360
|
this.text = text;
|
|
314
361
|
this.padding = padding;
|
|
315
362
|
this.messageTimestamp = messageTimestamp;
|
|
363
|
+
this.runStartIndex = runStartIndex;
|
|
316
364
|
this.style = style;
|
|
317
365
|
this.theme = theme;
|
|
318
366
|
this._expanded = expandedThinking.has(messageTimestamp);
|
|
@@ -334,7 +382,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
334
382
|
if (expanded) expandedThinking.add(this.messageTimestamp);
|
|
335
383
|
else {
|
|
336
384
|
expandedThinking.delete(this.messageTimestamp);
|
|
337
|
-
this.
|
|
385
|
+
evictExpandedWraps(this.messageTimestamp, this.runStartIndex);
|
|
338
386
|
}
|
|
339
387
|
}
|
|
340
388
|
|
|
@@ -368,13 +416,10 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
368
416
|
return { lines: [], hiddenLines: 0 };
|
|
369
417
|
}
|
|
370
418
|
if (this._expanded) {
|
|
371
|
-
|
|
372
|
-
this.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
return { lines: this.expandedBody.lines, hiddenLines: 0 };
|
|
419
|
+
return {
|
|
420
|
+
lines: wrapExpandedThinking(this.messageTimestamp, this.runStartIndex, this.text, width),
|
|
421
|
+
hiddenLines: 0,
|
|
422
|
+
};
|
|
378
423
|
}
|
|
379
424
|
const preview = layoutThinkingPreview(this.text, width, padding);
|
|
380
425
|
return { lines: preview.visible, hiddenLines: preview.hiddenLines };
|
|
@@ -430,7 +475,6 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
430
475
|
|
|
431
476
|
invalidate() {
|
|
432
477
|
this.collapsedMemo = undefined;
|
|
433
|
-
this.expandedBody = undefined;
|
|
434
478
|
}
|
|
435
479
|
}
|
|
436
480
|
|
|
@@ -715,6 +759,7 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
715
759
|
message.timestamp,
|
|
716
760
|
thinkingStyle,
|
|
717
761
|
activeTheme,
|
|
762
|
+
runStartIndex,
|
|
718
763
|
),
|
|
719
764
|
);
|
|
720
765
|
|
package/extensions/index.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { config } from "./config/config.ts";
|
|
|
5
5
|
import piAliases from "./feature/shell/aliases.ts";
|
|
6
6
|
import piStartupHeader from "./feature/shell/startup-header.ts";
|
|
7
7
|
import workingMessage from "./feature/shell/working-message.ts";
|
|
8
|
-
import modelStatus from "./feature/shell/model-status.ts";
|
|
9
8
|
|
|
10
9
|
// feature
|
|
11
10
|
import agentAutocomplete from "./feature/reference/subagent.ts";
|
|
@@ -23,7 +22,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
23
22
|
if (config.enableAliases) piAliases(pi);
|
|
24
23
|
piStartupHeader(pi);
|
|
25
24
|
if (config.enableWorkingMessage) workingMessage(pi);
|
|
26
|
-
modelStatus(pi);
|
|
27
25
|
|
|
28
26
|
// render stack:thinking controller 直接交给 style 作 query
|
|
29
27
|
markdownEnhance(pi);
|
|
@@ -322,70 +322,104 @@ function editWriteExpandedCard(theme: any): any {
|
|
|
322
322
|
);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
/** compact 展开卡:外卡片保持 userMessageBg 原色;内部 tool call card
|
|
325
|
+
/** compact 展开卡:外卡片保持 userMessageBg 原色;内部 tool call card
|
|
326
326
|
* 背景更深一层且只覆盖内容区(左右内缩、上下限首尾文本行),形成嵌套层次。
|
|
327
|
-
*
|
|
328
|
-
function
|
|
327
|
+
* hits:非工具子卡(thinking)的行区间,供展开后点击 hint。 */
|
|
328
|
+
function layoutExpandedToolCard(
|
|
329
|
+
theme: any,
|
|
330
|
+
children: any[],
|
|
331
|
+
width: number,
|
|
332
|
+
): { lines: string[]; hits: Array<{ child: any; start: number; end: number }> } {
|
|
329
333
|
const slot = "userMessageBg";
|
|
330
334
|
const toolBgAnsi = darkenBgAnsi(theme, slot);
|
|
335
|
+
const innerWidth = Math.max(0, width - 2);
|
|
336
|
+
const lines: string[] = [];
|
|
337
|
+
const hits: Array<{ child: any; start: number; end: number }> = [];
|
|
338
|
+
const isThinkingPreview = (child: any) => typeof child?.setHintHovered === "function";
|
|
339
|
+
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
340
|
+
// 展开不再显示摘要行:跳过第一个 child 的首行空白,避免白占一行
|
|
341
|
+
let skipLeadingBlank = true;
|
|
342
|
+
for (const child of children) {
|
|
343
|
+
const childLines = child.render(innerWidth);
|
|
344
|
+
// 展开的 thinking 与 tool 一样走嵌套深色卡,避免贴在外卡同色底上看不见。
|
|
345
|
+
const nest = child.__ccToolCard || (isThinkingPreview(child) && child.expanded === true);
|
|
346
|
+
if (!nest) {
|
|
347
|
+
let start = 0;
|
|
348
|
+
if (skipLeadingBlank) {
|
|
349
|
+
while (start < childLines.length && !hasVisibleText(childLines[start])) start++;
|
|
350
|
+
skipLeadingBlank = false;
|
|
351
|
+
}
|
|
352
|
+
const rangeStart = lines.length;
|
|
353
|
+
for (let i = start; i < childLines.length; i++) {
|
|
354
|
+
lines.push(paddedBackgroundRow(theme, slot, childLines[i], width));
|
|
355
|
+
}
|
|
356
|
+
if (lines.length > rangeStart) hits.push({ child, start: rangeStart, end: lines.length });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
// 工具卡:前导 1 空行 + 首尾文本行之间的内容区(深色);不保留尾随,
|
|
360
|
+
// 相邻工具卡之间正好 1 空行,底部由卡片 padding 收尾。
|
|
361
|
+
skipLeadingBlank = false;
|
|
362
|
+
let first = -1;
|
|
363
|
+
let last = -1;
|
|
364
|
+
for (let i = 0; i < childLines.length; i++) {
|
|
365
|
+
if (hasVisibleText(childLines[i])) {
|
|
366
|
+
if (first < 0) first = i;
|
|
367
|
+
last = i;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (first < 0) {
|
|
371
|
+
for (const line of childLines) {
|
|
372
|
+
lines.push(paddedBackgroundRow(theme, slot, line, width));
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
377
|
+
const rangeStart = lines.length;
|
|
378
|
+
// 子卡片内部上下各 1 行深色 padding
|
|
379
|
+
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, "", width));
|
|
380
|
+
for (let i = first; i <= last; i++) {
|
|
381
|
+
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, childLines[i], width));
|
|
382
|
+
}
|
|
383
|
+
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, "", width));
|
|
384
|
+
if (isThinkingPreview(child)) hits.push({ child, start: rangeStart, end: lines.length });
|
|
385
|
+
}
|
|
386
|
+
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
387
|
+
return { lines, hits };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function compactRoundCard(
|
|
391
|
+
cardItems: Array<{ child?: any; tool?: any }>,
|
|
392
|
+
toolRender: (tool: any, width: number) => string[],
|
|
393
|
+
): any {
|
|
331
394
|
const children: any[] = [];
|
|
395
|
+
for (const item of cardItems) {
|
|
396
|
+
if (item.child) children.push(item.child);
|
|
397
|
+
else if (item.tool) {
|
|
398
|
+
const tool = item.tool;
|
|
399
|
+
children.push({
|
|
400
|
+
__ccToolCard: true,
|
|
401
|
+
render: (innerWidth: number) => toolRender(tool, innerWidth),
|
|
402
|
+
invalidate: () => tool.invalidate?.(),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
332
406
|
return {
|
|
333
407
|
children,
|
|
334
|
-
addChild(child: any) {
|
|
335
|
-
children.push(child);
|
|
336
|
-
},
|
|
337
|
-
addToolChild(child: any) {
|
|
338
|
-
child.__ccToolCard = true;
|
|
339
|
-
children.push(child);
|
|
340
|
-
},
|
|
341
408
|
render(width: number): string[] {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
let start = 0;
|
|
351
|
-
if (skipLeadingBlank) {
|
|
352
|
-
while (start < childLines.length && !hasVisibleText(childLines[start])) start++;
|
|
353
|
-
skipLeadingBlank = false;
|
|
354
|
-
}
|
|
355
|
-
for (let i = start; i < childLines.length; i++) {
|
|
356
|
-
lines.push(paddedBackgroundRow(theme, slot, childLines[i], width));
|
|
357
|
-
}
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
// 工具卡:前导 1 空行 + 首尾文本行之间的内容区(深色);不保留尾随,
|
|
361
|
-
// 相邻工具卡之间正好 1 空行,底部由卡片 padding 收尾。
|
|
362
|
-
skipLeadingBlank = false;
|
|
363
|
-
let first = -1;
|
|
364
|
-
let last = -1;
|
|
365
|
-
for (let i = 0; i < childLines.length; i++) {
|
|
366
|
-
if (hasVisibleText(childLines[i])) {
|
|
367
|
-
if (first < 0) first = i;
|
|
368
|
-
last = i;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (first < 0) {
|
|
372
|
-
for (const line of childLines) {
|
|
373
|
-
lines.push(paddedBackgroundRow(theme, slot, line, width));
|
|
374
|
-
}
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
378
|
-
// 子卡片内部上下各 1 行深色 padding
|
|
379
|
-
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, "", width));
|
|
380
|
-
for (let i = first; i <= last; i++) {
|
|
381
|
-
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, childLines[i], width));
|
|
382
|
-
}
|
|
383
|
-
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, "", width));
|
|
409
|
+
return ["", ...layoutExpandedToolCard(themeOf(), children, width).lines];
|
|
410
|
+
},
|
|
411
|
+
childAtRow(localRow: number, width: number) {
|
|
412
|
+
if (localRow < 1) return null;
|
|
413
|
+
const row = localRow - 1;
|
|
414
|
+
const { hits } = layoutExpandedToolCard(themeOf(), children, width);
|
|
415
|
+
for (const hit of hits) {
|
|
416
|
+
if (row >= hit.start && row < hit.end) return hit.child;
|
|
384
417
|
}
|
|
385
|
-
|
|
386
|
-
|
|
418
|
+
return null;
|
|
419
|
+
},
|
|
420
|
+
invalidate() {
|
|
421
|
+
for (const child of children) child.invalidate?.();
|
|
387
422
|
},
|
|
388
|
-
invalidate() {},
|
|
389
423
|
};
|
|
390
424
|
}
|
|
391
425
|
|
|
@@ -477,7 +511,7 @@ function compactEditWriteLine(
|
|
|
477
511
|
component: any,
|
|
478
512
|
width: number,
|
|
479
513
|
writeMetadata?: WriteExecutionMetadataStore,
|
|
480
|
-
options: { hint?: boolean } = {},
|
|
514
|
+
options: { hint?: boolean; flushLeft?: boolean } = {},
|
|
481
515
|
): string[] {
|
|
482
516
|
const theme = themeOf();
|
|
483
517
|
const name = String(component.toolName ?? "tool");
|
|
@@ -511,7 +545,8 @@ function compactEditWriteLine(
|
|
|
511
545
|
statsStyled = ` ${theme.fg("dim", "(")}${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)}${theme.fg("dim", ")")}`;
|
|
512
546
|
}
|
|
513
547
|
}
|
|
514
|
-
|
|
548
|
+
// 展开卡 Box(1,1) 已 pad;折叠行自己留 1 格前导空格
|
|
549
|
+
const iconPart = `${options.flushLeft ? "" : " "}${theme.fg(iconColor, icon)} `;
|
|
515
550
|
const namePart = theme.fg("toolTitle", name);
|
|
516
551
|
const hintText =
|
|
517
552
|
options.hint !== false && component.expanded !== true ? ` • ${showMoreHintText()}` : "";
|
|
@@ -603,6 +638,7 @@ function compactEditWriteLines(
|
|
|
603
638
|
state.ccstyleIoView,
|
|
604
639
|
component.args,
|
|
605
640
|
component,
|
|
641
|
+
true, // 外层 Box(1,1) 已 pad
|
|
606
642
|
);
|
|
607
643
|
component.resultRendererComponent = detail;
|
|
608
644
|
}
|
|
@@ -614,12 +650,16 @@ function compactEditWriteLines(
|
|
|
614
650
|
const box = editWriteExpandedCard(theme);
|
|
615
651
|
box.addChild({
|
|
616
652
|
render(innerWidth: number): string[] {
|
|
617
|
-
return compactEditWriteLine(component, innerWidth, writeMetadata, {
|
|
653
|
+
return compactEditWriteLine(component, innerWidth, writeMetadata, {
|
|
654
|
+
hint: false,
|
|
655
|
+
flushLeft: true,
|
|
656
|
+
}).slice(1);
|
|
618
657
|
},
|
|
619
658
|
invalidate() {},
|
|
620
659
|
});
|
|
621
660
|
box.addChild(detail);
|
|
622
|
-
|
|
661
|
+
// 与原生 ToolExecutionComponent 一样:Spacer(1) + Box,否则贴上一条 tool 少 1 行间距
|
|
662
|
+
return ["", ...box.render(width)];
|
|
623
663
|
}
|
|
624
664
|
|
|
625
665
|
function compactAssistantLineComponent(
|
|
@@ -980,26 +1020,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
980
1020
|
tool.updateDisplay?.();
|
|
981
1021
|
}
|
|
982
1022
|
}
|
|
983
|
-
round.anchor.contentContainer.addChild(
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
if (item.child) box.addChild(item.child);
|
|
989
|
-
else if (item.tool) {
|
|
990
|
-
const tool = item.tool;
|
|
991
|
-
box.addToolChild({
|
|
992
|
-
render: (innerWidth: number) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
993
|
-
invalidate: () => tool.invalidate?.(),
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
return ["", ...box.render(width)];
|
|
998
|
-
},
|
|
999
|
-
invalidate() {
|
|
1000
|
-
for (const item of cardItems) item.child?.invalidate?.();
|
|
1001
|
-
},
|
|
1002
|
-
});
|
|
1023
|
+
round.anchor.contentContainer.addChild(
|
|
1024
|
+
compactRoundCard(cardItems, (tool, innerWidth) =>
|
|
1025
|
+
patch.toolOriginalRender.call(tool, innerWidth),
|
|
1026
|
+
),
|
|
1027
|
+
);
|
|
1003
1028
|
// 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
|
|
1004
1029
|
appendStopStatus(round.anchor, stopStatus);
|
|
1005
1030
|
return;
|
|
@@ -54,6 +54,16 @@ export function setHoveredThinking(block: ThinkingPreviewBlock | null): boolean
|
|
|
54
54
|
return true;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
let hoveredMessageDisplay: any = null;
|
|
58
|
+
|
|
59
|
+
export function setHoveredMessageDisplay(component: any): boolean {
|
|
60
|
+
if (component === hoveredMessageDisplay) return false;
|
|
61
|
+
hoveredMessageDisplay?.setHintHovered?.(false);
|
|
62
|
+
hoveredMessageDisplay = component;
|
|
63
|
+
component?.setHintHovered?.(true);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
let hoveredToolIoView: ExpandedToolIoView | null = null;
|
|
58
68
|
let hoveredToolIoSection: ToolIoSection | null = null;
|
|
59
69
|
|
|
@@ -110,6 +120,7 @@ export type FullscreenHoverTarget =
|
|
|
110
120
|
| { kind: "button" }
|
|
111
121
|
| { kind: "group"; component: ToolGroupComponent }
|
|
112
122
|
| { kind: "thinking"; component: ThinkingPreviewBlock }
|
|
123
|
+
| { kind: "message"; component: any }
|
|
113
124
|
| { kind: "assistant"; component: any }
|
|
114
125
|
| {
|
|
115
126
|
kind: "tool";
|
|
@@ -133,6 +144,8 @@ export function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | n
|
|
|
133
144
|
if (setHoveredToolGroup(nextGroup)) changed = true;
|
|
134
145
|
const nextThinking = target?.kind === "thinking" ? target.component : null;
|
|
135
146
|
if (setHoveredThinking(nextThinking)) changed = true;
|
|
147
|
+
const nextMessage = target?.kind === "message" ? target.component : null;
|
|
148
|
+
if (setHoveredMessageDisplay(nextMessage)) changed = true;
|
|
136
149
|
const nextAssistant = target?.kind === "assistant" ? target.component : null;
|
|
137
150
|
if (setHoveredCompactAssistant(nextAssistant)) changed = true;
|
|
138
151
|
const nextView = target?.kind === "tool" ? target.view : null;
|
|
@@ -3,6 +3,7 @@ import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
|
3
3
|
import { patchRegistry, TOOL_MOUSE_OWNER_KEY } from "../../utils/patch-keys.ts";
|
|
4
4
|
import { ToolGroupComponent } from "../tool/grouping.ts";
|
|
5
5
|
import { isCompactAssistantComponent, setHoveredCompactAssistant } from "../compact-mode.ts";
|
|
6
|
+
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
6
7
|
import { config } from "../../config/config.ts";
|
|
7
8
|
import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
|
|
8
9
|
import { setToolTuiFullscreen } from "../tool/show-more-hint.ts";
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
setHoveredToolCallId,
|
|
59
60
|
setHoveredToolGroup,
|
|
60
61
|
setHoveredThinking,
|
|
62
|
+
setHoveredMessageDisplay,
|
|
61
63
|
setHoveredToolIo,
|
|
62
64
|
type FullscreenHoverTarget,
|
|
63
65
|
} from "./hover.ts";
|
|
@@ -153,6 +155,42 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
153
155
|
tui.requestRender?.();
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
const EXPAND_PANEL_DOUBLE_CLICK_MS = 400;
|
|
159
|
+
let lastExpandPanelClick: { id: unknown; at: number } | null = null;
|
|
160
|
+
|
|
161
|
+
function expandPanelIdentity(card: any): unknown {
|
|
162
|
+
return card instanceof ThinkingPreviewBlock
|
|
163
|
+
? `${card.messageTimestamp}:${card.runStartIndex}`
|
|
164
|
+
: card;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isExpandPanelDoubleClick(card: any): boolean {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const id = expandPanelIdentity(card);
|
|
170
|
+
const prev = lastExpandPanelClick;
|
|
171
|
+
lastExpandPanelClick = { id, at: now };
|
|
172
|
+
return Boolean(prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function clearExpandPanelDoubleClick(): void {
|
|
176
|
+
lastExpandPanelClick = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function collapseExpandedCard(tui: any, card: any): boolean {
|
|
180
|
+
if (!isExpandPanelDoubleClick(card)) return false;
|
|
181
|
+
card.setExpanded(false);
|
|
182
|
+
setHoveredToolCallId(null);
|
|
183
|
+
setHoveredToolGroup(null);
|
|
184
|
+
setHoveredThinking(null);
|
|
185
|
+
setHoveredMessageDisplay(null);
|
|
186
|
+
setHoveredToolIo(null, null);
|
|
187
|
+
setHoveredCompactAssistant(null);
|
|
188
|
+
card.invalidate?.();
|
|
189
|
+
tui.requestRender?.();
|
|
190
|
+
clearExpandPanelDoubleClick();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
156
194
|
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
157
195
|
const region = interactionRegionAt(packet);
|
|
158
196
|
if (!region) return false;
|
|
@@ -160,16 +198,9 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
160
198
|
if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
|
|
161
199
|
const component = region.component;
|
|
162
200
|
if (!component) return false;
|
|
163
|
-
if (region.kind === "expanded-card")
|
|
164
|
-
component.setExpanded(false);
|
|
165
|
-
setHoveredToolCallId(null);
|
|
166
|
-
setHoveredToolGroup(null);
|
|
167
|
-
setHoveredToolIo(null, null);
|
|
168
|
-
component.invalidate?.();
|
|
169
|
-
tui.requestRender?.();
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
201
|
+
if (region.kind === "expanded-card") return collapseExpandedCard(tui, component);
|
|
172
202
|
component.setExpanded(true);
|
|
203
|
+
clearExpandPanelDoubleClick();
|
|
173
204
|
setHoveredToolCallId(null);
|
|
174
205
|
setHoveredToolGroup(null);
|
|
175
206
|
setHoveredToolIo(null, null);
|
|
@@ -242,10 +273,10 @@ const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
|
|
|
242
273
|
const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol("ccstyle.fullscreen-wheel-scroll-original");
|
|
243
274
|
|
|
244
275
|
/**
|
|
245
|
-
* 官方 fullscreen 工具卡点击:collapsed hint
|
|
246
|
-
* (有且仅保持一个展开:展开前收起其他工具卡),expanded
|
|
247
|
-
* 截断头 show-more
|
|
248
|
-
* 滚动条列、含 OSC8
|
|
276
|
+
* 官方 fullscreen 工具卡点击:collapsed hint 单击展开
|
|
277
|
+
* (有且仅保持一个展开:展开前收起其他工具卡),expanded 整卡双击收起,
|
|
278
|
+
* 截断头 show-more 单击打开全量预览;回到底部按钮 scrollToBottom。
|
|
279
|
+
* 滚动条列、含 OSC8 链接行、非工具区域、展开卡单击放行官方。
|
|
249
280
|
*/
|
|
250
281
|
function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
251
282
|
const layout = tui.currentLayout;
|
|
@@ -276,7 +307,8 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
276
307
|
const isGroup = component instanceof ToolGroupComponent;
|
|
277
308
|
const isAssistant = isCompactAssistantComponent(component);
|
|
278
309
|
const isThinking = component instanceof ThinkingPreviewBlock;
|
|
279
|
-
|
|
310
|
+
const isMessage = isMessageDisplayComponent(component);
|
|
311
|
+
if (!isTool && !isGroup && !isAssistant && !isThinking && !isMessage) return false;
|
|
280
312
|
if (!component.expanded) {
|
|
281
313
|
// collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
|
|
282
314
|
const hint = collapsedHintHitbox(line);
|
|
@@ -286,11 +318,14 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
286
318
|
collectFullscreenToolCards(hit.box.component, others);
|
|
287
319
|
for (const other of others) {
|
|
288
320
|
if (other !== component && other.expanded) {
|
|
321
|
+
// 展开 round 内 thinking 时不要把外层 compact 卡收起。
|
|
322
|
+
if (isThinking && isCompactAssistantComponent(other)) continue;
|
|
289
323
|
other.setExpanded(false);
|
|
290
324
|
other.invalidate?.();
|
|
291
325
|
}
|
|
292
326
|
}
|
|
293
327
|
component.setExpanded(true);
|
|
328
|
+
clearExpandPanelDoubleClick();
|
|
294
329
|
} else {
|
|
295
330
|
// 普通工具截断头 show-more:打开全量预览(不收起)。
|
|
296
331
|
const view = isTool ? component.resultRendererComponent : null;
|
|
@@ -312,13 +347,14 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
312
347
|
}
|
|
313
348
|
}
|
|
314
349
|
}
|
|
315
|
-
//
|
|
316
|
-
card
|
|
350
|
+
// 双击收起:内部工具仍归所属 group。单击放行官方(选择/链接)。
|
|
351
|
+
return collapseExpandedCard(tui, card);
|
|
317
352
|
}
|
|
318
353
|
// 点击后清 hover 高亮。
|
|
319
354
|
setHoveredToolCallId(null);
|
|
320
355
|
setHoveredToolGroup(null);
|
|
321
356
|
setHoveredThinking(null);
|
|
357
|
+
setHoveredMessageDisplay(null);
|
|
322
358
|
setHoveredToolIo(null, null);
|
|
323
359
|
setHoveredCompactAssistant(null);
|
|
324
360
|
card.invalidate?.();
|
|
@@ -389,6 +425,8 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
389
425
|
} else if (isCompactAssistantComponent(component)) {
|
|
390
426
|
// compact 摘要行:折叠时仅提示文字高亮,展开卡整体高亮。
|
|
391
427
|
if (component.expanded || overHint) target = { kind: "assistant", component };
|
|
428
|
+
} else if (isMessageDisplayComponent(component)) {
|
|
429
|
+
if (overHint) target = { kind: "message", component };
|
|
392
430
|
}
|
|
393
431
|
}
|
|
394
432
|
}
|
|
@@ -785,8 +823,10 @@ export function teardownToolMouseInteraction(
|
|
|
785
823
|
setHoveredToolCallId(null);
|
|
786
824
|
setHoveredToolGroup(null);
|
|
787
825
|
setHoveredThinking(null);
|
|
826
|
+
setHoveredMessageDisplay(null);
|
|
788
827
|
setHoveredToolIo(null, null);
|
|
789
828
|
setHoveredCompactAssistant(null);
|
|
829
|
+
clearExpandPanelDoubleClick();
|
|
790
830
|
try {
|
|
791
831
|
if (isLazyProxyTui(getToolMouseTui())) releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
792
832
|
else getToolMouseTui()?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
@@ -811,6 +851,7 @@ export function teardownToolMouseInteraction(
|
|
|
811
851
|
export function resetToolHoverState(): void {
|
|
812
852
|
setHoveredToolCallId(null);
|
|
813
853
|
setHoveredThinking(null);
|
|
854
|
+
setHoveredMessageDisplay(null);
|
|
814
855
|
setHoveredCompactAssistant(null);
|
|
815
856
|
setScrollButtonVisible(false);
|
|
816
857
|
setScrollButtonHovered(false);
|
|
@@ -2,6 +2,7 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
3
3
|
import { isCompactAssistantComponent } from "../compact-mode.ts";
|
|
4
4
|
import { ToolGroupComponent } from "../tool/grouping.ts";
|
|
5
|
+
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
5
6
|
import { isToolExecutionComponent, stripTerminalSequencesPreservingLayout } from "./packets.ts";
|
|
6
7
|
import { getScrollButtonWidget } from "./scroll.ts";
|
|
7
8
|
|
|
@@ -77,6 +78,29 @@ export function isScrollbarColumnAt(layout: any, x: number): boolean {
|
|
|
77
78
|
return hit;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/** compact 展开卡用 childAtRow 映射 thinking;其余按 render 行数下钻。 */
|
|
82
|
+
function nestedChildAtRow(node: any, localRow: number, width: number): any {
|
|
83
|
+
if (typeof node?.childAtRow === "function") {
|
|
84
|
+
return node.childAtRow(localRow, width);
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(node?.children)) return null;
|
|
87
|
+
let offset = 0;
|
|
88
|
+
for (const child of node.children) {
|
|
89
|
+
let count = 0;
|
|
90
|
+
try {
|
|
91
|
+
const rendered = child.render?.(width);
|
|
92
|
+
if (Array.isArray(rendered)) count = rendered.length;
|
|
93
|
+
} catch {
|
|
94
|
+
count = 0;
|
|
95
|
+
}
|
|
96
|
+
if (localRow < offset + count) {
|
|
97
|
+
return nestedChildAtRow(child, localRow - offset, width);
|
|
98
|
+
}
|
|
99
|
+
offset += count;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
80
104
|
/**
|
|
81
105
|
* 布局 leaf box 的组件通常是容器(documentContainer/dock 容器等),工具卡与
|
|
82
106
|
* widget 在其 children 内。按局部行遍历组件树,定位实际命中的子组件。
|
|
@@ -95,12 +119,21 @@ export function componentAtLocalRow(
|
|
|
95
119
|
return { component, row: localRow };
|
|
96
120
|
}
|
|
97
121
|
if (isCompactAssistantComponent(component)) {
|
|
98
|
-
//
|
|
122
|
+
// 折叠:整行摘要。展开:先命中内部 thinking hint,其余仍归外层卡片。
|
|
123
|
+
if (component.expanded === true) {
|
|
124
|
+
const inner = nestedChildAtRow(component, localRow, width);
|
|
125
|
+
if (inner instanceof ThinkingPreviewBlock) {
|
|
126
|
+
return { component: inner, row: localRow };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
99
129
|
return { component, row: localRow };
|
|
100
130
|
}
|
|
101
131
|
if (component instanceof ThinkingPreviewBlock) {
|
|
102
132
|
return { component, row: localRow };
|
|
103
133
|
}
|
|
134
|
+
if (isMessageDisplayComponent(component)) {
|
|
135
|
+
return { component, row: localRow };
|
|
136
|
+
}
|
|
104
137
|
if (component === getScrollButtonWidget()) {
|
|
105
138
|
return { component, row: localRow };
|
|
106
139
|
}
|
|
@@ -139,7 +172,8 @@ export function collectFullscreenToolCards(
|
|
|
139
172
|
isToolExecutionComponent(component) ||
|
|
140
173
|
component instanceof ToolGroupComponent ||
|
|
141
174
|
isCompactAssistantComponent(component) ||
|
|
142
|
-
component instanceof ThinkingPreviewBlock
|
|
175
|
+
component instanceof ThinkingPreviewBlock ||
|
|
176
|
+
isMessageDisplayComponent(component)
|
|
143
177
|
) {
|
|
144
178
|
out.push(component);
|
|
145
179
|
return;
|
|
@@ -55,6 +55,15 @@ const BRANCH_KIND: DisplayKind = {
|
|
|
55
55
|
body: (component) => String(component.message?.summary ?? ""),
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
function ensureHintHover(component: any): void {
|
|
59
|
+
if (typeof component.setHintHovered === "function") return;
|
|
60
|
+
component.setHintHovered = function (this: any, hovered: boolean) {
|
|
61
|
+
if (this.hintHovered === hovered) return;
|
|
62
|
+
this.hintHovered = hovered;
|
|
63
|
+
this.invalidate?.();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
58
67
|
function renderCcstyle(component: any, kind: DisplayKind): void {
|
|
59
68
|
const theme = displayTheme;
|
|
60
69
|
if (!theme) return; // 主题未就绪时保留原生渲染
|
|
@@ -64,13 +73,21 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
|
|
|
64
73
|
component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
|
|
65
74
|
}
|
|
66
75
|
component.clear();
|
|
76
|
+
ensureHintHover(component);
|
|
67
77
|
const icon = `${BRIGHT_GREEN}✓${ANSI_FG_RESET}`; // 已完成消息,等同工具成功态
|
|
68
78
|
const title = theme.fg("toolTitle", kind.title(component));
|
|
69
79
|
if (!component.expanded) {
|
|
70
|
-
const
|
|
80
|
+
const hovered = component.hintHovered === true;
|
|
81
|
+
const hint = `${theme.fg("dim", " • ")}${theme.fg(hovered ? "text" : "dim", showMoreHintText())}`;
|
|
71
82
|
component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
|
|
72
83
|
return;
|
|
73
84
|
}
|
|
85
|
+
// 展开卡与 tool 一致:userMessageBg + 上下左右 1 格
|
|
86
|
+
component.paddingX = 1;
|
|
87
|
+
component.paddingY = 1;
|
|
88
|
+
if (typeof theme.bg === "function" && typeof component.setBgFn === "function") {
|
|
89
|
+
component.setBgFn((text: string) => theme.bg("userMessageBg", text));
|
|
90
|
+
}
|
|
74
91
|
component.addChild(new Text(`${icon} ${title}`, 0, 0));
|
|
75
92
|
component.addChild(new Spacer(1));
|
|
76
93
|
component.addChild(
|
|
@@ -134,7 +151,7 @@ export function installMessageDisplayRendering(): () => void {
|
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
/** 三个消息组件共享的结构特征;CustomMessageComponent 用 rebuild 无 updateDisplay,不命中。 */
|
|
137
|
-
function isMessageDisplayComponent(value: any): boolean {
|
|
154
|
+
export function isMessageDisplayComponent(value: any): boolean {
|
|
138
155
|
return Boolean(
|
|
139
156
|
value &&
|
|
140
157
|
typeof value === "object" &&
|
package/package.json
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
function modelLabel(model: any): string | undefined {
|
|
4
|
-
if (!model) return undefined;
|
|
5
|
-
const name = model.name || model.id;
|
|
6
|
-
if (!name) return undefined;
|
|
7
|
-
return model.provider ? `${model.provider} · ${name}` : name;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function apply(ctx: any): void {
|
|
11
|
-
if (!ctx?.hasUI) return;
|
|
12
|
-
ctx.ui?.setStatus?.("model", modelLabel(ctx.model));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** 用 setStatus 挂模型名,不 setFooter,不接管原生 footer。 */
|
|
16
|
-
export default function (pi: ExtensionAPI): void {
|
|
17
|
-
pi.on("session_start", async (_event, ctx) => apply(ctx));
|
|
18
|
-
pi.on("model_select", async (_event, ctx) => apply(ctx));
|
|
19
|
-
}
|