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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-straw-boss",
3
- "version": "0.33.2",
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, redrawn only while something is working.
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 (8 fps) runs only while a row is working. Data is re-read when `watch` reports
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 = 125, pollMs = SAFETY_MS, minGapMs = 500, debounceMs = DEBOUNCE_MS }: { ui: ExtensionContext["ui"]; collect(): Promise<Snapshot> } & Timing): Dashboard {
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 moving.
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
  }