pi-zellij 0.4.0 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -13,6 +13,20 @@
13
13
  - Added `/zz` and `/zzh` to open a new pane from a zoxide match or direct directory path and start Pi there.
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
+ - 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.
19
+
20
+ ### Changed
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.
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.
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.
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.
28
+ - 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.
29
+ - 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.
16
30
 
17
31
  ### Removed
18
32
 
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
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, 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.
10
14
 
11
15
  ## Usage
12
16
 
@@ -34,6 +38,13 @@ If pi is already running, use:
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
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 |
47
+
37
48
  ## Feature overview
38
49
 
39
50
  ### Pane and tab workflows
@@ -49,6 +60,8 @@ If pi is already running, use:
49
60
 
50
61
  - `pi-zellij.commands` in `settings.json`
51
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
52
65
 
53
66
  ### Review and handoff workflows
54
67
 
@@ -66,6 +79,7 @@ If pi is already running, use:
66
79
  Extensions:
67
80
  - `zv-split`
68
81
  - `zv-open`
82
+ - `zv-highlight`
69
83
  - `zv-zoxide`
70
84
  - `zv-review`
71
85
  - `zv-continue`
@@ -118,6 +132,29 @@ Examples:
118
132
 
119
133
  Commands are executed via `sh -lc` in the current project directory.
120
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
+
121
158
  ### Configured floating commands
122
159
 
123
160
  You can register your own floating app shortcuts in Pi's main settings file under `pi-zellij.commands`.
@@ -184,6 +221,53 @@ Configured command names cannot reuse built-in Pi commands such as `/settings`,
184
221
 
185
222
  If the same command exists in both global and project settings, the project setting wins. After changing settings, run `/reload` in Pi.
186
223
 
224
+ ### Pane highlight on completion
225
+
226
+ You can optionally tint the current zellij pane when Pi finishes a turn and is waiting for input.
227
+
228
+ Supported locations:
229
+ - `~/.pi/agent/settings.json` for global settings
230
+ - `.pi/settings.json` for project-local settings
231
+
232
+ 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.
233
+
234
+ Minimal form:
235
+
236
+ ```json
237
+ {
238
+ "pi-zellij": {
239
+ "paneHighlight": true
240
+ }
241
+ }
242
+ ```
243
+
244
+ That enables a default done-state background tint. The feature is zellij-only and does nothing outside an active zellij session.
245
+
246
+ Object form:
247
+
248
+ ```json
249
+ {
250
+ "pi-zellij": {
251
+ "paneHighlight": {
252
+ "enabled": true,
253
+ "doneBg": "#17352a",
254
+ "doneFg": "#e7fff0",
255
+ "workingBg": "#2f2415"
256
+ }
257
+ }
258
+ }
259
+ ```
260
+
261
+ Supported keys:
262
+ - `enabled`
263
+ - set to `false` to disable the feature
264
+ - `doneBg`, `doneFg`
265
+ - pane colors to apply after `agent_end` when the pane is not currently focused
266
+ - `workingBg`, `workingFg`
267
+ - 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
268
+
269
+ 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.
270
+
187
271
  ### Zoxide jump commands
188
272
 
189
273
  - `/zz <query>`
@@ -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,9 +1,10 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import zvSplitExtension from "./zv-split.ts";
3
3
  import zvZoxideExtension from "./zv-zoxide.ts";
4
4
  import zvReviewExtension from "./zv-review.ts";
5
5
  import zvContinueExtension from "./zv-continue.ts";
6
6
  import zvOpenExtension from "./zv-open.ts";
7
+ import zvHighlightExtension from "./zv-highlight.ts";
7
8
 
