pi-zellij 0.3.1

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 ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - Initial `pi-zellij` release with zellij-powered pane workflows for Pi.
8
+
9
+ ### Changed
10
+
11
+ - Clarified README notification wording to match the current `zv-notify` behavior: `Waiting`, `Task Complete`, and `Error`.
12
+ - Added `zv-notify` for desktop notifications using `osascript` on macOS or `notify-send` on Linux.
13
+ - Added `/zv`, `/zj`, and `/zt` to open a new zellij pane or tab and start a fresh Pi session in the same working directory.
14
+ - Added `/zo` and `/zoh` to open a new pane and run any shell command there.
15
+ - Added configurable floating app commands via `pi-zellij.commands` in Pi `settings.json`, including shorthand entries such as `"zh": "hx"` and `"zg": "lazygit"`, plus object entries with `acceptArgs` support.
16
+ - Added compatibility fallback for legacy `pi-zv.commands` settings during the rename to `pi-zellij`.
17
+ - Reserved Pi built-in slash commands such as `/settings`, `/model`, and `/reload` so configured floating commands cannot shadow them.
18
+ - Added `/zz` and `/zzh` to open a new pane from a zoxide match or direct directory path and start Pi there.
19
+ - 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.
20
+ - 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>`.
package/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # pi-zellij
2
+
3
+ Pi package with zellij-powered terminal integrations for [Pi](https://pi.dev).
4
+
5
+ ## Why
6
+
7
+ [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, plus optional desktop notifications.
8
+
9
+ It includes split and tab commands, generic tool launchers, settings-driven floating app shortcuts, zoxide jumps, review workflows, split-based task handoff, and automatic run notifications.
10
+
11
+ ## Usage
12
+
13
+ Install with pi:
14
+
15
+ ```bash
16
+ pi install npm:pi-zellij
17
+ ```
18
+
19
+ Or with the installer:
20
+
21
+ ```bash
22
+ npx pi-zellij
23
+ ```
24
+
25
+ If pi is already running, use:
26
+
27
+ ```text
28
+ /reload
29
+ ```
30
+
31
+ ## Requirements
32
+
33
+ - `zellij` must be installed
34
+ - pane, tab, and floating commands must be run from inside an active zellij session
35
+ - `zoxide` is required for the zoxide commands
36
+ - notifications use:
37
+ - `osascript` on macOS
38
+ - `notify-send` on Linux
39
+
40
+ ## Feature overview
41
+
42
+ ### Pane and tab workflows
43
+
44
+ - `/zv`, `/zj`, `/zt`
45
+ - start a fresh Pi session in a new right pane, lower pane, or tab
46
+ - `/zo <command...>`, `/zoh <command...>`
47
+ - run any shell command in a new right pane or lower pane
48
+ - `/zz <query>`, `/zzh <query>`
49
+ - jump to a zoxide match or direct directory path and start Pi there
50
+
51
+ ### Floating tools
52
+
53
+ - `pi-zellij.commands` in `settings.json`
54
+ - registers floating app shortcuts such as `/zh` for `hx` or `/zg` for `lazygit`
55
+
56
+ ### Review and handoff workflows
57
+
58
+ - `/zcv`, `/zch`
59
+ - open continuation sessions in a split, optionally in a new git worktree
60
+ - `/review <target>`, `/review-diff [focus-or-pr-url]`
61
+ - expand bundled review prompts in the current pane
62
+ - `/zrv`, `/zrh`, plus review flags
63
+ - open review-focused Pi sessions in a split
64
+ - `/skill:code-review`
65
+ - loads the bundled structured review skill for files, directories, diffs, and PRs
66
+
67
+ ### Notifications
68
+
69
+ - automatic via `zv-notify`
70
+ - sends desktop notifications for Pi run states such as `Waiting`, `Task Complete`, and `Error`
71
+
72
+ ## Bundled extensions and resources
73
+
74
+ Extensions:
75
+ - `zv-notify`
76
+ - `zv-split`
77
+ - `zv-open`
78
+ - `zv-zoxide`
79
+ - `zv-review`
80
+ - `zv-continue`
81
+
82
+ Other bundled resources:
83
+ - `code-review` skill
84
+ - `/review` prompt template
85
+ - `/review-diff` prompt template
86
+
87
+ ## Commands
88
+
89
+ ### Split and tab commands
90
+
91
+ - `/zv`
92
+ - opens a new pane to the right
93
+ - starts a fresh `pi` session in the same `cwd`
94
+ - `/zj`
95
+ - opens a new pane below
96
+ - starts a fresh `pi` session in the same `cwd`
97
+ - `/zt`
98
+ - opens a new zellij tab
99
+ - starts a fresh `pi` session in the same `cwd`
100
+
101
+ All three commands also accept optional initial prompt text.
102
+
103
+ Examples:
104
+
105
+ ```text
106
+ /zv Review the auth flow in this repo
107
+ /zt Investigate flaky tests in this repo
108
+ ```
109
+
110
+ ### Tool split commands
111
+
112
+ - `/zo <command...>`
113
+ - opens a new pane to the right
114
+ - runs the given shell command in the same `cwd`
115
+ - `/zoh <command...>`
116
+ - opens a new pane below
117
+ - runs the given shell command in the same `cwd`
118
+
119
+ Examples:
120
+
121
+ ```text
122
+ /zo hx
123
+ /zo npm test
124
+ /zoh npm run dev
125
+ /zo watch -n 1 git status --short
126
+ ```
127
+
128
+ Commands are executed via `sh -lc` in the current project directory.
129
+
130
+ ### Configured floating commands
131
+
132
+ You can register your own floating app shortcuts in Pi's main settings file under `pi-zellij.commands`.
133
+
134
+ Supported locations:
135
+ - `~/.pi/agent/settings.json` for global commands
136
+ - `.pi/settings.json` for project-local commands
137
+
138
+ During the rename from `pi-zv` to `pi-zellij`, legacy `pi-zv.commands` is still accepted for compatibility. If both keys exist, `pi-zellij.commands` wins.
139
+
140
+ Simple form:
141
+
142
+ ```json
143
+ {
144
+ "pi-zellij": {
145
+ "commands": {
146
+ "zh": "hx",
147
+ "zg": "lazygit"
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ Each configured command opens in a floating zellij pane using a default `90%` by `90%` popup with `5%` margins.
154
+
155
+ Examples:
156
+
157
+ ```text
158
+ /zh
159
+ /zg
160
+ ```
161
+
162
+ For commands that should accept extra arguments, use the object form:
163
+
164
+ ```json
165
+ {
166
+ "pi-zellij": {
167
+ "commands": {
168
+ "zh": {
169
+ "run": "hx",
170
+ "acceptArgs": true,
171
+ "description": "Open Helix in a floating pane"
172
+ }
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ Then you can pass arguments through to the configured command:
179
+
180
+ ```text
181
+ /zh src/auth.ts
182
+ ```
183
+
184
+ Configured command names cannot reuse built-in Pi commands such as `/settings`, `/model`, or `/reload`, and they also cannot replace pi-zellij's own slash commands such as `/zv`, `/zj`, `/zt`, `/zz`, or `/zcv`.
185
+
186
+ If the same command exists in both global and project settings, the project setting wins. After changing settings, run `/reload` in Pi.
187
+
188
+ ### Zoxide jump commands
189
+
190
+ - `/zz <query>`
191
+ - resolves the query with `zoxide query`
192
+ - opens a new pane to the right
193
+ - starts a fresh pi session in the matched directory
194
+ - `/zzh <query>`
195
+ - resolves the query with `zoxide query`
196
+ - opens a new pane below
197
+ - starts a fresh pi session in the matched directory
198
+
199
+ Example:
200
+
201
+ ```text
202
+ /zz mono
203
+ ```
204
+
205
+ If the argument is already a valid directory path, `/zz` and `/zzh` use it directly instead of querying zoxide.
206
+
207
+ ### Continuation and worktree helpers
208
+
209
+ - `/zcv`
210
+ - opens a new pane to the right
211
+ - creates a related handoff session in the current checkout
212
+ - `/zch`
213
+ - opens a new pane below
214
+ - creates a related handoff session in the current checkout
215
+ - `/zcv <note>` / `/zch <note>`
216
+ - same as above, but adds a focus note to the handoff context
217
+ - `/zcv -c <branch>` / `/zch -c <branch>`
218
+ - creates a new branch worktree from the current `HEAD`, then opens a new pane there
219
+ - `/zcv -c <branch> --from <ref>` / `/zch -c <branch> --from <ref>`
220
+ - creates a new branch worktree from an explicit base ref such as `main` or `origin/main`
221
+ - `/zcv -c <branch> [--from <ref>] <note...>` / `/zch -c <branch> [--from <ref>] <note...>`
222
+ - same as above, but also adds a focus note to the worktree handoff
223
+
224
+ Examples:
225
+
226
+ ```text
227
+ /zcv
228
+ /zcv focus on tests
229
+ /zcv -c fix/notify-bug
230
+ /zcv -c fix/notify-bug --from main
231
+ /zcv -c fix/notify-bug --from main review the existing changes first
232
+ /zch -c feature/review-ui focus on edge cases
233
+ ```
234
+
235
+ ### Review helpers
236
+
237
+ `pi-zellij` also bundles a reusable `code-review` skill plus prompt templates for in-place review:
238
+
239
+ - `/review <target>`
240
+ - prompt template for reviewing a file, directory, or GitHub pull request URL in the current pane
241
+ - `/review-diff [focus-or-pr-url]`
242
+ - prompt template for reviewing the current git diff in the current pane, or a GitHub pull request URL via `gh`
243
+
244
+ Split review commands:
245
+
246
+ - `/zrv`
247
+ - with no arguments, reviews the current git diff in a new right pane
248
+ - `/zrh`
249
+ - with no arguments, reviews the current git diff in a new lower pane
250
+ - `/zrv [--bugs|--refactor|--tests] <target>` or `/zrv --diff [focus]`
251
+ - opens a new pane to the right
252
+ - starts a fresh pi review session in the same `cwd`
253
+ - `/zrh [--bugs|--refactor|--tests] <target>` or `/zrh --diff [focus]`
254
+ - opens a new pane below
255
+ - starts a fresh pi review session in the same `cwd`
256
+
257
+ `--diff` is the default, so `/zrv` and `/zrh` usually do not need the flag.
258
+
259
+ There are no `/review-v` or `/review-h` aliases in `pi-zellij`, so it can coexist more cleanly with other Pi packages.
260
+
261
+ Examples:
262
+
263
+ ```text
264
+ /zrv
265
+ /zrh
266
+ /zrv src/auth.ts
267
+ /zrv --bugs src/auth.ts
268
+ /zrh --refactor src/auth/
269
+ /zrv --diff
270
+ /zrh --diff focus on token refresh and retries
271
+ /zrv https://github.com/owner/repo/pull/123
272
+ ```
273
+
274
+ 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`.
275
+
276
+ ## Notifications
277
+
278
+ The bundled `zv-notify` extension summarizes each run and sends a desktop notification when supported by the host system.
279
+
280
+ Current notification types:
281
+ - `Waiting`
282
+ - `Task Complete`
283
+ - `Error`
284
+
285
+ You can control notification noise with one setting:
286
+ - `PI_ZV_NOTIFY_LEVEL=all` - `Waiting`, `Task Complete`, and `Error`
287
+ - `PI_ZV_NOTIFY_LEVEL=medium` - `Task Complete` and `Error`
288
+ - `PI_ZV_NOTIFY_LEVEL=low` - `Error` only
289
+ - `PI_ZV_NOTIFY_LEVEL=disabled` - disable notifications
290
+
291
+ ## Environment variables
292
+
293
+ - `PI_ZV_NOTIFY_LEVEL` - notification level: `all`, `medium`, `low`, or `disabled` (default: `all`)
294
+ - `PI_ZV_NOTIFY_THRESHOLD_MS` - duration threshold before a run is labeled `Task Complete` instead of `Waiting` (default: `15000`)
295
+ - `PI_ZV_NOTIFY_DEBOUNCE_MS` - minimum delay between duplicate notifications (default: `3000`)
296
+ - `PI_ZV_NOTIFY_TITLE` - notification title override (default: `Pi`)
@@ -0,0 +1,184 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+
5
+ const GIT_TIMEOUT_MS = 10000;
6
+ const DEFAULT_MAX_STATUS_LINES = 20;
7
+
8
+ export interface GitExecResult {
9
+ ok: boolean;
10
+ stdout: string;
11
+ stderr: string;
12
+ error?: string;
13
+ }
14
+
15
+ export interface GitRepoInfo {
16
+ repoRoot: string;
17
+ branch?: string;
18
+ statusLines: string[];
19
+ }
20
+
21
+ interface GitWorktreeInfo {
22
+ path: string;
23
+ branch?: string;
24
+ }
25
+
26
+ export async function execGit(pi: ExtensionAPI, cwd: string, args: string[]): Promise<GitExecResult> {
27
+ const result = await pi.exec("git", args, { timeout: GIT_TIMEOUT_MS, cwd });
28
+ if (result.killed) {
29
+ return {
30
+ ok: false,
31
+ stdout: result.stdout,
32
+ stderr: result.stderr,
33
+ error: "git command timed out",
34
+ };
35
+ }
36
+ if (result.code !== 0) {
37
+ return {
38
+ ok: false,
39
+ stdout: result.stdout,
40
+ stderr: result.stderr,
41
+ error: result.stderr.trim() || result.stdout.trim() || `git exited with code ${result.code}`,
42
+ };
43
+ }
44
+ return {
45
+ ok: true,
46
+ stdout: result.stdout,
47
+ stderr: result.stderr,
48
+ };
49
+ }
50
+
51
+ export async function getGitRepoInfo(pi: ExtensionAPI, cwd: string, maxStatusLines: number = DEFAULT_MAX_STATUS_LINES): Promise<GitRepoInfo | undefined> {
52
+ const rootResult = await execGit(pi, cwd, ["rev-parse", "--show-toplevel"]);
53
+ if (!rootResult.ok) return undefined;
54
+ const repoRoot = rootResult.stdout.trim();
55
+ if (!repoRoot) return undefined;
56
+
57
+ const branchResult = await execGit(pi, cwd, ["branch", "--show-current"]);
58
+ const statusResult = await execGit(pi, cwd, ["status", "--short", "--untracked-files=all"]);
59
+
60
+ return {
61
+ repoRoot,
62
+ branch: branchResult.ok ? branchResult.stdout.trim() || undefined : undefined,
63
+ statusLines: statusResult.ok
64
+ ? statusResult.stdout
65
+ .split("\n")
66
+ .map((line) => line.trimEnd())
67
+ .filter((line) => line.trim().length > 0)
68
+ .slice(0, maxStatusLines)
69
+ : [],
70
+ };
71
+ }
72
+
73
+ function parseWorktreeList(text: string): GitWorktreeInfo[] {
74
+ const worktrees: GitWorktreeInfo[] = [];
75
+ let current: GitWorktreeInfo | undefined;
76
+
77
+ for (const rawLine of text.split("\n")) {
78
+ const line = rawLine.trimEnd();
79
+ if (line.startsWith("worktree ")) {
80
+ if (current) worktrees.push(current);
81
+ current = { path: line.slice("worktree ".length).trim() };
82
+ continue;
83
+ }
84
+ if (!current) continue;
85
+ if (line.startsWith("branch ")) {
86
+ current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "") || undefined;
87
+ continue;
88
+ }
89
+ if (line.length === 0) {
90
+ worktrees.push(current);
91
+ current = undefined;
92
+ }
93
+ }
94
+
95
+ if (current) worktrees.push(current);
96
+ return worktrees;
97
+ }
98
+
99
+ function slugifyBranchName(branch: string): string {
100
+ return branch
101
+ .trim()
102
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
103
+ .replace(/-+/g, "-")
104
+ .replace(/^-|-$/g, "") || "worktree";
105
+ }
106
+
107
+ export async function branchExists(pi: ExtensionAPI, repoRoot: string, branch: string): Promise<boolean> {
108
+ const result = await pi.exec("git", ["show-ref", "--verify", "--", `refs/heads/${branch}`], { timeout: GIT_TIMEOUT_MS, cwd: repoRoot });
109
+ return !result.killed && result.code === 0;
110
+ }
111
+
112
+ async function resolveWorktreePath(
113
+ pi: ExtensionAPI,
114
+ repoRoot: string,
115
+ branch: string,
116
+ ): Promise<{ ok: true; path: string; reused: boolean } | { ok: false; error: string }> {
117
+ const listResult = await execGit(pi, repoRoot, ["worktree", "list", "--porcelain"]);
118
+ if (!listResult.ok) {
119
+ return { ok: false, error: listResult.error || "Failed to list git worktrees" };
120
+ }
121
+
122
+ const existing = parseWorktreeList(listResult.stdout).find((worktree) => worktree.branch === branch);
123
+ if (existing) {
124
+ return { ok: true, path: existing.path, reused: true };
125
+ }
126
+
127
+ const worktreeRoot = join(dirname(repoRoot), `${basename(repoRoot)}-worktrees`);
128
+ const targetPath = join(worktreeRoot, slugifyBranchName(branch));
129
+ if (existsSync(targetPath)) {
130
+ return { ok: false, error: `Worktree path already exists and cannot be reused: ${targetPath}` };
131
+ }
132
+
133
+ mkdirSync(worktreeRoot, { recursive: true });
134
+ return { ok: true, path: targetPath, reused: false };
135
+ }
136
+
137
+ export async function ensureExistingBranchWorktree(
138
+ pi: ExtensionAPI,
139
+ repoRoot: string,
140
+ branch: string,
141
+ ): Promise<{ ok: true; path: string; reused: boolean } | { ok: false; error: string }> {
142
+ if (!(await branchExists(pi, repoRoot, branch))) {
143
+ return { ok: false, error: `Local branch does not exist: ${branch}` };
144
+ }
145
+
146
+ const pathResult = await resolveWorktreePath(pi, repoRoot, branch);
147
+ if (!pathResult.ok) return pathResult;
148
+ if (pathResult.reused) return pathResult;
149
+
150
+ const addResult = await execGit(pi, repoRoot, ["worktree", "add", pathResult.path, branch]);
151
+ if (!addResult.ok) {
152
+ return { ok: false, error: addResult.error || `Failed to create worktree for branch ${branch}` };
153
+ }
154
+
155
+ return pathResult;
156
+ }
157
+
158
+ export async function ensureCreatedBranchWorktree(
159
+ pi: ExtensionAPI,
160
+ repoRoot: string,
161
+ branch: string,
162
+ fromRef?: string,
163
+ ): Promise<{ ok: true; path: string } | { ok: false; error: string }> {
164
+ if (await branchExists(pi, repoRoot, branch)) {
165
+ return { ok: false, error: `Branch already exists: ${branch}` };
166
+ }
167
+
168
+ const pathResult = await resolveWorktreePath(pi, repoRoot, branch);
169
+ if (!pathResult.ok) return pathResult;
170
+ if (pathResult.reused) {
171
+ return { ok: true, path: pathResult.path };
172
+ }
173
+
174
+ const args = ["worktree", "add", "-b", branch, pathResult.path];
175
+ if (fromRef?.trim()) {
176
+ args.push(fromRef.trim());
177
+ }
178
+ const addResult = await execGit(pi, repoRoot, args);
179
+ if (!addResult.ok) {
180
+ return { ok: false, error: addResult.error || `Failed to create branch worktree ${branch}` };
181
+ }
182
+
183
+ return { ok: true, path: pathResult.path };
184
+ }
@@ -0,0 +1,16 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import zvNotifyExtension from "./zv-notify.ts";
3
+ import zvSplitExtension from "./zv-split.ts";
4
+ import zvZoxideExtension from "./zv-zoxide.ts";
5
+ import zvReviewExtension from "./zv-review.ts";
6
+ import zvContinueExtension from "./zv-continue.ts";
7
+ import zvOpenExtension from "./zv-open.ts";
8
+
9
+ export default function piZellijExtensionBundle(pi: ExtensionAPI) {
10
+ zvNotifyExtension(pi);
11
+ zvSplitExtension(pi);
12
+ zvZoxideExtension(pi);
13
+ zvReviewExtension(pi);
14
+ zvContinueExtension(pi);
15
+ zvOpenExtension(pi);
16
+ }
@@ -0,0 +1 @@
1
+ Continue the current task from this new pane.{{FOCUS_NOTE_SENTENCE}} Use the inherited session history and the handoff summary already present in this session. Start with the highest-priority next step.
@@ -0,0 +1 @@
1
+ Continue the current task in this git worktree for branch {{TARGET_BRANCH}}.{{FOCUS_NOTE_SENTENCE}} Use the handoff summary already present in this session. First confirm the branch and repository state in this worktree, then proceed with the highest-priority next step.