pi-compact-tools 0.9.3 → 0.10.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.
@@ -29,8 +29,10 @@ import {
29
29
  } from "./compact-tools-core.ts";
30
30
  import { colorizeRgb, interpolateRgb } from "./compact-tools-color.ts";
31
31
  import { loadConfig } from "./compact-tools-config.ts";
32
+ import { installToolRowPatch, setRowResolver, type RowRenderers, type ToolRow } from "./compact-tools-custom.ts";
32
33
  import {
33
34
  formatResultLineSummary,
35
+ countEditChanges,
34
36
  getArgumentDetails,
35
37
  getCallDetails,
36
38
  getEditDiff,
@@ -39,12 +41,15 @@ import {
39
41
  getTextResult,
40
42
  isReadTextResult,
41
43
  splitReadFooter,
44
+ summarizeCustomArguments,
45
+ summarizeFailure,
42
46
  summarizeShellCommand,
43
47
  } from "./compact-tools-invocation.ts";
44
48
  import {
45
49
  CachedContainer,
46
50
  limitComponentLines,
47
51
  prefixedText,
52
+ railComponent,
48
53
  renderArguments,
49
54
  renderCodeDiff,
50
55
  renderCodeView,
@@ -54,15 +59,19 @@ import {
54
59
  } from "./compact-tools-layout.ts";
55
60
  import { chromePainter, indicatorPulse } from "./compact-tools-palette.ts";
56
61
  import { ProgressController } from "./compact-tools-progress.ts";
62
+ import { showReleaseNotice } from "./compact-tools-release.ts";
57
63
  import { ToolRuntime } from "./compact-tools-runtime.ts";
64
+ import { SilentModeController } from "./compact-tools-silent.ts";
58
65
  import { ThinkingCycleController } from "./compact-tools-thinking.ts";
59
- import type {
60
- BuiltInDefinition,
61
- CompactToolName,
62
- RenderContext,
63
- RowState,
64
- ShellToolName,
65
- ToolArgs,
66
+ import { ViewportKeeper } from "./compact-tools-viewport.ts";
67
+ import {
68
+ SUPPORTED_TOOL_SET,
69
+ type BuiltInDefinition,
70
+ type CompactToolName,
71
+ type RenderContext,
72
+ type RowState,
73
+ type ShellToolName,
74
+ type ToolArgs,
66
75
  } from "./compact-tools-types.ts";
67
76
 
68
77
  /** Context lines kept around each change while an edit result is collapsed. */
@@ -134,6 +143,8 @@ function renderControls(
134
143
  running: boolean,
135
144
  isError: boolean,
136
145
  lineSummary?: string,
146
+ failureReason?: string,
147
+ changes?: { added: number; removed: number },
137
148
  ): Component {
138
149
  const duration = running ? undefined : formatDuration(state);
139
150
  const status = running
@@ -143,8 +154,24 @@ function renderControls(
143
154
  // background, so the status word stays chrome rather than repeating that signal.
144
155
  const chrome = chromePainter(theme);
145
156
  let details = chrome(status);
146
- if (lineSummary && !running) details += chrome(` (${lineSummary})`);
147
- return prefixedText(details, chrome(" └─ "), " ");
157
+ if (failureReason && isError && !running) details += chrome(" · ") + theme.fg("error", failureReason);
158
+ // An edit reports what it changed the way a diff does: lines added and removed.
159
+ else if (changes && !running && !isError) {
160
+ details += chrome(" (") + theme.fg("toolDiffAdded", `+${changes.added}`) + chrome(" ")
161
+ + theme.fg("toolDiffRemoved", `-${changes.removed}`) + chrome(")");
162
+ }
163
+ // A failed call produced no result worth counting; "(0 lines)" would only mislead.
164
+ else if (lineSummary && !running && !isError) details += chrome(` (${lineSummary})`);
165
+ return prefixedText(details, chrome(" └ "), " ");
166
+ }
167
+
168
+ /**
169
+ * A failed row whose output is hidden gets its reason on the status line. When
170
+ * the output shows, expanded or as a preview, it already says why, and repeating
171
+ * it there only doubles the error.
172
+ */
173
+ function hiddenFailureReason(name: string, state: RowState, isError: boolean, output: string): string | undefined {
174
+ return isError && !state.expanded && !state.preview ? summarizeFailure(name, output) : undefined;
148
175
  }
149
176
 
150
177
  function renderCallTitle(name: string, theme: Theme, ctx: RenderContext, state: RowState): string {
@@ -234,7 +261,8 @@ function renderFileResult(
234
261
  state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
235
262
  state.resultLineSummaryComputed = true;
236
263
  }
237
- container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary));
264
+ container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary,
265
+ hiddenFailureReason(name, state, ctx.isError, output), name === "edit" ? countEditChanges(result) : undefined));
238
266
  rememberResult(state, result, options, ctx);
239
267
  return container;
240
268
  }
