ac-teams 0.0.4 → 0.0.6

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/docs/usage.md CHANGED
@@ -32,7 +32,11 @@ A task request is a proposal, not an instruction to create a task. Main receives
32
32
 
33
33
  The send receipt confirms delivery, not acceptance or task creation. This is agent guidance, not an automatic semantic duplicate detector.
34
34
 
35
- Team tool results and messages are compact by default. Ctrl+O expands them into labeled fields. UI state colors do not replace the underlying progress, ownership, or dependency fields.
35
+ Each team tool call uses one line with a status dot, tool name, action, and result preview. Bold, colored badges such as `[todo]` separate statuses from titles and names.
36
+
37
+ The `+N lines` hint shows the hidden detail count. Ctrl+O expands the full request and response JSON; mouse-enabled Pi views also support clicking the row.
38
+
39
+ Team notification messages remain compact; Ctrl+O expands their labeled fields. UI state colors do not replace the underlying progress, ownership, or dependency fields.
36
40
 
37
41
  ## Show tasks and configure the displays
38
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ac-teams",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Small, persistent teams for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,10 +15,12 @@
15
15
  "engines": { "node": ">=22.19.0" },
16
16
  "peerDependencies": {
17
17
  "@earendil-works/pi-coding-agent": "^1.4.0",
18
+ "@earendil-works/pi-tui": "^1.4.0",
18
19
  "typebox": "^1.0.0"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@earendil-works/pi-coding-agent": "1.4.0",
23
+ "@earendil-works/pi-tui": "1.4.0",
22
24
  "@types/node": "^22.0.0",
23
25
  "typebox": "^1.0.0",
24
26
  "typescript": "^5.9.0"
package/src/index.ts CHANGED
@@ -211,11 +211,6 @@ export default function teams(pi: ExtensionAPI) {
211
211
  await connection.open();
212
212
  await connection.request("join", { sessionId: ctx.sessionManager.getSessionId(), sessionFile: ctx.sessionManager.getSessionFile(), tools: pi.getAllTools().map(tool => tool.name), trusted: ctx.isProjectTrusted() });
213
213
  }
214
- if (ctx.mode === "tui") {
215
- try { Object.assign(display, readDisplaySettings()); }
216
- catch (error) { Object.assign(display, defaultDisplaySettings); warn(String(error)); }
217
- disposeUI?.(); disposeUI = installTeamFooter(pi, ctx, host(ctx), display);
218
- }
219
214
  startTelemetry(ctx);
220
215
  if (!warned) {
221
216
  warned = true;
@@ -223,6 +218,13 @@ export default function teams(pi: ExtensionAPI) {
223
218
  if (competitors.length) warn(`Other delegation tools are active (${competitors.map(tool => tool.name).join(", ")}). ac-teams limits do not cover them; disable competing launchers for a single team authority.`);
224
219
  }
225
220
  });
221
+ // CLI-loaded teammates run session_start before other extensions install their editors.
222
+ pi.on("resources_discover", (_event, ctx) => {
223
+ if (ctx.mode !== "tui") return;
224
+ try { Object.assign(display, readDisplaySettings()); }
225
+ catch (error) { Object.assign(display, defaultDisplaySettings); warn(String(error)); }
226
+ disposeUI?.(); disposeUI = installTeamFooter(pi, ctx, host(ctx), display);
227
+ });
226
228
  pi.on("session_shutdown", close);
