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 +5 -0
- package/README.md +24 -1
- package/extensions/zv-core.ts +26 -3
- package/extensions/zv-open.ts +144 -4
- package/extensions/zv-split.ts +268 -21
- package/package.json +4 -1
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`.
|
package/extensions/zv-core.ts
CHANGED
|
@@ -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?:
|
|
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
|
|
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
|
}
|
package/extensions/zv-open.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI,
|
|
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:
|
|
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:
|
|
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
|
}
|
package/extensions/zv-split.ts
CHANGED
|
@@ -1,34 +1,191 @@
|
|
|
1
|
-
import
|
|
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
|
|
14
|
+
type PiCommandOptions,
|
|
15
|
+
type PiThinkingLevel,
|
|
9
16
|
type SplitDirection,
|
|
10
|
-
type TabOpenResult,
|
|
11
17
|
} from "./zv-core.ts";
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
111
|
+
async function openPiSession(
|
|
27
112
|
pi: ExtensionAPI,
|
|
28
|
-
ctx:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
|
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.
|
|
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",
|