opencode-herdr-orchestration 0.3.3 → 0.3.4
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/package.json +1 -1
- package/src/agents.js +11 -2
- package/src/prompts.js +179 -6
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -326,8 +326,16 @@ const GOVERNOR_SEND_KEYS_CTRL_C_ALLOWS = {
|
|
|
326
326
|
// pane IDs are opaque and labels are absent from scan plus split plus close
|
|
327
327
|
// command strings, while a `*Dev*` glob would overmatch legitimate
|
|
328
328
|
// `--cwd C:\Dev\...` values, so no such glob is added; the prompt plus docs
|
|
329
|
-
// architecture tab section exclusion stays primary with per-tab cap plus
|
|
330
|
-
// new-tab overflow plus never touch Dev in any tab; see docs residual.
|
|
329
|
+
// architecture tab section exclusion stays primary with per-tab cap plus
|
|
330
|
+
// new-tab overflow plus never touch Dev in any tab; see docs residual.
|
|
331
|
+
// Role-tab layout: dedicated tabs titled at creation via tab create label
|
|
332
|
+
// (`Sheepdog - <descriptor>`, `grazers`, `sheep`, `shearers` plus indexed
|
|
333
|
+
// overflow `grazers-2` etc.), never renamed (tab rename stays denied);
|
|
334
|
+
// shepherd caller tab never hosts worker panes; sheepdog stays alone;
|
|
335
|
+
// grazers plus sheep up to four panes in quadrant geometry (split right
|
|
336
|
+
// once then each side down once, parsing each ID from JSON); shearers up
|
|
337
|
+
// to two panes side by side; per-role overflow to new indexed role tabs;
|
|
338
|
+
// roles never mix within a role tab.
|
|
331
339
|
const SHEPHERD_PANE_ALLOWS = {
|
|
332
340
|
"herdr tab list*": "allow",
|
|
333
341
|
"herdr tab create*": "allow",
|
|
@@ -337,6 +345,7 @@ const SHEPHERD_PANE_ALLOWS = {
|
|
|
337
345
|
};
|
|
338
346
|
const GOVERNOR_PANE_ALLOWS = {
|
|
339
347
|
"herdr tab list*": "allow",
|
|
348
|
+
"herdr tab create*": "allow",
|
|
340
349
|
"herdr pane get*": "allow",
|
|
341
350
|
"herdr pane rename*": "allow",
|
|
342
351
|
"herdr agent rename*": "allow",
|
package/src/prompts.js
CHANGED
|
@@ -4,9 +4,37 @@
|
|
|
4
4
|
// Every spawning role carries the same shared sentences; role-specific
|
|
5
5
|
// ownership plus startup destinations differ per role. Leaves stay unchanged.
|
|
6
6
|
// Spawn plus response plus state matrices stay intact with no new spawn targets.
|
|
7
|
-
//
|
|
7
|
+
// Role tabs are created lazily on first need and reused thereafter.
|
|
8
8
|
export const PANE_CAP = 4;
|
|
9
9
|
export const DEV_PANE_LABELS = Object.freeze(["Dev", "Developer Terminal"]);
|
|
10
|
+
// Dedicated role-tab layout: per-role tabs titled at creation, never renamed.
|
|
11
|
+
// Caps per role tab stay within the hard four-pane cap: grazers 4, sheep 4,
|
|
12
|
+
// shearers 2 side by side, sheepdog alone. The shepherd caller tab is sacred
|
|
13
|
+
// and never hosts worker panes.
|
|
14
|
+
export const ROLE_TAB_CAPS = Object.freeze({
|
|
15
|
+
sheepdog: 1,
|
|
16
|
+
grazer: 4,
|
|
17
|
+
sheep: 4,
|
|
18
|
+
"shearer-low": 2,
|
|
19
|
+
"shearer-medium": 2,
|
|
20
|
+
});
|
|
21
|
+
export const ROLE_TAB_BASE_LABELS = Object.freeze({
|
|
22
|
+
sheepdog: "Sheepdog",
|
|
23
|
+
grazer: "grazers",
|
|
24
|
+
sheep: "sheep",
|
|
25
|
+
"shearer-low": "shearers",
|
|
26
|
+
"shearer-medium": "shearers",
|
|
27
|
+
});
|
|
28
|
+
export const ROLE_TAB_POLICY_SENTENCES = Object.freeze([
|
|
29
|
+
"dedicated role tabs are titled at creation via tab create label and never renamed.",
|
|
30
|
+
"Never place worker panes on the shepherd caller tab",
|
|
31
|
+
"Sheepdog stays alone in its own tab titled Sheepdog plus descriptor with no flock workers.",
|
|
32
|
+
"Grazer role tabs hold at most four panes in quadrant geometry with overflow to indexed grazers tabs.",
|
|
33
|
+
"Sheep role tabs hold at most four panes with overflow to indexed sheep tabs.",
|
|
34
|
+
"Shearer role tabs hold at most two panes side by side with overflow to indexed shearers tabs.",
|
|
35
|
+
"Four-pane quadrant geometry is split right once then split each side down once parsing each new pane ID from JSON",
|
|
36
|
+
"Never mix roles within a role tab.",
|
|
37
|
+
]);
|
|
10
38
|
export const PANE_POLICY_SHARED_SENTENCES = Object.freeze([
|
|
11
39
|
"at most four panes per tab including the caller pane",
|
|
12
40
|
"Reuse first within the four-pane cap per tab",
|
|
@@ -33,14 +61,17 @@ export const PANE_POLICY_SHARED_SENTENCES = Object.freeze([
|
|
|
33
61
|
export const PANE_POLICY_SHARED_PARAGRAPH = String.raw`
|
|
34
62
|
Pane layout policy (single normative policy, same wording as docs architecture tab section): Four-pane cap: at most four panes per tab including the caller pane. Reuse first within the four-pane cap per tab. Never split when the filtered count is already four; overflow to a new tab instead. Overflow to a new tab with indexed role labels when the cap binds. Grouped by role with indexed labels (Sheepdog, sheep-1, sheep-2, shearer-low-1, grazer-1); tabs keep their existing labels. Reuse the matching pane when found; split only when no reusable pane exists and the cap permits. Reuse is preferred over clutter: reuse a capacity-available managed pane for the same role before splitting. Never derive IDs from sidebar order or examples; parse them from JSON responses. The pane labeled Dev (Developer Terminal) is excluded from every scan plus split plus placement plus rename plus close plus reuse. Never count it toward the four-pane cap, never list it as a reuse candidate, never split from it or into it, never place a worker there, never rename it, never close it, and never reuse it for overflow. Filter it during scan by terminal_title plus terminal_title_stripped plus label before counting plus reusing; when only the Dev pane would satisfy reuse, treat reuse as absent and either split elsewhere within cap or overflow to a new tab or report STOP with preserved state. Never create a workspace to evade the cap. Never touch the Dev pane in any tab. Start in the calling pane and never rely on another client focused pane. If any primitive below is missing, reuse the current pane via --pane plus --current and report STOP naming the missing capability with preserved state. Pre-create default stays minimal: reuse the current pane via --pane plus --current when primitives are missing. On finish, reuse the pane for the next same-role assignment after confirming idle plus done. Six-step placement is reuse-first plus evidence-only with new-tab overflow. Dynamic placement follows the same single normative pane policy with new-tab overflow and no separate rulebook.
|
|
35
63
|
`.trim();
|
|
64
|
+
export const ROLE_TAB_POLICY_PARAGRAPH = String.raw`
|
|
65
|
+
Role-tab layout policy (single normative role-tab policy, same wording as docs architecture tab section): dedicated role tabs are titled at creation via tab create label and never renamed. Never place worker panes on the shepherd caller tab: scan it only for caller context, then place every worker in its role tab. Sheepdog stays alone in its own tab titled Sheepdog plus descriptor with no flock workers. Grazer role tabs hold at most four panes in quadrant geometry with overflow to indexed grazers tabs. Sheep role tabs hold at most four panes with overflow to indexed sheep tabs. Shearer role tabs hold at most two panes side by side with overflow to indexed shearers tabs. Four-pane quadrant geometry is split right once then split each side down once parsing each new pane ID from JSON; two-pane shearer geometry is split right once. Never mix roles within a role tab. Reuse first within the per-role cap after confirming idle plus done; split only when no reusable same-role pane exists and the per-role cap permits; overflow to a new indexed role tab when the per-role cap binds. Dev exclusion plus evidence-only ID parsing plus no focused-pane reliance apply in every role tab.
|
|
66
|
+
`.trim();
|
|
36
67
|
export const SHEPHERD_PANE_OWNERSHIP_PARAGRAPH = String.raw`
|
|
37
|
-
Shepherd pane ownership plus startup: shepherd manages
|
|
68
|
+
Shepherd pane ownership plus startup: shepherd manages scan plus placement plus rename plus tab create and never closes; starts its grazers in dedicated grazers role tabs and never places panes on the calling tab; sheepdog stays alone in its own tab; leaves gain no pane plus tab plus rename commands and stay unchanged.
|
|
38
69
|
`.trim();
|
|
39
70
|
export const GOVERNOR_PANE_OWNERSHIP_PARAGRAPH = String.raw`
|
|
40
|
-
Governor pane ownership plus startup: shepherd-governor manages
|
|
71
|
+
Governor pane ownership plus startup: shepherd-governor manages scan plus placement plus rename plus tab create and never closes flock panes; starts its sheepdog alone in its own tab titled Sheepdog plus descriptor and starts supplementary grazers in dedicated grazers role tabs; never places panes on the calling tab; leaves gain no pane plus tab plus rename commands and stay unchanged.
|
|
41
72
|
`.trim();
|
|
42
73
|
export const SHEEPDOG_PANE_OWNERSHIP_PARAGRAPH = String.raw`
|
|
43
|
-
Sheepdog pane ownership plus startup plus dynamic placement: sheepdog manages flock panes scan plus placement plus rename plus close plus tab create
|
|
74
|
+
Sheepdog pane ownership plus startup plus dynamic placement: sheepdog manages flock panes scan plus placement plus rename plus close plus tab create across dedicated role tabs; only the creator may rename plus close its panes and only after commits are integrated or otherwise preserved and the worktree is clean; stays alone in its own tab titled Sheepdog plus descriptor with no flock workers; starts grazers in grazers tabs up to four panes in quadrant geometry and sheep in sheep tabs up to four panes and shearers in shearers tabs up to two panes side by side with overflow to indexed role tabs when the per-role cap binds; never places panes on the shepherd calling tab and never mixes roles within a role tab; startup destinations for grazer plus sheep plus shearer-low plus shearer-medium worker categories stay within the per-role cap with Dev excluded in any tab and role tabs created lazily on first need; six-step placement is reuse-first plus evidence-only with new-tab overflow with cap check plus reuse check plus split plus rename plus tab create plus start; on cap go to a new indexed role tab and never split beyond the per-role cap; reuse capacity-available managed panes first within the cap and reuse the pane on finish after confirming idle plus done; never create a workspace to evade the cap and never touch the Dev pane in any tab.
|
|
44
75
|
`.trim();
|
|
45
76
|
export const SHEPHERD_PROMPT = String.raw`
|
|
46
77
|
You are shepherd, the planning authority of the flock. You research the user's goal through grazer workers and present an implementation-ready plan. You never implement, integrate, or deliver; execution and final delivery belong to shepherd-governor after the user approves your plan by selecting it.
|
|
@@ -87,6 +118,8 @@ Shepherd ownership and semantic synchronization: claim a validated target lifecy
|
|
|
87
118
|
|
|
88
119
|
${PANE_POLICY_SHARED_PARAGRAPH}
|
|
89
120
|
|
|
121
|
+
${ROLE_TAB_POLICY_PARAGRAPH}
|
|
122
|
+
|
|
90
123
|
${SHEPHERD_PANE_OWNERSHIP_PARAGRAPH}
|
|
91
124
|
`.trim();
|
|
92
125
|
|
|
@@ -125,6 +158,8 @@ herdr agent start <name> --kind opencode --pane <pane-id> -- --agent sheepdog
|
|
|
125
158
|
|
|
126
159
|
Use grazer for supplementary research and sheepdog for execution squads. Delegate to sheepdog using structured contracts containing, where relevant: task_id, plan_id, base_commit, objective, owned_paths, forbidden_paths, dependencies, acceptance_criteria, verification, escalate_if, and deliver. Resolve global ambiguity before delegating. Sheepdog spawns and supervises the leaves and performs clean local integration; you do not supervise leaves directly and never perform semantic review yourself. Workers must escalate rather than guess when evidence contradicts the task, scope expands, public APIs or migrations change unexpectedly, a product or architecture decision is required, permissions block work, or repeated attempts fail.
|
|
127
160
|
|
|
161
|
+
Construct delegation text content-safe: never carry raw separator characters even inside quotes, because Bash matchers deny any command containing them after the prompt allow and the prompt then fails closed; quote identifiers and split or rephrase delivery instead of embedding separators. For large contracts, write a Markdown brief or handoff and send a minimal content-safe prompt referencing plan_id plus task_id plus base_commit plus owned paths plus brief path; Sheepdog reads the authoritative plan via herdr_plan_read before acknowledging its contract.
|
|
162
|
+
|
|
128
163
|
Topology hardening: prompt only the grazer and sheepdog workers you spawned. Never prompt, wait on, re-prompt, retrieve, or supervise sheep or shearer workers directly, even as recovery when sheepdog is unavailable, blocked, or denied. Name-based prompt patterns cannot encode worker role, so prompt bans plus start denial plus the response matrix are the load-bearing layers. Start denial allows only grazer and sheepdog creation and the response matrix allows only grazer and sheepdog retrieval where text matchers cannot enforce role. If a denied lifecycle operation appears necessary, produce STOP plus an explicit configuration failure report naming the missing capability and never auto-fallback to direct sheep execution. Sheepdog remains the sole supervisor of leaves and the sole path for bounded recovery contracts.
|
|
129
164
|
|
|
130
165
|
Parallelize only through sheepdog squads that will not conflict; require dedicated branches and worktrees with non-overlapping ownership and an explicit integration order in every contract. Sheepdog owns worker worktrees: it inspects the existing worktree list, creates each worker branch and worktree from the approved base commit, treats worktrees created from other worktrees as peers sharing the same Git common repository rather than children, never nests a worker checkout inside another worktree, records worker name, branch, path, base commit, and owned scope before delegation, and removes only worktrees it created, only after their commits are integrated or otherwise preserved and the worktree is clean, never with force.
|
|
@@ -146,7 +181,7 @@ FINALIZE - all milestones are complete; the final report follows.
|
|
|
146
181
|
|
|
147
182
|
FINALIZE closes a task and is never an acknowledgement. CORRECT, REPLAN, and STOP are legal in both channels but mean before-starting in first-reply position and after-a-milestone in milestone position. When a reply keyword contradicts the expected phase, treat the response as invalid, keep the worker's state, and re-prompt with a corrected contract.
|
|
148
183
|
|
|
149
|
-
After grazer or sheepdog settles, use herdr_agent_response as the authoritative result channel. Call it first with the agent name, then call it with each returned cursor until complete is true. Do not summarize, integrate, or act on the result until every page has been read in order. Use herdr agent read only for live status, blocked dialogs, and stuck-worker diagnosis; terminal snapshots are never a completed response. Wait with a bounded poll loop on a short interval: poll herdr agent get <name> about every 10 seconds until settled or the safety timeout expires. working means continue polling; idle or done means retrieve via herdr_agent_response until complete is true; blocked means inspect with herdr agent get plus herdr agent read then decide under user safety constraints with never blind input. Start or prompt command failures surface immediately with their distinct structured code instead of decaying to timeout; disappearance (agent_not_found on get, vanished from herdr agent list) gets an explicit disappearance report with preserved state; safety timeout stays the final bound only and surfaces as WAIT_TIMEOUT_EXPIRED with preserved state. Retries stay bounded. You wait only on your direct grazer and sheepdog workers with this loop; routine flock waits belong to sheepdog with no per-transition Shepherd wakeups and you never wait on sheep or shearer workers directly. If an interrupted sheepdog has no completed response, inspect its actual partial state and re-contract the work. Retrying and re-contracting leaves belongs to sheepdog. Treat unknown as inconclusive, not complete.
|
|
184
|
+
After grazer or sheepdog settles, use herdr_agent_response as the authoritative result channel. Call it first with the agent name, then call it with each returned cursor until complete is true. Do not summarize, integrate, or act on the result until every page has been read in order. Use herdr agent read only for live status, blocked dialogs, and stuck-worker diagnosis; terminal snapshots are never a completed response. If herdr_agent_response is unavailable, returns UNAUTHORIZED_AGENT, or the tool is missing from the merged config, produce STOP plus an explicit configuration failure report naming the missing capability with preserved state and never accept herdr agent read or terminal scrollback as the result. If retrieval returns UNSUPPORTED_WORKER_ROLE, produce STOP naming the missing direct-retrieval capability with sheepdog remaining the sole path and never prompt sheep or shearers directly. Wait with a bounded poll loop on a short interval: poll herdr agent get <name> about every 10 seconds until settled or the safety timeout expires. working means continue polling; idle or done means retrieve via herdr_agent_response until complete is true; blocked means inspect with herdr agent get plus herdr agent read then decide under user safety constraints with never blind input. Start or prompt command failures surface immediately with their distinct structured code instead of decaying to timeout; disappearance (agent_not_found on get, vanished from herdr agent list) gets an explicit disappearance report with preserved state; safety timeout stays the final bound only and surfaces as WAIT_TIMEOUT_EXPIRED with preserved state. Retries stay bounded. You wait only on your direct grazer and sheepdog workers with this loop; routine flock waits belong to sheepdog with no per-transition Shepherd wakeups and you never wait on sheep or shearer workers directly. If an interrupted sheepdog has no completed response, inspect its actual partial state and re-contract the work. Retrying and re-contracting leaves belongs to sheepdog. Treat unknown as inconclusive, not complete.
|
|
150
185
|
|
|
151
186
|
If an agent is blocked, inspect it with herdr agent get and herdr agent read then decide; do not answer approvals or questions without applying the user's safety constraints and never blind input. Synthesize agent findings instead of forwarding raw reports. Resolve contradictions when repository evidence permits and surface unresolved product choices to the user.
|
|
152
187
|
|
|
@@ -160,6 +195,8 @@ Shepherd ownership and semantic synchronization: hand off the validated target l
|
|
|
160
195
|
|
|
161
196
|
${PANE_POLICY_SHARED_PARAGRAPH}
|
|
162
197
|
|
|
198
|
+
${ROLE_TAB_POLICY_PARAGRAPH}
|
|
199
|
+
|
|
163
200
|
${GOVERNOR_PANE_OWNERSHIP_PARAGRAPH}
|
|
164
201
|
|
|
165
202
|
${GOVERNOR_PROJECT_PERMISSION_SKILL_PARAGRAPH}
|
|
@@ -213,6 +250,8 @@ Raw Developer steering is never yours to read directly: the shepherd phases own
|
|
|
213
250
|
|
|
214
251
|
${PANE_POLICY_SHARED_PARAGRAPH}
|
|
215
252
|
|
|
253
|
+
${ROLE_TAB_POLICY_PARAGRAPH}
|
|
254
|
+
|
|
216
255
|
${SHEEPDOG_PANE_OWNERSHIP_PARAGRAPH}
|
|
217
256
|
`.trim();
|
|
218
257
|
|
|
@@ -265,7 +304,8 @@ Raw Developer steering is not yours: shepherd phases own raw check, read, consum
|
|
|
265
304
|
// need no broader spawn authority and no missing CLI primitives. They mirror
|
|
266
305
|
// the shared normative wording above: four-pane cap per tab, Dev exclusion
|
|
267
306
|
// in any tab, reuse first within cap, new-tab overflow with indexed role
|
|
268
|
-
// labels, startup destinations per tab,
|
|
307
|
+
// labels, startup destinations per tab, role tabs titled at creation with
|
|
308
|
+
// the caller tab sacred, plus per-role caps with quadrant geometry.
|
|
269
309
|
// Caller context stays via current pane ID and never relies on another
|
|
270
310
|
// client focused pane.
|
|
271
311
|
export function isDevPane(pane) {
|
|
@@ -372,3 +412,136 @@ export function sheepdogStartupDestination({
|
|
|
372
412
|
reason: placement.reason,
|
|
373
413
|
};
|
|
374
414
|
}
|
|
415
|
+
|
|
416
|
+
export function shepherdStartupDestination({
|
|
417
|
+
panes,
|
|
418
|
+
tabId,
|
|
419
|
+
existingLabels,
|
|
420
|
+
reuseCandidateId,
|
|
421
|
+
currentPaneId,
|
|
422
|
+
} = {}) {
|
|
423
|
+
const label = nextRoleLabel(existingLabels, "grazer");
|
|
424
|
+
const placement = decidePanePlacement({ panes, tabId, reuseCandidateId, currentPaneId });
|
|
425
|
+
return {
|
|
426
|
+
label,
|
|
427
|
+
action: placement.action,
|
|
428
|
+
paneId: placement.paneId,
|
|
429
|
+
reason: placement.reason,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function governorStartupDestination({
|
|
434
|
+
panes,
|
|
435
|
+
tabId,
|
|
436
|
+
roleCategory,
|
|
437
|
+
existingLabels,
|
|
438
|
+
reuseCandidateId,
|
|
439
|
+
currentPaneId,
|
|
440
|
+
} = {}) {
|
|
441
|
+
const category = typeof roleCategory === "string" && roleCategory.length > 0 ? roleCategory : "sheepdog";
|
|
442
|
+
const label = category === "grazer" ? nextRoleLabel(existingLabels, "grazer") : nextRoleLabel(existingLabels, "Sheepdog");
|
|
443
|
+
const placement = decidePanePlacement({ panes, tabId, reuseCandidateId, currentPaneId });
|
|
444
|
+
return {
|
|
445
|
+
label,
|
|
446
|
+
action: placement.action,
|
|
447
|
+
paneId: placement.paneId,
|
|
448
|
+
reason: placement.reason,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Role-tab placement helpers (pure, evidence-only, same policy).
|
|
453
|
+
// Role tabs are titled at creation and never renamed; the shepherd caller
|
|
454
|
+
// tab never hosts worker panes; sheepdog stays alone; grazers and sheep
|
|
455
|
+
// fill quadrant tabs up to four panes; shearers fill tabs up to two panes.
|
|
456
|
+
// Tabs overflow to indexed role tabs (grazers-2, sheep-2, shearers-2) when
|
|
457
|
+
// the per-role cap binds. Roles never mix within a role tab.
|
|
458
|
+
export function roleTabCapFor(roleCategory) {
|
|
459
|
+
if (roleCategory === "sheepdog") return ROLE_TAB_CAPS.sheepdog;
|
|
460
|
+
if (roleCategory === "grazer") return ROLE_TAB_CAPS.grazer;
|
|
461
|
+
if (roleCategory === "sheep") return ROLE_TAB_CAPS.sheep;
|
|
462
|
+
if (roleCategory === "shearer-low" || roleCategory === "shearer-medium") return ROLE_TAB_CAPS["shearer-low"];
|
|
463
|
+
return PANE_CAP;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function roleTabBaseLabel(roleCategory) {
|
|
467
|
+
if (roleCategory === "sheepdog") return ROLE_TAB_BASE_LABELS.sheepdog;
|
|
468
|
+
if (roleCategory === "grazer") return ROLE_TAB_BASE_LABELS.grazer;
|
|
469
|
+
if (roleCategory === "sheep") return ROLE_TAB_BASE_LABELS.sheep;
|
|
470
|
+
if (roleCategory === "shearer-low" || roleCategory === "shearer-medium") return ROLE_TAB_BASE_LABELS["shearer-low"];
|
|
471
|
+
return "flock";
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function nextRoleTabLabel(existingTabLabels, roleCategory) {
|
|
475
|
+
const base = roleTabBaseLabel(roleCategory);
|
|
476
|
+
const labels = Array.isArray(existingTabLabels) ? existingTabLabels : [];
|
|
477
|
+
if (!labels.includes(base)) return base;
|
|
478
|
+
let index = 2;
|
|
479
|
+
while (labels.includes(`${base}-${index}`)) index += 1;
|
|
480
|
+
return `${base}-${index}`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function roleTabGeometry(roleCategory) {
|
|
484
|
+
if (roleCategory === "shearer-low" || roleCategory === "shearer-medium") {
|
|
485
|
+
return { panes: 2, steps: ["split right once"] };
|
|
486
|
+
}
|
|
487
|
+
if (roleCategory === "sheepdog") {
|
|
488
|
+
return { panes: 1, steps: [] };
|
|
489
|
+
}
|
|
490
|
+
return { panes: 4, steps: ["split right once", "split each side down once parsing each new pane ID from JSON"] };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function sheepdogRoleTabDestination({
|
|
494
|
+
roleCategory,
|
|
495
|
+
tabs,
|
|
496
|
+
panes,
|
|
497
|
+
reuseCandidateId,
|
|
498
|
+
callerTabId,
|
|
499
|
+
} = {}) {
|
|
500
|
+
const category = typeof roleCategory === "string" && roleCategory.length > 0 ? roleCategory : "sheep";
|
|
501
|
+
const cap = Math.min(roleTabCapFor(category), PANE_CAP);
|
|
502
|
+
const tabList = Array.isArray(tabs) ? tabs : [];
|
|
503
|
+
const paneList = Array.isArray(panes) ? panes : [];
|
|
504
|
+
const byId = new Map(paneList.map((pane) => [pane.pane_id, pane]));
|
|
505
|
+
const candidate = reuseCandidateId === undefined ? undefined : byId.get(reuseCandidateId);
|
|
506
|
+
const candidateTab = candidate ? tabList.find((tab) => tab.tab_id === candidate.tab_id) : undefined;
|
|
507
|
+
const candidateLabel = candidateTab ? candidateTab.label : undefined;
|
|
508
|
+
const base = roleTabBaseLabel(category);
|
|
509
|
+
const candidateInRoleTab =
|
|
510
|
+
candidate !== undefined &&
|
|
511
|
+
!isDevPane(candidate) &&
|
|
512
|
+
typeof candidateLabel === "string" &&
|
|
513
|
+
(candidateLabel === base || candidateLabel.startsWith(`${base}-`)) &&
|
|
514
|
+
candidate.tab_id !== callerTabId;
|
|
515
|
+
const managedInRoleTab = (tabId) => managedPanesInTab(paneList, tabId).length;
|
|
516
|
+
const roomyRoleTab = tabList.find((tab) => {
|
|
517
|
+
if (tab.tab_id === callerTabId) return false;
|
|
518
|
+
if (!(tab.label === base || (typeof tab.label === "string" && tab.label.startsWith(`${base}-`)))) return false;
|
|
519
|
+
return managedInRoleTab(tab.tab_id) < cap;
|
|
520
|
+
});
|
|
521
|
+
if (candidateInRoleTab && managedInRoleTab(candidate.tab_id) <= cap) {
|
|
522
|
+
return {
|
|
523
|
+
tabLabel: candidateLabel,
|
|
524
|
+
action: managedInRoleTab(candidate.tab_id) >= cap ? "overflow-reuse" : "reuse",
|
|
525
|
+
paneId: reuseCandidateId,
|
|
526
|
+
reason:
|
|
527
|
+
managedInRoleTab(candidate.tab_id) >= cap
|
|
528
|
+
? "Reuse first within the per-role cap"
|
|
529
|
+
: "Reuse the matching pane when found in its role tab.",
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (roomyRoleTab) {
|
|
533
|
+
const count = managedInRoleTab(roomyRoleTab.tab_id);
|
|
534
|
+
return {
|
|
535
|
+
tabLabel: roomyRoleTab.label,
|
|
536
|
+
action: count === 0 ? "split" : "split",
|
|
537
|
+
paneId: null,
|
|
538
|
+
reason: "split only when no reusable same-role pane exists and the per-role cap permits.",
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
tabLabel: nextRoleTabLabel(tabList.map((tab) => tab.label), category),
|
|
543
|
+
action: "new-tab",
|
|
544
|
+
paneId: null,
|
|
545
|
+
reason: "Overflow to a new indexed role tab when the per-role cap binds.",
|
|
546
|
+
};
|
|
547
|
+
}
|