@heyhuynhgiabuu/pi-task 0.1.3 → 0.1.5

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 CHANGED
@@ -4,6 +4,29 @@ All notable changes to `@heyhuynhgiabuu/pi-task` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.1.4] — 2026-06-21
8
+
9
+ ### Fixed
10
+
11
+ - Detect the current tmux pane size before launching a task pane and choose
12
+ the split direction based on available space: side-by-side for wide panes,
13
+ stacked for narrow panes.
14
+ - Target the exact pane that was measured when running `tmux split-window`,
15
+ avoiding focus races where a different pane could be split.
16
+ - Apply the same pane-size-aware split logic to the subagent tmux helper.
17
+
18
+ ### Verified
19
+
20
+ - `npm test` passes
21
+ - `npm run typecheck` passes
22
+ - `npm run build` passes
23
+ - `npm run smoke` passes
24
+ - `npm pack --dry-run` succeeds
25
+ - Real tmux integration check passed for narrow `120x40` and wide `200x40`
26
+ sessions.
27
+
28
+ [0.1.4]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.4
29
+
7
30
  ## [0.1.3] — 2026-06-21
8
31
 
9
32
  ### Fixed
package/README.md CHANGED
@@ -45,7 +45,7 @@ Foreground task:
45
45
 
46
46
  Background task:
47
47
 
48
- ```json
48
+ ```
49
49
  {
50
50
  "agent_type": "scout",
51
51
  "description": "Research SDK docs",
@@ -54,6 +54,33 @@ Background task:
54
54
  }