8
9
  export default function piZellijExtensionBundle(pi: ExtensionAPI) {
9
10
  zvSplitExtension(pi);
@@ -11,4 +12,5 @@ export default function piZellijExtensionBundle(pi: ExtensionAPI) {
11
12
  zvReviewExtension(pi);
12
13
  zvContinueExtension(pi);
13
14
  zvOpenExtension(pi);
15
+ zvHighlightExtension(pi);
14
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,19 @@
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 PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
7
+ export type PaneOpenResult = { ok: true; paneId?: string } | { ok: false; error: string };
8
+ export type TabOpenResult = { ok: true; tabId?: string } | { ok: false; error: string };
9
+
10
+ export interface PiCommandOptions {
11
+ sessionFile?: string;
12
+ prompt?: string;
13
+ provider?: string;
14
+ model?: string;
15
+ thinking?: PiThinkingLevel;
16
+ }
6
17
 
7
18
  interface ZellijExecResult {
8
19
  ok: boolean;
@@ -11,22 +22,70 @@ interface ZellijExecResult {
11
22
  error?: string;
12
23
  }
13
24
 
14
- function isInsideZellijSession(): boolean {
25
+ export function isInsideZellijSession(): boolean {
15
26
  return Boolean(process.env.ZELLIJ || process.env.ZELLIJ_SESSION_NAME || process.env.ZELLIJ_PANE_ID);
16
27
  }
17
28
 
29
+ function getLastNonEmptyLine(value: string): string | undefined {
30
+ const lines = value
31
+ .split(/\r?\n/)
32
+ .map((line) => line.trim())
33
+ .filter((line) => line.length > 0);
34
+ return lines.length > 0 ? lines[lines.length - 1] : undefined;
35
+ }
36
+
37
+ function normalizePaneId(value: string): string | undefined {
38
+ const trimmed = value.trim();
39
+ if (!trimmed) {
40
+ return undefined;
41
+ }
42
+ if (/^(?:terminal|plugin)_\d+$/.test(trimmed)) {
43
+ return trimmed;
44
+ }
45
+ if (/^\d+$/.test(trimmed)) {
46
+ return `terminal_${trimmed}`;
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ function getCreatedPaneId(stdout: string): string | undefined {
52
+ return normalizePaneId(getLastNonEmptyLine(stdout) ?? "");
53
+ }
54
+
55
+ function getCreatedTabId(stdout: string): string | undefined {
56
+ const candidate = getLastNonEmptyLine(stdout);
57
+ return candidate && /^\d+$/.test(candidate) ? candidate : undefined;
58
+ }
59
+
60
+ export function formatPaneSuccessMessage(message: string, paneId?: string): string {
61
+ return paneId ? `${message} (${paneId})` : message;
62
+ }
63
+
64
+ export function formatTabSuccessMessage(message: string, tabId?: string): string {
65
+ return tabId ? `${message} (tab ${tabId})` : message;
66
+ }
67
+
18
68
  export function shellEscape(value: string): string {
19
69
  return `'${value.replace(/'/g, `'\\''`)}'`;
20
70
  }
21
71
 
22
- export function buildPiCommand(cwd: string, options?: { sessionFile?: string; prompt?: string }): string {
72
+ export function buildPiCommand(cwd: string, options?: PiCommandOptions): string {
23
73
  const commandParts = ["cd", shellEscape(cwd), "&&", "exec", "pi"];
24
74
  if (options?.sessionFile) {
25
75
  commandParts.push("--session", shellEscape(options.sessionFile));
26
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
+ }
27
86
  const prompt = options?.prompt?.trim();
28
87
  if (prompt) {
29
- commandParts.push(shellEscape(prompt));
88
+ commandParts.push("--", shellEscape(prompt));
30
89
  }
31
90
  return commandParts.join(" ");
32
91
  }
@@ -60,28 +119,110 @@ async function execZellij(pi: ExtensionAPI, args: string[]): Promise<ZellijExecR
60
119
  };
61
120
  }
62
121
 
122
+ function getCurrentPaneTargetArgs(): string[] {
123
+ return process.env.ZELLIJ_PANE_ID ? ["-p", process.env.ZELLIJ_PANE_ID] : [];
124
+ }
125
+
126
+ export async function resetCurrentPaneColor(pi: ExtensionAPI): Promise<{ ok: true } | { ok: false; error: string }> {
127
+ if (!isInsideZellijSession()) {
128
+ return { ok: false, error: "This command must be run from inside an active zellij session" };
129
+ }
130
+
131
+ const result = await execZellij(pi, ["action", "set-pane-color", ...getCurrentPaneTargetArgs(), "--reset"]);
132
+ if (!result.ok) {
133
+ return { ok: false, error: result.error || "Failed to reset zellij pane color" };
134
+ }
135
+
136
+ return { ok: true };
137
+ }
138
+
139
+ export async function setCurrentPaneColor(
140
+ pi: ExtensionAPI,
141
+ options: { bg?: string; fg?: string },
142
+ ): Promise<{ ok: true } | { ok: false; error: string }> {
143
+ if (!isInsideZellijSession()) {
144
+ return { ok: false, error: "This command must be run from inside an active zellij session" };
145
+ }
146
+
147
+ const paneTargetArgs = getCurrentPaneTargetArgs();
148
+ const args = ["action", "set-pane-color", ...paneTargetArgs];
149
+ if (options.bg) {
150
+ args.push("--bg", options.bg);
151
+ }
152
+ if (options.fg) {
153
+ args.push("--fg", options.fg);
154
+ }
155
+ if (args.length === 2 + paneTargetArgs.length) {
156
+ return resetCurrentPaneColor(pi);
157
+ }
158
+
159
+ const result = await execZellij(pi, args);
160
+ if (!result.ok) {
161
+ return { ok: false, error: result.error || "Failed to set zellij pane color" };
162
+ }
163
+
164
+ return { ok: true };
165
+ }
166
+
167
+ function buildNewTabArgs(cwd: string, options?: { name?: string }): string[] {
168
+ const args = ["action", "new-tab", "--cwd", cwd];
169
+ if (options?.name) {
170
+ args.push("--name", options.name);
171
+ }
172
+ return args;
173
+ }
174
+
175
+ async function openCommandInNewTabLegacy(
176
+ pi: ExtensionAPI,
177
+ cwd: string,
178
+ command: string,
179
+ options?: { name?: string },
180
+ ): Promise<TabOpenResult> {
181
+ const newTabResult = await execZellij(pi, buildNewTabArgs(cwd, options));
182
+ if (!newTabResult.ok) {
183
+ return { ok: false, error: newTabResult.error || "Failed to open a new zellij tab" };
184
+ }
185
+
186
+ const writeCommandResult = await execZellij(pi, ["action", "write-chars", command]);
187
+ if (!writeCommandResult.ok) {
188
+ return { ok: false, error: writeCommandResult.error || "Failed to write command to new zellij tab" };
189
+ }
190
+
191
+ const submitCommandResult = await execZellij(pi, ["action", "write", "10"]);
192
+ if (!submitCommandResult.ok) {
193
+ return { ok: false, error: submitCommandResult.error || "Failed to start command in new zellij tab" };
194
+ }
195
+
196
+ return { ok: true, tabId: getCreatedTabId(newTabResult.stdout) };
197
+ }
198
+
63
199
  export async function openCommandInNewSplit(
64
200
  pi: ExtensionAPI,
65
201
  direction: SplitDirection,
66
202
  command: string,
67
- ): Promise<{ ok: true } | { ok: false; error: string }> {
203
+ options?: { name?: string },
204
+ ): Promise<PaneOpenResult> {
68
205
  if (!isInsideZellijSession()) {
69
206
  return { ok: false, error: "This command must be run from inside an active zellij session" };
70
207
  }
71
208
 
72
- const result = await execZellij(pi, ["run", "--direction", direction, "--", "sh", "-lc", command]);
209
+ const args = ["run", "--direction", direction];
210
+ if (options?.name) args.push("--name", options.name);
211
+ args.push("--", "sh", "-lc", command);
212
+
213
+ const result = await execZellij(pi, args);
73
214
  if (!result.ok) {
74
215
  return { ok: false, error: result.error || "Failed to open a new zellij pane" };
75
216
  }
76
217
 
77
- return { ok: true };
218
+ return { ok: true, paneId: getCreatedPaneId(result.stdout) };
78
219
  }
79
220
 
80
221
  export async function openCommandInFloatingPane(
81
222
  pi: ExtensionAPI,
82
223
  command: string,
83
224
  options?: { name?: string; width?: string; height?: string; x?: string; y?: string },
84
- ): Promise<{ ok: true } | { ok: false; error: string }> {
225
+ ): Promise<PaneOpenResult> {
85
226
  if (!isInsideZellijSession()) {
86
227
  return { ok: false, error: "This command must be run from inside an active zellij session" };
87
228
  }
@@ -99,7 +240,7 @@ export async function openCommandInFloatingPane(
99
240
  return { ok: false, error: result.error || "Failed to open a new floating zellij pane" };
100
241
  }
101
242
 
102
- return { ok: true };
243
+ return { ok: true, paneId: getCreatedPaneId(result.stdout) };
103
244
  }
104
245
 
105
246
  export async function openCommandInNewTab(
@@ -107,30 +248,15 @@ export async function openCommandInNewTab(
107
248
  cwd: string,
108
249
  command: string,
109
250
  options?: { name?: string },
110
- ): Promise<{ ok: true } | { ok: false; error: string }> {
251
+ ): Promise<TabOpenResult> {
111
252
  if (!isInsideZellijSession()) {
112
253
  return { ok: false, error: "This command must be run from inside an active zellij session" };
113
254
  }
114
255
 
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" };
128
- }
129
-
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" };
256
+ const newTabResult = await execZellij(pi, [...buildNewTabArgs(cwd, options), "--", "sh", "-lc", command]);
257
+ if (newTabResult.ok) {
258
+ return { ok: true, tabId: getCreatedTabId(newTabResult.stdout) };
133
259
  }
134
260
 
135
- return { ok: true };
261
+ return openCommandInNewTabLegacy(pi, cwd, command, options);
136
262
  }