pi-straw-boss 0.6.9 → 0.6.10
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 +9 -3
- package/package.json +1 -1
- package/src/dashboard.ts +10 -5
- package/src/herdr.ts +15 -4
- package/src/index.ts +3 -3
- package/src/order.ts +17 -16
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Run a few agents on the same project and they start stepping on each other: two
|
|
|
19
19
|
- **A browser view** of the whole net, `/boss gui`: every workspace as a base with its tower, agents, and subagents, animated and read-only. Click a base to zoom in, click a unit to read its chat as a live comms log; units that wait on you queue up as alerts.
|
|
20
20
|
- **Broadcast** to a tab, a workspace, every tower, or every session.
|
|
21
21
|
- **Tidy tabs.** When a pi starts in a tab or quits out of it, the tab's panes are made equal again, so workers split off one by one don't leave a chain of halves.
|
|
22
|
-
- **A net view in Herdr's sidebar.** An optional plugin turns the Agents list into the net, towers first and each subagent under its launcher, on one key.
|
|
22
|
+
- **A net view in Herdr's sidebar.** An optional plugin turns the Agents list into the net, towers first and each subagent under its launcher, on by default and switched by one key.
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
@@ -36,6 +36,8 @@ You need pi running inside Herdr 0.9.4 or newer, and [pi-intercom](https://www.n
|
|
|
36
36
|
3. Type `/boss tower` to open the workspace's tower and watch its dashboard. Agents open it themselves when they call it.
|
|
37
37
|
4. Type `/boss gui` from any session to see the whole net in your browser.
|
|
38
38
|
|
|
39
|
+
The tower only routes and coordinates, so it can run a lighter model: set `towerModel` in `~/.pi/agent/straw-boss/config.json` ([how it works](docs/how-it-works.md#how-it-works)).
|
|
40
|
+
|
|
39
41
|
## Tools and commands
|
|
40
42
|
|
|
41
43
|
| Name | What it does |
|
|
@@ -55,10 +57,14 @@ Subagents and sessions in Herdr worktree workspaces never join: the session that
|
|
|
55
57
|
|
|
56
58
|
An optional Herdr plugin in [`herdr-plugin/`](herdr-plugin) turns the sidebar's Agents list into a net view.
|
|
57
59
|
It lists the head tower's workspace first, in each workspace the tower, then each tab's main agent and members, and every subagent right under the session that launched it, even in a worktree workspace.
|
|
58
|
-
|
|
59
|
-
pi-straw-boss writes the sort key (`sb_order`)
|
|
60
|
+
The view is on whenever the Herdr server starts, and one key switches it off and on again.
|
|
61
|
+
pi-straw-boss writes the sort key (`sb_order`), a role label (`sb_mark`) and, on subagents, a tree branch (`sb_indent`) the view and your sidebar rows can use.
|
|
60
62
|
It needs Herdr 0.9.3 or newer and node.
|
|
61
63
|
|
|
64
|
+
<p align="center"><img src="docs/sidebar-net-view.png" width="308" alt="Herdr's sidebar in the net view, one row per agent: each workspace's tower with the radio tower glyph first, main agents marked with a star and members with a person, and every subagent indented under the session that launched it"></p>
|
|
65
|
+
|
|
66
|
+
<p align="center"><sub>The net view with each agent's role mark. <a href="docs/how-it-works.md#sidebar-net-view">How it works</a> has the sidebar rows that show them.</sub></p>
|
|
67
|
+
|
|
62
68
|
Link it from a checkout, then bind a key in Herdr's `config.toml`:
|
|
63
69
|
|
|
64
70
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-straw-boss",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "Straw Boss coordination for pi in Herdr: sessions join a tab's net by choice, the first joiner is its main agent, a workspace tower opens on demand with a live monitoring dashboard, and boss_broadcast reaches the rest",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/dashboard.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import { headOf, type HerdrRun, isSub, type Pane, panes, parentOf, type Session, workspaces, type Workspace } from "./herdr.ts";
|
|
5
|
+
import { ARROW, headOf, type HerdrRun, isSub, MARK, type Pane, panes, parentOf, type Session, TOWER_LABEL, workspaces, type Workspace } from "./herdr.ts";
|
|
6
6
|
|
|
7
7
|
// The tower's monitoring screen: a widget above the editor, never focused, redrawn only while something is working.
|
|
8
8
|
//
|
|
@@ -19,7 +19,7 @@ export type Mark = "working" | "waiting" | "idle" | "done" | "error" | "unknown"
|
|
|
19
19
|
/** One line of the tree. `net` rows are tab (0) > session (1) > subagent (2); `towers` rows are flat. */
|
|
20
20
|
export interface Row { group: "net" | "towers"; depth: number; mark: Mark; text: string; kind?: string; note?: string; workspace?: string; id?: string; file?: string; pane?: string }
|
|
21
21
|
export interface Snapshot { title: string; mark: Mark; head: boolean; rows: Row[]; gui?: string }
|
|
22
|
-
export type Tone = "accent" | "warning" | "muted" | "success" | "dim" | "error" | "border" | "title";
|
|
22
|
+
export type Tone = "accent" | "warning" | "muted" | "success" | "dim" | "error" | "border" | "title" | "tower" | "main" | "member";
|
|
23
23
|
export type Paint = (tone: Tone, text: string) => string;
|
|
24
24
|
export interface Sources { run: HerdrRun; sessions(): Promise<Session[]>; workspaceId: string; now?: number; gui?(): Promise<string | undefined> }
|
|
25
25
|
|
|
@@ -29,6 +29,8 @@ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇",
|
|
|
29
29
|
const GLYPH: Record<Exclude<Mark, "working">, string> = { waiting: "?", idle: "○", done: "✓", error: "✗", unknown: "·" };
|
|
30
30
|
const TONE: Record<Mark, Tone> = { working: "accent", waiting: "warning", idle: "dim", done: "success", error: "error", unknown: "muted" };
|
|
31
31
|
const HERDR_MARK: Record<string, Mark> = { working: "working", blocked: "waiting", idle: "idle", done: "done" };
|
|
32
|
+
// A row's role mark goes before its name, the same glyph as Herdr's sidebar shows (src/herdr.ts); a tab or sub workspace has none.
|
|
33
|
+
const ROLE: Record<string, [Tone, string]> = { main: ["main", MARK.main], member: ["member", MARK.member], subagent: ["dim", ARROW], tower: ["tower", TOWER_LABEL], "head tower": ["tower", TOWER_LABEL] };
|
|
32
34
|
const RECENT_MS = 10 * 60_000;
|
|
33
35
|
const MAX_ROWS = 18;
|
|
34
36
|
const MAX_WIDTH = 76;
|
|
@@ -51,7 +53,8 @@ export function render({ title, mark, head, rows, gui }: Snapshot, frame: number
|
|
|
51
53
|
const last = (i: number) => i < 0 || shown.slice(i + 1).find((r) => r.group === shown[i]!.group && r.depth <= shown[i]!.depth)?.depth !== shown[i]!.depth;
|
|
52
54
|
const ancestor = (i: number, depth: number) => shown.findLastIndex((r, k) => k < i && r.group === shown[i]!.group && r.depth === depth);
|
|
53
55
|
const prefix = (i: number) => (shown[i]!.group === "net" ? Array.from({ length: shown[i]!.depth }, (_, d) => (last(ancestor(i, d)) ? " " : "│ ")).join("") + (last(i) ? "└─ " : "├─ ") : "");
|
|
54
|
-
const
|
|
56
|
+
const role = (kind?: string) => (kind && ROLE[kind] ? `${paint(...ROLE[kind]!)} ` : "");
|
|
57
|
+
const cells = shown.map((r, i) => paint("border", prefix(i)) + (r.mark === "unknown" && !r.kind ? "" : `${glyph(r.mark)} `) + role(r.kind) + r.text);
|
|
55
58
|
const [w1, w2] = [Math.max(...cells.map(visibleWidth), 0), Math.max(...shown.map((r) => r.kind?.length ?? 0))];
|
|
56
59
|
const line = (i: number) => `${pad(cells[i]!, w1)} ${paint("muted", (shown[i]!.kind ?? "").padEnd(w2))} ${paint("dim", shown[i]!.note ?? "")}`.trimEnd();
|
|
57
60
|
const section = (group: Row["group"], name: string, empty?: string) => {
|
|
@@ -61,7 +64,7 @@ export function render({ title, mark, head, rows, gui }: Snapshot, frame: number
|
|
|
61
64
|
// What this pane is, and where the browser view lives, come before the tree.
|
|
62
65
|
const intro = [paint("dim", "Control tower: main agents talk here (call_tower)."), gui ? `${paint("muted", "GUI")} ${paint("accent", gui)}` : paint("dim", "GUI not running; /boss gui opens it"), ""];
|
|
63
66
|
const body = [...intro, ...section("net", "this net", "nothing has joined yet"), ...section("towers", "other towers"), ...(rows.length > MAX_ROWS ? [paint("muted", `… ${rows.length - MAX_ROWS} more`)] : [])];
|
|
64
|
-
const [name, tag] = [` ${glyph(mark)} ${paint("title",
|
|
67
|
+
const [name, tag] = [` ${glyph(mark)} ${role("tower")}${paint("title", title)} `, head ? ` ${paint("accent", "head tower")} ` : ""];
|
|
65
68
|
const dashes = "─".repeat(Math.max(1, total - 4 - visibleWidth(name) - visibleWidth(tag)));
|
|
66
69
|
const frameLine = (text: string) => paint("border", "│ ") + pad(truncateToWidth(text, inner), inner) + paint("border", " │");
|
|
67
70
|
return [paint("border", "╭─") + name + paint("border", dashes) + tag + paint("border", "─╮"), ...body.map(frameLine), paint("border", `╰${"─".repeat(total - 2)}╯`)].map((l) => truncateToWidth(l, width));
|
|
@@ -181,7 +184,9 @@ export function startDashboard({ ui, collect: read, frameMs = 125, pollMs = 3000
|
|
|
181
184
|
};
|
|
182
185
|
live = { poke: () => void (Date.now() - last >= minGapMs && refresh()), stop };
|
|
183
186
|
void refresh();
|
|
184
|
-
|
|
187
|
+
// Every color is a theme role. The title and the role marks are bold: the tower in warning, a main agent in accent, a member in success.
|
|
188
|
+
const strong: Partial<Record<Tone, "accent" | "warning" | "success">> = { title: "accent", tower: "warning", main: "accent", member: "success" };
|
|
189
|
+
const paint: Paint = (tone, text) => (strong[tone] ? theme.bold(theme.fg(strong[tone], text)) : theme.fg(tone as Exclude<Tone, "title" | "tower" | "main" | "member">, text));
|
|
185
190
|
return { render: (width) => render(snap, frame, width, paint), invalidate() {}, dispose: stop };
|
|
186
191
|
});
|
|
187
192
|
return { poke: () => live?.poke(), stop: () => (ui.setWidget(KEY, undefined), live?.stop(), (live = undefined), undefined) };
|
package/src/herdr.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { createConnection } from "node:net";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -24,7 +25,7 @@ export interface HerdrTab { tab_id: string; workspace_id: string; label?: string
|
|
|
24
25
|
export interface Location { status: string; workspace?: { id: string; label: string }; tab?: { id: string; label: string }; paneId?: string }
|
|
25
26
|
/** A live pi-intercom session; `status` is idle, thinking, or tool:<name>. */
|
|
26
27
|
export interface Session { id: string; name?: string; status?: string; herdrLocation?: Location }
|
|
27
|
-
export interface TowerEnv { run: HerdrRun; sleep: Sleep; live(sessionId: string): Promise<boolean>; moveFirst(tabId: string): Promise<void
|
|
28
|
+
export interface TowerEnv { run: HerdrRun; sleep: Sleep; live(sessionId: string): Promise<boolean>; moveFirst(tabId: string): Promise<void>; model?: string }
|
|
28
29
|
|
|
29
30
|
const TTL_MS = 10 * 60_000;
|
|
30
31
|
const START_MS = 30_000;
|
|
@@ -50,8 +51,18 @@ async function result(run: HerdrRun, args: string[], timeoutMs?: number) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
/** The user's `towerModel` from `config.json` in the state directory, read at each tower start; anything unreadable means pi's default model. */
|
|
55
|
+
export function towerModel(stateDir: string): string | undefined {
|
|
56
|
+
let model: unknown;
|
|
57
|
+
try { model = (JSON.parse(readFileSync(join(stateDir, "config.json"), "utf8")) as { towerModel?: unknown }).towerModel; } catch { /* pi's default */ }
|
|
58
|
+
return typeof model === "string" && model.trim() ? model.trim() : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The tower tab is labelled with the Nerd Font radio tower glyph (md-radio_tower, U+F043B) alone. The role marks (the `sb_mark` token, and the
|
|
62
|
+
// dashboard's rows) are Font Awesome's star (U+F005) for a main agent and person (U+F007) for a member, and `[sub]` for a subagent.
|
|
54
63
|
export const TOWER_LABEL = "\u{f043b}";
|
|
64
|
+
export const MARK = { main: "\uf005", member: "\uf007" } as const;
|
|
65
|
+
export const ARROW = "[sub]";
|
|
55
66
|
|
|
56
67
|
export async function place(run: HerdrRun): Promise<Place> {
|
|
57
68
|
const pane = (await result(run, ["pane", "current"])).pane;
|
|
@@ -123,7 +134,7 @@ export function ensureTower(env: TowerEnv, workspace: Workspace, cwd: string) {
|
|
|
123
134
|
return pending;
|
|
124
135
|
}
|
|
125
136
|
|
|
126
|
-
async function open({ run, sleep, live, moveFirst }: TowerEnv, workspace: Workspace, cwd: string) {
|
|
137
|
+
async function open({ run, sleep, live, moveFirst, model }: TowerEnv, workspace: Workspace, cwd: string) {
|
|
127
138
|
const ws = workspace.workspace_id;
|
|
128
139
|
const held = async () => {
|
|
129
140
|
const id = (await workspaces(run)).find((w) => w.workspace_id === ws)?.tokens?.tower;
|
|
@@ -152,7 +163,7 @@ async function open({ run, sleep, live, moveFirst }: TowerEnv, workspace: Worksp
|
|
|
152
163
|
}
|
|
153
164
|
if (!running) {
|
|
154
165
|
// Herdr agent names are unique across workspaces and lowercase only, so the name carries the lowercased workspace id.
|
|
155
|
-
const start = ["agent", "start", `tower-${ws.toLowerCase()}`, "--kind", "pi", "--pane", paneId, "--timeout", String(START_MS), "--", "--name", `tower · ${workspace.label}
|
|
166
|
+
const start = ["agent", "start", `tower-${ws.toLowerCase()}`, "--kind", "pi", "--pane", paneId, "--timeout", String(START_MS), "--", "--name", `tower · ${workspace.label}`, ...(model ? ["--model", model] : [])];
|
|
156
167
|
// A new tab's shell may not be at its prompt yet: herdr answers agent_pane_busy until it is, and any other failure is final.
|
|
157
168
|
for (let attempt = 1; ; attempt++) {
|
|
158
169
|
try {
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { netSource } from "./net.ts";
|
|
|
12
12
|
import { clearOrder, markKids, netTokens } from "./order.ts";
|
|
13
13
|
import { copyText, pairingKeys, showLines } from "./pairing.ts";
|
|
14
14
|
import { type Run, switchRemote, tailscale } from "./remote.ts";
|
|
15
|
-
import { ensureTower, headOf, herdrApi, type HerdrApi, herdrCall, type HerdrCall, herdrRun, type HerdrRun, isSub, type Pane, panes, parentOf, place, type Place, report, type Session, type Sleep, waitFor, workspaces, type Workspace } from "./herdr.ts";
|
|
15
|
+
import { ensureTower, headOf, towerModel, herdrApi, type HerdrApi, herdrCall, type HerdrCall, herdrRun, type HerdrRun, isSub, type Pane, panes, parentOf, place, type Place, report, type Session, type Sleep, waitFor, workspaces, type Workspace } from "./herdr.ts";
|
|
16
16
|
|
|
17
17
|
// pi-intercom's extension API as constants and minimal types: intercom is a runtime requirement, not an import.
|
|
18
18
|
const REGISTER = "intercom:extension-register";
|
|
@@ -242,7 +242,7 @@ export function createStrawBoss({ run = herdrRun, api = herdrApi(), call = herdr
|
|
|
242
242
|
if (isTower && target.workspace_id === at.workspaceId) throw new Error("This session is the tower of that workspace");
|
|
243
243
|
const live = async (id: string) => (await c.listSessions()).some((s) => s.id === id);
|
|
244
244
|
const moveFirst = (tabId: string) => api("tab.move", { tab_id: tabId, insert_index: 0 }).catch((error: Error) => tell(`Straw Boss: could not move the tower tab first: ${error.message}`, "warning"));
|
|
245
|
-
return { target, routed: target !== asked ? asked : undefined, ...(await ensureTower({ run, sleep, live, moveFirst }, target, process.cwd())) };
|
|
245
|
+
return { target, routed: target !== asked ? asked : undefined, ...(await ensureTower({ run, sleep, live, moveFirst, model: towerModel(guiDir) }, target, process.cwd())) };
|
|
246
246
|
};
|
|
247
247
|
const status = async () => {
|
|
248
248
|
const sessions = await c.listSessions();
|
|
@@ -403,7 +403,7 @@ export function createStrawBoss({ run = herdrRun, api = herdrApi(), call = herdr
|
|
|
403
403
|
const held = holdsTower;
|
|
404
404
|
holdsTower = false;
|
|
405
405
|
if (!role() || !at) return;
|
|
406
|
-
await report(run, "pane", at.paneId, { sb_role: null, sb_order: null, sb_mark: null }).catch(() => {});
|
|
406
|
+
await report(run, "pane", at.paneId, { sb_role: null, sb_order: null, sb_mark: null, sb_indent: null }).catch(() => {});
|
|
407
407
|
await clearOrder(run, [...kids]);
|
|
408
408
|
if (held) await report(run, "workspace", at.workspaceId, { tower: null }).catch(() => {});
|
|
409
409
|
});
|
package/src/order.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { subagents } from "./dashboard.ts";
|
|
2
|
-
import { type HerdrRun, type HerdrTab, isSub, type Pane, panes, type Place, report, tabs, workspaces, type Workspace } from "./herdr.ts";
|
|
2
|
+
import { ARROW, type HerdrRun, type HerdrTab, isSub, MARK, type Pane, panes, type Place, report, tabs, workspaces, type Workspace } from "./herdr.ts";
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Three display tokens for the sidebar plugin and for Herdr's own Agents rows, written next to `sb_role`:
|
|
5
5
|
// - `sb_order`: a sort key that lays the net out as the head tower's workspace first, then the other workspaces in Herdr's order, in each the tower
|
|
6
6
|
// and then the tabs in tab order, in each tab the main agent and then the members, and every subagent right after the session that launched it,
|
|
7
7
|
// its own subagents after it in turn. Segments are fixed-width and `.`-separated, so plain string order is the numeric order of every segment,
|
|
8
8
|
// and a subagent's key is its parent's key plus one more segment, which sorts after the parent and before the parent's next sibling.
|
|
9
9
|
// - `sb_mark`: a short label that says what a net member is, a star alone for a main agent, a person (U+F007) alone for a member, and `[sub]` and the launcher's name for a subagent.
|
|
10
10
|
// A tower has none: its tab already carries the radio tower glyph.
|
|
11
|
+
// - `sb_indent`: a tree branch on a subagent alone, like Herdr's Spaces panel draws a worktree: `├─` while a later subagent of the same launcher is marked, `└─` on the last. A row put before the state icon shows it as the
|
|
12
|
+
// indent under the launcher. A tower, a main agent and a member carry none.
|
|
11
13
|
// A session writes them for itself and for the subagents it launched, which never join and write nothing, on the same 4 minute refresh as `sb_role`.
|
|
12
14
|
|
|
13
|
-
/**
|
|
14
|
-
export const
|
|
15
|
-
export const ARROW = "[sub]";
|
|
15
|
+
/** What `sb_indent` holds on a subagent: the branch to it from its launcher, and `last` for the youngest marked sibling. */
|
|
16
|
+
export const INDENT = { more: "\u251c\u2500", last: "\u2514\u2500" } as const;
|
|
16
17
|
// The dashboard shows eight subagents per session, but every live one is keyed: one left out would sort outside its launcher's span. Three digits count them.
|
|
17
18
|
const MAX_KIDS = 999;
|
|
18
19
|
const NAME_MAX = 24;
|
|
@@ -38,26 +39,26 @@ export function orderKey(role: "tower" | "main" | "member", pane: Pane, ps: Pane
|
|
|
38
39
|
*/
|
|
39
40
|
export function descendants(parent: Pane | undefined, key: string, name: string, ps: Pane[], now: number, seen = new Set<string>(parent ? [parent.pane_id] : [])): Kid[] {
|
|
40
41
|
if (!parent) return [];
|
|
41
|
-
return subagents(parent, ps, now, MAX_KIDS).flatMap((row, i) => {
|
|
42
|
+
return subagents(parent, ps, now, MAX_KIDS).flatMap((row, i, rows) => {
|
|
42
43
|
if (!row.pane || seen.has(row.pane)) return [];
|
|
43
44
|
seen.add(row.pane);
|
|
44
|
-
const order = `${key}.${part(i)}
|
|
45
|
-
return [{ pane: row.pane, order, mark: `${ARROW} ${short(name)}
|
|
45
|
+
const order = `${key}.${part(i)}`, indent = rows.slice(i + 1).some((r) => r.pane && !seen.has(r.pane)) ? INDENT.more : INDENT.last;
|
|
46
|
+
return [{ pane: row.pane, order, mark: `${ARROW} ${short(name)}`, indent }, ...descendants(ps.find((p) => p.pane_id === row.pane), order, row.text, ps, now, seen)];
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
/** The subagent panes a session marks: each gets `sb_order` and `
|
|
50
|
-
export interface Kid { pane: string; order: string; mark: string }
|
|
50
|
+
/** The subagent panes a session marks: each gets `sb_order`, `sb_mark` and `sb_indent`. */
|
|
51
|
+
export interface Kid { pane: string; order: string; mark: string; indent: string }
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
|
-
* What a session publishes besides its role: its own
|
|
54
|
-
*
|
|
54
|
+
* What a session publishes besides its role: its own tokens (null clears them when Herdr cannot place the session, a tower has no mark, and none
|
|
55
|
+
* has an indent, which clears the one a pane keeps from when it ran a subagent) and those of the subagents it launched. Reads Herdr only.
|
|
55
56
|
*/
|
|
56
57
|
export async function netTokens(run: HerdrRun, place: Place, role: "tower" | "main" | "member", name: string, now = Date.now()): Promise<{ own: Record<string, string | null>; kids: Kid[]; joined: Set<string> }> {
|
|
57
58
|
const [ps, all, tabList] = [await panes(run), await workspaces(run), await tabs(run, place.workspaceId)];
|
|
58
59
|
const pane = ps.find((p) => p.pane_id === place.paneId), key = pane && orderKey(role, pane, ps, all, tabList);
|
|
59
60
|
const joined = new Set(ps.filter((p) => p.tokens?.sb_role).map((p) => p.pane_id));
|
|
60
|
-
return { own: { sb_order: key ?? null, sb_mark: role === "tower" ? null : MARK[role] }, kids: key ? descendants(pane, key, name, ps, now) : [], joined };
|
|
61
|
+
return { own: { sb_order: key ?? null, sb_mark: role === "tower" ? null : MARK[role], sb_indent: null }, kids: key ? descendants(pane, key, name, ps, now) : [], joined };
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -65,11 +66,11 @@ export async function netTokens(run: HerdrRun, place: Place, role: "tower" | "ma
|
|
|
65
66
|
* (a shell where a subagent ran can start a pi that writes its own). Returns the panes now marked. A failed write waits for the next refresh.
|
|
66
67
|
*/
|
|
67
68
|
export async function markKids(run: HerdrRun, kids: Kid[], before: Set<string>, joined = new Set<string>()): Promise<Set<string>> {
|
|
68
|
-
for (const kid of kids) await report(run, "pane", kid.pane, { sb_order: kid.order, sb_mark: kid.mark }).catch(() => {});
|
|
69
|
+
for (const kid of kids) await report(run, "pane", kid.pane, { sb_order: kid.order, sb_mark: kid.mark, sb_indent: kid.indent }).catch(() => {});
|
|
69
70
|
const marked = new Set(kids.map((k) => k.pane));
|
|
70
71
|
await clearOrder(run, [...before].filter((id) => !marked.has(id) && !joined.has(id)));
|
|
71
72
|
return marked;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/** Removes
|
|
75
|
-
export const clearOrder = (run: HerdrRun, paneIds: string[]) => Promise.all(paneIds.map((id) => report(run, "pane", id, { sb_order: null, sb_mark: null }).catch(() => {})));
|
|
75
|
+
/** Removes the three tokens from panes, ignoring a pane that has already closed. */
|
|
76
|
+
export const clearOrder = (run: HerdrRun, paneIds: string[]) => Promise.all(paneIds.map((id) => report(run, "pane", id, { sb_order: null, sb_mark: null, sb_indent: null }).catch(() => {})));
|