pi-shorthand 0.11.0 → 0.13.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 +2 -0
- package/package.json +2 -2
- package/src/display.ts +9 -11
- package/src/index.ts +90 -7
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ Programs edit a private workspace, with host files outside the repository kept r
|
|
|
40
40
|
|
|
41
41
|
The `code` tool uses Pi's working directory by default. Pass `cwd` to target another checkout; relative paths are resolved from Pi's working directory. For example, when Pi starts in a bare worktree container, `cwd: "child"` targets its `child` worktree. The chosen directory must be inside a Git worktree.
|
|
42
42
|
|
|
43
|
+
Scripts run by Pi's `codemode` tool can call `code` too. They get back what the run changed as data, not text.
|
|
44
|
+
|
|
43
45
|
## Without Pi
|
|
44
46
|
|
|
45
47
|
The engine is published separately as [shorthand-code](https://github.com/sebinsua/shortsight/tree/main/packages/shorthand-code), with a `shorthand` command that runs a program from a file or stdin and prints the same result. See `shorthand --help`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-shorthand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Pi tool for editing repositories with Bun programs, with first-class support for TypeScript and JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast-grep",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"type": "module",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"shorthand-code": "0.
|
|
29
|
+
"shorthand-code": "0.13.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@earendil-works/pi-ai": "*",
|
package/src/display.ts
CHANGED
|
@@ -25,8 +25,7 @@ const ANSI_CUBE_VALUES = [0, 95, 135, 175, 215, 255];
|
|
|
25
25
|
const ANSI_GRAY_VALUES = Array.from({ length: 24 }, (_, index) => 8 + index * 10);
|
|
26
26
|
|
|
27
27
|
const OUTPUT_PREVIEW_LINES = 5; // like Pi's bash tool
|
|
28
|
-
const
|
|
29
|
-
const LISTED_FILES = 8; // …showing this many, then "and N more files"
|
|
28
|
+
const LISTED_FILES = 8; // a diff too tall for the screen lists this many files, then "and N more files"
|
|
30
29
|
const EXPANDED_DIFF_LINES = 2000; // even expanded, a diff of hundreds of files stops here
|
|
31
30
|
|
|
32
31
|
export function callLine(
|
|
@@ -60,7 +59,8 @@ function indent(line: string): string {
|
|
|
60
59
|
return ` ${line}`;
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
/** `collapseDiff` swaps the applied diff for a list of its files; see FitsScreen for when. */
|
|
63
|
+
export function resultLines(run: RunResult, expanded: boolean, theme: Theme, collapseDiff = false): string[] {
|
|
64
64
|
const applied = run.changes.filter((change) => run.applied.includes(change.path));
|
|
65
65
|
const notApplied = run.changes.filter(
|
|
66
66
|
(change) => !run.applied.includes(change.path) && !run.rolledBack.includes(change.path),
|
|
@@ -95,7 +95,7 @@ export function resultLines(run: RunResult, expanded: boolean, theme: Theme): st
|
|
|
95
95
|
const sections: string[][] = [];
|
|
96
96
|
const background: ToolBackground =
|
|
97
97
|
run.exitCode === 0 && run.conflicts.length === 0 && run.rolledBack.length === 0 ? "toolSuccessBg" : "toolErrorBg";
|
|
98
|
-
if (applied.length > 0) sections.push(diffLines(applied, expanded, theme, background));
|
|
98
|
+
if (applied.length > 0) sections.push(diffLines(applied, collapseDiff && !expanded, theme, background));
|
|
99
99
|
if (output && (expanded || !error)) sections.push(outputLines(output, expanded, run.changes.length > 0, theme));
|
|
100
100
|
if (notApplied.length > 0) sections.push(notAppliedLines(notApplied, expanded, theme, background));
|
|
101
101
|
for (const section of sections) lines.push("", ...section);
|
|
@@ -157,14 +157,12 @@ function outputLines(output: string, expanded: boolean, labelled: boolean, theme
|
|
|
157
157
|
return [...label, hint, ...lines.slice(-OUTPUT_PREVIEW_LINES)];
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
/** Each file's diff under its name, in Pi's own diff style
|
|
161
|
-
function diffLines(changes: FileChange[],
|
|
162
|
-
|
|
163
|
-
const total = files.reduce((sum, file) => sum + file.length, 0);
|
|
164
|
-
if (!expanded && total > INLINE_DIFF_LINES) {
|
|
160
|
+
/** Each file's diff under its name, in Pi's own diff style, or when collapsed a list of the files. */
|
|
161
|
+
function diffLines(changes: FileChange[], collapse: boolean, theme: Theme, background: ToolBackground): string[] {
|
|
162
|
+
if (collapse) {
|
|
165
163
|
return [...fileList(changes, theme), theme.fg("muted", `(${keyHint("app.tools.expand", "to see the diff")})`)];
|
|
166
164
|
}
|
|
167
|
-
|
|
165
|
+
const files = changes.map((change) => [fileLine(change, theme), ...renderFileDiff(change, theme, background)]);
|
|
168
166
|
const lines = files.flatMap((file, index) => (index === 0 ? file : ["", ...file]));
|
|
169
167
|
if (lines.length <= EXPANDED_DIFF_LINES) return lines;
|
|
170
168
|
const more = lines.length - EXPANDED_DIFF_LINES;
|
|
@@ -316,7 +314,7 @@ function colorDistance(first: number[], second: number[]): number {
|
|
|
316
314
|
|
|
317
315
|
function notAppliedLines(changes: FileChange[], expanded: boolean, theme: Theme, background: ToolBackground): string[] {
|
|
318
316
|
const heading = theme.fg("muted", `Would have changed ${fileCount(changes)} · `) + stats(changes, theme);
|
|
319
|
-
if (expanded) return [heading, "", ...diffLines(changes,
|
|
317
|
+
if (expanded) return [heading, "", ...diffLines(changes, false, theme, background)];
|
|
320
318
|
return [heading + theme.fg("muted", ` (${keyHint("app.tools.expand", "to see the diff")})`)];
|
|
321
319
|
}
|
|
322
320
|
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
9
9
|
import { type ExtensionAPI, type Theme, truncateHead, truncateTail } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
10
|
+
import { type Component, Text } from "@earendil-works/pi-tui";
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_TIMEOUT_SECONDS,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "shorthand-code";
|
|
23
23
|
import { callLine, resultLines, unstructuredResultText } from "./display.ts";
|
|
24
24
|
|
|
25
|
-
const DESCRIPTION = `Edit repository files with a TypeScript program run by Bun. Best for changes across many files, repeated edits and semantic TypeScript renames or moves; a small change to one file is quicker as a direct edit. Top-level await and ordinary Bun/Node APIs work. Use repository-relative paths. Set cwd to a checkout path when Pi's working directory is outside the repository, such as a child worktree in a bare worktree container. The program runs in an isolated workspace; changes apply on successful exit by default and the tool reports the diff.
|
|
25
|
+
const DESCRIPTION = `Edit repository files with a TypeScript program run by Bun. Best for changes across many files, repeated edits and semantic TypeScript renames or moves; a small change to one file is quicker as a direct edit. Top-level await and ordinary Bun/Node APIs work. Use repository-relative paths. Set cwd to a checkout path when Pi's working directory is outside the repository, such as a child worktree in a bare worktree container. The program runs in an isolated workspace; changes apply on successful exit by default and the tool reports the diff. Tests, type-checks and builds belong outside the program: in a shell call, or, when a codemode script calls code, later in that script.
|
|
26
26
|
|
|
27
27
|
Common operations:
|
|
28
28
|
- edit({ path, oldText, newText }) replaces exactly one literal occurrence; missing or ambiguous text is an error. Use text edits for known source, structural matching when it saves enumerating occurrences or preserves varying syntax.
|
|
@@ -35,7 +35,7 @@ Common operations:
|
|
|
35
35
|
- await refactor.renameFile({ from, to }) moves a TypeScript file and updates module paths that resolve to it.
|
|
36
36
|
- await refactor.move({ file, symbol, to }) moves a top-level declaration to another file and updates the imports that follow it.
|
|
37
37
|
|
|
38
|
-
See the shorthand skill for renames, moves and call-site migrations. Read its advanced-refactors.md guide only to extract code, move syntax or use rule objects. The default timeout is
|
|
38
|
+
See the shorthand skill for renames, moves and call-site migrations. Read its advanced-refactors.md guide only to extract code, move syntax or use rule objects. The default timeout is five seconds; request more for longer programs. Time spent inside the helpers above does not count toward it, up to 60 extra seconds.`;
|
|
39
39
|
|
|
40
40
|
const GRAPH_DESCRIPTION =
|
|
41
41
|
"- await graph.query(request | request[]) queries symbols and relationships; graph results can be passed to sg as scope. The graph shows the repository before this program's edits.\n";
|
|
@@ -51,6 +51,59 @@ export function codeDescription(graphAvailable: boolean): string {
|
|
|
51
51
|
: DESCRIPTION;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const paths = (description: string) => Type.Array(Type.String(), { description });
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What a script gets back from `code`, for example one run by Pi's codemode tool. The model still sees the text.
|
|
58
|
+
* A failed run resolves to this too, so a script checks `exitCode`.
|
|
59
|
+
*/
|
|
60
|
+
export const CODE_OUTPUT_SCHEMA = Type.Object({
|
|
61
|
+
exitCode: Type.Optional(
|
|
62
|
+
Type.Union([Type.Number(), Type.Null()], { description: "0 on success; null when the program was killed" }),
|
|
63
|
+
),
|
|
64
|
+
timedOut: Type.Optional(Type.Boolean()),
|
|
65
|
+
output: Type.Optional(Type.String({ description: "The program's stdout and stderr" })),
|
|
66
|
+
errorLine: Type.Optional(Type.String({ description: "On failure, the program line the error came from" })),
|
|
67
|
+
warnings: Type.Optional(Type.Array(Type.String(), { description: "Likely mistakes spotted before it ran" })),
|
|
68
|
+
changes: Type.Optional(
|
|
69
|
+
Type.Array(
|
|
70
|
+
Type.Object({
|
|
71
|
+
path: Type.String(),
|
|
72
|
+
kind: StringEnum(["added", "modified", "deleted"] as const),
|
|
73
|
+
patch: Type.String(),
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
applied: Type.Optional(paths("Changed files written to the repository")),
|
|
78
|
+
conflicts: Type.Optional(paths("Files that changed outside the run while it ran; if any, nothing was applied")),
|
|
79
|
+
rolledBack: Type.Optional(paths("Changed files not kept after a failure")),
|
|
80
|
+
infrastructureError: Type.Optional(Type.String({ description: "Set, alone, when the program couldn't be run" })),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export function codeOutput(result: RunResult) {
|
|
84
|
+
const { exitCode, timedOut, output, errorLine, warnings, applied, conflicts, rolledBack } = result;
|
|
85
|
+
const changes = result.changes.map((change) => ({ path: change.path, kind: change.kind, patch: change.patch }));
|
|
86
|
+
return {
|
|
87
|
+
exitCode,
|
|
88
|
+
timedOut,
|
|
89
|
+
output,
|
|
90
|
+
...(errorLine === undefined ? {} : { errorLine }),
|
|
91
|
+
warnings,
|
|
92
|
+
changes,
|
|
93
|
+
applied,
|
|
94
|
+
conflicts,
|
|
95
|
+
rolledBack,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Base name of the temporary files that hold a large run's full diff or output. A call a codemode script makes has
|
|
101
|
+
* an id like `<parent id>/1`, and ids can hold `|`, so anything but a safe file-name character is replaced.
|
|
102
|
+
*/
|
|
103
|
+
export function spillName(toolCallId: string): string {
|
|
104
|
+
return `pi-shorthand-${toolCallId.replace(/[^\w.-]/g, "_")}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
54
107
|
export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknown> = sightreadAvailable) {
|
|
55
108
|
// The shorthand skill ships with shorthand-code, whose `shorthand --skill` prints it for other agents.
|
|
56
109
|
pi.on("resources_discover", () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
@@ -67,7 +120,7 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
67
120
|
label: "Code",
|
|
68
121
|
description: codeDescription(graphAvailable),
|
|
69
122
|
promptSnippet:
|
|
70
|
-
"Make multi-file, repetitive or rename/move changes with one Bun program; a small change to one file is quicker as a direct edit.
|
|
123
|
+
"Make multi-file, repetitive or rename/move changes with one Bun program; a small change to one file is quicker as a direct edit. Keep verification outside the program",
|
|
71
124
|
|
|
72
125
|
parameters: Type.Object({
|
|
73
126
|
title: Type.String({ description: "A few words describing the change, shown to the user" }),
|
|
@@ -90,6 +143,7 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
90
143
|
}),
|
|
91
144
|
),
|
|
92
145
|
}),
|
|
146
|
+
outputSchema: CODE_OUTPUT_SCHEMA,
|
|
93
147
|
|
|
94
148
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
95
149
|
// While it runs, show how long it's been going and the latest reported step.
|
|
@@ -122,6 +176,7 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
122
176
|
isError: true,
|
|
123
177
|
content: [{ type: "text" as const, text: [error.message, ...diagnosticLines(error.diagnostics)].join("\n") }],
|
|
124
178
|
details: { infrastructureError: error.message, diagnostics: error.diagnostics },
|
|
179
|
+
structuredContent: { infrastructureError: error.message },
|
|
125
180
|
};
|
|
126
181
|
} finally {
|
|
127
182
|
clearInterval(progress);
|
|
@@ -130,10 +185,11 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
130
185
|
content: [
|
|
131
186
|
{
|
|
132
187
|
type: "text",
|
|
133
|
-
text: textForModel(result, { name:
|
|
188
|
+
text: textForModel(result, { name: spillName(toolCallId), truncateHead, truncateTail }),
|
|
134
189
|
},
|
|
135
190
|
],
|
|
136
191
|
details: result,
|
|
192
|
+
structuredContent: codeOutput(result),
|
|
137
193
|
};
|
|
138
194
|
},
|
|
139
195
|
|
|
@@ -151,7 +207,7 @@ export function renderCodeResult(
|
|
|
151
207
|
result: { content: readonly unknown[]; details?: unknown },
|
|
152
208
|
{ expanded, isPartial }: { expanded: boolean; isPartial: boolean },
|
|
153
209
|
theme: Theme,
|
|
154
|
-
):
|
|
210
|
+
): Component {
|
|
155
211
|
if (isPartial) {
|
|
156
212
|
const progress = (result.details as { progress?: string } | undefined)?.progress;
|
|
157
213
|
return new Text(theme.fg("muted", progress ? `running… ${progress}` : "running…"), 0, 0);
|
|
@@ -170,5 +226,32 @@ export function renderCodeResult(
|
|
|
170
226
|
}
|
|
171
227
|
const run = result.details as RunResult | undefined;
|
|
172
228
|
if (!run) return new Text(theme.fg("error", unstructuredResultText(result.content)), 0, 0);
|
|
173
|
-
|
|
229
|
+
const full = new Text(resultLines(run, expanded, theme).join("\n"), 0, 0);
|
|
230
|
+
if (expanded) return full;
|
|
231
|
+
return new FitsScreen(full, new Text(resultLines(run, false, theme, true).join("\n"), 0, 0));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Rows Pi keeps for itself below a tool result: the editor, its borders and the footer. */
|
|
235
|
+
const PI_CHROME_ROWS = 8;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Shows the whole result while it fits on screen once wrapped, like Pi's edit tool, and otherwise the version
|
|
239
|
+
* with the diff collapsed to a list of files. Measured on each render, so resizing the terminal is followed.
|
|
240
|
+
*/
|
|
241
|
+
export class FitsScreen implements Component {
|
|
242
|
+
constructor(
|
|
243
|
+
private readonly full: Component,
|
|
244
|
+
private readonly collapsed: Component,
|
|
245
|
+
private readonly rows = () => process.stdout.rows || 24,
|
|
246
|
+
) {}
|
|
247
|
+
|
|
248
|
+
render(width: number): string[] {
|
|
249
|
+
const lines = this.full.render(width);
|
|
250
|
+
return lines.length <= this.rows() - PI_CHROME_ROWS ? lines : this.collapsed.render(width);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
invalidate(): void {
|
|
254
|
+
this.full.invalidate();
|
|
255
|
+
this.collapsed.invalidate();
|
|
256
|
+
}
|
|
174
257
|
}
|