@zswarm/core 0.1.4 → 0.1.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/README.md +4 -1
- package/dist/exec.d.ts +14 -1
- package/dist/exec.js +107 -14
- package/dist/harness.js +12 -7
- package/dist/index.d.ts +11 -10
- package/dist/index.js +8 -8
- package/dist/ops/bus.d.ts +16 -10
- package/dist/ops/bus.js +164 -98
- package/dist/ops/delivery.d.ts +3 -3
- package/dist/ops/delivery.js +4 -7
- package/dist/ops/dispatch.d.ts +7 -1
- package/dist/ops/dispatch.js +208 -68
- 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/routing.d.ts +13 -0
- package/dist/ops/routing.js +21 -0
- package/dist/ops/serve.d.ts +1 -0
- package/dist/ops/serve.js +7 -1
- 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 +231 -91
- package/dist/ops/types.d.ts +7 -1
- package/dist/ops/util.d.ts +4 -0
- package/dist/ops/util.js +7 -1
- package/dist/ops/wait.d.ts +8 -1
- package/dist/ops/wait.js +7 -3
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +50 -8
- package/dist/state.d.ts +7 -6
- package/dist/state.js +77 -17
- package/dist/zellij/args.d.ts +4 -0
- package/dist/zellij/args.js +1 -0
- package/dist/zellij/binary.d.ts +34 -0
- package/dist/zellij/binary.js +169 -8
- package/dist/zellij/bus.d.ts +11 -0
- package/dist/zellij/bus.js +22 -1
- package/dist/zellij/client.d.ts +23 -5
- package/dist/zellij/client.js +99 -33
- 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,114 @@ 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
|
-
// sinceLast trades the fixed 400ms window for "moved since your last call".
|
|
101
|
-
// The plugin remembers the previous screen, so there is no gap to wait out —
|
|
102
|
-
// but ask twice in quick succession and everything reads idle.
|
|
165
|
+
const live = targets.filter((pane) => !pane.exited);
|
|
103
166
|
if (isTrue(args.sinceLast) && supplied?.readChanged) {
|
|
104
|
-
const
|
|
167
|
+
const left = remaining();
|
|
168
|
+
const changed = left > 0 ? await supplied.readChanged(live.map((p) => p.id), left) : null;
|
|
169
|
+
throwIfAborted(signal);
|
|
105
170
|
if (changed) {
|
|
106
171
|
const peers = targets
|
|
107
172
|
.map((pane) => {
|
|
173
|
+
if (pane.exited) {
|
|
174
|
+
return peerEntry(pane, "exited", "", verbose);
|
|
175
|
+
}
|
|
108
176
|
const row = changed.get(pane.id);
|
|
109
|
-
const state =
|
|
110
|
-
? "
|
|
111
|
-
: row
|
|
177
|
+
const state = !row
|
|
178
|
+
? "unknown"
|
|
179
|
+
: row.changed
|
|
112
180
|
? "busy"
|
|
113
181
|
: promptHolds(row?.screen ?? "", resolveHarness(pane))
|
|
114
182
|
? "waiting"
|
|
115
183
|
: "idle";
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
184
|
+
return peerEntry(pane, state, row?.screen ?? "", verbose, {
|
|
185
|
+
...(row?.first ? { first: true } : {}),
|
|
186
|
+
});
|
|
126
187
|
})
|
|
127
188
|
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
128
189
|
return {
|
|
@@ -133,60 +194,139 @@ export async function peerStatus(client, args, clock, supplied) {
|
|
|
133
194
|
sampled: false,
|
|
134
195
|
sinceLast: true,
|
|
135
196
|
peers,
|
|
197
|
+
tabs: statusTabs(peers),
|
|
136
198
|
free: peers.filter((p) => p.state === "idle").map((p) => p.id),
|
|
137
199
|
},
|
|
138
200
|
};
|
|
139
201
|
}
|
|
140
202
|
}
|
|
141
203
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
204
|
+
* Bus path: one batched read per round, each bounded by the remaining budget.
|
|
205
|
+
* Direct path: each pane completes its own before→sleep→after pair under a
|
|
206
|
+
* shared concurrency limit so one stall does not block healthy peers.
|
|
145
207
|
*/
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
208
|
+
const samples = new Map();
|
|
209
|
+
if (supplied?.readScreens) {
|
|
210
|
+
const sampleRound = async () => {
|
|
211
|
+
throwIfAborted(signal);
|
|
212
|
+
const left = remaining();
|
|
213
|
+
if (left <= 0) {
|
|
214
|
+
return new Map(live.map((p) => [p.id, null]));
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const batched = await supplied.readScreens(live.map((p) => p.id), left);
|
|
218
|
+
throwIfAborted(signal);
|
|
219
|
+
if (!batched)
|
|
220
|
+
return new Map(); // signal fallback to dumps below
|
|
221
|
+
return new Map(live.map((p) => {
|
|
222
|
+
const text = batched.get(p.id);
|
|
223
|
+
return [
|
|
224
|
+
p.id,
|
|
225
|
+
text === undefined ? null : normalizeScreen(text),
|
|
226
|
+
];
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (isCancelledError(err) || signal?.aborted) {
|
|
231
|
+
throw new ZellijError("cancelled", "operation cancelled");
|
|
232
|
+
}
|
|
233
|
+
return new Map(live.map((p) => [p.id, null]));
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
let before = await sampleRound();
|
|
237
|
+
if (before.size === 0) {
|
|
238
|
+
// Bus declined — fall through to per-pane dumps.
|
|
239
|
+
before = new Map();
|
|
152
240
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
241
|
+
else {
|
|
242
|
+
let after = new Map();
|
|
243
|
+
if (remaining() > sampleMs) {
|
|
244
|
+
await clock.sleep(sampleMs);
|
|
245
|
+
throwIfAborted(signal);
|
|
246
|
+
after = await sampleRound();
|
|
247
|
+
}
|
|
248
|
+
for (const pane of live) {
|
|
249
|
+
samples.set(pane.id, {
|
|
250
|
+
before: before.get(pane.id) ?? null,
|
|
251
|
+
after: after.get(pane.id) ?? null,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
157
254
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
255
|
+
}
|
|
256
|
+
if (samples.size === 0) {
|
|
257
|
+
// Per-pane sample pairs under bounded concurrency.
|
|
258
|
+
await mapPool(live, STATUS_DUMP_CONCURRENCY, async (pane) => {
|
|
259
|
+
throwIfAborted(signal);
|
|
260
|
+
const dumpOne = async () => {
|
|
261
|
+
const left = remaining();
|
|
262
|
+
if (left <= 0)
|
|
263
|
+
return null;
|
|
264
|
+
try {
|
|
265
|
+
const dumped = await client.dumpPane({
|
|
266
|
+
session,
|
|
267
|
+
paneId: pane.id,
|
|
268
|
+
timeoutMs: left,
|
|
269
|
+
});
|
|
270
|
+
throwIfAborted(signal);
|
|
271
|
+
return normalizeScreen(dumped.text);
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (isCancelledError(err) || signal?.aborted) {
|
|
275
|
+
throw new ZellijError("cancelled", "operation cancelled");
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const before = await dumpOne();
|
|
281
|
+
throwIfAborted(signal);
|
|
282
|
+
let after = null;
|
|
283
|
+
// This pane's interval starts when its first screen is available. Queue
|
|
284
|
+
// time and a slow first read cannot replace time between observations.
|
|
285
|
+
// Without room for the whole interval, keep it unknown rather than idle.
|
|
286
|
+
if (before !== null && remaining() > sampleMs) {
|
|
287
|
+
await clock.sleep(sampleMs);
|
|
288
|
+
throwIfAborted(signal);
|
|
289
|
+
after = await dumpOne();
|
|
290
|
+
}
|
|
291
|
+
samples.set(pane.id, { before, after });
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
throwIfAborted(signal);
|
|
163
295
|
const peers = [];
|
|
296
|
+
let partial = false;
|
|
164
297
|
for (const pane of targets) {
|
|
165
|
-
|
|
166
|
-
|
|
298
|
+
if (pane.exited) {
|
|
299
|
+
peers.push(peerEntry(pane, "exited", "", verbose));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const pair = samples.get(pane.id);
|
|
303
|
+
const first = pair?.before ?? null;
|
|
304
|
+
const after = pair?.after ?? null;
|
|
305
|
+
if (first == null || after == null) {
|
|
306
|
+
partial = true;
|
|
307
|
+
peers.push(peerEntry(pane, "unknown", after ?? first ?? "", verbose));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
167
310
|
const state = classify({
|
|
168
|
-
exited:
|
|
311
|
+
exited: false,
|
|
169
312
|
before: first,
|
|
170
313
|
after,
|
|
171
314
|
profile: resolveHarness(pane),
|
|
172
315
|
});
|
|
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);
|
|
316
|
+
peers.push(peerEntry(pane, state, after, verbose));
|
|
185
317
|
}
|
|
186
318
|
peers.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
187
319
|
const free = peers.filter((p) => p.state === "idle").map((p) => p.id);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
320
|
+
const data = {
|
|
321
|
+
session,
|
|
322
|
+
source,
|
|
323
|
+
sampled: true,
|
|
324
|
+
sampleMs,
|
|
325
|
+
peers,
|
|
326
|
+
tabs: statusTabs(peers),
|
|
327
|
+
free,
|
|
191
328
|
};
|
|
329
|
+
if (partial || remaining() <= 0)
|
|
330
|
+
data.partial = true;
|
|
331
|
+
return { ok: true, data };
|
|
192
332
|
}
|
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";
|
|
@@ -19,6 +23,8 @@ export type DispatchDeps = {
|
|
|
19
23
|
state?: StateStore;
|
|
20
24
|
policy?: Policy;
|
|
21
25
|
env?: NodeJS.ProcessEnv;
|
|
26
|
+
/** MCP cancellation; aborted waits stop instead of running to timeout. */
|
|
27
|
+
signal?: AbortSignal;
|
|
22
28
|
};
|
|
23
29
|
export type Clock = {
|
|
24
30
|
now: () => number;
|
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"): {
|
|
@@ -45,4 +48,5 @@ export declare function numberArg(args: Record<string, unknown>, key: string, fa
|
|
|
45
48
|
min: number;
|
|
46
49
|
max: number;
|
|
47
50
|
}): number;
|
|
51
|
+
export declare function throwIfAborted(signal?: AbortSignal): void;
|
|
48
52
|
export declare function dumpMaxChars(args: Record<string, unknown>, fallback?: number): number;
|
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 {
|
|
@@ -73,6 +74,11 @@ export function numberArg(args, key, fallback, limits) {
|
|
|
73
74
|
}
|
|
74
75
|
return Math.min(limits.max, Math.max(limits.min, Math.floor(n)));
|
|
75
76
|
}
|
|
77
|
+
export function throwIfAborted(signal) {
|
|
78
|
+
if (signal?.aborted) {
|
|
79
|
+
throw new ZellijError("cancelled", "operation cancelled");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
76
82
|
export function dumpMaxChars(args, fallback = DEFAULT_DUMP_MAX_CHARS) {
|
|
77
83
|
if (args.max === undefined || args.max === null || args.max === "") {
|
|
78
84
|
return fallback;
|
package/dist/ops/wait.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ import type { Clock, OpsResult } from "./types.js";
|
|
|
5
5
|
export declare function buildMatcher(args: Record<string, unknown>): ((text: string) => boolean) | null;
|
|
6
6
|
/** Held-pipe wait answer, shaped by `parseWaitReply` in `zellij/bus.ts`. */
|
|
7
7
|
export type { BusWait };
|
|
8
|
+
/** Clamped timing the bus path must use — never the raw request numbers. */
|
|
9
|
+
export type WaitBusTiming = {
|
|
10
|
+
idleMs: number;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
pollMs: number;
|
|
13
|
+
full: boolean;
|
|
14
|
+
};
|
|
8
15
|
/**
|
|
9
16
|
* Poll a pane's screen until it goes quiet, prints a match, or the deadline
|
|
10
17
|
* passes — so callers stop re-dumping in a loop of their own.
|
|
@@ -16,4 +23,4 @@ export type { BusWait };
|
|
|
16
23
|
export declare function waitForPane(client: ZellijClient, target: {
|
|
17
24
|
session: string;
|
|
18
25
|
pane: ZellijPane;
|
|
19
|
-
}, args: Record<string, unknown>, clock: Clock, waitViaBus?: () => Promise<BusWait | null
|
|
26
|
+
}, args: Record<string, unknown>, clock: Clock, waitViaBus?: (timing: WaitBusTiming) => Promise<BusWait | null>, signal?: AbortSignal): Promise<OpsResult>;
|
package/dist/ops/wait.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
|
-
import { DEFAULT_WAIT_MAX_CHARS, dumpMaxChars, isTrue, normalizeScreen, numberArg, optionalString, truncateDumpText, } from "./util.js";
|
|
2
|
+
import { DEFAULT_WAIT_MAX_CHARS, dumpMaxChars, isTrue, normalizeScreen, numberArg, optionalString, throwIfAborted, truncateDumpText, } from "./util.js";
|
|
3
3
|
const WAIT_DEFAULTS = {
|
|
4
4
|
idleMs: 2_000,
|
|
5
5
|
pollMs: 150,
|
|
@@ -56,7 +56,7 @@ function waitResult(reason, ctx) {
|
|
|
56
56
|
* Null (plugin missing or silent) falls back to dump-screen polling. The
|
|
57
57
|
* pipe's own timeout belongs to the transport; this op does not cap it.
|
|
58
58
|
*/
|
|
59
|
-
export async function waitForPane(client, target, args, clock, waitViaBus) {
|
|
59
|
+
export async function waitForPane(client, target, args, clock, waitViaBus, signal) {
|
|
60
60
|
const { session, pane } = target;
|
|
61
61
|
const matcher = buildMatcher(args);
|
|
62
62
|
const requested = optionalString(args.for) ?? (matcher ? "match" : "idle");
|
|
@@ -72,8 +72,11 @@ export async function waitForPane(client, target, args, clock, waitViaBus) {
|
|
|
72
72
|
const pollMs = numberArg(args, "pollMs", WAIT_DEFAULTS.pollMs, WAIT_LIMITS.pollMs);
|
|
73
73
|
const timeoutMs = numberArg(args, "timeoutMs", WAIT_DEFAULTS.timeoutMs, WAIT_LIMITS.timeoutMs);
|
|
74
74
|
const started = clock.now();
|
|
75
|
+
const full = isTrue(args.full);
|
|
75
76
|
if (waitViaBus) {
|
|
76
|
-
|
|
77
|
+
throwIfAborted(signal);
|
|
78
|
+
const reply = await waitViaBus({ idleMs, timeoutMs, pollMs, full });
|
|
79
|
+
throwIfAborted(signal);
|
|
77
80
|
// `gone` is filtered upstream: a pane that vanished mid-wait is reported by
|
|
78
81
|
// the polling path, which has the error message for it.
|
|
79
82
|
if (reply && reply.reason !== "gone") {
|
|
@@ -96,6 +99,7 @@ export async function waitForPane(client, target, args, clock, waitViaBus) {
|
|
|
96
99
|
let polls = 0;
|
|
97
100
|
let changes = 0;
|
|
98
101
|
for (;;) {
|
|
102
|
+
throwIfAborted(signal);
|
|
99
103
|
const dumped = await client.dumpPane({
|
|
100
104
|
session,
|
|
101
105
|
paneId: pane.id,
|
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
|
};
|