@tt-a1i/openpi 0.1.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 +643 -0
- package/SETUP.md +74 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-package.png +0 -0
- package/assets/readme-hero-mobile.svg +72 -0
- package/assets/readme-hero.svg +118 -0
- package/assets/readme-runtime-mobile.svg +91 -0
- package/assets/readme-runtime.svg +111 -0
- package/extensions/ask-user/handoff.ts +205 -0
- package/extensions/ask-user/index.ts +1110 -0
- package/extensions/ask-user/limits.ts +89 -0
- package/extensions/ask-user/prompt.ts +76 -0
- package/extensions/background-terminals/index.ts +653 -0
- package/extensions/background-terminals/src/domain.ts +99 -0
- package/extensions/background-terminals/src/manager.ts +989 -0
- package/extensions/background-terminals/src/output.ts +84 -0
- package/extensions/background-terminals/src/prompt.ts +195 -0
- package/extensions/background-terminals/src/result-delivery.ts +43 -0
- package/extensions/background-terminals/src/runtime.ts +36 -0
- package/extensions/background-terminals/src/ui/output-view.ts +55 -0
- package/extensions/background-terminals/src/ui/ps.ts +642 -0
- package/extensions/background-terminals/src/ui/tool-result.ts +146 -0
- package/extensions/background-terminals/src/watch.ts +192 -0
- package/extensions/context-pivot/index.ts +222 -0
- package/extensions/copy-all/index.ts +65 -0
- package/extensions/cron/index.ts +173 -0
- package/extensions/cron/schedule.ts +127 -0
- package/extensions/file-mutation-display/index.ts +105 -0
- package/extensions/file-mutation-display/render.ts +107 -0
- package/extensions/file-search/index.ts +515 -0
- package/extensions/file-search/src/args.ts +129 -0
- package/extensions/file-search/src/binaries.ts +419 -0
- package/extensions/file-search/src/output.ts +142 -0
- package/extensions/file-search/src/process.ts +309 -0
- package/extensions/file-search/src/prompt.ts +53 -0
- package/extensions/git-info/index.ts +272 -0
- package/extensions/git-info/src/changed-files-view.ts +414 -0
- package/extensions/git-info/src/process.ts +107 -0
- package/extensions/git-info/src/refresh-coordinator.ts +13 -0
- package/extensions/git-info/src/runtime.ts +28 -0
- package/extensions/goal/controller.ts +794 -0
- package/extensions/goal/index.ts +521 -0
- package/extensions/goal/prompts.ts +122 -0
- package/extensions/goal/state.ts +763 -0
- package/extensions/goal/ui.ts +158 -0
- package/extensions/model-info/index.ts +234 -0
- package/extensions/plan-mode/bash-policy.ts +313 -0
- package/extensions/plan-mode/index.ts +539 -0
- package/extensions/post-edit/index.ts +129 -0
- package/extensions/sessions/LICENSE.upstream +21 -0
- package/extensions/sessions/git-stats.ts +226 -0
- package/extensions/sessions/index.ts +1092 -0
- package/extensions/sessions/sessions.ts +385 -0
- package/extensions/setup/index.ts +408 -0
- package/extensions/shared/activity-status.ts +65 -0
- package/extensions/shared/below-editor-navigation.ts +343 -0
- package/extensions/shared/child-session.ts +352 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +102 -0
- package/extensions/shared/plan-mode-state.ts +65 -0
- package/extensions/shared/setup-config.ts +971 -0
- package/extensions/shared/subagent-roles.ts +22 -0
- package/extensions/shared/terminal-text.ts +38 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/worktree.ts +526 -0
- package/extensions/subagents/index.ts +1225 -0
- package/extensions/subagents/navigation.ts +121 -0
- package/extensions/subagents/src/agent-types.ts +543 -0
- package/extensions/subagents/src/backend.ts +63 -0
- package/extensions/subagents/src/backends/pi.ts +493 -0
- package/extensions/subagents/src/backends/stub.ts +296 -0
- package/extensions/subagents/src/by-the-way.ts +21 -0
- package/extensions/subagents/src/domain.ts +271 -0
- package/extensions/subagents/src/format.ts +48 -0
- package/extensions/subagents/src/manager.ts +769 -0
- package/extensions/subagents/src/prompt.ts +190 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +51 -0
- package/extensions/subagents/src/ui/takeover.ts +615 -0
- package/extensions/subagents/src/ui/transcript.ts +293 -0
- package/extensions/subagents/src/ui/wait-result.ts +89 -0
- package/extensions/suggestions/index.ts +172 -0
- package/extensions/suggestions/src/config.ts +12 -0
- package/extensions/suggestions/src/predictor.ts +147 -0
- package/extensions/suggestions/src/prompt.ts +20 -0
- package/extensions/suggestions/src/transcript.ts +233 -0
- package/extensions/suggestions/src/ui.ts +224 -0
- package/extensions/tasks/index.ts +512 -0
- package/extensions/tasks/tasks.ts +649 -0
- package/extensions/tasks/ui.ts +421 -0
- package/extensions/turn-time/index.ts +61 -0
- package/extensions/ui-customization/footer.ts +512 -0
- package/extensions/ui-customization/index.ts +217 -0
- package/extensions/workflows/acceptance.ts +298 -0
- package/extensions/workflows/artifacts.ts +225 -0
- package/extensions/workflows/controller.ts +210 -0
- package/extensions/workflows/dashboard.ts +1226 -0
- package/extensions/workflows/index.ts +1884 -0
- package/extensions/workflows/journal.ts +188 -0
- package/extensions/workflows/meta.ts +250 -0
- package/extensions/workflows/model.ts +423 -0
- package/extensions/workflows/navigation.ts +93 -0
- package/extensions/workflows/prompt.ts +212 -0
- package/extensions/workflows/replay-safety.ts +577 -0
- package/extensions/workflows/runner.ts +786 -0
- package/extensions/workflows/sandbox-child.cjs +402 -0
- package/extensions/workflows/sandbox.ts +397 -0
- package/extensions/workflows/serialization.ts +162 -0
- package/extensions/workflows/worktree-handoff.ts +216 -0
- package/package.json +87 -0
- package/scripts/prepare-effect-tsgo.mjs +16 -0
- package/skills/background-terminals/SKILL.md +30 -0
- package/skills/subagents/SKILL.md +15 -0
- package/themes/github-dark-default.json +89 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cron: user-scheduled prompts for this session.
|
|
3
|
+
*
|
|
4
|
+
* `/cron every 5m <prompt>` or `/cron in 30s <prompt>` queues a prompt that
|
|
5
|
+
* fires on a timer. Jobs live only in this session (in memory, cleared on
|
|
6
|
+
* shutdown) — deliberately no persisted config and no model tool: this is a
|
|
7
|
+
* user affordance, not something the agent schedules for itself.
|
|
8
|
+
*
|
|
9
|
+
* A job fires only when the session is idle, delivered the same way every
|
|
10
|
+
* other background completion is (followUp + triggerTurn), so it can never
|
|
11
|
+
* interrupt a streaming turn.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ExtensionAPI,
|
|
16
|
+
ExtensionContext,
|
|
17
|
+
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import {
|
|
19
|
+
advanceDeliveredJobs,
|
|
20
|
+
dueJobs,
|
|
21
|
+
formatInterval,
|
|
22
|
+
parseCronCommand,
|
|
23
|
+
type CronJob,
|
|
24
|
+
} from "./schedule.ts";
|
|
25
|
+
|
|
26
|
+
/** How often the scheduler looks for due jobs. */
|
|
27
|
+
const POLL_MS = 30_000;
|
|
28
|
+
|
|
29
|
+
interface CronRuntime {
|
|
30
|
+
now(): number;
|
|
31
|
+
startPolling(tick: () => void): () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SYSTEM_RUNTIME: CronRuntime = {
|
|
35
|
+
now: Date.now,
|
|
36
|
+
startPolling(tick) {
|
|
37
|
+
const timer = setInterval(tick, POLL_MS);
|
|
38
|
+
// Never hold the process open for a scheduled prompt.
|
|
39
|
+
timer.unref?.();
|
|
40
|
+
return () => clearInterval(timer);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default function cron(
|
|
45
|
+
pi: ExtensionAPI,
|
|
46
|
+
runtime: CronRuntime = SYSTEM_RUNTIME,
|
|
47
|
+
) {
|
|
48
|
+
let jobs: CronJob[] = [];
|
|
49
|
+
let nextId = 1;
|
|
50
|
+
let stopPolling: (() => void) | undefined;
|
|
51
|
+
let context: ExtensionContext | undefined;
|
|
52
|
+
|
|
53
|
+
const stopTicker = () => {
|
|
54
|
+
stopPolling?.();
|
|
55
|
+
stopPolling = undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const fire = (job: CronJob) => {
|
|
59
|
+
try {
|
|
60
|
+
pi.sendMessage(
|
|
61
|
+
{
|
|
62
|
+
customType: "cron-fire",
|
|
63
|
+
content: job.prompt,
|
|
64
|
+
display: true,
|
|
65
|
+
details: { id: job.id, recurring: job.intervalMs !== undefined },
|
|
66
|
+
},
|
|
67
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
68
|
+
);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
// Session may be shutting down; leave the job due for the next tick.
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const tick = () => {
|
|
77
|
+
const ctx = context;
|
|
78
|
+
if (!ctx || jobs.length === 0) return;
|
|
79
|
+
// Only fire into an idle session: a followUp during a live turn would
|
|
80
|
+
// queue behind it anyway, and firing on a timer mid-stream is surprising.
|
|
81
|
+
if (!ctx.isIdle()) return;
|
|
82
|
+
const now = runtime.now();
|
|
83
|
+
const due = dueJobs(jobs, now);
|
|
84
|
+
if (due.length === 0) return;
|
|
85
|
+
const deliveredIds = new Set<number>();
|
|
86
|
+
for (const job of due) {
|
|
87
|
+
if (fire(job)) deliveredIds.add(job.id);
|
|
88
|
+
}
|
|
89
|
+
jobs = advanceDeliveredJobs(jobs, deliveredIds, now);
|
|
90
|
+
if (jobs.length === 0) stopTicker();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const startTicker = () => {
|
|
94
|
+
if (stopPolling) return;
|
|
95
|
+
stopPolling = runtime.startPolling(tick);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
pi.registerCommand("cron", {
|
|
99
|
+
description:
|
|
100
|
+
"Schedule a prompt for this session (`/cron every 5m <prompt>`, `/cron in 30s <prompt>`, `/cron list`, `/cron remove <id>`)",
|
|
101
|
+
handler: async (rawArgs, ctx) => {
|
|
102
|
+
const parsed = parseCronCommand(rawArgs);
|
|
103
|
+
|
|
104
|
+
if (parsed.action === "help") {
|
|
105
|
+
ctx.ui.notify(
|
|
106
|
+
parsed.error ??
|
|
107
|
+
"Usage: /cron every <5m> <prompt> · /cron in <30s> <prompt> · /cron list · /cron remove <id>\nJobs live in this session only and fire when the session is idle.",
|
|
108
|
+
parsed.error ? "warning" : "info",
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (parsed.action === "list") {
|
|
114
|
+
if (jobs.length === 0) {
|
|
115
|
+
ctx.ui.notify("No scheduled prompts in this session.", "info");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const now = runtime.now();
|
|
119
|
+
const lines = jobs.map((job) => {
|
|
120
|
+
const inSeconds = Math.max(
|
|
121
|
+
0,
|
|
122
|
+
Math.round((job.nextRunAt - now) / 1000),
|
|
123
|
+
);
|
|
124
|
+
const when = job.intervalMs
|
|
125
|
+
? `every ${formatInterval(job.intervalMs)}`
|
|
126
|
+
: "once";
|
|
127
|
+
return `${job.id}. ${when} · next in ${inSeconds}s · ${job.prompt}`;
|
|
128
|
+
});
|
|
129
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (parsed.action === "remove") {
|
|
134
|
+
const before = jobs.length;
|
|
135
|
+
jobs = jobs.filter((job) => job.id !== parsed.id);
|
|
136
|
+
if (jobs.length === 0) stopTicker();
|
|
137
|
+
ctx.ui.notify(
|
|
138
|
+
jobs.length === before
|
|
139
|
+
? `No scheduled prompt with id ${parsed.id}.`
|
|
140
|
+
: `Removed scheduled prompt ${parsed.id}.`,
|
|
141
|
+
jobs.length === before ? "warning" : "info",
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const intervalMs = parsed.intervalMs!;
|
|
147
|
+
const job: CronJob = {
|
|
148
|
+
id: nextId++,
|
|
149
|
+
prompt: parsed.prompt!,
|
|
150
|
+
...(parsed.oneShot ? {} : { intervalMs }),
|
|
151
|
+
nextRunAt: runtime.now() + intervalMs,
|
|
152
|
+
};
|
|
153
|
+
jobs.push(job);
|
|
154
|
+
startTicker();
|
|
155
|
+
ctx.ui.notify(
|
|
156
|
+
parsed.oneShot
|
|
157
|
+
? `Scheduled prompt ${job.id} once in ${formatInterval(intervalMs)}.`
|
|
158
|
+
: `Scheduled prompt ${job.id} every ${formatInterval(intervalMs)}.`,
|
|
159
|
+
"info",
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
pi.on("session_start", (_event, ctx) => {
|
|
165
|
+
context = ctx;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
pi.on("session_shutdown", () => {
|
|
169
|
+
stopTicker();
|
|
170
|
+
jobs = [];
|
|
171
|
+
context = undefined;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scheduling logic for `/cron`. Kept free of timers, IO, and session
|
|
3
|
+
* state so the tricky parts — parsing, due-ness, and how a recurring job
|
|
4
|
+
* advances after a missed window — are directly testable.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface CronJob {
|
|
8
|
+
readonly id: number;
|
|
9
|
+
/** Prompt delivered to the agent when the job fires. */
|
|
10
|
+
readonly prompt: string;
|
|
11
|
+
/** Milliseconds between runs; undefined for a one-shot job. */
|
|
12
|
+
readonly intervalMs?: number;
|
|
13
|
+
/** Absolute epoch ms of the next run. */
|
|
14
|
+
readonly nextRunAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const UNITS: Record<string, number> = {
|
|
18
|
+
s: 1_000,
|
|
19
|
+
m: 60_000,
|
|
20
|
+
h: 3_600_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const MIN_INTERVAL_MS = 30_000;
|
|
24
|
+
export const CRON_PROMPT_MAX_CHARS = 2_000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse a duration like `30s`, `5m`, `2h`. Deliberately a small duration
|
|
28
|
+
* grammar rather than crontab syntax: the ~30s poll cannot honor finer cron
|
|
29
|
+
* fields, so accepting them would promise precision that does not exist.
|
|
30
|
+
*/
|
|
31
|
+
export function parseDuration(text: string): number | undefined {
|
|
32
|
+
const match = /^(\d+)\s*([smh])$/i.exec(text.trim());
|
|
33
|
+
if (!match) return undefined;
|
|
34
|
+
const value = Number(match[1]);
|
|
35
|
+
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
36
|
+
return value * UNITS[match[2].toLowerCase()];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ParsedCronCommand {
|
|
40
|
+
action: "add" | "list" | "remove" | "help";
|
|
41
|
+
/** add: every <duration> (recurring) or in <duration> (one-shot). */
|
|
42
|
+
intervalMs?: number;
|
|
43
|
+
oneShot?: boolean;
|
|
44
|
+
prompt?: string;
|
|
45
|
+
id?: number;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse `/cron` arguments:
|
|
51
|
+
* every 5m <prompt> recurring
|
|
52
|
+
* in 30s <prompt> one-shot
|
|
53
|
+
* list | remove <id> management
|
|
54
|
+
*/
|
|
55
|
+
export function parseCronCommand(raw: string): ParsedCronCommand {
|
|
56
|
+
const args = raw.trim();
|
|
57
|
+
if (!args || args === "help") return { action: "help" };
|
|
58
|
+
if (args === "list") return { action: "list" };
|
|
59
|
+
|
|
60
|
+
const remove = /^remove\s+(\d+)$/i.exec(args);
|
|
61
|
+
if (remove) return { action: "remove", id: Number(remove[1]) };
|
|
62
|
+
|
|
63
|
+
const schedule = /^(every|in)\s+(\S+)\s+([\s\S]+)$/i.exec(args);
|
|
64
|
+
if (!schedule) {
|
|
65
|
+
return {
|
|
66
|
+
action: "help",
|
|
67
|
+
error:
|
|
68
|
+
"Usage: /cron every <5m> <prompt> | in <30s> <prompt> | list | remove <id>",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const intervalMs = parseDuration(schedule[2]);
|
|
72
|
+
if (intervalMs === undefined) {
|
|
73
|
+
return {
|
|
74
|
+
action: "help",
|
|
75
|
+
error: `Could not read "${schedule[2]}" as a duration. Use 30s, 5m, or 2h.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (intervalMs < MIN_INTERVAL_MS) {
|
|
79
|
+
return {
|
|
80
|
+
action: "help",
|
|
81
|
+
error: `Minimum interval is ${MIN_INTERVAL_MS / 1000}s (the scheduler polls about that often).`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const prompt = schedule[3].trim().slice(0, CRON_PROMPT_MAX_CHARS);
|
|
85
|
+
if (!prompt) return { action: "help", error: "Provide a prompt to run." };
|
|
86
|
+
return {
|
|
87
|
+
action: "add",
|
|
88
|
+
intervalMs,
|
|
89
|
+
oneShot: schedule[1].toLowerCase() === "in",
|
|
90
|
+
prompt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Jobs whose next run is at or before `now`. */
|
|
95
|
+
export function dueJobs(jobs: readonly CronJob[], now: number) {
|
|
96
|
+
return jobs.filter((job) => job.nextRunAt <= now);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Advance only jobs whose prompt was successfully queued for delivery. */
|
|
100
|
+
export function advanceDeliveredJobs(
|
|
101
|
+
jobs: readonly CronJob[],
|
|
102
|
+
deliveredIds: ReadonlySet<number>,
|
|
103
|
+
now: number,
|
|
104
|
+
) {
|
|
105
|
+
return jobs.flatMap((job) => {
|
|
106
|
+
if (!deliveredIds.has(job.id)) return [job];
|
|
107
|
+
const next = advanceJob(job, now);
|
|
108
|
+
return next ? [next] : [];
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Next state for a job that just fired: one-shots drop out, recurring jobs
|
|
114
|
+
* schedule from NOW rather than from the missed slot, so a long busy gap can
|
|
115
|
+
* never produce a burst of catch-up runs.
|
|
116
|
+
*/
|
|
117
|
+
export function advanceJob(job: CronJob, now: number): CronJob | undefined {
|
|
118
|
+
if (job.intervalMs === undefined) return undefined;
|
|
119
|
+
return { ...job, nextRunAt: now + job.intervalMs };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Human-readable duration for listings. */
|
|
123
|
+
export function formatInterval(ms: number) {
|
|
124
|
+
if (ms % UNITS.h === 0) return `${ms / UNITS.h}h`;
|
|
125
|
+
if (ms % UNITS.m === 0) return `${ms / UNITS.m}m`;
|
|
126
|
+
return `${Math.round(ms / UNITS.s)}s`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBashToolDefinition,
|
|
3
|
+
createEditToolDefinition,
|
|
4
|
+
createWriteToolDefinition,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ToolDefinition,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
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
|
+
}
|
|
46
|
+
|
|
47
|
+
function withCompactBashRenderer(
|
|
48
|
+
definition: ToolDefinition<any, any, any>,
|
|
49
|
+
): ToolDefinition<any, any, any> {
|
|
50
|
+
const renderCall = definition.renderCall;
|
|
51
|
+
const renderResult = definition.renderResult;
|
|
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
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Override only the TUI renderers. The wrapped definitions are Pi's native
|
|
84
|
+
* Bash/Write/Edit tools, so schemas, execution, mutation queues, diffs, and
|
|
85
|
+
* errors stay on the upstream implementation.
|
|
86
|
+
*/
|
|
87
|
+
export default function fileMutationDisplay(pi: ExtensionAPI) {
|
|
88
|
+
pi.on("session_start", (_event, ctx) => {
|
|
89
|
+
const display = loadSetupConfig().ui;
|
|
90
|
+
if (ctx.mode === "tui") {
|
|
91
|
+
// Ctrl+O remains a temporary override. A new/reloaded session starts from
|
|
92
|
+
// the persisted defaults instead of inheriting an old expanded toggle.
|
|
93
|
+
ctx.ui.setToolsExpanded(
|
|
94
|
+
display.subagentResultDisplay === "full" &&
|
|
95
|
+
display.bashToolDisplay === "full" &&
|
|
96
|
+
display.fileMutationDisplay === "full",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
pi.registerTool(withCompactBashRenderer(createBashToolDefinition(ctx.cwd)));
|
|
100
|
+
pi.registerTool(
|
|
101
|
+
withCompactCallRenderer(createWriteToolDefinition(ctx.cwd)),
|
|
102
|
+
);
|
|
103
|
+
pi.registerTool(withCompactCallRenderer(createEditToolDefinition(ctx.cwd)));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
truncateToWidth,
|
|
5
|
+
visibleWidth,
|
|
6
|
+
type Component,
|
|
7
|
+
} from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
// Native Write/Edit renderings include their title and context rows. Three
|
|
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;
|
|
14
|
+
|
|
15
|
+
function expandHint(hidden: number) {
|
|
16
|
+
return `… ${hidden} more line${hidden === 1 ? "" : "s"} · ${keyHint("app.tools.expand", "to expand")}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Keep a long command identifiable without allowing it to wrap for many rows. */
|
|
20
|
+
export function singleLineRenderedComponent(
|
|
21
|
+
component: Component,
|
|
22
|
+
theme: Theme,
|
|
23
|
+
): Component {
|
|
24
|
+
return {
|
|
25
|
+
render(width) {
|
|
26
|
+
const lines = component.render(width);
|
|
27
|
+
if (lines.length <= 1) return lines;
|
|
28
|
+
if (width <= 2) return [truncateToWidth(lines[0] ?? "", width, "")];
|
|
29
|
+
return [
|
|
30
|
+
truncateToWidth(lines[0] ?? "", width - 2, "") + theme.fg("dim", " …"),
|
|
31
|
+
];
|
|
32
|
+
},
|
|
33
|
+
invalidate() {
|
|
34
|
+
component.invalidate();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Preserve one output row, warnings, and final timing in compact Bash mode. */
|
|
40
|
+
export function compactBashRenderedComponent(
|
|
41
|
+
component: Component,
|
|
42
|
+
theme: Theme,
|
|
43
|
+
maximum = BASH_OUTPUT_PREVIEW_LINES,
|
|
44
|
+
): Component {
|
|
45
|
+
return {
|
|
46
|
+
render(width) {
|
|
47
|
+
const rendered = component.render(width);
|
|
48
|
+
const visible = rendered.filter(
|
|
49
|
+
(line) => stripVTControlCharacters(line).trim().length > 0,
|
|
50
|
+
);
|
|
51
|
+
let nativeHidden = 0;
|
|
52
|
+
const content: string[] = [];
|
|
53
|
+
const metadata: string[] = [];
|
|
54
|
+
let status: string | undefined;
|
|
55
|
+
for (const line of visible) {
|
|
56
|
+
const plain = stripVTControlCharacters(line).trim();
|
|
57
|
+
const hiddenMatch = plain.match(/\(?([0-9]+) earlier lines,/);
|
|
58
|
+
if (hiddenMatch) {
|
|
59
|
+
nativeHidden += Number(hiddenMatch[1]);
|
|
60
|
+
} else if (/^(Took|Elapsed)\s/.test(plain)) {
|
|
61
|
+
status = line;
|
|
62
|
+
} else if (/^\[(Full output|Truncated):/.test(plain)) {
|
|
63
|
+
metadata.push(line);
|
|
64
|
+
} else {
|
|
65
|
+
content.push(line);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const preview = content.slice(0, maximum);
|
|
69
|
+
const hidden =
|
|
70
|
+
nativeHidden + Math.max(0, content.length - preview.length);
|
|
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
|
+
}
|
|
83
|
+
|
|
84
|
+
export function compactRenderedComponent(
|
|
85
|
+
component: Component,
|
|
86
|
+
theme: Theme,
|
|
87
|
+
maximum = FILE_MUTATION_PREVIEW_LINES,
|
|
88
|
+
background?: (text: string) => string,
|
|
89
|
+
): Component {
|
|
90
|
+
return {
|
|
91
|
+
render(width) {
|
|
92
|
+
const lines = component.render(width);
|
|
93
|
+
if (lines.length <= maximum) return lines;
|
|
94
|
+
const hidden = lines.length - maximum;
|
|
95
|
+
const hint = theme.fg("dim", expandHint(hidden));
|
|
96
|
+
const paddedHint =
|
|
97
|
+
hint + " ".repeat(Math.max(0, width - visibleWidth(hint)));
|
|
98
|
+
return [
|
|
99
|
+
...lines.slice(0, maximum),
|
|
100
|
+
background ? background(paddedHint) : hint,
|
|
101
|
+
];
|
|
102
|
+
},
|
|
103
|
+
invalidate() {
|
|
104
|
+
component.invalidate();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|