pi-zellij 0.3.1 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,11 +5,6 @@
5
5
  ### Added
6
6
 
7
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
8
  - 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
9
  - Added `/zo` and `/zoh` to open a new pane and run any shell command there.
15
10
  - 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.
@@ -18,3 +13,16 @@
18
13
  - Added `/zz` and `/zzh` to open a new pane from a zoxide match or direct directory path and start Pi there.
19
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.
20
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
+ - Added opt-in `paneHighlight` settings so Pi can tint the current zellij pane when an agent turn completes, with optional working-state colors.
17
+
18
+ ### Changed
19
+
20
+ - 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
+ - `/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
+ - 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.
23
+ - Pane focus polling no longer writes transient zellij query timeout warnings into the Pi editor; refocus-based clearing is skipped if focus state cannot be queried reliably.
24
+ - Done-state pane tint is now only applied while the Pi pane is unfocused, so the editor is reset immediately instead of staying green while typing in the active pane.
25
+
26
+ ### Removed
27
+
28
+ - 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
@@ -1,12 +1,16 @@
1
1
  # pi-zellij
2
2
 
3
- Pi package with zellij-powered terminal integrations for [Pi](https://pi.dev).
3
+ Pi package with [zellij](https://zellij.dev)-powered terminal integrations for [Pi](https://pi.dev). Make your workflow agent driven.
4
+
5
+ ## Workflow
6
+
7
+ ![pi-zellij workflow](assets/workflow.gif)
4
8
 
5
9
  ## Why
6
10
 
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.
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.
8
12
 
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.
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.
10
14
 
11
15
  ## Usage
12
16
 
@@ -33,9 +37,13 @@ If pi is already running, use:
33
37
  - `zellij` must be installed
34
38
  - pane, tab, and floating commands must be run from inside an active zellij session
35
39
  - `zoxide` is required for the zoxide commands
36
- - notifications use:
37
- - `osascript` on macOS
38
- - `notify-send` on Linux
40
+
41
+ ### Recommended zellij version
42
+
43
+ | zellij version | status | notes |
44
+ | --- | --- | --- |
45
+ | `0.44.0+` | recommended | `pi-zellij` can show created pane/tab IDs in success notifications, and `/zt` can launch its initial command directly via `zellij action new-tab -- <command>` |
46
+ | older versions | supported with fallback | commands still work, but zellij may not expose created IDs and `/zt` may fall back to the older typed-input startup path |
39
47
 
40
48
  ## Feature overview
41
49
 
@@ -52,6 +60,8 @@ If pi is already running, use:
52
60
 
53
61
  - `pi-zellij.commands` in `settings.json`
54
62
  - registers floating app shortcuts such as `/zh` for `hx` or `/zg` for `lazygit`
63
+ - `pi-zellij.paneHighlight` in `settings.json`
64
+ - optionally tints the current zellij pane when Pi finishes a turn and is waiting for input
55
65
 
56
66
  ### Review and handoff workflows
57
67
 
@@ -64,17 +74,12 @@ If pi is already running, use:
64
74
  - `/skill:code-review`
65
75
  - loads the bundled structured review skill for files, directories, diffs, and PRs
66
76
 
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
77
  ## Bundled extensions and resources
73
78
 
74
79
  Extensions:
75
- - `zv-notify`
76
80
  - `zv-split`
77
81
  - `zv-open`
82
+ - `zv-highlight`
78
83
  - `zv-zoxide`
79
84
  - `zv-review`
80
85
  - `zv-continue`
@@ -159,7 +164,9 @@ Examples:
159
164
  /zg
160
165
  ```
161
166
 
162
- For commands that should accept extra arguments, use the object form:
167
+ For commands that should accept extra arguments, use the object form.
168
+
169
+ Helix and lazygit example:
163
170
 
164
171
  ```json
165
172
  {
@@ -169,22 +176,75 @@ For commands that should accept extra arguments, use the object form:
169
176
  "run": "hx",
170
177
  "acceptArgs": true,
171
178
  "description": "Open Helix in a floating pane"
179
+ },
180
+ "zg": {
181
+ "run": "lazygit",
182
+ "description": "Open lazygit in a floating pane"
172
183
  }
173
184
  }
174
185
  }
175
186
  }
176
187
  ```
177
188
 
178
- Then you can pass arguments through to the configured command:
189
+ Then you can use:
179
190
 
180
191
  ```text
192
+ /zh
181
193
  /zh src/auth.ts
194
+ /zg
182
195
  ```
183
196
 
184
197
  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
198
 
186
199
  If the same command exists in both global and project settings, the project setting wins. After changing settings, run `/reload` in Pi.
187
200
 
