pi-straw-boss 0.33.2 → 0.33.3
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/dashboard.ts +8 -8
- package/src/herdr.ts +1 -1
- package/src/tower.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-straw-boss",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.3",
|
|
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
|
@@ -4,7 +4,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import { ARROW, headOf, type HerdrRun, type HerdrWorld, isSub, MARK, type Pane, parentOf, type Session, TOWER_LABEL, type Workspace, worldOf } from "./herdr.ts";
|
|
6
6
|
|
|
7
|
-
// The tower's monitoring screen: a widget above the editor, never focused,
|
|
7
|
+
// The tower's monitoring screen: a widget above the editor, never focused, animated only while something is working and the tower's tab is on screen.
|
|
8
8
|
//
|
|
9
9
|
// Data sources, all read-only:
|
|
10
10
|
// - pi-intercom listSessions(): every live session, its status (idle, thinking, tool:<name>), and its Herdr location.
|
|
@@ -20,11 +20,11 @@ export type Mark = "working" | "waiting" | "idle" | "done" | "error" | "unknown"
|
|
|
20
20
|
/** One line of the tree. `net` rows are tab (0) > session (1) > subagent (2); `towers` rows are flat. */
|
|
21
21
|
/** `ended`: a subagent whose run is over (pi-herdr-agents recorded it done, or its pane is gone or back at a shell); its parent has the result. */
|
|
22
22
|
export interface Row { group: "net" | "towers"; depth: number; mark: Mark; text: string; kind?: string; note?: string; workspace?: string; id?: string; file?: string; pane?: string; ended?: boolean; finished?: boolean }
|
|
23
|
-
export interface Snapshot { title: string; mark: Mark; head: boolean; rows: Row[]; gui?: string }
|
|
23
|
+
export interface Snapshot { title: string; mark: Mark; head: boolean; rows: Row[]; gui?: string; visible?: boolean } // visible: the tower's tab is the active tab of the focused workspace (unknown without a tabId)
|
|
24
24
|
export type Tone = "accent" | "warning" | "muted" | "success" | "dim" | "error" | "border" | "title" | "tower" | "main" | "member";
|
|
25
25
|
export type Paint = (tone: Tone, text: string) => string;
|
|
26
26
|
/** `world` reads the panes and workspaces; without it they are listed through `run`. */
|
|
27
|
-
export interface Sources { run: HerdrRun; world?: HerdrWorld; sessions(): Promise<Session[]>; workspaceId: string; now?: number; gui?(): Promise<string | undefined> }
|
|
27
|
+
export interface Sources { run: HerdrRun; world?: HerdrWorld; sessions(): Promise<Session[]>; workspaceId: string; tabId?: string; now?: number; gui?(): Promise<string | undefined> }
|
|
28
28
|
|
|
29
29
|
const KEY = "straw-boss-dashboard";
|
|
30
30
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -187,7 +187,7 @@ export function subagents(parent: Pane | undefined, all: Pane[], now: number, li
|
|
|
187
187
|
* One reading of the world as rows: this tower's own net in detail (each tab's joined sessions with their subagents, then its
|
|
188
188
|
* sub workspaces), and one summary line for every other tower. Other workspaces' sessions, and sessions that did not join, are not listed.
|
|
189
189
|
*/
|
|
190
|
-
export async function collect({ run, world = worldOf(run), sessions, workspaceId, now = Date.now(), gui }: Sources): Promise<Snapshot> {
|
|
190
|
+
export async function collect({ run, world = worldOf(run), sessions, workspaceId, tabId, now = Date.now(), gui }: Sources): Promise<Snapshot> {
|
|
191
191
|
const [live, { panes: ps, workspaces: all }] = await Promise.all([sessions(), world()]);
|
|
192
192
|
const head = headOf(all);
|
|
193
193
|
const mine = all.find((w) => w.workspace_id === workspaceId);
|
|
@@ -215,7 +215,7 @@ export async function collect({ run, world = worldOf(run), sessions, workspaceId
|
|
|
215
215
|
for (const w of all.filter((x) => x !== mine && !isSub(x) && live.some((s) => s.id === x.tokens?.tower))) rows.push({ group: "towers", depth: 0, mark: markOf(w.agent_status), text: w.label, kind: w === head ? "head tower" : "tower" });
|
|
216
216
|
// The title shows what needs attention most: a waiting row, then a working one, else the workspace's own status.
|
|
217
217
|
const attention = (["waiting", "working"] as const).find((m) => rows.some((r) => r.mark === m || (m === "working" && r.mark === "commanding"))); // commanding is active, like working
|
|
218
|
-
return { title: mine?.label ?? workspaceId, mark: attention ?? markOf(mine?.agent_status), head: Boolean(mine) && mine === head, rows, gui: await gui?.().catch(() => undefined) };
|
|
218
|
+
return { title: mine?.label ?? workspaceId, mark: attention ?? markOf(mine?.agent_status), head: Boolean(mine) && mine === head, rows, gui: await gui?.().catch(() => undefined), visible: tabId === undefined ? undefined : Boolean(mine?.focused) && mine?.active_tab_id === tabId };
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
export interface Dashboard { poke(): void; stop(): void }
|
|
@@ -227,12 +227,12 @@ export const SAFETY_MS = 30_000;
|
|
|
227
227
|
const DEBOUNCE_MS = 500;
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
|
-
* Shows the dashboard in a tower's TUI. The spinner timer (
|
|
230
|
+
* Shows the dashboard in a tower's TUI. The spinner timer (1 fps; each tick redraws the whole TUI) runs only while a row is working and the tower is not `visible: false`. Data is re-read when `watch` reports
|
|
231
231
|
* a change (collected for `debounceMs`, so a burst is one read), when poke() says intercom changed (at once), and every `pollMs` as a safety net;
|
|
232
232
|
* two reads are never closer than `minGapMs`, and a request that comes inside the gap or while a read runs is read when it ends, not dropped.
|
|
233
233
|
* stop() clears the timers and the subscription and removes the widget, and pi disposing the widget does the same.
|
|
234
234
|
*/
|
|
235
|
-
export function startDashboard({ ui, collect: read, watch, frameMs =
|
|
235
|
+
export function startDashboard({ ui, collect: read, watch, frameMs = 1000, pollMs = SAFETY_MS, minGapMs = 500, debounceMs = DEBOUNCE_MS }: { ui: ExtensionContext["ui"]; collect(): Promise<Snapshot> } & Timing): Dashboard {
|
|
236
236
|
let live: Dashboard | undefined;
|
|
237
237
|
ui.setWidget(KEY, (tui, theme) => {
|
|
238
238
|
let snap: Snapshot = { title: "", mark: "unknown", head: false, rows: [] }, frame = 0, busy = false, again = false, stopped = false, last = 0, due = Infinity;
|
|
@@ -258,7 +258,7 @@ export function startDashboard({ ui, collect: read, watch, frameMs = 125, pollMs
|
|
|
258
258
|
busy = false;
|
|
259
259
|
last = Date.now();
|
|
260
260
|
if (stopped) return;
|
|
261
|
-
const working = snap.mark === "working" || snap.rows.some((r) => r.mark === "working");
|
|
261
|
+
const working = snap.visible !== false && (snap.mark === "working" || snap.rows.some((r) => r.mark === "working"));
|
|
262
262
|
if (working && !spinner) spinner = setInterval(() => (frame++, tui.requestRender()), frameMs).unref();
|
|
263
263
|
if (!working && spinner) spinner = void clearInterval(spinner);
|
|
264
264
|
tui.requestRender();
|
package/src/herdr.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class HerdrError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
export type Sleep = (ms: number) => Promise<void>;
|
|
21
21
|
export interface Place { paneId: string; tabId: string; workspaceId: string }
|
|
22
|
-
export interface Workspace { workspace_id: string; label: string; agent_status?: string; tokens?: Record<string, string>; worktree?: { checkout_path?: string; is_linked_worktree?: boolean; repo_key?: string } }
|
|
22
|
+
export interface Workspace { workspace_id: string; label: string; agent_status?: string; focused?: boolean; active_tab_id?: string; tokens?: Record<string, string>; worktree?: { checkout_path?: string; is_linked_worktree?: boolean; repo_key?: string } }
|
|
23
23
|
export interface Pane { pane_id: string; tab_id: string; workspace_id?: string; agent?: string; agent_status?: string; agent_session?: { value: string }; label?: string; cwd?: string; tokens?: Record<string, string> }
|
|
24
24
|
export interface HerdrTab { tab_id: string; workspace_id: string; label?: string }
|
|
25
25
|
export interface Location { status: string; workspace?: { id: string; label: string }; tab?: { id: string; label: string }; paneId?: string }
|
package/src/tower.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { ENTRY, type Kit } from "./state.ts";
|
|
|
7
7
|
import type { Tools } from "./tools.ts";
|
|
8
8
|
|
|
9
9
|
const SETTLE_MS = 2000;
|
|
10
|
-
// What Herdr reports that changes the dashboard: panes (their agent status and tokens), tabs, and workspaces (their tokens and labels), coming, going, and
|
|
11
|
-
const SCREEN_EVENTS = ["pane.created", "pane.closed", "pane.updated", "pane.moved", "pane.exited", "pane.agent_detected", "tab.created", "tab.closed", "tab.renamed", "tab.moved", "workspace.created", "workspace.updated", "workspace.metadata_updated", "workspace.renamed", "workspace.moved", "workspace.reordered", "workspace.closed"];
|
|
10
|
+
// What Herdr reports that changes the dashboard: panes (their agent status and tokens), tabs, and workspaces (their tokens and labels), coming, going, moving, and taking focus (the spinner runs only on screen).
|
|
11
|
+
const SCREEN_EVENTS = ["pane.created", "pane.closed", "pane.updated", "pane.moved", "pane.exited", "pane.agent_detected", "tab.created", "tab.closed", "tab.renamed", "tab.moved", "tab.focused", "workspace.focused", "workspace.created", "workspace.updated", "workspace.metadata_updated", "workspace.renamed", "workspace.moved", "workspace.reordered", "workspace.closed"];
|
|
12
12
|
|
|
13
13
|
/** A session that is a tower (`STRAW_BOSS_ROLE=tower`, or marked so in its session) takes the role: its marks, its tools, its dashboard. */
|
|
14
14
|
export function createTower({ pi, cfg, s, guiDir, me }: Kit, { membership, presence, tools }: { membership: Membership; presence: Presence; tools: Tools }) {
|
|
@@ -39,6 +39,6 @@ export function createTower({ pi, cfg, s, guiDir, me }: Kit, { membership, prese
|
|
|
39
39
|
presence.keepFresh();
|
|
40
40
|
await presence.publish();
|
|
41
41
|
// The dashboard lives exactly as long as this session holds the tower role.
|
|
42
|
-
if (s.hasUI) s.dashboard = startDashboard({ ui: s.ui!, watch: (refresh) => events(SCREEN_EVENTS, () => refresh()), collect: () => collect({ run, world, sessions: () => s.c.listSessions(), workspaceId: s.at.workspaceId, gui: async () => { const r = readRecord(guiDir); return r && (await answers(r)) ? urlOf(r) : undefined; } }) });
|
|
42
|
+
if (s.hasUI) s.dashboard = startDashboard({ ui: s.ui!, watch: (refresh) => events(SCREEN_EVENTS, () => refresh()), collect: () => collect({ run, world, sessions: () => s.c.listSessions(), workspaceId: s.at.workspaceId, tabId: s.at.tabId, gui: async () => { const r = readRecord(guiDir); return r && (await answers(r)) ? urlOf(r) : undefined; } }) });
|
|
43
43
|
};
|
|
44
44
|
}
|