@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/spawn.js
CHANGED
|
@@ -1,97 +1,163 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
2
|
import { createGitClient } from "../git.js";
|
|
3
3
|
import { tokenizeCommand } from "../keys.js";
|
|
4
|
-
import { isTrue, isVerbose, optionalString, paneViewFull } from "./util.js";
|
|
4
|
+
import { isTrue, isVerbose, numberArg, optionalString, paneViewFull, throwIfAborted } from "./util.js";
|
|
5
|
+
import { observationBudget } from "./observation.js";
|
|
5
6
|
import { ensurePeerWorktree } from "./worktree.js";
|
|
6
7
|
function paneDirection(value) {
|
|
7
|
-
const dir = optionalString(value);
|
|
8
|
+
const dir = optionalString(value)?.toLowerCase();
|
|
8
9
|
if (!dir)
|
|
9
10
|
return null;
|
|
10
|
-
|
|
11
|
-
if (!["right", "left", "up", "down"].includes(lowered)) {
|
|
11
|
+
if (!["right", "left", "up", "down"].includes(dir)) {
|
|
12
12
|
throw new ZellijError("bad_arg", "direction must be right|left|up|down");
|
|
13
13
|
}
|
|
14
|
-
return
|
|
14
|
+
return dir;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
export async function spawnPane(client, args, git) {
|
|
18
|
-
const
|
|
16
|
+
/** Create exactly once. Only observations are retried, never the create action. */
|
|
17
|
+
export async function spawnPane(client, args, git, clock = { now: Date.now, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) }, signal) {
|
|
18
|
+
const started = clock.now();
|
|
19
|
+
const budget = observationBudget(clock, numberArg(args, "timeoutMs", 30_000, { min: 1, max: 900_000 }), signal);
|
|
20
|
+
const observeMs = numberArg(args, "observeMs", 3_000, { min: 0, max: 30_000 });
|
|
19
21
|
const command = tokenizeCommand(args.command ?? args.cmd);
|
|
20
|
-
|
|
22
|
+
const direction = paneDirection(args.direction);
|
|
23
|
+
const newTab = isTrue(args.newTab) || args.tab === true;
|
|
24
|
+
const { session } = await client.resolveSession(optionalString(args.session), budget.require());
|
|
21
25
|
const worktree = optionalString(args.worktree)
|
|
22
26
|
? await ensurePeerWorktree(git ?? createGitClient(), args)
|
|
23
27
|
: null;
|
|
24
28
|
const cwd = worktree ? worktree.path : optionalString(args.cwd);
|
|
25
29
|
const name = optionalString(args.name) ?? worktree?.branch ?? null;
|
|
26
|
-
const
|
|
27
|
-
const before = new Set(
|
|
30
|
+
const beforePanes = await client.listPanes(session, budget.require(), { fresh: true });
|
|
31
|
+
const before = new Set(beforePanes.map((p) => p.id));
|
|
28
32
|
let paneId = null;
|
|
29
33
|
let tabId = null;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
command,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
layout: optionalString(args.layout),
|
|
37
|
-
closeOnExit,
|
|
38
|
-
});
|
|
39
|
-
tabId = created.tabId;
|
|
34
|
+
let tabSource = newTab ? "created" : "explicit";
|
|
35
|
+
if (newTab) {
|
|
36
|
+
tabId = (await client.newTab({
|
|
37
|
+
session, command, cwd, name, layout: optionalString(args.layout),
|
|
38
|
+
closeOnExit: isTrue(args.closeOnExit), timeoutMs: budget.require(),
|
|
39
|
+
})).tabId;
|
|
40
40
|
}
|
|
41
41
|
else {
|
|
42
42
|
const input = {
|
|
43
|
-
session,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
name,
|
|
47
|
-
direction: paneDirection(args.direction),
|
|
48
|
-
floating: isTrue(args.floating),
|
|
49
|
-
closeOnExit,
|
|
50
|
-
width: optionalString(args.width),
|
|
51
|
-
height: optionalString(args.height),
|
|
43
|
+
session, command, cwd, name, direction, floating: isTrue(args.floating),
|
|
44
|
+
closeOnExit: isTrue(args.closeOnExit),
|
|
45
|
+
width: optionalString(args.width), height: optionalString(args.height),
|
|
52
46
|
};
|
|
53
|
-
if (typeof args.tabId === "number")
|
|
54
|
-
input.tabId = args.tabId;
|
|
55
|
-
// `tab` as a name places the pane in that tab; ids come via tabId.
|
|
56
47
|
const tabName = optionalString(args.tab);
|
|
57
|
-
if (
|
|
58
|
-
|
|
48
|
+
if (typeof args.tabId === "number") {
|
|
49
|
+
tabId = args.tabId;
|
|
59
50
|
}
|
|
51
|
+
else if (tabName) {
|
|
52
|
+
tabId = client.resolveTab(await client.listTabs(session, budget.require(), { fresh: true }), tabName).id;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Address a tab explicitly even when no viewer is attached.
|
|
56
|
+
const focused = beforePanes.find((p) => p.focused && p.tabId != null);
|
|
57
|
+
const first = beforePanes.find((p) => !p.isPlugin && p.tabId != null);
|
|
58
|
+
tabId = focused?.tabId ?? first?.tabId ?? null;
|
|
59
|
+
tabSource = focused ? "focused-pane" : "first-tab";
|
|
60
|
+
if (tabId === null) {
|
|
61
|
+
const tabs = await client.listTabs(session, budget.require(), { fresh: true });
|
|
62
|
+
tabId = [...tabs].sort((a, b) => a.position - b.position)[0]?.id ?? null;
|
|
63
|
+
}
|
|
64
|
+
if (tabId === null)
|
|
65
|
+
throw new ZellijError("tab_not_found", "no tab for spawn; use --new-tab");
|
|
66
|
+
}
|
|
67
|
+
input.tabId = tabId;
|
|
68
|
+
input.timeoutMs = budget.require();
|
|
60
69
|
paneId = (await client.newPane(input)).paneId;
|
|
61
70
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
const observedUntil = Math.min(budget.deadline, clock.now() + observeMs);
|
|
72
|
+
let pane = null;
|
|
73
|
+
let resolvedBy = paneId ? "stdout" : "unresolved";
|
|
74
|
+
let observation = "timeout";
|
|
75
|
+
let diagnostic;
|
|
76
|
+
let renameAttempted = false;
|
|
77
|
+
let aliasObserved = !name;
|
|
78
|
+
const candidatesSeen = new Set();
|
|
79
|
+
let firstRead = true;
|
|
80
|
+
let candidateId = null;
|
|
81
|
+
try {
|
|
82
|
+
while (budget.remaining() > 0 && (firstRead || clock.now() < observedUntil)) {
|
|
83
|
+
firstRead = false;
|
|
84
|
+
try {
|
|
85
|
+
const left = observeMs === 0 ? budget.require() : Math.min(budget.require(), Math.max(1, observedUntil - clock.now()));
|
|
86
|
+
const panes = await client.listPanes(session, left, { fresh: true });
|
|
87
|
+
throwIfAborted(signal);
|
|
88
|
+
if (paneId) {
|
|
89
|
+
pane = panes.find((p) => p.id === paneId && !p.isPlugin) ?? null;
|
|
90
|
+
}
|
|
91
|
+
else if (newTab && tabId !== null) {
|
|
92
|
+
const candidates = panes.filter((p) => !p.isPlugin && !before.has(p.id) && p.tabId === tabId);
|
|
93
|
+
for (const candidate of candidates)
|
|
94
|
+
candidatesSeen.add(candidate.id);
|
|
95
|
+
if (candidatesSeen.size === 1 && candidates.length === 1) {
|
|
96
|
+
const candidate = candidates[0];
|
|
97
|
+
// A second observation catches a layout whose panes arrive in
|
|
98
|
+
// separate manifests. Never lock onto its first partial snapshot.
|
|
99
|
+
if (candidateId === candidate.id) {
|
|
100
|
+
pane = candidate;
|
|
101
|
+
paneId = pane.id;
|
|
102
|
+
resolvedBy = "tab";
|
|
103
|
+
}
|
|
104
|
+
candidateId = candidate.id;
|
|
105
|
+
}
|
|
106
|
+
else if (candidatesSeen.size > 1) {
|
|
107
|
+
observation = "ambiguous";
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// A global before/after diff cannot identify our concurrent creation.
|
|
113
|
+
observation = "unresolved";
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (pane) {
|
|
117
|
+
aliasObserved = !name || pane.title === name;
|
|
118
|
+
if (!aliasObserved && !renameAttempted) {
|
|
119
|
+
renameAttempted = true;
|
|
120
|
+
await client.renamePane({ session, paneId: pane.id, name: name, timeoutMs: observeMs === 0 ? budget.require() : Math.min(budget.require(), Math.max(1, observedUntil - clock.now())) });
|
|
121
|
+
throwIfAborted(signal);
|
|
122
|
+
}
|
|
123
|
+
else if (aliasObserved) {
|
|
124
|
+
observation = "observed";
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
throwIfAborted(signal);
|
|
131
|
+
if (err instanceof ZellijError && err.code === "cancelled")
|
|
132
|
+
throw err;
|
|
133
|
+
diagnostic = err instanceof Error ? err.message : String(err);
|
|
134
|
+
}
|
|
135
|
+
if (observeMs === 0 || clock.now() >= observedUntil)
|
|
136
|
+
break;
|
|
137
|
+
await budget.pause(observedUntil);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (signal?.aborted || (err instanceof ZellijError && err.code === "cancelled")) {
|
|
142
|
+
return { ok: false, error: {
|
|
143
|
+
code: "cancelled", message: "spawn was acknowledged; observation cancelled",
|
|
144
|
+
details: { session, paneId, tabId, created: true },
|
|
145
|
+
} };
|
|
77
146
|
}
|
|
147
|
+
throw err;
|
|
78
148
|
}
|
|
79
149
|
const data = {
|
|
80
|
-
session,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
150
|
+
session, paneId, tabId, tabSource, resolvedBy, created: true,
|
|
151
|
+
observed: pane !== null, exited: pane?.exited ?? null,
|
|
152
|
+
live: pane !== null && !pane.exited,
|
|
153
|
+
ready: pane?.exited ? false : "unknown",
|
|
154
|
+
observation: { status: observation, elapsedMs: clock.now() - started, ...(diagnostic ? { diagnostic } : {}) },
|
|
155
|
+
command: command.length ? command : null,
|
|
85
156
|
};
|
|
86
|
-
if (
|
|
87
|
-
data.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
created: worktree.created,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (tabId !== null)
|
|
94
|
-
data.tabId = tabId;
|
|
157
|
+
if (name)
|
|
158
|
+
data.alias = { name, observed: aliasObserved };
|
|
159
|
+
if (worktree)
|
|
160
|
+
data.worktree = { path: worktree.path, branch: worktree.branch, created: worktree.created };
|
|
95
161
|
if (pane && isVerbose(args))
|
|
96
162
|
data.pane = paneViewFull(pane);
|
|
97
163
|
return { ok: true, data };
|
package/dist/ops/status.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ZellijClient } from "../zellij/client.js";
|
|
|
3
3
|
import type { ZellijPane } from "../zellij/panes.js";
|
|
4
4
|
import type { Clock, OpsResult } from "./types.js";
|
|
5
5
|
/** `running` only appears when sampling is off — busy and idle are indistinguishable then. */
|
|
6
|
-
export type PeerState = "busy" | "waiting" | "idle" | "exited" | "running";
|
|
6
|
+
export type PeerState = "busy" | "waiting" | "idle" | "exited" | "running" | "unknown";
|
|
7
7
|
/** Pane list already in hand, so status does not re-fetch what the caller has. */
|
|
8
8
|
export type StatusSource = {
|
|
9
9
|
session: string;
|
|
@@ -12,25 +12,50 @@ export type StatusSource = {
|
|
|
12
12
|
/**
|
|
13
13
|
* Batched screen reader. Returns null when it cannot serve the whole set, and
|
|
14
14
|
* status falls back to one `dump-screen` per pane.
|
|
15
|
+
* `timeoutMs` is the remaining overall status budget.
|
|
15
16
|
*/
|
|
16
|
-
readScreens?: (paneIds: string[]) => Promise<Map<string, string> | null>;
|
|
17
|
+
readScreens?: (paneIds: string[], timeoutMs: number) => Promise<Map<string, string> | null>;
|
|
17
18
|
/** "Has this moved since you last asked?", answered without a sample gap. */
|
|
18
|
-
readChanged?: (paneIds: string[]) => Promise<Map<string, {
|
|
19
|
+
readChanged?: (paneIds: string[], timeoutMs: number) => Promise<Map<string, {
|
|
19
20
|
changed: boolean;
|
|
20
21
|
first: boolean;
|
|
21
22
|
screen: string;
|
|
22
23
|
}> | null>;
|
|
23
24
|
};
|
|
25
|
+
/** Default overall budget for a sampled status pass (covers IPC + dumps). */
|
|
26
|
+
export declare const DEFAULT_STATUS_TIMEOUT_MS = 30000;
|
|
27
|
+
/** How many dump-screen calls to run at once on the direct (non-bus) path. */
|
|
28
|
+
export declare const STATUS_DUMP_CONCURRENCY = 3;
|
|
24
29
|
export declare function lastLine(screen: string): string;
|
|
30
|
+
export type WaitingEvidence = {
|
|
31
|
+
reason: "approval_menu" | "prompt";
|
|
32
|
+
evidence: string;
|
|
33
|
+
source: "screen";
|
|
34
|
+
};
|
|
35
|
+
export declare function waitingPrompt(screen: string, profile?: HarnessProfile | null): WaitingEvidence | null;
|
|
36
|
+
/** Compact summary of the selected terminal peers, including inactive tabs. */
|
|
37
|
+
export declare function statusTabs(peers: Record<string, unknown>[]): {
|
|
38
|
+
id: unknown;
|
|
39
|
+
name: unknown;
|
|
40
|
+
panes: number;
|
|
41
|
+
states: Record<string, number>;
|
|
42
|
+
}[];
|
|
25
43
|
export declare function classify(input: {
|
|
26
44
|
exited: boolean;
|
|
27
45
|
before: string;
|
|
28
46
|
after: string;
|
|
29
|
-
/** The pane's harness; its waiting patterns name the prompts worth blocking on. */
|
|
30
47
|
profile?: HarnessProfile | null;
|
|
31
48
|
}): PeerState;
|
|
49
|
+
/** Run `fn` over items with at most `limit` in flight. */
|
|
50
|
+
export declare function mapPool<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
32
51
|
/**
|
|
33
52
|
* Sample every pane twice and say who is working, who is stuck on a prompt,
|
|
34
53
|
* and who is free — the routing question `list` cannot answer.
|
|
54
|
+
*
|
|
55
|
+
* `deadlineAt` is the absolute clock deadline for the whole status op (setup
|
|
56
|
+
* included). When omitted, one is derived from `timeoutMs` at entry.
|
|
35
57
|
*/
|
|
36
|
-
export declare function peerStatus(client: ZellijClient, args: Record<string, unknown>, clock: Clock, supplied?: StatusSource | null
|
|
58
|
+
export declare function peerStatus(client: ZellijClient, args: Record<string, unknown>, clock: Clock, supplied?: StatusSource | null, opts?: {
|
|
59
|
+
deadlineAt?: number;
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
}): Promise<OpsResult>;
|