227
229
  const switchGuard = () => {
228
230
  if (child || coordinator?.live().length) {
package/src/render.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { stripVTControlCharacters } from "node:util";
2
- import type { MessageRenderer, Theme, ToolRenderers } from "@earendil-works/pi-coding-agent";
3
- import { truncateToWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
2
+ import { keyHint, type MessageRenderer, type Theme, type ToolRenderers } from "@earendil-works/pi-coding-agent";
3
+ import { Container, Text, TruncatedText, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
4
4
 
5
5
  export const cleanText = (text: string): string => stripVTControlCharacters(text).replaceAll("\t", " ").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
6
6
  const inline = (value: unknown) => cleanText(String(value)).replace(/[\n\r\t]/g, " ");
@@ -46,18 +46,78 @@ function entry(title: string, value: unknown, expanded: boolean, theme: Theme, e
46
46
  };
47
47
  }
48
48
 
49
+ function preview(items: unknown[], count: string, theme: Theme): string {
50
+ const entries = items.slice(0, 3).map(item => toolSummary(item, theme)).filter(Boolean);
51
+ if (items.length > 3) entries.push(`+${items.length - 3} more`);
52
+ return [count, ...entries].join(" · ");
53
+ }
54
+
55
+ function toolSummary(data: unknown, theme: Theme): string {
56
+ if (Array.isArray(data)) return preview(data, `${data.length} messages`, theme);
57
+ if (data && typeof data === "object") {
58
+ const value = data as Record<string, unknown>;
59
+ if (Array.isArray(value.members)) return preview(value.members, `${value.members.length} members`, theme);
60
+ if (Array.isArray(value.tasks)) return preview(value.tasks, `${value.tasks.length} of ${value.total} tasks`, theme);
61
+ const label = value.title ?? value.name ?? value.brief ?? value.text ?? value.query ?? value.id;
62
+ const status = value.status === undefined ? "" : inline(value.status);
63
+ const color = status === "done" ? "success"
64
+ : status === "interrupted" ? "error"
65
+ : status === "busy" || status === "in_progress" ? "accent"
66
+ : status === "todo" || status === "starting" || status === "stopping" ? "warning" : "muted";
67
+ const parts = [status ? theme.fg(color, theme.bold(`[${status}]`)) : "", label === undefined ? "" : inline(label)];
68
+ if (Array.isArray(value.recipients)) parts.push(`to ${value.recipients.map(inline).join(", ")}`);
69
+ for (const key of ["seq", "limit", "depth"]) {
70
+ if (value[key] !== undefined) parts.push(`${key}: ${inline(value[key])}`);
71
+ }
72
+ return parts.filter(Boolean).join(" ");
73
+ }
74
+ return data === undefined || data === null ? "" : inline(data);
75
+ }
76
+
49
77
  export function teamToolRenderers(name: string): ToolRenderers {
50
78
  return {
79
+ renderShell: "self",
51
80
  renderCall(args, theme, context) {
52
- return entry(name, args, context.expanded, theme);
81
+ if (!context.isPartial) return new Container();
82
+ const { action, ...fields } = (args ?? {}) as Record<string, unknown>;
83
+ const request = toolSummary(fields, theme);
84
+ const text = `${theme.fg("warning", "●")} ${theme.fg("toolTitle", name)} · ${inline(action ?? "...")} → running${request ? ` · ${request}` : ""}`;
85
+ return new TruncatedText(text, context.outputPad, 0);
53
86
  },
54
87
  renderResult(result, options, theme, context) {
88
+ if (options.isPartial) return new Container();
89
+ const { action, ...fields } = (context.args ?? {}) as Record<string, unknown>;
90
+ const text = result.content.filter(block => block.type === "text").map(block => block.text).join("\n");
55
91
  let data: unknown = result.details;
56
92
  if (data === undefined) {
57
- const text = result.content.filter(part => part.type === "text").map(part => part.text).join("\n");
58
93
  try { data = JSON.parse(text); } catch { data = text; }
59
94
  }
60
- return entry(`${name} ${options.isPartial ? "receiving" : "result"}`, data, options.expanded, theme, context.isError);
95
+ const outcome = context.isError ? `error · ${inline(text)}` : toolSummary(data, theme) || "done";
96
+ const request = toolSummary(fields, theme);
97
+ const heading = `${theme.fg(context.isError ? "error" : "success", "●")} ${theme.fg("toolTitle", name)} · ${inline(action ?? "...")} → ${outcome}${request && !outcome.includes(request) ? ` · ${request}` : ""}`;
98
+ const requestJSON = cleanText(JSON.stringify(context.args, null, 2));
99
+ const responseJSON = cleanText(result.details === undefined ? text : JSON.stringify(result.details, null, 2));
100
+ const hiddenLines = requestJSON.split("\n").length + responseJSON.split("\n").length + 2;
101
+ const hint = theme.fg("muted", options.expanded ? " (" : ` … +${hiddenLines} lines (`)
102
+ + keyHint("app.tools.expand", options.expanded ? "to collapse" : "to expand") + theme.fg("muted", ")");
103
+ const row = new Container();
104
+ row.addChild({
105
+ invalidate() {},
106
+ render(width) {
107
+ const pad = " ".repeat(Math.min(context.outputPad, Math.floor(width / 2)));
108
+ const available = Math.max(0, width - pad.length * 2);
109
+ // Keep the hint visible unless the terminal is too narrow to also identify the tool.
110
+ const suffix = available >= visibleWidth(hint) + 30 ? hint : "";
111
+ return [pad + truncateToWidth(heading, available - visibleWidth(suffix)) + suffix + pad];
112
+ },
113
+ });
114
+ if (options.expanded) {
115
+ row.addChild(new Text(theme.fg("muted", "Request"), context.outputPad, 0));
116
+ row.addChild(new Text(requestJSON, context.outputPad, 0));
117
+ row.addChild(new Text(theme.fg("muted", "Response"), context.outputPad, 0));
118
+ row.addChild(new Text(responseJSON, context.outputPad, 0));
119
+ }
120
+ return row;
61
121
  },
62
122
  };
63
123
  }
@@ -94,7 +94,7 @@ export function installTeamFooter(_pi: ExtensionAPI, ctx: ExtensionContext, host
94
94
  }
95
95
  const next = await host.snapshot();
96
96
  if (disposed) return;
97
- members = next.members; tasks = next.tasks;
97
+ members = next.members.filter(isLive); tasks = next.tasks;
98
98
  const count = ids().length;
99
99
  ctx.ui.setStatus("ac-teams", `Teams ${[
100
100
  ...(display.agents ? [`${count} ${count === 1 ? "agent" : "agents"}`, `${members.filter(member => member.status === "busy").length} working`] : []),
@@ -126,14 +126,15 @@ export function installTeamFooter(_pi: ExtensionAPI, ctx: ExtensionContext, host
126
126
  const titleRow = theme.fg(color, `${selected === id ? ">" : " "} ${host.self === id ? "●" : "○"} ${cleanText(title).replaceAll("\n", " ")}`);
127
127
  const attention = member?.needsInput ? theme.fg("error", " · Needs input") : "";
128
128
  const description = ` ${id === "root" ? "" : status + " · "}${modelContext(metrics)}${assigned ? ` · ${taskName(assigned)}` : ""}${member?.error || member?.activity ? ` · ${member.error || member.activity}` : ""}`;
129
- return titleRow + attention + theme.fg(color, cleanText(description).replaceAll("\n", " "));
129
+ const shortcut = id === "root" && host.self !== "root" ? theme.fg("dim", " [Ctrl+Shift+Left]") : "";
130
+ return titleRow + shortcut + attention + theme.fg(color, cleanText(description).replaceAll("\n", " "));
130
131
  };
131
132
  const peers = ids().slice(1);
132
133
  return [
133
- ...(host.self !== "root" ? [theme.fg("dim", "Ctrl+Shift+Left: Main (keeps questions open)")] : []),
134
134
  ...(display.agents ? [row("root"), ...peers.slice(start, start + slots()).map(row),
135
- ...(peers.length > 4 ? [theme.fg("dim", ` ↑ ${start} above · ↓ ${Math.max(0, peers.length - start - slots())} below`)] : [])] : []),
136
- ].map(line => truncateToWidth(line, width));
135
+ ...(peers.length > 4 ? [theme.fg("dim", ` ↑ ${start} above · ↓ ${Math.max(0, peers.length - start - slots())} below`)] : [])] :
136
+ host.self !== "root" ? [row("root")] : []),
137
+ ].map(line => truncateToWidth(line, width, "..."));
137
138
  },
138
139
  invalidate() {},
139
140
  };