pi-zellij 0.4.0 → 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 +9 -0
- package/README.md +63 -2
- package/extensions/git-core.ts +1 -1
- package/extensions/index.ts +3 -1
- package/extensions/zv-continue.ts +5 -3
- package/extensions/zv-core.ts +129 -26
- package/extensions/zv-highlight.ts +523 -0
- package/extensions/zv-open.ts +7 -5
- package/extensions/zv-review.ts +10 -4
- package/extensions/zv-split.ts +15 -6
- package/extensions/zv-zoxide.ts +10 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
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
|
+
|
|
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.
|
|
16
25
|
|
|
17
26
|
### Removed
|
|
18
27
|
|
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
|
+

|
|
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, 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`
|
|
@@ -184,6 +198,53 @@ Configured command names cannot reuse built-in Pi commands such as `/settings`,
|
|
|
184
198
|
|
|
185
199
|
If the same command exists in both global and project settings, the project setting wins. After changing settings, run `/reload` in Pi.
|
|
186
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
|
+
|
|
187
248
|
### Zoxide jump commands
|
|
188
249
|
|
|
189
250
|
- `/zz <query>`
|
package/extensions/git-core.ts
CHANGED
package/extensions/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
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 "@
|
|
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<
|
|
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
|
}
|
package/extensions/zv-core.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
|
116
|
-
if (
|
|
117
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { isInsideZellijSession, resetCurrentPaneColor, setCurrentPaneColor } from "./zv-core.ts";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DONE_BG = "#17352a";
|
|
8
|
+
const GLOBAL_SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
|
|
9
|
+
const SETTINGS_SECTION_NAMES = ["pi-zellij", "pi-zv"] as const;
|
|
10
|
+
const FOCUS_POLL_INTERVAL_MS = 1000;
|
|
11
|
+
const FOCUS_QUERY_TIMEOUT_MS = 2000;
|
|
12
|
+
const MAX_FOCUS_QUERY_FAILURES = 3;
|
|
13
|
+
|
|
14
|
+
interface PaneHighlightConfigInput {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
doneBg?: string;
|
|
17
|
+
doneFg?: string;
|
|
18
|
+
workingBg?: string;
|
|
19
|
+
workingFg?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PaneHighlightConfig {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
doneBg: string;
|
|
25
|
+
doneFg?: string;
|
|
26
|
+
workingBg?: string;
|
|
27
|
+
workingFg?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface MessageLike {
|
|
31
|
+
role?: unknown;
|
|
32
|
+
stopReason?: unknown;
|
|
33
|
+
content?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type JsonRecord = Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
function readJsonFile(path: string): Record<string, unknown> | undefined {
|
|
39
|
+
if (!existsSync(path)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
46
|
+
console.warn(`[pi-zellij] Ignoring non-object settings file: ${path}`);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return parsed as Record<string, unknown>;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
console.warn(`[pi-zellij] Failed to read settings from ${path}: ${message}`);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readPaneHighlightSetting(settingsPath: string): unknown {
|
|
58
|
+
const settings = readJsonFile(settingsPath);
|
|
59
|
+
for (const sectionName of SETTINGS_SECTION_NAMES) {
|
|
60
|
+
const section = settings?.[sectionName];
|
|
61
|
+
if (!section) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (typeof section !== "object" || Array.isArray(section)) {
|
|
65
|
+
console.warn(`[pi-zellij] Ignoring invalid \"${sectionName}\" settings in ${settingsPath}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const paneHighlight = (section as { paneHighlight?: unknown }).paneHighlight;
|
|
69
|
+
if (paneHighlight === undefined) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
return paneHighlight;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeOptionalString(value: unknown): string | undefined {
|
|
78
|
+
if (typeof value !== "string") {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizePaneHighlightConfig(value: unknown, settingsPath: string): Partial<PaneHighlightConfig> | undefined {
|
|
86
|
+
if (value === undefined) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "boolean") {
|
|
90
|
+
return { enabled: value };
|
|
91
|
+
}
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
93
|
+
console.warn(`[pi-zellij] Ignoring invalid paneHighlight settings in ${settingsPath}`);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const config = value as PaneHighlightConfigInput;
|
|
98
|
+
return {
|
|
99
|
+
enabled: config.enabled !== false,
|
|
100
|
+
doneBg: normalizeOptionalString(config.doneBg),
|
|
101
|
+
doneFg: normalizeOptionalString(config.doneFg),
|
|
102
|
+
workingBg: normalizeOptionalString(config.workingBg),
|
|
103
|
+
workingFg: normalizeOptionalString(config.workingFg),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function loadPaneHighlightConfig(cwd: string): PaneHighlightConfig {
|
|
108
|
+
let merged: Partial<PaneHighlightConfig> = { enabled: false };
|
|
109
|
+
|
|
110
|
+
for (const settingsPath of [GLOBAL_SETTINGS_PATH, join(cwd, ".pi", "settings.json")]) {
|
|
111
|
+
const normalized = normalizePaneHighlightConfig(readPaneHighlightSetting(settingsPath), settingsPath);
|
|
112
|
+
if (!normalized) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
merged = { ...merged, ...normalized };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
enabled: merged.enabled === true,
|
|
120
|
+
doneBg: merged.doneBg ?? DEFAULT_DONE_BG,
|
|
121
|
+
doneFg: merged.doneFg,
|
|
122
|
+
workingBg: merged.workingBg,
|
|
123
|
+
workingFg: merged.workingFg,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasWorkingColors(config: PaneHighlightConfig): boolean {
|
|
128
|
+
return Boolean(config.workingBg || config.workingFg);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
132
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizePaneId(value: unknown): string | undefined {
|
|
136
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
|
|
137
|
+
return `terminal_${value}`;
|
|
138
|
+
}
|
|
139
|
+
if (typeof value !== "string") {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const trimmed = value.trim();
|
|
144
|
+
if (!trimmed) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
if (/^(?:terminal|plugin)_\d+$/.test(trimmed)) {
|
|
148
|
+
return trimmed;
|
|
149
|
+
}
|
|
150
|
+
if (/^\d+$/.test(trimmed)) {
|
|
151
|
+
return `terminal_${trimmed}`;
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readBoolean(value: unknown): boolean | undefined {
|
|
157
|
+
if (typeof value === "boolean") {
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
if (typeof value === "string") {
|
|
161
|
+
if (value === "true") return true;
|
|
162
|
+
if (value === "false") return false;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeTabId(value: unknown): number | undefined {
|
|
168
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
172
|
+
return Number(value.trim());
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getPaneEntries(parsed: unknown): JsonRecord[] {
|
|
178
|
+
if (Array.isArray(parsed)) {
|
|
179
|
+
return parsed.filter(isRecord);
|
|
180
|
+
}
|
|
181
|
+
if (!isRecord(parsed)) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const key of ["panes", "items", "data", "results"]) {
|
|
186
|
+
const candidate = parsed[key];
|
|
187
|
+
if (Array.isArray(candidate)) {
|
|
188
|
+
return candidate.filter(isRecord);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return [parsed];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getPaneIdFromEntry(entry: JsonRecord): string | undefined {
|
|
196
|
+
for (const key of ["pane_id", "paneId", "id", "terminal_id", "terminalId"]) {
|
|
197
|
+
const normalized = normalizePaneId(entry[key]);
|
|
198
|
+
if (normalized) {
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const nestedPane = entry.pane;
|
|
204
|
+
if (isRecord(nestedPane)) {
|
|
205
|
+
return getPaneIdFromEntry(nestedPane);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getTabIdFromEntry(entry: JsonRecord): number | undefined {
|
|
212
|
+
for (const key of ["tab_id", "tabId"]) {
|
|
213
|
+
const normalized = normalizeTabId(entry[key]);
|
|
214
|
+
if (normalized !== undefined) {
|
|
215
|
+
return normalized;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const nestedTab = entry.tab;
|
|
220
|
+
if (isRecord(nestedTab)) {
|
|
221
|
+
return getTabIdFromEntry(nestedTab);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const nestedPane = entry.pane;
|
|
225
|
+
if (isRecord(nestedPane)) {
|
|
226
|
+
return getTabIdFromEntry(nestedPane);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getFocusedStateFromEntry(entry: JsonRecord): boolean | undefined {
|
|
233
|
+
for (const key of ["is_focused", "focused", "isFocused", "active", "is_active", "isActive"]) {
|
|
234
|
+
const normalized = readBoolean(entry[key]);
|
|
235
|
+
if (normalized !== undefined) {
|
|
236
|
+
return normalized;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const nestedState = entry.state;
|
|
241
|
+
if (isRecord(nestedState)) {
|
|
242
|
+
return getFocusedStateFromEntry(nestedState);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const nestedPane = entry.pane;
|
|
246
|
+
if (isRecord(nestedPane)) {
|
|
247
|
+
return getFocusedStateFromEntry(nestedPane);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function hasVisibleAssistantContent(message: MessageLike): boolean {
|
|
254
|
+
if (typeof message.content === "string") {
|
|
255
|
+
return message.content.trim().length > 0;
|
|
256
|
+
}
|
|
257
|
+
if (!Array.isArray(message.content)) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return message.content.length > 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function shouldHighlightAfterAgentEnd(messages: readonly unknown[]): boolean {
|
|
264
|
+
const assistantMessages = messages.filter((message): message is MessageLike => {
|
|
265
|
+
return isRecord(message) && message.role === "assistant";
|
|
266
|
+
});
|
|
267
|
+
if (assistantMessages.length === 0) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1]!;
|
|
272
|
+
if (lastAssistantMessage.stopReason === "aborted") {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
return hasVisibleAssistantContent(lastAssistantMessage);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export default function zvHighlightExtension(pi: ExtensionAPI) {
|
|
279
|
+
let config = loadPaneHighlightConfig(process.cwd());
|
|
280
|
+
let lastActionError: string | undefined;
|
|
281
|
+
let focusPollTimer: ReturnType<typeof setInterval> | undefined;
|
|
282
|
+
let focusPollInFlight = false;
|
|
283
|
+
let shouldClearOnRefocus = false;
|
|
284
|
+
let sawPaneBlurSinceDone = false;
|
|
285
|
+
let consecutiveFocusQueryFailures = 0;
|
|
286
|
+
|
|
287
|
+
function warnActionError(message: string): void {
|
|
288
|
+
if (lastActionError === message) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
lastActionError = message;
|
|
292
|
+
console.warn(`[pi-zellij] ${message}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function clearActionError(): void {
|
|
296
|
+
lastActionError = undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function stopFocusPolling(): void {
|
|
300
|
+
if (focusPollTimer) {
|
|
301
|
+
clearInterval(focusPollTimer);
|
|
302
|
+
focusPollTimer = undefined;
|
|
303
|
+
}
|
|
304
|
+
focusPollInFlight = false;
|
|
305
|
+
shouldClearOnRefocus = false;
|
|
306
|
+
sawPaneBlurSinceDone = false;
|
|
307
|
+
consecutiveFocusQueryFailures = 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function getActiveTabId(): Promise<number | undefined> {
|
|
311
|
+
const result = await pi.exec("zellij", ["action", "current-tab-info"], {
|
|
312
|
+
timeout: FOCUS_QUERY_TIMEOUT_MS,
|
|
313
|
+
});
|
|
314
|
+
if (result.killed || result.code !== 0) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const match = /^\s*id:\s*(\d+)\s*$/m.exec(result.stdout);
|
|
319
|
+
return match ? Number(match[1]) : undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function getCurrentPaneFocusedState(): Promise<boolean | undefined> {
|
|
323
|
+
if (!isInsideZellijSession()) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const currentPaneId = normalizePaneId(process.env.ZELLIJ_PANE_ID);
|
|
328
|
+
if (!currentPaneId) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const result = await pi.exec("zellij", ["action", "list-panes", "--json", "--state"], {
|
|
333
|
+
timeout: FOCUS_QUERY_TIMEOUT_MS,
|
|
334
|
+
});
|
|
335
|
+
if (result.killed || result.code !== 0) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const parsed = JSON.parse(result.stdout) as unknown;
|
|
341
|
+
for (const entry of getPaneEntries(parsed)) {
|
|
342
|
+
if (getPaneIdFromEntry(entry) !== currentPaneId) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const focused = getFocusedStateFromEntry(entry);
|
|
346
|
+
if (focused === undefined) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
if (!focused) {
|
|
350
|
+
clearActionError();
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const paneTabId = getTabIdFromEntry(entry);
|
|
355
|
+
const activeTabId = await getActiveTabId();
|
|
356
|
+
clearActionError();
|
|
357
|
+
if (paneTabId === undefined || activeTabId === undefined) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return paneTabId === activeTabId;
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
} catch {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function refreshConfig(cwd: string): Promise<void> {
|
|
369
|
+
const wasEnabled = config.enabled;
|
|
370
|
+
config = loadPaneHighlightConfig(cwd);
|
|
371
|
+
if ((!wasEnabled && !config.enabled) || !isInsideZellijSession()) {
|
|
372
|
+
stopFocusPolling();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
stopFocusPolling();
|
|
377
|
+
const result = await resetCurrentPaneColor(pi);
|
|
378
|
+
if (!result.ok) {
|
|
379
|
+
warnActionError(`pane highlight reset failed: ${result.error}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
clearActionError();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function resetPaneIfEnabled(): Promise<void> {
|
|
386
|
+
stopFocusPolling();
|
|
387
|
+
if (!config.enabled || !isInsideZellijSession()) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const result = await resetCurrentPaneColor(pi);
|
|
392
|
+
if (!result.ok) {
|
|
393
|
+
warnActionError(`pane highlight reset failed: ${result.error}`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
clearActionError();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function applyWorkingState(): Promise<void> {
|
|
400
|
+
stopFocusPolling();
|
|
401
|
+
if (!config.enabled || !isInsideZellijSession()) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!hasWorkingColors(config)) {
|
|
405
|
+
await resetPaneIfEnabled();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = await setCurrentPaneColor(pi, {
|
|
410
|
+
bg: config.workingBg,
|
|
411
|
+
fg: config.workingFg,
|
|
412
|
+
});
|
|
413
|
+
if (!result.ok) {
|
|
414
|
+
warnActionError(`pane highlight update failed: ${result.error}`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
clearActionError();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function pollFocusForReset(): Promise<void> {
|
|
421
|
+
if (!shouldClearOnRefocus || focusPollInFlight) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
focusPollInFlight = true;
|
|
426
|
+
try {
|
|
427
|
+
const focused = await getCurrentPaneFocusedState();
|
|
428
|
+
if (focused === undefined) {
|
|
429
|
+
consecutiveFocusQueryFailures += 1;
|
|
430
|
+
if (consecutiveFocusQueryFailures >= MAX_FOCUS_QUERY_FAILURES) {
|
|
431
|
+
stopFocusPolling();
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
consecutiveFocusQueryFailures = 0;
|
|
436
|
+
if (!sawPaneBlurSinceDone) {
|
|
437
|
+
if (!focused) {
|
|
438
|
+
sawPaneBlurSinceDone = true;
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!focused) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
await resetPaneIfEnabled();
|
|
446
|
+
} finally {
|
|
447
|
+
focusPollInFlight = false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function armFocusBasedReset(): void {
|
|
452
|
+
stopFocusPolling();
|
|
453
|
+
if (!config.enabled || !isInsideZellijSession() || !process.env.ZELLIJ_PANE_ID) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
shouldClearOnRefocus = true;
|
|
458
|
+
sawPaneBlurSinceDone = true;
|
|
459
|
+
focusPollTimer = setInterval(() => {
|
|
460
|
+
void pollFocusForReset();
|
|
461
|
+
}, FOCUS_POLL_INTERVAL_MS);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function applyDoneState(messages: readonly unknown[]): Promise<void> {
|
|
465
|
+
if (!config.enabled || !isInsideZellijSession()) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (!shouldHighlightAfterAgentEnd(messages)) {
|
|
469
|
+
await resetPaneIfEnabled();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const focused = await getCurrentPaneFocusedState();
|
|
474
|
+
if (focused !== false) {
|
|
475
|
+
await resetPaneIfEnabled();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const result = await setCurrentPaneColor(pi, {
|
|
480
|
+
bg: config.doneBg,
|
|
481
|
+
fg: config.doneFg,
|
|
482
|
+
});
|
|
483
|
+
if (!result.ok) {
|
|
484
|
+
warnActionError(`pane highlight update failed: ${result.error}`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
clearActionError();
|
|
488
|
+
armFocusBasedReset();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
492
|
+
await refreshConfig(ctx.cwd);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
496
|
+
await refreshConfig(ctx.cwd);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
500
|
+
await refreshConfig(ctx.cwd);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
504
|
+
await refreshConfig(ctx.cwd);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
pi.on("input", async () => {
|
|
508
|
+
await resetPaneIfEnabled();
|
|
509
|
+
return { action: "continue" };
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
pi.on("agent_start", async () => {
|
|
513
|
+
await applyWorkingState();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
pi.on("agent_end", async (event) => {
|
|
517
|
+
await applyDoneState(event.messages);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
pi.on("session_shutdown", async () => {
|
|
521
|
+
await resetPaneIfEnabled();
|
|
522
|
+
});
|
|
523
|
+
}
|
package/extensions/zv-open.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } 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";
|
|
5
5
|
import {
|
|
6
6
|
buildShellCommand,
|
|
7
|
+
formatPaneSuccessMessage,
|
|
7
8
|
openCommandInFloatingPane as openCommandInFloatingZellijPane,
|
|
8
9
|
openCommandInNewSplit,
|
|
10
|
+
type PaneOpenResult,
|
|
9
11
|
type SplitDirection,
|
|
10
12
|
} from "./zv-core.ts";
|
|
11
13
|
|
|
@@ -73,7 +75,7 @@ async function openToolInSplit(
|
|
|
73
75
|
ctx: ExtensionCommandContext,
|
|
74
76
|
direction: SplitDirection,
|
|
75
77
|
args: string,
|
|
76
|
-
): Promise<
|
|
78
|
+
): Promise<PaneOpenResult> {
|
|
77
79
|
return openCommandInNewSplit(pi, direction, buildShellCommand(ctx.cwd, args.trim()));
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -82,7 +84,7 @@ async function openToolInFloatingPane(
|
|
|
82
84
|
ctx: ExtensionCommandContext,
|
|
83
85
|
command: string,
|
|
84
86
|
name?: string,
|
|
85
|
-
): Promise<
|
|
87
|
+
): Promise<PaneOpenResult> {
|
|
86
88
|
return openCommandInFloatingZellijPane(pi, buildShellCommand(ctx.cwd, command.trim()), {
|
|
87
89
|
name,
|
|
88
90
|
...DEFAULT_FLOATING_PANE_OPTIONS,
|
|
@@ -107,7 +109,7 @@ function registerOpenCommand(
|
|
|
107
109
|
|
|
108
110
|
const result = await openToolInSplit(pi, ctx, direction, command);
|
|
109
111
|
if (result.ok) {
|
|
110
|
-
ctx.ui.notify(successMessage, "info");
|
|
112
|
+
ctx.ui.notify(formatPaneSuccessMessage(successMessage, result.paneId), "info");
|
|
111
113
|
} else {
|
|
112
114
|
ctx.ui.notify(`tool split failed: ${result.error}`, "error");
|
|
113
115
|
}
|
|
@@ -257,7 +259,7 @@ function registerConfiguredFloatingCommand(
|
|
|
257
259
|
const command = trimmedArgs.length > 0 ? `${config.run} ${trimmedArgs}` : config.run;
|
|
258
260
|
const result = await openToolInFloatingPane(pi, ctx, command, commandName);
|
|
259
261
|
if (result.ok) {
|
|
260
|
-
ctx.ui.notify(`Opened /${commandName} in a floating pane`, "info");
|
|
262
|
+
ctx.ui.notify(formatPaneSuccessMessage(`Opened /${commandName} in a floating pane`, result.paneId), "info");
|
|
261
263
|
} else {
|
|
262
264
|
ctx.ui.notify(`floating pane failed: ${result.error}`, "error");
|
|
263
265
|
}
|
package/extensions/zv-review.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@
|
|
2
|
-
import {
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
buildPiCommand,
|
|
4
|
+
formatPaneSuccessMessage,
|
|
5
|
+
openCommandInNewSplit,
|
|
6
|
+
type PaneOpenResult,
|
|
7
|
+
type SplitDirection,
|
|
8
|
+
} from "./zv-core.ts";
|
|
3
9
|
|
|
4
10
|
type ReviewMode = "general" | "bugs" | "refactor" | "tests" | "diff";
|
|
5
11
|
|
|
@@ -89,7 +95,7 @@ async function openReviewSplit(
|
|
|
89
95
|
ctx: ExtensionCommandContext,
|
|
90
96
|
direction: SplitDirection,
|
|
91
97
|
request: ReviewRequest,
|
|
92
|
-
): Promise<
|
|
98
|
+
): Promise<PaneOpenResult> {
|
|
93
99
|
return openCommandInNewSplit(pi, direction, buildPiCommand(ctx.cwd, { prompt: buildReviewPrompt(request) }));
|
|
94
100
|
}
|
|
95
101
|
|
|
@@ -111,7 +117,7 @@ function registerReviewCommand(
|
|
|
111
117
|
|
|
112
118
|
const result = await openReviewSplit(pi, ctx, direction, parsed.request);
|
|
113
119
|
if (result.ok) {
|
|
114
|
-
ctx.ui.notify(successMessage, "info");
|
|
120
|
+
ctx.ui.notify(formatPaneSuccessMessage(successMessage, result.paneId), "info");
|
|
115
121
|
} else {
|
|
116
122
|
ctx.ui.notify(`review pane failed: ${result.error}`, "error");
|
|
117
123
|
}
|
package/extensions/zv-split.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@
|
|
2
|
-
import {
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
buildPiCommand,
|
|
4
|
+
formatPaneSuccessMessage,
|
|
5
|
+
formatTabSuccessMessage,
|
|
6
|
+
openCommandInNewSplit,
|
|
7
|
+
openCommandInNewTab,
|
|
8
|
+
type PaneOpenResult,
|
|
9
|
+
type SplitDirection,
|
|
10
|
+
type TabOpenResult,
|
|
11
|
+
} from "./zv-core.ts";
|
|
3
12
|
|
|
4
13
|
async function openPiInSplit(
|
|
5
14
|
pi: ExtensionAPI,
|
|
6
15
|
ctx: ExtensionCommandContext,
|
|
7
16
|
direction: SplitDirection,
|
|
8
17
|
args: string,
|
|
9
|
-
): Promise<
|
|
18
|
+
): Promise<PaneOpenResult> {
|
|
10
19
|
return openCommandInNewSplit(
|
|
11
20
|
pi,
|
|
12
21
|
direction,
|
|
@@ -18,7 +27,7 @@ async function openPiInTab(
|
|
|
18
27
|
pi: ExtensionAPI,
|
|
19
28
|
ctx: ExtensionCommandContext,
|
|
20
29
|
args: string,
|
|
21
|
-
): Promise<
|
|
30
|
+
): Promise<TabOpenResult> {
|
|
22
31
|
return openCommandInNewTab(pi, ctx.cwd, buildPiCommand(ctx.cwd, { prompt: args.trim().length > 0 ? args : undefined }));
|
|
23
32
|
}
|
|
24
33
|
|
|
@@ -34,7 +43,7 @@ function registerSplitCommand(
|
|
|
34
43
|
handler: async (args, ctx) => {
|
|
35
44
|
const result = await openPiInSplit(pi, ctx, direction, args);
|
|
36
45
|
if (result.ok) {
|
|
37
|
-
ctx.ui.notify(successMessage, "info");
|
|
46
|
+
ctx.ui.notify(formatPaneSuccessMessage(successMessage, result.paneId), "info");
|
|
38
47
|
} else {
|
|
39
48
|
ctx.ui.notify(`zellij split failed: ${result.error}`, "error");
|
|
40
49
|
}
|
|
@@ -53,7 +62,7 @@ function registerTabCommand(
|
|
|
53
62
|
handler: async (args, ctx) => {
|
|
54
63
|
const result = await openPiInTab(pi, ctx, args);
|
|
55
64
|
if (result.ok) {
|
|
56
|
-
ctx.ui.notify(successMessage, "info");
|
|
65
|
+
ctx.ui.notify(formatTabSuccessMessage(successMessage, result.tabId), "info");
|
|
57
66
|
} else {
|
|
58
67
|
ctx.ui.notify(`zellij tab failed: ${result.error}`, "error");
|
|
59
68
|
}
|
package/extensions/zv-zoxide.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { existsSync, statSync } from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildPiCommand,
|
|
8
|
+
formatPaneSuccessMessage,
|
|
9
|
+
openCommandInNewSplit,
|
|
10
|
+
type PaneOpenResult,
|
|
11
|
+
type SplitDirection,
|
|
12
|
+
} from "./zv-core.ts";
|
|
7
13
|
|
|
8
14
|
const ZOXIDE_TIMEOUT_MS = 5000;
|
|
9
15
|
const MAX_COMPLETIONS = 10;
|
|
@@ -90,7 +96,7 @@ async function openPiInZoxideSplit(
|
|
|
90
96
|
query: string,
|
|
91
97
|
direction: SplitDirection,
|
|
92
98
|
commandName: string,
|
|
93
|
-
): Promise<
|
|
99
|
+
): Promise<PaneOpenResult> {
|
|
94
100
|
const targetResult = await resolveZoxideTarget(pi, query, ctx.cwd, commandName);
|
|
95
101
|
if (!targetResult.ok) {
|
|
96
102
|
return targetResult;
|
|
@@ -121,7 +127,7 @@ function registerZoxideCommand(
|
|
|
121
127
|
|
|
122
128
|
const result = await openPiInZoxideSplit(pi, ctx, query, direction, name);
|
|
123
129
|
if (result.ok) {
|
|
124
|
-
ctx.ui.notify(successMessage, "info");
|
|
130
|
+
ctx.ui.notify(formatPaneSuccessMessage(successMessage, result.paneId), "info");
|
|
125
131
|
} else {
|
|
126
132
|
ctx.ui.notify(`zoxide failed: ${result.error}`, "error");
|
|
127
133
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-zellij",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Pi package with zellij-powered terminal integrations",
|
|
5
5
|
"author": "Javi Molina",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
]
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@
|
|
49
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@
|
|
52
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
53
53
|
}
|
|
54
54
|
}
|