pi-pretty-tui 0.2.2 → 0.2.3

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.md CHANGED
@@ -26,7 +26,7 @@ Pi may warn that built-in tools are being overridden. This is expected: pi-prett
26
26
 
27
27
  ## Working with tool calls
28
28
 
29
- Click a `Running(...)` or `Done(...)` row to keep it as a parent and reveal tools, completed `● thought` entries, and extension updates beneath it. Click an individual tool or thought to reveal nested details without replacing its summary, or click the parent again to collapse the whole group. Press `Ctrl+O` at any time to use Pi's global expansion control for all tool output and thinking content. Tool names use each definition's `label` verbatim, falling back to its raw `name`; pi-pretty-tui does not rename or title-case them.
29
+ Click a `Running(...)` or `Done(...)` row to keep it as a parent and reveal tools, completed `● thought` entries, and extension updates beneath it. Click an individual tool or thought to reveal nested details without replacing its summary. The last item in each expanded tree, `└─ [↑ Collapse]`, collapses just that group without scrolling back to its parent; clicking the parent also works. Press `Ctrl+O` at any time to use Pi's global expansion control for all tool output and thinking content. The end-of-tree action still collapses only its own group after global expansion. Third-party replacements for `grep` and `find` (such as FFF) use compact tree rows too, with their native details available when expanded. Tool names use each definition's `label` verbatim, falling back to its raw `name`; pi-pretty-tui does not rename or title-case them.
30
30
 
31
31
  Extension calls to `ctx.ui.notify(message, "info")` (or with the type omitted) join an existing thought/tool group in chronological order; without existing work they remain independent and never attach to a future group. Warnings and errors stay standalone, use yellow `⚠` and red `✕`, and are hard boundaries. Tool failures remain owned by their tool details. None of these extension notifications or errors appear in the transient UI-status line.
32
32
 
package/README.zh-CN.md CHANGED
@@ -26,7 +26,7 @@ Pi 可能会提示内置工具被覆盖。这是正常现象:pi-pretty-tui 会
26
26
 
27
27
  ## 使用工具调用
28
28
 
29
- 点击 `Running(...)` 或 `Done(...)` 行,可以保留父级并展开其下方的工具、已完成的 `● thought` 和扩展更新。点击单个工具或思考子项可在保留摘要的同时展开嵌套详情,再次点击父级可折叠整个分组。任何时候都可以按 `Ctrl+O`,使用 Pi 的全局展开控制来显示或隐藏全部工具输出和思考内容。工具名称会原样使用定义中的 `label`,没有 `label` 时回退到原始 `name`;pi-pretty-tui 不再重命名或转换大小写。
29
+ 点击 `Running(...)` 或 `Done(...)` 行,可以保留父级并展开其下方的工具、已完成的 `● thought` 和扩展更新。点击单个工具或思考子项可在保留摘要的同时展开嵌套详情。每棵展开的树末尾都有 `└─ [↑ Collapse]`,无需滚回父级即可只折叠当前分组;再次点击父级也可以折叠。任何时候都可以按 `Ctrl+O`,使用 Pi 的全局展开控制来显示或隐藏全部工具输出和思考内容;全局展开后,树尾操作仍只折叠本组。第三方替换的 `grep`、`find`(如 FFF)也使用紧凑的树节点,展开子项时仍可查看原生详情。工具名称会原样使用定义中的 `label`,没有 `label` 时回退到原始 `name`;pi-pretty-tui 不再重命名或转换大小写。
30
30
 
31
31
  扩展调用 `ctx.ui.notify(message, "info")`(或省略 type)时,如果当前已有 thought/tool 活动组,通知会按真实时间顺序加入该组;如果没有现有工作,则独立显示,并且不会附着到未来的工具组。warning 和 error 保持独立,分别使用黄色 `⚠` 和红色 `✕`,并形成硬边界。工具失败继续归属于对应工具详情。上述扩展通知和错误都不会进入临时 UI 状态栏。
32
32
 
@@ -214,6 +214,8 @@ export default function prettyTui(pi: ExtensionAPI) {
214
214
  };
215
215
  const activityTimeline = new ActivityTimeline();
216
216
  const revealedActivityGroups = new Set<string>();
217
+ // A local collapse must still work when Ctrl+O has expanded every group.
218
+ const collapsedActivityGroups = new Set<string>();
217
219
  const expandedThinkingMembers = new Set<string>();
218
220
  const thinkingComponents = new Map<string, any>();
219
221
  const activityFallbackThemes = new Map<string, any>();