@@ -265,13 +293,14 @@ function renderShellResult(
265
293
  runtime.syncExpansion(state, ctx.expanded, name);
266
294
  if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
267
295
  const output = getTextResult(result);
296
+ const failureReason = hiddenFailureReason(name, state, ctx.isError, output);
268
297
  runtime.setResultAvailable(state, name, output.length > 0);
269
298
  if (!options.isPartial && !state.resultLineSummaryComputed) {
270
299
  state.resultLineSummary = formatResultLineSummary(name, ctx.args, result, output);
271
300
  state.resultLineSummaryComputed = true;
272
301
  }
273
302
  if (!state.expanded && !state.preview) {
274
- const controls = renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary);
303
+ const controls = renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary, failureReason);
275
304
  rememberResult(state, result, options, ctx);
276
305
  return controls;
277
306
  }
@@ -280,11 +309,107 @@ function renderShellResult(
280
309
  if (component) {
281
310
  container.addChild(state.expanded ? component : limitComponentLines(component, runtime.config.previewLines, theme));
282
311
  }
283
- container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary));
312
+ container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary, failureReason));
313
+ rememberResult(state, result, options, ctx);
314
+ return container;
315
+ }
316
+
317
+ function renderCustomCall(name: string, args: ToolArgs, theme: Theme, ctx: RenderContext): Component {
318
+ const state = runtime.syncRow(ctx, ctx.state.endedAt === undefined);
319
+ runtime.syncExpansion(state, ctx.expanded, name);
320
+ const summary = summarizeCustomArguments(args);
321
+ const container = new CachedContainer();
322
+ container.addChild(renderToolCall(
323
+ renderCallTitle(name, theme, ctx, state),
324
+ summary ? theme.fg("toolOutput", summary) : undefined,
325
+ theme,
326
+ ));
327
+ if (state.expanded && Object.keys(args).length > 0) container.addChild(renderArguments(args, theme));
328
+ return container;
329
+ }
330
+
331
+ type AuthorResultRenderer = (expanded: boolean) => Component | undefined;
332
+
333
+ function renderCustomBody(
334
+ output: string,
335
+ expanded: boolean,
336
+ theme: Theme,
337
+ isError: boolean,
338
+ renderAuthorResult: AuthorResultRenderer | undefined,
339
+ ): Component | undefined {
340
+ if (renderAuthorResult) {
341
+ try {
342
+ const component = renderAuthorResult(expanded);
343
+ if (component) return railComponent(component, theme);
344
+ } catch {
345
+ // A failing third-party renderer degrades to the plain text result.
346
+ }
347
+ }
348
+ return renderOutput(output, theme, isError);
349
+ }
350
+
351
+ function renderCustomResult(
352
+ name: string,
353
+ result: AgentToolResult<unknown>,
354
+ options: ToolRenderResultOptions,
355
+ theme: Theme,
356
+ ctx: RenderContext,
357
+ renderAuthorResult: AuthorResultRenderer | undefined,
358
+ ): Component {
359
+ const state = runtime.syncRow(ctx, options.isPartial, !options.isPartial);
360
+ runtime.syncExpansion(state, ctx.expanded, name);
361
+ if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
362
+ const output = getTextResult(result);
363
+ runtime.setResultAvailable(state, name, output.length > 0 || renderAuthorResult !== undefined);
364
+ if (!options.isPartial && !state.resultLineSummaryComputed) {
365
+ state.resultLineSummary = output ? formatResultLineSummary(name, ctx.args, result, output) : undefined;
366
+ state.resultLineSummaryComputed = true;
367
+ }
368
+ const container = new CachedContainer();
369
+ if (state.expanded || state.preview) {
370
+ const body = renderCustomBody(output, state.expanded === true, theme, ctx.isError, renderAuthorResult);
371
+ if (body) container.addChild(state.expanded ? body : limitComponentLines(body, runtime.config.previewLines, theme));
372
+ }
373
+ const failureReason = hiddenFailureReason(name, state, ctx.isError, output);
374
+ container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, state.resultLineSummary, failureReason));
284
375
  rememberResult(state, result, options, ctx);
285
376
  return container;
286
377
  }
287
378
 
