@tt-a1i/openpi 0.3.1 → 0.5.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 +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -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/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { EditorComponent } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
BelowEditorNavigationEditor,
|
|
5
|
+
BelowEditorStripState,
|
|
6
|
+
} from "../../shared/below-editor-navigation.ts";
|
|
7
|
+
import { capabilitiesRequestedByPrompt } from "../../shared/capability-intent.ts";
|
|
8
|
+
|
|
9
|
+
const DELEGATE_NAMES = /\bsubagents?\b|子代理/giu;
|
|
10
|
+
const WORKFLOW_NAMES = /\bworkflows?\b|工作流/giu;
|
|
11
|
+
const FOREGROUND_RESET = "\u001b[39m";
|
|
12
|
+
|
|
13
|
+
interface CapabilityKeywordColorOptions {
|
|
14
|
+
readonly colorMode: "truecolor" | "256color";
|
|
15
|
+
readonly light: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isLightNamedTheme(name: string | undefined) {
|
|
19
|
+
return name !== undefined && /(?:^|[-_])light(?:$|[-_])/iu.test(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Claude-style lavender keyword color. The light variant preserves readable
|
|
24
|
+
* contrast instead of mechanically reusing the bright dark-terminal swatch.
|
|
25
|
+
*/
|
|
26
|
+
export function colorCapabilityKeyword(
|
|
27
|
+
text: string,
|
|
28
|
+
options: CapabilityKeywordColorOptions,
|
|
29
|
+
) {
|
|
30
|
+
const start = options.light
|
|
31
|
+
? options.colorMode === "truecolor"
|
|
32
|
+
? "\u001b[38;2;130;80;223m"
|
|
33
|
+
: "\u001b[38;5;98m"
|
|
34
|
+
: options.colorMode === "truecolor"
|
|
35
|
+
? "\u001b[38;2;210;168;255m"
|
|
36
|
+
: "\u001b[38;5;183m";
|
|
37
|
+
return `${start}${text}${FOREGROUND_RESET}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function highlightCapabilityNames(
|
|
41
|
+
line: string,
|
|
42
|
+
capabilities: readonly string[],
|
|
43
|
+
highlight: (text: string) => string,
|
|
44
|
+
) {
|
|
45
|
+
let result = line;
|
|
46
|
+
if (capabilities.includes("delegate")) {
|
|
47
|
+
result = result.replace(DELEGATE_NAMES, (match) => highlight(match));
|
|
48
|
+
}
|
|
49
|
+
if (capabilities.includes("workflow")) {
|
|
50
|
+
result = result.replace(WORKFLOW_NAMES, (match) => highlight(match));
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Transparent, pre-submit feedback for capability intent. It colours only
|
|
57
|
+
* names whose capability the shared classifier would load after submission;
|
|
58
|
+
* it never changes editor text, Session history, or model context.
|
|
59
|
+
*/
|
|
60
|
+
export class CapabilityIntentHighlightEditor extends BelowEditorNavigationEditor {
|
|
61
|
+
private readonly highlight: (text: string) => string;
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
base: EditorComponent,
|
|
65
|
+
keybindings: KeybindingsManager,
|
|
66
|
+
highlight: (text: string) => string,
|
|
67
|
+
) {
|
|
68
|
+
super(
|
|
69
|
+
base,
|
|
70
|
+
keybindings,
|
|
71
|
+
new BelowEditorStripState(),
|
|
72
|
+
() => false,
|
|
73
|
+
() => undefined,
|
|
74
|
+
() => undefined,
|
|
75
|
+
);
|
|
76
|
+
this.highlight = highlight;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override render(width: number) {
|
|
80
|
+
const capabilities = capabilitiesRequestedByPrompt(this.getText());
|
|
81
|
+
if (
|
|
82
|
+
!capabilities.includes("delegate") &&
|
|
83
|
+
!capabilities.includes("workflow")
|
|
84
|
+
) {
|
|
85
|
+
return super.render(width);
|
|
86
|
+
}
|
|
87
|
+
return super
|
|
88
|
+
.render(width)
|
|
89
|
+
.map((line) =>
|
|
90
|
+
highlightCapabilityNames(line, capabilities, this.highlight),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
type KeybindingsManager,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Key, matchesKey, type EditorComponent } from "@earendil-works/pi-tui";
|
|
7
|
+
import {
|
|
8
|
+
BelowEditorNavigationEditor,
|
|
9
|
+
BelowEditorStripState,
|
|
10
|
+
} from "../shared/below-editor-navigation.ts";
|
|
11
|
+
import {
|
|
12
|
+
registerEditorLayer,
|
|
13
|
+
removeEditorLayer,
|
|
14
|
+
} from "../shared/editor-layers.ts";
|
|
15
|
+
|
|
16
|
+
export function shouldClearContext(
|
|
17
|
+
data: string,
|
|
18
|
+
editorText: string,
|
|
19
|
+
isIdle: boolean,
|
|
20
|
+
) {
|
|
21
|
+
return isIdle && editorText.length === 0 && matchesKey(data, Key.ctrl("c"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ClearContextEditor extends BelowEditorNavigationEditor {
|
|
25
|
+
private readonly isIdle: () => boolean;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
base: EditorComponent,
|
|
29
|
+
keybindings: KeybindingsManager,
|
|
30
|
+
isIdle: () => boolean,
|
|
31
|
+
) {
|
|
32
|
+
super(
|
|
33
|
+
base,
|
|
34
|
+
keybindings,
|
|
35
|
+
new BelowEditorStripState(),
|
|
36
|
+
() => false,
|
|
37
|
+
() => undefined,
|
|
38
|
+
() => undefined,
|
|
39
|
+
);
|
|
40
|
+
this.isIdle = isIdle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override handleInput(data: string) {
|
|
44
|
+
if (shouldClearContext(data, this.getText(), this.isIdle())) {
|
|
45
|
+
// Route through Pi's built-in /new command so session replacement keeps
|
|
46
|
+
// Pi's persistence, cleanup, and transcript lifecycle as the source of truth.
|
|
47
|
+
this.setText("/new");
|
|
48
|
+
this.onSubmit?.("/new");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
super.handleInput(data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function installClearContextShortcut(pi: ExtensionAPI, ctx: ExtensionContext) {
|
|
57
|
+
if (ctx.mode !== "tui") return () => {};
|
|
58
|
+
|
|
59
|
+
registerEditorLayer(pi, ctx, {
|
|
60
|
+
id: "clear-context",
|
|
61
|
+
order: 100,
|
|
62
|
+
wrap: (base, _tui, _theme, keybindings) =>
|
|
63
|
+
new ClearContextEditor(base, keybindings, () => ctx.isIdle()),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return () => removeEditorLayer(pi, "clear-context");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default function clearContext(pi: ExtensionAPI) {
|
|
70
|
+
let removeShortcut = () => {};
|
|
71
|
+
|
|
72
|
+
pi.on("session_start", (_event, ctx) => {
|
|
73
|
+
removeShortcut();
|
|
74
|
+
removeShortcut = installClearContextShortcut(pi, ctx);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
pi.on("session_shutdown", () => {
|
|
78
|
+
removeShortcut();
|
|
79
|
+
removeShortcut = () => {};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { installClearContextShortcut };
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
|
|
12
12
|
export const MIN_CONTEXT_PIVOT_TOKENS = 30_000;
|
|
13
13
|
const STATUS_KEY = "context-pivot";
|
|
14
|
+
const NOTHING_TO_COMPACT_ERROR = "Nothing to compact (session too small)";
|
|
15
|
+
const NO_DISCARDABLE_HISTORY_MESSAGE =
|
|
16
|
+
"Context pivot could not run: this session has no discardable history to compact. Continue in the current session, or use /sessions to choose another session; to begin cleanly, start a new Session in Pi.";
|
|
14
17
|
|
|
15
18
|
interface PendingPivot {
|
|
16
19
|
brief: string;
|
|
@@ -57,6 +60,13 @@ export function buildPivotSummary(brief: string): string {
|
|
|
57
60
|
].join("\n");
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function formatContextPivotError(error: unknown): string {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
return message === NOTHING_TO_COMPACT_ERROR
|
|
66
|
+
? NO_DISCARDABLE_HISTORY_MESSAGE
|
|
67
|
+
: `Context pivot failed: ${message}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
function impossibleKeptId(entries: readonly SessionEntry[]) {
|
|
61
71
|
return `${entries.at(-1)?.id ?? "context-pivot"}-context-pivot-cut`;
|
|
62
72
|
}
|
|
@@ -73,7 +83,7 @@ function validateBrief(brief: string, ctx: ExtensionContext) {
|
|
|
73
83
|
}
|
|
74
84
|
if (tokens < MIN_CONTEXT_PIVOT_TOKENS) {
|
|
75
85
|
throw new Error(
|
|
76
|
-
`Context is only ${Math.round(tokens).toLocaleString()} tokens; use context_pivot once context reaches at least ${MIN_CONTEXT_PIVOT_TOKENS.toLocaleString()} tokens, or /
|
|
86
|
+
`Context is only ${Math.round(tokens).toLocaleString()} tokens; use context_pivot once context reaches at least ${MIN_CONTEXT_PIVOT_TOKENS.toLocaleString()} tokens, or use /sessions to browse or switch an existing session; start a new Session in Pi when a clean session is needed.`,
|
|
77
87
|
);
|
|
78
88
|
}
|
|
79
89
|
}
|
|
@@ -127,7 +137,7 @@ export default function contextPivot(pi: ExtensionAPI) {
|
|
|
127
137
|
name: "context_pivot",
|
|
128
138
|
label: "Context Pivot",
|
|
129
139
|
description:
|
|
130
|
-
"Deliberately replace a long, noisy active context with a concise brief for the next phase while staying in the same Pi session. Use once context is at least 30k tokens and the work is moving between phases such as research → implementation or implementation → review; below 30k it is rejected. Use /
|
|
140
|
+
"Deliberately replace a long, noisy active context with a concise brief for the next phase while staying in the same Pi session. Use once context is at least 30k tokens and the work is moving between phases such as research → implementation or implementation → review; below 30k it is rejected. Use /sessions to browse or switch an existing session; start a new Session in Pi when a genuinely new session is needed.",
|
|
131
141
|
promptSnippet:
|
|
132
142
|
"Compress a long current session into a clean brief before changing phase",
|
|
133
143
|
promptGuidelines: [
|
|
@@ -176,11 +186,11 @@ export default function contextPivot(pi: ExtensionAPI) {
|
|
|
176
186
|
onError: (error) => {
|
|
177
187
|
if (pivotGeneration === generation) pending = undefined;
|
|
178
188
|
clear();
|
|
189
|
+
const message = formatContextPivotError(error);
|
|
179
190
|
if (ctx.hasUI) {
|
|
180
|
-
ctx.ui.notify(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
);
|
|
191
|
+
ctx.ui.notify(message, "error");
|
|
192
|
+
} else {
|
|
193
|
+
console.error(message);
|
|
184
194
|
}
|
|
185
195
|
},
|
|
186
196
|
});
|
|
@@ -81,8 +81,14 @@ export function parseCronCommand(raw: string): ParsedCronCommand {
|
|
|
81
81
|
error: `Minimum interval is ${MIN_INTERVAL_MS / 1000}s (the scheduler polls about that often).`,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
-
const prompt = schedule[3].trim()
|
|
84
|
+
const prompt = schedule[3].trim();
|
|
85
85
|
if (!prompt) return { action: "help", error: "Provide a prompt to run." };
|
|
86
|
+
if (prompt.length > CRON_PROMPT_MAX_CHARS) {
|
|
87
|
+
return {
|
|
88
|
+
action: "help",
|
|
89
|
+
error: `Prompt is too long. Maximum is ${CRON_PROMPT_MAX_CHARS} characters.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
86
92
|
return {
|
|
87
93
|
action: "add",
|
|
88
94
|
intervalMs,
|
|
@@ -1,88 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createBashToolDefinition,
|
|
3
3
|
createEditToolDefinition,
|
|
4
|
+
createFindToolDefinition,
|
|
5
|
+
createGrepToolDefinition,
|
|
6
|
+
createLsToolDefinition,
|
|
7
|
+
createReadToolDefinition,
|
|
4
8
|
createWriteToolDefinition,
|
|
5
9
|
type ExtensionAPI,
|
|
6
10
|
type ToolDefinition,
|
|
7
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { TSchema } from "typebox";
|
|
8
13
|
import { loadSetupConfig } from "../shared/setup-config.ts";
|
|
9
|
-
import {
|
|
10
|
-
compactBashRenderedComponent,
|
|
11
|
-
compactRenderedComponent,
|
|
12
|
-
singleLineRenderedComponent,
|
|
13
|
-
} from "./render.ts";
|
|
14
|
-
|
|
15
|
-
function withCompactCallRenderer(
|
|
16
|
-
definition: ToolDefinition<any, any, any>,
|
|
17
|
-
): ToolDefinition<any, any, any> {
|
|
18
|
-
const renderCall = definition.renderCall;
|
|
19
|
-
if (!renderCall) return definition;
|
|
20
|
-
return {
|
|
21
|
-
...definition,
|
|
22
|
-
renderCall(args, theme, context) {
|
|
23
|
-
const component = renderCall(args, theme, context);
|
|
24
|
-
if (
|
|
25
|
-
context.expanded ||
|
|
26
|
-
loadSetupConfig().ui.fileMutationDisplay === "full"
|
|
27
|
-
) {
|
|
28
|
-
return component;
|
|
29
|
-
}
|
|
30
|
-
// While the model is still streaming a large Write/Edit payload, a
|
|
31
|
-
// syntax-highlighted preview changes on nearly every token and makes the
|
|
32
|
-
// entire tool block flash. Keep that phase to a stable one-line header;
|
|
33
|
-
// reveal the bounded preview once arguments are complete.
|
|
34
|
-
if (!context.argsComplete) {
|
|
35
|
-
return singleLineRenderedComponent(component, theme);
|
|
36
|
-
}
|
|
37
|
-
const background = context.isPartial
|
|
38
|
-
? (text: string) => theme.bg("toolPendingBg", text)
|
|
39
|
-
: context.isError
|
|
40
|
-
? (text: string) => theme.bg("toolErrorBg", text)
|
|
41
|
-
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
42
|
-
return compactRenderedComponent(component, theme, undefined, background);
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
14
|
+
import { withActivityRenderer } from "./render.ts";
|
|
46
15
|
|
|
47
|
-
function
|
|
48
|
-
definition: ToolDefinition<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!renderCall || !renderResult) return definition;
|
|
53
|
-
return {
|
|
54
|
-
...definition,
|
|
55
|
-
renderCall(args, theme, context) {
|
|
56
|
-
// A compact wrapper is not the native Text component expected through
|
|
57
|
-
// lastComponent, so rebuild the cheap call renderer on each update.
|
|
58
|
-
const component = renderCall(args, theme, {
|
|
59
|
-
...context,
|
|
60
|
-
lastComponent: undefined,
|
|
61
|
-
});
|
|
62
|
-
if (context.expanded || loadSetupConfig().ui.bashToolDisplay === "full") {
|
|
63
|
-
return component;
|
|
64
|
-
}
|
|
65
|
-
return singleLineRenderedComponent(component, theme);
|
|
66
|
-
},
|
|
67
|
-
renderResult(result, options, theme, context) {
|
|
68
|
-
// Bash streams partial results. Never hand our wrapper back to Pi's
|
|
69
|
-
// native BashResultRenderComponent updater.
|
|
70
|
-
const component = renderResult(result, options, theme, {
|
|
71
|
-
...context,
|
|
72
|
-
lastComponent: undefined,
|
|
73
|
-
});
|
|
74
|
-
if (options.expanded || loadSetupConfig().ui.bashToolDisplay === "full") {
|
|
75
|
-
return component;
|
|
76
|
-
}
|
|
77
|
-
return compactBashRenderedComponent(component, theme);
|
|
78
|
-
},
|
|
79
|
-
};
|
|
16
|
+
function compact<TParams extends TSchema, TDetails, TState>(
|
|
17
|
+
definition: ToolDefinition<TParams, TDetails, TState>,
|
|
18
|
+
enabled: boolean,
|
|
19
|
+
) {
|
|
20
|
+
return enabled ? withActivityRenderer(definition) : definition;
|
|
80
21
|
}
|
|
81
22
|
|
|
82
23
|
/**
|
|
83
|
-
* Override only
|
|
84
|
-
*
|
|
85
|
-
* errors stay on the upstream implementation.
|
|
24
|
+
* Override only Pi's TUI projection. Every wrapped definition retains its
|
|
25
|
+
* native schema, prompt metadata, execute function, result, and details.
|
|
86
26
|
*/
|
|
87
27
|
export default function fileMutationDisplay(pi: ExtensionAPI) {
|
|
88
28
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -96,10 +36,28 @@ export default function fileMutationDisplay(pi: ExtensionAPI) {
|
|
|
96
36
|
display.fileMutationDisplay === "full",
|
|
97
37
|
);
|
|
98
38
|
}
|
|
99
|
-
|
|
39
|
+
|
|
40
|
+
pi.registerTool(
|
|
41
|
+
compact(
|
|
42
|
+
createBashToolDefinition(ctx.cwd),
|
|
43
|
+
display.bashToolDisplay !== "full",
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
pi.registerTool(
|
|
47
|
+
compact(
|
|
48
|
+
createWriteToolDefinition(ctx.cwd),
|
|
49
|
+
display.fileMutationDisplay !== "full",
|
|
50
|
+
),
|
|
51
|
+
);
|
|
100
52
|
pi.registerTool(
|
|
101
|
-
|
|
53
|
+
compact(
|
|
54
|
+
createEditToolDefinition(ctx.cwd),
|
|
55
|
+
display.fileMutationDisplay !== "full",
|
|
56
|
+
),
|
|
102
57
|
);
|
|
103
|
-
pi.registerTool(
|
|
58
|
+
pi.registerTool(withActivityRenderer(createReadToolDefinition(ctx.cwd)));
|
|
59
|
+
pi.registerTool(withActivityRenderer(createGrepToolDefinition(ctx.cwd)));
|
|
60
|
+
pi.registerTool(withActivityRenderer(createFindToolDefinition(ctx.cwd)));
|
|
61
|
+
pi.registerTool(withActivityRenderer(createLsToolDefinition(ctx.cwd)));
|
|
104
62
|
});
|
|
105
63
|
}
|
|
@@ -1,107 +1,166 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
Theme,
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { TSchema } from "typebox";
|
|
8
|
+
import { renderPaddedToolActivityLine } from "../shared/tool-activity.ts";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
// visible rows keep the operation identifiable while making repeated file
|
|
11
|
-
// mutations substantially quieter than Claude Code's default preview.
|
|
12
|
-
export const FILE_MUTATION_PREVIEW_LINES = 3;
|
|
13
|
-
export const BASH_OUTPUT_PREVIEW_LINES = 1;
|
|
10
|
+
type ActivityStatus = "pending" | "success" | "error";
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
type ActivityRenderState<TDetails> = {
|
|
13
|
+
openpiActivity?: {
|
|
14
|
+
result?: AgentToolResult<TDetails>;
|
|
15
|
+
status: ActivityStatus;
|
|
16
|
+
startedAt?: number;
|
|
17
|
+
endedAt?: number;
|
|
18
|
+
interval?: NodeJS.Timeout;
|
|
19
|
+
nativeCallComponent?: Component;
|
|
20
|
+
nativeResultComponent?: Component;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const emptyComponent: Component = {
|
|
25
|
+
render: () => [],
|
|
26
|
+
invalidate() {},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function textOutput(result: AgentToolResult<unknown> | undefined) {
|
|
30
|
+
return (
|
|
31
|
+
result?.content
|
|
32
|
+
.filter((item) => item.type === "text")
|
|
33
|
+
.map((item) => item.text)
|
|
34
|
+
.join("\n") ?? ""
|
|
35
|
+
);
|
|
17
36
|
}
|
|
18
37
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
function activityComponent(
|
|
39
|
+
name: string,
|
|
40
|
+
args: unknown,
|
|
41
|
+
state: NonNullable<ActivityRenderState<unknown>["openpiActivity"]>,
|
|
22
42
|
theme: Theme,
|
|
43
|
+
cwd: string,
|
|
23
44
|
): Component {
|
|
24
45
|
return {
|
|
25
46
|
render(width) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
const line = renderPaddedToolActivityLine(
|
|
48
|
+
{
|
|
49
|
+
name,
|
|
50
|
+
args,
|
|
51
|
+
output: textOutput(state.result),
|
|
52
|
+
details: state.result?.details,
|
|
53
|
+
status: state.status,
|
|
54
|
+
cwd,
|
|
55
|
+
startedAt: state.startedAt,
|
|
56
|
+
endedAt: state.endedAt,
|
|
57
|
+
},
|
|
58
|
+
theme,
|
|
59
|
+
width,
|
|
60
|
+
);
|
|
61
|
+
return line ? [line] : [];
|
|
35
62
|
},
|
|
63
|
+
invalidate() {},
|
|
36
64
|
};
|
|
37
65
|
}
|
|
38
66
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Preserve Pi's complete tool definition and execution semantics while
|
|
69
|
+
* replacing only the collapsed operator-facing projection.
|
|
70
|
+
*/
|
|
71
|
+
export function withActivityRenderer<TParams extends TSchema, TDetails, TState>(
|
|
72
|
+
definition: ToolDefinition<TParams, TDetails, TState>,
|
|
73
|
+
): ToolDefinition<TParams, TDetails, TState & ActivityRenderState<TDetails>> {
|
|
74
|
+
const nativeRenderCall = definition.renderCall;
|
|
75
|
+
const nativeRenderResult = definition.renderResult;
|
|
45
76
|
return {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
77
|
+
...definition,
|
|
78
|
+
renderShell: "self",
|
|
79
|
+
renderCall(args, theme, context) {
|
|
80
|
+
const state = context.state as TState & ActivityRenderState<TDetails>;
|
|
81
|
+
state.openpiActivity ??= { status: "pending" };
|
|
82
|
+
const activity = state.openpiActivity;
|
|
83
|
+
if (context.executionStarted && activity.startedAt === undefined) {
|
|
84
|
+
activity.startedAt = Date.now();
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
context.executionStarted &&
|
|
88
|
+
definition.name === "bash" &&
|
|
89
|
+
activity.status === "pending" &&
|
|
90
|
+
!context.expanded &&
|
|
91
|
+
activity.interval === undefined
|
|
92
|
+
) {
|
|
93
|
+
activity.interval = setInterval(() => context.invalidate(), 1000);
|
|
94
|
+
activity.interval.unref();
|
|
95
|
+
}
|
|
96
|
+
if (context.expanded && activity.interval) {
|
|
97
|
+
clearInterval(activity.interval);
|
|
98
|
+
activity.interval = undefined;
|
|
99
|
+
}
|
|
100
|
+
if (context.expanded && nativeRenderCall) {
|
|
101
|
+
const nativeContext: Parameters<typeof nativeRenderCall>[2] = {
|
|
102
|
+
...context,
|
|
103
|
+
state,
|
|
104
|
+
lastComponent: activity.nativeCallComponent,
|
|
105
|
+
};
|
|
106
|
+
const component = nativeRenderCall(args, theme, nativeContext);
|
|
107
|
+
activity.nativeCallComponent = component;
|
|
108
|
+
return component;
|
|
109
|
+
}
|
|
110
|
+
return activityComponent(
|
|
111
|
+
definition.name,
|
|
112
|
+
args,
|
|
113
|
+
activity as NonNullable<ActivityRenderState<unknown>["openpiActivity"]>,
|
|
114
|
+
theme,
|
|
115
|
+
context.cwd,
|
|
50
116
|
);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
117
|
+
},
|
|
118
|
+
renderResult(result, options, theme, context) {
|
|
119
|
+
const state = context.state as TState & ActivityRenderState<TDetails>;
|
|
120
|
+
state.openpiActivity ??= { status: "pending" };
|
|
121
|
+
const activity = state.openpiActivity;
|
|
122
|
+
activity.result = result;
|
|
123
|
+
activity.status = options.isPartial
|
|
124
|
+
? "pending"
|
|
125
|
+
: context.isError
|
|
126
|
+
? "error"
|
|
127
|
+
: "success";
|
|
128
|
+
if (
|
|
129
|
+
options.isPartial &&
|
|
130
|
+
definition.name === "bash" &&
|
|
131
|
+
!options.expanded &&
|
|
132
|
+
activity.interval === undefined
|
|
133
|
+
) {
|
|
134
|
+
activity.interval = setInterval(() => context.invalidate(), 1000);
|
|
135
|
+
activity.interval.unref();
|
|
136
|
+
}
|
|
137
|
+
if (!options.isPartial || context.isError || options.expanded) {
|
|
138
|
+
activity.endedAt ??= Date.now();
|
|
139
|
+
if (activity.interval) {
|
|
140
|
+
clearInterval(activity.interval);
|
|
141
|
+
activity.interval = undefined;
|
|
66
142
|
}
|
|
67
143
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return [
|
|
72
|
-
...preview,
|
|
73
|
-
...(hidden > 0 ? [theme.fg("dim", expandHint(hidden))] : []),
|
|
74
|
-
...metadata,
|
|
75
|
-
...(status ? [status] : []),
|
|
76
|
-
];
|
|
77
|
-
},
|
|
78
|
-
invalidate() {
|
|
79
|
-
component.invalidate();
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
}
|
|
144
|
+
if (options.isPartial && options.expanded) {
|
|
145
|
+
activity.endedAt = undefined;
|
|
146
|
+
}
|
|
83
147
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
background ? background(paddedHint) : hint,
|
|
101
|
-
];
|
|
102
|
-
},
|
|
103
|
-
invalidate() {
|
|
104
|
-
component.invalidate();
|
|
148
|
+
if (options.expanded && nativeRenderResult) {
|
|
149
|
+
const nativeContext: Parameters<typeof nativeRenderResult>[3] = {
|
|
150
|
+
...context,
|
|
151
|
+
state,
|
|
152
|
+
lastComponent: activity.nativeResultComponent,
|
|
153
|
+
};
|
|
154
|
+
const component = nativeRenderResult(
|
|
155
|
+
result,
|
|
156
|
+
options,
|
|
157
|
+
theme,
|
|
158
|
+
nativeContext,
|
|
159
|
+
);
|
|
160
|
+
activity.nativeResultComponent = component;
|
|
161
|
+
return component;
|
|
162
|
+
}
|
|
163
|
+
return emptyComponent;
|
|
105
164
|
},
|
|
106
165
|
};
|
|
107
166
|
}
|