@tt-a1i/openpi 0.3.0 → 0.4.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 +87 -24
- package/SETUP.md +3 -3
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/src/prompt.ts +1 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +35 -44
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +387 -88
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +18 -18
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +330 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/sessions/index.ts +70 -55
- package/extensions/setup/index.ts +6 -6
- package/extensions/shared/activity-status.ts +6 -5
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +7 -1
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +24 -5
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/text-projection.ts +56 -0
- package/extensions/shared/tool-surface.ts +13 -6
- package/extensions/subagents/index.ts +216 -170
- package/extensions/subagents/navigation.ts +52 -23
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backends/stub.ts +7 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +620 -537
- package/extensions/subagents/src/prompt.ts +153 -38
- package/extensions/subagents/src/result-artifact.ts +142 -0
- package/extensions/subagents/src/result-delivery.ts +50 -5
- package/extensions/subagents/src/runtime.ts +8 -5
- package/extensions/subagents/src/ui/takeover.ts +84 -109
- package/extensions/subagents/src/ui/transcript.ts +76 -42
- package/extensions/subagents/src/ui/wait-result.ts +1 -1
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/workflows/artifacts.ts +35 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +64 -0
- package/extensions/workflows/dashboard.ts +353 -173
- package/extensions/workflows/handoff.ts +62 -20
- package/extensions/workflows/index.ts +647 -387
- package/extensions/workflows/model.ts +57 -15
- package/extensions/workflows/navigation.ts +33 -14
- package/extensions/workflows/prompt.ts +104 -8
- package/extensions/workflows/replay-safety.ts +16 -6
- package/extensions/workflows/result-delivery.ts +189 -0
- package/extensions/workflows/sandbox-child.cjs +11 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +7 -4
- package/skills/workflows/SKILL.md +53 -10
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -3,21 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Resolution order (per tool, first usable wins):
|
|
5
5
|
* 1. A normally installed system binary (`fd`/`fdfind`, `rg`) — used silently.
|
|
6
|
-
* 2. An existing
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 2. An existing binary in the agent's managed bin directory
|
|
7
|
+
* (`~/.pi/agent/bin`), cached by an earlier download — used silently.
|
|
8
|
+
* 3. A fresh download of an official release into that directory — the only
|
|
9
|
+
* case that should surface a UI notification.
|
|
9
10
|
*
|
|
10
11
|
* The decision logic is an Effect over a small injectable environment
|
|
11
12
|
* (`BinaryEnv`) so tests can drive it without touching the filesystem or the
|
|
12
13
|
* network. `liveBinaryEnv` is the real implementation.
|
|
13
14
|
*/
|
|
14
15
|
|
|
16
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
17
|
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
|
|
16
18
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
17
19
|
import { execFile } from "node:child_process";
|
|
18
20
|
import { tmpdir } from "node:os";
|
|
19
21
|
import { dirname, join } from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
22
|
import { promisify } from "node:util";
|
|
22
23
|
import { Crypto, Data, Effect, Encoding, FileSystem, Stream } from "effect";
|
|
23
24
|
import { FetchHttpClient, HttpClient } from "effect/unstable/http";
|
|
@@ -56,13 +57,13 @@ const RG_SHA256: Readonly<Record<string, string>> = {
|
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
export type ToolName = "fd" | "rg";
|
|
59
|
-
export type BinarySource = "system" | "
|
|
60
|
+
export type BinarySource = "system" | "cached" | "installed";
|
|
60
61
|
|
|
61
62
|
export interface ToolSpec {
|
|
62
63
|
readonly tool: ToolName;
|
|
63
64
|
/** Commands probed on PATH, in order. Debian/Ubuntu install fd as `fdfind`. */
|
|
64
65
|
readonly systemCommands: readonly string[];
|
|
65
|
-
/** Executable name used inside release archives and the
|
|
66
|
+
/** Executable name used inside release archives and the managed bin directory. */
|
|
66
67
|
readonly binaryName: string;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -144,10 +145,9 @@ export function currentTarget(): PlatformTarget {
|
|
|
144
145
|
return { os: process.platform, arch: process.arch };
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
/**
|
|
148
|
-
export function
|
|
149
|
-
|
|
150
|
-
return join(moduleDir, "..", "..", "..", "bin");
|
|
148
|
+
/** Pi agent's managed binaries directory (`~/.pi/agent/bin`). */
|
|
149
|
+
export function managedBinDir() {
|
|
150
|
+
return join(getAgentDir(), "bin");
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
export class UnsupportedPlatformError extends Data.TaggedError(
|
|
@@ -179,7 +179,7 @@ export interface ResolvedBinary {
|
|
|
179
179
|
readonly version?: string;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/** Resolve one tool: system binary,
|
|
182
|
+
/** Resolve one tool: system binary, cached fallback, or fresh install. */
|
|
183
183
|
export function resolveBinary(
|
|
184
184
|
spec: ToolSpec,
|
|
185
185
|
binDir: string,
|
|
@@ -193,9 +193,9 @@ export function resolveBinary(
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
const
|
|
197
|
-
if (yield* env.probe(
|
|
198
|
-
return { tool: spec.tool, command:
|
|
196
|
+
const cached = join(binDir, spec.binaryName);
|
|
197
|
+
if (yield* env.probe(cached, spec.tool)) {
|
|
198
|
+
return { tool: spec.tool, command: cached, source: "cached" as const };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
const asset = releaseAsset(spec.tool, target);
|
|
@@ -205,17 +205,17 @@ export function resolveBinary(
|
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
yield* env.install(asset,
|
|
208
|
+
yield* env.install(asset, cached);
|
|
209
209
|
|
|
210
|
-
if (!(yield* env.probe(
|
|
210
|
+
if (!(yield* env.probe(cached, spec.tool))) {
|
|
211
211
|
return yield* new InstallError({
|
|
212
|
-
message: `${spec.tool} ${asset.version} was installed to ${
|
|
212
|
+
message: `${spec.tool} ${asset.version} was installed to ${cached} but failed to run.`,
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
return {
|
|
217
217
|
tool: spec.tool,
|
|
218
|
-
command:
|
|
218
|
+
command: cached,
|
|
219
219
|
source: "installed" as const,
|
|
220
220
|
version: asset.version,
|
|
221
221
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
visibleWidth,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import { Effect } from "effect";
|
|
10
|
+
import { hintLine } from "../../shared/screen-chrome.ts";
|
|
10
11
|
import { sanitizeTerminalText } from "../../shared/terminal-text.ts";
|
|
11
12
|
import { runCommand } from "./process.ts";
|
|
12
13
|
|
|
@@ -174,6 +175,15 @@ export async function showChangedFiles(
|
|
|
174
175
|
return Math.max(8, Math.floor(tui.terminal.rows * 0.8) - 2);
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Rows the diff pane actually paints: one row of the frame's budget goes
|
|
180
|
+
* to the hint line below it. Scroll limits must use this and not
|
|
181
|
+
* bodyHeight(), or the last diff line can never be scrolled into view.
|
|
182
|
+
*/
|
|
183
|
+
function visibleRows() {
|
|
184
|
+
return Math.max(4, bodyHeight() - 1);
|
|
185
|
+
}
|
|
186
|
+
|
|
177
187
|
function ensureSelectedFileVisible() {
|
|
178
188
|
const visibleFiles = Math.max(1, Math.floor(bodyHeight() / 2));
|
|
179
189
|
if (selectedIndex < sidebarOffset) sidebarOffset = selectedIndex;
|
|
@@ -192,7 +202,7 @@ export async function showChangedFiles(
|
|
|
192
202
|
function moveDiff(amount: number) {
|
|
193
203
|
const maxOffset = Math.max(
|
|
194
204
|
0,
|
|
195
|
-
files[selectedIndex]!.diff.length -
|
|
205
|
+
files[selectedIndex]!.diff.length - visibleRows(),
|
|
196
206
|
);
|
|
197
207
|
diffOffset = Math.max(0, Math.min(maxOffset, diffOffset + amount));
|
|
198
208
|
tui.requestRender();
|
|
@@ -216,13 +226,18 @@ export async function showChangedFiles(
|
|
|
216
226
|
return theme.fg("text", expanded);
|
|
217
227
|
}
|
|
218
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Frame edge with an optional label set into it. Muted, not accent: the
|
|
231
|
+
* frame is not the content, and the accent is spent on the focused pane
|
|
232
|
+
* seam where it actually tells you something.
|
|
233
|
+
*/
|
|
219
234
|
function border(width: number, label: string, top: boolean) {
|
|
220
235
|
const left = top ? "┌" : "└";
|
|
221
236
|
const right = top ? "┐" : "┘";
|
|
222
|
-
const text = `─ ${label}
|
|
237
|
+
const text = label ? `─ ${label} ` : "─";
|
|
223
238
|
const remaining = Math.max(0, width - visibleWidth(text) - 2);
|
|
224
239
|
return theme.fg(
|
|
225
|
-
"
|
|
240
|
+
"borderMuted",
|
|
226
241
|
truncateToWidth(
|
|
227
242
|
`${left}${text}${"─".repeat(remaining)}${right}`,
|
|
228
243
|
width,
|
|
@@ -289,11 +304,11 @@ export async function showChangedFiles(
|
|
|
289
304
|
return;
|
|
290
305
|
}
|
|
291
306
|
if (matchesKey(data, Key.ctrl("d"))) {
|
|
292
|
-
moveDiff(Math.max(1, Math.floor(
|
|
307
|
+
moveDiff(Math.max(1, Math.floor(visibleRows() / 2)));
|
|
293
308
|
return;
|
|
294
309
|
}
|
|
295
310
|
if (matchesKey(data, Key.ctrl("u"))) {
|
|
296
|
-
moveDiff(-Math.max(1, Math.floor(
|
|
311
|
+
moveDiff(-Math.max(1, Math.floor(visibleRows() / 2)));
|
|
297
312
|
return;
|
|
298
313
|
}
|
|
299
314
|
if (matchesKey(data, Key.home) || data === "g") {
|
|
@@ -304,21 +319,23 @@ export async function showChangedFiles(
|
|
|
304
319
|
if (matchesKey(data, Key.end) || data === "G") {
|
|
305
320
|
diffOffset = Math.max(
|
|
306
321
|
0,
|
|
307
|
-
files[selectedIndex]!.diff.length -
|
|
322
|
+
files[selectedIndex]!.diff.length - visibleRows(),
|
|
308
323
|
);
|
|
309
324
|
tui.requestRender();
|
|
310
325
|
}
|
|
311
326
|
}
|
|
312
327
|
|
|
313
328
|
function render(width: number) {
|
|
314
|
-
const height =
|
|
329
|
+
const height = visibleRows();
|
|
315
330
|
const sidebarWidth = Math.min(
|
|
316
331
|
48,
|
|
317
332
|
Math.max(24, Math.floor(width * 0.34)),
|
|
318
333
|
);
|
|
319
334
|
const diffWidth = Math.max(1, width - sidebarWidth - 3);
|
|
320
335
|
const selectedFile = files[selectedIndex]!;
|
|
321
|
-
|
|
336
|
+
// No shouted FILES/DIFF badge: the accent-lit pane seam already says
|
|
337
|
+
// where the keyboard is, without a second thing to read.
|
|
338
|
+
const title = `local changes · ${files.length} ${files.length === 1 ? "file" : "files"}`;
|
|
322
339
|
const lines = [border(width, title, true)];
|
|
323
340
|
|
|
324
341
|
for (let row = 0; row < height; row += 1) {
|
|
@@ -329,7 +346,7 @@ export async function showChangedFiles(
|
|
|
329
346
|
if (file) {
|
|
330
347
|
const isSelected = fileIndex === selectedIndex;
|
|
331
348
|
if (row % 2 === 0) {
|
|
332
|
-
const marker = isSelected ? "
|
|
349
|
+
const marker = isSelected ? "❯ " : " ";
|
|
333
350
|
const isBinary =
|
|
334
351
|
file.additions === null || file.deletions === null;
|
|
335
352
|
const stats = isBinary
|
|
@@ -386,11 +403,27 @@ export async function showChangedFiles(
|
|
|
386
403
|
);
|
|
387
404
|
}
|
|
388
405
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
406
|
+
lines.push(border(width, "", false));
|
|
407
|
+
// Hints below the frame rather than crammed into its bottom edge, with
|
|
408
|
+
// keys a step brighter than the words describing them.
|
|
409
|
+
lines.push(
|
|
410
|
+
hintLine(
|
|
411
|
+
theme,
|
|
412
|
+
focus === "files"
|
|
413
|
+
? [
|
|
414
|
+
["j/k or ↑/↓", "select"],
|
|
415
|
+
["enter/space/l", "open diff"],
|
|
416
|
+
["esc", "close"],
|
|
417
|
+
]
|
|
418
|
+
: [
|
|
419
|
+
["j/k or ↑/↓", "scroll"],
|
|
420
|
+
["ctrl-d/u", "page"],
|
|
421
|
+
["g/G", "top/bottom"],
|
|
422
|
+
["esc/h", "files"],
|
|
423
|
+
],
|
|
424
|
+
width,
|
|
425
|
+
),
|
|
426
|
+
);
|
|
394
427
|
return lines;
|
|
395
428
|
}
|
|
396
429
|
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-read — read-only git inspection tools for pi.
|
|
3
|
+
*
|
|
4
|
+
* Registers git_show, git_diff, and git_log: the minimal surface a review or
|
|
5
|
+
* exploration agent needs from git history without any write path. All three
|
|
6
|
+
* share the file-search output discipline (bounded preview, complete output
|
|
7
|
+
* persisted up to 10 MiB) and every user-controlled argument is validated
|
|
8
|
+
* before it reaches a child process, so a revision or path can never inject
|
|
9
|
+
* flags or escape the repository. The tools are child-safe by design and are
|
|
10
|
+
* classified in CHILD_SAFE_PACKAGE_TOOL_NAMES so review/advisor subagents can
|
|
11
|
+
* read diffs their tool boundary otherwise excludes (issue #61).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
17
|
+
import type {
|
|
18
|
+
AgentToolResult,
|
|
19
|
+
ExtensionAPI,
|
|
20
|
+
ExtensionContext,
|
|
21
|
+
} from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
23
|
+
import { Cause, Effect, Exit } from "effect";
|
|
24
|
+
import { type Static, Type } from "typebox";
|
|
25
|
+
import { formatCapturedOutput } from "../file-search/src/output.ts";
|
|
26
|
+
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
27
|
+
import {
|
|
28
|
+
OPENPI_TOOL_SURFACE,
|
|
29
|
+
patchOwnedTools,
|
|
30
|
+
} from "../shared/tool-surface.ts";
|
|
31
|
+
import {
|
|
32
|
+
buildDiffArgs,
|
|
33
|
+
buildLogArgs,
|
|
34
|
+
buildShowArgs,
|
|
35
|
+
type GitDiffParams,
|
|
36
|
+
type GitLogParams,
|
|
37
|
+
type GitShowParams,
|
|
38
|
+
InvalidPathError,
|
|
39
|
+
InvalidRevisionError,
|
|
40
|
+
} from "./src/args.ts";
|
|
41
|
+
import { type GitOutcome, runGit } from "./src/process.ts";
|
|
42
|
+
import {
|
|
43
|
+
GIT_DIFF_PARAMETER_DESCRIPTIONS,
|
|
44
|
+
GIT_DIFF_PROMPT_GUIDELINES,
|
|
45
|
+
GIT_DIFF_PROMPT_SNIPPET,
|
|
46
|
+
GIT_DIFF_TOOL_DESCRIPTION,
|
|
47
|
+
GIT_LOG_PARAMETER_DESCRIPTIONS,
|
|
48
|
+
GIT_LOG_PROMPT_GUIDELINES,
|
|
49
|
+
GIT_LOG_PROMPT_SNIPPET,
|
|
50
|
+
GIT_LOG_TOOL_DESCRIPTION,
|
|
51
|
+
GIT_SHOW_PARAMETER_DESCRIPTIONS,
|
|
52
|
+
GIT_SHOW_PROMPT_GUIDELINES,
|
|
53
|
+
GIT_SHOW_PROMPT_SNIPPET,
|
|
54
|
+
GIT_SHOW_TOOL_DESCRIPTION,
|
|
55
|
+
} from "./src/prompt.ts";
|
|
56
|
+
|
|
57
|
+
interface GitToolDetails {
|
|
58
|
+
readonly command: string;
|
|
59
|
+
readonly truncated: boolean;
|
|
60
|
+
readonly lineCount?: number;
|
|
61
|
+
readonly fullOutputPath?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function causeMessage(cause: unknown): string {
|
|
65
|
+
if (
|
|
66
|
+
cause instanceof InvalidRevisionError ||
|
|
67
|
+
cause instanceof InvalidPathError
|
|
68
|
+
) {
|
|
69
|
+
return cause.message;
|
|
70
|
+
}
|
|
71
|
+
if (cause instanceof Error) return cause.message;
|
|
72
|
+
return String(cause);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unwrapToolExit<A, E>(exit: Exit.Exit<A, E>, tool: string) {
|
|
76
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
77
|
+
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
78
|
+
throw new Error(`${tool} was cancelled.`);
|
|
79
|
+
}
|
|
80
|
+
throw new Error(causeMessage(Cause.squash(exit.cause)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function gitResult(
|
|
84
|
+
outcome: GitOutcome,
|
|
85
|
+
command: string,
|
|
86
|
+
): AgentToolResult<GitToolDetails> {
|
|
87
|
+
const formatted = formatCapturedOutput(outcome.output);
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: sanitizeTerminalText(formatted.text) || "(no output)",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
details: {
|
|
96
|
+
command,
|
|
97
|
+
truncated: formatted.truncated,
|
|
98
|
+
lineCount: formatted.lineCount,
|
|
99
|
+
fullOutputPath: formatted.fullOutputPath,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function showParameters() {
|
|
105
|
+
return Type.Object({
|
|
106
|
+
revision: Type.String({
|
|
107
|
+
description: GIT_SHOW_PARAMETER_DESCRIPTIONS.revision,
|
|
108
|
+
}),
|
|
109
|
+
path: Type.Optional(
|
|
110
|
+
Type.String({ description: GIT_SHOW_PARAMETER_DESCRIPTIONS.path }),
|
|
111
|
+
),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function diffParameters() {
|
|
116
|
+
return Type.Object({
|
|
117
|
+
from: Type.Optional(
|
|
118
|
+
Type.String({ description: GIT_DIFF_PARAMETER_DESCRIPTIONS.from }),
|
|
119
|
+
),
|
|
120
|
+
to: Type.Optional(
|
|
121
|
+
Type.String({ description: GIT_DIFF_PARAMETER_DESCRIPTIONS.to }),
|
|
122
|
+
),
|
|
123
|
+
staged: Type.Optional(
|
|
124
|
+
Type.Boolean({ description: GIT_DIFF_PARAMETER_DESCRIPTIONS.staged }),
|
|
125
|
+
),
|
|
126
|
+
stat: Type.Optional(
|
|
127
|
+
Type.Boolean({ description: GIT_DIFF_PARAMETER_DESCRIPTIONS.stat }),
|
|
128
|
+
),
|
|
129
|
+
path: Type.Optional(
|
|
130
|
+
Type.String({ description: GIT_DIFF_PARAMETER_DESCRIPTIONS.path }),
|
|
131
|
+
),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function logParameters() {
|
|
136
|
+
return Type.Object({
|
|
137
|
+
revision: Type.Optional(
|
|
138
|
+
Type.String({ description: GIT_LOG_PARAMETER_DESCRIPTIONS.revision }),
|
|
139
|
+
),
|
|
140
|
+
file: Type.Optional(
|
|
141
|
+
Type.String({ description: GIT_LOG_PARAMETER_DESCRIPTIONS.file }),
|
|
142
|
+
),
|
|
143
|
+
limit: Type.Optional(
|
|
144
|
+
Type.Integer({
|
|
145
|
+
description: GIT_LOG_PARAMETER_DESCRIPTIONS.limit,
|
|
146
|
+
minimum: 1,
|
|
147
|
+
maximum: 1000,
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
oneline: Type.Optional(
|
|
151
|
+
Type.Boolean({ description: GIT_LOG_PARAMETER_DESCRIPTIONS.oneline }),
|
|
152
|
+
),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function displayRevision(value: string | undefined, fallback = "HEAD") {
|
|
157
|
+
return value === undefined || value.trim() === "" ? fallback : value.trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default function gitReadTools(pi: ExtensionAPI) {
|
|
161
|
+
const resultDirectories = new Set<string>();
|
|
162
|
+
const rememberOutput = (outcome: GitOutcome) => {
|
|
163
|
+
if (outcome.output.fullOutputPath) {
|
|
164
|
+
resultDirectories.add(path.dirname(outcome.output.fullOutputPath));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
pi.on("session_start", () => {
|
|
169
|
+
patchOwnedTools(pi, "gitRead", {
|
|
170
|
+
enable: OPENPI_TOOL_SURFACE.gitRead.entry,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
pi.on("session_shutdown", () => {
|
|
175
|
+
for (const directory of resultDirectories) {
|
|
176
|
+
try {
|
|
177
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
178
|
+
} catch {
|
|
179
|
+
// Temporary git artifacts are best-effort cleanup.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
resultDirectories.clear();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
pi.registerTool<ReturnType<typeof showParameters>, GitToolDetails>({
|
|
186
|
+
name: "git_show",
|
|
187
|
+
label: "Git Show",
|
|
188
|
+
description: GIT_SHOW_TOOL_DESCRIPTION,
|
|
189
|
+
promptSnippet: GIT_SHOW_PROMPT_SNIPPET,
|
|
190
|
+
promptGuidelines: GIT_SHOW_PROMPT_GUIDELINES,
|
|
191
|
+
parameters: showParameters(),
|
|
192
|
+
|
|
193
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
194
|
+
const exit = await Effect.runPromiseExit(
|
|
195
|
+
Effect.gen(function* () {
|
|
196
|
+
const args = buildShowArgs(params as GitShowParams);
|
|
197
|
+
const outcome = yield* runGit(args, ctx.cwd);
|
|
198
|
+
rememberOutput(outcome);
|
|
199
|
+
return gitResult(outcome, args.join(" "));
|
|
200
|
+
}),
|
|
201
|
+
signal ? { signal } : undefined,
|
|
202
|
+
);
|
|
203
|
+
return unwrapToolExit(exit, "git_show");
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
renderCall(args) {
|
|
207
|
+
return new Text(`git show ${displayRevision(args.revision)}`, 0, 0);
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
renderResult(result, { expanded }, theme) {
|
|
211
|
+
const details = result.details;
|
|
212
|
+
let text = details?.command
|
|
213
|
+
? `showed ${details.lineCount ?? 0} lines`
|
|
214
|
+
: "shown";
|
|
215
|
+
if (details?.truncated) text += " (truncated)";
|
|
216
|
+
if (expanded)
|
|
217
|
+
text += expandedResultPreview(result, details?.fullOutputPath, theme);
|
|
218
|
+
return new Text(text, 0, 0);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
pi.registerTool<ReturnType<typeof diffParameters>, GitToolDetails>({
|
|
223
|
+
name: "git_diff",
|
|
224
|
+
label: "Git Diff",
|
|
225
|
+
description: GIT_DIFF_TOOL_DESCRIPTION,
|
|
226
|
+
promptSnippet: GIT_DIFF_PROMPT_SNIPPET,
|
|
227
|
+
promptGuidelines: GIT_DIFF_PROMPT_GUIDELINES,
|
|
228
|
+
parameters: diffParameters(),
|
|
229
|
+
|
|
230
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
231
|
+
const exit = await Effect.runPromiseExit(
|
|
232
|
+
Effect.gen(function* () {
|
|
233
|
+
const args = buildDiffArgs(params as GitDiffParams);
|
|
234
|
+
const outcome = yield* runGit(args, ctx.cwd);
|
|
235
|
+
rememberOutput(outcome);
|
|
236
|
+
return gitResult(outcome, args.join(" "));
|
|
237
|
+
}),
|
|
238
|
+
signal ? { signal } : undefined,
|
|
239
|
+
);
|
|
240
|
+
return unwrapToolExit(exit, "git_diff");
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
renderCall(args) {
|
|
244
|
+
const from = displayRevision(
|
|
245
|
+
args.from,
|
|
246
|
+
args.staged ? "HEAD (staged)" : "index",
|
|
247
|
+
);
|
|
248
|
+
const to = args.to ? displayRevision(args.to) : "worktree";
|
|
249
|
+
let text = `git diff ${from} → ${to}`;
|
|
250
|
+
if (args.stat) text += " (stat)";
|
|
251
|
+
if (args.path) text += ` ${args.path}`;
|
|
252
|
+
return new Text(text, 0, 0);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
renderResult(result, { expanded }, theme) {
|
|
256
|
+
const details = result.details;
|
|
257
|
+
let text = details?.command
|
|
258
|
+
? `${details.lineCount ?? 0} diff lines`
|
|
259
|
+
: "diffed";
|
|
260
|
+
if (details?.truncated) text += " (truncated)";
|
|
261
|
+
if (expanded)
|
|
262
|
+
text += expandedResultPreview(result, details?.fullOutputPath, theme);
|
|
263
|
+
return new Text(text, 0, 0);
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
pi.registerTool<ReturnType<typeof logParameters>, GitToolDetails>({
|
|
268
|
+
name: "git_log",
|
|
269
|
+
label: "Git Log",
|
|
270
|
+
description: GIT_LOG_TOOL_DESCRIPTION,
|
|
271
|
+
promptSnippet: GIT_LOG_PROMPT_SNIPPET,
|
|
272
|
+
promptGuidelines: GIT_LOG_PROMPT_GUIDELINES,
|
|
273
|
+
parameters: logParameters(),
|
|
274
|
+
|
|
275
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
276
|
+
const exit = await Effect.runPromiseExit(
|
|
277
|
+
Effect.gen(function* () {
|
|
278
|
+
const args = buildLogArgs(params as GitLogParams);
|
|
279
|
+
const outcome = yield* runGit(args, ctx.cwd);
|
|
280
|
+
rememberOutput(outcome);
|
|
281
|
+
return gitResult(outcome, args.join(" "));
|
|
282
|
+
}),
|
|
283
|
+
signal ? { signal } : undefined,
|
|
284
|
+
);
|
|
285
|
+
return unwrapToolExit(exit, "git_log");
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
renderCall(args) {
|
|
289
|
+
let text = `git log ${displayRevision(args.revision)}`;
|
|
290
|
+
if (args.file) text += ` -- ${args.file}`;
|
|
291
|
+
if (args.limit !== undefined) text += ` -n ${args.limit}`;
|
|
292
|
+
return new Text(text, 0, 0);
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
renderResult(result, { expanded }, theme) {
|
|
296
|
+
const details = result.details;
|
|
297
|
+
let text = details?.command
|
|
298
|
+
? `${details.lineCount ?? 0} output lines`
|
|
299
|
+
: "logged";
|
|
300
|
+
if (details?.truncated) text += " (truncated)";
|
|
301
|
+
if (expanded)
|
|
302
|
+
text += expandedResultPreview(result, details?.fullOutputPath, theme);
|
|
303
|
+
return new Text(text, 0, 0);
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const EXPANDED_PREVIEW_LINES = 20;
|
|
309
|
+
|
|
310
|
+
export function expandedResultPreview(
|
|
311
|
+
result: { content: { type: string; text?: string }[] },
|
|
312
|
+
fullOutputPath: string | undefined,
|
|
313
|
+
theme: { fg(color: string, text: string): string },
|
|
314
|
+
) {
|
|
315
|
+
let text = "";
|
|
316
|
+
const content = result.content[0];
|
|
317
|
+
if (content?.type === "text" && content.text) {
|
|
318
|
+
const lines = sanitizeTerminalText(content.text).split("\n");
|
|
319
|
+
for (const line of lines.slice(0, EXPANDED_PREVIEW_LINES)) {
|
|
320
|
+
text += `\n${theme.fg("dim", line)}`;
|
|
321
|
+
}
|
|
322
|
+
if (lines.length > EXPANDED_PREVIEW_LINES) {
|
|
323
|
+
text += `\n${theme.fg("muted", `... ${lines.length - EXPANDED_PREVIEW_LINES} more lines`)}`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (fullOutputPath) {
|
|
327
|
+
text += `\n${theme.fg("dim", `Full output: ${sanitizeTerminalText(fullOutputPath)}`)}`;
|
|
328
|
+
}
|
|
329
|
+
return text;
|
|
330
|
+
}
|