@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +67 -3
  2. package/dist/constants.d.ts +4 -0
  3. package/dist/constants.js +4 -0
  4. package/dist/conversation.d.ts +8 -0
  5. package/dist/conversation.js +96 -1
  6. package/dist/helpers.d.ts +2 -2
  7. package/dist/helpers.js +4 -9
  8. package/dist/index.d.ts +6 -23
  9. package/dist/index.js +91 -589
  10. package/dist/lifecycle/completion.d.ts +3 -0
  11. package/dist/lifecycle/completion.js +50 -0
  12. package/dist/lifecycle/index.d.ts +5 -0
  13. package/dist/lifecycle/index.js +5 -0
  14. package/dist/lifecycle/polling.d.ts +16 -0
  15. package/dist/lifecycle/polling.js +61 -0
  16. package/dist/lifecycle/restore.d.ts +2 -0
  17. package/dist/lifecycle/restore.js +34 -0
  18. package/dist/lifecycle/toolStats.d.ts +2 -0
  19. package/dist/lifecycle/toolStats.js +17 -0
  20. package/dist/lifecycle/widget.d.ts +8 -0
  21. package/dist/lifecycle/widget.js +75 -0
  22. package/dist/session-text.d.ts +9 -0
  23. package/dist/session-text.js +50 -0
  24. package/dist/subagent/runSdk.js +50 -26
  25. package/dist/subagent/tmux.d.ts +12 -9
  26. package/dist/subagent/tmux.js +107 -44
  27. package/dist/subagent/waitCompletion.d.ts +4 -5
  28. package/dist/subagent/waitCompletion.js +27 -43
  29. package/dist/tool/index.d.ts +5 -0
  30. package/dist/tool/index.js +5 -0
  31. package/dist/tool/prompt.d.ts +8 -0
  32. package/dist/tool/prompt.js +17 -0
  33. package/dist/tool/renderCall.d.ts +3 -0
  34. package/dist/tool/renderCall.js +12 -0
  35. package/dist/tool/renderResult.d.ts +8 -0
  36. package/dist/tool/renderResult.js +51 -0
  37. package/dist/tool/schema.d.ts +8 -0
  38. package/dist/tool/schema.js +24 -0
  39. package/dist/tool/taskComplete.d.ts +8 -0
  40. package/dist/tool/taskComplete.js +65 -0
  41. package/dist/types.d.ts +54 -0
  42. package/dist/types.js +1 -0
  43. package/package.json +1 -1
@@ -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: ["pipe", "pipe", "pipe"],
6
+ stdio: ["ignore", "pipe", "pipe"],
10
7
  }).trim();
11
8
  }
12
- export function hasTmux() {
9
+ function tmuxCmdQuiet(args) {
13
10
  try {
14
- execFileSync("tmux", ["-V"], { stdio: "pipe" });
15
- return true;
11
+ return tmuxCmd(args);
16
12
  }
17
13
  catch {
18
- return false;
14
+ return "";
19
15
  }
20
16
  }
21
- export function paneExists(paneId) {
17
+ function shellQuote(value) {
18
+ return `'${value.replace(/'/g, `'\\''`)}'`;
19
+ }
20
+ export function hasTmux() {
22
21
  try {
23
- const out = tmuxCmd(["list-panes", "-a", "-F", "#{pane_id}"]);
24
- return out.split("\n").includes(paneId);
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
- try {
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
- try {
40
- const args = ["display-message", "-p", "#{pane_width} #{pane_height}"];
41
- if (targetPane)
42
- args.splice(1, 0, "-t", targetPane);
43
- const raw = tmuxCmd(args);
44
- const [widthRaw, heightRaw] = raw.trim().split(/\s+/, 2);
45
- const width = Number(widthRaw);
46
- const height = Number(heightRaw);
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
- /* ignore */
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
- try {
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?: "result-file" | "session-jsonl" | "pane" | "timeout" | "signal";
5
+ source?: "session-jsonl" | "pane" | "timeout" | "signal";
6
6
  }
7
- export interface TaskCompletionOptions {
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: TaskCompletionOptions): Promise<TaskCompletionSnapshot>;
18
- export declare function waitForTaskCompletion(options: TaskCompletionOptions): Promise<TaskCompletionSnapshot>;
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 { readFile } from "node:fs/promises";
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((r) => setTimeout(r, ms));
6
- async function readResultFile(resultPath) {
7
- if (!existsSync(resultPath))
8
- return null;
9
- const text = (await readFile(resultPath, "utf-8")).trim();
10
- return text.length > 0 ? text : null;
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
- // Session files are written by pi directly into `sessionDir`
14
- // (flat). Filter by session_info.name so a new task never
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
- // When the pane has exited, give pi a brief moment to flush the
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
- const result = await readResultFile(options.resultPath);
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 session text yet. If the pane is gone and we never got
42
- // session text, the subagent failed.
43
- if (options.paneId && !paneExists(options.paneId)) {
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
- // Pane still exists and no session text yet — keep polling.
47
- return { status: "running", content: "", source: "pane" };
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
- const deadline = Date.now() + timeoutMs;
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
- if (Date.now() >= deadline) {
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,5 @@
1
+ export { renderCall } from "./renderCall.js";
2
+ export { renderResult } from "./renderResult.js";
3
+ export { createTaskCompleteRenderer } from "./taskComplete.js";
4
+ export { buildTaskPrompt } from "./prompt.js";
5
+ export { taskParametersSchema } from "./schema.js";
@@ -0,0 +1,5 @@
1
+ export { renderCall } from "./renderCall.js";
2
+ export { renderResult } from "./renderResult.js";
3
+ export { createTaskCompleteRenderer } from "./taskComplete.js";
4
+ export { buildTaskPrompt } from "./prompt.js";
5
+ export { taskParametersSchema } from "./schema.js";
@@ -0,0 +1,8 @@
1
+ export interface BuildTaskPromptOptions {
2
+ description: string;
3
+ agentName: string;
4
+ agentSource: string;
5
+ prompt: string;
6
+ cwd: string;
7
+ }
8
+ export declare function buildTaskPrompt(options: BuildTaskPromptOptions): string;
@@ -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,3 @@
1
+ import { Text } from "@earendil-works/pi-tui";
2
+ /** Render the task tool call line while the subagent is running. */
3
+ export declare function renderCall(args: unknown, theme: any): Text;
@@ -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
+ }
@@ -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.1.6",
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",