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 +19 -27
- package/extensions/compact-transcript.ts +132 -5
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,25 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
A compact transcript extension for [pi](https://pi.dev).
|
|
4
4
|
|
|
5
|
-
|
|
|
5
|
+
| With the extension | Without the extension|
|
|
6
6
|
|---|---|
|
|
7
|
-
|
|
|
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,
|
|
12
|
-
-
|
|
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. `◆ 4× 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
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 === "
|
|
749
|
-
|
|
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.
|
|
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": [
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./extensions/compact-transcript.ts"
|
|
26
|
+
]
|
|
25
27
|
},
|
|
26
28
|
"peerDependencies": {
|
|
27
29
|
"@earendil-works/pi-coding-agent": "*",
|