pi-compact-tools 0.16.0 → 0.16.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
@@ -93,12 +93,12 @@ Optional, in `~/.pi/agent/compact-tools.json` or `<project>/.pi/compact-tools.js
93
93
  Completed `codemode` rows in **Compact, Claude, and Codex** list the outcomes of the tools the script actually called, beside the duration and output-line count:
94
94
 
95
95
  ```text
96
- Done in 1.234s (42 lines) · ✓ read(3) ✗ bash(1) edit(3) [+10 -3|✗|+5 -7]
96
+ Done in 1.234s (42 lines) · read 3 · bash 1 · edit 3 2
97
97
  ```
98
98
 
99
- Most calls are grouped by tool and outcome, in first-call order. A script that catches a failed call can finish successfully while still showing `✗ bash(1)`. Edits instead use one ordered list with the total attempt count: each successful call shows its actual diff without a checkmark, each failed call shows `✗`, and an unfinished call shows `?`. A successful edit without a saved diff shows `ok`; it does not hide other calls’ known changes. Diff entries use a dim `|` separator with no surrounding whitespace; added and removed counts keep their diff colors. Collapsed rows show the first three edit attempts and `… +N more`; omitted failures or unfinished calls are counted beside that marker so the preview cannot hide them. Expanded rows show every recorded edit attempt. Incomplete records show `(partial)`. Long summaries wrap to the pane width. Previews and author-provided expanded views remain available. Claude and Codex place the `Done` / `Failed` summary after the output, without another branch marker and aligned with the output continuation rows. The disclosure stays last: Claude’s `… +N lines (ctrl+o to expand)` or Codex’s `+ Show details` / `− Show less`. Completion rows do not consume Claude’s output-preview limit.
99
+ Each tool appears once with plain colored counts and no count parentheses or pipe: `read 3 · bash 1 · edit 3 2`. Single-outcome calls show one green success count, red failure count, or yellow unfinished-only count. Mixed success/failure calls show the green success count followed by the red failure count, separated only by a space; zero-count placeholders are omitted. Unfinished calls alongside settled calls add a separate yellow note such as `edit 3 2 1 unfinished`. Summary names retain their theme title hue at 75% brightness without bold, while outcome numbers keep their hues at 80% brightness. The main tool title and code preview remain unchanged. `Done in … (N lines)` uses a neutral tone one step brighter than the frame; spaces and `·` separators retain the dim frame chrome. There are no circles, leading bars, underlines, patch totals, or per-call diff lists. Non-edit groups retain first-occurrence order, and edit always appears last. Incomplete records show `(partial)`. Long summaries wrap to the pane width. Previews and author-provided expanded views remain available; expansion does not add patch details. Claude and Codex place the indented completion summary after output and keep disclosure last. Completion rows do not consume Claude’s output-preview limit.
100
100
 
101
- The summary uses the host’s nested-call events and saved records when available, without changing what the model sees. Exact per-call edit diffs survive an in-process `/reload`. After restarting or resuming, Pi’s saved nested records restore call counts, but do not contain edit result diffs, so edits show per-call statuses such as `edit(3) [ok|✗|ok]` rather than invented changes. `/tree` navigation restores the selected branch’s records. Hosts without nested-call data keep the previous row rendering.
101
+ The summary uses the host’s nested-call events and saved records without changing what the model sees. It retains only tool names and statuses, never arguments, results, or edit diff totals. An in-process `/reload`, resumed sessions, and `/tree` navigation preserve or restore call counts and outcomes. Hosts without nested-call data keep the previous row rendering.
102
102
 
103
103
  ## UI-only behavior
104
104
 
@@ -141,7 +141,7 @@ export function paletteOf(theme: Theme): object {
141
141
 
142
142
  /** Every token the derived colors are made from. */
143
143
  const PALETTE_TOKENS: readonly ThemeColor[] = [
144
- "text", "dim", "muted", "toolDiffAdded", "toolDiffRemoved",
144
+ "text", "dim", "muted", "toolTitle", "success", "error", "warning", "toolDiffAdded", "toolDiffRemoved",
145
145
  "thinkingOff", "thinkingMinimal", "thinkingLow", "thinkingMedium", "thinkingHigh", "thinkingXhigh",
146
146
  ];
147
147
  /** One object per palette seen; a session switches among a handful of themes at most. */
@@ -1,15 +1,14 @@
1
- import type { AgentToolResult, Theme } from "@earendil-works/pi-coding-agent";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { stripTerminalSequences } from "@earendil-works/pi-tui";
3
- import { countEditChanges } from "./compact-tools-invocation.ts";
3
+ import { chromePainter, nestedCountPainter, nestedTitlePainter } from "./compact-tools-palette.ts";
4
4
 
5
5
  const SUMMARY_KEY = Symbol.for("pi-compact-tools.nested-summaries.v1");
6
6
  const MAX_PARENTS = 2_000;
7
7
  const MAX_CALLS = 256;
8
- const EDIT_PREVIEW_CALLS = 3;
9
8
  const MAX_NAME_LENGTH = 256;
10
9
  const MAX_ID_LENGTH = 1_024;
11
10
  type Status = "ok" | "error" | "unfinished";
12
- type Call = { name: string; status: Status; changes?: { added: number; removed: number } };
11
+ type Call = { name: string; status: Status };
13
12
  type Summary = { calls: Map<string, Call>; partial: boolean; finished: boolean; live: boolean; revision: object };
14
13
  type Shared = typeof globalThis & { [SUMMARY_KEY]?: Map<string, Summary> };
15
14
  type NestedEvent = { toolCallId: string; toolName: string; parentToolCallId?: string; isError?: boolean; result?: unknown };
@@ -78,12 +77,8 @@ export class NestedCallSummaries {
78
77
  return summary.finished;
79
78
  }
80
79
  const status = ended ? event.isError ? "error" : "ok" : "unfinished";
81
- const result = event.result;
82
- const changes = status === "ok" && event.toolName === "edit" && result && typeof result === "object"
83
- ? countEditChanges(result as AgentToolResult<unknown>) : undefined;
84
- if (previous?.name === event.toolName && previous.status === status
85
- && previous.changes?.added === changes?.added && previous.changes?.removed === changes?.removed) return false;
86
- summary.calls.set(event.toolCallId, { name: event.toolName, status, ...(changes ? { changes } : {}) });
80
+ if (previous?.name === event.toolName && previous.status === status) return false;
81
+ summary.calls.set(event.toolCallId, { name: event.toolName, status });
87
82
  summary.live = true;
88
83
  summary.revision = {};
89
84
  return summary.finished;
@@ -95,7 +90,7 @@ export class NestedCallSummaries {
95
90
  const value = message as { role?: string; toolName?: string; toolCallId?: string; nestedCalls?: { calls?: unknown; complete?: unknown } };
96
91
  if (value.role !== "toolResult" || value.toolName !== "codemode" || typeof value.toolCallId !== "string"
97
92
  || value.toolCallId.length > MAX_ID_LENGTH || !Array.isArray(value.nestedCalls?.calls)) return false;
98
- // A bounded persisted snapshot must not overwrite exact live edit totals or drop observed calls.
93
+ // A bounded persisted snapshot must not drop observed live calls.
99
94
  const existing = this.summaries.get(value.toolCallId);
100
95
  if (existing?.live && existing.calls.size > 0) return false;
101
96
  const summary: Summary = { calls: new Map(), partial: value.nestedCalls.complete !== true, finished: true, live: false, revision: {} };
@@ -122,48 +117,45 @@ export class NestedCallSummaries {
122
117
  }
123
118
  }
124
119
 
125
- /** Most tools group by outcome; edits keep every call's status and diff in start order. */
126
- format(id: string, theme: Theme, expanded = false): string | undefined {
120
+ /** Other tool groups keep first-occurrence order; edit counts always come last. */
121
+ format(id: string, theme: Theme, _expanded = false): string | undefined {
127
122
  const summary = this.summaries.get(id);
128
123
  if (!summary || (summary.calls.size === 0 && !summary.partial)) return undefined;
129
- const groups = new Map<string, { name: string; status: Status; calls: Call[] }>();
124
+ type Group = { name: string; calls: Call[] };
125
+ const groups: Group[] = [];
126
+ const byTool = new Map<string, Group>();
130
127
  for (const call of summary.calls.values()) {
131
- const key = JSON.stringify([call.name, call.name === "edit" ? "all" : call.status]);
132
- let group = groups.get(key);
133
- if (!group) groups.set(key, group = { name: call.name, status: call.status, calls: [] });
128
+ const key = call.name;
129
+ let group = byTool.get(key);
130
+ if (!group) {
131
+ group = { name: call.name, calls: [] };
132
+ byTool.set(key, group);
133
+ groups.push(group);
134
+ }
134
135
  group.calls.push(call);
135
136
  }
136
- const parts: string[] = [];
137
- for (const group of groups.values()) {
138
- const color = group.status === "ok" ? "success" : group.status === "error" ? "error" : "warning";
139
- const glyph = group.status === "ok" ? "✓" : group.status === "error" ? "✗" : "?";
137
+ const editIndex = groups.findIndex((group) => group.name === "edit");
138
+ if (editIndex >= 0) groups.push(...groups.splice(editIndex, 1));
139
+ const title = nestedTitlePainter(theme);
140
+ const chrome = chromePainter(theme);
141
+ const success = nestedCountPainter(theme, "success");
142
+ const error = nestedCountPainter(theme, "error");
143
+ const warning = nestedCountPainter(theme, "warning");
144
+ const parts = groups.map((group) => {
140
145
  const name = stripTerminalSequences(group.name).replace(/[\x00-\x20\x7f-\x9f]/gu, " ").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/gu, "").trim() || "tool";
141
- if (group.name === "edit") {
142
- // Include failures and unfinished calls in the same ordered list. A
143
- // missing successful diff loses only its numbers, not other calls' diffs.
144
- const shown = expanded ? group.calls : group.calls.slice(0, EDIT_PREVIEW_CALLS);
145
- const entries = shown.map((call) => {
146
- if (call.status === "error") return theme.fg("error", "✗");
147
- if (call.status === "unfinished") return theme.fg("warning", "?");
148
- return call.changes
149
- ? theme.fg("toolDiffAdded", `+${call.changes.added}`) + " " + theme.fg("toolDiffRemoved", `-${call.changes.removed}`)
150
- : theme.fg("dim", "ok");
151
- });
152
- const hidden = group.calls.slice(shown.length);
153
- if (hidden.length > 0) {
154
- // The preview must not hide that an omitted edit failed or is unfinished.
155
- const failed = hidden.filter((call) => call.status === "error").length;
156
- const unfinished = hidden.filter((call) => call.status === "unfinished").length;
157
- let more = theme.fg("dim", `… +${hidden.length} more`);
158
- const statuses = [failed ? theme.fg("error", `${failed} failed`) : "", unfinished ? theme.fg("warning", `${unfinished} unfinished`) : ""].filter(Boolean);
159
- if (statuses.length > 0) more += " (" + statuses.join(", ") + ")";
160
- entries.push(more);
161
- }
162
- parts.push(`${name}(${group.calls.length}) [` + entries.join(theme.fg("dim", "|")) + "]");
163
- } else parts.push(theme.fg(color, glyph) + ` ${name}(${group.calls.length})`);
164
- }
165
- if (summary.partial || [...summary.calls.values()].some((call) => call.status === "unfinished")) parts.push(theme.fg("warning", "(partial)"));
166
- return parts.join(" ");
146
+
147
+ const successes = group.calls.filter((call) => call.status === "ok").length;
148
+ const failures = group.calls.filter((call) => call.status === "error").length;
149
+ const unfinished = group.calls.filter((call) => call.status === "unfinished").length;
150
+ const counts = [successes ? success(String(successes)) : "",
151
+ failures ? error(String(failures)) : ""].filter(Boolean);
152
+ const settled = counts.length > 0;
153
+ if (!settled) counts.push(warning(String(unfinished)));
154
+ return title(name) + chrome(" ") + counts.join(chrome(" "))
155
+ + (settled && unfinished ? chrome(" ") + warning(`${unfinished} unfinished`) : "");
156
+ });
157
+ const partial = summary.partial || [...summary.calls.values()].some((call) => call.status === "unfinished");
158
+ return parts.join(chrome(" · ")) + (partial ? (parts.length ? " " : "") + warning("(partial)") : "");
167
159
  }
168
160
 
169
161
  private trim(): void {
@@ -38,10 +38,11 @@ type ContrastBand = { color: ThemeColor; keepHue: number; minimum: number; maxim
38
38
  type ContrastRange = { minimum: number; maximum: number };
39
39
 
40
40
  /**
41
- * One tone for the whole frame — rails, the result connector, and the status line it leads
42
- * to — so the frame reads as a single quiet object rather than competing with the output.
41
+ * A quiet frame tone for rails, connectors, and separators. Completion metadata
42
+ * sits one small brightness step above it so duration and line counts remain legible.
43
43
  */
44
44
  const CHROME: ContrastBand = { color: "dim", keepHue: 0.35, minimum: 0.22, maximum: 0.28 };
45
+ const COMPLETION: ContrastBand = { ...CHROME, minimum: 0.26, maximum: 0.32 };
45
46
  /** The indicator breathes either side of the chrome tone, dipping below it and peaking above. */
46
47
  const PULSE_LOW: ContrastBand = { color: "dim", keepHue: 0.4, minimum: 0.18, maximum: 0.3 };
47
48
  const PULSE_HIGH: ContrastBand = { color: "muted", keepHue: 0.4, minimum: 0.4, maximum: 0.6 };
@@ -105,7 +106,34 @@ export function chromePainter(theme: Theme): (text: string) => string {
105
106
  });
106
107
  }
107
108
 
108
- /** Paint one rail, connector, or status line in the chrome tone. */
109
+ /** Duration and line-count metadata is slightly brighter than rails and separators. */
110
+ export function completionPainter(theme: Theme): (text: string) => string {
111
+ return perPalette(theme, "completion-metadata", () => {
112
+ const color = bandedRgb(theme, COMPLETION);
113
+ return color ? rgbPainter(theme, color) : (text: string) => theme.fg("dim", text);
114
+ });
115
+ }
116
+
117
+ /** Nested names keep the title hue, slightly darker than ordinary titles, without bold. */
118
+ export function nestedTitlePainter(theme: Theme): (text: string) => string {
119
+ return perPalette(theme, "nested-title", () => {
120
+ const title = themeColorRgb(theme, "toolTitle");
121
+ const paint = title ? rgbPainter(theme, interpolateRgb(title, { r: 0, g: 0, b: 0 }, 0.25))
122
+ : (text: string) => theme.fg("toolTitle", text);
123
+ return paint;
124
+ });
125
+ }
126
+
127
+ /** Quiet summary numbers retain outcome hues at 80% brightness. */
128
+ export function nestedCountPainter(theme: Theme, color: "success" | "error" | "warning"): (text: string) => string {
129
+ return perPalette(theme, `nested-count:${color}`, () => {
130
+ const rgb = themeColorRgb(theme, color);
131
+ return rgb ? rgbPainter(theme, interpolateRgb(rgb, { r: 0, g: 0, b: 0 }, 0.20))
132
+ : (text: string) => theme.fg(color, text);
133
+ });
134
+ }
135
+
136
+ /** Paint frame rails, connectors, separators, or running status in the quiet chrome tone. */
109
137
  export function paintChrome(theme: Theme, text: string): string {
110
138
  return chromePainter(theme)(text);
111
139
  }
@@ -52,7 +52,7 @@ import { CODEX_BULLET, CODEX_INDENT } from "./compact-tools-codex.ts";
52
52
  import { NestedCallSummaries } from "./compact-tools-nested.ts";
53
53
  import { renderCodexCall, renderCodexResult } from "./compact-tools-codex-rows.ts";
54
54
  import { ToolGroupController } from "./compact-tools-grouping.ts";
55
- import { chromePainter, paintIndicator } from "./compact-tools-palette.ts";
55
+ import { chromePainter, completionPainter, paintIndicator } from "./compact-tools-palette.ts";
56
56
  import { ProgressController } from "./compact-tools-progress.ts";
57
57
  import { showReleaseNotice } from "./compact-tools-release.ts";
58
58
  import { ToolRuntime } from "./compact-tools-runtime.ts";
@@ -190,9 +190,10 @@ function renderControls(
190
190
  ? (duration ?? "Running")
191
191
  : `${isError ? "Failed" : "Done"}${duration ? ` in ${duration}` : ""}`;
192
192
  // A failed row already reads as an error through its output color and Pi's row
193
- // background, so the status word stays chrome rather than repeating that signal.
193
+ // background, so completion metadata stays neutral rather than repeating that signal.
194
194
  const chrome = chromePainter(theme);
195
- let details = chrome(status);
195
+ const metadata = running ? chrome : completionPainter(theme);
196
+ let details = metadata(status);
196
197
  if (failureReason && isError && !running) details += chrome(" · ") + theme.fg("error", failureReason);
197
198
  // An edit reports what it changed the way a diff does: lines added and removed.
198
199
  else if (changes && !running && !isError) {
@@ -200,7 +201,7 @@ function renderControls(
200
201
  + theme.fg("toolDiffRemoved", `-${changes.removed}`) + chrome(")");
201
202
  }
202
203
  // A failed call produced no result worth counting; "(0 lines)" would only mislead.
203
- else if (state.resultLineSummary && !running && !isError) details += chrome(` (${state.resultLineSummary})`);
204
+ else if (state.resultLineSummary && !running && !isError) details += metadata(` (${state.resultLineSummary})`);
204
205
  if (nestedSummary && !running) details += chrome(" · ") + nestedSummary;
205
206
  return nestedSummary ? prefixedSentence(details, chrome(prefix), indent, true) : prefixedText(details, chrome(prefix), indent);
206
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-compact-tools",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Tool rows for Pi in three styles: Claude, Codex, and Compact. Switch with /compact-tools; includes silent mode and a GitHub Dark theme.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,18 @@
1
1
  {
2
+ "0.16.1": {
3
+ "New": [
4
+ "Minimal codemode summaries show each tool once: read 3 · bash 1 · edit 3 2; non-edit tools keep first-call order and edit always comes last",
5
+ "Plain counts use green for success, red for failure, and yellow for unfinished calls, without zero placeholders, count parentheses, status icons, pipes, or edit diff lists",
6
+ "Summary names retain their theme title hue at 75% brightness without bold; outcome counts retain their hues at 80% brightness",
7
+ "Completion duration and output-line metadata is one step brighter; rails, connectors, separators, main tool titles, and code previews are unchanged"
8
+ ],
9
+ "Fix": [
10
+ "Interleaved nested calls aggregate into one group per tool, with success counts before failure counts",
11
+ "Reloads, resumed sessions, and /tree navigation preserve or restore nested counts and outcomes without retaining tool arguments, results, or edit diff bookkeeping",
12
+ "Unfinished calls and incomplete records remain visible, including when mixed with settled outcomes"
13
+ ],
14
+ "Deprecated": []
15
+ },
2
16
  "0.16.0": {
3
17
  "New": [
4
18
  "Completed codemode rows summarize nested tool successes and failures beside their duration and output-line count without changing model input",