pi-compact-transcript 0.1.0 → 0.2.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
@@ -3,8 +3,9 @@
3
3
  A compact transcript extension for [pi](https://pi.dev):
4
4
 
5
5
  - Collapses hidden thinking blocks into `Thinking...` and consecutive thinking blocks into `Thinking... (Nx)`.
6
+ - Adds a short, visible next-step summary to collapsed thinking rows without exposing raw chain-of-thought.
6
7
  - Collapses built-in tool calls/results into one-line previews.
7
- - Consecutive tool uses are summarized as `Used N tools <latest tool preview>`.
8
+ - Consecutive tool uses in the same agent run are summarized as `Used N tools <latest tool preview>`.
8
9
  - Minimizes vertical space so long agent runs do not scroll away as quickly.
9
10
 
10
11
  ## Install from GitHub
@@ -50,4 +51,4 @@ Set these in `~/.pi/agent/settings.json`, or use `/settings` in pi.
50
51
 
51
52
  This extension overrides the built-in tool renderers for `bash`, `read`, `write`, `edit`, `grep`, `find`, and `ls`. It delegates execution to pi's original built-in tools; only display is changed.
52
53
 
53
- The consecutive-thinking collapse uses pi's current internal assistant-message renderer. If pi changes that internal path in a future release, the extension falls back to the normal hidden-thinking label behavior.
54
+ The consecutive-thinking collapse and compact self-rendered tool rows use pi's current internal TUI components. If pi changes those internal paths in a future release, the extension falls back to the normal hidden-thinking/tool rendering behavior.
@@ -28,6 +28,9 @@ type ToolInfo = {
28
28
  const BUILT_INS: BuiltInName[] = ["bash", "read", "write", "edit", "grep", "find", "ls"];
29
29
  const LABEL = "Thinking...";
30
30
 
31
+ let currentTools: ToolInfo[] = [];
32
+ let toolsById = new Map<string, ToolInfo>();
33
+
31
34
  const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
32
35
 
33
36
  function createBuiltInTools(cwd: string) {
@@ -119,7 +122,61 @@ function resultPreview(result: any): string {
119
122
  return `${lines.length} lines`;
120
123
  }
121
124
 
122
- async function patchAssistantThinkingRenderer() {
125
+ function toolCallName(block: any): string {
126
+ return block?.name ?? block?.toolName ?? block?.tool_name ?? block?.tool?.name ?? "tool";
127
+ }
128
+
129
+ function toolCallArgs(block: any): any {
130
+ return block?.args ?? block?.input ?? block?.arguments ?? block?.tool?.args ?? {};
131
+ }
132
+
133
+ function resetToolRun() {
134
+ currentTools = [];
135
+ toolsById = new Map();
136
+ }
137
+
138
+ function upsertToolInfo(id: string, name: string, args: any, invalidate?: () => void): ToolInfo {
139
+ let info = toolsById.get(id);
140
+ if (!info) {
141
+ info = { id, name, args, preview: previewFor(name, args) };
142
+ toolsById.set(id, info);
143
+ if (!currentTools.some((tool) => tool.id === id)) currentTools.push(info);
144
+ }
145
+ info.name = name;
146
+ info.args = args;
147
+ info.preview = previewFor(name, args);
148
+ if (invalidate) info.invalidate = invalidate;
149
+ return info;
150
+ }
151
+
152
+ function compactToolLine(toolCallId: string, name: string, args: any, theme: any, invalidate?: () => void, result?: any): string {
153
+ const info = upsertToolInfo(toolCallId, name, args, invalidate);
154
+ const suffix = resultPreview(result);
155
+ if (suffix) info.result = suffix;
156
+
157
+ if (currentTools[currentTools.length - 1]?.id !== toolCallId) return "";
158
+
159
+ const details = info.result ? `${info.preview} {${oneLine(info.result)}}` : info.preview;
160
+ if (currentTools.length === 1) return theme.fg("muted", limitPlain(details));
161
+ const prefix = `Used ${currentTools.length} tools `;
162
+ return theme.fg("toolTitle", prefix) + theme.fg("muted", limitPlain(details, Math.max(20, (process.stdout.columns || 100) - prefix.length - 6)));
163
+ }
164
+
165
+ function nextStepSummary(content: any[], fromIndex: number): string {
166
+ const tools = content.slice(fromIndex + 1).filter((c: any) => c?.type === "toolCall");
167
+ if (tools.length === 0) return " Next, I’ll continue from this reasoning and respond with the result. I’ll keep the visible output brief.";
168
+
169
+ const first = tools[0];
170
+ const preview = limitPlain(previewFor(toolCallName(first), toolCallArgs(first)), 90);
171
+ if (tools.length === 1) {
172
+ return ` Next, I’ll use ${preview}. I’ll inspect the result and decide the next visible step from there.`;
173
+ }
174
+ const last = tools[tools.length - 1];
175
+ const lastPreview = limitPlain(previewFor(toolCallName(last), toolCallArgs(last)), 70);
176
+ return ` Next, I’ll run ${tools.length} tool calls, starting with ${preview}. I’ll use the latest result to continue, ending this batch around ${lastPreview}.`;
177
+ }
178
+
179
+ async function patchInternalRenderers() {
123
180
  try {
124
181
  const require = createRequire(import.meta.url);
125
182
  const piMain = require.resolve("@earendil-works/pi-coding-agent");
@@ -127,7 +184,48 @@ async function patchAssistantThinkingRenderer() {
127
184
  const assistantModule = await import(
128
185
  pathToFileURL(join(distDir, "modes/interactive/components/assistant-message.js")).href
129
186
  );
187
+ const toolExecutionModule = await import(
188
+ pathToFileURL(join(distDir, "modes/interactive/components/tool-execution.js")).href
189
+ );
130
190
  const themeModule = await import(pathToFileURL(join(distDir, "modes/interactive/theme/theme.js")).href);
191
+
192
+ const ToolExecutionComponent = toolExecutionModule.ToolExecutionComponent;
193
+ if (ToolExecutionComponent?.prototype && !ToolExecutionComponent.prototype.__compactTranscriptPatched) {
194
+ const originalRender = ToolExecutionComponent.prototype.render;
195
+ ToolExecutionComponent.prototype.updateDisplay = function () {
196
+ this.__compactTranscriptForceSelf = true;
197
+ this.hideComponent = false;
198
+ this.selfRenderContainer.clear();
199
+ this.imageComponents = [];
200
+ this.imageSpacers = [];
201
+
202
+ const line = compactToolLine(
203
+ this.toolCallId,
204
+ this.toolName,
205
+ this.args,
206
+ themeModule.theme,
207
+ () => {
208
+ this.invalidate();
209
+ this.ui?.requestRender?.();
210
+ },
211
+ this.result,
212
+ );
213
+
214
+ if (!line) {
215
+ this.hideComponent = true;
216
+ return;
217
+ }
218
+
219
+ this.selfRenderContainer.addChild(new Text(line, 0, 0));
220
+ };
221
+ ToolExecutionComponent.prototype.render = function (width: number) {
222
+ if (this.hideComponent) return [];
223
+ if (this.__compactTranscriptForceSelf) return this.selfRenderContainer.render(width);
224
+ return originalRender.call(this, width);
225
+ };
226
+ ToolExecutionComponent.prototype.__compactTranscriptPatched = true;
227
+ }
228
+
131
229
  const Component = assistantModule.AssistantMessageComponent;
132
230
  if (!Component?.prototype || Component.prototype.__compactTranscriptPatched) return;
133
231
  const original = Component.prototype.updateContent;
@@ -161,8 +259,9 @@ async function patchAssistantThinkingRenderer() {
161
259
  i += count - 1;
162
260
 
163
261
  const label = count > 1 ? `${LABEL} (${count}x)` : LABEL;
262
+ const summary = nextStepSummary(message.content, i);
164
263
  this.contentContainer.addChild(
165
- new Text(themeModule.theme.italic(themeModule.theme.fg("thinkingText", label)), this.outputPad, 0),
264
+ new Text(themeModule.theme.italic(themeModule.theme.fg("thinkingText", limitPlain(`${label}${summary}`))), this.outputPad, 0),
166
265
  );
167
266
 
168
267
  const hasVisibleAfter = message.content
@@ -180,9 +279,7 @@ async function patchAssistantThinkingRenderer() {
180
279
  }
181
280
 
182
281
  export default async function (pi: ExtensionAPI) {
183
- await patchAssistantThinkingRenderer();
184
- let current: ToolInfo[] = [];
185
- let byId = new Map<string, ToolInfo>();
282
+ await patchInternalRenderers();
186
283
  let consecutiveThinking = 0;
187
284
 
188
285
  function thinkingLabel() {
@@ -190,8 +287,7 @@ export default async function (pi: ExtensionAPI) {
190
287
  }
191
288
 
192
289
  function resetTools() {
193
- current = [];
194
- byId = new Map();
290
+ resetToolRun();
195
291
  }
196
292
 
197
293
  function applyThinkingLabel(ctx: ExtensionContext) {
@@ -203,8 +299,11 @@ export default async function (pi: ExtensionAPI) {
203
299
  ctx.ui.setWorkingMessage(LABEL);
204
300
  });
205
301
 
206
- pi.on("turn_start", (_event, ctx) => {
302
+ pi.on("agent_start", () => {
207
303
  resetTools();
304
+ });
305
+
306
+ pi.on("turn_start", (_event, ctx) => {
208
307
  consecutiveThinking = 0;
209
308
  applyThinkingLabel(ctx);
210
309
  });
@@ -219,49 +318,24 @@ export default async function (pi: ExtensionAPI) {
219
318
  if (type && !type.startsWith("thinking_")) {
220
319
  consecutiveThinking = 0;
221
320
  applyThinkingLabel(ctx);
321
+ if (type === "text_delta" || type === "text_start") resetTools();
222
322
  }
223
323
  });
224
324
 
225
325
  pi.on("tool_execution_start", (event: any) => {
226
- const previous = current[current.length - 1];
227
- const info: ToolInfo = {
228
- id: event.toolCallId,
229
- name: event.toolName,
230
- args: event.args,
231
- preview: previewFor(event.toolName, event.args),
232
- };
233
- current.push(info);
234
- byId.set(info.id, info);
326
+ const previous = currentTools[currentTools.length - 1];
327
+ upsertToolInfo(event.toolCallId, event.toolName, event.args);
235
328
  previous?.invalidate?.();
236
329
  });
237
330
 
238
331
  pi.on("tool_execution_end", (event: any) => {
239
- const info = byId.get(event.toolCallId);
332
+ const info = toolsById.get(event.toolCallId);
240
333
  if (!info) return;
241
334
  const suffix = resultPreview(event.result);
242
335
  if (suffix) info.result = suffix;
243
336
  info.invalidate?.();
244
337
  });
245
338
 
246
- function compactLine(toolCallId: string, name: string, args: any, theme: any, context: any): string {
247
- let info = byId.get(toolCallId);
248
- if (!info) {
249
- info = { id: toolCallId, name, args, preview: previewFor(name, args) };
250
- byId.set(toolCallId, info);
251
- if (!current.some((t) => t.id === toolCallId)) current.push(info);
252
- }
253
- info.invalidate = context.invalidate;
254
- info.args = args;
255
- info.preview = previewFor(name, args);
256
-
257
- if (current[current.length - 1]?.id !== toolCallId) return "";
258
-
259
- const details = info.result ? `${info.preview} {${oneLine(info.result)}}` : info.preview;
260
- if (current.length === 1) return theme.fg("muted", limitPlain(details));
261
- const prefix = `Used ${current.length} tools `;
262
- return theme.fg("toolTitle", prefix) + theme.fg("muted", limitPlain(details, Math.max(20, (process.stdout.columns || 100) - prefix.length - 6)));
263
- }
264
-
265
339
  for (const name of BUILT_INS) {
266
340
  const base = getBuiltInTools(process.cwd())[name] as any;
267
341
  pi.registerTool({
@@ -275,7 +349,7 @@ export default async function (pi: ExtensionAPI) {
275
349
  },
276
350
  renderCall(args: any, theme: any, context: any) {
277
351
  const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
278
- text.setText(compactLine(context.toolCallId, name, args, theme, context));
352
+ text.setText(compactToolLine(context.toolCallId, name, args, theme, context.invalidate));
279
353
  return text;
280
354
  },
281
355
  renderResult(_result: any, _options: any, _theme: any, _context: any) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-compact-transcript",
3
- "version": "0.1.0",
4
- "description": "A compact transcript extension for pi: collapsed thinking and one-line tool previews.",
3
+ "version": "0.2.1",
4
+ "description": "A compact transcript extension for pi: collapsed thinking summaries and one-line tool previews.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Alan Hagedorn <avhagedorn@gmail.com>",