pi-compact-tools 0.13.1 → 0.14.0
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 +52 -26
- package/extensions/compact-tools-activity.ts +10 -2
- package/extensions/compact-tools-claude.ts +20 -26
- package/extensions/compact-tools-codex-rows.ts +465 -0
- package/extensions/compact-tools-codex.ts +453 -0
- package/extensions/compact-tools-command.ts +196 -0
- package/extensions/compact-tools-custom.ts +9 -0
- package/extensions/compact-tools-grouping.ts +130 -31
- package/extensions/compact-tools-invocation.ts +30 -1
- package/extensions/compact-tools-layout.ts +124 -27
- package/extensions/compact-tools-palette.ts +6 -7
- package/extensions/compact-tools-release.ts +30 -7
- package/extensions/compact-tools-runtime.ts +16 -0
- package/extensions/compact-tools-shell.ts +277 -0
- package/extensions/compact-tools-silent.ts +4 -1
- package/extensions/compact-tools-types.ts +10 -2
- package/extensions/compact-tools-viewport.ts +5 -7
- package/extensions/compact-tools.ts +115 -38
- package/package.json +10 -6
- package/release-notes.json +23 -0
- package/schemas/compact-tools.schema.json +3 -3
- package/scripts/check-resources.mjs +0 -82
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
1
2
|
import type {
|
|
2
3
|
AgentToolResult,
|
|
3
4
|
ExtensionAPI,
|
|
@@ -15,11 +16,14 @@ import {
|
|
|
15
16
|
createPowerShellToolDefinition,
|
|
16
17
|
createReadToolDefinition,
|
|
17
18
|
createWriteToolDefinition,
|
|
19
|
+
ToolExecutionComponent,
|
|
18
20
|
} from "@earendil-works/pi-coding-agent";
|
|
19
21
|
import type { Component } from "@earendil-works/pi-tui";
|
|
20
22
|
import { classifyCallStatus, formatDurationMs, normalizeLineEndings, type RowStatus } from "./compact-tools-core.ts";
|
|
21
23
|
import { loadConfig } from "./compact-tools-config.ts";
|
|
22
|
-
import {
|
|
24
|
+
import { paletteOf } from "./compact-tools-color.ts";
|
|
25
|
+
import { registerCompactToolsCommand } from "./compact-tools-command.ts";
|
|
26
|
+
import { clearRowResolver, installToolRowPatch, setRowResolver, type RowRenderers, type ToolRow } from "./compact-tools-custom.ts";
|
|
23
27
|
import {
|
|
24
28
|
formatResultLineSummary,
|
|
25
29
|
countEditChanges,
|
|
@@ -40,7 +44,7 @@ import {
|
|
|
40
44
|
claudeRows,
|
|
41
45
|
LiveCallContainer,
|
|
42
46
|
limitComponentLines,
|
|
43
|
-
|
|
47
|
+
prefixedSentence,
|
|
44
48
|
prefixedText,
|
|
45
49
|
railComponent,
|
|
46
50
|
renderArguments,
|
|
@@ -49,14 +53,17 @@ import {
|
|
|
49
53
|
renderOutput,
|
|
50
54
|
renderToolCall,
|
|
51
55
|
styleMultiline,
|
|
56
|
+
WithinWidth,
|
|
52
57
|
} from "./compact-tools-layout.ts";
|
|
53
|
-
import { CLAUDE_DIFF_CONTEXT_LINES, CLAUDE_OUTPUT_ROWS, CLAUDE_WRITE_ROWS, claudeFailure, claudeOutcome, claudeTitle } from "./compact-tools-claude.ts";
|
|
58
|
+
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";
|
|
59
|
+
import { CODEX_BULLET } from "./compact-tools-codex.ts";
|
|
60
|
+
import { renderCodexCall, renderCodexResult } from "./compact-tools-codex-rows.ts";
|
|
54
61
|
import { ToolGroupController } from "./compact-tools-grouping.ts";
|
|
55
62
|
import { chromePainter, paintIndicator } from "./compact-tools-palette.ts";
|
|
56
63
|
import { ProgressController } from "./compact-tools-progress.ts";
|
|
57
64
|
import { showReleaseNotice } from "./compact-tools-release.ts";
|
|
58
65
|
import { ToolRuntime } from "./compact-tools-runtime.ts";
|
|
59
|
-
import { SilentModeController } from "./compact-tools-silent.ts";
|
|
66
|
+
import { findChat, isKind, SilentModeController } from "./compact-tools-silent.ts";
|
|
60
67
|
import { ThinkingCycleController } from "./compact-tools-thinking.ts";
|
|
61
68
|
import { ViewportKeeper } from "./compact-tools-viewport.ts";
|
|
62
69
|
import {
|
|
@@ -70,13 +77,28 @@ import {
|
|
|
70
77
|
|
|
71
78
|
/** Context lines kept around each change while an edit result is collapsed. */
|
|
72
79
|
const PREVIEW_DIFF_CONTEXT_LINES = 1;
|
|
73
|
-
/** Claude Code sets what a call returned under the "⎿" rather than beside a rail. */
|
|
74
|
-
const CLAUDE_INDENT = " ";
|
|
75
80
|
|
|
76
81
|
const runtime = new ToolRuntime();
|
|
77
82
|
const RENDER_WIDGET_KEY = "compact-tools-render";
|
|
78
83
|
const registeredTools = new Set<CompactToolName>();
|
|
79
84
|
let registeredConfiguration: string | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the last session found its directory trusted. A /reload rebuilds the
|
|
87
|
+
* transcript before session_start says so again, so the tools a trusted project
|
|
88
|
+
* adds are registered from the start rather than after its rows were made. Pi
|
|
89
|
+
* evaluates this module afresh on every load, so the answer is kept on globalThis.
|
|
90
|
+
*/
|
|
91
|
+
const TRUST_KEY = Symbol.for("pi-compact-tools.trust");
|
|
92
|
+
type TrustSlot = typeof globalThis & { [TRUST_KEY]?: { cwd: string; trusted: boolean } };
|
|
93
|
+
|
|
94
|
+
/** A directory as the file system names it, so `/var` and `/private/var` are one place. */
|
|
95
|
+
function realDirectory(path: string): string {
|
|
96
|
+
try {
|
|
97
|
+
return realpathSync(path);
|
|
98
|
+
} catch {
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
80
102
|
|
|
81
103
|
/** How a tool's row is drawn: a file tool's result is code, a shell's is output, a custom tool's is its author's. */
|
|
82
104
|
type RowKind = "file" | "shell" | "custom";
|
|
@@ -90,6 +112,10 @@ function isClaude(): boolean {
|
|
|
90
112
|
return runtime.config.style === "claude";
|
|
91
113
|
}
|
|
92
114
|
|
|
115
|
+
function isCodex(): boolean {
|
|
116
|
+
return runtime.config.style === "codex";
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
function pathArgument(args: ToolArgs): string {
|
|
94
120
|
return typeof args.path === "string" ? args.path : typeof args.file_path === "string" ? args.file_path : "";
|
|
95
121
|
}
|
|
@@ -107,13 +133,13 @@ function callStatus(ctx: RenderContext, state: RowState): RowStatus {
|
|
|
107
133
|
* result, so a dot painted with the call would miss the result that arrived with
|
|
108
134
|
* it; reading the row's state at draw time sees it. A settled dot never changes.
|
|
109
135
|
*/
|
|
110
|
-
function liveIndicator(theme: Theme, ctx: RenderContext, state: RowState): () => string {
|
|
136
|
+
function liveIndicator(theme: Theme, ctx: RenderContext, state: RowState, glyph?: string): () => string {
|
|
111
137
|
let settled: string | undefined;
|
|
112
138
|
return () => {
|
|
113
139
|
if (settled !== undefined) return settled;
|
|
114
140
|
const status = callStatus(ctx, state);
|
|
115
141
|
const indicator = paintIndicator(theme, status,
|
|
116
|
-
runtime.syncIndicator(ctx.toolCallId, status === "running", () => ctx.invalidate()));
|
|
142
|
+
runtime.syncIndicator(ctx.toolCallId, status === "running", () => ctx.invalidate()), glyph);
|
|
117
143
|
if (status === "success" || status === "error") settled = indicator;
|
|
118
144
|
return indicator;
|
|
119
145
|
};
|
|
@@ -128,6 +154,7 @@ function canReuseResult<TArgs>(
|
|
|
128
154
|
state: RowState,
|
|
129
155
|
result: AgentToolResult<unknown>,
|
|
130
156
|
options: ToolRenderResultOptions,
|
|
157
|
+
theme: Theme,
|
|
131
158
|
ctx: RenderContext<TArgs>,
|
|
132
159
|
): ctx is RenderContext<TArgs> & { lastComponent: Component } {
|
|
133
160
|
return ctx.lastComponent !== undefined
|
|
@@ -137,10 +164,11 @@ function canReuseResult<TArgs>(
|
|
|
137
164
|
&& state.lastResultExpanded === state.expanded
|
|
138
165
|
&& state.lastResultPreview === state.preview
|
|
139
166
|
&& state.lastResultError === ctx.isError
|
|
140
|
-
&& state.lastResultConfigRevision === state.configRevision
|
|
167
|
+
&& state.lastResultConfigRevision === state.configRevision
|
|
168
|
+
&& state.lastResultPalette === paletteOf(theme);
|
|
141
169
|
}
|
|
142
170
|
|
|
143
|
-
function rememberResult(state: RowState, result: AgentToolResult<unknown>, options: ToolRenderResultOptions, isError: boolean): void {
|
|
171
|
+
function rememberResult(state: RowState, result: AgentToolResult<unknown>, options: ToolRenderResultOptions, isError: boolean, theme: Theme): void {
|
|
144
172
|
state.lastResultContent = result.content;
|
|
145
173
|
state.lastResultDetails = result.details;
|
|
146
174
|
state.lastResultPartial = options.isPartial;
|
|
@@ -148,6 +176,7 @@ function rememberResult(state: RowState, result: AgentToolResult<unknown>, optio
|
|
|
148
176
|
state.lastResultPreview = state.preview;
|
|
149
177
|
state.lastResultError = isError;
|
|
150
178
|
state.lastResultConfigRevision = state.configRevision;
|
|
179
|
+
state.lastResultPalette = paletteOf(theme);
|
|
151
180
|
}
|
|
152
181
|
|
|
153
182
|
function renderControls(
|
|
@@ -188,19 +217,22 @@ function renderRowCall(name: string, args: ToolArgs, theme: Theme, ctx: RenderCo
|
|
|
188
217
|
const state = runtime.syncRow(ctx);
|
|
189
218
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
190
219
|
const kind = rowKind(name);
|
|
191
|
-
const indicator = liveIndicator(theme, ctx, state);
|
|
220
|
+
const indicator = liveIndicator(theme, ctx, state, isCodex() ? CODEX_BULLET : undefined);
|
|
192
221
|
// Painted once now as well, so a row that starts running starts the animation clock.
|
|
193
222
|
indicator();
|
|
223
|
+
// Codex's head reads the call's outcome, so it is worded each time it is drawn.
|
|
224
|
+
if (isCodex()) return renderCodexCall(name, kind, args, theme, state, () => callStatus(ctx, state), indicator, ctx.toolCallId);
|
|
194
225
|
if (isClaude()) {
|
|
195
226
|
// Claude Code's call line: `⦁ Bash(npm test)`, `⦁ Update(src/app.ts)`.
|
|
196
227
|
const { label, argument } = claudeTitle(name, args, state.expanded === true);
|
|
197
228
|
const title = ` ${theme.fg("toolTitle", theme.bold(label))}`
|
|
198
229
|
+ (argument ? styleMultiline(`(${argument})`, (line) => theme.fg("toolOutput", line)) : "");
|
|
199
|
-
// The title wraps rather than being cut to the row; only a
|
|
200
|
-
|
|
230
|
+
// The title wraps under the tool's name rather than being cut to the row; only a
|
|
231
|
+
// very long command is shortened.
|
|
232
|
+
const container = new LiveCallContainer(indicator, (dot) => renderToolCall(dot + title, undefined, theme, CLAUDE_TITLE_INDENT));
|
|
201
233
|
// The title already lists plain arguments; only nested ones need the full view.
|
|
202
|
-
if (kind === "custom" && state.expanded && hasNestedArguments(args)) container.addChild(renderArguments(args, theme));
|
|
203
|
-
return container;
|
|
234
|
+
if (kind === "custom" && state.expanded && hasNestedArguments(args)) container.addChild(renderArguments(args, theme, CLAUDE_INDENT));
|
|
235
|
+
return new WithinWidth(container);
|
|
204
236
|
}
|
|
205
237
|
const title = ` ${theme.fg("toolTitle", theme.bold(name))}`;
|
|
206
238
|
let details: string | undefined;
|
|
@@ -216,10 +248,13 @@ function renderRowCall(name: string, args: ToolArgs, theme: Theme, ctx: RenderCo
|
|
|
216
248
|
const container = new LiveCallContainer(indicator, (dot) => renderToolCall(dot + title, details, theme));
|
|
217
249
|
const extra = kind === "file" ? getArgumentDetails(name, args) : kind === "custom" && state.expanded ? args : {};
|
|
218
250
|
if (Object.keys(extra).length > 0) container.addChild(renderArguments(extra, theme));
|
|
219
|
-
return container;
|
|
251
|
+
return new WithinWidth(container);
|
|
220
252
|
}
|
|
221
253
|
|
|
222
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* A result body as code where it is code: numbered file text, or an edit's diff.
|
|
256
|
+
* `rail` is what each row starts with; the compact result rail unless given.
|
|
257
|
+
*/
|
|
223
258
|
function renderFileBody(
|
|
224
259
|
name: string,
|
|
225
260
|
args: ToolArgs,
|
|
@@ -228,20 +263,22 @@ function renderFileBody(
|
|
|
228
263
|
expanded: boolean,
|
|
229
264
|
theme: Theme,
|
|
230
265
|
isError: boolean,
|
|
266
|
+
rail?: string,
|
|
231
267
|
): Component | undefined {
|
|
232
|
-
|
|
268
|
+
const fallback = () => renderOutput(output, theme, isError, rail);
|
|
269
|
+
if (isError) return fallback();
|
|
233
270
|
const path = pathArgument(args);
|
|
234
271
|
if (name === "edit" && getEditDiff(result)) {
|
|
235
272
|
return renderCodeDiff(getEditPatch(result), getEditDiff(result), path, theme,
|
|
236
|
-
expanded ? {} : { contextLines: PREVIEW_DIFF_CONTEXT_LINES }) ??
|
|
273
|
+
expanded ? { rail } : { contextLines: PREVIEW_DIFF_CONTEXT_LINES, rail }) ?? fallback();
|
|
237
274
|
}
|
|
238
275
|
if (name === "read" && isReadTextResult(result)) {
|
|
239
276
|
const { body, footer } = splitReadFooter(output);
|
|
240
277
|
const startLine = typeof args.offset === "number" ? args.offset : 1;
|
|
241
|
-
return renderCodeView(body, path, theme, { startLine, footer }) ??
|
|
278
|
+
return renderCodeView(body, path, theme, { startLine, footer, rail }) ?? fallback();
|
|
242
279
|
}
|
|
243
|
-
if (name === "write") return renderCodeView(output, path, theme) ??
|
|
244
|
-
return
|
|
280
|
+
if (name === "write") return renderCodeView(output, path, theme, { rail }) ?? fallback();
|
|
281
|
+
return fallback();
|
|
245
282
|
}
|
|
246
283
|
|
|
247
284
|
function renderRowBody(
|
|
@@ -253,17 +290,18 @@ function renderRowBody(
|
|
|
253
290
|
theme: Theme,
|
|
254
291
|
isError: boolean,
|
|
255
292
|
author: AuthorResultRenderer | undefined,
|
|
293
|
+
rail?: string,
|
|
256
294
|
): Component | undefined {
|
|
257
|
-
if (rowKind(name) !== "custom") return renderFileBody(name, args, result, output, expanded, theme, isError);
|
|
295
|
+
if (rowKind(name) !== "custom") return renderFileBody(name, args, result, output, expanded, theme, isError, rail);
|
|
258
296
|
if (author) {
|
|
259
297
|
try {
|
|
260
298
|
const component = author(expanded);
|
|
261
|
-
if (component) return railComponent(component, theme);
|
|
299
|
+
if (component) return railComponent(component, theme, rail);
|
|
262
300
|
} catch {
|
|
263
301
|
// A failing third-party renderer degrades to the plain text result.
|
|
264
302
|
}
|
|
265
303
|
}
|
|
266
|
-
return renderOutput(output, theme, isError);
|
|
304
|
+
return renderOutput(output, theme, isError, rail);
|
|
267
305
|
}
|
|
268
306
|
|
|
269
307
|
function claudeMoreLines(theme: Theme, hidden: number): string {
|
|
@@ -309,12 +347,12 @@ function claudeBody(
|
|
|
309
347
|
// A command's output is already the result line itself.
|
|
310
348
|
const kind = rowKind(name);
|
|
311
349
|
if (!expanded || kind === "shell") return undefined;
|
|
312
|
-
if (kind === "custom") return author ? renderRowBody(name, args, result, output, true, theme, false, author) : undefined;
|
|
313
|
-
return renderFileBody(name, args, result, output, true, theme, false);
|
|
350
|
+
if (kind === "custom") return author ? renderRowBody(name, args, result, output, true, theme, false, author, CLAUDE_INDENT) : undefined;
|
|
351
|
+
return renderFileBody(name, args, result, output, true, theme, false, CLAUDE_INDENT);
|
|
314
352
|
}
|
|
315
353
|
|
|
316
354
|
/**
|
|
317
|
-
* Claude Code's result block: a "
|
|
355
|
+
* Claude Code's result block: a "⎿" line saying what happened, or the output
|
|
318
356
|
* itself for commands and custom tools, then any diff or code beneath it.
|
|
319
357
|
*/
|
|
320
358
|
function renderClaudeResult(
|
|
@@ -331,7 +369,7 @@ function renderClaudeResult(
|
|
|
331
369
|
const chrome = chromePainter(theme);
|
|
332
370
|
const container = new CachedContainer();
|
|
333
371
|
// One line under the call, wrapped rather than cut to the row.
|
|
334
|
-
const line = (text: string) =>
|
|
372
|
+
const line = (text: string) => prefixedSentence(text, chrome(CLAUDE_RESULT_PREFIX), CLAUDE_INDENT);
|
|
335
373
|
if (options.isPartial) {
|
|
336
374
|
container.addChild(line(chrome("Running…")));
|
|
337
375
|
return container;
|
|
@@ -344,7 +382,7 @@ function renderClaudeResult(
|
|
|
344
382
|
container.addChild(line(theme.fg("error", failure.headline)));
|
|
345
383
|
container.addChild(claudeOutput(failure.detail, expanded, theme, true, "", CLAUDE_INDENT));
|
|
346
384
|
} else if (expanded && output.trim()) {
|
|
347
|
-
container.addChild(claudeOutput(`Error: ${output.trim()}`, true, theme, true, "", chrome(
|
|
385
|
+
container.addChild(claudeOutput(`Error: ${output.trim()}`, true, theme, true, "", chrome(CLAUDE_RESULT_PREFIX)));
|
|
348
386
|
} else {
|
|
349
387
|
container.addChild(line(theme.fg("error", failure.headline)));
|
|
350
388
|
}
|
|
@@ -356,7 +394,7 @@ function renderClaudeResult(
|
|
|
356
394
|
? formatResultLineSummary(name, args, result, output) ?? "Done"
|
|
357
395
|
: claudeOutcome(name, args, result, output, (value) => theme.bold(value));
|
|
358
396
|
if (outcome !== undefined) container.addChild(line(theme.fg("toolOutput", outcome)));
|
|
359
|
-
else container.addChild(claudeOutput(output, expanded, theme, false, rowKind(name) === "shell" ? "(No output)" : "(No content)", chrome(
|
|
397
|
+
else container.addChild(claudeOutput(output, expanded, theme, false, rowKind(name) === "shell" ? "(No output)" : "(No content)", chrome(CLAUDE_RESULT_PREFIX)));
|
|
360
398
|
if (body) container.addChild(body);
|
|
361
399
|
return container;
|
|
362
400
|
}
|
|
@@ -376,14 +414,19 @@ function renderRowResult(
|
|
|
376
414
|
): Component {
|
|
377
415
|
const state = runtime.syncRow(ctx, !options.isPartial);
|
|
378
416
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
379
|
-
if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
|
|
417
|
+
if (canReuseResult(state, result, options, theme, ctx)) return ctx.lastComponent;
|
|
380
418
|
const kind = rowKind(name);
|
|
381
419
|
const output = kind === "file" ? getFileOutput(name, ctx.args, result, ctx.isError) : getTextResult(result);
|
|
382
420
|
const hasEditDiff = name === "edit" && getEditDiff(result).length > 0;
|
|
383
421
|
runtime.setResultAvailable(state, name, hasEditDiff || output.length > 0 || author !== undefined);
|
|
384
422
|
let component: Component;
|
|
385
|
-
if (
|
|
386
|
-
component =
|
|
423
|
+
if (isCodex()) {
|
|
424
|
+
component = renderCodexResult({
|
|
425
|
+
name, kind, args: ctx.args, result, output, theme, state, isError: ctx.isError,
|
|
426
|
+
isPartial: options.isPartial, expanded: state.expanded === true, toolCallId: ctx.toolCallId, author,
|
|
427
|
+
});
|
|
428
|
+
} else if (isClaude()) {
|
|
429
|
+
component = new WithinWidth(renderClaudeResult(name, ctx.args, result, options, theme, ctx.isError, state.expanded === true, output, author));
|
|
387
430
|
} else {
|
|
388
431
|
if (!options.isPartial && !state.resultLineSummaryComputed) {
|
|
389
432
|
// A custom tool with no text has nothing to count.
|
|
@@ -400,9 +443,9 @@ function renderRowResult(
|
|
|
400
443
|
const failureReason = ctx.isError && !state.expanded && !state.preview ? summarizeFailure(name, output) : undefined;
|
|
401
444
|
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError, failureReason,
|
|
402
445
|
name === "edit" ? countEditChanges(result) : undefined));
|
|
403
|
-
component = container;
|
|
446
|
+
component = new WithinWidth(container);
|
|
404
447
|
}
|
|
405
|
-
rememberResult(state, result, options, ctx.isError);
|
|
448
|
+
rememberResult(state, result, options, ctx.isError, theme);
|
|
406
449
|
return component;
|
|
407
450
|
}
|
|
408
451
|
|
|
@@ -491,6 +534,28 @@ function configure(pi: ExtensionAPI, cwd?: string, projectTrusted = false): void
|
|
|
491
534
|
registeredConfiguration = signature;
|
|
492
535
|
}
|
|
493
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Apply a style saved by /compact-tools without reloading Pi. The compact, Claude, and
|
|
539
|
+
* Codex styles share every tool registration and read the style as they draw, so the
|
|
540
|
+
* configuration is swapped and each tool row on screen is drawn again, a running one
|
|
541
|
+
* included. Anything else that changed, or a switch to or from off, needs a reload.
|
|
542
|
+
*/
|
|
543
|
+
function applyStyle(cwd: string, trusted: boolean): boolean {
|
|
544
|
+
const config = loadConfig(cwd, trusted);
|
|
545
|
+
const current = runtime.config;
|
|
546
|
+
if (config.style === "off" || current.style === "off") return false;
|
|
547
|
+
if (JSON.stringify({ ...config, style: current.style }) !== JSON.stringify(current)) return false;
|
|
548
|
+
runtime.configure(config);
|
|
549
|
+
registeredConfiguration = JSON.stringify([cwd, config]);
|
|
550
|
+
const chat = boundTui ? findChat(boundTui) : undefined;
|
|
551
|
+
for (const child of chat?.children ?? []) {
|
|
552
|
+
// Pi asks a row's renderers again whenever it redraws the row; this is that redraw.
|
|
553
|
+
if (isKind(child, ToolExecutionComponent)) (child as { updateDisplay?(): void }).updateDisplay?.();
|
|
554
|
+
}
|
|
555
|
+
boundTui?.requestRender();
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
494
559
|
/** Note the calls an assistant message makes, as Pi reads them to make their rows. */
|
|
495
560
|
function noteCalls(message: unknown): void {
|
|
496
561
|
// Only a run's own updates announce calls; the check is cheap enough for every update.
|
|
@@ -511,15 +576,20 @@ function madeByTool(event: object): boolean {
|
|
|
511
576
|
return typeof (event as { parentToolCallId?: unknown }).parentToolCallId === "string";
|
|
512
577
|
}
|
|
513
578
|
|
|
579
|
+
/** Pi's TUI while one is bound: where a style switched in place finds the rows to draw again. */
|
|
580
|
+
let boundTui: { requestRender(): void } | undefined;
|
|
581
|
+
|
|
514
582
|
/** Hand the animation Pi's frame request, reached through an invisible widget as the extension API allows. */
|
|
515
583
|
function bindRenderer(ctx: ExtensionContext): void {
|
|
516
584
|
ctx.ui.setWidget(RENDER_WIDGET_KEY, (tui) => {
|
|
585
|
+
boundTui = tui;
|
|
517
586
|
runtime.bindRenderer(() => tui.requestRender());
|
|
518
587
|
return { render: () => [], invalidate() {} };
|
|
519
588
|
}, { placement: "belowEditor" });
|
|
520
589
|
}
|
|
521
590
|
|
|
522
591
|
function unbindRenderer(ctx: ExtensionContext | undefined): void {
|
|
592
|
+
boundTui = undefined;
|
|
523
593
|
runtime.bindRenderer(undefined);
|
|
524
594
|
ctx?.ui.setWidget(RENDER_WIDGET_KEY, undefined, { placement: "belowEditor" });
|
|
525
595
|
}
|
|
@@ -533,6 +603,7 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
533
603
|
registeredConfiguration = undefined;
|
|
534
604
|
registeredTools.clear();
|
|
535
605
|
|
|
606
|
+
registerCompactToolsCommand(pi, () => runtime.config, applyStyle);
|
|
536
607
|
const thinkingCycle = new ThinkingCycleController(pi);
|
|
537
608
|
const progress = new ProgressController(pi);
|
|
538
609
|
const viewport = new ViewportKeeper();
|
|
@@ -544,12 +615,16 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
544
615
|
let renderContext: ExtensionContext | undefined;
|
|
545
616
|
// Register once while the extension runtime is being built. In particular, this
|
|
546
617
|
// makes the overrides available before Pi restores the active tool set on /reload.
|
|
547
|
-
|
|
618
|
+
const cwd = process.cwd();
|
|
619
|
+
const lastTrust = (globalThis as TrustSlot)[TRUST_KEY];
|
|
620
|
+
configure(pi, cwd, lastTrust?.cwd === realDirectory(cwd) && lastTrust.trusted);
|
|
548
621
|
pi.on("session_start", (event, ctx) => {
|
|
549
622
|
if (event.reason !== "reload") runtime.clearTimings();
|
|
550
623
|
// Rows restored from the session are drawn next; while idle none of them is running.
|
|
551
624
|
if (typeof ctx.isIdle === "function") runtime.setBusy(!ctx.isIdle());
|
|
552
|
-
|
|
625
|
+
const trusted = ctx.isProjectTrusted();
|
|
626
|
+
(globalThis as TrustSlot)[TRUST_KEY] = { cwd: realDirectory(ctx.cwd), trusted };
|
|
627
|
+
configure(pi, ctx.cwd, trusted);
|
|
553
628
|
unbindRenderer(renderContext);
|
|
554
629
|
renderContext = undefined;
|
|
555
630
|
if (ctx.mode === "tui" && runtime.config.style !== "off") {
|
|
@@ -597,5 +672,7 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
597
672
|
unbindRenderer(renderContext);
|
|
598
673
|
renderContext = undefined;
|
|
599
674
|
runtime.reset(event.reason !== "reload");
|
|
675
|
+
// A reload that keeps the extension sets it again before Pi rebuilds the transcript.
|
|
676
|
+
clearRowResolver(resolveCustomRow);
|
|
600
677
|
});
|
|
601
678
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-compact-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.14.0",
|
|
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",
|
|
7
7
|
"author": "Dong Hyeong Lee",
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"pi-package",
|
|
18
18
|
"pi-coding-agent",
|
|
19
19
|
"pi-extension",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"codex",
|
|
20
22
|
"terminal-ui",
|
|
21
23
|
"theme"
|
|
22
24
|
],
|
|
@@ -24,14 +26,13 @@
|
|
|
24
26
|
"extensions",
|
|
25
27
|
"themes",
|
|
26
28
|
"examples",
|
|
27
|
-
"scripts",
|
|
28
29
|
"schemas",
|
|
29
30
|
"release-notes.json",
|
|
30
31
|
"README.md",
|
|
31
32
|
"LICENSE"
|
|
32
33
|
],
|
|
33
34
|
"scripts": {
|
|
34
|
-
"test": "tsx --test tests/*.test.ts",
|
|
35
|
+
"test": "tsx --test tests/*.test.ts && tsx --test tests/resources.validation.ts",
|
|
35
36
|
"check": "tsc --noEmit && npm test && npm run check:resources",
|
|
36
37
|
"check:resources": "node scripts/check-resources.mjs",
|
|
37
38
|
"prepublishOnly": "npm run check"
|
|
@@ -50,9 +51,12 @@
|
|
|
50
51
|
"@earendil-works/pi-tui": ">=0.85.1"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
54
|
-
"@earendil-works/pi-tui": "0.
|
|
54
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
55
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
56
|
+
"@sinonjs/fake-timers": "15.0.0",
|
|
55
57
|
"@types/node": "^24.0.0",
|
|
58
|
+
"@types/sinonjs__fake-timers": "15.0.1",
|
|
59
|
+
"ajv": "8.20.0",
|
|
56
60
|
"tsx": "^4.23.13",
|
|
57
61
|
"typescript": "^5.9.0"
|
|
58
62
|
},
|
package/release-notes.json
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
+
"0.14.0": {
|
|
3
|
+
"New": [
|
|
4
|
+
"Codex style added.",
|
|
5
|
+
"`/compact-tools` command added. Try `/compact-tools codex`."
|
|
6
|
+
],
|
|
7
|
+
"Fix": [
|
|
8
|
+
"Commands that write or run something, such as `tee`, `xargs`, or `find -delete`, keep their own row instead of folding into a group",
|
|
9
|
+
"`/compact-tools codex` without a scope saves where the style is decided, so a switch always takes effect",
|
|
10
|
+
"`/compact-tools` and `/silent` run on the first Enter",
|
|
11
|
+
"A trusted project's tools apply right after a style switch",
|
|
12
|
+
"Fixes for narrow panes, long error messages, and pipelines in Compact summaries"
|
|
13
|
+
],
|
|
14
|
+
"Deprecated": []
|
|
15
|
+
},
|
|
16
|
+
"0.13.2": [
|
|
17
|
+
"Verified on Pi 1.0.0 while keeping compatibility with Pi 0.85.1 and later",
|
|
18
|
+
"Silent activity keeps animating through retries and follow-up turns until final settlement",
|
|
19
|
+
"Hidden file results defer syntax highlighting until their code is actually shown",
|
|
20
|
+
"Completed tool results repaint correctly when the theme changes",
|
|
21
|
+
"Large file views render without exceeding JavaScript argument limits",
|
|
22
|
+
"Fullscreen viewport anchoring and code-row layout allocate less work",
|
|
23
|
+
"Execution timing history stays bounded even when tool rows are not drawn"
|
|
24
|
+
],
|
|
2
25
|
"0.13.1": [],
|
|
3
26
|
"0.13.0": [
|
|
4
27
|
"Follows Pi 0.99's system theme and light terminals: rails, dots, and the working label take their colors from the theme itself",
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"type": "string"
|
|
10
10
|
},
|
|
11
11
|
"style": {
|
|
12
|
-
"description": "How tool rows are drawn. compact draws each call as a compact, expandable row. claude draws them the way Claude Code does: runs of reads, searches, listings, and commands that only look around fold into one summary line, while other commands, edits, writes, and custom tools keep rows of their own with Bash(…), Update(…), and similar titles; click a group or press Ctrl+O to open it. off leaves every tool with Pi's own renderer.",
|
|
12
|
+
"description": "How tool rows are drawn. compact draws each call as a compact, expandable row. claude draws them the way Claude Code does: runs of reads, searches, listings, and commands that only look around fold into one summary line, while other commands, edits, writes, and custom tools keep rows of their own with Bash(…), Update(…), and similar titles; click a group or press Ctrl+O to open it. codex draws them the way Codex CLI does: calls that only look around fold into a • Explored group, commands read Ran or Failed (exit N) with their last three output lines, edits preview their changed lines, and custom tools read • Called; Ctrl+O or a click opens each one. off leaves every tool with Pi's own renderer.",
|
|
13
13
|
"type": "string",
|
|
14
|
-
"enum": ["compact", "claude", "off"],
|
|
14
|
+
"enum": ["compact", "claude", "codex", "off"],
|
|
15
15
|
"default": "compact"
|
|
16
16
|
},
|
|
17
17
|
"mode": {
|
|
18
|
-
"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, in
|
|
18
|
+
"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, in any style.",
|
|
19
19
|
"type": "string",
|
|
20
20
|
"enum": ["normal", "silent"],
|
|
21
21
|
"default": "normal"
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
const resources = [
|
|
4
|
-
"themes/github-dark-pro.json",
|
|
5
|
-
"examples/compact-tools.json",
|
|
6
|
-
"examples/compact-tools-2.json",
|
|
7
|
-
"schemas/compact-tools.schema.json",
|
|
8
|
-
"release-notes.json",
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
for (const path of resources) {
|
|
12
|
-
const value = JSON.parse(await readFile(new URL(`../${path}`, import.meta.url), "utf8"));
|
|
13
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
-
throw new Error(`${path} must contain a JSON object`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const theme = JSON.parse(
|
|
19
|
-
await readFile(new URL("../themes/github-dark-pro.json", import.meta.url), "utf8"),
|
|
20
|
-
);
|
|
21
|
-
if (theme.name !== "github-dark-pro" || !theme.colors || typeof theme.colors !== "object") {
|
|
22
|
-
throw new Error("themes/github-dark-pro.json is not a valid pi theme document");
|
|
23
|
-
}
|
|
24
|
-
if (!theme.colors.border || !theme.colors.dim || !theme.colors.muted) {
|
|
25
|
-
throw new Error("github-dark-pro must define separator colors");
|
|
26
|
-
}
|
|
27
|
-
if (!theme.vars?.thinkingText || !theme.colors.thinkingMax) {
|
|
28
|
-
throw new Error("github-dark-pro must define thinking text and summary colors");
|
|
29
|
-
}
|
|
30
|
-
if (!theme.colors.mdQuote || !theme.colors.mdCodeBlockBorder) {
|
|
31
|
-
throw new Error("github-dark-pro must define thinking detail and code fence colors");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const example = JSON.parse(
|
|
35
|
-
await readFile(new URL("../examples/compact-tools.json", import.meta.url), "utf8"),
|
|
36
|
-
);
|
|
37
|
-
if (!example.auto_compact || example.auto_compact.edit !== false) {
|
|
38
|
-
throw new Error("example auto_compact policy must keep edit diffs visible by default");
|
|
39
|
-
}
|
|
40
|
-
if (Object.values(example.auto_compact).some((enabled) => typeof enabled !== "boolean")) {
|
|
41
|
-
throw new Error("example auto_compact values must be booleans");
|
|
42
|
-
}
|
|
43
|
-
if (!Number.isInteger(example.previewLines) || example.previewLines < 1 || example.previewLines > 100) {
|
|
44
|
-
throw new Error("example previewLines must be an integer from 1 to 100");
|
|
45
|
-
}
|
|
46
|
-
const alternateExample = JSON.parse(
|
|
47
|
-
await readFile(new URL("../examples/compact-tools-2.json", import.meta.url), "utf8"),
|
|
48
|
-
);
|
|
49
|
-
for (const [name, config] of [["compact-tools", example], ["compact-tools-2", alternateExample]]) {
|
|
50
|
-
if ("durationIndicators" in config) {
|
|
51
|
-
throw new Error(`${name} must not contain the removed durationIndicators setting`);
|
|
52
|
-
}
|
|
53
|
-
if ("spinner" in config) {
|
|
54
|
-
throw new Error(`${name} must not contain the removed spinner setting`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const schema = JSON.parse(
|
|
59
|
-
await readFile(new URL("../schemas/compact-tools.schema.json", import.meta.url), "utf8"),
|
|
60
|
-
);
|
|
61
|
-
if (schema.properties?.durationIndicators) {
|
|
62
|
-
throw new Error("schema must not expose the removed durationIndicators setting");
|
|
63
|
-
}
|
|
64
|
-
if (schema.properties?.spinner) {
|
|
65
|
-
throw new Error("schema must not expose the removed spinner setting");
|
|
66
|
-
}
|
|
67
|
-
if (!schema.properties?.auto_compact?.properties?.edit) {
|
|
68
|
-
throw new Error("schema must describe per-tool auto_compact policies");
|
|
69
|
-
}
|
|
70
|
-
if (schema.properties?.previewLines?.default !== 10) {
|
|
71
|
-
throw new Error("schema must expose the bounded previewLines setting");
|
|
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
|
-
|
|
82
|
-
console.log("Resource checks passed");
|