201
+ ### Pane highlight on completion
202
+
203
+ You can optionally tint the current zellij pane when Pi finishes a turn and is waiting for input.
204
+
205
+ Supported locations:
206
+ - `~/.pi/agent/settings.json` for global settings
207
+ - `.pi/settings.json` for project-local settings
208
+
209
+ During the rename from `pi-zv` to `pi-zellij`, legacy `pi-zv.paneHighlight` is still accepted for compatibility. If both keys exist, `pi-zellij.paneHighlight` wins.
210
+
211
+ Minimal form:
212
+
213
+ ```json
214
+ {
215
+ "pi-zellij": {
216
+ "paneHighlight": true
217
+ }
218
+ }
219
+ ```
220
+
221
+ That enables a default done-state background tint. The feature is zellij-only and does nothing outside an active zellij session.
222
+
223
+ Object form:
224
+
225
+ ```json
226
+ {
227
+ "pi-zellij": {
228
+ "paneHighlight": {
229
+ "enabled": true,
230
+ "doneBg": "#17352a",
231
+ "doneFg": "#e7fff0",
232
+ "workingBg": "#2f2415"
233
+ }
234
+ }
235
+ }
236
+ ```
237
+
238
+ Supported keys:
239
+ - `enabled`
240
+ - set to `false` to disable the feature
241
+ - `doneBg`, `doneFg`
242
+ - pane colors to apply after `agent_end` when the pane is not currently focused
243
+ - `workingBg`, `workingFg`
244
+ - optional pane colors to apply while Pi is working; if omitted, `pi-zellij` resets the pane to its default colors when the next input is submitted or when the pane is focused again after being elsewhere
245
+
246
+ When enabled, `pi-zellij` resets the pane color on session start, session switch, the next submitted input, pane refocus when zellij focus state is available, and session shutdown so completed-turn highlights do not linger across sessions. If the pane is already focused when a turn completes, the done-state tint is skipped so the Pi editor does not stay tinted while you type. Aborted runs do not apply the done-state tint. After changing these settings, run `/reload` in Pi.
247
+
188
248
  ### Zoxide jump commands
189
249
 
190
250
  - `/zz <query>`
@@ -273,24 +333,3 @@ Examples:
273
333
 
274
334
  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
335
 
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`)
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { existsSync, mkdirSync } from "node:fs";
4
4
 
@@ -1,16 +1,16 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import zvNotifyExtension from "./zv-notify.ts";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
2
  import zvSplitExtension from "./zv-split.ts";
4
3
  import zvZoxideExtension from "./zv-zoxide.ts";
5
4
  import zvReviewExtension from "./zv-review.ts";
6
5
  import zvContinueExtension from "./zv-continue.ts";
7
6
  import zvOpenExtension from "./zv-open.ts";
7
+ import zvHighlightExtension from "./zv-highlight.ts";
8
8
 
9
9
  export default function piZellijExtensionBundle(pi: ExtensionAPI) {
10
- zvNotifyExtension(pi);
11
10
  zvSplitExtension(pi);
12
11
  zvZoxideExtension(pi);
13
12
  zvReviewExtension(pi);
14
13
  zvContinueExtension(pi);
15
14
  zvOpenExtension(pi);
15
+ zvHighlightExtension(pi);
16
16
  }
@@ -1,10 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { SessionManager, type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
4
+ import { SessionManager, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
5
5
  import {
6
6
  buildPiCommand,
7
+ formatPaneSuccessMessage,
7
8
  openCommandInNewSplit,
9
+ type PaneOpenResult,
8
10
  type SplitDirection,
9
11
  } from "./zv-core.ts";
10
12
  import {
@@ -388,7 +390,7 @@ async function openContinueSplit(
388
390
  ctx: ExtensionCommandContext,
389
391
  direction: SplitDirection,
390
392
  request: ContinueRequest,
391
- ): Promise<{ ok: true } | { ok: false; error: string }> {
393
+ ): Promise<PaneOpenResult> {
392
394
  const handoffTarget = await resolveHandoffTarget(pi, ctx, request);
393
395
  if (!handoffTarget.ok) {
394
396
  return handoffTarget;
@@ -422,7 +424,7 @@ function registerContinueCommand(
422
424
 
423
425
  const result = await openContinueSplit(pi, ctx, direction, parsed.request);
424
426
  if (result.ok) {
425
- ctx.ui.notify(successMessage, "info");
427
+ ctx.ui.notify(formatPaneSuccessMessage(successMessage, result.paneId), "info");
426
428
  } else {
427
429
  ctx.ui.notify(`continuation split failed: ${result.error}`, "error");
428
430
  }
@@ -1,8 +1,10 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  const ZELLIJ_TIMEOUT_MS = 5000;
4
4
 
5
5
  export type SplitDirection = "right" | "down";
6
+ export type PaneOpenResult = { ok: true; paneId?: string } | { ok: false; error: string };
7
+ export type TabOpenResult = { ok: true; tabId?: string } | { ok: false; error: string };
6
8
 
7
9
  interface ZellijExecResult {
8
10
  ok: boolean;
@@ -11,10 +13,49 @@ interface ZellijExecResult {
11
13
  error?: string;
12
14
  }
13
15
 
14
- function isInsideZellijSession(): boolean {
16
+ export function isInsideZellijSession(): boolean {
15
17
  return Boolean(process.env.ZELLIJ || process.env.ZELLIJ_SESSION_NAME || process.env.ZELLIJ_PANE_ID);
16
18
  }
17
19
 
20
+ function getLastNonEmptyLine(value: string): string | undefined {
21
+ const lines = value
22
+ .split(/\r?\n/)
23
+ .map((line) => line.trim())
24
+ .filter((line) => line.length > 0);
25
+ return lines.length > 0 ? lines[lines.length - 1] : undefined;
26
+ }
27
+
28
+ function normalizePaneId(value: string): string | undefined {
29
+ const trimmed = value.trim();
30
+ if (!trimmed) {
31
+ return undefined;
32
+ }
33
+ if (/^(?:terminal|plugin)_\d+$/.test(trimmed)) {
34
+ return trimmed;
35
+ }
36
+ if (/^\d+$/.test(trimmed)) {
37
+ return `terminal_${trimmed}`;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ function getCreatedPaneId(stdout: string): string | undefined {
43
+ return normalizePaneId(getLastNonEmptyLine(stdout) ?? "");
44
+ }
45
+
46
+ function getCreatedTabId(stdout: string): string | undefined {
47
+ const candidate = getLastNonEmptyLine(stdout);
48
+ return candidate && /^\d+$/.test(candidate) ? candidate : undefined;
49
+ }
50
+
51
+ export function formatPaneSuccessMessage(message: string, paneId?: string): string {
52
+ return paneId ? `${message} (${paneId})` : message;
53
+ }
54
+
55
+ export function formatTabSuccessMessage(message: string, tabId?: string): string {
56
+ return tabId ? `${message} (tab ${tabId})` : message;
57
+ }
58
+
18
59
  export function shellEscape(value: string): string {
19
60
  return `'${value.replace(/'/g, `'\\''`)}'`;
