@bacnh85/pi-subagent 0.15.1 → 0.15.3
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 +14 -0
- package/README.md +2 -2
- package/extensions/index.ts +10 -4
- package/extensions/runner.ts +6 -2
- package/extensions/security.ts +79 -3
- package/extensions/thread-viewer.ts +60 -21
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.3 (2026-08-20)
|
|
4
|
+
### Improvements
|
|
5
|
+
- Timeouts have been extracted as Environment Variables enabling overriding.
|
|
6
|
+
- `PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS` default : 3 Mins
|
|
7
|
+
- `PI_SUBAGENT_HARD_TIMEOUT_MINS` default: 20 Mins
|
|
8
|
+
|
|
9
|
+
## 0.15.2 (2026-08-18)
|
|
10
|
+
|
|
11
|
+
### Improvements
|
|
12
|
+
|
|
13
|
+
- Colored borders around thread viewer overlay using agent color.
|
|
14
|
+
- Colored scroll-indicator arrows (↑/↓) matching agent color.
|
|
15
|
+
- Scroll offset only resets when switching to a different thread, not on every refresh.
|
|
16
|
+
|
|
3
17
|
## 0.15.1 (2026-08-17)
|
|
4
18
|
|
|
5
19
|
### Improvements
|
package/README.md
CHANGED
|
@@ -140,8 +140,8 @@ each writes into its own checkout.
|
|
|
140
140
|
|
|
141
141
|
Every child execution receives a timeout:
|
|
142
142
|
|
|
143
|
-
- **Default inactivity window:** 3 minutes (`DEFAULT_TIMEOUT_MS`); real SDK lifecycle activity resets it.
|
|
144
|
-
- **Absolute cap:** 20 minutes for every child, even when active.
|
|
143
|
+
- **Default inactivity window:** 3 minutes (`DEFAULT_TIMEOUT_MS`); real SDK lifecycle activity resets it. Overridable with Environment Variable `PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS`
|
|
144
|
+
- **Absolute cap:** Default 20 minutes for every child, even when active. Overridable with Environment variable `PI_SUBAGENT_HARD_TIMEOUT_MINS`
|
|
145
145
|
- **Maximum requested inactivity window:** 60 minutes (`MAX_TIMEOUT_MS`); values must be positive integers.
|
|
146
146
|
- Timeout diagnostics distinguish `Idle timeout` from `Hard timeout` and parent cancellation.
|
|
147
147
|
- 30-second progress heartbeats only keep the parent transport alive; they never reset inactivity.
|
package/extensions/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
startHeartbeat,
|
|
40
40
|
} from "./runner.ts";
|
|
41
41
|
import {
|
|
42
|
+
flushWarnings,
|
|
42
43
|
isRateLimitError,
|
|
43
44
|
normalizeTimeout,
|
|
44
45
|
resolveSafeCwd,
|
|
@@ -107,14 +108,14 @@ const TaskItem = Type.Object({
|
|
|
107
108
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
108
109
|
task: Type.String({ description: "Task to delegate to the agent" }),
|
|
109
110
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent" })),
|
|
110
|
-
timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in
|
|
111
|
+
timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in ms; aborts on no activity within timeout. Default: 3 min (PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS). The agent always has a lifetime cap: default 20 min or (PI_SUBAGENT_HARD_TIMEOUT_MINS)." })),
|
|
111
112
|
});
|
|
112
113
|
|
|
113
114
|
const ChainItem = Type.Object({
|
|
114
115
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
115
116
|
task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
|
|
116
117
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent" })),
|
|
117
|
-
timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in
|
|
118
|
+
timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in ms; aborts on no activity within timeout. Default: 3 min (PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS). The agent always has a lifetime cap: default 20 min or (PI_SUBAGENT_HARD_TIMEOUT_MINS)." })),
|
|
118
119
|
});
|
|
119
120
|
|
|
120
121
|
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
@@ -153,7 +154,7 @@ const SubagentParams = Type.Object({
|
|
|
153
154
|
// Project-agent confirmation is enforced via trusted configuration.
|
|
154
155
|
// See Security model section in README.
|
|
155
156
|
cwd: Type.Optional(Type.String({ description: "Working directory (single mode, must be inside workspace)" })),
|
|
156
|
-
timeout: Type.Optional(Type.Number({ description: "
|
|
157
|
+
timeout: Type.Optional(Type.Number({ description: "Inactivity timeout for the whole run, in ms; resets on activity, aborts on silence. Default 3 min (PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS). Lifetime cap: 20 min or (PI_SUBAGENT_HARD_TIMEOUT_MINS)." })),
|
|
157
158
|
instructions: Type.Optional(Type.String({ description: "Bounded repository/task instructions passed to each child (max 16 KB)" })),
|
|
158
159
|
abortOnFailure: Type.Optional(Type.Boolean({ description: "In parallel mode, cancel remaining tasks when one fails. Default: false.", default: false })),
|
|
159
160
|
});
|
|
@@ -477,6 +478,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
477
478
|
"Use /subagent to list all available agents or /subagent <name> for agent details.",
|
|
478
479
|
],
|
|
479
480
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
481
|
+
// Surface env-var timeout warnings collected at module load. The
|
|
482
|
+
// interactive TUI swallows module-load stderr, so notify on launch.
|
|
483
|
+
for (const msg of flushWarnings()) {
|
|
484
|
+
ctx.ui?.notify?.(msg, "warning");
|
|
485
|
+
}
|
|
480
486
|
const agentScope: AgentScope = params.agentScope ?? "user";
|
|
481
487
|
const discovery = discoverAgents(ctx.cwd, agentScope, bundledAgentsDir);
|
|
482
488
|
const agents = discovery.agents;
|
|
@@ -1711,4 +1717,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
1711
1717
|
}
|
|
1712
1718
|
}, { overlay: true, overlayOptions: { maxHeight: "70%" } }); // Overlay: editor stays visible below
|
|
1713
1719
|
}
|
|
1714
|
-
}
|
|
1720
|
+
}
|
package/extensions/runner.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
classifyStopReason,
|
|
31
31
|
createCombinedAbortSignal,
|
|
32
32
|
type SubagentStatus,
|
|
33
|
+
DEFAULT_TIMEOUT_MS,
|
|
34
|
+
HARD_TIMEOUT_MS,
|
|
33
35
|
} from "./security.ts";
|
|
34
36
|
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
@@ -46,8 +48,10 @@ export interface UsageStats {
|
|
|
46
48
|
turns: number;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export
|
|
50
|
-
export const
|
|
51
|
+
/** Re-export from security.ts — single source of truth for both timeouts. */
|
|
52
|
+
export const DEFAULT_INACTIVITY_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
|
|
53
|
+
export { HARD_TIMEOUT_MS };
|
|
54
|
+
|
|
51
55
|
|
|
52
56
|
// ---------------------------------------------------------------------------
|
|
53
57
|
// Extension resource loader
|
package/extensions/security.ts
CHANGED
|
@@ -65,10 +65,57 @@ export const MUTATION_TOOLS: readonly string[] = ["edit", "write"];
|
|
|
65
65
|
export const EXECUTION_TOOLS: readonly string[] = ["bash"];
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* Generic warning sink for configuration problems. Stderr so it is visible
|
|
69
|
+
* with or without a TUI attached. Warnings are also buffered so the extension
|
|
70
|
+
* host can surface them as TUI notifications — the interactive TUI swallows
|
|
71
|
+
* module-load stderr, so module-load warnings alone are invisible in-TUI.
|
|
72
|
+
*/
|
|
73
|
+
const warnings: string[] = [];
|
|
74
|
+
function warn(message: string): void {
|
|
75
|
+
process.stderr.write(`[pi-subagent] ${message}\n`);
|
|
76
|
+
warnings.push(message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Flush env-var config warnings collected at module load. Called once from
|
|
81
|
+
* the /subagent tool handler so the warnings surface as TUI notifications
|
|
82
|
+
* (module-load stderr is not visible inside the interactive TUI).
|
|
70
83
|
*/
|
|
71
|
-
export
|
|
84
|
+
export function flushWarnings(): string[] {
|
|
85
|
+
return warnings.splice(0, warnings.length);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read a numeric value from an env var, with validation:
|
|
90
|
+
* - undefined/empty -> defaultValue (no warning)
|
|
91
|
+
* - not a finite number (NaN) -> defaultValue + warning
|
|
92
|
+
* - below `min` -> `min` + warning
|
|
93
|
+
* - above `max` -> `max` + warning
|
|
94
|
+
* - otherwise -> the parsed value (no warning)
|
|
95
|
+
*/
|
|
96
|
+
export function getNumericEnvVar(
|
|
97
|
+
envVar: string,
|
|
98
|
+
defaultValue: number,
|
|
99
|
+
min: number,
|
|
100
|
+
max: number,
|
|
101
|
+
): number {
|
|
102
|
+
const raw = process.env[envVar];
|
|
103
|
+
if (raw === undefined || raw === "") return defaultValue;
|
|
104
|
+
const value = Number(raw);
|
|
105
|
+
if (!Number.isFinite(value)) {
|
|
106
|
+
warn(`ENV VAR: ${envVar}="${raw}" is not a number; using default ${defaultValue}.`);
|
|
107
|
+
return defaultValue;
|
|
108
|
+
}
|
|
109
|
+
if (value < min) {
|
|
110
|
+
warn(`ENV VAR: ${envVar}=${value} is below the minimum of ${min}; using ${min}.`);
|
|
111
|
+
return min;
|
|
112
|
+
}
|
|
113
|
+
if (value > max) {
|
|
114
|
+
warn(`ENV VAR: ${envVar}=${value} is above the maximum of ${max}; using ${max}.`);
|
|
115
|
+
return max;
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
72
119
|
|
|
73
120
|
/**
|
|
74
121
|
* Absolute maximum timeout. Any requested value above this cap is rejected
|
|
@@ -76,6 +123,35 @@ export const DEFAULT_TIMEOUT_MS = 3 * 60 * 1_000; // 3 minutes
|
|
|
76
123
|
*/
|
|
77
124
|
export const MAX_TIMEOUT_MS = 60 * 60 * 1_000; // 60 minutes
|
|
78
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Timeout caps, in minutes, shared by getNumericEnvVar for both env vars.
|
|
128
|
+
* No timeout may be shorter than 1 minute; the inactivity window may not
|
|
129
|
+
* exceed the 60-minute absolute maximum.
|
|
130
|
+
*/
|
|
131
|
+
const MIN_TIMEOUT_MINS = 1;
|
|
132
|
+
const MAX_TIMEOUT_MINS = MAX_TIMEOUT_MS / 60_000;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Default inactivity timeout. Real SDK lifecycle activity resets this window;
|
|
136
|
+
* the runner enforces a fixed absolute cap (HARD_TIMEOUT_MS).
|
|
137
|
+
*
|
|
138
|
+
* Reads PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS (default 3 min, range 1–60) and
|
|
139
|
+
* PI_SUBAGENT_HARD_TIMEOUT_MINS (default 20 min, range 1–60) from env via
|
|
140
|
+
* getNumericEnvVar, which warns on invalid/out-of-range values. The hard cap
|
|
141
|
+
* is additionally clamped up to the inactivity window: a lifetime cap smaller
|
|
142
|
+
* than the idle window is nonsensical.
|
|
143
|
+
*/
|
|
144
|
+
const INACTIVITY_TIMEOUT_MINS = getNumericEnvVar("PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS", 3, MIN_TIMEOUT_MINS, MAX_TIMEOUT_MINS);
|
|
145
|
+
let hardTimeoutMins = getNumericEnvVar("PI_SUBAGENT_HARD_TIMEOUT_MINS", 20, MIN_TIMEOUT_MINS, MAX_TIMEOUT_MINS);
|
|
146
|
+
if (hardTimeoutMins < INACTIVITY_TIMEOUT_MINS) {
|
|
147
|
+
warn(
|
|
148
|
+
`PI_SUBAGENT_HARD_TIMEOUT_MINS (${hardTimeoutMins}) is below the inactivity window (${INACTIVITY_TIMEOUT_MINS}); raising hard cap to ${INACTIVITY_TIMEOUT_MINS}.`,
|
|
149
|
+
);
|
|
150
|
+
hardTimeoutMins = INACTIVITY_TIMEOUT_MINS;
|
|
151
|
+
}
|
|
152
|
+
export const DEFAULT_TIMEOUT_MS = INACTIVITY_TIMEOUT_MINS * 60 * 1_000;
|
|
153
|
+
export const HARD_TIMEOUT_MS = hardTimeoutMins * 60 * 1_000;
|
|
154
|
+
|
|
79
155
|
// ---------------------------------------------------------------------------
|
|
80
156
|
// Canonical result status
|
|
81
157
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-subagent",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"description": "In-process subagents for Pi with isolated SDK sessions, parallel and chained delegation, and inspectable threads.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,9 +73,15 @@
|
|
|
73
73
|
"@earendil-works/pi-tui": "^0.84.0",
|
|
74
74
|
"@types/mocha": "^10.0.10",
|
|
75
75
|
"@types/node": "^20.19.43",
|
|
76
|
-
"mocha": "^
|
|
76
|
+
"mocha": "^11.8.0",
|
|
77
77
|
"tsx": "^4.22.4",
|
|
78
78
|
"typescript": "^5.9.3",
|
|
79
79
|
"typebox": "^1.3.1"
|
|
80
|
+
},
|
|
81
|
+
"overrides": {
|
|
82
|
+
"serialize-javascript@>=5.0.0 <7.0.5": "^7.0.5",
|
|
83
|
+
"js-yaml@>=4.0.0 <4.3.1": "^4.3.1",
|
|
84
|
+
"brace-expansion@>=2.0.0 <2.1.4": "^2.1.4",
|
|
85
|
+
"diff": "^8.0.3"
|
|
80
86
|
}
|
|
81
87
|
}
|