379
+ /**
380
+ * Compact renderers for one row of a tool this extension did not register. The
381
+ * author's result renderer still draws the expanded body; the row chrome, status,
382
+ * and collapse behavior match the built-ins.
383
+ */
384
+ function createCustomRenderers(name: string, author: ToolDefinition<any, any, any> | undefined): RowRenderers {
385
+ // Pi hands both renderers the row's own state object, which belongs to the tool's
386
+ // author. Compact bookkeeping is kept here so neither side overwrites the other.
387
+ const state: RowState = {};
388
+ let authorResult: Component | undefined;
389
+ const own = (ctx: RenderContext<any>): RenderContext => ({ ...ctx, args: ctx.args ?? {}, state });
390
+ return {
391
+ renderCall: (args, theme, ctx) => renderCustomCall(name, (args ?? {}) as ToolArgs, theme, own(ctx)),
392
+ renderResult: (result, options, theme, ctx) => {
393
+ const authorRenderer = author?.renderResult;
394
+ const renderAuthorResult = authorRenderer
395
+ ? (expanded: boolean) => {
396
+ authorResult = authorRenderer(result, { ...options, expanded }, theme, { ...ctx, lastComponent: authorResult });
397
+ return authorResult;
398
+ }
399
+ : undefined;
400
+ return renderCustomResult(name, result, options, theme, own(ctx), renderAuthorResult);
401
+ },
402
+ };
403
+ }
404
+
405
+ function resolveCustomRow(row: ToolRow): RowRenderers | undefined {
406
+ const customTools = runtime.config.custom_tools;
407
+ // Built-ins are governed by `tools`: ones left out keep Pi's own renderer.
408
+ if (!customTools.enabled || SUPPORTED_TOOL_SET.has(row.toolName)) return undefined;
409
+ if (customTools.exclude.includes(row.toolName)) return undefined;
410
+ return createCustomRenderers(row.toolName, row.toolDefinition);
411
+ }
412
+
288
413
  const toolFactories: Record<CompactToolName, (cwd: string) => BuiltInDefinition> = {
289
414
  read: createReadToolDefinition,
290
415
  write: createWriteToolDefinition,
@@ -369,6 +494,11 @@ export default function compactTools(pi: ExtensionAPI): void {
369
494
 
370
495
  const thinkingCycle = new ThinkingCycleController(pi);
371
496
  const progress = new ProgressController(pi);
497
+ const viewport = new ViewportKeeper();
498
+ const silent = new SilentModeController(pi, showReleaseNotice);
499
+ const customRowsAvailable = installToolRowPatch();
500
+ setRowResolver(resolveCustomRow);
501
+ let customRowsWarned = false;
372
502
  // Register once while the extension runtime is being built. In particular, this
373
503
  // makes the overrides available before Pi restores the active tool set on /reload.
374
504
  configure(pi, process.cwd());
@@ -376,16 +506,30 @@ export default function compactTools(pi: ExtensionAPI): void {
376
506
  if (event.reason !== "reload") runtime.clearTimings();
377
507
  configure(pi, ctx.cwd, ctx.isProjectTrusted());
378
508
  if (ctx.mode === "tui") {
509
+ // First, so its input listener sees relayout keys before the thinking controller consumes them.
510
+ viewport.bind(ctx);
379
511
  progress.bind(ctx);
380
512
  thinkingCycle.bind(ctx);
513
+ silent.bind(ctx, runtime.config.mode, event.reason === "reload");
514
+ if (!silent.isEnabled()) showReleaseNotice(ctx);
515
+ if (runtime.config.custom_tools.enabled && !customRowsAvailable && !customRowsWarned) {
516
+ customRowsWarned = true;
517
+ ctx.ui.notify("Compact rendering for custom tools is unavailable in this version of Pi", "warning");
518
+ }
381
519
  } else {
520
+ viewport.dispose();
382
521
  progress.dispose();
383
522
  thinkingCycle.dispose();
523
+ silent.dispose();
384
524
  }
385
525
  });
526
+ pi.on("tool_execution_start", (event) => runtime.noteExecutionStart(event.toolCallId));
527
+ pi.on("tool_execution_end", (event) => runtime.noteExecutionEnd(event.toolCallId));
386
528
  pi.on("session_shutdown", (event) => {
387
529
  thinkingCycle.dispose();
388
530
  progress.dispose();
531
+ silent.dispose();
532
+ viewport.dispose();
389
533
  runtime.reset(event.reason !== "reload");
390
534
  });
391
535
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-compact-tools",
3
- "version": "0.9.3",
4
- "description": "Compact, expandable UI for pi's built-in tools, bundled with a polished GitHub Dark theme.",
3
+ "version": "0.10.1",
4
+ "description": "Compact, expandable UI for pi's built-in and custom tools, bundled with a polished GitHub Dark theme.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Dong Hyeong Lee",
@@ -26,6 +26,7 @@
26
26
  "examples",
27
27
  "scripts",
28
28
  "schemas",
29
+ "release-notes.json",
29
30
  "README.md",
30
31
  "LICENSE"
31
32
  ],