20
61
  }
@@ -60,11 +101,88 @@ async function execZellij(pi: ExtensionAPI, args: string[]): Promise<ZellijExecR
60
101
  };
61
102
  }
62
103
 
104
+ function getCurrentPaneTargetArgs(): string[] {
105
+ return process.env.ZELLIJ_PANE_ID ? ["-p", process.env.ZELLIJ_PANE_ID] : [];
106
+ }
107
+
108
+ export async function resetCurrentPaneColor(pi: ExtensionAPI): Promise<{ ok: true } | { ok: false; error: string }> {
109
+ if (!isInsideZellijSession()) {
110
+ return { ok: false, error: "This command must be run from inside an active zellij session" };
111
+ }
112
+
113
+ const result = await execZellij(pi, ["action", "set-pane-color", ...getCurrentPaneTargetArgs(), "--reset"]);
114
+ if (!result.ok) {
115
+ return { ok: false, error: result.error || "Failed to reset zellij pane color" };
116
+ }
117
+
118
+ return { ok: true };
119
+ }
120
+
121
+ export async function setCurrentPaneColor(
122
+ pi: ExtensionAPI,
123
+ options: { bg?: string; fg?: string },
124
+ ): Promise<{ ok: true } | { ok: false; error: string }> {
125
+ if (!isInsideZellijSession()) {
126
+ return { ok: false, error: "This command must be run from inside an active zellij session" };
127
+ }
128
+
129
+ const paneTargetArgs = getCurrentPaneTargetArgs();
130
+ const args = ["action", "set-pane-color", ...paneTargetArgs];
131
+ if (options.bg) {
132
+ args.push("--bg", options.bg);
133
+ }
134
+ if (options.fg) {
135
+ args.push("--fg", options.fg);
136
+ }
137
+ if (args.length === 2 + paneTargetArgs.length) {
138
+ return resetCurrentPaneColor(pi);
139
+ }
140
+
141
+ const result = await execZellij(pi, args);
142
+ if (!result.ok) {
143
+ return { ok: false, error: result.error || "Failed to set zellij pane color" };
144
+ }
145
+
146
+ return { ok: true };
147
+ }
148
+
149
+ function buildNewTabArgs(cwd: string, options?: { name?: string }): string[] {
150
+ const args = ["action", "new-tab", "--cwd", cwd];
151
+ if (options?.name) {
152
+ args.push("--name", options.name);
153
+ }
154
+ return args;
155
+ }
156
+
157
+ async function openCommandInNewTabLegacy(
158
+ pi: ExtensionAPI,
159
+ cwd: string,
160
+ command: string,
161
+ options?: { name?: string },
162
+ ): Promise<TabOpenResult> {
163
+ const newTabResult = await execZellij(pi, buildNewTabArgs(cwd, options));
164
+ if (!newTabResult.ok) {
165
+ return { ok: false, error: newTabResult.error || "Failed to open a new zellij tab" };
166
+ }
167
+
168
+ const writeCommandResult = await execZellij(pi, ["action", "write-chars", command]);
169
+ if (!writeCommandResult.ok) {
170
+ return { ok: false, error: writeCommandResult.error || "Failed to write command to new zellij tab" };
171
+ }
172
+
173
+ const submitCommandResult = await execZellij(pi, ["action", "write", "10"]);
174
+ if (!submitCommandResult.ok) {
175
+ return { ok: false, error: submitCommandResult.error || "Failed to start command in new zellij tab" };
176
+ }
177
+
178
+ return { ok: true, tabId: getCreatedTabId(newTabResult.stdout) };
179
+ }
180
+
63
181
  export async function openCommandInNewSplit(
64
182
  pi: ExtensionAPI,
65
183
  direction: SplitDirection,
66
184
  command: string,
67
- ): Promise<{ ok: true } | { ok: false; error: string }> {
185
+ ): Promise<PaneOpenResult> {
68
186
  if (!isInsideZellijSession()) {
69
187
  return { ok: false, error: "This command must be run from inside an active zellij session" };
70
188
  }
@@ -74,14 +192,14 @@ export async function openCommandInNewSplit(
74
192
  return { ok: false, error: result.error || "Failed to open a new zellij pane" };
75
193
  }
76
194
 
77
- return { ok: true };
195
+ return { ok: true, paneId: getCreatedPaneId(result.stdout) };
78
196
  }
