@heyhuynhgiabuu/pi-task 0.1.6 → 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 +67 -3
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/conversation.d.ts +8 -0
- package/dist/conversation.js +96 -1
- package/dist/helpers.d.ts +2 -2
- package/dist/helpers.js +4 -9
- package/dist/index.d.ts +6 -23
- package/dist/index.js +91 -589
- 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 +9 -0
- package/dist/session-text.js +50 -0
- 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 +4 -5
- package/dist/subagent/waitCompletion.js +27 -43
- 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,10 +2,9 @@ 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;
|
|
@@ -14,5 +13,5 @@ export interface TaskCompletionOptions {
|
|
|
14
13
|
pollMs?: number;
|
|
15
14
|
sinceMs?: number;
|
|
16
15
|
}
|
|
17
|
-
export declare function checkTaskCompletion(options:
|
|
18
|
-
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,56 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { getLastAssistantTextFromSessionDir } from "../session-text.js";
|
|
1
|
+
import { getLastAssistantTextFromSessionDir, hasAgentFinished, } from "../session-text.js";
|
|
4
2
|
import { paneExists } from "./tmux.js";
|
|
5
|
-
const sleep = (ms) => new Promise((
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
*/
|
|
12
10
|
function readSessionText(sessionDir, sessionName, sinceMs) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// completes from an older task's JSONL.
|
|
11
|
+
if (!hasAgentFinished(sessionDir, sessionName, sinceMs))
|
|
12
|
+
return null;
|
|
16
13
|
const text = getLastAssistantTextFromSessionDir(sessionDir, sessionName, sinceMs).trim();
|
|
17
14
|
return text.length > 0 ? text : null;
|
|
18
15
|
}
|
|
19
16
|
export async function checkTaskCompletion(options) {
|
|
20
|
-
//
|
|
21
|
-
// session file. Without this, the read can catch a partial
|
|
22
|
-
// file (e.g. the last `agent_end` / `message_end` events not
|
|
23
|
-
// yet written) and report "failed" even though the subagent
|
|
24
|
-
// completed successfully.
|
|
17
|
+
// If the pane has exited, give pi a brief moment to flush JSONL.
|
|
25
18
|
if (options.paneId && !paneExists(options.paneId)) {
|
|
26
19
|
await sleep(500);
|
|
27
20
|
}
|
|
28
|
-
|
|
29
|
-
if (result) {
|
|
30
|
-
return { status: "completed", content: result, source: "result-file" };
|
|
31
|
-
}
|
|
32
|
-
// Check session text FIRST. If the subagent's session file has
|
|
33
|
-
// its final assistant message, the subagent is done — kill the
|
|
34
|
-
// pane and return, regardless of whether the pane shell is
|
|
35
|
-
// still open (e.g. remain-on-exit on, or the command exited but
|
|
36
|
-
// tmux kept the shell alive).
|
|
21
|
+
// Session JSONL is the single authoritative completion source.
|
|
37
22
|
const sessionResult = readSessionText(options.sessionDir, options.sessionName, options.sinceMs);
|
|
38
23
|
if (sessionResult) {
|
|
39
24
|
return { status: "completed", content: sessionResult, source: "session-jsonl" };
|
|
40
25
|
}
|
|
41
|
-
// No
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return { status: "failed", content: "Subagent pane exited without producing a result." };
|
|
26
|
+
// No terminal assistant message yet. If the pane is alive, keep waiting.
|
|
27
|
+
if (options.paneId && paneExists(options.paneId)) {
|
|
28
|
+
return { status: "running", content: "", source: "pane" };
|
|
45
29
|
}
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
return {
|
|
31
|
+
status: "failed",
|
|
32
|
+
content: "Subagent pane exited without producing a result.",
|
|
33
|
+
};
|
|
48
34
|
}
|
|
49
35
|
export async function waitForTaskCompletion(options) {
|
|
36
|
+
const started = Date.now();
|
|
50
37
|
const timeoutMs = options.timeoutMs ?? 30 * 60 * 1000;
|
|
51
38
|
const pollMs = options.pollMs ?? 1000;
|
|
52
|
-
|
|
53
|
-
while (true) {
|
|
39
|
+
while (Date.now() - started < timeoutMs) {
|
|
54
40
|
if (options.signal?.aborted) {
|
|
55
41
|
return {
|
|
56
42
|
status: "cancelled",
|
|
@@ -61,13 +47,11 @@ export async function waitForTaskCompletion(options) {
|
|
|
61
47
|
const snapshot = await checkTaskCompletion(options);
|
|
62
48
|
if (snapshot.status !== "running")
|
|
63
49
|
return snapshot;
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
status: "timeout",
|
|
67
|
-
content: `Task timed out after ${Math.round(timeoutMs / 1000)}s without producing a result.`,
|
|
68
|
-
source: "timeout",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
50
|
+
await sleep(pollMs);
|
|
72
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
|
+
};
|
|
73
57
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { keyHint, keyText, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
/**
|
|
4
|
+
* Renderer for background task completion notifications.
|
|
5
|
+
* Supports collapsed/expanded views with Ctrl+O.
|
|
6
|
+
*/
|
|
7
|
+
export function createTaskCompleteRenderer() {
|
|
8
|
+
return (message, { expanded }, theme) => {
|
|
9
|
+
const d = message.details;
|
|
10
|
+
if (!d)
|
|
11
|
+
return undefined;
|
|
12
|
+
const agentType = d.agent_type || "";
|
|
13
|
+
const desc = d.description || "";
|
|
14
|
+
const result = (d.result || "").trim();
|
|
15
|
+
const durationMs = d.duration_ms || 0;
|
|
16
|
+
const toolUses = d.tool_uses || 0;
|
|
17
|
+
let line = " " + theme.fg("accent", agentType);
|
|
18
|
+
if (desc)
|
|
19
|
+
line += theme.fg("dim", ` - ${desc}`);
|
|
20
|
+
const useStr = toolUses > 0 ? `${toolUses} toolcalls` : "";
|
|
21
|
+
const durStr = durationMs >= 1000 ? formatMs(durationMs) : "";
|
|
22
|
+
const statsParts = [useStr, durStr].filter(Boolean);
|
|
23
|
+
const statsText = statsParts.join(" • ");
|
|
24
|
+
if (statsText) {
|
|
25
|
+
line += "\n " + theme.fg("dim", statsText);
|
|
26
|
+
}
|
|
27
|
+
const expandHint = expandCollapseHint("to expand");
|
|
28
|
+
const collapseHint = expandCollapseHint("to collapse");
|
|
29
|
+
if (expanded) {
|
|
30
|
+
if (result)
|
|
31
|
+
line += "\n " + theme.fg("muted", result);
|
|
32
|
+
line += "\n " + theme.fg("dim", ` (${collapseHint})`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const preview = result.slice(0, 120);
|
|
36
|
+
if (preview) {
|
|
37
|
+
line +=
|
|
38
|
+
"\n " +
|
|
39
|
+
theme.fg("dim", ` ⎿ ${preview}`) +
|
|
40
|
+
(result.length > 120 ? theme.fg("dim", "…") : "");
|
|
41
|
+
}
|
|
42
|
+
if (result.length > 120) {
|
|
43
|
+
line += "\n " + theme.fg("dim", ` (${expandHint})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!line.trim())
|
|
47
|
+
return undefined;
|
|
48
|
+
const subtleBg = (text) => `\x1b[48;2;30;28;44m${text}\x1b[0m`;
|
|
49
|
+
return new Text(line, 0, 1, subtleBg);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function expandCollapseHint(action) {
|
|
53
|
+
return keyText("app.tools.expand").trim()
|
|
54
|
+
? keyHint("app.tools.expand", action)
|
|
55
|
+
: rawKeyHint("Ctrl+O", action);
|
|
56
|
+
}
|
|
57
|
+
function formatMs(ms) {
|
|
58
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
59
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
60
|
+
const seconds = totalSeconds % 60;
|
|
61
|
+
if (minutes > 0) {
|
|
62
|
+
return `${minutes}m ${seconds}s`;
|
|
63
|
+
}
|
|
64
|
+
return `${seconds}s`;
|
|
65
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ToolCallRecord } from "./helpers.js";
|
|
2
|
+
export interface BackgroundTask {
|
|
3
|
+
dir: string;
|
|
4
|
+
agentType: string;
|
|
5
|
+
sessionName: string;
|
|
6
|
+
paneId?: string;
|
|
7
|
+
originalPane: string | null;
|
|
8
|
+
description: string;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
toolUses: number;
|
|
11
|
+
turns: number;
|
|
12
|
+
conversationId?: string;
|
|
13
|
+
/** Most recent tool calls (capped), updated every COUNT_POLL_MS. */
|
|
14
|
+
recentCalls: ToolCallRecord[];
|
|
15
|
+
/** Consecutive completion-poll failures; reset to 0 on a successful poll. */
|
|
16
|
+
pollErrors?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Serializable subset for active task registry persistence. */
|
|
19
|
+
export interface RegistryEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
agentType: string;
|
|
22
|
+
description: string;
|
|
23
|
+
sessionName: string;
|
|
24
|
+
startedAt: number;
|
|
25
|
+
paneId?: string;
|
|
26
|
+
piDir: string;
|
|
27
|
+
dir: string;
|
|
28
|
+
conversationId?: string;
|
|
29
|
+
sessionRef?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Durable task→session mapping used for resume after task completion. */
|
|
32
|
+
export interface TaskSessionHistoryEntry extends RegistryEntry {
|
|
33
|
+
status: "running" | "done" | "cancelled" | "aborted" | "failed" | "timeout";
|
|
34
|
+
completedAt?: number;
|
|
35
|
+
background: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Details attached to tool result for rendering. */
|
|
38
|
+
export interface TaskDetails {
|
|
39
|
+
task_id: string;
|
|
40
|
+
agent_type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
conversation_id?: string;
|
|
43
|
+
phase: "done" | "timeout" | "aborted" | "failed";
|
|
44
|
+
status?: string;
|
|
45
|
+
summary?: string;
|
|
46
|
+
findings?: string;
|
|
47
|
+
evidence?: string;
|
|
48
|
+
confidence?: string;
|
|
49
|
+
duration_ms?: number;
|
|
50
|
+
turn_count?: number;
|
|
51
|
+
tool_uses?: number;
|
|
52
|
+
background?: boolean;
|
|
53
|
+
tmux_session?: string;
|
|
54
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Delegating task/subagent extension for Pi: foreground/background subagents, widgets, tmux observability, SDK fallback.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|