55
55
  ```
56
56
 
57
+ Durable specialist conversation:
58
+
59
+ ```
60
+ {
61
+ "agent_type": "scout",
62
+ "conversation_id": "research-ai",
63
+ "description": "Ask research assistant",
64
+ "background": false,
65
+ "prompt": "Continue our prior research thread. What did we conclude about retrieval evaluation?"
66
+ }
67
+ ```
68
+
69
+ `conversation_id` maps to one existing `task-<id>` artifact under `.pi/artifacts/` and reuses its `sessions/` directory on later calls. This is for scoped specialist memory, e.g. a reusable research assistant. Use `/task-sessions` to list known durable conversations.
70
+
71
+ Stored files:
72
+
73
+ ```
74
+ .pi/artifacts/task-registry.json
75
+ .pi/artifacts/task-<id>/CONTEXT.md
76
+ .pi/artifacts/task-<id>/RESULT.md
77
+ .pi/artifacts/task-<id>/SESSION.md
78
+ .pi/artifacts/task-<id>/metadata.json
79
+ .pi/artifacts/task-<id>/sessions/
80
+ ```
81
+
82
+ Note: true conversation resume requires the tmux/CLI backend so Pi can reopen the saved subagent session. SDK fallback can run one-shot tasks, but it cannot resume a prior Pi session.
83
+
57
84
  ## Agent precedence
58
85
 
59
86
  When two agents have the same name, later sources override earlier ones:
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Conversational subagent helpers.
3
+ *
4
+ * Durable subagent conversations reuse the existing
5
+ * `.pi/artifacts/task-<id>/` artifact convention and add a small
6
+ * `conversation_id` -> `task-<id>` registry under the same artifacts dir.
7
+ */
8
+ export interface ConversationMetadata {
9
+ conversation_id: string;
10
+ task_id: string;
11
+ artifact: string;
12
+ agent_type: string;
13
+ session_dir: string;
14
+ session_name: string;
15
+ created_at: string;
16
+ last_used_at: string;
17
+ last_prompt?: string;
18
+ }
19
+ export type ConversationRegistry = Record<string, string>;
20
+ export declare const CONVERSATION_REGISTRY_FILE = "task-conversations.json";
21
+ export declare function getArtifactsDir(piDir: string): string;
22
+ export declare function getConversationRegistryPath(piDir: string): string;
23
+ export declare function taskArtifactName(taskId: string): string;
24
+ export declare function taskIdFromArtifactName(artifactName: string): string;
25
+ export declare function normalizeConversationId(value: unknown): string | undefined;
26
+ export declare function readConversationRegistry(piDir: string): ConversationRegistry;
27
+ export declare function writeConversationRegistry(piDir: string, registry: ConversationRegistry): void;
28
+ export declare function readConversationMetadata(metadataPath: string): ConversationMetadata | undefined;
29
+ export declare function buildSessionCard(metadata: ConversationMetadata): string;
30
+ export declare function writeConversationArtifacts(options: {
31
+ taskDir: string;
32
+ taskId: string;
33
+ conversationId: string;
34
+ agentType: string;
35
+ sessionDir: string;
36
+ sessionName: string;
37
+ prompt: string;
38
+ }): ConversationMetadata;
39
+ export declare function renderConversationSessions(piDir: string): string;
@@ -0,0 +1,123 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export const CONVERSATION_REGISTRY_FILE = "task-conversations.json";
4
+ export function getArtifactsDir(piDir) {
5
+ return join(piDir, "artifacts");
6
+ }
7
+ export function getConversationRegistryPath(piDir) {
8
+ return join(getArtifactsDir(piDir), CONVERSATION_REGISTRY_FILE);
9
+ }
10
+ export function taskArtifactName(taskId) {
11
+ return taskId.startsWith("task-") ? taskId : `task-${taskId}`;
12
+ }
13
+ export function taskIdFromArtifactName(artifactName) {
14
+ return artifactName.startsWith("task-")
15
+ ? artifactName.slice("task-".length)
16
+ : artifactName;
17
+ }
18
+ export function normalizeConversationId(value) {
19
+ if (typeof value !== "string")
20
+ return undefined;
21
+ const conversationId = value.trim();
22
+ if (!conversationId)
23
+ return undefined;
24
+ if (!/^[A-Za-z0-9._-]{1,80}$/.test(conversationId)) {
25
+ throw new Error("conversation_id must be 1-80 chars and contain only letters, numbers, '.', '_' or '-'");
26
+ }
27
+ return conversationId;
28
+ }
29
+ export function readConversationRegistry(piDir) {
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(getConversationRegistryPath(piDir), "utf-8"));
32
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
33
+ return {};
34
+ }
35
+ const registry = {};
36
+ for (const [key, value] of Object.entries(parsed)) {
37
+ if (typeof value === "string")
38
+ registry[key] = value;
39
+ }
40
+ return registry;
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ }
46
+ export function writeConversationRegistry(piDir, registry) {
47
+ const artifactsDir = getArtifactsDir(piDir);
48
+ mkdirSync(artifactsDir, { recursive: true });
49
+ writeFileSync(getConversationRegistryPath(piDir), `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
50
+ }
51
+ export function readConversationMetadata(metadataPath) {
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(metadataPath, "utf-8"));
54
+ if (!parsed.conversation_id || !parsed.task_id)
55
+ return undefined;
56
+ return parsed;
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ export function buildSessionCard(metadata) {
63
+ return [
64
+ `# ${metadata.conversation_id}`,
65
+ "",
66
+ `Agent: ${metadata.agent_type}`,
67
+ `Task: ${taskArtifactName(metadata.task_id)}`,
68
+ `Last used: ${metadata.last_used_at}`,
69
+ `Session dir: ${metadata.session_dir}`,
70
+ "",
71
+ "## Resume",
72
+ "",
73
+ "```json",
74
+ JSON.stringify({
75
+ agent_type: metadata.agent_type,
76
+ conversation_id: metadata.conversation_id,
77
+ prompt: "Continue from the prior specialist conversation.",
78
+ }, null, 2),
79
+ "```",
80
+ "",
81
+ "## Last prompt",
82
+ "",
83
+ metadata.last_prompt ?? "",
84
+ "",
85
+ ].join("\n");
86
+ }
87
+ export function writeConversationArtifacts(options) {
88
+ const now = new Date().toISOString();
89
+ const metadataPath = join(options.taskDir, "metadata.json");
90
+ const existing = readConversationMetadata(metadataPath);
91
+ const metadata = {
92
+ conversation_id: options.conversationId,
93
+ task_id: options.taskId,
94
+ artifact: taskArtifactName(options.taskId),
95
+ agent_type: options.agentType,
96
+ session_dir: options.sessionDir,
97
+ session_name: options.sessionName,
98
+ created_at: existing?.created_at ?? now,
99
+ last_used_at: now,
100
+ last_prompt: options.prompt,
101
+ };
102
+ mkdirSync(options.taskDir, { recursive: true });
103
+ writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf-8");
104
+ writeFileSync(join(options.taskDir, "SESSION.md"), buildSessionCard(metadata), "utf-8");
105
+ return metadata;
106
+ }
107
+ export function renderConversationSessions(piDir) {
108
+ const registry = readConversationRegistry(piDir);
109
+ const entries = Object.entries(registry).sort(([left], [right]) => left.localeCompare(right));
110
+ if (entries.length === 0) {
111
+ return 'No durable task conversations found. Start one with task({ conversation_id: "research-ai", ... }).';
112
+ }
113
+ const lines = ["Durable task conversations:"];
114
+ for (const [conversationId, artifactName] of entries) {
115
+ const taskId = taskIdFromArtifactName(artifactName);
116
+ const metadata = readConversationMetadata(join(getArtifactsDir(piDir), taskArtifactName(taskId), "metadata.json"));
117
+ const suffix = metadata
118
+ ? ` — ${metadata.agent_type}, last used ${metadata.last_used_at}`
119
+ : "";
120
+ lines.push(`${conversationId} -> ${taskArtifactName(taskId)}${suffix}`);
121
+ }
122
+ return lines.join("\n");
123
+ }
package/dist/helpers.d.ts CHANGED
@@ -50,7 +50,9 @@ export declare function parseResultXml(raw: string): ParsedResult;
50
50
  export declare function formatMs(ms: number): string;
