ac-teams 0.0.2 → 0.0.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/README.md CHANGED
@@ -26,9 +26,9 @@ Your Pi session is the root; its child agents are members. Ask Pi to delegate a
26
26
 
27
27
  `/teams` or `/teams panel` opens Tasks, Teammates, and Activity. `/teams teammate <name-or-id>` opens a teammate directly; subcommands and teammate names autocomplete. Use the task graph or list, send feedback, and open a member's full live Pi session.
28
28
 
29
- The task summary sits above the input. The agent selector below status shows descriptive names, models, and context usage. `/teams config` toggles these displays independently.
29
+ The task summary sits above the input. The agent selector sits below the input, above your existing footer, and shows descriptive names, models, and context usage. ac-teams publishes a compact team status without replacing the footer or editor supplied by other extensions. `/teams config` toggles these displays independently.
30
30
 
31
- From an empty editor, press Down to select the team footer, then Enter to open a member. Ctrl+Shift+Left returns to Main without stopping the member. `/teams stop` stops every descendant.
31
+ From an empty editor, press Down to select the team row, then Enter to open a member. Ctrl+Shift+Left returns to Main without stopping the member. `/teams stop` stops every descendant.
32
32
 
33
33
  Defaults allow up to **10 live members** and **two levels of delegation**. Use the fewest useful agents.
34
34
 