@@ -342,7 +344,8 @@ export default function prettyTui(pi: ExtensionAPI) {
342
344
  };
343
345
 
344
346
  const activityGroupRevealed = (group: ActivityGroup): boolean =>
345
- cleanToolsExpanded || revealedActivityGroups.has(group.id);
347
+ !collapsedActivityGroups.has(group.id) &&
348
+ (cleanToolsExpanded || revealedActivityGroups.has(group.id));
346
349
 
347
350
  // A group is revealable once it owns anything renderable beneath its parent
348
351
  // row. Thinking counts on its own: a tool-free turn still gets a parent row
@@ -355,6 +358,67 @@ export default function prettyTui(pi: ExtensionAPI) {
355
358
  last: group.members[group.members.length - 1]?.id === member.id,
356
359
  });
357
360
 
361
+ const collapseLabel = "[↑ Collapse]";
362
+ const collapsePrefix = " └─ ";
363
+ const renderActivityCollapse = (group: ActivityGroup, width: number): string => {
364
+ const theme = activityGroupTheme(group);
365
+ return truncateToWidth(
366
+ theme.fg("dim", collapsePrefix) + theme.fg("muted", collapseLabel),
367
+ Math.max(1, width),
368
+ "",
369
+ );
370
+ };
371
+ const withActivityCollapse = (
372
+ group: ActivityGroup,
373
+ member: ActivityMember,
374
+ width: number,
375
+ lines: string[],
376
+ ): string[] => activityMemberPosition(group, member).last && activityGroupRevealed(group)
377
+ ? [...lines, renderActivityCollapse(group, width)]
378
+ : lines;
379
+ const activityCollapseClicked = (
380
+ group: ActivityGroup,
381
+ member: ActivityMember,
382
+ event: any,
383
+ row: number,
384
+ ): boolean => activityGroupRevealed(group) &&
385
+ activityMemberPosition(group, member).last &&
386
+ event.type === "click" && event.button === "left" && event.y === row &&
387
+ event.x >= visibleWidth(collapsePrefix) &&
388
+ event.x < Math.min(event.width, visibleWidth(collapsePrefix + collapseLabel));
389
+
390
+ const refreshActivityGroup = (group: ActivityGroup): void => {
391
+ for (const child of group.members) {
392
+ if (child.kind === "tool" && child.toolCallId) {
393
+ cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
394
+ } else if (child.kind === "thinking" && child.messageKey) {
395
+ thinkingComponents.get(child.messageKey)?.invalidate?.();
396
+ } else if (child.kind === "update") {
397
+ activityUpdateComponents.get(child.id)?.invalidate?.();
398
+ }
399
+ }
400
+ };
401
+ const revealActivityGroup = (group: ActivityGroup, ui?: any): void => {
402
+ collapsedActivityGroups.delete(group.id);
403
+ revealedActivityGroups.add(group.id);
404
+ for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
405
+ refreshActivityGroup(group);
406
+ (ui ?? currentTui)?.requestRender?.();
407
+ };
408
+ const collapseActivityGroup = (group: ActivityGroup, ui?: any): void => {
409
+ revealedActivityGroups.delete(group.id);
410
+ if (cleanToolsExpanded) collapsedActivityGroups.add(group.id);
411
+ else collapsedActivityGroups.delete(group.id);
412
+ for (const toolCallId of group.toolCallIds) {
413
+ cleanCompactToolCallIds.delete(toolCallId);
414
+ const component = cleanToolComponents.get(toolCallId);
415
+ if (component?.expanded) component.setExpanded(false);
416
+ }
417
+ for (const child of group.members) expandedThinkingMembers.delete(child.id);
418
+ refreshActivityGroup(group);
419
+ (ui ?? currentTui)?.requestRender?.();
420
+ };
421
+
358
422
  const activityTreeStyle = (
359
423
  group: ActivityGroup,
360
424
  member: ActivityMember,
@@ -362,8 +426,10 @@ export default function prettyTui(pi: ExtensionAPI) {
362
426
  memberTheme?: any,
363
427
  ) => {
364
428
  const { last } = activityMemberPosition(group, member);
365
- const rawPrefix = last ? " └─ " : " ├─ ";
366
- const rawContinuation = last ? " " : " │ ";
429
+ // While expanded, the collapse action is the final sibling in the tree.
430
+ const hasFollowingCollapse = last && activityGroupRevealed(group);
431
+ const rawPrefix = last && !hasFollowingCollapse ? " └─ " : " ├─ ";
432
+ const rawContinuation = last && !hasFollowingCollapse ? " " : " │ ";
367
433
  const prefixWidth = visibleWidth(rawPrefix);
368
434
  if (width <= prefixWidth) {
369
435
  return { prefix: "", continuation: "", childWidth: Math.max(1, width), prefixWidth: 0 };
@@ -450,9 +516,10 @@ export default function prettyTui(pi: ExtensionAPI) {
450
516
  truncateToWidth((index === 0 ? prefix : continuation) + line, Math.max(1, width), "")
451
517
  );
452
518
  const position = activityMemberPosition(group, member);
453
- return position.first
454
- ? ["", ...renderActivityGroupSummary(group, width), ...decorated]
455
- : decorated;
519
+ return withActivityCollapse(
520
+ group, member, width,
521
+ position.first ? ["", ...renderActivityGroupSummary(group, width), ...decorated] : decorated,
522
+ );
456
523
  };
457
524
 
458
525
  const renderPendingActivityUpdate = (member: ActivityMember, width: number): string[] => {
@@ -495,29 +562,16 @@ export default function prettyTui(pi: ExtensionAPI) {
495
562
  const position = activityMemberPosition(group, member);
496
563
  if (!activityGroupRevealed(group)) {
497
564
  if (!position.first) return undefined;
498
- revealedActivityGroups.add(group.id);
499
- for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
500
- } else {
501
- const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
502
- if (!position.first || event.y <= 0 || event.y > summaryHeight) return undefined;
503
- revealedActivityGroups.delete(group.id);
504
- for (const toolCallId of group.toolCallIds) {
505
- cleanCompactToolCallIds.delete(toolCallId);
506
- const component = cleanToolComponents.get(toolCallId);
507
- if (component?.expanded) component.setExpanded(false);
508
- }
509
- for (const child of group.members) expandedThinkingMembers.delete(child.id);
510
- }
511
- for (const child of group.members) {
512
- if (child.kind === "tool" && child.toolCallId) {
513
- cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
514
- } else if (child.kind === "thinking" && child.messageKey) {
515
- thinkingComponents.get(child.messageKey)?.invalidate?.();
516
- } else if (child.kind === "update") {
517
- activityUpdateComponents.get(child.id)?.invalidate?.();
518
- }
565
+ revealActivityGroup(group);
566
+ return { handled: true };
519
567
  }
520
- currentTui?.requestRender?.();
568
+ const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
569
+ const onSummary = position.first && event.y > 0 && event.y <= summaryHeight;
570
+ const onCollapse = position.last && activityCollapseClicked(
571
+ group, member, event, renderActivityUpdate(group, member, event.width).length - 1,
572
+ );
573
+ if (!onSummary && !onCollapse) return undefined;
574
+ collapseActivityGroup(group);
521
575
  return { handled: true };
522
576
  };
523
577
 
@@ -606,6 +660,7 @@ export default function prettyTui(pi: ExtensionAPI) {
606
660
 
607
661
  cleanCompactToolCallIds.clear();
608
662
  revealedActivityGroups.clear();
663
+ collapsedActivityGroups.clear();
609
664
  expandedThinkingMembers.clear();
610
665
  renderMode = requested;
611
666
  if (renderMode === "clean") refreshLatestActivityWidget();
@@ -1015,15 +1070,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1015
1070
  );
1016
1071
 
1017
1072
  const refreshGroup = (group: ActivityGroup) => {
1018
- for (const member of group.members) {
1019
- if (member.kind === "tool" && member.toolCallId) {
1020
- cleanToolComponents.get(member.toolCallId)?.updateDisplay?.();
1021
- } else if (member.kind === "thinking" && member.messageKey) {
1022
- thinkingComponents.get(member.messageKey)?.invalidate?.();
1023
- } else if (member.kind === "update") {
1024
- activityUpdateComponents.get(member.id)?.invalidate?.();
1025
- }
1026
- }
1073
+ refreshActivityGroup(group);
1027
1074
  currentTui?.requestRender?.();
1028
1075
  };
1029
1076
 
@@ -1189,9 +1236,10 @@ export default function prettyTui(pi: ExtensionAPI) {
1189
1236
  const decorated = contentLines.map((line, index) =>
1190
1237
  truncateToWidth((index === 0 ? prefix : continuation) + line, Math.max(1, width), "")
1191
1238
  );
1192
- const output = position.first
1193
- ? ["", ...renderActivityGroupSummary(group, width), ...decorated]
1194
- : decorated;
1239
+ const output = withActivityCollapse(
1240
+ group, member, width,
1241
+ position.first ? ["", ...renderActivityGroupSummary(group, width), ...decorated] : decorated,
1242
+ );
1195
1243
  if (cacheable) {
1196
1244
  this[cleanThinkingRenderCacheKey] = {
1197
1245
  width,
@@ -1232,10 +1280,11 @@ export default function prettyTui(pi: ExtensionAPI) {
1232
1280
  // visible Markdown. The Markdown is rendered after the activity rows;
1233
1281
  // route clicks in that suffix back to Pi's native component so links,
1234
1282
  // text selection, and code-block Copy controls keep working.
1283
+ let activityHeight: number | undefined;
1235
1284
  if (visibleAssistantText(message)) {
1236
1285
  const visibleLines = originalRender.call(this, event.width);
1237
1286
  const renderedLines = patchedRender.call(this, event.width);
1238
- const activityHeight = Math.max(0, renderedLines.length - visibleLines.length);
1287
+ activityHeight = Math.max(0, renderedLines.length - visibleLines.length);
1239
1288
  if (event.y >= activityHeight) {
1240
1289
  return originalHandleMouse.call(this, {
1241
1290
  ...event,
@@ -1247,25 +1296,21 @@ export default function prettyTui(pi: ExtensionAPI) {
1247
1296
 
1248
1297
  if (!activityGroupRevealed(group)) {
1249
1298
  if (!isLeftClick) return undefined;
1250
- if (position.first) {
1251
- revealedActivityGroups.add(group.id);
1252
- for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
1253
- refreshGroup(group);
1254
- }
1299
+ if (position.first) revealActivityGroup(group);
1255
1300
  return { handled: true };
1256
1301
  }
1257
1302
  const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
1258
- if (position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) {
1259
- revealedActivityGroups.delete(group.id);
1260
- for (const toolCallId of group.toolCallIds) {
1261
- cleanCompactToolCallIds.delete(toolCallId);
1262
- const component = cleanToolComponents.get(toolCallId);
1263
- if (component?.expanded) component.setExpanded(false);
1264
- }
1265
- for (const child of group.members) expandedThinkingMembers.delete(child.id);
1266
- refreshGroup(group);
1303
+ const collapseRow = position.last
1304
+ ? (activityHeight ?? patchedRender.call(this, event.width).length) - 1
1305
+ : -1;
1306
+ if (
1307
+ (position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) ||
1308
+ activityCollapseClicked(group, member, event, collapseRow)
1309
+ ) {
1310
+ collapseActivityGroup(group);
1267
1311
  return { handled: true };
1268
1312
  }
1313
+ if (position.last && event.y === collapseRow) return undefined;
1269
1314
  if (isLeftClick) {
1270
1315
  if (expandedThinkingMembers.has(member.id)) expandedThinkingMembers.delete(member.id);
1271
1316
  else expandedThinkingMembers.add(member.id);
@@ -1390,6 +1435,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1390
1435
  const originalShowError = interactiveModePrototype.showError;
1391
1436
  const patchedSetToolsExpanded = function (this: any, expanded: boolean) {
1392
1437
  cleanToolsExpanded = expanded;
1438
+ collapsedActivityGroups.clear();
1393
1439
  changingAllToolsExpansion = true;
1394
1440
  if (!expanded) {
1395
1441
  cleanCompactToolCallIds.clear();
@@ -2426,32 +2472,8 @@ export default function prettyTui(pi: ExtensionAPI) {
2426
2472
  const originalToolHandleMouse = toolExecutionPrototype.handleMouse;
2427
2473
  const renderedModeKey = Symbol("pretty-tui.tool-rendered-mode");
2428
2474
  const cleanChildRenderCacheKey = Symbol("pretty-tui.clean-child-render-cache");
2429
- const refreshActivityGroup = (group: ActivityGroup) => {
2430
- for (const member of group.members) {
2431
- if (member.kind === "tool" && member.toolCallId) {
2432
- cleanToolComponents.get(member.toolCallId)?.updateDisplay();
2433
- } else if (member.kind === "thinking" && member.messageKey) {
2434
- thinkingComponents.get(member.messageKey)?.invalidate?.();
2435
- }
2436
- }
2437
- };
2438
- const revealCleanGroup = (group: ActivityGroup, ui?: any) => {
2439
- revealedActivityGroups.add(group.id);
2440
- for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
2441
- refreshActivityGroup(group);
2442
- ui?.requestRender?.();
2443
- };
2444
- const collapseCleanGroup = (group: ActivityGroup, ui?: any) => {
2445
- revealedActivityGroups.delete(group.id);
2446
- for (const toolCallId of group.toolCallIds) {
2447
- const component = cleanToolComponents.get(toolCallId);
2448
- if (component?.expanded) originalSetExpanded.call(component, false);
2449
- cleanCompactToolCallIds.delete(toolCallId);
2450
- }
2451
- for (const member of group.members) expandedThinkingMembers.delete(member.id);
2452
- refreshActivityGroup(group);
2453
- ui?.requestRender?.();
2454
- };
2475
+ const revealCleanGroup = revealActivityGroup;
2476
+ const collapseCleanGroup = collapseActivityGroup;
2455
2477
  const patchedMarkExecutionStarted = function (this: any) {
2456
2478
  activityTimeline.addTool(this.toolCallId, this.toolName);
2457
2479
  if (!cleanRun.currentToolCallIds.includes(this.toolCallId)) {
@@ -2512,7 +2534,11 @@ export default function prettyTui(pi: ExtensionAPI) {
2512
2534
  continuation,
2513
2535
  childWidth,
2514
2536
  } = activityTreeStyle(group, member, width, childTheme);
2515
- const thirdParty = !SPECIALIZED_TOOL_NAMES.has(this.toolName);
2537
+ // Other extensions (notably FFF in override mode) can register their own
2538
+ // grep/find renderers under built-in names. Their default boxed shell is
2539
+ // not one of our self-rendering specialized tools.
2540
+ const thirdParty = !SPECIALIZED_TOOL_NAMES.has(this.toolName) ||
2541
+ this.toolDefinition?.renderShell !== "self";
2516
2542
  const cacheable = !this.expanded && Boolean(this.result) && !this.isPartial;
2517
2543
  const cached = cacheable ? this[cleanChildRenderCacheKey] : undefined;
2518
2544
  let decoratedContent: string[];
@@ -2578,8 +2604,10 @@ export default function prettyTui(pi: ExtensionAPI) {
2578
2604
  };
2579
2605
  }
2580
2606
  }
2581
- if (!position.first) return decoratedContent;
2582
- return ["", ...renderActivityGroupSummary(group, width), ...decoratedContent];
2607
+ return withActivityCollapse(
2608
+ group, member, width,
2609
+ position.first ? ["", ...renderActivityGroupSummary(group, width), ...decoratedContent] : decoratedContent,
2610
+ );
2583
2611
  };
2584
2612
  const patchedToolHandleMouse = function (this: any, event: any) {
2585
2613
  const member = activityTimeline.memberForTool(this.toolCallId);
@@ -2598,7 +2626,11 @@ export default function prettyTui(pi: ExtensionAPI) {
2598
2626
  if (!revealed) return isLeftClick ? { handled: true } : undefined;
2599
2627
 
2600
2628
  const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
2601
- if (position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) {
2629
+ const onSummary = position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick;
2630
+ const onCollapse = position.last && activityCollapseClicked(
2631
+ group, member, event, patchedToolRender.call(this, event.width).length - 1,
2632
+ );
2633
+ if (onSummary || onCollapse) {
2602
2634
  changingAllToolsExpansion = true;
2603
2635
  try {
2604
2636
  collapseCleanGroup(group, this.ui);
@@ -2612,7 +2644,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2612
2644
  // replaces that spacer with the compact header for non-first members.
2613
2645
  const thirdPartyHeaderY = position.first ? summaryHeight + 1 : 0;
2614
2646
  if (
2615
- !SPECIALIZED_TOOL_NAMES.has(this.toolName) &&
2647
+ (!SPECIALIZED_TOOL_NAMES.has(this.toolName) || this.toolDefinition?.renderShell !== "self") &&
2616
2648
  isLeftClick &&
2617
2649
  event.y === thirdPartyHeaderY
2618
2650
  ) {
@@ -3074,6 +3106,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3074
3106
  responseHasAssistantMessage = false;
3075
3107
  nativeCustomBoundaryKeys.clear();
3076
3108
  revealedActivityGroups.clear();
3109
+ collapsedActivityGroups.clear();
3077
3110
  expandedThinkingMembers.clear();
3078
3111
  thinkingComponents.clear();
3079
3112
  activityFallbackThemes.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pretty-tui",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "A beautiful, polished TUI for the Pi coding agent",
5
5
  "license": "MIT",
6
6
  "author": "Kainan Yang <ykn0309@whu.edu.cn>",