@zswarm/core 0.1.5 → 0.1.7
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/README.md +4 -1
- package/dist/exec.d.ts +47 -2
- package/dist/exec.js +249 -20
- package/dist/harness.js +12 -7
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -4
- package/dist/ops/broadcast.js +1 -1
- package/dist/ops/bus.d.ts +6 -3
- package/dist/ops/bus.js +85 -35
- package/dist/ops/dispatch.d.ts +7 -1
- package/dist/ops/dispatch.js +235 -70
- package/dist/ops/guards.d.ts +2 -0
- package/dist/ops/guards.js +11 -1
- package/dist/ops/observation.d.ts +8 -0
- package/dist/ops/observation.js +26 -0
- package/dist/ops/panes.js +4 -4
- package/dist/ops/routing.d.ts +13 -0
- package/dist/ops/routing.js +21 -0
- package/dist/ops/spawn.d.ts +3 -3
- package/dist/ops/spawn.js +130 -64
- package/dist/ops/status.d.ts +30 -5
- package/dist/ops/status.js +245 -96
- package/dist/ops/types.d.ts +5 -1
- package/dist/ops/util.d.ts +3 -0
- package/dist/ops/util.js +2 -1
- package/dist/ops/worktree.js +2 -2
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +60 -9
- package/dist/zellij/args.d.ts +2 -0
- package/dist/zellij/binary.d.ts +47 -0
- package/dist/zellij/binary.js +201 -8
- package/dist/zellij/bus.d.ts +2 -0
- package/dist/zellij/bus.js +3 -2
- package/dist/zellij/cache.d.ts +24 -0
- package/dist/zellij/cache.js +90 -0
- package/dist/zellij/client.d.ts +37 -6
- package/dist/zellij/client.js +181 -42
- package/dist/zellij/panes.js +3 -0
- package/dist/zellij/session.d.ts +19 -3
- package/dist/zellij/session.js +39 -9
- package/package.json +3 -3
package/dist/ops/status.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { ZellijError } from "../errors.js";
|
|
1
2
|
import { resolveHarness } from "../harness.js";
|
|
2
|
-
import { isTrue, normalizeScreen, numberArg, optionalString } from "./util.js";
|
|
3
|
+
import { isTrue, normalizeScreen, numberArg, optionalString, throwIfAborted, } from "./util.js";
|
|
3
4
|
/**
|
|
4
5
|
* Legacy last-line prompt shapes for callers without a profile. A term counts
|
|
5
6
|
* only when it is a question, not chrome: a y/n form, a trailing `?`, or the
|
|
@@ -18,6 +19,10 @@ const QUESTION = /(\(y\/n\)|\[y\/n\]|\(yes\/no\)|\[y\/n\/a\]|password:|passphras
|
|
|
18
19
|
* where it is or it would fire on chrome.
|
|
19
20
|
*/
|
|
20
21
|
const PROMPT_WINDOW = 24;
|
|
22
|
+
/** Default overall budget for a sampled status pass (covers IPC + dumps). */
|
|
23
|
+
export const DEFAULT_STATUS_TIMEOUT_MS = 30_000;
|
|
24
|
+
/** How many dump-screen calls to run at once on the direct (non-bus) path. */
|
|
25
|
+
export const STATUS_DUMP_CONCURRENCY = 3;
|
|
21
26
|
export function lastLine(screen) {
|
|
22
27
|
const lines = screen.split("\n").filter((l) => l.trim());
|
|
23
28
|
return lines.length > 0 ? lines[lines.length - 1].trim() : "";
|
|
@@ -30,25 +35,39 @@ function trailingLines(screen, n) {
|
|
|
30
35
|
.filter((line) => line.length > 0)
|
|
31
36
|
.slice(-n);
|
|
32
37
|
}
|
|
33
|
-
|
|
34
|
-
* True when the trailing window holds a named approval prompt. The profile's
|
|
35
|
-
* waiting patterns run across the window; the legacy last-line QUESTION check
|
|
36
|
-
* stays as the conservative fallback for callers without a profile. Bias is
|
|
37
|
-
* toward false negatives: a hit must be prompt UI we can name, never a bare
|
|
38
|
-
* question mark or "confirm".
|
|
39
|
-
*/
|
|
40
|
-
function promptHolds(screen, profile) {
|
|
38
|
+
export function waitingPrompt(screen, profile) {
|
|
41
39
|
const lines = trailingLines(screen, PROMPT_WINDOW);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
// A menu requires a choice structure AND navigation chrome. A lone "Allow
|
|
41
|
+
// once" in logs/help must not turn a worker into an approval request.
|
|
42
|
+
const choices = lines.filter((line) => /^(?:[›❯>●]\s*)?\d+[.)]\s+(?:Yes\b|No\b|Allow\b|Deny\b|Cancel\b)/i.test(line));
|
|
43
|
+
const navigation = lines.some((line) => /(?:enter|return)\s+(?:to\s+)?(?:select|confirm)|(?:esc|escape)\s+(?:to\s+)?cancel/i.test(line));
|
|
44
|
+
const approval = lines.find((line) => /^(?:[›❯>]\s*)?(?:Approve (?:this|the) (?:operation|command|action)|Approval required|Would you like to run the following command\?|Run this command\?)[?:]?$/i.test(line));
|
|
45
|
+
if (approval && choices.length >= 2 && navigation) {
|
|
46
|
+
return { reason: "approval_menu", evidence: choices.slice(0, 3).join("\n").slice(0, 320), source: "screen" };
|
|
47
|
+
}
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (profile?.waiting.some((re) => re.test(line))) {
|
|
50
|
+
return { reason: "prompt", evidence: line.slice(0, 320), source: "screen" };
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
|
-
|
|
53
|
+
const last = lines[lines.length - 1] ?? "";
|
|
54
|
+
return QUESTION.test(last) ? { reason: "prompt", evidence: last.slice(0, 320), source: "screen" } : null;
|
|
55
|
+
}
|
|
56
|
+
function promptHolds(screen, profile) {
|
|
57
|
+
return waitingPrompt(screen, profile) !== null;
|
|
58
|
+
}
|
|
59
|
+
/** Compact summary of the selected terminal peers, including inactive tabs. */
|
|
60
|
+
export function statusTabs(peers) {
|
|
61
|
+
const tabs = new Map();
|
|
62
|
+
for (const peer of peers) {
|
|
63
|
+
const key = JSON.stringify([peer.tabId, peer.tab]);
|
|
64
|
+
const tab = tabs.get(key) ?? { id: peer.tabId, name: peer.tab, panes: 0, states: {} };
|
|
65
|
+
tab.panes++;
|
|
66
|
+
const state = String(peer.state);
|
|
67
|
+
tab.states[state] = (tab.states[state] ?? 0) + 1;
|
|
68
|
+
tabs.set(key, tab);
|
|
69
|
+
}
|
|
70
|
+
return [...tabs.values()];
|
|
52
71
|
}
|
|
53
72
|
export function classify(input) {
|
|
54
73
|
if (input.exited)
|
|
@@ -57,72 +76,121 @@ export function classify(input) {
|
|
|
57
76
|
return "busy";
|
|
58
77
|
return promptHolds(input.after, input.profile) ? "waiting" : "idle";
|
|
59
78
|
}
|
|
79
|
+
/** Run `fn` over items with at most `limit` in flight. */
|
|
80
|
+
export async function mapPool(items, limit, fn) {
|
|
81
|
+
if (items.length === 0)
|
|
82
|
+
return [];
|
|
83
|
+
const concurrency = Math.max(1, Math.min(limit, items.length));
|
|
84
|
+
const results = new Array(items.length);
|
|
85
|
+
let next = 0;
|
|
86
|
+
async function worker() {
|
|
87
|
+
while (next < items.length) {
|
|
88
|
+
const i = next++;
|
|
89
|
+
results[i] = await fn(items[i], i);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
function isCancelledError(err) {
|
|
96
|
+
return ((err instanceof ZellijError && err.code === "cancelled") ||
|
|
97
|
+
(err instanceof Error && /cancelled/i.test(err.message)));
|
|
98
|
+
}
|
|
99
|
+
function peerEntry(pane, state, screen, verbose, extra) {
|
|
100
|
+
const entry = {
|
|
101
|
+
id: pane.id,
|
|
102
|
+
title: pane.title,
|
|
103
|
+
tab: pane.tabName ?? null,
|
|
104
|
+
tabId: pane.tabId ?? null,
|
|
105
|
+
state,
|
|
106
|
+
lastLine: lastLine(screen).slice(0, 160),
|
|
107
|
+
...extra,
|
|
108
|
+
};
|
|
109
|
+
if (state === "waiting")
|
|
110
|
+
entry.waiting = waitingPrompt(screen, resolveHarness(pane));
|
|
111
|
+
if (verbose) {
|
|
112
|
+
entry.command = pane.command ?? null;
|
|
113
|
+
entry.cwd = pane.cwd ?? null;
|
|
114
|
+
}
|
|
115
|
+
return entry;
|
|
116
|
+
}
|
|
60
117
|
/**
|
|
61
118
|
* Sample every pane twice and say who is working, who is stuck on a prompt,
|
|
62
119
|
* and who is free — the routing question `list` cannot answer.
|
|
120
|
+
*
|
|
121
|
+
* `deadlineAt` is the absolute clock deadline for the whole status op (setup
|
|
122
|
+
* included). When omitted, one is derived from `timeoutMs` at entry.
|
|
63
123
|
*/
|
|
64
|
-
export async function peerStatus(client, args, clock, supplied) {
|
|
124
|
+
export async function peerStatus(client, args, clock, supplied, opts = {}) {
|
|
125
|
+
const signal = opts.signal;
|
|
126
|
+
throwIfAborted(signal);
|
|
127
|
+
const budget = numberArg(args, "timeoutMs", DEFAULT_STATUS_TIMEOUT_MS, {
|
|
128
|
+
min: 1_000,
|
|
129
|
+
max: 900_000,
|
|
130
|
+
});
|
|
131
|
+
const deadline = opts.deadlineAt ?? clock.now() + budget;
|
|
132
|
+
const remaining = () => Math.max(0, deadline - clock.now());
|
|
133
|
+
const setupBudget = () => {
|
|
134
|
+
throwIfAborted(signal);
|
|
135
|
+
const left = remaining();
|
|
136
|
+
if (left <= 0) {
|
|
137
|
+
throw new ZellijError("zellij_failed", "status timed out during setup");
|
|
138
|
+
}
|
|
139
|
+
return left;
|
|
140
|
+
};
|
|
141
|
+
// Prefer the caller's already-resolved session/panes so dispatch's setup
|
|
142
|
+
// budget is not spent twice.
|
|
65
143
|
const session = supplied?.session ??
|
|
66
|
-
(await client.resolveSession(typeof args.session === "string" ? args.session : undefined)).session;
|
|
67
|
-
|
|
144
|
+
(await client.resolveSession(typeof args.session === "string" ? args.session : undefined, setupBudget())).session;
|
|
145
|
+
throwIfAborted(signal);
|
|
146
|
+
const panes = supplied?.panes ?? (await client.listPanes(session, setupBudget()));
|
|
147
|
+
throwIfAborted(signal);
|
|
68
148
|
const source = supplied?.source ?? "zellij";
|
|
69
149
|
const only = optionalString(args.to);
|
|
150
|
+
const verbose = isTrue(args.verbose);
|
|
70
151
|
const targets = only
|
|
71
152
|
? [client.resolvePane(panes, only)]
|
|
72
153
|
: panes.filter((p) => !p.isPlugin);
|
|
73
|
-
// sampleMs=0 asks for the cheap answer: who is alive, from state the plugin
|
|
74
|
-
// already holds. Below that, two samples too close together read as idle.
|
|
75
154
|
const requested = numberArg(args, "sampleMs", 400, { min: 0, max: 10_000 });
|
|
76
155
|
if (requested === 0) {
|
|
77
156
|
const peers = targets
|
|
78
|
-
.map((pane) =>
|
|
79
|
-
const entry = {
|
|
80
|
-
id: pane.id,
|
|
81
|
-
title: pane.title,
|
|
82
|
-
state: pane.exited ? "exited" : "running",
|
|
83
|
-
};
|
|
84
|
-
if (isTrue(args.verbose)) {
|
|
85
|
-
entry.command = pane.command ?? null;
|
|
86
|
-
entry.cwd = pane.cwd ?? null;
|
|
87
|
-
entry.tab = pane.tabName ?? null;
|
|
88
|
-
}
|
|
89
|
-
return entry;
|
|
90
|
-
})
|
|
157
|
+
.map((pane) => peerEntry(pane, pane.exited ? "exited" : "running", "", verbose))
|
|
91
158
|
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
92
|
-
// No `free`: without sampling there is no way to tell busy from idle.
|
|
93
159
|
return {
|
|
94
160
|
ok: true,
|
|
95
|
-
data: { session, source, sampled: false, sampleMs: 0, peers },
|
|
161
|
+
data: { session, source, sampled: false, sampleMs: 0, peers, tabs: statusTabs(peers) },
|
|
96
162
|
};
|
|
97
163
|
}
|
|
98
164
|
const sampleMs = Math.max(50, requested);
|
|
99
|
-
const live = targets.filter((pane) => !pane.exited)
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
const
|
|
165
|
+
const live = targets.filter((pane) => !pane.exited);
|
|
166
|
+
// Explicit sampling retains its interval semantics. Ordinary status prefers
|
|
167
|
+
// one bus observation, including a single-pane crew, with no sample sleep.
|
|
168
|
+
const preferChanges = isTrue(args.sinceLast) || (args.sinceLast === undefined && args.sampleMs === undefined);
|
|
169
|
+
if (preferChanges && supplied?.readChanged) {
|
|
170
|
+
const left = remaining();
|
|
171
|
+
let changed = null;
|
|
172
|
+
try {
|
|
173
|
+
changed = live.length === 0 ? new Map() : left > 0 ? await supplied.readChanged(live.map((p) => p.id), left) : null;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (isCancelledError(err) || signal?.aborted)
|
|
177
|
+
throw new ZellijError("cancelled", "operation cancelled");
|
|
178
|
+
// A missing/older bus falls back to bounded screen samples below.
|
|
179
|
+
}
|
|
180
|
+
throwIfAborted(signal);
|
|
105
181
|
if (changed) {
|
|
106
182
|
const peers = targets
|
|
107
183
|
.map((pane) => {
|
|
184
|
+
if (pane.exited) {
|
|
185
|
+
return peerEntry(pane, "exited", "", verbose);
|
|
186
|
+
}
|
|
108
187
|
const row = changed.get(pane.id);
|
|
109
|
-
const state =
|
|
110
|
-
? "
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
: "idle";
|
|
116
|
-
const entry = {
|
|
117
|
-
id: pane.id,
|
|
118
|
-
title: pane.title,
|
|
119
|
-
state,
|
|
120
|
-
lastLine: lastLine(row?.screen ?? "").slice(0, 160),
|
|
121
|
-
};
|
|
122
|
-
// First sight has nothing to compare against, so idle is a guess.
|
|
123
|
-
if (row?.first)
|
|
124
|
-
entry.first = true;
|
|
125
|
-
return entry;
|
|
188
|
+
const state = !row ? "unknown"
|
|
189
|
+
: promptHolds(row.screen, resolveHarness(pane)) ? "waiting"
|
|
190
|
+
: row.first ? "unknown" : row.changed ? "busy" : "idle";
|
|
191
|
+
return peerEntry(pane, state, row?.screen ?? "", verbose, {
|
|
192
|
+
...(row?.first ? { first: true } : {}),
|
|
193
|
+
});
|
|
126
194
|
})
|
|
127
195
|
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
128
196
|
return {
|
|
@@ -132,61 +200,142 @@ export async function peerStatus(client, args, clock, supplied) {
|
|
|
132
200
|
source,
|
|
133
201
|
sampled: false,
|
|
134
202
|
sinceLast: true,
|
|
203
|
+
observation: "bus-changes",
|
|
135
204
|
peers,
|
|
205
|
+
tabs: statusTabs(peers),
|
|
136
206
|
free: peers.filter((p) => p.state === "idle").map((p) => p.id),
|
|
137
207
|
},
|
|
138
208
|
};
|
|
139
209
|
}
|
|
140
210
|
}
|
|
141
211
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
212
|
+
* Bus path: one batched read per round, each bounded by the remaining budget.
|
|
213
|
+
* Direct path: each pane completes its own before→sleep→after pair under a
|
|
214
|
+
* shared concurrency limit so one stall does not block healthy peers.
|
|
145
215
|
*/
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
216
|
+
const samples = new Map();
|
|
217
|
+
if (supplied?.readScreens) {
|
|
218
|
+
const sampleRound = async () => {
|
|
219
|
+
throwIfAborted(signal);
|
|
220
|
+
const left = remaining();
|
|
221
|
+
if (left <= 0) {
|
|
222
|
+
return new Map(live.map((p) => [p.id, null]));
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const batched = await supplied.readScreens(live.map((p) => p.id), left);
|
|
226
|
+
throwIfAborted(signal);
|
|
227
|
+
if (!batched)
|
|
228
|
+
return new Map(); // signal fallback to dumps below
|
|
229
|
+
return new Map(live.map((p) => {
|
|
230
|
+
const text = batched.get(p.id);
|
|
231
|
+
return [
|
|
232
|
+
p.id,
|
|
233
|
+
text === undefined ? null : normalizeScreen(text),
|
|
234
|
+
];
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
if (isCancelledError(err) || signal?.aborted) {
|
|
239
|
+
throw new ZellijError("cancelled", "operation cancelled");
|
|
240
|
+
}
|
|
241
|
+
return new Map(live.map((p) => [p.id, null]));
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
let before = await sampleRound();
|
|
245
|
+
if (before.size === 0) {
|
|
246
|
+
// Bus declined — fall through to per-pane dumps.
|
|
247
|
+
before = new Map();
|
|
152
248
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
249
|
+
else {
|
|
250
|
+
let after = new Map();
|
|
251
|
+
if (remaining() > sampleMs) {
|
|
252
|
+
await clock.sleep(sampleMs);
|
|
253
|
+
throwIfAborted(signal);
|
|
254
|
+
after = await sampleRound();
|
|
255
|
+
}
|
|
256
|
+
for (const pane of live) {
|
|
257
|
+
samples.set(pane.id, {
|
|
258
|
+
before: before.get(pane.id) ?? null,
|
|
259
|
+
after: after.get(pane.id) ?? null,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
157
262
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
263
|
+
}
|
|
264
|
+
if (samples.size === 0) {
|
|
265
|
+
// Per-pane sample pairs under bounded concurrency.
|
|
266
|
+
await mapPool(live, STATUS_DUMP_CONCURRENCY, async (pane) => {
|
|
267
|
+
throwIfAborted(signal);
|
|
268
|
+
const dumpOne = async () => {
|
|
269
|
+
const left = remaining();
|
|
270
|
+
if (left <= 0)
|
|
271
|
+
return null;
|
|
272
|
+
try {
|
|
273
|
+
const dumped = await client.dumpPane({
|
|
274
|
+
session,
|
|
275
|
+
paneId: pane.id,
|
|
276
|
+
timeoutMs: left,
|
|
277
|
+
});
|
|
278
|
+
throwIfAborted(signal);
|
|
279
|
+
return normalizeScreen(dumped.text);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
if (isCancelledError(err) || signal?.aborted) {
|
|
283
|
+
throw new ZellijError("cancelled", "operation cancelled");
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const before = await dumpOne();
|
|
289
|
+
throwIfAborted(signal);
|
|
290
|
+
let after = null;
|
|
291
|
+
// This pane's interval starts when its first screen is available. Queue
|
|
292
|
+
// time and a slow first read cannot replace time between observations.
|
|
293
|
+
// Without room for the whole interval, keep it unknown rather than idle.
|
|
294
|
+
if (before !== null && remaining() > sampleMs) {
|
|
295
|
+
await clock.sleep(sampleMs);
|
|
296
|
+
throwIfAborted(signal);
|
|
297
|
+
after = await dumpOne();
|
|
298
|
+
}
|
|
299
|
+
samples.set(pane.id, { before, after });
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
throwIfAborted(signal);
|
|
163
303
|
const peers = [];
|
|
304
|
+
let partial = false;
|
|
164
305
|
for (const pane of targets) {
|
|
165
|
-
|
|
166
|
-
|
|
306
|
+
if (pane.exited) {
|
|
307
|
+
peers.push(peerEntry(pane, "exited", "", verbose));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const pair = samples.get(pane.id);
|
|
311
|
+
const first = pair?.before ?? null;
|
|
312
|
+
const after = pair?.after ?? null;
|
|
313
|
+
if (first == null || after == null) {
|
|
314
|
+
partial = true;
|
|
315
|
+
peers.push(peerEntry(pane, "unknown", after ?? first ?? "", verbose));
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
167
318
|
const state = classify({
|
|
168
|
-
exited:
|
|
319
|
+
exited: false,
|
|
169
320
|
before: first,
|
|
170
321
|
after,
|
|
171
322
|
profile: resolveHarness(pane),
|
|
172
323
|
});
|
|
173
|
-
|
|
174
|
-
id: pane.id,
|
|
175
|
-
title: pane.title,
|
|
176
|
-
state,
|
|
177
|
-
lastLine: lastLine(after).slice(0, 160),
|
|
178
|
-
};
|
|
179
|
-
if (isTrue(args.verbose)) {
|
|
180
|
-
entry.command = pane.command ?? null;
|
|
181
|
-
entry.cwd = pane.cwd ?? null;
|
|
182
|
-
entry.tab = pane.tabName ?? null;
|
|
183
|
-
}
|
|
184
|
-
peers.push(entry);
|
|
324
|
+
peers.push(peerEntry(pane, state, after, verbose));
|
|
185
325
|
}
|
|
186
326
|
peers.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
187
327
|
const free = peers.filter((p) => p.state === "idle").map((p) => p.id);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
328
|
+
const data = {
|
|
329
|
+
session,
|
|
330
|
+
source,
|
|
331
|
+
sampled: true,
|
|
332
|
+
observation: "samples",
|
|
333
|
+
sampleMs,
|
|
334
|
+
peers,
|
|
335
|
+
tabs: statusTabs(peers),
|
|
336
|
+
free,
|
|
191
337
|
};
|
|
338
|
+
if (partial || remaining() <= 0)
|
|
339
|
+
data.partial = true;
|
|
340
|
+
return { ok: true, data };
|
|
192
341
|
}
|
package/dist/ops/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RoutingContext } from "./routing.js";
|
|
2
|
+
export type OpsResult = ({
|
|
2
3
|
ok: true;
|
|
3
4
|
data: unknown;
|
|
4
5
|
} | {
|
|
@@ -6,7 +7,10 @@ export type OpsResult = {
|
|
|
6
7
|
error: {
|
|
7
8
|
code: string;
|
|
8
9
|
message: string;
|
|
10
|
+
details?: Record<string, unknown>;
|
|
9
11
|
};
|
|
12
|
+
}) & {
|
|
13
|
+
context?: RoutingContext;
|
|
10
14
|
};
|
|
11
15
|
import type { GitClient } from "../git.js";
|
|
12
16
|
import type { Policy } from "../policy.js";
|
package/dist/ops/util.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare function paneViewSlim(p: ZellijPane): {
|
|
|
13
13
|
title: string;
|
|
14
14
|
command: string | null;
|
|
15
15
|
tab: string | null;
|
|
16
|
+
tabId: number | null;
|
|
16
17
|
};
|
|
17
18
|
/**
|
|
18
19
|
* The event-bus view. Zellij's pane manifest carries no command, so the key is
|
|
@@ -22,6 +23,7 @@ export declare function paneViewBus(p: ZellijPane): {
|
|
|
22
23
|
id: string;
|
|
23
24
|
title: string;
|
|
24
25
|
tab: string | null;
|
|
26
|
+
tabId: number | null;
|
|
25
27
|
};
|
|
26
28
|
export declare function paneViewFull(p: ZellijPane): {
|
|
27
29
|
cwd: string | null;
|
|
@@ -32,6 +34,7 @@ export declare function paneViewFull(p: ZellijPane): {
|
|
|
32
34
|
title: string;
|
|
33
35
|
command: string | null;
|
|
34
36
|
tab: string | null;
|
|
37
|
+
tabId: number | null;
|
|
35
38
|
};
|
|
36
39
|
/** Truncate dump text; default keeps the tail (recent output). */
|
|
37
40
|
export declare function truncateDumpText(text: string, maxChars: number, keep?: "tail" | "head"): {
|
package/dist/ops/util.js
CHANGED
|
@@ -25,6 +25,7 @@ export function paneViewSlim(p) {
|
|
|
25
25
|
title: p.title,
|
|
26
26
|
command: p.command ?? null,
|
|
27
27
|
tab: p.tabName ?? null,
|
|
28
|
+
tabId: p.tabId ?? null,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
@@ -32,7 +33,7 @@ export function paneViewSlim(p) {
|
|
|
32
33
|
* left out rather than reported as null — absent means unknown, not none.
|
|
33
34
|
*/
|
|
34
35
|
export function paneViewBus(p) {
|
|
35
|
-
return { id: p.id, title: p.title, tab: p.tabName ?? null };
|
|
36
|
+
return { id: p.id, title: p.title, tab: p.tabName ?? null, tabId: p.tabId ?? null };
|
|
36
37
|
}
|
|
37
38
|
export function paneViewFull(p) {
|
|
38
39
|
return {
|
package/dist/ops/worktree.js
CHANGED
|
@@ -62,7 +62,7 @@ export async function listPeerWorktrees(git, client, args) {
|
|
|
62
62
|
let panes = [];
|
|
63
63
|
try {
|
|
64
64
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
|
65
|
-
panes = await client.listPanes(session);
|
|
65
|
+
panes = await client.listPanes(session, undefined, { fresh: true });
|
|
66
66
|
}
|
|
67
67
|
catch {
|
|
68
68
|
// Worktrees are useful to list even with no live Zellij session.
|
|
@@ -118,7 +118,7 @@ export async function removePeerWorktree(git, client, args, env = process.env) {
|
|
|
118
118
|
let occupants = [];
|
|
119
119
|
try {
|
|
120
120
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
|
121
|
-
occupants = panesIn(await client.listPanes(session), target.path);
|
|
121
|
+
occupants = panesIn(await client.listPanes(session, undefined, { fresh: true }), target.path);
|
|
122
122
|
}
|
|
123
123
|
catch (err) {
|
|
124
124
|
// No live session means nobody is working in it. Any other failure must
|
package/dist/schema.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type ParamSpec = {
|
|
|
14
14
|
flags: string[];
|
|
15
15
|
/** Repeatable flags collect into an array (`--key a --key b`). */
|
|
16
16
|
repeat?: boolean;
|
|
17
|
+
/** Local CLI preprocessing, excluded from the MCP protocol. */
|
|
18
|
+
cliOnly?: boolean;
|
|
17
19
|
values?: readonly string[];
|
|
18
20
|
description: string;
|
|
19
21
|
};
|
package/dist/schema.js
CHANGED
|
@@ -63,7 +63,25 @@ export const PARAMS = [
|
|
|
63
63
|
name: "all",
|
|
64
64
|
type: "boolean",
|
|
65
65
|
flags: ["--all", "-a"],
|
|
66
|
-
description: "broadcast: every terminal pane in the session",
|
|
66
|
+
description: "broadcast: every terminal pane in the session; sessions: include EXITED resurrectable sessions",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "live",
|
|
70
|
+
type: "boolean",
|
|
71
|
+
flags: ["--live", "--active"],
|
|
72
|
+
description: "sessions: only live (non-EXITED) sessions — this is the default; use --all to include EXITED",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "local",
|
|
76
|
+
type: "boolean",
|
|
77
|
+
flags: ["--local"],
|
|
78
|
+
description: "route this call to the local machine only — clears ZSWARM_SSH, ZSWARM_SERVE, and remote ZSWARM_TMP for the invocation",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "ssh",
|
|
82
|
+
type: "string",
|
|
83
|
+
flags: ["--ssh"],
|
|
84
|
+
description: "route this call over SSH to user@host (or an alias) for the invocation; clears ZSWARM_SERVE; put flags in ZSWARM_SSH_OPTS",
|
|
67
85
|
},
|
|
68
86
|
{
|
|
69
87
|
name: "group",
|
|
@@ -111,13 +129,25 @@ export const PARAMS = [
|
|
|
111
129
|
name: "sampleMs",
|
|
112
130
|
type: "number",
|
|
113
131
|
flags: ["--sample-ms"],
|
|
114
|
-
description: "status:
|
|
132
|
+
description: "status: explicitly sample twice with this gap (fallback default 400); 0 reports running/exited only; default prefers bus changes",
|
|
115
133
|
},
|
|
116
134
|
{
|
|
117
135
|
name: "sinceLast",
|
|
118
136
|
type: "boolean",
|
|
119
137
|
flags: ["--since-last"],
|
|
120
|
-
description: "status: classify
|
|
138
|
+
description: "status: classify changes since the previous bus observation (default when bus available); first observation is unknown unless a prompt is recognized",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "fresh",
|
|
142
|
+
type: "boolean",
|
|
143
|
+
flags: ["--fresh"],
|
|
144
|
+
description: "bypass the short-lived session/pane/tab listing cache for this call",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "serveAddress",
|
|
148
|
+
type: "string",
|
|
149
|
+
flags: ["--serve"],
|
|
150
|
+
description: "use an existing serve endpoint (e.g. 127.0.0.1:9419 through an SSH tunnel); preferred for frequent status calls; uses ZSWARM_SERVE_TOKEN",
|
|
121
151
|
},
|
|
122
152
|
{
|
|
123
153
|
name: "limit",
|
|
@@ -143,6 +173,13 @@ export const PARAMS = [
|
|
|
143
173
|
flags: ["--body", "-b", "--text"],
|
|
144
174
|
description: "send: message body",
|
|
145
175
|
},
|
|
176
|
+
{
|
|
177
|
+
name: "bodyFile",
|
|
178
|
+
type: "string",
|
|
179
|
+
flags: ["--body-file"],
|
|
180
|
+
cliOnly: true,
|
|
181
|
+
description: "send: read UTF-8 on the caller from PATH, or - for stdin; cannot combine with --body/--text",
|
|
182
|
+
},
|
|
146
183
|
{
|
|
147
184
|
name: "text",
|
|
148
185
|
type: "string",
|
|
@@ -220,7 +257,7 @@ export const PARAMS = [
|
|
|
220
257
|
name: "timeoutMs",
|
|
221
258
|
type: "number",
|
|
222
259
|
flags: ["--timeout-ms"],
|
|
223
|
-
description: "wait:
|
|
260
|
+
description: "wait: timeout (default 60000); status/spawn: overall deadline (default 30000), including setup and observation",
|
|
224
261
|
},
|
|
225
262
|
{
|
|
226
263
|
name: "keys",
|
|
@@ -302,6 +339,12 @@ export const PARAMS = [
|
|
|
302
339
|
values: ["auto", "double-enter", "none"],
|
|
303
340
|
description: "send/broadcast: auto verifies the paste actually submitted and presses Enter again if not (default)",
|
|
304
341
|
},
|
|
342
|
+
{
|
|
343
|
+
name: "observeMs",
|
|
344
|
+
type: "number",
|
|
345
|
+
flags: ["--observe-ms"],
|
|
346
|
+
description: "spawn: observe creation/alias for up to 3000ms; pane lookup: retry absence for 1000ms; 0 disables retries",
|
|
347
|
+
},
|
|
305
348
|
{
|
|
306
349
|
name: "settleMs",
|
|
307
350
|
type: "number",
|
|
@@ -312,7 +355,7 @@ export const PARAMS = [
|
|
|
312
355
|
name: "expect",
|
|
313
356
|
type: "string",
|
|
314
357
|
flags: ["--expect"],
|
|
315
|
-
description: "
|
|
358
|
+
description: "send/keys/interrupt: case-insensitive substring required on the current screen immediately before input",
|
|
316
359
|
},
|
|
317
360
|
{
|
|
318
361
|
name: "message",
|
|
@@ -423,8 +466,10 @@ export function mcpInputSchema() {
|
|
|
423
466
|
description: OP_NAMES.join(" | "),
|
|
424
467
|
},
|
|
425
468
|
};
|
|
426
|
-
for (const param of PARAMS)
|
|
427
|
-
|
|
469
|
+
for (const param of PARAMS) {
|
|
470
|
+
if (!param.cliOnly)
|
|
471
|
+
properties[param.name] = propertyFor(param);
|
|
472
|
+
}
|
|
428
473
|
return {
|
|
429
474
|
type: "object",
|
|
430
475
|
additionalProperties: false,
|
|
@@ -450,7 +495,7 @@ export function cliUsage() {
|
|
|
450
495
|
const value = param.type === "boolean" ? "" : param.type === "number" ? " N" : " VALUE";
|
|
451
496
|
lines.push(` ${param.flags.join(", ").padEnd(28)}${value.trim().padEnd(6)}${param.description}`);
|
|
452
497
|
}
|
|
453
|
-
lines.push("", "Guards: writes refuse zswarm's own pane (--allow-self) and exited panes (--force). --expect requires the screen to contain a substring first.", "Bus: `zswarm bus --install` once per Zellij session. `--force` closes orphan bus panes and reloads; do not use it as a retry.", "Remote: ZSWARM_SSH (+ ZSWARM_TMP=auto or ZSWARM_SSH_MODE=interactive on Windows). Or run `zswarm serve --listen` next to Zellij and set ZSWARM_SERVE (+ ZSWARM_SERVE_TOKEN). Serve binds loopback only and always requires a token.", "Env: ZSWARM_BIN, ZSWARM_PATH, ZSWARM_SESSION, ZSWARM_SELF_PANE, ZSWARM_FROM, ZELLIJ_PANE_ID, ZELLIJ_SESSION_NAME, ZSWARM_BUS, ZSWARM_BUS_PLUGIN, ZSWARM_SSH, ZSWARM_TMP, ZSWARM_SSH_MODE, ZSWARM_SERVE, ZSWARM_SERVE_TOKEN", "");
|
|
498
|
+
lines.push("", "Guards: writes refuse zswarm's own pane (--allow-self) and exited panes (--force). --expect requires the screen to contain a substring first.", "Bus: `zswarm bus --install` once per Zellij session. `--force` closes orphan bus panes and reloads; do not use it as a retry.", "Remote: ZSWARM_SSH (+ ZSWARM_TMP=auto or ZSWARM_SSH_MODE=interactive on Windows). Or run `zswarm serve --listen` next to Zellij and set ZSWARM_SERVE (+ ZSWARM_SERVE_TOKEN). Serve binds loopback only and always requires a token.", "Env: ZSWARM_BIN, ZSWARM_PATH, ZSWARM_SESSION, ZSWARM_SELF_PANE, ZSWARM_FROM, ZELLIJ_PANE_ID, ZELLIJ_SESSION_NAME, ZSWARM_BUS, ZSWARM_BUS_PLUGIN, ZSWARM_SSH, ZSWARM_TMP, ZSWARM_SSH_MODE, ZSWARM_SERVE, ZSWARM_SERVE_TOKEN, ZSWARM_CACHE_TTL_MS", "");
|
|
454
499
|
return lines.join("\n");
|
|
455
500
|
}
|
|
456
501
|
/** Turn argv (without the op) into dispatch args, driven by PARAMS. */
|
|
@@ -499,6 +544,9 @@ export function parseCliArgv(argv) {
|
|
|
499
544
|
out[param.name] = true;
|
|
500
545
|
continue;
|
|
501
546
|
}
|
|
547
|
+
if ((param.name === "body" || param.name === "bodyFile") && Object.hasOwn(out, param.name)) {
|
|
548
|
+
throw new ZellijError("usage", "provide exactly one body source");
|
|
549
|
+
}
|
|
502
550
|
const value = rest[++i];
|
|
503
551
|
if (value === undefined) {
|
|
504
552
|
throw new ZellijError("usage", `${token} needs a value`);
|
|
@@ -519,7 +567,7 @@ export function parseCliArgv(argv) {
|
|
|
519
567
|
out.to = token;
|
|
520
568
|
continue;
|
|
521
569
|
}
|
|
522
|
-
if (!out
|
|
570
|
+
if (!Object.hasOwn(out, "body") && op === "send") {
|
|
523
571
|
out.body = token;
|
|
524
572
|
continue;
|
|
525
573
|
}
|
|
@@ -527,5 +575,8 @@ export function parseCliArgv(argv) {
|
|
|
527
575
|
}
|
|
528
576
|
for (const [name, values] of repeated)
|
|
529
577
|
out[name] = values;
|
|
578
|
+
if (out.bodyFile !== undefined && (op !== "send" || out.body !== undefined || out.text !== undefined)) {
|
|
579
|
+
throw new ZellijError("usage", "--body-file requires send and cannot be combined with another body source");
|
|
580
|
+
}
|
|
530
581
|
return out;
|
|
531
582
|
}
|