pi-compact-transcript 0.5.0 → 0.6.1

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
@@ -2,25 +2,27 @@
2
2
 
3
3
  A compact transcript extension for [pi](https://pi.dev).
4
4
 
5
- | Without the extension | With the extension |
5
+ | With the extension | Without the extension|
6
6
  |---|---|
7
- | ![pi transcript without the extension](docs/plugin_disabled.png) | ![pi transcript with the extension](docs/plugin_enabled.png) |
7
+ | <img width="2131" height="552" alt="image" src="https://github.com/user-attachments/assets/4ec40fe8-13da-4415-8274-e26e43cca4e1" /> | <img width="2135" height="1275" alt="image" src="https://github.com/user-attachments/assets/b6fe8d3b-9170-435c-b353-98a6aa84fe7b" />
8
+ |
8
9
 
9
10
  What it does:
10
11
 
11
- - Collapses every tool call/result into a one-line preview, including custom/external tools added by other extensions.
12
- - Each tool row carries a status diamond: blinking gray `◆` while the tool runs, solid green `◆` on success, solid red `◆` on failure.
13
- - Consecutive uses of the same tool coalesce into a single row, e.g. `◆ 4× read src/foo.ts {12 lines · 8s}`; a different tool starts a new row with its own diamond.
14
- - Tool rows show durations when they take a second or longer; grouped rows show the total.
15
- - Failed tools always get their own visible row (red diamond) and end the current burst.
16
- - Tool rows render dimmed so assistant text stands out.
17
- - Hidden thinking blocks are suppressed entirely — no `Thinking...` markers.
12
+ - Collapses every tool call/result into a dimmed one-line preview with a status diamond: blinking gray `◆` while running, green on success, red on failure. Durations show at a second or longer.
13
+ - Consecutive uses of the same tool coalesce into a single row, e.g. `◆ read src/foo.ts {12 lines · 8s}`. Failed tools always get their own visible row.
18
14
  - Each agent run ends with a one-line summary, e.g. `Read 6 files, edited 2, ran 3 commands, 1 failed · 42s`.
19
- - Unknown tools preview their most meaningful string argument (command, code, query, path, url, ...) instead of dumping raw JSON args.
20
- - Expanded tool output still falls back to pi's original renderer, so you can use pi's normal tool expansion when details matter.
21
- - Minimizes vertical space so long agent runs do not scroll away as quickly.
15
+ - Suppresses `Thinking...` markers when thinking is hidden.
16
+ - Works with custom/external tools from other extensions; unknown tools preview their most meaningful string argument (command, code, query, path, url, ...) instead of raw JSON.
17
+ - Expanded tool output still uses pi's original renderer, so pi's normal tool expansion works when details matter.
22
18
 
23
- ## Install from GitHub
19
+ ## Install
20
+
21
+ ```bash
22
+ pi install npm:pi-compact-transcript
23
+ ```
24
+
25
+ Or from GitHub:
24
26
 
25
27
  ```bash
26
28
  pi install git:github.com/avhagedorn/pi-compact-transcript
@@ -38,17 +40,9 @@ Reload or restart pi after installing:
38
40
  /reload
39
41
  ```
40
42
 
41
- ## Install from npm
42
-
43
- Once published to npm:
44
-
45
- ```bash
46
- pi install npm:pi-compact-transcript
47
- ```
48
-
49
43
  ## Recommended settings
50
44
 
51
- The extension works best with hidden thinking and no output padding:
45
+ Works best with hidden thinking and no output padding — set in `~/.pi/agent/settings.json` or via `/settings`:
52
46
 
53
47
  ```json
54
48
  {
@@ -57,8 +51,6 @@ The extension works best with hidden thinking and no output padding:
57
51
  }
58
52
  ```
59
53
 
60
- Set these in `~/.pi/agent/settings.json`, or use `/settings` in pi.
61
-
62
54
  Thinking suppression only applies when `hideThinkingBlock` is on; with it off, pi renders thinking traces normally.
63
55
 
64
56
  ## Commands
@@ -69,10 +61,10 @@ Thinking suppression only applies when `hideThinkingBlock` is on; with it off, p
69
61
  /compact-transcript status # show current state
70
62
  ```
71
63
 
72
- Toggling applies to the visible transcript immediately — existing rows re-render, no reload needed. The pre-0.5 mode names (`balanced`, `aggressive`, `debug`, `disabled`) are accepted as legacy aliases for `on`/`off`.
64
+ Toggling re-renders the visible transcript immediately — no reload needed. Pre-0.5 mode names (`balanced`, `aggressive`, `debug`, `disabled`) are accepted as legacy aliases for `on`/`off`.
73
65
 
74
66
  ## Notes
75
67
 
76
- This extension changes display only. Tool execution is still handled by pi and any other extensions that registered or override tools.
68
+ This extension changes display only tool execution is still handled by pi and any other extensions that registered or override tools.
77
69
 
78
- For built-in and extension tools, compact rendering uses pi's public exported TUI components where available. The cross-tool burst compaction and thinking suppression still rely on pi's current TUI component internals, so if pi changes those internals in a future release, the extension falls back to pi's normal rendering behavior for the affected rows.
70
+ Compact rendering uses pi's public exported TUI components where available. Burst compaction and thinking suppression rely on pi's current TUI internals; if a future pi release changes those, the affected rows fall back to pi's normal rendering.
@@ -70,6 +70,9 @@ type RuntimeState = {
70
70
  toolComponents: Set<any>;
71
71
  assistantComponents: Set<any>;
72
72
  currentTheme?: Theme;
73
+ thinkingHidden: boolean;
74
+ currentThoughtHeading?: string;
75
+ thoughtAnchorId?: string;
73
76
  };
74
77
 
75
78
  const DEFAULT_CONFIG: CompactTranscriptConfig = {
@@ -117,8 +120,13 @@ function getState(): RuntimeState {
117
120
  runStats: newRunStats(),
118
121
  toolComponents: new Set(),
119
122
  assistantComponents: new Set(),
123
+ thinkingHidden: true,
120
124
  };
121
- return globalWithState[STATE_KEY]!;
125
+ const runtimeState = globalWithState[STATE_KEY]!;
126
+ // /reload keeps the global object alive; initialize fields added by newer
127
+ // versions when an older extension instance created the state object.
128
+ runtimeState.thinkingHidden ??= true;
129
+ return runtimeState;
122
130
  }
123
131
 
124
132
  const state = getState();
@@ -259,6 +267,8 @@ function resetToolRun() {
259
267
  state.currentBurst = [];
260
268
  state.hiddenToolIds = new Set();
261
269
  state.runningToolIds = new Set();
270
+ state.currentThoughtHeading = undefined;
271
+ state.thoughtAnchorId = undefined;
262
272
  stopBlinkTimer();
263
273
  }
264
274
 
@@ -310,6 +320,110 @@ function statusMarker(theme: Theme, opts: { running?: boolean; isError?: boolean
310
320
  return theme.fg("dim", "◆ ");
311
321
  }
312
322
 
323
+ function textSignalHasVisibleContent(assistantMessageEvent: any): boolean {
324
+ const type = assistantMessageEvent?.type;
325
+ if (type === "text_delta") {
326
+ return typeof assistantMessageEvent.delta === "string" && assistantMessageEvent.delta.trim().length > 0;
327
+ }
328
+ if (type === "text_end") {
329
+ return typeof assistantMessageEvent.content === "string" && assistantMessageEvent.content.trim().length > 0;
330
+ }
331
+ return false;
332
+ }
333
+
334
+ function thoughtTickerEnabled(): boolean {
335
+ // The ticker is a compact replacement for hidden thinking. When thinking is
336
+ // fully visible, showing the same headline under a tool would be duplicate.
337
+ return isEnabled() && state.thinkingHidden;
338
+ }
339
+
340
+ function cleanThoughtHeading(line: string): string {
341
+ let clean = oneLine(line)
342
+ .replace(/^#{1,6}\s+/, "")
343
+ .replace(/^[-*]\s+/, "")
344
+ .trim();
345
+ clean = clean
346
+ .replace(/^\*\*(.+)\*\*$/, "$1")
347
+ .replace(/^__(.+)__$/, "$1")
348
+ .replace(/^`(.+)`$/, "$1");
349
+ return clean.trim();
350
+ }
351
+
352
+ function extractThoughtHeading(text: unknown): string {
353
+ if (typeof text !== "string") return "";
354
+ const firstLine = text.split(/\r?\n/).find((line) => line.trim().length > 0);
355
+ return firstLine ? cleanThoughtHeading(firstLine) : "";
356
+ }
357
+
358
+ function latestThoughtHeading(message: any): string {
359
+ if (!Array.isArray(message?.content)) return "";
360
+ for (let i = message.content.length - 1; i >= 0; i--) {
361
+ const content = message.content[i];
362
+ if (content?.type === "thinking") return extractThoughtHeading(content.thinking);
363
+ }
364
+ return "";
365
+ }
366
+
367
+ function invalidateToolById(id: string | undefined) {
368
+ if (!id) return;
369
+ state.toolsById.get(id)?.invalidate?.();
370
+ }
371
+
372
+ function latestVisibleTool(): ToolInfo | undefined {
373
+ return Array.from(state.toolsById.values())
374
+ .reverse()
375
+ .find((tool) => !tool.hidden);
376
+ }
377
+
378
+ function clearCurrentThought() {
379
+ if (!state.currentThoughtHeading && !state.thoughtAnchorId) return;
380
+ const previousAnchorId = state.thoughtAnchorId;
381
+ state.currentThoughtHeading = undefined;
382
+ state.thoughtAnchorId = undefined;
383
+ invalidateToolById(previousAnchorId);
384
+ }
385
+
386
+ function setCurrentThought(heading: string) {
387
+ const nextHeading = oneLine(heading);
388
+ if (!thoughtTickerEnabled() || !nextHeading) {
389
+ clearCurrentThought();
390
+ return;
391
+ }
392
+
393
+ const previousAnchorId = state.thoughtAnchorId;
394
+ const nextAnchorId = latestVisibleTool()?.id;
395
+ const changed = state.currentThoughtHeading !== nextHeading || previousAnchorId !== nextAnchorId;
396
+ state.currentThoughtHeading = nextHeading;
397
+ state.thoughtAnchorId = nextAnchorId;
398
+ if (!changed) return;
399
+
400
+ invalidateToolById(previousAnchorId);
401
+ if (nextAnchorId !== previousAnchorId) invalidateToolById(nextAnchorId);
402
+ }
403
+
404
+ function updateCurrentThoughtFromMessage(message: any) {
405
+ const heading = latestThoughtHeading(message);
406
+ // A new thinking block starts empty; keep showing the previous heading until
407
+ // the replacement has real text so the ticker does not blink off/on between
408
+ // tool completion and the next streamed thought.
409
+ if (heading) setCurrentThought(heading);
410
+ }
411
+
412
+ function anchorCurrentThoughtTo(info: ToolInfo) {
413
+ if (!thoughtTickerEnabled() || !state.currentThoughtHeading || state.thoughtAnchorId === info.id) return;
414
+ const previousAnchorId = state.thoughtAnchorId;
415
+ state.thoughtAnchorId = info.id;
416
+ invalidateToolById(previousAnchorId);
417
+ info.invalidate?.();
418
+ }
419
+
420
+ function currentThoughtLine(toolCallId: string, theme: Theme): string {
421
+ if (!thoughtTickerEnabled() || state.thoughtAnchorId !== toolCallId || !state.currentThoughtHeading) return "";
422
+ const prefix = " ↳ ";
423
+ const budget = previewWidth((process.stdout.columns || 100) - prefix.length);
424
+ return theme.fg("dim", prefix) + theme.fg("thinkingText", limitPlain(state.currentThoughtHeading, budget));
425
+ }
426
+
313
427
  function upsertToolInfo(id: string, name: string, args: any, invalidate?: () => void): ToolInfo {
314
428
  let info = state.toolsById.get(id);
315
429
  if (!info) {
@@ -368,9 +482,10 @@ function beginTool(id: string, name: string, args: any) {
368
482
  ensureBlinkTimer();
369
483
  recordToolStart(name, args);
370
484
  joinBurst(info);
485
+ anchorCurrentThoughtTo(info);
371
486
  // The component may already be on screen from argument streaming; repaint
372
- // now so the running marker and burst count appear immediately instead of
373
- // waiting for the next result event or blink tick.
487
+ // now so the running marker, burst count, and thought ticker appear
488
+ // immediately instead of waiting for the next result event or blink tick.
374
489
  info.invalidate?.();
375
490
  }
376
491
 
@@ -515,6 +630,8 @@ function patchToolExecutionComponent() {
515
630
  }
516
631
 
517
632
  this.selfRenderContainer.addChild(new Text(line, 0, 0));
633
+ const thoughtLine = currentThoughtLine(this.toolCallId, theme);
634
+ if (thoughtLine) this.selfRenderContainer.addChild(new Text(thoughtLine, 0, 0));
518
635
  };
519
636
 
520
637
  proto.render = function patchedRender(width: number) {
@@ -534,6 +651,8 @@ function patchAssistantMessageComponent() {
534
651
 
535
652
  proto.updateContent = function patchedUpdateContent(message: any) {
536
653
  state.assistantComponents.add(this);
654
+ state.thinkingHidden = !!this.hideThinkingBlock;
655
+ if (!state.thinkingHidden) clearCurrentThought();
537
656
  if (!isEnabled() || !this.hideThinkingBlock || !Array.isArray(message?.content)) {
538
657
  return originalUpdateContent.call(this, message);
539
658
  }
@@ -549,6 +668,7 @@ function patchAssistantMessageComponent() {
549
668
  const texts = message.content.filter((c: any) => c.type === "text" && c.text?.trim());
550
669
  if (texts.length === 0) return;
551
670
 
671
+ clearCurrentThought();
552
672
  // Assistant text ends a tool burst. The live path also does this via
553
673
  // message_update events; doing it here too keeps hydrated history from
554
674
  // grouping tool rows across turn boundaries.
@@ -733,6 +853,7 @@ export default function compactTranscript(pi: ExtensionAPI) {
733
853
  pi.on("agent_end", (_event, _ctx) => {
734
854
  state.agentActive = false;
735
855
  state.currentBurst = [];
856
+ clearCurrentThought();
736
857
  state.runningToolIds.clear();
737
858
  stopBlinkTimer();
738
859
  appendRunSummary(pi);
@@ -745,8 +866,14 @@ export default function compactTranscript(pi: ExtensionAPI) {
745
866
  pi.on("message_update", (event, ctx) => {
746
867
  captureTheme(ctx);
747
868
  const type = event.assistantMessageEvent?.type;
748
- if (type === "text_delta" || type === "text_start") {
749
- // Visible assistant text ends the current tool burst.
869
+ if (typeof type === "string" && type.startsWith("thinking_")) {
870
+ updateCurrentThoughtFromMessage(event.message);
871
+ }
872
+ if (textSignalHasVisibleContent(event.assistantMessageEvent)) {
873
+ clearCurrentThought();
874
+ // Visible assistant text ends the current tool burst. Do not split on
875
+ // text_start alone: some providers create empty text blocks before a
876
+ // tool-only turn, and those blocks are hidden from the transcript.
750
877
  state.currentBurst = [];
751
878
  }
752
879
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-compact-transcript",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "A compact transcript extension for pi: collapsed thinking summaries and one-line tool previews.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,7 +21,9 @@
21
21
  "terminal"
22
22
  ],
23
23
  "pi": {
24
- "extensions": ["./extensions/compact-transcript.ts"]
24
+ "extensions": [
25
+ "./extensions/compact-transcript.ts"
26
+ ]
25
27
  },
26
28
  "peerDependencies": {
27
29
  "@earendil-works/pi-coding-agent": "*",