79
197
 
80
198
  export async function openCommandInFloatingPane(
81
199
  pi: ExtensionAPI,
82
200
  command: string,
83
201
  options?: { name?: string; width?: string; height?: string; x?: string; y?: string },
84
- ): Promise<{ ok: true } | { ok: false; error: string }> {
202
+ ): Promise<PaneOpenResult> {
85
203
  if (!isInsideZellijSession()) {
86
204
  return { ok: false, error: "This command must be run from inside an active zellij session" };
87
205
  }
@@ -99,7 +217,7 @@ export async function openCommandInFloatingPane(
99
217
  return { ok: false, error: result.error || "Failed to open a new floating zellij pane" };
100
218
  }
101
219
 
102
- return { ok: true };
220
+ return { ok: true, paneId: getCreatedPaneId(result.stdout) };
103
221
  }
104
222
 
105
223
  export async function openCommandInNewTab(
@@ -107,30 +225,15 @@ export async function openCommandInNewTab(
107
225
  cwd: string,
108
226
  command: string,
109
227
  options?: { name?: string },
110
- ): Promise<{ ok: true } | { ok: false; error: string }> {
228
+ ): Promise<TabOpenResult> {
111
229
  if (!isInsideZellijSession()) {
112
230
  return { ok: false, error: "This command must be run from inside an active zellij session" };
113
231
  }
114
232
 
115
- const newTabArgs = ["action", "new-tab", "--cwd", cwd];
116
- if (options?.name) {
117
- newTabArgs.push("--name", options.name);
118
- }
119
-
120
- const newTabResult = await execZellij(pi, newTabArgs);
121
- if (!newTabResult.ok) {
122
- return { ok: false, error: newTabResult.error || "Failed to open a new zellij tab" };
123
- }
124
-
125
- const writeCommandResult = await execZellij(pi, ["action", "write-chars", command]);
126
- if (!writeCommandResult.ok) {
127
- return { ok: false, error: writeCommandResult.error || "Failed to write command to new zellij tab" };
233
+ const newTabResult = await execZellij(pi, [...buildNewTabArgs(cwd, options), "--", "sh", "-lc", command]);
234
+ if (newTabResult.ok) {
235
+ return { ok: true, tabId: getCreatedTabId(newTabResult.stdout) };
128
236
  }
129
237
 
130
- const submitCommandResult = await execZellij(pi, ["action", "write", "10"]);
131
- if (!submitCommandResult.ok) {
132
- return { ok: false, error: submitCommandResult.error || "Failed to start command in new zellij tab" };
133
- }
134
-
135
- return { ok: true };
238
+ return openCommandInNewTabLegacy(pi, cwd, command, options);
136
239
  }