pi-better-harness 0.1.18 → 0.1.20
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.
|
@@ -23,12 +23,16 @@ const SshSchema = Type.Object({
|
|
|
23
23
|
identity_file: Type.Optional(Type.String({ description: "SSH identity file path." })),
|
|
24
24
|
jump: Type.Optional(Type.String({ description: "SSH jump host passed with -J." })),
|
|
25
25
|
options: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Additional SSH -o key/value options. Agent-safe defaults remain enforced." })),
|
|
26
|
+
}, {
|
|
27
|
+
description: "Structured SSH connection for remote background tasks. Set ssh instead of wrapping command in a hand-written ssh command; command is the remote command, and Pi keeps durable local logs, status, callbacks, and remote stop semantics.",
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
const RemoteSchema = Type.Object({
|
|
29
31
|
session: Type.Optional(Type.Union([Type.Literal("tmux"), Type.Literal("direct")], { description: "Remote session mode. SSH spawn defaults to durable tmux. Watch always uses direct one-shot polls. Explicit direct spawn has weaker stop semantics." })),
|
|
30
32
|
install_tmux: Type.Optional(Type.Boolean({ description: "Allow SSH spawn to install tmux non-interactively when missing. Defaults true in tmux mode and is ignored for watch and direct spawn." })),
|
|
31
33
|
workdir: Type.Optional(Type.String({ description: "Remote working directory for the spawned command." })),
|
|
34
|
+
}, {
|
|
35
|
+
description: "Remote execution controls used with ssh. Omit session for SSH spawn to get the durable tmux default; SSH watch runs direct one-shot polls regardless of session and does not install tmux.",
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
const CommandFields = {
|
|
@@ -117,7 +121,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
117
121
|
pi.registerTool({
|
|
118
122
|
name: "bg_task_spawn",
|
|
119
123
|
label: "BG Spawn",
|
|
120
|
-
description: "Start a long-running background process and return immediately with its task id. For remote work, prefer structured ssh: spawn defaults to a remote tmux session with durable local logs and real remote stop. If tmux is missing, the preset attempts to install tmux non-interactively and fails closed with operator guidance when setup cannot proceed. Explicit remote.session=direct skips tmux, but direct mode has weaker stop semantics and may leave the remote process running. Never wait or poll in the foreground.",
|
|
124
|
+
description: "Start a long-running background process and return immediately with its task id. For remote work, prefer structured ssh: pass ssh:{host,user} and put the remote command in command; spawn defaults to a remote tmux session with durable local logs and real remote stop. If tmux is missing, the preset attempts to install tmux non-interactively and fails closed with operator guidance when setup cannot proceed. Explicit remote.session=direct skips tmux, but direct mode has weaker stop semantics and may leave the remote process running. Never wait or poll in the foreground.",
|
|
121
125
|
parameters: SpawnParams,
|
|
122
126
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
123
127
|
activeSession = getCallbackOrigin(ctx);
|
|
@@ -130,7 +134,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
130
134
|
pi.registerTool({
|
|
131
135
|
name: "bg_task_watch",
|
|
132
136
|
label: "BG Watch",
|
|
133
|
-
description: "Poll a command in the background until success_when, failure_when, or timeout matches. For remote work,
|
|
137
|
+
description: "Poll a command in the background until success_when, failure_when, or timeout matches. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command; each interval opens a direct one-shot SSH poll without tmux installation. Returns immediately with its task id. Default timeout 900 seconds; pass timeout_seconds:0 to disable.",
|
|
134
138
|
parameters: WatchParams,
|
|
135
139
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
136
140
|
activeSession = getCallbackOrigin(ctx);
|
|
@@ -190,7 +194,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
190
194
|
pi.registerTool({
|
|
191
195
|
name: "bg_task",
|
|
192
196
|
label: "BG Task",
|
|
193
|
-
description: "Action wrapper for background tasks: spawn, watch, list, status, log, stop, or clear. For remote work,
|
|
197
|
+
description: "Action wrapper for background tasks: spawn, watch, list, status, log, stop, or clear. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command. SSH spawn defaults to durable tmux; SSH watches use direct one-shot polls without tmux installation; remote.session=direct is a weaker-stop spawn escape hatch. Spawn/watch return immediately; do not poll in foreground. For action:status, default compact output and use verbose:true only for full metadata. For action:log, default compact tail and use tail_lines:0 only for explicit full logs.",
|
|
194
198
|
parameters: ActionParams,
|
|
195
199
|
renderResult(result: unknown, options: unknown, theme: unknown) {
|
|
196
200
|
return renderBackgroundTaskLogDisplay(result, options, theme);
|
|
@@ -164,7 +164,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
164
164
|
let continuationQueuedFor: string | null = null;
|
|
165
165
|
let idleContinuationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
166
166
|
let idleContinuationSignature = "";
|
|
167
|
-
let refreshGoalWidget: (() => void) | undefined;
|
|
167
|
+
let refreshGoalWidget: ((force?: boolean) => void) | undefined;
|
|
168
168
|
let lastAgentEvidence: ContinuationEvidence | null = null;
|
|
169
169
|
|
|
170
170
|
const getGoal = (ctx: ExtensionContext): GoalSnapshot | null => currentGoalSnapshot(ctx);
|
|
@@ -180,21 +180,53 @@ export default function (pi: ExtensionAPI): void {
|
|
|
180
180
|
appendContinuationState(createContinuationState(goal.goalId));
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
const isForegroundBusy = (ctx: ExtensionContext): boolean =>
|
|
184
|
+
foregroundRunning || !ctx.isIdle();
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Chat `notify` appends lines above the Working/bash status region. While the
|
|
188
|
+
* agent is streaming, that height change desyncs Pi main-screen differential
|
|
189
|
+
* rendering and stacks "Working..." / "Elapsed Xs" into scrollback. Keep
|
|
190
|
+
* mid-stream feedback out of the transcript; use the footer status instead.
|
|
191
|
+
*/
|
|
192
|
+
const notifyGoal = (
|
|
193
|
+
ctx: ExtensionContext,
|
|
194
|
+
message: string,
|
|
195
|
+
type: "info" | "warning" | "error" = "info",
|
|
196
|
+
): void => {
|
|
197
|
+
if (!ctx.hasUI) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (isForegroundBusy(ctx) && type !== "error") {
|
|
201
|
+
try {
|
|
202
|
+
ctx.ui.setStatus(EXTENSION_NAME, message);
|
|
203
|
+
} catch {
|
|
204
|
+
// Footer status is best-effort only.
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
ctx.ui.notify(message, type);
|
|
209
|
+
};
|
|
210
|
+
|
|
183
211
|
const setGoal = (goal: GoalSnapshot, ctx: ExtensionContext, source: GoalEntrySource): void => {
|
|
212
|
+
const previous = getGoal(ctx);
|
|
213
|
+
const wasVisible = previous !== null && isGoalClockVisible(previous);
|
|
184
214
|
pi.appendEntry(EXTENSION_NAME, goalSetEntry(goal, source));
|
|
185
215
|
continuationQueuedFor = null;
|
|
186
216
|
backgroundDrainTracker = null;
|
|
187
217
|
clearIdleContinuation();
|
|
188
|
-
|
|
218
|
+
// Force a full redraw when the dock height changes (absent ↔ visible clock).
|
|
219
|
+
refreshGoalWidget?.(!wasVisible || !isGoalClockVisible(goal));
|
|
189
220
|
};
|
|
190
221
|
|
|
191
222
|
const clearGoal = (ctx: ExtensionContext, source: GoalEntrySource): void => {
|
|
192
223
|
const current = getGoal(ctx);
|
|
224
|
+
const wasVisible = current !== null && isGoalClockVisible(current);
|
|
193
225
|
pi.appendEntry(EXTENSION_NAME, goalClearEntry(current?.goalId ?? null, source));
|
|
194
226
|
continuationQueuedFor = null;
|
|
195
227
|
backgroundDrainTracker = null;
|
|
196
228
|
clearIdleContinuation();
|
|
197
|
-
refreshGoalWidget?.();
|
|
229
|
+
refreshGoalWidget?.(wasVisible);
|
|
198
230
|
};
|
|
199
231
|
|
|
200
232
|
const queueGoalContinuation = (goal: GoalSnapshot): void => {
|
|
@@ -395,8 +427,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
395
427
|
ctx.ui.setWidget(
|
|
396
428
|
EXTENSION_NAME,
|
|
397
429
|
(tui, theme) => {
|
|
398
|
-
const
|
|
399
|
-
|
|
430
|
+
const requestRender = (force = false): void => {
|
|
431
|
+
try {
|
|
432
|
+
tui.requestRender(force);
|
|
433
|
+
} catch {
|
|
434
|
+
// UI paint is best-effort only.
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
const scheduler = createRenderScheduler(() => requestRender(false));
|
|
438
|
+
const refresh = (force = false): void => {
|
|
439
|
+
if (force) {
|
|
440
|
+
scheduler.cancel();
|
|
441
|
+
// Height transitions must reset main-screen differential state so
|
|
442
|
+
// Working/Elapsed lines keep rewriting in place instead of stacking.
|
|
443
|
+
requestRender(true);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
scheduler.request();
|
|
447
|
+
};
|
|
400
448
|
refreshGoalWidget = refresh;
|
|
401
449
|
|
|
402
450
|
return {
|
|
@@ -444,49 +492,53 @@ export default function (pi: ExtensionAPI): void {
|
|
|
444
492
|
|
|
445
493
|
if (!trimmed) {
|
|
446
494
|
const continuation = current ? currentContinuationState(ctx, current.goalId) : null;
|
|
495
|
+
// Inspection is always explicit user intent; show the full summary even while busy.
|
|
447
496
|
ctx.ui.notify(formatGoal(current, continuation, observeGoalStall(current, continuation, { foregroundRunning })), "info");
|
|
448
497
|
return;
|
|
449
498
|
}
|
|
450
499
|
|
|
451
500
|
if (trimmed === "pause") {
|
|
452
501
|
if (!current || current.status !== "active") {
|
|
453
|
-
ctx
|
|
502
|
+
notifyGoal(ctx, "Only active goals can be paused.", "warning");
|
|
454
503
|
return;
|
|
455
504
|
}
|
|
456
505
|
setGoal(goalWithStatus(current, "paused"), ctx, "command");
|
|
457
|
-
ctx
|
|
506
|
+
notifyGoal(ctx, "Goal paused.");
|
|
458
507
|
return;
|
|
459
508
|
}
|
|
460
509
|
|
|
461
510
|
if (trimmed === "resume") {
|
|
462
511
|
if (!current || current.status !== "paused") {
|
|
463
|
-
ctx
|
|
512
|
+
notifyGoal(ctx, "Only paused goals can be resumed.", "warning");
|
|
464
513
|
return;
|
|
465
514
|
}
|
|
466
515
|
const goal = goalWithStatus(current, "active");
|
|
467
516
|
setGoal(goal, ctx, "command");
|
|
468
517
|
queueGoalContinuation(goal);
|
|
469
|
-
ctx
|
|
518
|
+
notifyGoal(ctx, "Goal resumed.");
|
|
470
519
|
return;
|
|
471
520
|
}
|
|
472
521
|
|
|
473
522
|
if (trimmed === "clear") {
|
|
474
523
|
clearGoal(ctx, "command");
|
|
475
|
-
ctx
|
|
524
|
+
notifyGoal(ctx, "Goal cleared.");
|
|
476
525
|
return;
|
|
477
526
|
}
|
|
478
527
|
|
|
479
528
|
if (trimmed === "complete") {
|
|
480
529
|
if (!current) {
|
|
481
|
-
ctx
|
|
530
|
+
notifyGoal(ctx, "No goal is set.", "warning");
|
|
482
531
|
return;
|
|
483
532
|
}
|
|
484
533
|
setGoal(goalWithStatus(current, "complete"), ctx, "command");
|
|
485
|
-
ctx
|
|
534
|
+
notifyGoal(ctx, "Goal marked complete.");
|
|
486
535
|
return;
|
|
487
536
|
}
|
|
488
537
|
|
|
489
|
-
|
|
538
|
+
// Confirm dialogs swap the editor and reflow the dock. While the agent is
|
|
539
|
+
// streaming that desyncs main-screen differential paints, so replace
|
|
540
|
+
// silently mid-turn and only confirm when idle.
|
|
541
|
+
if (current && current.status !== "complete" && ctx.hasUI && !isForegroundBusy(ctx)) {
|
|
490
542
|
const replace = await ctx.ui.confirm(
|
|
491
543
|
"Replace active goal?",
|
|
492
544
|
`Current goal: ${current.objective}`,
|
|
@@ -498,9 +550,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
498
550
|
|
|
499
551
|
try {
|
|
500
552
|
const goal = startOrReplaceGoal(trimmed, null, ctx, "command");
|
|
501
|
-
ctx
|
|
553
|
+
notifyGoal(ctx, `Goal set: ${goal.objective}`);
|
|
502
554
|
} catch (error) {
|
|
503
|
-
ctx
|
|
555
|
+
notifyGoal(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
504
556
|
}
|
|
505
557
|
},
|
|
506
558
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Pi extension bundle for subagents, durable background tasks, and goal tracking.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"test": "node --test test/*.test.mjs"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"pi-better-background-tasks": "0.2.
|
|
47
|
-
"pi-better-goal": "0.1.
|
|
46
|
+
"pi-better-background-tasks": "0.2.1",
|
|
47
|
+
"pi-better-goal": "0.1.21",
|
|
48
48
|
"pi-better-subagents": "0.1.19"
|
|
49
49
|
},
|
|
50
50
|
"bundledDependencies": [
|