@@ -0,0 +1,11 @@
1
+ {
2
+ "0.10.1": [],
3
+ "0.10.0": [
4
+ "Silent mode shows only prompts and final answers (Ctrl+' or /silent)",
5
+ "Tools from other packages get the same compact rows as the built-ins",
6
+ "Edits show the lines they added and removed, in the diff colors",
7
+ "Thinking summaries are shown in full instead of cut off",
8
+ "Fullscreen scrolling stays in place when expanding rows or cycling thinking",
9
+ "Failed rows show a short reason while their output is hidden"
10
+ ]
11
+ }
@@ -8,6 +8,12 @@
8
8
  "$schema": {
9
9
  "type": "string"
10
10
  },
11
+ "mode": {
12
+ "description": "normal shows every tool row. silent leaves only your prompts and the final answers: tool rows, in-between assistant turns, thinking, error and abort notices, extension notices, and Pi's own status lines are hidden while a light animates beneath the prompt. Ctrl+' or /silent toggles it for the current session.",
13
+ "type": "string",
14
+ "enum": ["normal", "silent"],
15
+ "default": "normal"
16
+ },
11
17
  "tools": {
12
18
  "description": "Built-in tools that use compact rendering. Omitted tools keep Pi's default renderer.",
13
19
  "type": "array",
@@ -18,6 +24,32 @@
18
24
  },
19
25
  "default": ["read", "write", "edit", "bash"]
20
26
  },
27
+ "custom_tools": {
28
+ "description": "Compact rendering for tools registered by other extensions. true/false toggles it; the object form also sets whether rows start hidden and which tools keep their own renderer.",
29
+ "oneOf": [
30
+ { "type": "boolean" },
31
+ {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "enabled": { "type": "boolean", "default": true },
36
+ "auto_compact": {
37
+ "description": "True starts custom tool rows hidden; false starts with a bounded preview.",
38
+ "type": "boolean",
39
+ "default": true
40
+ },
41
+ "exclude": {
42
+ "description": "Tool names that keep their own renderer.",
43
+ "type": "array",
44
+ "uniqueItems": true,
45
+ "items": { "type": "string" },
46
+ "default": []
47
+ }
48
+ }
49
+ }
50
+ ],
51
+ "default": { "enabled": true, "auto_compact": true, "exclude": [] }
52
+ },
21
53
  "previewLines": {
22
54
  "description": "Maximum rendered lines shown initially when auto_compact is false. Explicit expansion always shows the complete result.",
23
55
  "type": "integer",
@@ -26,9 +58,9 @@
26
58
  "default": 10
27
59
  },
28
60
  "auto_compact": {
29
- "description": "Whether each compact-rendered tool starts hidden. False starts with a bounded preview; Pi's normal click and Ctrl+O flow can expand either state.",
61
+ "description": "Whether each compact-rendered tool starts hidden. False starts with a bounded preview; Pi's normal click and Ctrl+O flow can expand either state. Custom tools can be listed by name too, which overrides custom_tools.auto_compact for that tool.",
30
62
  "type": "object",
31
- "additionalProperties": false,
63
+ "additionalProperties": { "type": "boolean" },
32
64
  "properties": {
33
65
  "read": { "type": "boolean", "default": true },
34
66
  "write": { "type": "boolean", "default": true },
@@ -5,6 +5,7 @@ const resources = [
5
5
  "examples/compact-tools.json",
6
6
  "examples/compact-tools-2.json",
7
7
  "schemas/compact-tools.schema.json",
8
+ "release-notes.json",
8
9
  ];
9
10
 
10
11
  for (const path of resources) {
@@ -70,4 +71,12 @@ if (schema.properties?.previewLines?.default !== 10) {
70
71
  throw new Error("schema must expose the bounded previewLines setting");
71
72
  }
72
73
 
74
+ const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
75
+ const releaseNotes = JSON.parse(await readFile(new URL("../release-notes.json", import.meta.url), "utf8"));
76
+ // Every published version needs an entry. An empty list is a quiet release that shows no notice.
77
+ if (!Array.isArray(releaseNotes[manifest.version]) ||
78
+ releaseNotes[manifest.version].some((note) => typeof note !== "string" || !note.trim())) {
79
+ throw new Error(`release-notes.json must have an entry for v${manifest.version} before publishing; use [] for no notice`);
80
+ }
81
+
73
82
  console.log("Resource checks passed");