pi-zellij 0.4.6 → 0.4.8

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
@@ -14,9 +14,14 @@
14
14
  - Added `zv-review` with `/zrv` and `/zrh`, plus bundled `code-review` skill and `/review` / `/review-diff` prompt templates for focused review workflows, including GitHub pull request review via `gh` when given a PR URL.
15
15
  - Added `zv-continue` with `/zcv` and `/zch` for split-based task handoff in the current checkout or by creating a git worktree branch with `-c <branch>`.
16
16
  - Added opt-in `paneHighlight` settings so Pi can tint the current zellij pane when an agent turn completes, with optional working-state colors.
17
+ - Added an agent-facing `zellij_open_terminal` tool so Pi can open explicitly requested interactive terminal commands in right or lower splits, tabs, and floating panes.
18
+ - Added an agent-facing `zellij_start_pi` tool for fresh Pi sessions in right or lower splits and tabs, with optional initial prompts, model settings, pane titles, and explicit history inheritance through a separate cloned session.
17
19
 
18
20
  ### Changed
19
21
 
22
+ - Explicit history inheritance now rejects branches with no conversation messages before creating a cloned session or opening a pane.
23
+ - Expanded `zellij_open_terminal` prompt guidance so agents recognize requests to start another Pi session and prefer the dedicated `zellij_start_pi` interface when available.
24
+ - Pi launch command construction now supports quoted provider, model, and thinking options and stops option parsing before initial prompts.
20
25
  - When zellij reports created pane or tab IDs, `pi-zellij` now shows them in success notifications for split, floating, zoxide, review, continuation, and tab commands.
21
26
  - `/zt` now uses `zellij action new-tab -- <command>` when available instead of always simulating typed input, while keeping the previous typed-input path as a compatibility fallback.
22
27
  - Pane highlights now clear on the next submitted input or when the pane is focused again after being elsewhere, instead of waiting for the next agent start event. Aborted runs no longer apply the done-state tint.
