@heyhuynhgiabuu/pi-task 0.1.5 → 0.2.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/CHANGELOG.md +116 -4
- package/README.md +16 -11
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/conversation.d.ts +76 -21
- package/dist/conversation.js +280 -70
- package/dist/helpers.d.ts +8 -8
- package/dist/helpers.js +34 -15
- package/dist/index.d.ts +6 -23
- package/dist/index.js +233 -634
- package/dist/lifecycle/completion.d.ts +3 -0
- package/dist/lifecycle/completion.js +50 -0
- package/dist/lifecycle/index.d.ts +5 -0
- package/dist/lifecycle/index.js +5 -0
- package/dist/lifecycle/polling.d.ts +16 -0
- package/dist/lifecycle/polling.js +61 -0
- package/dist/lifecycle/restore.d.ts +2 -0
- package/dist/lifecycle/restore.js +34 -0
- package/dist/lifecycle/toolStats.d.ts +2 -0
- package/dist/lifecycle/toolStats.js +17 -0
- package/dist/lifecycle/widget.d.ts +8 -0
- package/dist/lifecycle/widget.js +75 -0
- package/dist/session-text.d.ts +11 -2
- package/dist/session-text.js +78 -2
- package/dist/subagent/buildArgv.d.ts +1 -0
- package/dist/subagent/buildArgv.js +1 -1
- package/dist/subagent/runSdk.js +50 -26
- package/dist/subagent/tmux.d.ts +12 -9
- package/dist/subagent/tmux.js +107 -44
- package/dist/subagent/waitCompletion.d.ts +5 -5
- package/dist/subagent/waitCompletion.js +32 -41
- package/dist/task-widget.d.ts +21 -0
- package/dist/task-widget.js +122 -0
- package/dist/tool/index.d.ts +5 -0
- package/dist/tool/index.js +5 -0
- package/dist/tool/prompt.d.ts +8 -0
- package/dist/tool/prompt.js +17 -0
- package/dist/tool/renderCall.d.ts +3 -0
- package/dist/tool/renderCall.js +12 -0
- package/dist/tool/renderResult.d.ts +8 -0
- package/dist/tool/renderResult.js +51 -0
- package/dist/tool/schema.d.ts +8 -0
- package/dist/tool/schema.js +24 -0
- package/dist/tool/taskComplete.d.ts +8 -0
- package/dist/tool/taskComplete.js +65 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/dist/subagent/tmux.js
CHANGED
|
@@ -1,56 +1,48 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tmux helpers for subagent panes (shared by task extension).
|
|
3
|
-
*/
|
|
4
1
|
import { execFileSync } from "node:child_process";
|
|
5
2
|
import { buildTmuxSplitWindowArgs, chooseTmuxSplitDirection } from "../helpers.js";
|
|
6
3
|
export function tmuxCmd(args) {
|
|
7
4
|
return execFileSync("tmux", args, {
|
|
8
5
|
encoding: "utf-8",
|
|
9
|
-
stdio: ["
|
|
6
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
7
|
}).trim();
|
|
11
8
|
}
|
|
12
|
-
|
|
9
|
+
function tmuxCmdQuiet(args) {
|
|
13
10
|
try {
|
|
14
|
-
|
|
15
|
-
return true;
|
|
11
|
+
return tmuxCmd(args);
|
|
16
12
|
}
|
|
17
13
|
catch {
|
|
18
|
-
return
|
|
14
|
+
return "";
|
|
19
15
|
}
|
|
20
16
|
}
|
|
21
|
-
|
|
17
|
+
function shellQuote(value) {
|
|
18
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
19
|
+
}
|
|
20
|
+
export function hasTmux() {
|
|
22
21
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
execFileSync("tmux", ["-V"], { stdio: "ignore" });
|
|
23
|
+
if (!process.env.TMUX)
|
|
24
|
+
return false;
|
|
25
|
+
tmuxCmd(["display-message", "-p", "#{pane_id}"]);
|
|
26
|
+
return true;
|
|
25
27
|
}
|
|
26
28
|
catch {
|
|
27
29
|
return false;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
export function getCurrentPaneId() {
|
|
31
|
-
|
|
32
|
-
return tmuxCmd(["display-message", "-p", "#{pane_id}"]);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
33
|
+
return tmuxCmdQuiet(["display-message", "-p", "#{pane_id}"]) || null;
|
|
37
34
|
}
|
|
38
35
|
export function getCurrentPaneSize(targetPane) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!Number.isFinite(width) || !Number.isFinite(height))
|
|
48
|
-
return null;
|
|
49
|
-
return { width, height };
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
36
|
+
const args = ["display-message", "-p", "#{pane_width} #{pane_height}"];
|
|
37
|
+
if (targetPane)
|
|
38
|
+
args.splice(1, 0, "-t", targetPane);
|
|
39
|
+
const raw = tmuxCmdQuiet(args);
|
|
40
|
+
const [widthRaw, heightRaw] = raw.trim().split(/\s+/, 2);
|
|
41
|
+
const width = Number(widthRaw);
|
|
42
|
+
const height = Number(heightRaw);
|
|
43
|
+
if (!Number.isFinite(width) || !Number.isFinite(height))
|
|
52
44
|
return null;
|
|
53
|
-
}
|
|
45
|
+
return { width, height };
|
|
54
46
|
}
|
|
55
47
|
export function splitWindowPane(cwd, command) {
|
|
56
48
|
const originalPane = getCurrentPaneId();
|
|
@@ -59,21 +51,49 @@ export function splitWindowPane(cwd, command) {
|
|
|
59
51
|
const paneId = tmuxCmd(buildTmuxSplitWindowArgs(cwd, command, direction, originalPane));
|
|
60
52
|
return { paneId, originalPane };
|
|
61
53
|
}
|
|
54
|
+
export function setPaneRemainOnExit(paneId, enabled) {
|
|
55
|
+
tmuxCmdQuiet([
|
|
56
|
+
"set-option",
|
|
57
|
+
"-p",
|
|
58
|
+
"-t",
|
|
59
|
+
paneId,
|
|
60
|
+
"remain-on-exit",
|
|
61
|
+
enabled ? "on" : "off",
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
export function setPaneSelfDestruct(paneId, enabled, delaySeconds = 1) {
|
|
65
|
+
const hook = enabled
|
|
66
|
+
? `run-shell 'sleep ${Math.max(0, delaySeconds)}; tmux kill-pane -t ${paneId} 2>/dev/null || true'`
|
|
67
|
+
: "";
|
|
68
|
+
tmuxCmdQuiet(["set-hook", "-p", "-t", paneId, "pane-died", hook]);
|
|
69
|
+
}
|
|
70
|
+
export function paneExists(paneId) {
|
|
71
|
+
return tmuxCmdQuiet(["display-message", "-p", "-t", paneId, "#{pane_id}"]) === paneId;
|
|
72
|
+
}
|
|
73
|
+
export function paneDead(paneId) {
|
|
74
|
+
const value = tmuxCmdQuiet(["display-message", "-p", "-t", paneId, "#{pane_dead}"]);
|
|
75
|
+
return value === "1" || value === "";
|
|
76
|
+
}
|
|
77
|
+
export function capturePaneTail(paneId, lines = 80) {
|
|
78
|
+
return tmuxCmdQuiet([
|
|
79
|
+
"capture-pane",
|
|
80
|
+
"-p",
|
|
81
|
+
"-t",
|
|
82
|
+
paneId,
|
|
83
|
+
"-S",
|
|
84
|
+
`-${Math.max(1, lines)}`,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
62
87
|
export function killAgentPane(paneId, originalPane) {
|
|
63
|
-
try {
|
|
64
|
-
tmuxCmd(["kill-pane", "-t", paneId]);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
/* already dead */
|
|
68
|
-
}
|
|
69
88
|
if (originalPane) {
|
|
70
89
|
try {
|
|
71
90
|
tmuxCmd(["select-pane", "-t", originalPane]);
|
|
72
91
|
}
|
|
73
92
|
catch {
|
|
74
|
-
|
|
93
|
+
// Original pane may have been closed; still try to kill the agent pane.
|
|
75
94
|
}
|
|
76
95
|
}
|
|
96
|
+
tmuxCmdQuiet(["kill-pane", "-t", paneId]);
|
|
77
97
|
}
|
|
78
98
|
/** Inject text into a running subagent pane (steer / follow-up). */
|
|
79
99
|
export function tmuxSteerPane(paneId, message) {
|
|
@@ -86,12 +106,55 @@ export function tmuxSteerPane(paneId, message) {
|
|
|
86
106
|
tmuxCmd(["paste-buffer", "-b", bufferName, "-t", paneId]);
|
|
87
107
|
}
|
|
88
108
|
finally {
|
|
89
|
-
|
|
90
|
-
tmuxCmd(["delete-buffer", "-b", bufferName]);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
/* ignore */
|
|
94
|
-
}
|
|
109
|
+
tmuxCmdQuiet(["delete-buffer", "-b", bufferName]);
|
|
95
110
|
}
|
|
96
111
|
tmuxCmd(["send-keys", "-t", paneId, "Enter"]);
|
|
97
112
|
}
|
|
113
|
+
function sessionWatcherScript(sessionFilePath) {
|
|
114
|
+
const quotedPath = shellQuote(sessionFilePath);
|
|
115
|
+
// Watch a *specific* file so stale sibling files can't trigger premature /exit.
|
|
116
|
+
return `(
|
|
117
|
+
if [ -s ${quotedPath} ]; then
|
|
118
|
+
last_size=$(wc -c < ${quotedPath} 2>/dev/null || echo 0)
|
|
119
|
+
else
|
|
120
|
+
last_size=-1
|
|
121
|
+
fi
|
|
122
|
+
stable=0
|
|
123
|
+
deadline=$(( $(date +%s) + 86400 ))
|
|
124
|
+
while [ "$(date +%s)" -lt "$deadline" ]; do
|
|
125
|
+
if [ -s ${quotedPath} ]; then
|
|
126
|
+
size=$(wc -c < ${quotedPath} 2>/dev/null || echo 0)
|
|
127
|
+
if [ "$size" = "$last_size" ]; then
|
|
128
|
+
stable=$((stable + 1))
|
|
129
|
+
else
|
|
130
|
+
stable=0
|
|
131
|
+
last_size=$size
|
|
132
|
+
fi
|
|
133
|
+
if [ "$stable" -ge 3 ]; then
|
|
134
|
+
tmux send-keys -t "$TMUX_PANE" /exit Enter 2>/dev/null || true
|
|
135
|
+
sleep 0.2
|
|
136
|
+
tmux send-keys -t "$TMUX_PANE" 'exit 0' Enter 2>/dev/null || true
|
|
137
|
+
sleep 2
|
|
138
|
+
tmux kill-pane -t "$TMUX_PANE" 2>/dev/null || true
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
sleep 0.5
|
|
143
|
+
done
|
|
144
|
+
) & watcher_pid=$!`;
|
|
145
|
+
}
|
|
146
|
+
export function wrapWithPaneExitWatcher(sessionFilePath, command) {
|
|
147
|
+
const script = `tmux set-option -p -t "$TMUX_PANE" remain-on-exit on 2>/dev/null || true
|
|
148
|
+
${sessionWatcherScript(sessionFilePath)}
|
|
149
|
+
${command}
|
|
150
|
+
exit_code=$?
|
|
151
|
+
kill "$watcher_pid" 2>/dev/null || true
|
|
152
|
+
wait "$watcher_pid" 2>/dev/null || true
|
|
153
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
154
|
+
tmux set-hook -p -t "$TMUX_PANE" pane-died '' 2>/dev/null || true
|
|
155
|
+
tmux set-option -p -t "$TMUX_PANE" remain-on-exit off 2>/dev/null || true
|
|
156
|
+
tmux kill-pane -t "$TMUX_PANE" 2>/dev/null || true
|
|
157
|
+
fi
|
|
158
|
+
exit "$exit_code"`;
|
|
159
|
+
return `sh -c ${shellQuote(script)}`;
|
|
160
|
+
}
|
|
@@ -2,16 +2,16 @@ export type TaskCompletionStatus = "running" | "completed" | "failed" | "cancell
|
|
|
2
2
|
export interface TaskCompletionSnapshot {
|
|
3
3
|
status: TaskCompletionStatus;
|
|
4
4
|
content: string;
|
|
5
|
-
source?: "
|
|
5
|
+
source?: "session-jsonl" | "pane" | "timeout" | "signal";
|
|
6
6
|
}
|
|
7
|
-
export interface
|
|
8
|
-
resultPath: string;
|
|
7
|
+
export interface WaitForTaskCompletionOptions {
|
|
9
8
|
sessionDir: string;
|
|
10
9
|
sessionName: string;
|
|
11
10
|
paneId?: string;
|
|
12
11
|
signal?: AbortSignal;
|
|
13
12
|
timeoutMs?: number;
|
|
14
13
|
pollMs?: number;
|
|
14
|
+
sinceMs?: number;
|
|
15
15
|
}
|
|
16
|
-
export declare function checkTaskCompletion(options:
|
|
17
|
-
export declare function waitForTaskCompletion(options:
|
|
16
|
+
export declare function checkTaskCompletion(options: Omit<WaitForTaskCompletionOptions, "signal" | "timeoutMs" | "pollMs">): Promise<TaskCompletionSnapshot>;
|
|
17
|
+
export declare function waitForTaskCompletion(options: WaitForTaskCompletionOptions): Promise<TaskCompletionSnapshot>;
|
|
@@ -1,49 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { getLastAssistantTextFromSessionDir } from "../session-text.js";
|
|
1
|
+
import { getLastAssistantTextFromSessionDir, hasAgentFinished, } from "../session-text.js";
|
|
5
2
|
import { paneExists } from "./tmux.js";
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
+
/**
|
|
5
|
+
* v0.1.6: The subagent's final assistant message from the auto-saved
|
|
6
|
+
* persistent JSONL session IS the result. No RESULT.md, no agent instructions
|
|
7
|
+
* to write a file. Completion is gated by the assistant's terminal
|
|
8
|
+
* `stopReason` (not `toolUse`, not streaming text).
|
|
9
|
+
*/
|
|
10
|
+
function readSessionText(sessionDir, sessionName, sinceMs) {
|
|
11
|
+
if (!hasAgentFinished(sessionDir, sessionName, sinceMs))
|
|
8
12
|
return null;
|
|
9
|
-
const text = (
|
|
10
|
-
return text.length > 0 ? text : null;
|
|
11
|
-
}
|
|
12
|
-
function readSessionText(sessionDir, sessionName) {
|
|
13
|
-
const sessionPath = join(sessionDir, "sessions", sessionName);
|
|
14
|
-
const text = getLastAssistantTextFromSessionDir(sessionPath).trim();
|
|
13
|
+
const text = getLastAssistantTextFromSessionDir(sessionDir, sessionName, sinceMs).trim();
|
|
15
14
|
return text.length > 0 ? text : null;
|
|
16
15
|
}
|
|
17
16
|
export async function checkTaskCompletion(options) {
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
17
|
+
// If the pane has exited, give pi a brief moment to flush JSONL.
|
|
18
|
+
if (options.paneId && !paneExists(options.paneId)) {
|
|
19
|
+
await sleep(500);
|
|
21
20
|
}
|
|
21
|
+
// Session JSONL is the single authoritative completion source.
|
|
22
|
+
const sessionResult = readSessionText(options.sessionDir, options.sessionName, options.sinceMs);
|
|
23
|
+
if (sessionResult) {
|
|
24
|
+
return { status: "completed", content: sessionResult, source: "session-jsonl" };
|
|
25
|
+
}
|
|
26
|
+
// No terminal assistant message yet. If the pane is alive, keep waiting.
|
|
22
27
|
if (options.paneId && paneExists(options.paneId)) {
|
|
23
28
|
return { status: "running", content: "", source: "pane" };
|
|
24
29
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
content: sessionText,
|
|
30
|
-
source: "session-jsonl",
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
if (options.paneId) {
|
|
34
|
-
return {
|
|
35
|
-
status: "failed",
|
|
36
|
-
content: "Task pane exited before producing a result or assistant response.",
|
|
37
|
-
source: "pane",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
return { status: "running", content: "", source: "pane" };
|
|
30
|
+
return {
|
|
31
|
+
status: "failed",
|
|
32
|
+
content: "Subagent pane exited without producing a result.",
|
|
33
|
+
};
|
|
41
34
|
}
|
|
42
35
|
export async function waitForTaskCompletion(options) {
|
|
36
|
+
const started = Date.now();
|
|
43
37
|
const timeoutMs = options.timeoutMs ?? 30 * 60 * 1000;
|
|
44
38
|
const pollMs = options.pollMs ?? 1000;
|
|
45
|
-
|
|
46
|
-
while (true) {
|
|
39
|
+
while (Date.now() - started < timeoutMs) {
|
|
47
40
|
if (options.signal?.aborted) {
|
|
48
41
|
return {
|
|
49
42
|
status: "cancelled",
|
|
@@ -54,13 +47,11 @@ export async function waitForTaskCompletion(options) {
|
|
|
54
47
|
const snapshot = await checkTaskCompletion(options);
|
|
55
48
|
if (snapshot.status !== "running")
|
|
56
49
|
return snapshot;
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
status: "timeout",
|
|
60
|
-
content: `Task timed out after ${Math.round(timeoutMs / 1000)}s without producing a result.`,
|
|
61
|
-
source: "timeout",
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
50
|
+
await sleep(pollMs);
|
|
65
51
|
}
|
|
52
|
+
return {
|
|
53
|
+
status: "timeout",
|
|
54
|
+
content: `Task timed out after ${Math.round(timeoutMs / 1000)}s without producing a result.`,
|
|
55
|
+
source: "timeout",
|
|
56
|
+
};
|
|
66
57
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ToolCallRecord } from "./helpers.js";
|
|
2
|
+
export interface WidgetTask {
|
|
3
|
+
agentType: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
startedAt: number;
|
|
6
|
+
toolUses: number;
|
|
7
|
+
recentCalls?: ToolCallRecord[];
|
|
8
|
+
}
|
|
9
|
+
export interface ThemeLike {
|
|
10
|
+
fg(color: string, text: string): string;
|
|
11
|
+
}
|
|
12
|
+
export declare const TASK_WIDGET_RENDER_MS = 80;
|
|
13
|
+
export declare function renderTaskWidget(params: {
|
|
14
|
+
foregroundTasks: Iterable<[string, WidgetTask]>;
|
|
15
|
+
backgroundTasks: Iterable<[string, WidgetTask]>;
|
|
16
|
+
foregroundCount: number;
|
|
17
|
+
backgroundCount: number;
|
|
18
|
+
width: number;
|
|
19
|
+
theme?: ThemeLike | null;
|
|
20
|
+
now?: number;
|
|
21
|
+
}): string[];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { formatMs } from "./helpers.js";
|
|
3
|
+
export const TASK_WIDGET_RENDER_MS = 80;
|
|
4
|
+
const SPINNER_FRAMES = [
|
|
5
|
+
"\u280B",
|
|
6
|
+
"\u2819",
|
|
7
|
+
"\u2838",
|
|
8
|
+
"\u2834",
|
|
9
|
+
"\u2826",
|
|
10
|
+
"\u2827",
|
|
11
|
+
"\u2807",
|
|
12
|
+
"\u280F",
|
|
13
|
+
];
|
|
14
|
+
const MAX_TOOL_LINES = 12;
|
|
15
|
+
const MAX_BACKGROUND_LINES = 8;
|
|
16
|
+
const MAX_WIDTH = 120;
|
|
17
|
+
const TREE_MIDDLE = "\u251C\u2500"; // ├─
|
|
18
|
+
const TREE_LAST = "\u2514\u2500"; // └─
|
|
19
|
+
function color(theme, token, text) {
|
|
20
|
+
return theme?.fg ? theme.fg(token, text) : text;
|
|
21
|
+
}
|
|
22
|
+
function toolStatusMark(theme, status, spinner) {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case "done":
|
|
25
|
+
return color(theme, "success", "\u2713");
|
|
26
|
+
case "error":
|
|
27
|
+
return color(theme, "error", "\u2717");
|
|
28
|
+
case "in_progress":
|
|
29
|
+
default:
|
|
30
|
+
return color(theme, "accent", spinner);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function formatToolCount(count) {
|
|
34
|
+
return `${count} ${count === 1 ? "tool" : "tools"}`;
|
|
35
|
+
}
|
|
36
|
+
function formatLatestTool(task, spinner, theme) {
|
|
37
|
+
const latest = task.recentCalls?.at(-1);
|
|
38
|
+
if (!latest) {
|
|
39
|
+
return `${toolStatusMark(theme, "in_progress", spinner)} ${color(theme, "dim", "waiting")}`;
|
|
40
|
+
}
|
|
41
|
+
const detail = latest.detail ? ` ${latest.detail}` : "";
|
|
42
|
+
return (`${toolStatusMark(theme, latest.status, spinner)} ` +
|
|
43
|
+
color(theme, "text", latest.name) +
|
|
44
|
+
color(theme, "dim", detail));
|
|
45
|
+
}
|
|
46
|
+
function renderForegroundTask(task, now, maxWidth, spinner, theme) {
|
|
47
|
+
const agentName = task.agentType.charAt(0).toUpperCase() + task.agentType.slice(1);
|
|
48
|
+
const elapsed = formatMs(now - task.startedAt);
|
|
49
|
+
const description = task.description ? ` — ${task.description}` : "";
|
|
50
|
+
const lines = [];
|
|
51
|
+
const header = color(theme, "accent", spinner) +
|
|
52
|
+
" " +
|
|
53
|
+
color(theme, "toolTitle", agentName) +
|
|
54
|
+
color(theme, "dim", description) +
|
|
55
|
+
color(theme, "dim", " \u2022 ") +
|
|
56
|
+
color(theme, "warning", elapsed) +
|
|
57
|
+
(task.toolUses > 0
|
|
58
|
+
? color(theme, "dim", " \u2022 ") +
|
|
59
|
+
color(theme, "success", formatToolCount(task.toolUses))
|
|
60
|
+
: "");
|
|
61
|
+
lines.push(truncateToWidth(header, maxWidth));
|
|
62
|
+
const recent = task.recentCalls ?? [];
|
|
63
|
+
const slice = recent.slice(-MAX_TOOL_LINES);
|
|
64
|
+
slice.forEach((tc, idx) => {
|
|
65
|
+
const connector = idx === slice.length - 1 ? TREE_LAST : TREE_MIDDLE;
|
|
66
|
+
const detail = tc.detail ? ` ${tc.detail}` : "";
|
|
67
|
+
const line = " " +
|
|
68
|
+
color(theme, "dim", connector) +
|
|
69
|
+
" " +
|
|
70
|
+
toolStatusMark(theme, tc.status, spinner) +
|
|
71
|
+
" " +
|
|
72
|
+
color(theme, "text", tc.name) +
|
|
73
|
+
color(theme, "dim", detail);
|
|
74
|
+
lines.push(truncateToWidth(line, maxWidth));
|
|
75
|
+
});
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
function renderBackgroundLine(id, task, now, maxWidth, spinner, theme) {
|
|
79
|
+
const elapsed = formatMs(now - task.startedAt);
|
|
80
|
+
const latest = formatLatestTool(task, spinner, theme);
|
|
81
|
+
const line = color(theme, "dim", "- ") +
|
|
82
|
+
color(theme, "toolTitle", task.agentType) +
|
|
83
|
+
color(theme, "dim", " \u00b7 ") +
|
|
84
|
+
color(theme, "accent", id) +
|
|
85
|
+
color(theme, "dim", " \u00b7 ") +
|
|
86
|
+
color(theme, "warning", elapsed) +
|
|
87
|
+
color(theme, "dim", " \u00b7 ") +
|
|
88
|
+
color(theme, "success", formatToolCount(task.toolUses)) +
|
|
89
|
+
color(theme, "dim", " \u00b7 ") +
|
|
90
|
+
latest;
|
|
91
|
+
return truncateToWidth(line, maxWidth);
|
|
92
|
+
}
|
|
93
|
+
export function renderTaskWidget(params) {
|
|
94
|
+
const { foregroundTasks, backgroundTasks, foregroundCount, backgroundCount, width, theme, } = params;
|
|
95
|
+
if (foregroundCount === 0 && backgroundCount === 0)
|
|
96
|
+
return [];
|
|
97
|
+
const now = params.now ?? Date.now();
|
|
98
|
+
const maxWidth = Math.min(width, MAX_WIDTH);
|
|
99
|
+
const tick = Math.floor(now / TASK_WIDGET_RENDER_MS);
|
|
100
|
+
const spinner = SPINNER_FRAMES[tick % SPINNER_FRAMES.length];
|
|
101
|
+
const lines = [];
|
|
102
|
+
for (const [, task] of foregroundTasks) {
|
|
103
|
+
lines.push(...renderForegroundTask(task, now, maxWidth, spinner, theme));
|
|
104
|
+
lines.push("");
|
|
105
|
+
}
|
|
106
|
+
const renderedBackground = [];
|
|
107
|
+
for (const entry of backgroundTasks) {
|
|
108
|
+
if (renderedBackground.length >= MAX_BACKGROUND_LINES)
|
|
109
|
+
break;
|
|
110
|
+
renderedBackground.push(entry);
|
|
111
|
+
}
|
|
112
|
+
for (const [id, task] of renderedBackground) {
|
|
113
|
+
lines.push(renderBackgroundLine(id, task, now, maxWidth, spinner, theme));
|
|
114
|
+
}
|
|
115
|
+
const hidden = backgroundCount - renderedBackground.length;
|
|
116
|
+
if (hidden > 0) {
|
|
117
|
+
lines.push(truncateToWidth(color(theme, "dim", `+ ${hidden} more background tasks`), maxWidth));
|
|
118
|
+
}
|
|
119
|
+
// Keep a little breathing room above the editor.
|
|
120
|
+
lines.push("");
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TASK_PROMPT_INSTRUCTIONS } from "../helpers.js";
|
|
2
|
+
export function buildTaskPrompt(options) {
|
|
3
|
+
return [
|
|
4
|
+
`# Task: ${options.description}`,
|
|
5
|
+
"",
|
|
6
|
+
"## Agent",
|
|
7
|
+
`${options.agentName} (${options.agentSource})`,
|
|
8
|
+
"",
|
|
9
|
+
"## Instructions",
|
|
10
|
+
options.prompt,
|
|
11
|
+
"",
|
|
12
|
+
"## Working Directory",
|
|
13
|
+
options.cwd,
|
|
14
|
+
"",
|
|
15
|
+
TASK_PROMPT_INSTRUCTIONS,
|
|
16
|
+
].join("\n");
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
/** Render the task tool call line while the subagent is running. */
|
|
3
|
+
export function renderCall(args, theme) {
|
|
4
|
+
const params = args;
|
|
5
|
+
const agentName = params.agent_type || "...";
|
|
6
|
+
const desc = params.description || "";
|
|
7
|
+
let text = theme.fg("toolTitle", "");
|
|
8
|
+
text += theme.fg("accent", agentName);
|
|
9
|
+
if (desc)
|
|
10
|
+
text += theme.fg("dim", ` - ${desc}`);
|
|
11
|
+
return new Text(text, 0, 0);
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Container } from "@earendil-works/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* Custom renderResult for the task tool.
|
|
4
|
+
* Supports collapsed/expanded views with Ctrl+O (app.tools.expand).
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderResult(result: any, options: {
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
}, theme: any): Container;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { keyHint, keyText, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { formatMs } from "../helpers.js";
|
|
4
|
+
/**
|
|
5
|
+
* Custom renderResult for the task tool.
|
|
6
|
+
* Supports collapsed/expanded views with Ctrl+O (app.tools.expand).
|
|
7
|
+
*/
|
|
8
|
+
export function renderResult(result, options, theme) {
|
|
9
|
+
const details = (result.details ?? {});
|
|
10
|
+
const stats = [];
|
|
11
|
+
if (typeof details.tool_uses === "number" && details.tool_uses > 0) {
|
|
12
|
+
stats.push(theme.fg("muted", `${details.tool_uses} toolcall${details.tool_uses === 1 ? "" : "s"}`));
|
|
13
|
+
}
|
|
14
|
+
if (typeof details.duration_ms === "number" && details.duration_ms > 0) {
|
|
15
|
+
stats.push(theme.fg("muted", formatMs(details.duration_ms)));
|
|
16
|
+
}
|
|
17
|
+
const firstContent = result.content?.[0];
|
|
18
|
+
const fullText = firstContent && "text" in firstContent
|
|
19
|
+
? (firstContent.text ?? "").trim()
|
|
20
|
+
: "";
|
|
21
|
+
const preview = fullText.slice(0, 120);
|
|
22
|
+
const expandHint = expandCollapseHint("to expand");
|
|
23
|
+
const collapseHint = expandCollapseHint("to collapse");
|
|
24
|
+
const container = new Container();
|
|
25
|
+
if (stats.length) {
|
|
26
|
+
container.addChild(new Text(stats.join(theme.fg("dim", " • ")), 0, 0));
|
|
27
|
+
}
|
|
28
|
+
if (options.expanded) {
|
|
29
|
+
if (fullText) {
|
|
30
|
+
for (const line of fullText.split("\n")) {
|
|
31
|
+
container.addChild(new Text(truncateToWidth(line, 200), 0, 0));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
container.addChild(new Text(theme.fg("dim", ` (${collapseHint})`), 0, 0));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
if (preview) {
|
|
38
|
+
container.addChild(new Text(theme.fg("dim", ` ⎿ ${preview}`) +
|
|
39
|
+
(fullText.length > 120 ? theme.fg("dim", "…") : ""), 0, 0));
|
|
40
|
+
}
|
|
41
|
+
if (fullText.length > 120) {
|
|
42
|
+
container.addChild(new Text(theme.fg("dim", ` (${expandHint})`), 0, 0));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return container;
|
|
46
|
+
}
|
|
47
|
+
function expandCollapseHint(action) {
|
|
48
|
+
return keyText("app.tools.expand").trim()
|
|
49
|
+
? keyHint("app.tools.expand", action)
|
|
50
|
+
: rawKeyHint("Ctrl+O", action);
|
|
51
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function taskParametersSchema(): import("@sinclair/typebox").TObject<{
|
|
2
|
+
agent_type: import("@sinclair/typebox").TString;
|
|
3
|
+
prompt: import("@sinclair/typebox").TString;
|
|
4
|
+
description: import("@sinclair/typebox").TString;
|
|
5
|
+
task_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
6
|
+
conversation_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
7
|
+
background: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
export function taskParametersSchema() {
|
|
3
|
+
return Type.Object({
|
|
4
|
+
agent_type: Type.String({
|
|
5
|
+
description: "The type of specialist agent to use for this task",
|
|
6
|
+
}),
|
|
7
|
+
prompt: Type.String({
|
|
8
|
+
description: "The complete task for the agent to perform. Be detailed and self-contained.",
|
|
9
|
+
}),
|
|
10
|
+
description: Type.String({
|
|
11
|
+
description: "A short (3-5 word) summary of the task",
|
|
12
|
+
}),
|
|
13
|
+
task_id: Type.Optional(Type.String({
|
|
14
|
+
description: "Resume an existing background task by id instead of starting a new task.",
|
|
15
|
+
})),
|
|
16
|
+
conversation_id: Type.Optional(Type.String({
|
|
17
|
+
description: "Durable specialist conversation id. Reuses .pi/artifacts/task-<id>/sessions when called again.",
|
|
18
|
+
})),
|
|
19
|
+
background: Type.Optional(Type.Boolean({
|
|
20
|
+
description: "Run in background (async). You will be notified when it completes. DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background.",
|
|
21
|
+
default: true,
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* Renderer for background task completion notifications.
|
|
4
|
+
* Supports collapsed/expanded views with Ctrl+O.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createTaskCompleteRenderer(): (message: any, { expanded }: {
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
}, theme: any) => Text | undefined;
|