pi-compact-tools 0.15.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 +14 -2
- package/extensions/compact-tools-codex-rows.ts +4 -2
- package/extensions/compact-tools-color.ts +1 -1
- package/extensions/compact-tools-layout.ts +19 -9
- package/extensions/compact-tools-nested.ts +169 -0
- package/extensions/compact-tools-palette.ts +31 -3
- package/extensions/compact-tools-runtime.ts +7 -1
- package/extensions/compact-tools.ts +79 -17
- package/package.json +3 -3
- package/release-notes.json +32 -0
- package/themes/github-dark-pro.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ pi install npm:pi-compact-tools
|
|
|
12
12
|
|
|
13
13
|
Restart Pi or run `/reload`, then run `/compact-tools` to choose a style. To try it for one session without installing: `pi -e npm:pi-compact-tools`.
|
|
14
14
|
|
|
15
|
-
Requires Node.js 22.19+ and Pi 0.85.1+. **Pi 1.0
|
|
15
|
+
Requires Node.js 22.19+ and Pi 0.85.1+. **Pi 1.2.0 or later is recommended** for native renderer integration and compact HTML exports. It changes only how tool calls are drawn, not what they run or what the model sees.
|
|
16
16
|
|
|
17
17
|
## Styles
|
|
18
18
|
|
|
@@ -88,6 +88,18 @@ Optional, in `~/.pi/agent/compact-tools.json` or `<project>/.pi/compact-tools.js
|
|
|
88
88
|
- `auto_compact`: `true` starts a Compact row collapsed; `false` shows up to `previewLines` rows. Custom tool names work too.
|
|
89
89
|
- `custom_tools`: rows for tools from other packages. `exclude` keeps a tool's own look.
|
|
90
90
|
|
|
91
|
+
## Codemode summaries
|
|
92
|
+
|
|
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
|
+
|
|
95
|
+
```text
|
|
96
|
+
Done in 1.234s (42 lines) · read 3 · bash 1 · edit 3 2
|
|
97
|
+
```
|
|
98
|
+
|
|
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
|
+
|
|
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
|
+
|
|
91
103
|
## UI-only behavior
|
|
92
104
|
|
|
93
105
|
This package never registers or activates agent tools and does not add model-facing
|
|
@@ -118,7 +130,7 @@ npm run check
|
|
|
118
130
|
pi -e .
|
|
119
131
|
```
|
|
120
132
|
|
|
121
|
-
Development dependencies are pinned to Pi 1.
|
|
133
|
+
Development dependencies are pinned to Pi 1.2.0; the peer minimum remains Pi 0.85.1. CI checks Pi 0.85.1, 0.99.1, 1.0.0, 1.0.2, 1.1.0, and 1.2.0 on Node 22.19.0 and 24. Schema validation and deterministic test clocks are development-only dependencies, not runtime dependencies. Development scripts and tests are not included in the published package. `npm test` runs the rendering tests first and the resource/packaging checks separately so their subprocesses do not disturb animation tests.
|
|
122
134
|
|
|
123
135
|
For a local before/after performance comparison, run `npx tsx tests/benchmark.ts <clean-baseline-directory>` with the same host dependencies in both directories. Measurements are informational, not timing-sensitive test assertions.
|
|
124
136
|
|
|
@@ -272,6 +272,8 @@ type ResultInput = {
|
|
|
272
272
|
expanded: boolean;
|
|
273
273
|
toolCallId: string;
|
|
274
274
|
author?: (expanded: boolean) => Component | undefined;
|
|
275
|
+
/** A codemode completion summary, in this style's native indentation. */
|
|
276
|
+
completion?: Component;
|
|
275
277
|
};
|
|
276
278
|
|
|
277
279
|
/**
|
|
@@ -425,11 +427,11 @@ function customResult(input: ResultInput, width: number): string[] {
|
|
|
425
427
|
const start = lines.findIndex((line) => stripTerminalSequences(line).trim() !== "");
|
|
426
428
|
rows = (start < 0 ? [] : lines.slice(start)).map((line, index) => (index === 0 ? first : CODEX_INDENT) + line);
|
|
427
429
|
} else if (text || isError) rows = prefixedRows(textLines(outputLines(isError ? `Error: ${text}` : text).join("\n"), "dim"), first, CODEX_INDENT, width, theme);
|
|
428
|
-
return [...rows, dimDisclosure(theme, width, true)];
|
|
430
|
+
return [...rows, ...(input.completion?.render(width) ?? []), dimDisclosure(theme, width, true)];
|
|
429
431
|
}
|
|
430
432
|
let lines = text ? tailLines(text) : [];
|
|
431
433
|
if (isError && outputLines(text).length <= CODEX_PREVIEW_ROWS) lines = lines.length ? [`Error: ${lines[0]}`, ...lines.slice(1)] : ["Error: "];
|
|
432
|
-
return [...previewRows(lines, theme, width), dimDisclosure(theme, width, false)];
|
|
434
|
+
return [...previewRows(lines, theme, width), ...(input.completion?.render(width) ?? []), dimDisclosure(theme, width, false)];
|
|
433
435
|
}
|
|
434
436
|
|
|
435
437
|
/** Everything a Codex row draws beneath its head. */
|
|
@@ -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. */
|
|
@@ -203,12 +203,18 @@ export function prefixedLines(text: string, firstPrefix: string, continuationPre
|
|
|
203
203
|
* A sentence under a tree prefix, wrapped at words as a terminal UI wraps prose:
|
|
204
204
|
* a word moves to the next row whole, and only a word longer than a row is split.
|
|
205
205
|
*/
|
|
206
|
-
export function prefixedSentence(text: string, firstPrefix: string, continuationPrefix: string): Component {
|
|
206
|
+
export function prefixedSentence(text: string, firstPrefix: string, continuationPrefix: string, trimWrappedIndent = false): Component {
|
|
207
207
|
const prefixWidth = Math.max(visibleWidth(firstPrefix), visibleWidth(continuationPrefix));
|
|
208
208
|
const lines = text.replace(/\t/g, " ").split("\n");
|
|
209
209
|
return new CachedComponent((width) => {
|
|
210
210
|
const available = Math.max(1, width - prefixWidth);
|
|
211
|
-
return lines.flatMap((line) => wrapTextWithAnsi(line, available).map((row) =>
|
|
211
|
+
return lines.flatMap((line) => wrapTextWithAnsi(line, available).map((row, index) => {
|
|
212
|
+
if (trimWrappedIndent && index > 0) {
|
|
213
|
+
const spaces = stripTerminalSequences(row).match(/^ +/u)?.[0].length ?? 0;
|
|
214
|
+
if (spaces) row = sliceByColumn(row, spaces, visibleWidth(row) - spaces, true);
|
|
215
|
+
}
|
|
216
|
+
return truncateToWidth(row, available, "");
|
|
217
|
+
}))
|
|
212
218
|
.map((row, index) => `${index === 0 ? firstPrefix : continuationPrefix}${row}`);
|
|
213
219
|
});
|
|
214
220
|
}
|
|
@@ -216,8 +222,9 @@ export function prefixedSentence(text: string, firstPrefix: string, continuation
|
|
|
216
222
|
/**
|
|
217
223
|
* Output as Claude Code previews it. Each line is wrapped to the width, and rows
|
|
218
224
|
* rather than lines are counted: collapsed, `maxRows` show, all of them when only
|
|
219
|
-
* one more would be hidden, and
|
|
220
|
-
* columns short of the terminal, so previews break where
|
|
225
|
+
* one more would be hidden, then an optional completion and `more(hidden)`.
|
|
226
|
+
* Claude Code wraps ten columns short of the terminal, so previews break where
|
|
227
|
+
* its own do. Completion rows never count toward the output-preview limit.
|
|
221
228
|
*/
|
|
222
229
|
export function claudeRows(
|
|
223
230
|
lines: readonly string[],
|
|
@@ -225,17 +232,20 @@ export function claudeRows(
|
|
|
225
232
|
continuationPrefix: string,
|
|
226
233
|
maxRows: number | undefined,
|
|
227
234
|
more: (hidden: number) => string,
|
|
235
|
+
completion?: Component,
|
|
228
236
|
): Component {
|
|
229
237
|
const prefixWidth = Math.max(visibleWidth(firstPrefix), visibleWidth(continuationPrefix));
|
|
230
238
|
return new CachedComponent((width) => {
|
|
231
239
|
const available = Math.max(1, width - prefixWidth);
|
|
232
240
|
const wrapWidth = maxRows === undefined ? available : Math.min(available, Math.max(width - 10, 10));
|
|
233
241
|
let rows = lines.flatMap((line) => hardWrapTextWithAnsi(line, wrapWidth).map((row) => row.trimEnd()));
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
242
|
+
const hidden = maxRows !== undefined && rows.length > maxRows + 1 ? rows.length - maxRows : 0;
|
|
243
|
+
if (hidden) rows = rows.slice(0, maxRows);
|
|
244
|
+
const rendered = rows.map((row, index) => `${index === 0 ? firstPrefix : continuationPrefix}${row}`);
|
|
245
|
+
if (completion) rendered.push(...completion.render(width));
|
|
246
|
+
if (hidden) rendered.push((rendered.length ? continuationPrefix : firstPrefix) + truncateToWidth(more(hidden), available, "…"));
|
|
247
|
+
return rendered;
|
|
248
|
+
}, () => completion?.invalidate());
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
export function hardWrapTextWithAnsi(text: string, width: number): string[] {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
3
|
+
import { chromePainter, nestedCountPainter, nestedTitlePainter } from "./compact-tools-palette.ts";
|
|
4
|
+
|
|
5
|
+
const SUMMARY_KEY = Symbol.for("pi-compact-tools.nested-summaries.v1");
|
|
6
|
+
const MAX_PARENTS = 2_000;
|
|
7
|
+
const MAX_CALLS = 256;
|
|
8
|
+
const MAX_NAME_LENGTH = 256;
|
|
9
|
+
const MAX_ID_LENGTH = 1_024;
|
|
10
|
+
type Status = "ok" | "error" | "unfinished";
|
|
11
|
+
type Call = { name: string; status: Status };
|
|
12
|
+
type Summary = { calls: Map<string, Call>; partial: boolean; finished: boolean; live: boolean; revision: object };
|
|
13
|
+
type Shared = typeof globalThis & { [SUMMARY_KEY]?: Map<string, Summary> };
|
|
14
|
+
type NestedEvent = { toolCallId: string; toolName: string; parentToolCallId?: string; isError?: boolean; result?: unknown };
|
|
15
|
+
|
|
16
|
+
/** UI-only, bounded bookkeeping. Never retain arguments, results, or full diffs. */
|
|
17
|
+
export class NestedCallSummaries {
|
|
18
|
+
private readonly summaries: Map<string, Summary>;
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
const shared = globalThis as Shared;
|
|
22
|
+
this.summaries = shared[SUMMARY_KEY] ??= new Map();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clear(): void {
|
|
26
|
+
this.summaries.clear();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
begin(id: string, name: string): void {
|
|
30
|
+
if (name !== "codemode" || id.length > MAX_ID_LENGTH) return;
|
|
31
|
+
this.summaries.delete(id);
|
|
32
|
+
this.summaries.set(id, { calls: new Map(), partial: false, finished: false, live: true, revision: {} });
|
|
33
|
+
this.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
finish(id: string): void {
|
|
37
|
+
const summary = this.summaries.get(id);
|
|
38
|
+
if (summary) {
|
|
39
|
+
summary.finished = true;
|
|
40
|
+
summary.revision = {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A revision is also read at draw time, so even a late child end cannot freeze a footer. */
|
|
45
|
+
revision(id: string): object | undefined {
|
|
46
|
+
return this.summaries.get(id)?.revision;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Host-assigned descendant ids are `<caller id>/<n>`; retain the whole provider root id. */
|
|
50
|
+
private root(parent: string): string | undefined {
|
|
51
|
+
let id = parent;
|
|
52
|
+
while (id) {
|
|
53
|
+
if (this.summaries.has(id)) return id;
|
|
54
|
+
const slash = id.lastIndexOf("/");
|
|
55
|
+
if (slash < 0) return undefined;
|
|
56
|
+
id = id.slice(0, slash);
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Returns true only when a finished parent needs a fresh terminal frame. */
|
|
62
|
+
note(event: NestedEvent, ended: boolean): boolean {
|
|
63
|
+
if (typeof event.parentToolCallId !== "string" || event.parentToolCallId.length > MAX_ID_LENGTH) return false;
|
|
64
|
+
const root = this.root(event.parentToolCallId);
|
|
65
|
+
const summary = root === undefined ? undefined : this.summaries.get(root);
|
|
66
|
+
if (!summary) return false;
|
|
67
|
+
if (typeof event.toolCallId !== "string" || typeof event.toolName !== "string"
|
|
68
|
+
|| event.toolCallId.length > MAX_ID_LENGTH || event.toolName.length > MAX_NAME_LENGTH) {
|
|
69
|
+
if (!summary.partial) { summary.partial = true; summary.revision = {}; }
|
|
70
|
+
return summary.finished;
|
|
71
|
+
}
|
|
72
|
+
const previous = summary.calls.get(event.toolCallId);
|
|
73
|
+
// A duplicate start must not turn an already settled call back into a running one.
|
|
74
|
+
if (!ended && previous) return false;
|
|
75
|
+
if (!previous && summary.calls.size >= MAX_CALLS) {
|
|
76
|
+
if (!summary.partial) { summary.partial = true; summary.revision = {}; }
|
|
77
|
+
return summary.finished;
|
|
78
|
+
}
|
|
79
|
+
const status = ended ? event.isError ? "error" : "ok" : "unfinished";
|
|
80
|
+
if (previous?.name === event.toolName && previous.status === status) return false;
|
|
81
|
+
summary.calls.set(event.toolCallId, { name: event.toolName, status });
|
|
82
|
+
summary.live = true;
|
|
83
|
+
summary.revision = {};
|
|
84
|
+
return summary.finished;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Pi persists `{calls: [{id, name, status}], complete}`, deliberately without results. */
|
|
88
|
+
restore(message: unknown): boolean {
|
|
89
|
+
if (!message || typeof message !== "object") return false;
|
|
90
|
+
const value = message as { role?: string; toolName?: string; toolCallId?: string; nestedCalls?: { calls?: unknown; complete?: unknown } };
|
|
91
|
+
if (value.role !== "toolResult" || value.toolName !== "codemode" || typeof value.toolCallId !== "string"
|
|
92
|
+
|| value.toolCallId.length > MAX_ID_LENGTH || !Array.isArray(value.nestedCalls?.calls)) return false;
|
|
93
|
+
// A bounded persisted snapshot must not drop observed live calls.
|
|
94
|
+
const existing = this.summaries.get(value.toolCallId);
|
|
95
|
+
if (existing?.live && existing.calls.size > 0) return false;
|
|
96
|
+
const summary: Summary = { calls: new Map(), partial: value.nestedCalls.complete !== true, finished: true, live: false, revision: {} };
|
|
97
|
+
let inspected = 0;
|
|
98
|
+
for (const item of value.nestedCalls.calls) {
|
|
99
|
+
if (inspected++ >= MAX_CALLS) { summary.partial = true; break; }
|
|
100
|
+
if (!item || typeof item !== "object") { summary.partial = true; continue; }
|
|
101
|
+
const call = item as { id?: unknown; name?: unknown; status?: unknown };
|
|
102
|
+
if (typeof call.id !== "string" || typeof call.name !== "string"
|
|
103
|
+
|| call.id.length > MAX_ID_LENGTH || call.name.length > MAX_NAME_LENGTH
|
|
104
|
+
|| (call.status !== "ok" && call.status !== "error" && call.status !== "unfinished")) { summary.partial = true; continue; }
|
|
105
|
+
if (!summary.calls.has(call.id) && summary.calls.size >= MAX_CALLS) { summary.partial = true; break; }
|
|
106
|
+
summary.calls.set(call.id, { name: call.name, status: call.status as Status });
|
|
107
|
+
}
|
|
108
|
+
this.summaries.set(value.toolCallId, summary);
|
|
109
|
+
this.trim();
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The live branch only, never abandoned entries; callers clear on non-reload session changes. */
|
|
114
|
+
restoreBranch(entries: readonly unknown[]): void {
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (entry && typeof entry === "object") this.restore((entry as { message?: unknown }).message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Other tool groups keep first-occurrence order; edit counts always come last. */
|
|
121
|
+
format(id: string, theme: Theme, _expanded = false): string | undefined {
|
|
122
|
+
const summary = this.summaries.get(id);
|
|
123
|
+
if (!summary || (summary.calls.size === 0 && !summary.partial)) return undefined;
|
|
124
|
+
type Group = { name: string; calls: Call[] };
|
|
125
|
+
const groups: Group[] = [];
|
|
126
|
+
const byTool = new Map<string, Group>();
|
|
127
|
+
for (const call of summary.calls.values()) {
|
|
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
|
+
}
|
|
135
|
+
group.calls.push(call);
|
|
136
|
+
}
|
|
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) => {
|
|
145
|
+
const name = stripTerminalSequences(group.name).replace(/[\x00-\x20\x7f-\x9f]/gu, " ").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/gu, "").trim() || "tool";
|
|
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)") : "");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private trim(): void {
|
|
162
|
+
if (this.summaries.size <= MAX_PARENTS) return;
|
|
163
|
+
for (const [id, summary] of this.summaries) {
|
|
164
|
+
if (summary.finished) { this.summaries.delete(id); return; }
|
|
165
|
+
}
|
|
166
|
+
const oldest = this.summaries.keys().next().value;
|
|
167
|
+
if (oldest !== undefined) this.summaries.delete(oldest);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -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
|
-
*
|
|
42
|
-
*
|
|
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
|
-
/**
|
|
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
|
}
|
|
@@ -51,7 +51,8 @@ export class ToolRuntime {
|
|
|
51
51
|
private readonly currentCalls = new Set<string>();
|
|
52
52
|
/**
|
|
53
53
|
* Calls a tool is making through `ctx.executeTool()`, as codemode scripts do. They
|
|
54
|
-
* never get a row of their own, so
|
|
54
|
+
* never get a row of their own, so no execution timing is kept for them.
|
|
55
|
+
* The independent nested-summary observer keeps only their outcome counts.
|
|
55
56
|
*/
|
|
56
57
|
private readonly nestedCalls = new Set<string>();
|
|
57
58
|
|
|
@@ -98,6 +99,11 @@ export class ToolRuntime {
|
|
|
98
99
|
this.requestRender = requestRender;
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/** A late summary changes only a draw-time footer, not every row's layout. */
|
|
103
|
+
repaint(): void {
|
|
104
|
+
if (!isSilent()) this.requestRender?.();
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
/**
|
|
102
108
|
* The agent started or stopped working. Once it stops nothing can still be
|
|
103
109
|
* running, so a row that never got its result stops pulsing and waits instead.
|
|
@@ -48,10 +48,11 @@ import {
|
|
|
48
48
|
WithinWidth,
|
|
49
49
|
} from "./compact-tools-layout.ts";
|
|
50
50
|
import { CLAUDE_DIFF_CONTEXT_LINES, CLAUDE_INDENT, CLAUDE_OUTPUT_ROWS, CLAUDE_RESULT_PREFIX, CLAUDE_TITLE_INDENT, CLAUDE_WRITE_ROWS, claudeFailure, claudeOutcome, claudeTitle } from "./compact-tools-claude.ts";
|
|
51
|
-
import { CODEX_BULLET } from "./compact-tools-codex.ts";
|
|
51
|
+
import { CODEX_BULLET, CODEX_INDENT } from "./compact-tools-codex.ts";
|
|
52
|
+
import { NestedCallSummaries } from "./compact-tools-nested.ts";
|
|
52
53
|
import { renderCodexCall, renderCodexResult } from "./compact-tools-codex-rows.ts";
|
|
53
54
|
import { ToolGroupController } from "./compact-tools-grouping.ts";
|
|
54
|
-
import { chromePainter, paintIndicator } from "./compact-tools-palette.ts";
|
|
55
|
+
import { chromePainter, completionPainter, paintIndicator } from "./compact-tools-palette.ts";
|
|
55
56
|
import { ProgressController } from "./compact-tools-progress.ts";
|
|
56
57
|
import { showReleaseNotice } from "./compact-tools-release.ts";
|
|
57
58
|
import { ToolRuntime } from "./compact-tools-runtime.ts";
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
const PREVIEW_DIFF_CONTEXT_LINES = 1;
|
|
71
72
|
|
|
72
73
|
const runtime = new ToolRuntime();
|
|
74
|
+
const nestedSummaries = new NestedCallSummaries();
|
|
73
75
|
const RENDER_WIDGET_KEY = "compact-tools-render";
|
|
74
76
|
let registeredConfiguration: string | undefined;
|
|
75
77
|
/**
|
|
@@ -179,15 +181,19 @@ function renderControls(
|
|
|
179
181
|
isError: boolean,
|
|
180
182
|
failureReason?: string,
|
|
181
183
|
changes?: { added: number; removed: number },
|
|
184
|
+
nestedSummary?: string,
|
|
185
|
+
prefix = " └ ",
|
|
186
|
+
indent = " ",
|
|
182
187
|
): Component {
|
|
183
188
|
const duration = running ? undefined : formatDuration(state);
|
|
184
189
|
const status = running
|
|
185
190
|
? (duration ?? "Running")
|
|
186
191
|
: `${isError ? "Failed" : "Done"}${duration ? ` in ${duration}` : ""}`;
|
|
187
192
|
// A failed row already reads as an error through its output color and Pi's row
|
|
188
|
-
// background, so
|
|
193
|
+
// background, so completion metadata stays neutral rather than repeating that signal.
|
|
189
194
|
const chrome = chromePainter(theme);
|
|
190
|
-
|
|
195
|
+
const metadata = running ? chrome : completionPainter(theme);
|
|
196
|
+
let details = metadata(status);
|
|
191
197
|
if (failureReason && isError && !running) details += chrome(" · ") + theme.fg("error", failureReason);
|
|
192
198
|
// An edit reports what it changed the way a diff does: lines added and removed.
|
|
193
199
|
else if (changes && !running && !isError) {
|
|
@@ -195,8 +201,9 @@ function renderControls(
|
|
|
195
201
|
+ theme.fg("toolDiffRemoved", `-${changes.removed}`) + chrome(")");
|
|
196
202
|
}
|
|
197
203
|
// A failed call produced no result worth counting; "(0 lines)" would only mislead.
|
|
198
|
-
else if (state.resultLineSummary && !running && !isError) details +=
|
|
199
|
-
|
|
204
|
+
else if (state.resultLineSummary && !running && !isError) details += metadata(` (${state.resultLineSummary})`);
|
|
205
|
+
if (nestedSummary && !running) details += chrome(" · ") + nestedSummary;
|
|
206
|
+
return nestedSummary ? prefixedSentence(details, chrome(prefix), indent, true) : prefixedText(details, chrome(prefix), indent);
|
|
200
207
|
}
|
|
201
208
|
|
|
202
209
|
/** Arguments a one-line summary cannot show: objects, or lists of them. */
|
|
@@ -306,14 +313,14 @@ function claudeMoreLines(theme: Theme, hidden: number): string {
|
|
|
306
313
|
}
|
|
307
314
|
|
|
308
315
|
/** Output previewed in rows the way Claude Code previews it, all of it once opened. */
|
|
309
|
-
function claudeOutput(output: string, expanded: boolean, theme: Theme, isError: boolean, empty: string, firstPrefix: string): Component {
|
|
316
|
+
function claudeOutput(output: string, expanded: boolean, theme: Theme, isError: boolean, empty: string, firstPrefix: string, completion?: Component): Component {
|
|
310
317
|
const normalized = normalizeLineEndings(output).trimEnd();
|
|
311
318
|
// Pi's bash reports an empty run as "(no output)"; Claude Code dims its own words for it.
|
|
312
319
|
const lines = !normalized || normalized === "(no output)"
|
|
313
320
|
? [chromePainter(theme)(empty)]
|
|
314
321
|
: normalized.split("\n").map((line) => theme.fg(isError ? "error" : "toolOutput", line));
|
|
315
322
|
return claudeRows(lines, firstPrefix, CLAUDE_INDENT, expanded ? undefined : CLAUDE_OUTPUT_ROWS,
|
|
316
|
-
(hidden) => claudeMoreLines(theme, hidden));
|
|
323
|
+
(hidden) => claudeMoreLines(theme, hidden), completion);
|
|
317
324
|
}
|
|
318
325
|
|
|
319
326
|
/**
|
|
@@ -363,6 +370,7 @@ function renderClaudeResult(
|
|
|
363
370
|
expanded: boolean,
|
|
364
371
|
output: string,
|
|
365
372
|
author: AuthorResultRenderer | undefined,
|
|
373
|
+
completion?: Component,
|
|
366
374
|
): Component {
|
|
367
375
|
const chrome = chromePainter(theme);
|
|
368
376
|
const container = new CachedContainer();
|
|
@@ -378,11 +386,12 @@ function renderClaudeResult(
|
|
|
378
386
|
// shortened to one line, and a click shows it whole in its place.
|
|
379
387
|
if (failure.detail) {
|
|
380
388
|
container.addChild(line(theme.fg("error", failure.headline)));
|
|
381
|
-
container.addChild(claudeOutput(failure.detail, expanded, theme, true, "", CLAUDE_INDENT));
|
|
389
|
+
container.addChild(claudeOutput(failure.detail, expanded, theme, true, "", CLAUDE_INDENT, completion));
|
|
382
390
|
} else if (expanded && output.trim()) {
|
|
383
|
-
container.addChild(claudeOutput(`Error: ${output.trim()}`, true, theme, true, "", chrome(CLAUDE_RESULT_PREFIX)));
|
|
391
|
+
container.addChild(claudeOutput(`Error: ${output.trim()}`, true, theme, true, "", chrome(CLAUDE_RESULT_PREFIX), completion));
|
|
384
392
|
} else {
|
|
385
393
|
container.addChild(line(theme.fg("error", failure.headline)));
|
|
394
|
+
if (completion) container.addChild(completion);
|
|
386
395
|
}
|
|
387
396
|
return container;
|
|
388
397
|
}
|
|
@@ -392,8 +401,9 @@ function renderClaudeResult(
|
|
|
392
401
|
? formatResultLineSummary(name, args, result, output) ?? "Done"
|
|
393
402
|
: claudeOutcome(name, args, result, output, (value) => theme.bold(value));
|
|
394
403
|
if (outcome !== undefined) container.addChild(line(theme.fg("toolOutput", outcome)));
|
|
395
|
-
else container.addChild(claudeOutput(output, expanded, theme, false, rowKind(name) === "shell" ? "(No output)" : "(No content)", chrome(CLAUDE_RESULT_PREFIX)));
|
|
404
|
+
else container.addChild(claudeOutput(output, expanded, theme, false, rowKind(name) === "shell" ? "(No output)" : "(No content)", chrome(CLAUDE_RESULT_PREFIX), completion));
|
|
396
405
|
if (body) container.addChild(body);
|
|
406
|
+
if (completion && outcome !== undefined) container.addChild(completion);
|
|
397
407
|
return container;
|
|
398
408
|
}
|
|
399
409
|
|
|
@@ -409,6 +419,7 @@ function renderRowResult(
|
|
|
409
419
|
theme: Theme,
|
|
410
420
|
ctx: RenderContext,
|
|
411
421
|
author?: AuthorResultRenderer,
|
|
422
|
+
watchNested = true,
|
|
412
423
|
): Component {
|
|
413
424
|
const state = runtime.syncRow(ctx, !options.isPartial);
|
|
414
425
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
@@ -417,14 +428,23 @@ function renderRowResult(
|
|
|
417
428
|
const output = kind === "file" && !author ? getFileOutput(name, ctx.args, result, ctx.isError) : getTextResult(result);
|
|
418
429
|
const hasEditDiff = name === "edit" && getEditDiff(result).length > 0;
|
|
419
430
|
runtime.setResultAvailable(state, name, hasEditDiff || output.length > 0 || author !== undefined);
|
|
431
|
+
const nestedSummary = name === "codemode" && !options.isPartial ? nestedSummaries.format(ctx.toolCallId, theme, state.expanded === true) : undefined;
|
|
432
|
+
if (nestedSummary && !state.resultLineSummaryComputed) {
|
|
433
|
+
state.resultLineSummary = output ? formatResultLineSummary(name, ctx.args, result, output) : undefined;
|
|
434
|
+
state.resultLineSummaryComputed = true;
|
|
435
|
+
}
|
|
420
436
|
let component: Component;
|
|
421
437
|
if (isCodex()) {
|
|
422
438
|
component = renderCodexResult({
|
|
423
439
|
name, kind: author ? "custom" : kind, args: ctx.args, result, output, theme, state, isError: ctx.isError,
|
|
424
440
|
isPartial: options.isPartial, expanded: state.expanded === true, toolCallId: ctx.toolCallId, author,
|
|
441
|
+
completion: nestedSummary ? renderControls(theme, state, false, ctx.isError, undefined, undefined, nestedSummary, CODEX_INDENT, CODEX_INDENT) : undefined,
|
|
425
442
|
});
|
|
426
443
|
} else if (isClaude()) {
|
|
427
|
-
|
|
444
|
+
const container = new CachedContainer();
|
|
445
|
+
const completion = nestedSummary ? renderControls(theme, state, false, ctx.isError, undefined, undefined, nestedSummary, CLAUDE_INDENT, CLAUDE_INDENT) : undefined;
|
|
446
|
+
container.addChild(renderClaudeResult(name, ctx.args, result, options, theme, ctx.isError, state.expanded === true, output, author, completion));
|
|
447
|
+
component = new WithinWidth(container);
|
|
428
448
|
} else {
|
|
429
449
|
if (!options.isPartial && !state.resultLineSummaryComputed) {
|
|
430
450
|
// A custom tool with no text has nothing to count.
|
|
@@ -440,13 +460,28 @@ function renderRowResult(
|
|
|
440
460
|
// the output shows, it already says why, and repeating it only doubles the error.
|
|
441
461
|
const failureReason = ctx.isError && !state.expanded && !state.preview ? summarizeFailure(name, output) : undefined;
|
|
442
462
|
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, failureReason,
|
|
443
|
-
name === "edit" ? countEditChanges(result) : undefined));
|
|
463
|
+
name === "edit" ? countEditChanges(result) : undefined, nestedSummary));
|
|
444
464
|
component = new WithinWidth(container);
|
|
445
465
|
}
|
|
446
466
|
rememberResult(state, result, options, ctx.isError, theme);
|
|
447
467
|
state.lastResultDurationMs = state.durationMs;
|
|
448
468
|
state.lastResultOutputPad = ctx.outputPad;
|
|
449
|
-
|
|
469
|
+
let drawn = withOutputPad(component, ctx.outputPad);
|
|
470
|
+
if (name !== "codemode" || options.isPartial || !watchNested) return drawn;
|
|
471
|
+
// The host can deliver a final nested snapshot (or a cancelled child's end)
|
|
472
|
+
// after the parent's result. Read through at draw time, not only on updateResult.
|
|
473
|
+
let revision = nestedSummaries.revision(ctx.toolCallId);
|
|
474
|
+
return {
|
|
475
|
+
render(width) {
|
|
476
|
+
const next = nestedSummaries.revision(ctx.toolCallId);
|
|
477
|
+
if (next !== revision) {
|
|
478
|
+
revision = next;
|
|
479
|
+
drawn = renderRowResult(name, result, options, theme, { ...ctx, lastComponent: undefined }, author, false);
|
|
480
|
+
}
|
|
481
|
+
return drawn.render(width);
|
|
482
|
+
},
|
|
483
|
+
invalidate() { drawn.invalidate(); },
|
|
484
|
+
};
|
|
450
485
|
}
|
|
451
486
|
|
|
452
487
|
/**
|
|
@@ -670,7 +705,12 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
670
705
|
const lastTrust = (globalThis as TrustSlot)[TRUST_KEY];
|
|
671
706
|
configure(pi, cwd, lastTrust?.cwd === realDirectory(cwd) && lastTrust.trusted);
|
|
672
707
|
pi.on("session_start", (event, ctx) => {
|
|
673
|
-
if (event.reason !== "reload")
|
|
708
|
+
if (event.reason !== "reload") {
|
|
709
|
+
runtime.clearTimings();
|
|
710
|
+
nestedSummaries.clear();
|
|
711
|
+
}
|
|
712
|
+
const branch = ctx.sessionManager?.getBranch?.();
|
|
713
|
+
if (Array.isArray(branch)) nestedSummaries.restoreBranch(branch);
|
|
674
714
|
// Rows restored from the session are drawn next; while idle none of them is running.
|
|
675
715
|
if (typeof ctx.isIdle === "function") runtime.setBusy(!ctx.isIdle());
|
|
676
716
|
const trusted = ctx.isProjectTrusted();
|
|
@@ -703,18 +743,39 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
703
743
|
groups.dispose();
|
|
704
744
|
}
|
|
705
745
|
});
|
|
746
|
+
// /tree changes the branch without session_start. Fill sibling-branch records
|
|
747
|
+
// before Pi rebuilds its transcript, while retaining richer same-process deltas.
|
|
748
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
749
|
+
const branch = ctx.sessionManager?.getBranch?.();
|
|
750
|
+
if (Array.isArray(branch)) nestedSummaries.restoreBranch(branch);
|
|
751
|
+
runtime.repaint();
|
|
752
|
+
});
|
|
706
753
|
pi.on("agent_start", () => runtime.setBusy(true));
|
|
707
754
|
pi.on("agent_end", () => runtime.setBusy(false));
|
|
708
755
|
// The calls of the run going on, read from each update as Pi reads them to make
|
|
709
756
|
// their rows, and told to extensions before Pi makes them.
|
|
710
757
|
pi.on("message_update", (event) => noteCalls(event.message));
|
|
758
|
+
// Notification-only: read the host's nested record without changing any message.
|
|
759
|
+
pi.on("message_end", (event) => {
|
|
760
|
+
if (nestedSummaries.restore(event.message)) runtime.repaint();
|
|
761
|
+
});
|
|
711
762
|
pi.on("tool_execution_start", (event) => {
|
|
712
|
-
if (madeByTool(event))
|
|
763
|
+
if (madeByTool(event)) {
|
|
764
|
+
runtime.noteNestedCall(event.toolCallId, true);
|
|
765
|
+
nestedSummaries.note(event, false);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
nestedSummaries.begin(event.toolCallId, event.toolName);
|
|
713
769
|
runtime.noteCall(event.toolCallId);
|
|
714
770
|
runtime.noteExecutionStart(event.toolCallId);
|
|
715
771
|
});
|
|
716
772
|
pi.on("tool_execution_end", (event) => {
|
|
717
|
-
if (madeByTool(event))
|
|
773
|
+
if (madeByTool(event)) {
|
|
774
|
+
runtime.noteNestedCall(event.toolCallId, false);
|
|
775
|
+
if (nestedSummaries.note(event, true)) runtime.repaint();
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
nestedSummaries.finish(event.toolCallId);
|
|
718
779
|
runtime.noteExecutionEnd(event.toolCallId, (event as { durationMs?: number }).durationMs);
|
|
719
780
|
});
|
|
720
781
|
pi.on("session_shutdown", (event) => {
|
|
@@ -726,6 +787,7 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
726
787
|
unbindRenderer(renderContext);
|
|
727
788
|
renderContext = undefined;
|
|
728
789
|
runtime.reset(event.reason !== "reload");
|
|
790
|
+
if (event.reason !== "reload") nestedSummaries.clear();
|
|
729
791
|
// A reload that keeps the extension sets it again before Pi rebuilds the transcript.
|
|
730
792
|
if (legacyResolver) clearRowResolver(legacyResolver);
|
|
731
793
|
refreshTranscriptRows = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-compact-tools",
|
|
3
|
-
"version": "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",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@earendil-works/pi-tui": ">=0.85.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
55
|
-
"@earendil-works/pi-tui": "1.
|
|
54
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
55
|
+
"@earendil-works/pi-tui": "1.2.0",
|
|
56
56
|
"@sinonjs/fake-timers": "15.0.0",
|
|
57
57
|
"@types/node": "^24.0.0",
|
|
58
58
|
"@types/sinonjs__fake-timers": "15.0.1",
|
package/release-notes.json
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
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
|
+
},
|
|
16
|
+
"0.16.0": {
|
|
17
|
+
"New": [
|
|
18
|
+
"Completed codemode rows summarize nested tool successes and failures beside their duration and output-line count without changing model input",
|
|
19
|
+
"Reloads retain exact live edit diffs; resumed sessions and /tree branches restore saved per-call outcomes without guessing changes",
|
|
20
|
+
"Codemode summaries show every edit attempt in order: edit(3) [+10 -3|✗|+5 -7], with no checkmark on successful changes",
|
|
21
|
+
"Compact, Claude, and Codex show the first three edit attempts when collapsed and every recorded attempt when expanded",
|
|
22
|
+
"Diff separators are dim and have no surrounding whitespace; additions and removals retain their diff colors"
|
|
23
|
+
],
|
|
24
|
+
"Fix": [
|
|
25
|
+
"Updated the GitHub Dark theme and theme fixtures to Pi 1.2's published schema; validated both startup and reload resource loading",
|
|
26
|
+
"Verified on Pi 1.2.0 while preserving the Pi 0.85.1 minimum and the existing 0.15.0 word-diff and platform key-label features",
|
|
27
|
+
"Per-call edit diffs keep first-call order even when nested calls finish out of order",
|
|
28
|
+
"Claude and Codex keep codemode output, an indented completion summary without a separate branch marker, and the disclosure in that order",
|
|
29
|
+
"Failed and unfinished edit attempts remain visible in previews, including omitted-attempt status counts",
|
|
30
|
+
"A missing successful edit diff shows ok for that call without hiding other calls’ known changes or guessing totals"
|
|
31
|
+
],
|
|
32
|
+
"Deprecated": []
|
|
33
|
+
},
|
|
2
34
|
"0.15.0": {
|
|
3
35
|
"New": [
|
|
4
36
|
"Claude style now marks the changed words in an edit diff, like Claude Code 2.1.296: a removed line and the added line that replaces it get a brighter tint on the words that differ. Lines that are mostly rewritten are left alone",
|