pi-zellij 0.4.6 → 0.4.9
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 +8 -0
- package/README.md +49 -6
- package/{skills/code-review/SKILL.md → extensions/templates/review.md} +4 -27
- package/extensions/zv-core.ts +26 -3
- package/extensions/zv-open.ts +144 -4
- package/extensions/zv-review.ts +99 -12
- package/extensions/zv-split.ts +268 -21
- package/install.mjs +1 -1
- package/package.json +4 -5
- package/prompts/review-diff.md +0 -2
- package/prompts/review.md +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
|
+
- Added optional `pi-zellij.review.skill` settings for `/zrv` and `/zrh` to use an existing loaded skill, with project overrides and errors for unavailable or unreadable skills.
|
|
7
8
|
- Initial `pi-zellij` release with zellij-powered pane workflows for Pi.
|
|
8
9
|
- Added `/zv`, `/zj`, and `/zt` to open a new zellij pane or tab and start a fresh Pi session in the same working directory.
|
|
9
10
|
- Added `/zo` and `/zoh` to open a new pane and run any shell command there.
|
|
@@ -14,9 +15,15 @@
|
|
|
14
15
|
- 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
16
|
- 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
17
|
- Added opt-in `paneHighlight` settings so Pi can tint the current zellij pane when an agent turn completes, with optional working-state colors.
|
|
18
|
+
- 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.
|
|
19
|
+
- 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
20
|
|
|
18
21
|
### Changed
|
|
19
22
|
|
|
23
|
+
- `/zrv` and `/zrh` now use self-contained review instructions by default, injected only into the new review session. `/review` and `/review-diff` no longer refer to a bundled skill.
|
|
24
|
+
- Explicit history inheritance now rejects branches with no conversation messages before creating a cloned session or opening a pane.
|
|
25
|
+
- 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.
|
|
26
|
+
- Pi launch command construction now supports quoted provider, model, and thinking options and stops option parsing before initial prompts.
|
|
20
27
|
- 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
28
|
- `/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
29
|
- 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.
|
|
@@ -25,4 +32,5 @@
|
|
|
25
32
|
|
|
26
33
|
### Removed
|
|
27
34
|
|
|
35
|
+
- Removed the bundled `code-review` skill from the package and installer so terminal workflows do not register a global review skill.
|
|
28
36
|
- Removed the bundled `zv-notify` extension so `pi-zellij` does not conflict with separate notification packages or user-specific notification setups.
|
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
|
|
|
@@ -70,9 +70,7 @@ If pi is already running, use:
|
|
|
70
70
|
- `/review <target>`, `/review-diff [focus-or-pr-url]`
|
|
71
71
|
- expand bundled review prompts in the current pane
|
|
72
72
|
- `/zrv`, `/zrh`, plus review flags
|
|
73
|
-
- open review-focused Pi sessions in a split
|
|
74
|
-
- `/skill:code-review`
|
|
75
|
-
- loads the bundled structured review skill for files, directories, diffs, and PRs
|
|
73
|
+
- open review-focused Pi sessions in a split with built-in instructions or a configured skill
|
|
76
74
|
|
|
77
75
|
## Bundled extensions and resources
|
|
78
76
|
|
|
@@ -85,7 +83,6 @@ Extensions:
|
|
|
85
83
|
- `zv-continue`
|
|
86
84
|
|
|
87
85
|
Other bundled resources:
|
|
88
|
-
- `code-review` skill
|
|
89
86
|
- `/review` prompt template
|
|
90
87
|
- `/review-diff` prompt template
|
|
91
88
|
|
|
@@ -132,6 +129,29 @@ Examples:
|
|
|
132
129
|
|
|
133
130
|
Commands are executed via `sh -lc` in the current project directory.
|
|
134
131
|
|
|
132
|
+
### Agent-opened Pi sessions and terminals
|
|
133
|
+
|
|
134
|
+
`pi-zellij` registers two agent-facing tools:
|
|
135
|
+
|
|
136
|
+
- `zellij_start_pi` starts another interactive Pi session with an optional initial prompt and model settings.
|
|
137
|
+
- `zellij_open_terminal` opens arbitrary interactive terminal commands.
|
|
138
|
+
|
|
139
|
+
Example requests:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
start a fresh Pi agent in a right split and ask it to review the auth flow
|
|
143
|
+
continue this conversation in a lower Pi pane and focus on the failing tests
|
|
144
|
+
start Pi in a new tab using openai/gpt-4o with high thinking
|
|
145
|
+
open k9s in a new tab
|
|
146
|
+
open lazygit in a right split
|
|
147
|
+
open npm run dev below
|
|
148
|
+
open htop in a floating pane
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`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.
|
|
152
|
+
|
|
153
|
+
`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.
|
|
154
|
+
|
|
135
155
|
### Configured floating commands
|
|
136
156
|
|
|
137
157
|
You can register your own floating app shortcuts in Pi's main settings file under `pi-zellij.commands`.
|
|
@@ -294,7 +314,7 @@ Examples:
|
|
|
294
314
|
|
|
295
315
|
### Review helpers
|
|
296
316
|
|
|
297
|
-
`pi-zellij`
|
|
317
|
+
`pi-zellij` includes built-in review instructions; no skill installation is required and no review skill is registered globally. It also bundles prompt templates for in-place review:
|
|
298
318
|
|
|
299
319
|
- `/review <target>`
|
|
300
320
|
- prompt template for reviewing a file, directory, or GitHub pull request URL in the current pane
|
|
@@ -333,3 +353,26 @@ Examples:
|
|
|
333
353
|
|
|
334
354
|
If the target is a GitHub pull request URL, the review workflow switches to PR review and instructs pi to inspect the pull request with `gh pr view` and `gh pr diff`.
|
|
335
355
|
|
|
356
|
+
#### Use your own review skill
|
|
357
|
+
|
|
358
|
+
By default, `/zrv` and `/zrh` inject the built-in review instructions into the new session's initial prompt only. To use an existing skill instead, set `pi-zellij.review.skill` in `~/.pi/agent/settings.json` or the trusted project's `.pi/settings.json`:
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{
|
|
362
|
+
"pi-zellij": {
|
|
363
|
+
"review": {
|
|
364
|
+
"skill": "my-code-review"
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Use the skill's name without the `/skill:` prefix. It must be loaded in the current Pi session. The commands expand its instructions before opening the pane, preserving its location for relative references and passing the review target and mode. The skill controls the workflow and output format; the request still tells Pi not to edit files unless asked.
|
|
371
|
+
|
|
372
|
+
- Omit the setting to use the built-in instructions.
|
|
373
|
+
- Project settings override global settings. Set `"skill": null` in the project to use the built-in instructions despite a global override.
|
|
374
|
+
- An unavailable or unreadable skill, or invalid review settings, produces an error without opening a pane. There is no silent fallback.
|
|
375
|
+
- Review settings are read on each invocation. After installing or enabling a skill, run `/reload` first.
|
|
376
|
+
|
|
377
|
+
This setting only affects `/zrv` and `/zrh`; `/review` and `/review-diff` remain self-contained prompt templates.
|
|
378
|
+
|
|
@@ -1,33 +1,15 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: code-review
|
|
3
|
-
description: Structured code review workflow for files, directories, and git diffs. Use when reviewing code for bugs, regressions, maintainability, missing tests, or refactor opportunities.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
1
|
# Code Review
|
|
7
2
|
|
|
8
|
-
Use this skill for targeted code review requests, including file reviews, module reviews, and current diff reviews.
|
|
9
|
-
|
|
10
3
|
## Review goals
|
|
11
4
|
|
|
12
5
|
- Find the most important issues first.
|
|
13
6
|
- Prefer concrete, evidence-based findings over broad stylistic commentary.
|
|
14
7
|
- Call out missing tests or risky edge cases when they matter.
|
|
15
|
-
- Do not edit files unless the user explicitly asks for changes.
|
|
16
|
-
|
|
17
|
-
## Review modes
|
|
18
|
-
|
|
19
|
-
Infer the mode from the user request:
|
|
20
|
-
- **General review** — correctness, readability, maintainability, and missing tests
|
|
21
|
-
- **Bug-focused review** — correctness, runtime failures, invalid assumptions, edge cases, regressions
|
|
22
|
-
- **Refactor review** — simplification, structure, naming, duplication, cohesion, maintainability
|
|
23
|
-
- **Test review** — missing coverage, brittle assertions, edge cases, regression protection
|
|
24
|
-
- **Diff review** — review the current git diff or requested change set for regressions and missing validation
|
|
25
|
-
- **GitHub PR review** — when given a GitHub pull request URL, inspect it with `gh pr view` and `gh pr diff` before concluding
|
|
26
8
|
|
|
27
9
|
## Workflow
|
|
28
10
|
|
|
29
11
|
1. Identify the exact review scope.
|
|
30
|
-
-
|
|
12
|
+
- Stick to the requested file, directory, symbol, diff, or pull request.
|
|
31
13
|
- If the scope is ambiguous, ask one concise clarifying question.
|
|
32
14
|
|
|
33
15
|
2. Inspect the relevant code before concluding.
|
|
@@ -44,11 +26,8 @@ Infer the mode from the user request:
|
|
|
44
26
|
- If there are no meaningful issues, say so clearly.
|
|
45
27
|
|
|
46
28
|
5. Make findings actionable.
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
- why it matters
|
|
50
|
-
- where it occurs
|
|
51
|
-
- what change would improve it
|
|
29
|
+
- Explain what is wrong, why it matters, where it occurs, and what change would improve it.
|
|
30
|
+
- Include file paths and line numbers where possible.
|
|
52
31
|
|
|
53
32
|
## Output format
|
|
54
33
|
|
|
@@ -61,13 +40,11 @@ Summary
|
|
|
61
40
|
|
|
62
41
|
Findings
|
|
63
42
|
1. Severity: <high|medium|low>
|
|
64
|
-
- Location: <file or diff area>
|
|
43
|
+
- Location: <file and line or diff area>
|
|
65
44
|
- Issue: <what is wrong>
|
|
66
45
|
- Impact: <why it matters>
|
|
67
46
|
- Fix: <concrete suggestion>
|
|
68
47
|
|
|
69
|
-
2. ...
|
|
70
|
-
|
|
71
48
|
Test gaps
|
|
72
49
|
- <missing test or "None noted">
|
|
73
50
|
```
|
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-review.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_DIR_NAME,
|
|
3
|
+
getAgentDir,
|
|
4
|
+
stripFrontmatter,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionCommandContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
2
11
|
import {
|
|
3
12
|
buildPiCommand,
|
|
4
13
|
formatPaneSuccessMessage,
|
|
@@ -7,6 +16,8 @@ import {
|
|
|
7
16
|
type SplitDirection,
|
|
8
17
|
} from "./zv-core.ts";
|
|
9
18
|
|
|
19
|
+
const REVIEW_TEMPLATE_PATH = join(dirname(fileURLToPath(import.meta.url)), "templates", "review.md");
|
|
20
|
+
|
|
10
21
|
type ReviewMode = "general" | "bugs" | "refactor" | "tests" | "diff";
|
|
11
22
|
|
|
12
23
|
interface ReviewRequest {
|
|
@@ -61,13 +72,56 @@ function getGitHubPullRequestUrl(value: string | undefined): string | undefined
|
|
|
61
72
|
return /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+(?:[/?#].*)?$/.test(trimmed) ? trimmed : undefined;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
76
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readReviewSkillSetting(settingsPath: string): string | null | undefined {
|
|
80
|
+
let settings: unknown;
|
|
81
|
+
try {
|
|
82
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
throw new Error(`Failed to read settings from ${settingsPath}: ${message}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!isRecord(settings)) {
|
|
90
|
+
throw new Error(`Expected a settings object in ${settingsPath}`);
|
|
91
|
+
}
|
|
92
|
+
const section = settings["pi-zellij"];
|
|
93
|
+
if (section === undefined) return undefined;
|
|
94
|
+
if (!isRecord(section)) {
|
|
95
|
+
throw new Error(`pi-zellij must be an object in ${settingsPath}`);
|
|
96
|
+
}
|
|
97
|
+
const review = section.review;
|
|
98
|
+
if (review === undefined) return undefined;
|
|
99
|
+
if (!isRecord(review)) {
|
|
100
|
+
throw new Error(`pi-zellij.review must be an object in ${settingsPath}`);
|
|
101
|
+
}
|
|
102
|
+
const skill = review.skill;
|
|
103
|
+
if (skill === undefined || skill === null) return skill;
|
|
104
|
+
if (typeof skill !== "string" || !skill.trim()) {
|
|
105
|
+
throw new Error(`pi-zellij.review.skill must be a non-empty skill name or null in ${settingsPath}`);
|
|
106
|
+
}
|
|
107
|
+
return skill.trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loadReviewSkill(ctx: ExtensionCommandContext): string | undefined {
|
|
111
|
+
const settingsPaths = [join(getAgentDir(), "settings.json")];
|
|
112
|
+
if (ctx.isProjectTrusted()) {
|
|
113
|
+
settingsPaths.push(join(ctx.cwd, CONFIG_DIR_NAME, "settings.json"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let skill: string | null | undefined;
|
|
117
|
+
for (const settingsPath of settingsPaths) {
|
|
118
|
+
const value = readReviewSkillSetting(settingsPath);
|
|
119
|
+
if (value !== undefined) skill = value;
|
|
120
|
+
}
|
|
121
|
+
return skill ?? undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildReviewScope(request: ReviewRequest): string {
|
|
71
125
|
const pullRequestUrl = getGitHubPullRequestUrl(request.targetOrFocus);
|
|
72
126
|
const modeInstruction =
|
|
73
127
|
request.mode === "bugs"
|
|
@@ -79,15 +133,43 @@ function buildReviewPrompt(request: ReviewRequest): string {
|
|
|
79
133
|
: "Focus on correctness, readability, maintainability, and missing tests.";
|
|
80
134
|
|
|
81
135
|
if (pullRequestUrl) {
|
|
82
|
-
return `Review GitHub pull request ${pullRequestUrl}. Use the gh CLI to inspect it, including gh pr view ${pullRequestUrl} and gh pr diff ${pullRequestUrl}. ${modeInstruction} Prioritize the changed code, likely regressions, and missing tests before adding lower-priority notes
|
|
136
|
+
return `Review GitHub pull request ${pullRequestUrl}. Use the gh CLI to inspect it, including gh pr view ${pullRequestUrl} and gh pr diff ${pullRequestUrl}. ${modeInstruction} Prioritize the changed code, likely regressions, and missing tests before adding lower-priority notes.`;
|
|
83
137
|
}
|
|
84
138
|
|
|
85
139
|
if (request.mode === "diff") {
|
|
86
140
|
const focus = request.targetOrFocus ? ` Extra focus: ${request.targetOrFocus}.` : "";
|
|
87
|
-
return `Review the current git diff in this repository.${focus} Prioritize regressions, correctness issues, risky edge cases, and missing tests
|
|
141
|
+
return `Review the current git diff in this repository.${focus} Prioritize regressions, correctness issues, risky edge cases, and missing tests.`;
|
|
88
142
|
}
|
|
89
143
|
|
|
90
|
-
return `Review ${request.targetOrFocus} from the current project. ${modeInstruction} If the target is a directory, review the most relevant files within that scope
|
|
144
|
+
return `Review ${request.targetOrFocus} from the current project. ${modeInstruction} If the target is a directory, review the most relevant files within that scope.`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildReviewPrompt(request: ReviewRequest, ctx: ExtensionCommandContext): string {
|
|
148
|
+
const scope = `${buildReviewScope(request)} Do not edit files unless asked.`;
|
|
149
|
+
const skillName = loadReviewSkill(ctx);
|
|
150
|
+
if (!skillName) {
|
|
151
|
+
return `${readFileSync(REVIEW_TEMPLATE_PATH, "utf8").trim()}\n\n${scope}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const skill = ctx.getSystemPromptOptions().skills?.find((entry) => entry.name === skillName);
|
|
155
|
+
if (!skill) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Review skill "${skillName}" is not available in this session. Enable or install it and run /reload, or set pi-zellij.review.skill to null.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let body: string;
|
|
162
|
+
try {
|
|
163
|
+
body = stripFrontmatter(readFileSync(skill.filePath, "utf8")).trim();
|
|
164
|
+
if (!body) throw new Error("Skill instructions are empty");
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
throw new Error(`Cannot load review skill "${skillName}" from ${skill.filePath}: ${message}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Expand before launching, as Pi does for /skill:name. This validates the file
|
|
171
|
+
// and carries even CLI-only skills into the child without rediscovering them.
|
|
172
|
+
return `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>\n\n${scope}`;
|
|
91
173
|
}
|
|
92
174
|
|
|
93
175
|
async function openReviewSplit(
|
|
@@ -96,7 +178,12 @@ async function openReviewSplit(
|
|
|
96
178
|
direction: SplitDirection,
|
|
97
179
|
request: ReviewRequest,
|
|
98
180
|
): Promise<PaneOpenResult> {
|
|
99
|
-
|
|
181
|
+
try {
|
|
182
|
+
const prompt = buildReviewPrompt(request, ctx);
|
|
183
|
+
return await openCommandInNewSplit(pi, direction, buildPiCommand(ctx.cwd, { prompt }));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
186
|
+
}
|
|
100
187
|
}
|
|
101
188
|
|
|
102
189
|
function registerReviewCommand(
|
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/install.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const PACKAGE_DIR = path.join(AGENT_DIR, "packages", PACKAGE_NAME);
|
|
|
14
14
|
const PACKAGE_SETTINGS_ENTRY = `./packages/${PACKAGE_NAME}`;
|
|
15
15
|
const SOURCE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const FILES_TO_COPY = ["package.json", "README.md", "CHANGELOG.md"];
|
|
17
|
-
const DIRECTORIES_TO_COPY = ["extensions", "
|
|
17
|
+
const DIRECTORIES_TO_COPY = ["extensions", "prompts"];
|
|
18
18
|
|
|
19
19
|
const args = process.argv.slice(2);
|
|
20
20
|
const isRemove = args.includes("--remove") || args.includes("-r");
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-zellij",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
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",
|
|
@@ -25,7 +28,6 @@
|
|
|
25
28
|
},
|
|
26
29
|
"files": [
|
|
27
30
|
"extensions/",
|
|
28
|
-
"skills/",
|
|
29
31
|
"prompts/",
|
|
30
32
|
"install.mjs",
|
|
31
33
|
"README.md",
|
|
@@ -38,9 +40,6 @@
|
|
|
38
40
|
"extensions": [
|
|
39
41
|
"./extensions/index.ts"
|
|
40
42
|
],
|
|
41
|
-
"skills": [
|
|
42
|
-
"./skills"
|
|
43
|
-
],
|
|
44
43
|
"prompts": [
|
|
45
44
|
"./prompts"
|
|
46
45
|
]
|
package/prompts/review-diff.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Review the current git diff for regressions and missing tests
|
|
3
3
|
---
|
|
4
|
-
Use the bundled `code-review` skill if it is available.
|
|
5
|
-
|
|
6
4
|
Review the current git diff in this repository. If `${@:1}` is a GitHub pull request URL, inspect that pull request instead using `gh pr view <url>` and `gh pr diff <url>`. ${@:1} Prioritize regressions, correctness issues, risky edge cases, and missing tests.
|
|
7
5
|
|
|
8
6
|
Start with a concise summary ordered by severity. Then list concrete findings with suggested fixes. Do not edit files unless asked.
|
package/prompts/review.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Review a file or directory for bugs, maintainability issues, and missing tests
|
|
3
3
|
---
|
|
4
|
-
Use the bundled `code-review` skill if it is available.
|
|
5
|
-
|
|
6
4
|
Review `$1` from the current project. If the target is a GitHub pull request URL, inspect it with `gh pr view <url>` and `gh pr diff <url>`. Focus on correctness, readability, maintainability, and missing tests. If the target is a directory, review the most relevant files within that scope.
|
|
7
5
|
|
|
8
6
|
Start with a concise summary ordered by severity. Then list concrete findings with suggested fixes. Do not edit files unless asked.
|