package/README.md CHANGED
@@ -10,7 +10,7 @@ Pi package with [zellij](https://zellij.dev)-powered terminal integrations for [
10
10
 
11
11
  [Pi](https://pi.dev) works well in the terminal, but pane orchestration is better handled by a terminal multiplexer. `pi-zellij` adds zellij-native split workflows for Pi.
12
12
 
13
- It includes split and tab commands, generic tool launchers, settings-driven floating app shortcuts, opt-in pane highlighting for completed agent turns, zoxide jumps, review workflows, and split-based task handoff.
13
+ It includes split and tab commands, generic tool launchers, agent-facing zellij tools, settings-driven floating app shortcuts, opt-in pane highlighting for completed agent turns, zoxide jumps, review workflows, and split-based task handoff.
14
14
 
15
15
  ## Usage
16
16
 
@@ -132,6 +132,29 @@ Examples:
132
132
 
133
133
  Commands are executed via `sh -lc` in the current project directory.
134
134
 
135
+ ### Agent-opened Pi sessions and terminals
136
+
137
+ `pi-zellij` registers two agent-facing tools:
138
+
139
+ - `zellij_start_pi` starts another interactive Pi session with an optional initial prompt and model settings.
140
+ - `zellij_open_terminal` opens arbitrary interactive terminal commands.
141
+
142
+ Example requests:
143
+
144
+ ```text
145
+ start a fresh Pi agent in a right split and ask it to review the auth flow
146
+ continue this conversation in a lower Pi pane and focus on the failing tests
147
+ start Pi in a new tab using openai/gpt-4o with high thinking
148
+ open k9s in a new tab
149
+ open lazygit in a right split
150
+ open npm run dev below
151
+ open htop in a floating pane
152
+ ```
153
+
154
+ `zellij_start_pi` supports `tab`, `right`, and `down` placements and defaults to `tab`. It starts a fresh session by default. The agent must set `continueSession` explicitly to inherit history; pi-zellij then clones the current active conversation into a separate session instead of opening the same session file in two processes. History inheritance requires a persisted source session with conversation history. The tool accepts `prompt`, `provider`, `model`, `thinking`, and `title` options, and handles the Pi command and shell quoting internally.
155
+
156
+ `zellij_open_terminal` supports `tab`, `right`, `down`, and `floating` placements and also defaults to `tab`. Floating terminals use the same 90% by 90% pane size as configured floating commands. Use it for TUIs, log tails, dev servers, watches, and other terminal views that should remain interactive instead of being captured through the normal shell tool.
157
+
135
158
  ### Configured floating commands
136
159
 
137
160
  You can register your own floating app shortcuts in Pi's main settings file under `pi-zellij.commands`.
@@ -3,9 +3,18 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  const ZELLIJ_TIMEOUT_MS = 5000;
4
4
 
5
5
  export type SplitDirection = "right" | "down";
6
+ export type PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
6
7
  export type PaneOpenResult = { ok: true; paneId?: string } | { ok: false; error: string };
7
8
  export type TabOpenResult = { ok: true; tabId?: string } | { ok: false; error: string };
8
9
 
10
+ export interface PiCommandOptions {
11
+ sessionFile?: string;
12
+ prompt?: string;
13
+ provider?: string;
14
+ model?: string;
15
+ thinking?: PiThinkingLevel;
16
+ }
17
+
9
18
  interface ZellijExecResult {
10
19
  ok: boolean;
11
20
  stdout: string;
@@ -60,14 +69,23 @@ export function shellEscape(value: string): string {
60
69
  return `'${value.replace(/'/g, `'\\''`)}'`;
61
70
  }
62
71
 
63
- export function buildPiCommand(cwd: string, options?: { sessionFile?: string; prompt?: string }): string {
72
+ export function buildPiCommand(cwd: string, options?: PiCommandOptions): string {
64
73
  const commandParts = ["cd", shellEscape(cwd), "&&", "exec", "pi"];
65
74
  if (options?.sessionFile) {
66
75
  commandParts.push("--session", shellEscape(options.sessionFile));
67
76
  }
77
+ if (options?.provider) {
78
+ commandParts.push("--provider", shellEscape(options.provider));
79
+ }
80
+ if (options?.model) {
81
+ commandParts.push("--model", shellEscape(options.model));
82
+ }
83
+ if (options?.thinking) {
84
+ commandParts.push("--thinking", shellEscape(options.thinking));
85
+ }
68
86
  const prompt = options?.prompt?.trim();
69
87
  if (prompt) {
70
- commandParts.push(shellEscape(prompt));
88
+ commandParts.push("--", shellEscape(prompt));
71
89
  }
72
90
  return commandParts.join(" ");
73
91
  }
@@ -182,12 +200,17 @@ export async function openCommandInNewSplit(
182
200
  pi: ExtensionAPI,
183
201
  direction: SplitDirection,
184
202
  command: string,
203
+ options?: { name?: string },
185
204
  ): Promise<PaneOpenResult> {
186
205
  if (!isInsideZellijSession()) {
187
206
  return { ok: false, error: "This command must be run from inside an active zellij session" };
188
207
  }
189
208
 
190
- const result = await execZellij(pi, ["run", "--direction", direction, "--", "sh", "-lc", command]);
209
+ const args = ["run", "--direction", direction];
210
+ if (options?.name) args.push("--name", options.name);
211
+ args.push("--", "sh", "-lc", command);
212
+
213
+ const result = await execZellij(pi, args);
191
214
  if (!result.ok) {
192
215
  return { ok: false, error: result.error || "Failed to open a new zellij pane" };
193
216
  }
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
@@ -7,8 +7,10 @@ import {
7
7
  formatPaneSuccessMessage,
8
8
  openCommandInFloatingPane as openCommandInFloatingZellijPane,
9
9
  openCommandInNewSplit,
10
+ openCommandInNewTab,
10
11
  type PaneOpenResult,
11
12
  type SplitDirection,
13
+ type TabOpenResult,
12
14
  } from "./zv-core.ts";
13
15
 
14
16
  const DEFAULT_FLOATING_PANE_OPTIONS = {
@@ -70,18 +72,59 @@ interface ConfiguredFloatingCommand {
70
72
  description: string;
71
73
  }
72
74
 
75
+ type TerminalPlacement = SplitDirection | "tab" | "floating";
76
+
77
+ type OpenToolContext = Pick<ExtensionContext, "cwd">;
78
+
79
+ interface ZellijOpenTerminalParams {
80
+ command: string;
81
+ placement?: TerminalPlacement;
82
+ title?: string;
83
+ }
84
+
85
+ interface OpenedTerminal {
86
+ ok: true;
87
+ placement: TerminalPlacement;
88
+ command: string;
89
+ paneId?: string;
90
+ tabId?: string;
91
+ }
92
+
93
+ const ZELLIJ_OPEN_TERMINAL_PARAMETERS = {
94
+ type: "object",
95
+ additionalProperties: false,
96
+ required: ["command"],
97
+ properties: {
98
+ command: {
99
+ type: "string",
100
+ description: "Interactive terminal command to run, for example k9s, htop, lazygit, or npm run dev",
101
+ },
102
+ placement: {
103
+ type: "string",
104
+ enum: ["right", "down", "tab", "floating"],
105
+ default: "tab",
106
+ description: "Where to open the command. Use floating for a 90% by 90% floating pane.",
107
+ },
108
+ title: {
109
+ type: "string",
110
+ description: "Optional zellij pane or tab name. Defaults to the command.",
111
+ },
112
+ },
113
+ } as const;
114
+
73
115
  async function openToolInSplit(
74
116
  pi: ExtensionAPI,
75
- ctx: ExtensionCommandContext,
117
+ ctx: OpenToolContext,
76
118
  direction: SplitDirection,
77
119
  args: string,
120
+ name?: string,
78
121
  ): Promise<PaneOpenResult> {
79
- return openCommandInNewSplit(pi, direction, buildShellCommand(ctx.cwd, args.trim()));
122
+ return openCommandInNewSplit(pi, direction, buildShellCommand(ctx.cwd, args.trim()), { name });
80
123
  }
81
124
 
82
125
  async function openToolInFloatingPane(
83
126
  pi: ExtensionAPI,
84
- ctx: ExtensionCommandContext,
127
+ ctx: OpenToolContext,
85
128
  command: string,
86
129
  name?: string,
87
130
  ): Promise<PaneOpenResult> {
@@ -91,6 +134,15 @@ async function openToolInFloatingPane(
91
134
  });
92
135
  }
93
136
 
137
+ async function openToolInTab(
138
+ pi: ExtensionAPI,
139
+ ctx: OpenToolContext,
140
+ command: string,
141
+ name?: string,
142
+ ): Promise<TabOpenResult> {
143
+ return openCommandInNewTab(pi, ctx.cwd, buildShellCommand(ctx.cwd, command.trim()), { name });
144
+ }
145
+
94
146
  function registerOpenCommand(
95
147
  pi: ExtensionAPI,
96
148
  name: string,
@@ -267,6 +319,92 @@ function registerConfiguredFloatingCommand(
267
319
  });
268
320
  }
269
321
 
322
+ function normalizeTerminalPlacement(value: unknown): TerminalPlacement {
323
+ return value === "right" || value === "down" || value === "tab" || value === "floating" ? value : "tab";
324
+ }
325
+
326
+ function getPlacementLabel(placement: TerminalPlacement): string {
327
+ if (placement === "right") {
328
+ return "right split";
329
+ }
330
+ if (placement === "down") {
331
+ return "lower split";
332
+ }
333
+ if (placement === "floating") {
334
+ return "floating pane";
335
+ }
336
+ return "tab";
337
+ }
338
+
339
+ async function openTerminalCommand(
340
+ pi: ExtensionAPI,
341
+ ctx: OpenToolContext,
342
+ params: ZellijOpenTerminalParams,
343
+ ): Promise<OpenedTerminal | { ok: false; error: string }> {
344
+ const command = typeof params.command === "string" ? params.command.trim() : "";
345
+ if (!command) {
346
+ return { ok: false, error: "Specify a command to open" };
347
+ }
348
+
349
+ const placement = normalizeTerminalPlacement(params.placement);
350
+ const title = params.title?.trim() || command;
351
+
352
+ if (placement === "tab") {
353
+ const result = await openToolInTab(pi, ctx, command, title);
354
+ if (!result.ok) {
355
+ return result;
356
+ }
357
+ return { ok: true, placement, command, tabId: result.tabId };
358
+ }
359
+
360
+ const result = placement === "floating"
361
+ ? await openToolInFloatingPane(pi, ctx, command, title)
362
+ : await openToolInSplit(pi, ctx, placement, command, title);
363
+ if (!result.ok) {
364
+ return result;
365
+ }
366
+
367
+ return { ok: true, placement, command, paneId: result.paneId };
368
+ }
369
+
370
+ function registerAgentTerminalTool(pi: ExtensionAPI): void {
371
+ pi.registerTool({
372
+ name: "zellij_open_terminal",
373
+ label: "Open zellij terminal",
374
+ description:
375
+ "Open an interactive terminal command in zellij as a right split, lower split, new tab, or floating pane. Use for user-requested Pi sessions, TUIs, logs, dev servers, watches, or long-running terminal views.",
376
+ promptSnippet:
377
+ "Open an interactive terminal command or another Pi session in zellij when the user asks for it in another pane, split, tab, or floating terminal.",
378
+ promptGuidelines: [
379
+ "Use zellij_open_terminal only when the user explicitly asks to open a command in zellij, another pane, split, tab, or floating terminal.",
380
+ "Use zellij_open_terminal from pi-zellij when the user requests another Pi session in a split or tab, including an initial prompt. Prefer zellij_start_pi when it is available.",
381
+ "Use zellij_open_terminal with placement='tab' when the user says tab, placement='right' for a side pane, placement='down' for a below/lower pane, and placement='floating' for a floating pane.",
382
+ "Use zellij_open_terminal for interactive TUIs like k9s, lazygit, htop, hunk, log tails, dev servers, or watches; do not use bash for these unless the user wants captured output.",
383
+ "Do not open terminals proactively with zellij_open_terminal without a user request.",
384
+ ],
385
+ parameters: ZELLIJ_OPEN_TERMINAL_PARAMETERS as any,
386
+ executionMode: "sequential",
387
+ async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) {
388
+ const params = rawParams as ZellijOpenTerminalParams;
389
+ const result = await openTerminalCommand(pi, ctx, params);
390
+ if (!result.ok) {
391
+ throw new Error(result.error);
392
+ }
393
+
394
+ return {
395
+ content: [{ type: "text", text: `Opened ${result.command} in a zellij ${getPlacementLabel(result.placement)}.` }],
396
+ details: {
397
+ command: result.command,
398
+ placement: result.placement,
399
+ cwd: ctx.cwd,
400
+ ...(result.paneId ? { paneId: result.paneId } : {}),
401
+ ...(result.tabId ? { tabId: result.tabId } : {}),
402
+ },
403
+ };
404
+ },
405
+ });
406
+ }
407
+
270
408
  export default function zvOpenExtension(pi: ExtensionAPI) {
271
409
  registerOpenCommand(
272
410
  pi,
@@ -292,4 +430,6 @@ export default function zvOpenExtension(pi: ExtensionAPI) {
292
430
  registerConfiguredFloatingCommand(pi, commandName, config);
293
431
  registeredConfiguredNames.add(commandName);
294
432
  }
433
+
434
+ registerAgentTerminalTool(pi);
295
435
  }
@@ -1,34 +1,191 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
1
+ import {
2
+ buildSessionContext,
3
+ SessionManager,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ } from "@earendil-works/pi-coding-agent";
2
7
  import {
3
8
  buildPiCommand,
4
9
  formatPaneSuccessMessage,
5
10
  formatTabSuccessMessage,
11
+ isInsideZellijSession,
6
12
  openCommandInNewSplit,
7
13
  openCommandInNewTab,
8
- type PaneOpenResult,
14
+ type PiCommandOptions,
15
+ type PiThinkingLevel,
9
16
  type SplitDirection,
10
- type TabOpenResult,
11
17
  } from "./zv-core.ts";
12
18
 
13
- async function openPiInSplit(
14
- pi: ExtensionAPI,
15
- ctx: ExtensionCommandContext,
16
- direction: SplitDirection,
17
- args: string,
18
- ): Promise<PaneOpenResult> {
19
- return openCommandInNewSplit(
20
- pi,
21
- direction,
22
- buildPiCommand(ctx.cwd, { prompt: args.trim().length > 0 ? args : undefined }),
23
- );
19
+ export type PiPlacement = SplitDirection | "tab";
20
+ type PiLaunchContext = Pick<ExtensionContext, "cwd">;
21
+ type PiContinuationContext = Pick<ExtensionContext, "cwd" | "sessionManager">;
22
+
23
+ export interface ZellijStartPiParams {
24
+ placement?: PiPlacement;
25
+ prompt?: string;
26
+ continueSession?: boolean;
27
+ provider?: string;
28
+ model?: string;
29
+ thinking?: PiThinkingLevel;
30
+ title?: string;
31
+ }
32
+
33
+ interface OpenedPiSession {
34
+ ok: true;
35
+ placement: PiPlacement;
36
+ paneId?: string;
37
+ tabId?: string;
38
+ }
39
+
40
+ const THINKING_LEVELS: readonly PiThinkingLevel[] = [
41
+ "off",
42
+ "minimal",
43
+ "low",
44
+ "medium",
45
+ "high",
46
+ "xhigh",
47
+ "max",
48
+ ];
49
+
50
+ const ZELLIJ_START_PI_PARAMETERS = {
51
+ type: "object",
52
+ additionalProperties: false,
53
+ properties: {
54
+ placement: {
55
+ type: "string",
56
+ enum: ["right", "down", "tab"],
57
+ default: "tab",
58
+ description: "Where to start Pi: a right split, lower split, or new tab. Defaults to tab.",
59
+ },
60
+ prompt: {
61
+ type: "string",
62
+ description: "Optional initial prompt for the new Pi session. The extension handles shell quoting.",
63
+ },
64
+ continueSession: {
65
+ type: "boolean",
66
+ default: false,
67
+ description:
68
+ "Clone the current active conversation into a separate session before starting Pi. Set true only when the user explicitly asks to continue or inherit the current history. Defaults to false for a fresh session.",
69
+ },
70
+ provider: {
71
+ type: "string",
72
+ description: "Optional Pi provider. Requires model; omit both to use Pi's normal session/default selection.",
73
+ },
74
+ model: {
75
+ type: "string",
76
+ description: "Optional Pi model pattern or ID, including provider/model and model:thinking forms.",
77
+ },
78
+ thinking: {
79
+ type: "string",
80
+ enum: THINKING_LEVELS,
81
+ description: "Optional Pi thinking level for the new session.",
82
+ },
83
+ title: {
84
+ type: "string",
85
+ description: "Optional zellij pane or tab name.",
86
+ },
87
+ },
88
+ } as const;
89
+
90
+ function normalizePiPlacement(value: unknown): PiPlacement {
91
+ return value === "right" || value === "down" || value === "tab" ? value : "tab";
92
+ }
93
+
94
+ function normalizeOptionalString(value: unknown): string | undefined {
95
+ if (typeof value !== "string") return undefined;
96
+ return value.trim() || undefined;
97
+ }
98
+
99
+ function normalizeThinkingLevel(value: unknown): PiThinkingLevel | undefined {
100
+ return typeof value === "string" && THINKING_LEVELS.includes(value as PiThinkingLevel)
101
+ ? (value as PiThinkingLevel)
102
+ : undefined;
103
+ }
104
+
105
+ function getPlacementLabel(placement: PiPlacement): string {
106
+ if (placement === "right") return "right split";
107
+ if (placement === "down") return "lower split";
108
+ return "tab";
24
109
  }
25
110
 
26
- async function openPiInTab(
111
+ async function openPiSession(
27
112
  pi: ExtensionAPI,
28
- ctx: ExtensionCommandContext,
29
- args: string,
30
- ): Promise<TabOpenResult> {
31
- return openCommandInNewTab(pi, ctx.cwd, buildPiCommand(ctx.cwd, { prompt: args.trim().length > 0 ? args : undefined }));
113
+ ctx: PiLaunchContext,
114
+ placement: PiPlacement,
115
+ options: PiCommandOptions,
116
+ title?: string,
117
+ ): Promise<OpenedPiSession | { ok: false; error: string }> {
118
+ const command = buildPiCommand(ctx.cwd, options);
119
+ if (placement === "tab") {
120
+ const result = await openCommandInNewTab(pi, ctx.cwd, command, { name: title });
121
+ if (!result.ok) return result;
122
+ return { ok: true, placement, tabId: result.tabId };
123
+ }
124
+
125
+ const result = await openCommandInNewSplit(pi, placement, command, { name: title });
126
+ if (!result.ok) return result;
127
+ return { ok: true, placement, paneId: result.paneId };
128
+ }
129
+
130
+ function createContinuationSession(
131
+ ctx: PiContinuationContext,
132
+ ): { ok: true; sessionFile: string } | { ok: false; error: string } {
133
+ const sourceSessionFile = ctx.sessionManager.getSessionFile();
134
+ if (!sourceSessionFile) {
135
+ return { ok: false, error: "Cannot inherit history from an ephemeral session" };
136
+ }
137
+
138
+ const currentLeaf = ctx.sessionManager.getLeafEntry();
139
+ if (!currentLeaf) {
140
+ return { ok: false, error: "The current session has no conversation history to inherit" };
141
+ }
142
+
143
+ // During tool execution, the active leaf is the assistant message containing
144
+ // this tool call. Exclude it so the child does not inherit a tool call without
145
+ // its result.
146
+ const continuationLeafId =
147
+ currentLeaf.type === "message" && currentLeaf.message.role === "assistant"
148
+ ? currentLeaf.parentId
149
+ : currentLeaf.id;
150
+ if (!continuationLeafId) {
151
+ return { ok: false, error: "The current session has no completed conversation history to inherit" };
152
+ }
153
+
154
+ try {
155
+ const inheritedContext = buildSessionContext(ctx.sessionManager.getEntries(), continuationLeafId);
156
+ if (inheritedContext.messages.length === 0) {
157
+ return { ok: false, error: "The current session has no conversation history to inherit" };
158
+ }
159
+
160
+ const inheritedBranch = ctx.sessionManager.getBranch(continuationLeafId);
161
+ const hasCompletedAssistantMessage = inheritedBranch.some(
162
+ (entry) => entry.type === "message" && entry.message.role === "assistant",
163
+ );
164
+ if (hasCompletedAssistantMessage) {
165
+ const sourceSession = SessionManager.open(sourceSessionFile, ctx.sessionManager.getSessionDir());
166
+ const sessionFile = sourceSession.createBranchedSession(continuationLeafId);
167
+ if (sessionFile) return { ok: true, sessionFile };
168
+ }
169
+
170
+ // A branch with only the triggering user message is not written by
171
+ // createBranchedSession until it receives an assistant response. Fork the
172
+ // file and move its active leaf instead so Pi can open it immediately.
173
+ const continuationSession = SessionManager.forkFrom(
174
+ sourceSessionFile,
175
+ ctx.cwd,
176
+ ctx.sessionManager.getSessionDir(),
177
+ );
178
+ continuationSession.branch(continuationLeafId);
179
+ continuationSession.appendCustomEntry("pi-zellij-continuation");
180
+ const sessionFile = continuationSession.getSessionFile();
181
+ if (!sessionFile) {
182
+ return { ok: false, error: "Failed to create a persistent continuation session" };
183
+ }
184
+ return { ok: true, sessionFile };
185
+ } catch (error) {
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ return { ok: false, error: `Failed to clone the current session: ${message}` };
188
+ }
32
189
  }
33
190
 
34
191
  function registerSplitCommand(
@@ -41,7 +198,8 @@ function registerSplitCommand(
41
198
  pi.registerCommand(name, {
42
199
  description,
43
200
  handler: async (args, ctx) => {
44
- const result = await openPiInSplit(pi, ctx, direction, args);
201
+ const prompt = normalizeOptionalString(args);
202
+ const result = await openPiSession(pi, ctx, direction, { prompt });
45
203
  if (result.ok) {
46
204
  ctx.ui.notify(formatPaneSuccessMessage(successMessage, result.paneId), "info");
47
205
  } else {
@@ -60,7 +218,8 @@ function registerTabCommand(
60
218
  pi.registerCommand(name, {
61
219
  description,
62
220
  handler: async (args, ctx) => {
63
- const result = await openPiInTab(pi, ctx, args);
221
+ const prompt = normalizeOptionalString(args);
222
+ const result = await openPiSession(pi, ctx, "tab", { prompt });
64
223
  if (result.ok) {
65
224
  ctx.ui.notify(formatTabSuccessMessage(successMessage, result.tabId), "info");
66
225
  } else {
@@ -70,6 +229,92 @@ function registerTabCommand(
70
229
  });
71
230
  }
72
231
 
232
+ function registerAgentPiTool(pi: ExtensionAPI): void {
233
+ pi.registerTool({
234
+ name: "zellij_start_pi",
235
+ label: "Start Pi in zellij",
236
+ description:
237
+ "Start another interactive Pi session in a zellij right split, lower split, or tab, with an optional initial prompt and model settings. Sessions are fresh by default; current history is inherited only when continueSession is explicitly true.",
238
+ promptSnippet:
239
+ "Start another interactive Pi agent in a zellij split or tab with an optional initial prompt, model settings, or explicitly inherited history.",
240
+ promptGuidelines: [
241
+ "Use zellij_start_pi when the user explicitly requests another Pi session or agent in a zellij split or tab, including a request with an initial prompt.",
242
+ "Use zellij_start_pi with placement='tab' when the user says tab, placement='right' for a side pane, and placement='down' for a below/lower pane.",
243
+ "zellij_start_pi starts a fresh session by default. Set continueSession=true only when the user explicitly asks the new Pi session to continue or inherit the current conversation.",
244
+ "Pass Pi tasks through zellij_start_pi's prompt parameter and model choices through provider, model, and thinking; do not construct or quote a Pi shell command yourself.",
245
+ "Do not start another Pi session proactively with zellij_start_pi without a user request.",
246
+ ],
247
+ parameters: ZELLIJ_START_PI_PARAMETERS as any,
248
+ executionMode: "sequential",
249
+ async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) {
250
+ const params = rawParams as ZellijStartPiParams;
251
+ if (!isInsideZellijSession()) {
252
+ throw new Error("This tool must be run from inside an active zellij session");
253
+ }
254
+
255
+ const placement = normalizePiPlacement(params.placement);
256
+ const prompt = normalizeOptionalString(params.prompt);
257
+ const provider = normalizeOptionalString(params.provider);
258
+ const model = normalizeOptionalString(params.model);
259
+ const thinking = normalizeThinkingLevel(params.thinking);
260
+ const title = normalizeOptionalString(params.title);
261
+
262
+ if (params.provider !== undefined && !provider) {
263
+ throw new Error("provider must be a non-empty string");
264
+ }
265
+ if (params.model !== undefined && !model) {
266
+ throw new Error("model must be a non-empty string");
267
+ }
268
+ if (provider && !model) {
269
+ throw new Error("provider requires model");
270
+ }
271
+ if (params.thinking !== undefined && !thinking) {
272
+ throw new Error(`Invalid thinking level: ${String(params.thinking)}`);
273
+ }
274
+
275
+ let sessionFile: string | undefined;
276
+ if (params.continueSession === true) {
277
+ const continuation = createContinuationSession(ctx);
278
+ if (!continuation.ok) throw new Error(continuation.error);
279
+ sessionFile = continuation.sessionFile;
280
+ }
281
+
282
+ const result = await openPiSession(
283
+ pi,
284
+ ctx,
285
+ placement,
286
+ { sessionFile, prompt, provider, model, thinking },
287
+ title,
288
+ );
289
+ if (!result.ok) {
290
+ throw new Error(result.error);
291
+ }
292
+
293
+ const historyLabel = sessionFile ? "a Pi session with inherited history" : "a fresh Pi session";
294
+ return {
295
+ content: [
296
+ {
297
+ type: "text",
298
+ text: `Opened ${historyLabel} in a zellij ${getPlacementLabel(result.placement)}.`,
299
+ },
300
+ ],
301
+ details: {
302
+ placement: result.placement,
303
+ cwd: ctx.cwd,
304
+ history: sessionFile ? "inherited" : "fresh",
305
+ ...(sessionFile ? { sessionFile } : {}),
306
+ ...(provider ? { provider } : {}),
307
+ ...(model ? { model } : {}),
308
+ ...(thinking ? { thinking } : {}),
309
+ ...(title ? { title } : {}),
310
+ ...(result.paneId ? { paneId: result.paneId } : {}),
311
+ ...(result.tabId ? { tabId: result.tabId } : {}),
312
+ },
313
+ };
314
+ },
315
+ });
316
+ }
317
+
73
318
  export default function zvSplitExtension(pi: ExtensionAPI) {
74
319
  registerSplitCommand(
75
320
  pi,
@@ -93,4 +338,6 @@ export default function zvSplitExtension(pi: ExtensionAPI) {
93
338
  "Open a new zellij tab and start a fresh pi session",
94
339
  "Opened a new tab",
95
340
  );
341
+
342
+ registerAgentPiTool(pi);
96
343
  }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "pi-zellij",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Pi package with zellij-powered terminal integrations",
5
5
  "author": "Javi Molina",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
+ "scripts": {
9
+ "test": "node --test tests/*.test.mjs"
10
+ },
8
11
  "keywords": [
9
12
  "pi-package",
10
13
  "pi",