@bacnh85/pi-subagent 0.15.0 → 0.15.2
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/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/extensions/index.ts +13 -6
- package/extensions/thread-viewer.ts +60 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.2 (2026-08-18)
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- Colored borders around thread viewer overlay using agent color.
|
|
8
|
+
- Colored scroll-indicator arrows (↑/↓) matching agent color.
|
|
9
|
+
- Scroll offset only resets when switching to a different thread, not on every refresh.
|
|
10
|
+
|
|
11
|
+
## 0.15.1 (2026-08-17)
|
|
12
|
+
|
|
13
|
+
### Improvements
|
|
14
|
+
|
|
15
|
+
- Project-local agent approval is now a single select — **Allow once / Trust for this session / Deny** — instead of a yes/no confirm repeated on every delegation. "Trust for this session" remembers the project agents dir for the session (cleared on `session_start`); dismissed dialogs and Deny cancel the delegation. Headless sessions still fail closed.
|
|
16
|
+
- New test coverage for the approval gate (Deny / dismissed / headless / trust-remembering / session_start clearing).
|
|
17
|
+
|
|
3
18
|
## 0.15.0 (2026-08-09)
|
|
4
19
|
|
|
5
20
|
### Packaging
|
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ Threads are session-memory only and are cleared when Pi replaces or reloads the
|
|
|
91
91
|
Agent files under `.pi/agents/` are controlled by the current repository. A project agent's system prompt may instruct a child to execute shell commands or modify files.
|
|
92
92
|
|
|
93
93
|
- **Project-agent approval cannot be disabled by the model.** The `confirmProjectAgents` parameter is not exposed in the tool schema. Confirmation policy comes from trusted user configuration only.
|
|
94
|
-
- **Interactive sessions** prompt the user before executing project agents.
|
|
94
|
+
- **Interactive sessions** prompt the user before executing project agents (**Allow once / Trust for this session / Deny**); "Trust for this session" remembers the project agents dir until the session ends. Dismissed dialogs and Deny cancel the delegation.
|
|
95
95
|
- **Headless sessions fail closed.** Project agents are not executed without UI confirmation unless the trusted setting `allowUnconfirmedProjectAgents` is enabled (via `PI_SUBAGENT_ALLOW_UNCONFIRMED_PROJECT_AGENTS=true` environment variable or pi settings).
|
|
96
96
|
- **The extension service path** (`pi-subagent:run` event) follows the same policy.
|
|
97
97
|
|
package/agents/planner.md
CHANGED
|
@@ -3,7 +3,7 @@ name: planner
|
|
|
3
3
|
description: Read-only planning and architecture specialist. Use for consequential design, tradeoff analysis, and implementation plans.
|
|
4
4
|
tools: read, grep, find, ls
|
|
5
5
|
models:
|
|
6
|
-
- zai-coding-cn/glm-5.
|
|
6
|
+
- zai-coding-cn/glm-5.3
|
|
7
7
|
- openrouter/nvidia/nemotron-3-ultra-550b-a55b:free
|
|
8
8
|
- opencode-go/deepseek-v4-pro
|
|
9
9
|
thinking: high
|
package/agents/reviewer.md
CHANGED
|
@@ -3,7 +3,7 @@ name: reviewer
|
|
|
3
3
|
description: Code review specialist. Use for correctness, security, regression, and meaningful test-gap review.
|
|
4
4
|
tools: read, grep, find, ls
|
|
5
5
|
models:
|
|
6
|
-
- zai-coding-cn/glm-5.
|
|
6
|
+
- zai-coding-cn/glm-5.3
|
|
7
7
|
- openrouter/nvidia/nemotron-3-ultra-550b-a55b:free
|
|
8
8
|
- opencode-go/deepseek-v4-pro
|
|
9
9
|
thinking: high
|
package/extensions/index.ts
CHANGED
|
@@ -95,6 +95,9 @@ function getTrustedConfig(ctx: ExtensionContext): { allowUnconfirmedProjectAgent
|
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** Session-scoped approvals for project-local agents ("Trust for this session"). */
|
|
99
|
+
const trustedProjectAgentDirs = new Set<string>();
|
|
100
|
+
|
|
98
101
|
|
|
99
102
|
// ---------------------------------------------------------------------------
|
|
100
103
|
// Tool parameter schema
|
|
@@ -186,6 +189,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
186
189
|
currentCtx = ctx;
|
|
187
190
|
if (event.reason === "reload") invalidateAgentCache();
|
|
188
191
|
threadStore.clear();
|
|
192
|
+
trustedProjectAgentDirs.clear();
|
|
189
193
|
// Clear any widget from a prior session.
|
|
190
194
|
widget.clearWidgetIfIdle();
|
|
191
195
|
// Mark prior-session running tasks as interrupted (we can't resume them).
|
|
@@ -591,19 +595,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
591
595
|
|
|
592
596
|
if (projectAgentsRequested.length > 0) {
|
|
593
597
|
if (confirmProjectAgents) {
|
|
594
|
-
|
|
598
|
+
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
599
|
+
if (trustedProjectAgentDirs.has(dir)) {
|
|
600
|
+
// Previously approved "Trust for this session" for this agents dir.
|
|
601
|
+
} else if (ctx.hasUI) {
|
|
595
602
|
const names = projectAgentsRequested.map((a) => a.name).join(", ");
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
"
|
|
599
|
-
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
603
|
+
const choice = await ctx.ui.select(
|
|
604
|
+
`Run project-local agents?\n\nAgents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
605
|
+
["Allow once", "Trust for this session", "Deny"],
|
|
600
606
|
);
|
|
601
|
-
if (
|
|
607
|
+
if (choice !== "Allow once" && choice !== "Trust for this session") {
|
|
602
608
|
return {
|
|
603
609
|
content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
|
|
604
610
|
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
|
605
611
|
};
|
|
606
612
|
}
|
|
613
|
+
if (choice === "Trust for this session") trustedProjectAgentDirs.add(dir);
|
|
607
614
|
} else {
|
|
608
615
|
// Fail closed in headless sessions.
|
|
609
616
|
return {
|
|
@@ -35,6 +35,12 @@ export interface ThreadViewerCallbacks {
|
|
|
35
35
|
|
|
36
36
|
/** Viewport height for the overlay (estimated lines). Must be > 3. */
|
|
37
37
|
const OVERLAY_HEIGHT = 24;
|
|
38
|
+
const borderSize = 1;
|
|
39
|
+
const truncateFit = function (text: string, width: number): string {
|
|
40
|
+
text = " " + text;
|
|
41
|
+
return truncateToWidth(text, width - ((borderSize*2) +1), "...", true) + " ";
|
|
42
|
+
|
|
43
|
+
}
|
|
38
44
|
|
|
39
45
|
export class ThreadViewer {
|
|
40
46
|
private thread: SubagentThread;
|
|
@@ -44,13 +50,17 @@ export class ThreadViewer {
|
|
|
44
50
|
private cachedWidth?: number;
|
|
45
51
|
private cachedUpdatedAt?: number;
|
|
46
52
|
private cachedLines?: string[];
|
|
53
|
+
private lastThreadId: string | null = null;
|
|
47
54
|
|
|
48
55
|
constructor(thread: SubagentThread, callbacks: ThreadViewerCallbacks, theme: ViewerTheme) {
|
|
49
56
|
this.thread = thread;
|
|
50
57
|
this.callbacks = callbacks;
|
|
51
58
|
this.theme = theme;
|
|
59
|
+
this.lastThreadId = thread.id;
|
|
60
|
+
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
|
|
54
64
|
handleInput(data: string): void {
|
|
55
65
|
if (matchesKey(data, Key.escape)) {
|
|
56
66
|
this.callbacks.onClose();
|
|
@@ -58,14 +68,12 @@ export class ThreadViewer {
|
|
|
58
68
|
}
|
|
59
69
|
if (matchesKey(data, Key.alt("left"))) {
|
|
60
70
|
if (this.callbacks.hasPrev) {
|
|
61
|
-
this.scrollOffset = 0;
|
|
62
71
|
this.callbacks.onPrev();
|
|
63
72
|
}
|
|
64
73
|
return;
|
|
65
74
|
}
|
|
66
75
|
if (matchesKey(data, Key.alt("right"))) {
|
|
67
76
|
if (this.callbacks.hasNext) {
|
|
68
|
-
this.scrollOffset = 0;
|
|
69
77
|
this.callbacks.onNext();
|
|
70
78
|
}
|
|
71
79
|
return;
|
|
@@ -131,19 +139,19 @@ export class ThreadViewer {
|
|
|
131
139
|
const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
|
|
132
140
|
header += ` ${t.fg(reasonColor, `[${result.stopReason}]`)}`;
|
|
133
141
|
}
|
|
134
|
-
lines.push(
|
|
142
|
+
lines.push(truncateFit(header, width));
|
|
135
143
|
|
|
136
144
|
// Error message
|
|
137
145
|
if (result && isErr && result.errorMessage) {
|
|
138
146
|
const msgColor = result.stopReason === "timeout" ? "warning" : "error";
|
|
139
|
-
lines.push(
|
|
147
|
+
lines.push(truncateFit(t.fg(msgColor, `Error: ${result.errorMessage}`), width));
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
lines.push("");
|
|
143
151
|
|
|
144
152
|
// Task
|
|
145
|
-
lines.push(
|
|
146
|
-
lines.push(
|
|
153
|
+
lines.push(truncateFit(t.fg("muted", "─── Task ───"), width));
|
|
154
|
+
lines.push(truncateFit(t.fg("dim", this.thread.task), width));
|
|
147
155
|
lines.push("");
|
|
148
156
|
|
|
149
157
|
if (status === "running") {
|
|
@@ -152,18 +160,18 @@ export class ThreadViewer {
|
|
|
152
160
|
const activity = this.thread.lastActivityAt ? `${Math.floor((now - this.thread.lastActivityAt) / 1000)}s ago (${this.thread.lastActivityLabel})` : "none yet";
|
|
153
161
|
const idleMs = this.thread.inactivityDeadline ? this.thread.inactivityDeadline - now : 0;
|
|
154
162
|
const idle = this.thread.inactivityDeadline ? `${Math.max(0, Math.ceil(idleMs / 1000))}s remaining` : "pending";
|
|
155
|
-
lines.push(
|
|
163
|
+
lines.push(truncateFit(t.fg(idleMs < 30_000 ? "warning" : "muted", `Elapsed ${elapsed}s · last activity ${activity} · idle ${idle}`), width));
|
|
156
164
|
}
|
|
157
165
|
if (status === "running" && (!result || result.messages.length === 0)) {
|
|
158
|
-
lines.push(
|
|
166
|
+
lines.push(truncateFit(t.fg("muted", "(waiting for first message...)"), width));
|
|
159
167
|
} else if (result) {
|
|
160
168
|
const displayItems = getDisplayItems(result.messages);
|
|
161
169
|
const finalOutput = getFinalOutput(result.messages);
|
|
162
170
|
|
|
163
|
-
lines.push(
|
|
171
|
+
lines.push(truncateFit(t.fg("muted", "─── Output ───"), width));
|
|
164
172
|
|
|
165
173
|
if (displayItems.length === 0 && !finalOutput) {
|
|
166
|
-
lines.push(
|
|
174
|
+
lines.push(truncateFit(t.fg("muted", "(no output)"), width));
|
|
167
175
|
} else {
|
|
168
176
|
const mdTheme = getMarkdownTheme();
|
|
169
177
|
|
|
@@ -171,7 +179,7 @@ export class ThreadViewer {
|
|
|
171
179
|
for (const item of displayItems) {
|
|
172
180
|
if (item.type === "toolCall") {
|
|
173
181
|
lines.push(
|
|
174
|
-
|
|
182
|
+
truncateFit(
|
|
175
183
|
t.fg("muted", "→ ") + formatToolCall(item.name, item.args, t.fg.bind(t)),
|
|
176
184
|
width,
|
|
177
185
|
),
|
|
@@ -182,7 +190,7 @@ export class ThreadViewer {
|
|
|
182
190
|
const md = new Markdown(item.text.trim(), 0, 0, mdTheme);
|
|
183
191
|
const mdLines = md.render(contentWidth);
|
|
184
192
|
for (const mdLine of mdLines) {
|
|
185
|
-
lines.push(` ${
|
|
193
|
+
lines.push(` ${truncateFit(mdLine, contentWidth)}`);
|
|
186
194
|
}
|
|
187
195
|
}
|
|
188
196
|
}
|
|
@@ -194,7 +202,7 @@ export class ThreadViewer {
|
|
|
194
202
|
const contentWidth = Math.max(1, width - 2);
|
|
195
203
|
const md = new Markdown(finalOutput.trim(), 0, 0, mdTheme);
|
|
196
204
|
for (const mdLine of md.render(contentWidth)) {
|
|
197
|
-
lines.push(` ${
|
|
205
|
+
lines.push(` ${truncateFit(mdLine, contentWidth)}`);
|
|
198
206
|
}
|
|
199
207
|
}
|
|
200
208
|
}
|
|
@@ -203,7 +211,7 @@ export class ThreadViewer {
|
|
|
203
211
|
const usageStr = formatUsageStats(result.usage, result.model);
|
|
204
212
|
if (usageStr) {
|
|
205
213
|
lines.push("");
|
|
206
|
-
lines.push(
|
|
214
|
+
lines.push(truncateFit(t.fg("dim", usageStr), width));
|
|
207
215
|
}
|
|
208
216
|
}
|
|
209
217
|
|
|
@@ -215,7 +223,7 @@ export class ThreadViewer {
|
|
|
215
223
|
if (this.callbacks.hasPrev) navParts.push("alt+← prev");
|
|
216
224
|
if (this.callbacks.hasNext) navParts.push("alt+→ next");
|
|
217
225
|
navParts.push("↑↓ scroll");
|
|
218
|
-
lines.push(
|
|
226
|
+
lines.push(truncateFit(t.fg("dim", navParts.join(" · ")), width));
|
|
219
227
|
|
|
220
228
|
this.cachedLines = lines;
|
|
221
229
|
this.cachedWidth = width;
|
|
@@ -227,6 +235,7 @@ export class ThreadViewer {
|
|
|
227
235
|
private renderVisible(allLines: string[], width: number): string[] {
|
|
228
236
|
const total = allLines.length;
|
|
229
237
|
const maxVisible = Math.max(3, OVERLAY_HEIGHT);
|
|
238
|
+
const color = this.thread.color ?? "accent";
|
|
230
239
|
|
|
231
240
|
// Clamp scrollOffset so the last page shows a full viewport minus one indicator line
|
|
232
241
|
const maxOffset =
|
|
@@ -246,21 +255,47 @@ export class ThreadViewer {
|
|
|
246
255
|
|
|
247
256
|
// Scroll indicator at top
|
|
248
257
|
if (aboveShown) {
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
const abmsg = this.theme.fg(color, `↑ ${offset}`) + this.theme.fg("muted", ` more lines above`);
|
|
259
|
+
visible.unshift(truncateFit(
|
|
260
|
+
abmsg,
|
|
251
261
|
width,
|
|
252
262
|
));
|
|
253
263
|
}
|
|
254
264
|
// Scroll indicator at bottom
|
|
255
265
|
if (belowShown) {
|
|
256
266
|
const remaining = total - offset - bodyHeight;
|
|
257
|
-
|
|
258
|
-
|
|
267
|
+
const remmsg = this.theme.fg(color, `↓ ${remaining}`) + this.theme.fg("muted", ` more lines below`);
|
|
268
|
+
visible.push(truncateFit(
|
|
269
|
+
remmsg,
|
|
259
270
|
width,
|
|
260
271
|
));
|
|
261
272
|
}
|
|
262
273
|
|
|
263
|
-
|
|
274
|
+
// Add border around the visible content
|
|
275
|
+
const borderWidth = width - 2;
|
|
276
|
+
const borderedLines: string[] = [];
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
// Top border with colored characters
|
|
280
|
+
const borderColor = this.theme.fg(color, "─");
|
|
281
|
+
const cornerColor = this.theme.fg(color, "┌");
|
|
282
|
+
const bottomCornerColor = this.theme.fg(color, "└");
|
|
283
|
+
const sideBorder = this.theme.fg(color, "│");
|
|
284
|
+
borderedLines.push(cornerColor + borderColor.repeat(borderWidth) + this.theme.fg(color, "┐"));
|
|
285
|
+
|
|
286
|
+
// Content with side borders
|
|
287
|
+
for (const line of visible) {
|
|
288
|
+
let borderedLine = sideBorder + line + sideBorder;
|
|
289
|
+
if (line.trim() === "") {
|
|
290
|
+
borderedLine = sideBorder + " ".repeat(Math.max(0, width - ( borderSize * 2))) + sideBorder;
|
|
291
|
+
}
|
|
292
|
+
borderedLines.push(borderedLine);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Bottom border with colored characters
|
|
296
|
+
borderedLines.push(bottomCornerColor + borderColor.repeat(borderWidth) + this.theme.fg(color, "┘"));
|
|
297
|
+
|
|
298
|
+
return borderedLines;
|
|
264
299
|
}
|
|
265
300
|
|
|
266
301
|
invalidate(): void {
|
|
@@ -273,7 +308,11 @@ export class ThreadViewer {
|
|
|
273
308
|
setThread(thread: SubagentThread, callbacks: ThreadViewerCallbacks): void {
|
|
274
309
|
this.thread = thread;
|
|
275
310
|
this.callbacks = callbacks;
|
|
276
|
-
|
|
311
|
+
// Only reset scrollOffset when switching to a different thread
|
|
312
|
+
if (this.lastThreadId !== thread.id) {
|
|
313
|
+
this.scrollOffset = 0;
|
|
314
|
+
this.lastThreadId = thread.id;
|
|
315
|
+
}
|
|
277
316
|
this.invalidate();
|
|
278
317
|
}
|
|
279
318
|
}
|
package/package.json
CHANGED