pi-minimal 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/.github/workflows/ci.yml +25 -0
- package/.github/workflows/publish.yml +29 -0
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/activity.js +12 -0
- package/dist/chrome.js +1 -0
- package/dist/config.js +45 -0
- package/dist/cwd.js +103 -0
- package/dist/footer.js +46 -0
- package/dist/header.js +14 -0
- package/dist/herdr.js +44 -0
- package/dist/index.js +155 -0
- package/dist/live-watch.js +52 -0
- package/dist/reclaim.js +19 -0
- package/dist/tool-rows.js +34 -0
- package/dist/tools.js +103 -0
- package/dist/tracker.js +63 -0
- package/dist/working-status.js +191 -0
- package/doc/plans/2026-09-22-pi-minimal-package.md +354 -0
- package/doc/specs/2026-09-22-pi-minimal-package.md +94 -0
- package/eslint.config.js +8 -0
- package/package.json +37 -0
- package/src/activity.ts +16 -0
- package/src/chrome.ts +1 -0
- package/src/config.ts +59 -0
- package/src/cwd.ts +108 -0
- package/src/footer.ts +54 -0
- package/src/header.ts +16 -0
- package/src/herdr.ts +66 -0
- package/src/index.ts +152 -0
- package/src/live-watch.ts +59 -0
- package/src/reclaim.ts +34 -0
- package/src/tool-rows.ts +58 -0
- package/src/tools.ts +161 -0
- package/src/tracker.ts +91 -0
- package/src/working-status.ts +214 -0
- package/test/activity.test.ts +27 -0
- package/test/config.test.ts +114 -0
- package/test/index.test.ts +262 -0
- package/test/package.test.ts +18 -0
- package/test/tools.test.ts +48 -0
- package/test/tracker.test.ts +104 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tests.json +8 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { activityBullet, rowIsRunning } from "./activity.js";
|
|
4
|
+
import { createLiveWatch } from "./live-watch.js";
|
|
5
|
+
import { nextGroupedRow, toolItem } from "./tool-rows.js";
|
|
6
|
+
const live = createLiveWatch(400);
|
|
7
|
+
export function stopLiveWatch() {
|
|
8
|
+
live.stop();
|
|
9
|
+
}
|
|
10
|
+
function liveId(context, item) {
|
|
11
|
+
return context.toolCallId || item;
|
|
12
|
+
}
|
|
13
|
+
function contentText(result) {
|
|
14
|
+
if (!result?.content)
|
|
15
|
+
return "";
|
|
16
|
+
return result.content
|
|
17
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
18
|
+
.map((part) => part.text)
|
|
19
|
+
.join("\n");
|
|
20
|
+
}
|
|
21
|
+
function collapsedResult() {
|
|
22
|
+
return new Container();
|
|
23
|
+
}
|
|
24
|
+
function expandedContent(result, options) {
|
|
25
|
+
if (!options.expanded)
|
|
26
|
+
return collapsedResult();
|
|
27
|
+
const text = contentText(result);
|
|
28
|
+
return text ? new Text(text, 0, 0) : collapsedResult();
|
|
29
|
+
}
|
|
30
|
+
function activityBlock(kind, args, theme, context) {
|
|
31
|
+
const item = toolItem(kind, args);
|
|
32
|
+
const id = liveId(context, item);
|
|
33
|
+
const row = nextGroupedRow(kind, item, id);
|
|
34
|
+
const running = rowIsRunning(context);
|
|
35
|
+
context.state.running = running;
|
|
36
|
+
if (running)
|
|
37
|
+
live.watch(id, context.invalidate);
|
|
38
|
+
else
|
|
39
|
+
live.unwatch(id);
|
|
40
|
+
const color = context.isError ? "error" : running ? "warning" : "dim";
|
|
41
|
+
return {
|
|
42
|
+
render(width) {
|
|
43
|
+
const pad = 1;
|
|
44
|
+
const inner = Math.max(0, width - pad);
|
|
45
|
+
const lines = [];
|
|
46
|
+
if (row.header) {
|
|
47
|
+
const blink = Boolean(context.state.running) && live.has(id);
|
|
48
|
+
const bullet = activityBullet(blink, live.frame);
|
|
49
|
+
lines.push(" ".repeat(pad) + theme.fg(color, truncateToWidth(`${bullet} ${row.header}`, inner)));
|
|
50
|
+
}
|
|
51
|
+
lines.push(" ".repeat(pad) + " " + theme.fg(color, truncateToWidth(row.item, Math.max(0, inner - 2))));
|
|
52
|
+
return lines;
|
|
53
|
+
},
|
|
54
|
+
invalidate() { },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function wrapOne(pi, kind, factory) {
|
|
58
|
+
try {
|
|
59
|
+
const tool = factory();
|
|
60
|
+
pi.registerTool({
|
|
61
|
+
...tool,
|
|
62
|
+
renderShell: "self",
|
|
63
|
+
renderCall(args, theme, context) {
|
|
64
|
+
return activityBlock(kind, args, theme, context);
|
|
65
|
+
},
|
|
66
|
+
renderResult(result, options, _theme, context) {
|
|
67
|
+
const renderContext = context;
|
|
68
|
+
if (!renderContext.isPartial) {
|
|
69
|
+
renderContext.state.done = true;
|
|
70
|
+
renderContext.state.running = false;
|
|
71
|
+
live.unwatch(liveId(renderContext, toolItem(kind, renderContext.args)));
|
|
72
|
+
}
|
|
73
|
+
return expandedContent(result, options);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// A missing built-in tool leaves the rest of the compact renderers available.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function wrapBuiltinTools(pi, ctx) {
|
|
82
|
+
let commandPrefix;
|
|
83
|
+
let shellPath;
|
|
84
|
+
let autoResizeImages;
|
|
85
|
+
try {
|
|
86
|
+
const settings = SettingsManager.create(ctx.cwd, undefined, {
|
|
87
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
88
|
+
});
|
|
89
|
+
commandPrefix = settings.getShellCommandPrefix();
|
|
90
|
+
shellPath = settings.getShellPath();
|
|
91
|
+
autoResizeImages = settings.getImageAutoResize();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Tool wrapping still works when optional shell or image settings are unavailable.
|
|
95
|
+
}
|
|
96
|
+
wrapOne(pi, "bash", () => createBashToolDefinition(ctx.cwd, { commandPrefix, shellPath }));
|
|
97
|
+
wrapOne(pi, "read", () => createReadToolDefinition(ctx.cwd, { autoResizeImages }));
|
|
98
|
+
wrapOne(pi, "edit", () => createEditToolDefinition(ctx.cwd));
|
|
99
|
+
wrapOne(pi, "write", () => createWriteToolDefinition(ctx.cwd));
|
|
100
|
+
wrapOne(pi, "grep", () => createGrepToolDefinition(ctx.cwd));
|
|
101
|
+
wrapOne(pi, "find", () => createFindToolDefinition(ctx.cwd));
|
|
102
|
+
wrapOne(pi, "ls", () => createLsToolDefinition(ctx.cwd));
|
|
103
|
+
}
|
package/dist/tracker.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const PHASES = ["brainstorm", "plan", "implement", "verify", "ship"];
|
|
2
|
+
function readSnapshot(ctx) {
|
|
3
|
+
let phases;
|
|
4
|
+
let tasks;
|
|
5
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
6
|
+
if (entry.type !== "message")
|
|
7
|
+
continue;
|
|
8
|
+
const msg = entry.message;
|
|
9
|
+
if (msg.role !== "toolResult" || !msg.details || msg.details.error)
|
|
10
|
+
continue;
|
|
11
|
+
if (msg.toolName === "phase_tracker" && msg.details.phases) {
|
|
12
|
+
phases = msg.details.phases;
|
|
13
|
+
}
|
|
14
|
+
if (msg.toolName === "plan_tracker" && msg.details.tasks) {
|
|
15
|
+
tasks = msg.details.tasks;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { phases, tasks };
|
|
19
|
+
}
|
|
20
|
+
function phaseActive(phases) {
|
|
21
|
+
return PHASES.some((p) => phases[p]?.status && phases[p].status !== "pending");
|
|
22
|
+
}
|
|
23
|
+
export function formatTrackerLines(ctx, theme) {
|
|
24
|
+
const { phases, tasks } = readSnapshot(ctx);
|
|
25
|
+
const lines = [];
|
|
26
|
+
if (phases && phaseActive(phases)) {
|
|
27
|
+
const parts = PHASES.map((p) => {
|
|
28
|
+
const st = phases[p]?.status || "pending";
|
|
29
|
+
const icon = st === "complete"
|
|
30
|
+
? theme.fg("success", "✓")
|
|
31
|
+
: st === "in_progress"
|
|
32
|
+
? theme.fg("warning", "→")
|
|
33
|
+
: st === "skipped"
|
|
34
|
+
? theme.fg("dim", "⊘")
|
|
35
|
+
: theme.fg("dim", "○");
|
|
36
|
+
const label = st === "skipped" ? theme.fg("dim", p) : p;
|
|
37
|
+
return `${icon} ${label}`;
|
|
38
|
+
});
|
|
39
|
+
lines.push(`${theme.fg("muted", "Phases")} ${parts.join(theme.fg("dim", " → "))}`);
|
|
40
|
+
}
|
|
41
|
+
if (tasks && tasks.length > 0) {
|
|
42
|
+
const done = tasks.filter((t) => t.status === "complete").length;
|
|
43
|
+
const icons = tasks
|
|
44
|
+
.map((t) => {
|
|
45
|
+
if (t.status === "complete")
|
|
46
|
+
return theme.fg("success", "✓");
|
|
47
|
+
if (t.status === "in_progress")
|
|
48
|
+
return theme.fg("warning", "→");
|
|
49
|
+
if (t.status === "failed")
|
|
50
|
+
return theme.fg("error", "✗");
|
|
51
|
+
return theme.fg("dim", "○");
|
|
52
|
+
})
|
|
53
|
+
.join("");
|
|
54
|
+
const current = tasks.find((t) => t.status === "in_progress") ?? tasks.find((t) => t.status === "pending");
|
|
55
|
+
const name = current ? theme.fg("text", ` ${current.name}`) : "";
|
|
56
|
+
lines.push(`${theme.fg("muted", "Tasks")} ${icons} ${theme.fg("dim", `(${done}/${tasks.length})`)}${name}`);
|
|
57
|
+
}
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
export function hideGauntletWidgets(ctx) {
|
|
61
|
+
ctx.ui.setWidget("phase_tracker", undefined);
|
|
62
|
+
ctx.ui.setWidget("plan_tracker", undefined);
|
|
63
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const DETAIL_MAX = 120;
|
|
2
|
+
export function formatElapsed(ms) {
|
|
3
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
4
|
+
const hours = Math.floor(total / 3600);
|
|
5
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
6
|
+
const seconds = total % 60;
|
|
7
|
+
if (hours > 0)
|
|
8
|
+
return `${hours}h ${minutes}m`;
|
|
9
|
+
if (minutes > 0)
|
|
10
|
+
return `${minutes}m ${seconds}s`;
|
|
11
|
+
return `${seconds}s`;
|
|
12
|
+
}
|
|
13
|
+
export function clipDetail(text, max = DETAIL_MAX) {
|
|
14
|
+
const one = text.replace(/\s+/g, " ").trim();
|
|
15
|
+
if (one.length <= max)
|
|
16
|
+
return one;
|
|
17
|
+
if (max <= 1)
|
|
18
|
+
return "…";
|
|
19
|
+
return `${one.slice(0, max - 1).trimEnd()}…`;
|
|
20
|
+
}
|
|
21
|
+
export function lastThoughtLine(message) {
|
|
22
|
+
if (!Array.isArray(message?.content))
|
|
23
|
+
return undefined;
|
|
24
|
+
let thinking = "";
|
|
25
|
+
for (const part of message.content) {
|
|
26
|
+
if (part?.type === "thinking" && typeof part.thinking === "string") {
|
|
27
|
+
thinking = part.thinking;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const lines = thinking
|
|
31
|
+
.split("\n")
|
|
32
|
+
.map((line) => line.replace(/^[#>*\-\d.\s]+/, "").trim())
|
|
33
|
+
.filter((line) => line.length > 2);
|
|
34
|
+
if (lines.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
return clipDetail(lines.slice(-2).join(" · "));
|
|
37
|
+
}
|
|
38
|
+
export function summarizeTool(name, args) {
|
|
39
|
+
const path = (typeof args?.path === "string" && args.path) ||
|
|
40
|
+
(typeof args?.file_path === "string" && args.file_path) ||
|
|
41
|
+
(typeof args?.target_directory === "string" && args.target_directory) ||
|
|
42
|
+
"";
|
|
43
|
+
const pattern = typeof args?.pattern === "string" ? args.pattern : "";
|
|
44
|
+
const command = typeof args?.command === "string" ? args.command.split("\n")[0]?.trim() ?? "" : "";
|
|
45
|
+
const base = path.split("/").filter(Boolean).slice(-2).join("/") || path;
|
|
46
|
+
switch (name) {
|
|
47
|
+
case "bash":
|
|
48
|
+
return command ? `Running ${clipDetail(command, 56)}` : "Running";
|
|
49
|
+
case "read":
|
|
50
|
+
return base ? `Reading ${base}` : "Reading";
|
|
51
|
+
case "edit":
|
|
52
|
+
return base ? `Editing ${base}` : "Editing";
|
|
53
|
+
case "write":
|
|
54
|
+
return base ? `Writing ${base}` : "Writing";
|
|
55
|
+
case "grep":
|
|
56
|
+
return pattern ? `Grep ${clipDetail(pattern, 40)}` : "Grep";
|
|
57
|
+
case "find":
|
|
58
|
+
return pattern ? `Finding ${clipDetail(pattern, 40)}` : "Finding";
|
|
59
|
+
case "ls":
|
|
60
|
+
return base ? `Listing ${base}` : "Listing";
|
|
61
|
+
default:
|
|
62
|
+
return name || "Working";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function formatWorkingMessage(snap) {
|
|
66
|
+
const elapsed = formatElapsed(snap.now - snap.startedAt);
|
|
67
|
+
if (snap.phase === "tool" && snap.tool) {
|
|
68
|
+
const extra = snap.toolCount && snap.toolCount > 1 ? ` · ${snap.toolCount} tools` : "";
|
|
69
|
+
return `${snap.tool}${extra} · ${elapsed}`;
|
|
70
|
+
}
|
|
71
|
+
if (snap.phase === "writing")
|
|
72
|
+
return `Writing · ${elapsed}`;
|
|
73
|
+
if (snap.thought)
|
|
74
|
+
return `Thinking · ${snap.thought} · ${elapsed}`;
|
|
75
|
+
return `Thinking · ${elapsed}`;
|
|
76
|
+
}
|
|
77
|
+
export function installWorkingStatus(pi) {
|
|
78
|
+
let ctx;
|
|
79
|
+
let startedAt = 0;
|
|
80
|
+
let phase = "idle";
|
|
81
|
+
let thought;
|
|
82
|
+
let lastMessage = "";
|
|
83
|
+
let timer;
|
|
84
|
+
const tools = new Map();
|
|
85
|
+
const stop = () => {
|
|
86
|
+
if (timer)
|
|
87
|
+
clearInterval(timer);
|
|
88
|
+
timer = undefined;
|
|
89
|
+
startedAt = 0;
|
|
90
|
+
phase = "idle";
|
|
91
|
+
thought = undefined;
|
|
92
|
+
lastMessage = "";
|
|
93
|
+
tools.clear();
|
|
94
|
+
if (ctx?.mode === "tui")
|
|
95
|
+
ctx.ui.setWorkingMessage();
|
|
96
|
+
};
|
|
97
|
+
const paint = (now = Date.now()) => {
|
|
98
|
+
if (!ctx || ctx.mode !== "tui" || phase === "idle")
|
|
99
|
+
return;
|
|
100
|
+
const next = formatWorkingMessage({
|
|
101
|
+
phase,
|
|
102
|
+
startedAt,
|
|
103
|
+
now,
|
|
104
|
+
thought,
|
|
105
|
+
tool: [...tools.values()].at(-1),
|
|
106
|
+
toolCount: tools.size,
|
|
107
|
+
});
|
|
108
|
+
if (next === lastMessage)
|
|
109
|
+
return;
|
|
110
|
+
lastMessage = next;
|
|
111
|
+
ctx.ui.setWorkingMessage(next);
|
|
112
|
+
};
|
|
113
|
+
const startClock = () => {
|
|
114
|
+
if (!startedAt)
|
|
115
|
+
startedAt = Date.now();
|
|
116
|
+
if (!timer)
|
|
117
|
+
timer = setInterval(() => paint(), 1000);
|
|
118
|
+
};
|
|
119
|
+
pi.on("session_start", (_event, next) => {
|
|
120
|
+
if (next.mode !== "tui")
|
|
121
|
+
return;
|
|
122
|
+
ctx = next;
|
|
123
|
+
});
|
|
124
|
+
pi.on("agent_start", () => {
|
|
125
|
+
if (!ctx || ctx.mode !== "tui")
|
|
126
|
+
return;
|
|
127
|
+
if (phase === "idle") {
|
|
128
|
+
tools.clear();
|
|
129
|
+
thought = undefined;
|
|
130
|
+
lastMessage = "";
|
|
131
|
+
startedAt = Date.now();
|
|
132
|
+
}
|
|
133
|
+
if (tools.size === 0)
|
|
134
|
+
phase = "thinking";
|
|
135
|
+
startClock();
|
|
136
|
+
paint();
|
|
137
|
+
});
|
|
138
|
+
pi.on("turn_start", () => {
|
|
139
|
+
if (phase === "idle")
|
|
140
|
+
return;
|
|
141
|
+
if (tools.size === 0)
|
|
142
|
+
phase = "thinking";
|
|
143
|
+
startClock();
|
|
144
|
+
paint();
|
|
145
|
+
});
|
|
146
|
+
pi.on("message_update", (event) => {
|
|
147
|
+
if (phase === "idle")
|
|
148
|
+
return;
|
|
149
|
+
const ev = event;
|
|
150
|
+
if (ev.message?.role && ev.message.role !== "assistant")
|
|
151
|
+
return;
|
|
152
|
+
const kind = ev.assistantMessageEvent?.type;
|
|
153
|
+
const nextThought = lastThoughtLine(ev.message);
|
|
154
|
+
if (nextThought)
|
|
155
|
+
thought = nextThought;
|
|
156
|
+
if (kind === "text_start" || kind === "text_delta" || kind === "text_end") {
|
|
157
|
+
if (tools.size === 0)
|
|
158
|
+
phase = "writing";
|
|
159
|
+
}
|
|
160
|
+
else if (tools.size === 0) {
|
|
161
|
+
phase = "thinking";
|
|
162
|
+
}
|
|
163
|
+
paint();
|
|
164
|
+
});
|
|
165
|
+
pi.on("tool_execution_start", (event) => {
|
|
166
|
+
if (phase === "idle")
|
|
167
|
+
return;
|
|
168
|
+
const ev = event;
|
|
169
|
+
const id = ev.toolCallId || `${ev.toolName}-${tools.size}`;
|
|
170
|
+
tools.set(id, summarizeTool(ev.toolName || "tool", ev.args));
|
|
171
|
+
phase = "tool";
|
|
172
|
+
paint();
|
|
173
|
+
});
|
|
174
|
+
pi.on("tool_execution_end", (event) => {
|
|
175
|
+
if (phase === "idle")
|
|
176
|
+
return;
|
|
177
|
+
const ev = event;
|
|
178
|
+
if (ev.toolCallId)
|
|
179
|
+
tools.delete(ev.toolCallId);
|
|
180
|
+
phase = tools.size > 0 ? "tool" : "thinking";
|
|
181
|
+
paint();
|
|
182
|
+
});
|
|
183
|
+
pi.on("agent_settled", () => {
|
|
184
|
+
stop();
|
|
185
|
+
});
|
|
186
|
+
pi.on("session_shutdown", () => {
|
|
187
|
+
ctx = undefined;
|
|
188
|
+
stop();
|
|
189
|
+
});
|
|
190
|
+
return stop;
|
|
191
|
+
}
|