package/docs/usage.md CHANGED
@@ -12,11 +12,11 @@ In interactive Pi, `/teams` or `/teams panel` opens the team panel. `/teams team
12
12
 
13
13
  ## Navigate without stopping members
14
14
 
15
- The agent selector sits below Pi's status lines. It shows each member's name, state, model display name, and context usage. It keeps Main pinned and shows at most five rows, including an overflow indicator when needed. A finished assignment leaves its member idle and selectable.
15
+ The agent selector sits below the input editor, above the existing footer. It shows each member's name, state, model display name, and context usage. It keeps Main pinned and shows at most five rows, including an overflow indicator when needed. A finished assignment leaves its member idle and selectable.
16
16
 
17
17
  Context shows Pi's reported token estimate, window size, and percentage, for example `Claude Opus 4.6 · 48k / 200k (24%)`. Reports refresh about once a second when they change. Unknown usage stays marked unknown, not zero. Stopped members retain their last report. These are context estimates, not billing totals.
18
18
 
19
- - Press Down from an empty editor to focus Main in the footer. Up/Down select members; Enter opens the selected live session; Esc returns to editing.
19
+ - Press Down from an empty editor to focus Main in the agent selector. Up/Down select members; Enter opens the selected live session; Esc returns to editing.
20
20
  - Ctrl+Shift+Left returns from a member to Main. Switching preserves the member's process, session, and draft. Returning does not stop it.
21
21
  - `/teams` opens three tabs. Tab moves between Tasks, Teammates, and Activity; Esc closes the panel.
22
22
  - **Tasks:** Arrow keys select nodes in the dependency graph. G toggles graph/list, and Enter shows full details. Green means Done, red Blocked, cyan Working, and neutral Ready; written labels remain visible. Large or unsupported graphs fall back to the list.
@@ -32,7 +32,7 @@ Team tool results and messages are compact by default. Ctrl+O expands them into
32
32
 
33
33
  ## Show tasks and configure the displays
34
34
 
35
- The task summary sits above the input editor; the agent selector stays below the status lines. It shows completed/total counts and up to five numbered task labels, ordered by:
35
+ The task summary sits above the input editor; the agent selector sits below the editor. The task summary shows completed/total counts and up to five numbered task labels, ordered by:
36
36
 
37
37
  1. In-progress work
38
38
  2. Ready prerequisites blocking other tasks, with the most dependents first
@@ -41,6 +41,8 @@ The task summary sits above the input editor; the agent selector stays below the
41
41
 
42
42
  Ordinary ready tasks remain in `/teams`; the summary is not the full task list. Completion time determines recent order for newly completed tasks. Older completed tasks without timestamps fall back to task-number order.
43
43
 
44
+ ac-teams leaves your existing footer intact and publishes a compact summary under the `ac-teams` extension-status key. Custom footers must support extension statuses to show it. In Zentui, use `/zentui extensions` to choose a placement for `ac-teams` if statuses are hidden by default. Agent navigation remains available in its own row even when the footer hides statuses.
45
+
44
46
  Use `/teams config` to toggle **Task summary** and **Agent selector** independently. Up/Down select a setting; Space or Enter toggles it; Esc closes. Changes save immediately for the current and future sessions. Other running sessions are not updated automatically. Both displays default to On.
45
47
 
46
48
  Hiding a display does not stop agents, change tasks, or disable `/teams`. With the agent selector hidden, ordinary editor Down behavior returns. These global display preferences do not change the team's resource ceilings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ac-teams",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Small, persistent teams for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  import { spawn, execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { CustomEditor, getSettingsListTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { matchesKey, renderFakeCursor, truncateToWidth, visibleWidth, SettingsList } from "@earendil-works/pi-tui";
4
+ import { matchesKey, renderFakeCursor, truncateToWidth, visibleWidth, SettingsList, type EditorComponent, type Focusable } from "@earendil-works/pi-tui";
5
5
  import { cleanText } from "./render.ts";
6
6
  import { isLive, taskName, memberName } from "./types.ts";
7
7
  import { saveDisplaySettings, type DisplaySettings } from "./config.ts";
@@ -68,11 +68,11 @@ export async function showDisplayConfig(ctx: ExtensionContext, settings: Display
68
68
  }, { overlay: true, overlayOptions: { width: 64, minWidth: 30, anchor: "center" } });
69
69
  }
70
70
 
71
- export function installTeamFooter(pi: ExtensionAPI, ctx: ExtensionContext, host: PanelHost, display: DisplaySettings): () => void {
71
+ export function installTeamFooter(_pi: ExtensionAPI, ctx: ExtensionContext, host: PanelHost, display: DisplaySettings): () => void {
72
72
  let members: Awaited<ReturnType<PanelHost["snapshot"]>>["members"] = [];
73
73
  let tasks: Awaited<ReturnType<PanelHost["snapshot"]>>["tasks"] = [];
74
74
  let selected: string | undefined, start = 0;
75
- let editor: CustomEditor | undefined;
75
+ let editor: (EditorComponent & Partial<Focusable> & { isShowingAutocomplete?(): boolean }) | undefined;
76
76
  let redraw = () => {};
77
77
  let disposed = false;
78
78
  let timer: NodeJS.Timeout | undefined;
@@ -87,10 +87,19 @@ export function installTeamFooter(pi: ExtensionAPI, ctx: ExtensionContext, host:
87
87
  };
88
88
  async function refresh() {
89
89
  try {
90
- if (!display.tasks && !display.agents) return;
90
+ if (!display.tasks && !display.agents) {
91
+ ctx.ui.setStatus("ac-teams", undefined);
92
+ redraw();
93
+ return;
94
+ }
91
95
  const next = await host.snapshot();
92
96
  if (disposed) return;
93
97
  members = next.members; tasks = next.tasks;
98
+ const count = ids().length;
99
+ ctx.ui.setStatus("ac-teams", `Teams ${[
100
+ ...(display.agents ? [`${count} ${count === 1 ? "agent" : "agents"}`, `${members.filter(member => member.status === "busy").length} working`] : []),
101
+ ...(display.tasks ? [`${tasks.filter(task => task.status === "done").length}/${tasks.length} tasks done`] : []),
102
+ ].join(" · ")}`);
94
103
  if (selected && !ids().includes(selected)) { selected = undefined; if (editor) editor.focused = true; }
95
104
  start = Math.min(start, Math.max(0, ids().length - 1 - slots()));
96
105
  reveal(); redraw();
@@ -102,13 +111,11 @@ export function installTeamFooter(pi: ExtensionAPI, ctx: ExtensionContext, host:
102
111
  render: width => display.tasks ? renderTaskSummary(tasks, theme, width) : [],
103
112
  invalidate() {},
104
113
  }), { placement: "aboveEditor" });
105
- ctx.ui.setFooter((tui, theme, data) => {
114
+ ctx.ui.setWidget("ac-teams-agents", (tui, theme) => {
106
115
  redraw = () => tui.requestRender();
107
- const unsubscribe = data.onBranchChange(redraw);
108
116
  return {
109
117
  render(width) {
110
118
  if (!display.agents && selected !== undefined) { selected = undefined; if (editor) editor.focused = true; }
111
- const branch = data.getGitBranch(), usage = ctx.getContextUsage()?.percent;
112
119
  const row = (id: string) => {
113
120
  const member = members.find(member => member.id === id);
114
121
  const assigned = tasks.find(task => task.owner === id && task.status === "in_progress") ?? tasks.find(task => task.owner === id && task.status !== "done");
@@ -119,44 +126,49 @@ export function installTeamFooter(pi: ExtensionAPI, ctx: ExtensionContext, host:
119
126
  return theme.fg(selected === id || host.self === id ? "accent" : "muted", `${selected === id ? ">" : " "} ${host.self === id ? "●" : "○"} ${cleanText(description).replaceAll("\n", " ")}`);
120
127
  };
121
128
  const peers = ids().slice(1);
122
- return [theme.fg("dim", `${cleanText(ctx.cwd)}${branch ? ` (${cleanText(branch)})` : ""}`),
123
- theme.fg("dim", `${usage == null ? "Unknown" : usage.toFixed(1) + "%"} context · ${ctx.model?.id ?? "no model"} · ${pi.getThinkingLevel()}`),
124
- ...Array.from(data.getExtensionStatuses().values(), status => theme.fg("dim", cleanText(status))),
125
- ...(display.agents ? ["", row("root"), ...peers.slice(start, start + slots()).map(row),
126
- ...(peers.length > 4 ? [theme.fg("dim", ` ↑ ${start} above · ↓ ${Math.max(0, peers.length - start - slots())} below`)] : [])] : []),
127
- ].map(line => truncateToWidth(line, width));
129
+ return (display.agents ? [row("root"), ...peers.slice(start, start + slots()).map(row),
130
+ ...(peers.length > 4 ? [theme.fg("dim", ` ↑ ${start} above · ↓ ${Math.max(0, peers.length - start - slots())} below`)] : [])] : []
131
+ ).map(line => truncateToWidth(line, width));
128
132
  },
129
133
  invalidate() {},
130
- dispose() { disposed = true; clearTimeout(timer); unsubscribe(); },
131
134
  };
132
- });
133
- ctx.ui.setEditorComponent((tui, theme, keys) => {
134
- class TeamEditor extends CustomEditor {
135
- render(width: number) {
136
- const lines = super.render(width);
137
- return selected === undefined ? lines : lines.map(line => line.replace(renderFakeCursor(" "), " "));
135
+ }, { placement: "belowEditor" });
136
+ const previousEditor = ctx.ui.getEditorComponent();
137
+ const teamEditor: NonNullable<typeof previousEditor> = (tui, theme, keys) => {
138
+ const current: NonNullable<typeof editor> = previousEditor?.(tui, theme, keys) ?? new CustomEditor(tui, theme, keys);
139
+ // Decorate the instance so custom editor methods and Pi's app-control wiring survive.
140
+ const render = current.render.bind(current), handleInput = current.handleInput.bind(current);
141
+ current.render = (width: number) => {
142
+ const lines = render(width);
143
+ return disposed || selected === undefined ? lines : lines.map(line => line.replace(renderFakeCursor(" "), " "));
144
+ };
145
+ current.handleInput = (input: string) => {
146
+ if (disposed) { handleInput(input); return; }
147
+ if (!display.agents) { selected = undefined; current.focused = true; handleInput(input); return; }
148
+ if (selected !== undefined) {
149
+ const destinations = ids(), index = destinations.indexOf(selected);
150
+ if (matchesKey(input, "down")) selected = destinations[Math.min(destinations.length - 1, index + 1)];
151
+ else if (matchesKey(input, "up")) selected = index <= 0 ? undefined : destinations[index - 1];
152
+ else if (matchesKey(input, "escape")) selected = undefined;
153
+ else if (matchesKey(input, "enter")) {
154
+ const id = selected; selected = undefined;
155
+ void host.visit(id).catch(error => ctx.ui.notify(cleanText(String(error)), "error"));
156
+ } else { selected = undefined; handleInput(input); }
157
+ current.focused = selected === undefined; reveal(); redraw(); return;
138
158
  }
139
- handleInput(input: string) {
140
- if (!display.agents) { selected = undefined; this.focused = true; super.handleInput(input); return; }
141
- if (selected !== undefined) {
142
- const destinations = ids(), index = destinations.indexOf(selected);
143
- if (matchesKey(input, "down")) selected = destinations[Math.min(destinations.length - 1, index + 1)];
144
- else if (matchesKey(input, "up")) selected = index <= 0 ? undefined : destinations[index - 1];
145
- else if (matchesKey(input, "escape")) selected = undefined;
146
- else if (matchesKey(input, "enter")) {
147
- const id = selected; selected = undefined;
148
- void host.visit(id).catch(error => ctx.ui.notify(cleanText(String(error)), "error"));
149
- } else { selected = undefined; super.handleInput(input); }
150
- this.focused = selected === undefined; reveal(); redraw(); return;
151
- }
152
- if (matchesKey(input, "down") && !this.getText() && !this.isShowingAutocomplete()) {
153
- selected = "root"; start = 0; this.focused = false; redraw(); return;
154
- }
155
- super.handleInput(input);
159
+ if (matchesKey(input, "down") && !current.getText() && !current.isShowingAutocomplete?.()) {
160
+ selected = "root"; start = 0; current.focused = false; redraw(); return;
156
161
  }
157
- }
158
- editor = new TeamEditor(tui, theme, keys);
159
- return editor;
160
- });
161
- return () => { disposed = true; clearTimeout(timer); ctx.ui.setWidget("ac-teams-tasks", undefined); ctx.ui.setFooter(undefined); ctx.ui.setEditorComponent(undefined); };
162
+ handleInput(input);
163
+ };
164
+ editor = current;
165
+ return current;
166
+ };
167
+ ctx.ui.setEditorComponent(teamEditor);
168
+ return () => {
169
+ disposed = true; clearTimeout(timer);
170
+ ctx.ui.setWidget("ac-teams-tasks", undefined); ctx.ui.setWidget("ac-teams-agents", undefined);
171
+ ctx.ui.setStatus("ac-teams", undefined);
172
+ if (ctx.ui.getEditorComponent() === teamEditor) ctx.ui.setEditorComponent(previousEditor);
173
+ };
162
174
  }