51
51
  export declare function parseIdTimestamp(id: string): number;
52
52
  export declare function shellQuote(value: string): string;
53
- export declare function buildTmuxSplitWindowArgs(cwd: string, command: string): string[];
53
+ export type TmuxSplitDirection = "-h" | "-v";
54
+ export declare function chooseTmuxSplitDirection(paneWidth: number, paneHeight: number): TmuxSplitDirection;
55
+ export declare function buildTmuxSplitWindowArgs(cwd: string, command: string, direction?: TmuxSplitDirection, targetPane?: string | null): string[];
54
56
  export interface BackgroundReceiptInput {
55
57
  taskId: string;
56
58
  agentType: string;
package/dist/helpers.js CHANGED
@@ -128,8 +128,29 @@ export function parseIdTimestamp(id) {
128
128
  export function shellQuote(value) {
129
129
  return `'${value.replace(/'/g, `'"'"'`)}'`;
130
130
  }
131
- export function buildTmuxSplitWindowArgs(cwd, command) {
132
- return ["split-window", "-h", "-P", "-F", "#{pane_id}", "-c", cwd, command];
131
+ export function chooseTmuxSplitDirection(paneWidth, paneHeight) {
132
+ const minSideBySideWidth = 160;
133
+ const minStackedHeight = 24;
134
+ if (Number.isFinite(paneWidth) && paneWidth >= minSideBySideWidth) {
135
+ return "-h";
136
+ }
137
+ if (Number.isFinite(paneHeight) && paneHeight >= minStackedHeight) {
138
+ return "-v";
139
+ }
140
+ return "-h";
141
+ }
142
+ export function buildTmuxSplitWindowArgs(cwd, command, direction = "-h", targetPane) {
143
+ const args = [
144
+ "split-window",
145
+ direction,
146
+ "-P",
147
+ "-F",
148
+ "#{pane_id}",
149
+ ];
150
+ if (targetPane)
151
+ args.push("-t", targetPane);
152
+ args.push("-c", cwd, command);
153
+ return args;
133
154
  }
134
155
  export function formatBackgroundReceipt(input) {
135
156
  return [
package/dist/index.d.ts CHANGED
@@ -15,4 +15,21 @@
15
15
  * detection, 30-minute timeout.
16
16
  */
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
+ export /** Details attached to tool result for rendering. */ interface TaskDetails {
19
+ task_id: string;
20
+ agent_type: string;
21
+ description: string;
22
+ conversation_id?: string;
23
+ phase: "done" | "timeout" | "aborted" | "failed";
24
+ status?: string;
25
+ summary?: string;
26
+ findings?: string;
27
+ evidence?: string;
28
+ confidence?: string;
29
+ duration_ms?: number;
30
+ turn_count?: number;
31
+ tool_uses?: number;
32
+ background?: boolean;
33
+ tmux_session?: string;
34
+ }
18
35
  export default function (pi: ExtensionAPI): void;
package/dist/index.js CHANGED
@@ -21,8 +21,9 @@ import { randomUUID } from "node:crypto";
21
21
  import { dirname, join } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { Type } from "@sinclair/typebox";
24
+ import { getArtifactsDir, normalizeConversationId, readConversationMetadata, readConversationRegistry, renderConversationSessions, taskArtifactName, taskIdFromArtifactName, writeConversationArtifacts, writeConversationRegistry, } from "./conversation.js";
24
25
  import { Text, truncateToWidth } from "@earendil-works/pi-tui";
25
- import { TASK_BACKGROUND_DEFAULT, TASK_RESULT_XML_INSTRUCTIONS, TASK_TOOL_DESCRIPTION, buildTmuxSplitWindowArgs, formatBackgroundReceipt, buildPiArgs, parseResultXml, formatMs, shellQuote, discoverAgents, formatAgentList, countToolUses, readRecentToolCalls, } from "./helpers.js";
26
+ import { TASK_BACKGROUND_DEFAULT, TASK_RESULT_XML_INSTRUCTIONS, TASK_TOOL_DESCRIPTION, buildTmuxSplitWindowArgs, chooseTmuxSplitDirection, formatBackgroundReceipt, buildPiArgs, parseResultXml, formatMs, shellQuote, discoverAgents, formatAgentList, countToolUses, readRecentToolCalls, } from "./helpers.js";
26
27
  import { runSdkSubagent } from "./subagent/runSdk.js";
27
28
  import { checkTaskCompletion, waitForTaskCompletion as waitForSessionTaskCompletion, } from "./subagent/waitCompletion.js";
28
29
  import { buildAgentToolSelection } from "./agent-tools.js";
@@ -31,7 +32,7 @@ const BUNDLED_AGENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "a
31
32
  const BACKGROUND_CHECK_MS = 10_000; // poll every 10 sec
32
33
  const COUNT_POLL_MS = 3_000; // update toolcall counts every 3 sec
33
34
  const TASK_TIMEOUT_MS = 30 * 60 * 1_000; // 30 minutes
34
- // ─── Registry helpers (durable JSON) ─────────────────────────────────────────
35
+ // Conversation helpers live in ./conversation.js.
35
36
  function readRegistry(piDir) {
36
37
  const path = join(piDir, "task-registry.json");
37
38
  try {
@@ -79,9 +80,28 @@ function getCurrentPaneId() {
79
80
  return null;
80
81
  }
81
82
  }
83
+ function getCurrentPaneSize(targetPane) {
84
+ try {
85
+ const args = ["display-message", "-p", "#{pane_width} #{pane_height}"];
86
+ if (targetPane)
87
+ args.splice(1, 0, "-t", targetPane);
88
+ const raw = tmuxCmd(args);
89
+ const [widthRaw, heightRaw] = raw.trim().split(/\s+/, 2);
90
+ const width = Number(widthRaw);
91
+ const height = Number(heightRaw);
92
+ if (!Number.isFinite(width) || !Number.isFinite(height))
93
+ return null;
94
+ return { width, height };
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
82
100
  function splitWindowPane(cwd, command) {
83
101
  const originalPane = getCurrentPaneId();
84
- const paneId = tmuxCmd(buildTmuxSplitWindowArgs(cwd, command));
102
+ const paneSize = getCurrentPaneSize(originalPane);
103
+ const direction = chooseTmuxSplitDirection(paneSize?.width ?? 0, paneSize?.height ?? 0);
104
+ const paneId = tmuxCmd(buildTmuxSplitWindowArgs(cwd, command, direction, originalPane));
85
105
  return { paneId, originalPane };
86
106
  }
87
107
  function killAgentPane(paneId, originalPane) {
@@ -171,6 +191,7 @@ export default function (pi) {
171
191
  startedAt: entry.startedAt,
172
192
  toolUses: 0,
173
193
  turns: 0,
194
+ conversationId: entry.conversationId,
174
195
  recentCalls: [],
175
196
  };
176
197
  backgroundTasks.set(entry.id, bgtask);
@@ -459,7 +480,10 @@ export default function (pi) {
459
480
  description: "A short (3-5 word) summary of the task",
460
481
  }),
461
482
  task_id: Type.Optional(Type.String({
462
- description: "Resume a previous task by ID (continues the same subagent session with its prior context instead of creating a fresh one)",
483
+ description: "Resume an existing background task by id instead of starting a new task.",
484
+ })),
485
+ conversation_id: Type.Optional(Type.String({
486
+ description: "Durable specialist conversation id. Reuses .pi/artifacts/task-<id>/sessions when called again.",
463
487
  })),
464
488
  background: Type.Optional(Type.Boolean({
465
489
  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.",
@@ -489,13 +513,115 @@ export default function (pi) {
489
513
  isError: true,
490
514
  };
491
515
  }
492
- // ── Resolve task identity: new or resume ───────────────────────────
516
+ // ── Resolve task identity: new, task resume, or conversation resume ──
517
+ const conversationId = normalizeConversationId(params.conversation_id);
518
+ const conversationRegistry = conversationId
519
+ ? readConversationRegistry(piDir)
520
+ : {};
521
+ const registeredArtifact = conversationId
522
+ ? conversationRegistry[conversationId]
523
+ : undefined;
524
+ const registeredTaskId = registeredArtifact
525
+ ? taskIdFromArtifactName(registeredArtifact)
526
+ : undefined;
527
+ if (params.task_id &&
528
+ registeredTaskId &&
529
+ params.task_id !== registeredTaskId) {
530
+ return {
531
+ content: [
532
+ {
533
+ type: "text",
534
+ text: `conversation_id "${conversationId}" maps to ${taskArtifactName(registeredTaskId)}, not ${taskArtifactName(params.task_id)}. Omit task_id or use the mapped task id.`,
535
+ },
536
+ ],
537
+ details: {
538
+ phase: "failed",
539
+ error: "conversation_id/task_id mismatch",
540
+ },
541
+ isError: true,
542
+ };
543
+ }
493
544
  let id;
494
545
  let sessionName;
495
546
  let artifactDir;
496
547
  let resultPath;
497
548
  let resume = false;
498
- if (params.task_id) {
549
+ if (registeredTaskId) {
550
+ id = registeredTaskId;
551
+ sessionName = taskArtifactName(id);
552
+ artifactDir = join(getArtifactsDir(piDir), sessionName);
553
+ resultPath = join(artifactDir, "RESULT.md");
554
+ if (!existsSync(artifactDir)) {
555
+ return {
556
+ content: [
557
+ {
558
+ type: "text",
559
+ text: `conversation_id "${conversationId}" points to missing artifact directory: ${artifactDir}`,
560
+ },
561
+ ],
562
+ details: {
563
+ phase: "failed",
564
+ error: "Conversation artifact dir missing",
565
+ conversation_id: conversationId,
566
+ },
567
+ isError: true,
568
+ };
569
+ }
570
+ const metadata = readConversationMetadata(join(artifactDir, "metadata.json"));
571
+ if (metadata?.agent_type && metadata.agent_type !== agent.name) {
572
+ return {
573
+ content: [
574
+ {
575
+ type: "text",
576
+ text: `conversation_id "${conversationId}" belongs to agent "${metadata.agent_type}", not "${agent.name}". Use the original agent_type or start a different conversation_id.`,
577
+ },
578
+ ],
579
+ details: {
580
+ phase: "failed",
581
+ error: "conversation_id agent_type mismatch",
582
+ conversation_id: conversationId,
583
+ },
584
+ isError: true,
585
+ };
586
+ }
587
+ resume = true;
588
+ const entry = readRegistry(piDir).find((candidate) => candidate.id === id);
589
+ if (params.background !== false &&
590
+ entry?.paneId &&
591
+ paneExists(entry.paneId)) {
592
+ const bgtask = {
593
+ dir: artifactDir,
594
+ agentType: entry.agentType,
595
+ sessionName,
596
+ paneId: entry.paneId,
597
+ originalPane: null,
598
+ description: params.description || entry.description,
599
+ startedAt: entry.startedAt,
600
+ toolUses: 0,
601
+ turns: 0,
602
+ conversationId,
603
+ recentCalls: [],
604
+ };
605
+ backgroundTasks.set(id, bgtask);
606
+ return {
607
+ content: [
608
+ {
609
+ type: "text",
610
+ text: `Resumed conversation "${conversationId}" via ${taskArtifactName(id)}. The subagent is running in background and will notify on completion.`,
611
+ },
612
+ ],
613
+ details: {
614
+ task_id: id,
615
+ agent_type: agent.name,
616
+ description: params.description,
617
+ conversation_id: conversationId,
618
+ tmux_session: sessionName,
619
+ background: true,
620
+ },
621
+ };
622
+ }
623
+ }
624
+ else if (params.task_id) {
499
625
  // Look up the task in the persistent registry
500
626
  const entries = readRegistry(piDir);
501
627
  const entry = entries.find((e) => e.id === params.task_id);
@@ -549,6 +675,7 @@ export default function (pi) {
549
675
  startedAt: entry.startedAt,
550
676
  toolUses: 0,
551
677
  turns: 0,
678
+ conversationId: entry.conversationId,
552
679
  recentCalls: [],
553
680
  };
554
681
  backgroundTasks.set(id, bgtask);
@@ -563,6 +690,7 @@ export default function (pi) {
563
690
  task_id: id,
564
691
  agent_type: agent.name,
565
692
  description: params.description,
693
+ conversation_id: entry.conversationId ?? conversationId,
566
694
  tmux_session: sessionName,
567
695
  background: true,
568
696
  },
@@ -571,11 +699,41 @@ export default function (pi) {
571
699
  }
572
700
  else {
573
701
  id = `${Date.now().toString(36)}-${randomUUID().slice(0, 4)}`;
574
- sessionName = `task-${id}`;
575
- artifactDir = join(piDir, "artifacts", sessionName);
702
+ sessionName = taskArtifactName(id);
703
+ artifactDir = join(getArtifactsDir(piDir), sessionName);
576
704
  await mkdir(artifactDir, { recursive: true });
577
705
  resultPath = join(artifactDir, "RESULT.md");
578
706
  }
707
+ if (conversationId && !hasTmux()) {
708
+ return {
709
+ content: [
710
+ {
711
+ type: "text",
712
+ text: "Durable conversations require the tmux/CLI backend so Pi can save and reopen the subagent session. Install/start tmux or omit conversation_id for a one-shot SDK task.",
713
+ },
714
+ ],
715
+ details: {
716
+ phase: "failed",
717
+ error: "tmux required for durable conversation",
718
+ conversation_id: conversationId,
719
+ },
720
+ isError: true,
721
+ };
722
+ }
723
+ if (conversationId) {
724
+ await mkdir(artifactDir, { recursive: true });
725
+ conversationRegistry[conversationId] = taskArtifactName(id);
726
+ writeConversationRegistry(piDir, conversationRegistry);
727
+ writeConversationArtifacts({
728
+ taskDir: artifactDir,
729
+ taskId: id,
730
+ conversationId,
731
+ agentType: agent.name,
732
+ sessionDir: join(artifactDir, "sessions"),
733
+ sessionName,
734
+ prompt: params.prompt,
735
+ });
736
+ }
579
737
  const descText = params.description || "";
580
738
  const isBackground = params.background ?? TASK_BACKGROUND_DEFAULT;
581
739
  // default true
@@ -642,6 +800,7 @@ export default function (pi) {
642
800
  startedAt: Date.now(),
643
801
  toolUses: 0,
644
802
  turns: 0,
803
+ conversationId,
645
804
  recentCalls: [],
646
805
  };
647
806
  if (foregroundTask) {
@@ -660,6 +819,7 @@ export default function (pi) {
660
819
  startedAt: Date.now(),
661
820
  toolUses: 0,
662
821
  turns: 0,
822
+ conversationId,
663
823
  recentCalls: [],
664
824
  };
665
825
  backgroundTasks.set(id, bgtask);
@@ -671,6 +831,7 @@ export default function (pi) {
671
831
  startedAt: bgtask.startedAt,
672
832
  piDir,
673
833
  dir: artifactDir,
834
+ conversationId,
674
835
  };
675
836
  const entries = readRegistry(piDir);
676
837
  entries.push(entry);
@@ -703,6 +864,7 @@ export default function (pi) {
703
864
  background: true,
704
865
  backend: "sdk",
705
866
  result_path: resultPath,
867
+ conversation_id: conversationId,
706
868
  },
707
869
  };
708
870
  }
@@ -717,6 +879,7 @@ export default function (pi) {
717
879
  backend: "sdk",
718
880
  session_path: sessionPath,
719
881
  result_path: resultPath,
882
+ conversation_id: conversationId,
720
883
  },
721
884
  };
722
885
  }
@@ -815,6 +978,7 @@ export default function (pi) {
815
978
  tool_uses: toolUses,
816
979
  turn_count: turns,
817
980
  background: false,
981
+ conversation_id: conversationId,
818
982
  },
819
983
  };
820
984
  }
@@ -829,6 +993,7 @@ export default function (pi) {
829
993
  startedAt: Date.now(),
830
994
  toolUses: 0,
831
995
  turns: 0,
996
+ conversationId,
832
997
  recentCalls: [],
833
998
  };
834
999
  backgroundTasks.set(id, bgtask);
@@ -842,6 +1007,7 @@ export default function (pi) {
842
1007
  paneId,
843
1008
  piDir,
844
1009
  dir: artifactDir,
1010
+ conversationId,
845
1011
  };
846
1012
  // Write to JSON registry for on-load restore
847
1013
  const entries = readRegistry(piDir);
@@ -955,4 +1121,12 @@ export default function (pi) {
955
1121
  return new Text(line, 0, 0);
956
1122
  },
957
1123
  });
1124
+ pi.registerCommand("task-sessions", {
1125
+ description: "List durable pi-task conversations",
1126
+ handler: async (_args, ctx) => {
1127
+ const cwd = ctx.sessionManager?.getCwd?.() ?? process.cwd();
1128
+ const { piDir } = discoverAgents(cwd);
1129
+ ctx.ui.notify(renderConversationSessions(piDir), "info");
1130
+ },
1131
+ });
958
1132
  }
@@ -5,6 +5,10 @@ export declare function tmuxCmd(args: string[]): string;
5
5
  export declare function hasTmux(): boolean;
6
6
  export declare function paneExists(paneId: string): boolean;
7
7
  export declare function getCurrentPaneId(): string | null;
8
+ export declare function getCurrentPaneSize(targetPane?: string | null): {
9
+ width: number;
10
+ height: number;
11
+ } | null;
8
12
  export declare function splitWindowPane(cwd: string, command: string): {
9
13
  paneId: string;
10
14
  originalPane: string | null;
@@ -2,6 +2,7 @@
2
2
  * Tmux helpers for subagent panes (shared by task extension).
3
3
  */
4
4
  import { execFileSync } from "node:child_process";
5
+ import { buildTmuxSplitWindowArgs, chooseTmuxSplitDirection } from "../helpers.js";
5
6
  export function tmuxCmd(args) {
6
7
  return execFileSync("tmux", args, {
7
8
  encoding: "utf-8",
@@ -34,18 +35,28 @@ export function getCurrentPaneId() {
34
35
  return null;
35
36
  }
36
37
  }
38
+ 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 {
52
+ return null;
53
+ }
54
+ }
37
55
  export function splitWindowPane(cwd, command) {
38
56
  const originalPane = getCurrentPaneId();
39
- const paneId = tmuxCmd([
40
- "split-window",
41
- "-h",
42
- "-P",
43
- "-F",
44
- "#{pane_id}",
45
- "-c",
46
- cwd,
47
- command,
48
- ]);
57
+ const paneSize = getCurrentPaneSize(originalPane);
58
+ const direction = chooseTmuxSplitDirection(paneSize?.width ?? 0, paneSize?.height ?? 0);
59
+ const paneId = tmuxCmd(buildTmuxSplitWindowArgs(cwd, command, direction, originalPane));
49
60
  return { paneId, originalPane };
50
61
  }
51
62
  export function killAgentPane(paneId, originalPane) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyhuynhgiabuu/pi-task",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",