ac-teams 0.0.13 → 0.0.14
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/operations.md +24 -0
- package/package.json +1 -1
- package/src/coordinator.ts +36 -20
- package/src/index.ts +8 -7
- package/src/resources.ts +44 -0
- package/src/store.ts +15 -1
- package/src/terminal-ui.ts +1 -1
- package/src/ui-types.ts +1 -1
package/docs/operations.md
CHANGED
|
@@ -31,6 +31,30 @@ pi --teams-limit 4 --teams-depth 1
|
|
|
31
31
|
|
|
32
32
|
Other delegation tools remain outside these limits. See [companion tools](usage.md#companion-tools-add-guidance-or-terminal-management).
|
|
33
33
|
|
|
34
|
+
### Check headroom before adding concurrency
|
|
35
|
+
|
|
36
|
+
`team_agent list` and `limits` include a fresh `resources` advisory. Launch and teammate resume results include the sample taken before startup.
|
|
37
|
+
|
|
38
|
+
It reports host available RAM, occupied swap, recent memory stalls, and live/starting member counts. Values are bytes; Linux pressure averages are percentages over the last ten seconds. Missing or invalid metrics are `null` with a warning, not zero or an all-clear.
|
|
39
|
+
|
|
40
|
+
The check reads only `/proc/meminfo` and `/proc/pressure/memory` on demand. It adds no polling, process-tree scans, or subprocesses.
|
|
41
|
+
|
|
42
|
+
Available RAM below 10% triggers a low-headroom warning (a heuristic, not a per-member budget). Any nonzero ten-second memory-stall average also warns. Occupied swap alone does not trigger a pressure warning.
|
|
43
|
+
|
|
44
|
+
Starting members reserve count slots synchronously, but may not yet consume their full memory. Concurrent launch results warn about earlier starts.
|
|
45
|
+
|
|
46
|
+
Sequence launches and recheck `list` after startup before adding more. These advisories never block a launch, change the configured ceiling, reserve RAM, or stop a member. Host figures do not account for a tighter container/cgroup limit or guarantee future capacity; other teams and non-team processes also consume resources.
|
|
47
|
+
|
|
48
|
+
Idle members keep Pi and its extensions alive. Stop unneeded members to release their owned processes, then resume their saved sessions when needed.
|
|
49
|
+
|
|
50
|
+
This preserves transcripts, tasks, messages, and worktrees. Extension-owned language servers and scanners can dominate memory; ac-teams does not disable them or share them across sessions.
|
|
51
|
+
|
|
52
|
+
### Avoid repeated work without dropping updates
|
|
53
|
+
|
|
54
|
+
The coordinator skips saves when member events leave the state unchanged or notification cursors do not advance. Real changes still use the same validated, atomic saves. Repeated model errors still deliver separate parent notifications.
|
|
55
|
+
|
|
56
|
+
The footer refreshes at the same interval without fetching message history. The panel still loads messages and complete tasks. Connected sessions cache display names until the snapshot file changes; they do not retain a second copy of the history.
|
|
57
|
+
|
|
34
58
|
## Display preferences are global
|
|
35
59
|
|
|
36
60
|
`/teams config` saves task-summary and agent-selector visibility to `~/.pi/ac-teams/display.json`:
|
package/package.json
CHANGED
package/src/coordinator.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer, type Server, type Socket } from "node:net";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { promisify } from "node:util";
|
|
3
|
+
import { isDeepStrictEqual, promisify } from "node:util";
|
|
4
4
|
import { chmodSync, closeSync, constants, copyFileSync, existsSync, fstatSync, mkdirSync, openSync, lstatSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { homedir } from "node:os";
|
|
@@ -12,6 +12,7 @@ import { Egress, egressPort, providerRoutes, type Credentials } from "./egress.t
|
|
|
12
12
|
import { Store, processAlive, processIdentity } from "./store.ts";
|
|
13
13
|
import { stateRoot } from "./config.ts";
|
|
14
14
|
import { RpcAgent } from "./rpc-agent.ts";
|
|
15
|
+
import { hostResources, type HostResources } from "./resources.ts";
|
|
15
16
|
import { TerminalAgent, tmux } from "./terminal-agent.ts";
|
|
16
17
|
import { authentic, isRecord, readLines, send } from "./connection.ts";
|
|
17
18
|
import { validate } from "./schemas.ts";
|
|
@@ -52,6 +53,8 @@ export interface CoordinatorOptions {
|
|
|
52
53
|
interactive?: boolean; model?: string; thinking?: string;
|
|
53
54
|
/** Version of the running Pi; members use the image built for it. Defaults to the `pi` on PATH. */
|
|
54
55
|
piVersion?: string;
|
|
56
|
+
/** Test hook: samples host headroom without depending on the test machine's load. */
|
|
57
|
+
resources?: () => HostResources;
|
|
55
58
|
/** Test hook: provider token refresh interval (default one minute). */
|
|
56
59
|
refreshMs?: number;
|
|
57
60
|
/** Test hook: runs after a member container passes all checks and before it starts. */
|
|
@@ -270,21 +273,22 @@ export class Coordinator {
|
|
|
270
273
|
const event = value as { type: string; active?: boolean; text?: string; model?: string; modelName?: string; contextUsage?: ContextUsage | null };
|
|
271
274
|
const member = this.store.state.members[actor];
|
|
272
275
|
if (!member || !isLive(member) || (member.status === "stopping" && event.type !== "model_error")) return;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
276
|
+
const current = { ...member };
|
|
277
|
+
if (event.type === "blocked") { if (event.active) current.needsInput = true; else delete current.needsInput; }
|
|
278
|
+
else if (event.type === "telemetry") {
|
|
279
|
+
current.model = event.model ?? current.model;
|
|
280
|
+
current.modelName = event.modelName;
|
|
281
|
+
current.contextUsage = event.contextUsage ?? undefined;
|
|
282
|
+
}
|
|
283
|
+
else if (event.type === "agent_start") { current.status = "busy"; current.activity = "Working"; delete current.error; delete current.summary; }
|
|
284
|
+
else if (event.type === "agent_settled") { delete current.needsInput; current.status = "idle"; current.activity = current.error ? "Model request failed" : current.summary || "Available for follow-up"; }
|
|
285
|
+
else if (event.type === "activity") current.activity = event.text;
|
|
286
|
+
else if (event.type === "summary") current.summary = event.text?.slice(0, 500);
|
|
287
|
+
else if (event.type === "model_error") current.error = event.text || "Model request failed";
|
|
288
|
+
if (!isDeepStrictEqual(member, current)) {
|
|
289
|
+
this.store.update(next => { next.members[actor] = structuredClone(current); });
|
|
290
|
+
this.publishStatus();
|
|
291
|
+
}
|
|
288
292
|
if (event.type === "model_error" && !isMain(member) && member.status !== "stopping") {
|
|
289
293
|
const recipient = member.parent && isLive(this.store.state.members[member.parent]) ? member.parent : this.self;
|
|
290
294
|
this.store.message(actor, { action: "send", recipients: [recipient], text: `Member ${actor} model error: ${event.text || "Model request failed"}` });
|
|
@@ -347,10 +351,20 @@ export class Coordinator {
|
|
|
347
351
|
}
|
|
348
352
|
/** Live members this coordinator runs; main sessions run in their own terminals and never count. */
|
|
349
353
|
live(): Member[] { return Object.values(this.store.state.members).filter(member => !isMain(member) && isLive(member)); }
|
|
354
|
+
private resources() {
|
|
355
|
+
const sample = (this.options.resources ?? hostResources)();
|
|
356
|
+
const live = this.live();
|
|
357
|
+
const startingMembers = live.filter(member => member.status === "starting").length;
|
|
358
|
+
const warnings = [...sample.warnings];
|
|
359
|
+
if (startingMembers) warnings.push(`${startingMembers} teammate(s) still starting; their full memory cost is not yet reflected. Sequence launches and recheck team_agent list after startup.`);
|
|
360
|
+
return { ...sample, liveMembers: live.length, startingMembers, warnings };
|
|
361
|
+
}
|
|
350
362
|
ack(id: string, through: number): void {
|
|
351
363
|
const last = this.store.state.messages.at(-1)?.seq ?? 0;
|
|
352
364
|
if (!Number.isSafeInteger(through) || through < 0 || through > last) throw new Error("Invalid notification cursor");
|
|
353
|
-
this.
|
|
365
|
+
if (this.closed) throw new Error("Team is stopping");
|
|
366
|
+
if (through <= this.store.state.members[id].notified) return;
|
|
367
|
+
this.store.update(state => { state.members[id].notified = through; });
|
|
354
368
|
}
|
|
355
369
|
private notify(id: string): void {
|
|
356
370
|
if (this.closed || this.notices.has(id)) return;
|
|
@@ -388,12 +402,12 @@ export class Coordinator {
|
|
|
388
402
|
}
|
|
389
403
|
private async agent(actor: string, args: AgentArgs, authorizedLimit: boolean): Promise<unknown> {
|
|
390
404
|
const state = this.store.state;
|
|
391
|
-
if (args.action === "list") return { team: state.id, limit: state.limit, depth: state.depth, sandbox: this.sandboxStatus(), lastMessage: state.messages.at(-1)?.seq ?? 0, members: Object.values(state.members).map(member => this.publicMember(member)) };
|
|
405
|
+
if (args.action === "list") return { team: state.id, limit: state.limit, depth: state.depth, sandbox: this.sandboxStatus(), resources: this.resources(), lastMessage: state.messages.at(-1)?.seq ?? 0, members: Object.values(state.members).map(member => this.publicMember(member)) };
|
|
392
406
|
if (args.action === "limits") {
|
|
393
407
|
if (!isMain(state.members[actor])) throw new Error("Only a main session can change team limits");
|
|
394
408
|
if ((args.limit ?? state.limit) > 10 && !authorizedLimit) throw new Error("A ceiling above 10 requires explicit user confirmation or --teams-limit");
|
|
395
409
|
this.store.update(next => { next.limit = args.limit ?? next.limit; next.depth = args.depth ?? next.depth; });
|
|
396
|
-
return { limit: this.store.state.limit, depth: this.store.state.depth, sandbox: this.sandboxStatus() };
|
|
410
|
+
return { limit: this.store.state.limit, depth: this.store.state.depth, sandbox: this.sandboxStatus(), resources: this.resources() };
|
|
397
411
|
}
|
|
398
412
|
if (args.action === "launch" || args.action === "resume") {
|
|
399
413
|
const launch = this.launch(actor, args);
|
|
@@ -455,6 +469,8 @@ export class Coordinator {
|
|
|
455
469
|
const locked = resumed ? resumed.provider : parent.access?.network.allow ? parent.provider : undefined;
|
|
456
470
|
if (access?.network.allow && locked && provider !== locked) throw new Error(`A member with restricted network access can only use ${locked} models`);
|
|
457
471
|
const member: Member = resumed ? { ...resumed, token: randomUUID(), status: "starting", error: undefined, contextUsage: undefined, modelName: undefined, handover: undefined } : { id, name: args.name, role: "member", parent: actor, depth, token: randomUUID(), cwd, status: "starting", notified: 0, access, provider: access ? provider : undefined };
|
|
472
|
+
const resources = this.resources();
|
|
473
|
+
for (const warning of resources.warnings) this.options.warning(warning);
|
|
458
474
|
// Reserve synchronously before the first await so concurrent launches cannot oversubscribe.
|
|
459
475
|
this.store.update(next => { next.members[id] = member; });
|
|
460
476
|
this.publishStatus();
|
|
@@ -531,7 +547,7 @@ export class Coordinator {
|
|
|
531
547
|
this.store.update(next => { if (next.members[id].status === "starting") next.members[id].status = prompt ? "busy" : "idle"; });
|
|
532
548
|
this.publishStatus();
|
|
533
549
|
if (prompt) await agent.request("prompt", { message: args.brief, streamingBehavior: "followUp" });
|
|
534
|
-
return this.publicMember(this.store.state.members[id]);
|
|
550
|
+
return { ...this.publicMember(this.store.state.members[id]), resources };
|
|
535
551
|
} catch (error) {
|
|
536
552
|
clearTimeout(timer); this.ready.delete(id);
|
|
537
553
|
if (agent) await agent.stop();
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { randomUUID } from "node:crypto";
|
|
|
13
13
|
import { execFile } from "node:child_process";
|
|
14
14
|
import { existsSync, readdirSync } from "node:fs";
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
|
-
import { readState, teamsHome } from "./store.ts";
|
|
16
|
+
import { createMemberLabels, readState, teamsHome } from "./store.ts";
|
|
17
17
|
import { teamEntryType, type TeamEntry } from "./mains.ts";
|
|
18
18
|
import { isMain, memberName, taskName, type TeamState } from "./types.ts";
|
|
19
19
|
import { Coordinator, assistantEvent, taskRequestGuidance } from "./coordinator.ts";
|
|
@@ -48,10 +48,11 @@ export default function teams(pi: ExtensionAPI) {
|
|
|
48
48
|
const teamTools = ["team_agent", "team_task", "team_message"];
|
|
49
49
|
const memberId = () => child ? environment[1]! : team?.member ?? "root";
|
|
50
50
|
const teamId = (ctx: ExtensionContext) => team?.team ?? ctx.sessionManager.getSessionId();
|
|
51
|
+
const cachedLabels = createMemberLabels();
|
|
51
52
|
function memberLabels(): Record<string, string> {
|
|
52
53
|
try {
|
|
53
|
-
|
|
54
|
-
return
|
|
54
|
+
if (coordinator) return Object.fromEntries(Object.values(coordinator.store.state.members).map(member => [member.id, memberName(member)]));
|
|
55
|
+
return cachedLabels(child ? dirname(environment[0]!) : join(teamsHome(), teamId(context!)));
|
|
55
56
|
} catch { return { root: "Main" }; } // A transcript can render before its team is available.
|
|
56
57
|
}
|
|
57
58
|
pi.registerMessageRenderer("ac-teams", teamMessageRenderer(memberLabels));
|
|
@@ -196,7 +197,7 @@ export default function teams(pi: ExtensionAPI) {
|
|
|
196
197
|
}
|
|
197
198
|
return coordinator!.handle(coordinator!.self, operation, args, authorized);
|
|
198
199
|
}
|
|
199
|
-
async function snapshot(ctx: ExtensionContext): Promise<TeamSnapshot> {
|
|
200
|
+
async function snapshot(ctx: ExtensionContext, includeMessages = true): Promise<TeamSnapshot> {
|
|
200
201
|
if (!child && !opening && !connection) return { members: [{ id: "root", cwd: ctx.cwd, depth: 0, status: ctx.isIdle() ? "idle" : "busy", notified: 0, needsInput: inputCount > 0 }], tasks: [], messages: [], limit: 10, depth: 2 };
|
|
201
202
|
const status = await handle("agent", { action: "list" }, ctx) as Pick<TeamSnapshot, "members" | "limit" | "depth"> & { lastMessage: number };
|
|
202
203
|
const tasks: TaskView[] = [];
|
|
@@ -206,7 +207,7 @@ export default function teams(pi: ExtensionAPI) {
|
|
|
206
207
|
tasks.push(...page.tasks); total = page.total;
|
|
207
208
|
if (!page.tasks.length) break;
|
|
208
209
|
} while (tasks.length < total);
|
|
209
|
-
const messages = await handle("message", { action: "history", after: Math.max(0, status.lastMessage - 100), limit: 100 }, ctx) as Message[];
|
|
210
|
+
const messages = includeMessages ? await handle("message", { action: "history", after: Math.max(0, status.lastMessage - 100), limit: 100 }, ctx) as Message[] : [];
|
|
210
211
|
return { members: status.members, limit: status.limit, depth: status.depth, tasks, messages };
|
|
211
212
|
}
|
|
212
213
|
function host(ctx: ExtensionContext): PanelHost {
|
|
@@ -217,7 +218,7 @@ export default function teams(pi: ExtensionAPI) {
|
|
|
217
218
|
return await coordinator!.handle(coordinator!.self, "ui", value) as string;
|
|
218
219
|
};
|
|
219
220
|
const api: PanelHost = {
|
|
220
|
-
self: memberId(), child, drafts, snapshot:
|
|
221
|
+
self: memberId(), child, drafts, snapshot: includeMessages => snapshot(ctx, includeMessages),
|
|
221
222
|
visit: async id => {
|
|
222
223
|
if (id === memberId()) return;
|
|
223
224
|
if (!child) await link(ctx);
|
|
@@ -279,7 +280,7 @@ export default function teams(pi: ExtensionAPI) {
|
|
|
279
280
|
if (change) report(ctx, change);
|
|
280
281
|
});
|
|
281
282
|
function output(data: unknown) { return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data }; }
|
|
282
|
-
const guidance = "Team launches use real long-lived Pi sessions. Idle members count against the shared ceiling. Use teammate names for messaging and stable IDs for member control and tasks; query status and history instead of acknowledging notifications. Limits cover only ac-teams, not other launchers or shell commands.";
|
|
283
|
+
const guidance = "Team launches use real long-lived Pi sessions. Idle members count against the shared ceiling and retain their processes and extensions. Check team_agent list resources before adding concurrency; teammate launch/resume results include pre-launch host headroom. Resource warnings are advisory, not a memory reservation or capacity guarantee. Sequence launches while members are starting or memory is pressured; stop unneeded idle members and resume their saved sessions later. Use teammate names for messaging and stable IDs for member control and tasks; query status and history instead of acknowledging notifications. Limits cover only ac-teams, not other launchers or shell commands.";
|
|
283
284
|
const taskTrackingGuidance = "Before delegating a deliverable through team_agent, create or reuse a team_task and include its ID in the member's brief. Use one shared task list for your own work and delegated work; do not create one task per teammate by default. Quick lookups and brief second opinions need no separate task unless their results need coordination or follow-up.";
|
|
284
285
|
pi.registerTool({
|
|
285
286
|
name: "team_agent", label: "Team agent", description: "Launch/resume Pi members, list/status, stop owned subtrees, change root limits, and explicitly clean stopped worktrees. Create or reuse team_task entries for delegated deliverables; quick lookups need no task unless results need coordination or follow-up.",
|
package/src/resources.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface HostResources {
|
|
4
|
+
scope: "host";
|
|
5
|
+
memory: { totalBytes: number; availableBytes: number; swapUsedBytes: number } | null;
|
|
6
|
+
/** Percentage of wall time stalled on memory over the last ten seconds (Linux PSI). */
|
|
7
|
+
memoryPressure: { someAvg10: number; fullAvg10: number } | null;
|
|
8
|
+
warnings: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** On-demand advisory only: no process walks, subprocesses, timers, or per-member budget. */
|
|
12
|
+
export function hostResources(read: (path: string) => string = path => readFileSync(path, "utf8"), platform: string = process.platform): HostResources {
|
|
13
|
+
const result: HostResources = { scope: "host", memory: null, memoryPressure: null, warnings: [] };
|
|
14
|
+
if (platform === "linux") {
|
|
15
|
+
try {
|
|
16
|
+
const text = read("/proc/meminfo");
|
|
17
|
+
const bytes = (key: string) => {
|
|
18
|
+
const match = text.match(new RegExp(`^${key}:\\s+(\\d+) kB$`, "m"));
|
|
19
|
+
return match ? Number(match[1]) * 1024 : NaN;
|
|
20
|
+
};
|
|
21
|
+
const totalBytes = bytes("MemTotal"), availableBytes = bytes("MemAvailable");
|
|
22
|
+
const swapTotal = bytes("SwapTotal"), swapFree = bytes("SwapFree");
|
|
23
|
+
if ([totalBytes, availableBytes, swapTotal, swapFree].every(Number.isSafeInteger) && totalBytes > 0 && availableBytes <= totalBytes && swapFree <= swapTotal) {
|
|
24
|
+
result.memory = { totalBytes, availableBytes, swapUsedBytes: swapTotal - swapFree };
|
|
25
|
+
if (availableBytes < totalBytes * 0.1) result.warnings.push("Host available memory is below 10%. Sequence work; stop unneeded idle members and resume their saved sessions later.");
|
|
26
|
+
}
|
|
27
|
+
} catch { /* Missing or unreadable metrics remain explicitly unavailable. */ }
|
|
28
|
+
try {
|
|
29
|
+
const text = read("/proc/pressure/memory");
|
|
30
|
+
const avg = (kind: string) => {
|
|
31
|
+
const match = text.match(new RegExp(`^${kind} avg10=(\\d+(?:\\.\\d+)?) `, "m"));
|
|
32
|
+
return match ? Number(match[1]) : NaN;
|
|
33
|
+
};
|
|
34
|
+
const someAvg10 = avg("some"), fullAvg10 = avg("full");
|
|
35
|
+
if ([someAvg10, fullAvg10].every(value => Number.isFinite(value) && value >= 0 && value <= 100) && fullAvg10 <= someAvg10) {
|
|
36
|
+
result.memoryPressure = { someAvg10, fullAvg10 };
|
|
37
|
+
if (someAvg10 > 0) result.warnings.push(`Host tasks stalled on memory for ${someAvg10}% of the last ten seconds. Prefer sequential work over more teammates.`);
|
|
38
|
+
}
|
|
39
|
+
} catch { /* PSI is optional on older kernels. */ }
|
|
40
|
+
}
|
|
41
|
+
if (!result.memory) result.warnings.push("Host memory headroom is unavailable; do not assume another teammate fits.");
|
|
42
|
+
if (!result.memoryPressure) result.warnings.push("Host memory pressure is unavailable; recent stalls cannot be assessed.");
|
|
43
|
+
return result;
|
|
44
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { stateRoot } from "./config.ts";
|
|
5
5
|
import { validatePolicy } from "./egress.ts";
|
|
6
6
|
import { stripVTControlCharacters } from "node:util";
|
|
7
|
-
import { isDescendant, isLive, isMain, memberNamePattern, memberNameMaxLength, taskLabelMaxLength, taskLabelPattern } from "./types.ts";
|
|
7
|
+
import { isDescendant, isLive, isMain, memberName, memberNamePattern, memberNameMaxLength, taskLabelMaxLength, taskLabelPattern } from "./types.ts";
|
|
8
8
|
import type { TeamState, TaskArgs, MessageArgs, Task, TaskView, Message, Worktree } from "./types.ts";
|
|
9
9
|
|
|
10
10
|
// UI envelopes add task context after the public 16,000-character input limit.
|
|
@@ -183,6 +183,20 @@ export function readState(dir: string): TeamState {
|
|
|
183
183
|
} finally { closeSync(fd); }
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/** Cache only display names, not histories; atomic snapshot replacements invalidate the cache. */
|
|
187
|
+
export function createMemberLabels(): (dir: string) => Record<string, string> {
|
|
188
|
+
let key: string | undefined, labels: Record<string, string>;
|
|
189
|
+
return dir => {
|
|
190
|
+
const file = join(dir, "state.json"), stat = lstatSync(file, { bigint: true });
|
|
191
|
+
const next = `${file}:${stat.dev}:${stat.ino}:${stat.mtimeNs}:${stat.size}:${stat.mode}:${stat.uid}:${stat.nlink}`;
|
|
192
|
+
if (next !== key) {
|
|
193
|
+
labels = Object.fromEntries(Object.values(readState(dir).members).map(member => [member.id, memberName(member)]));
|
|
194
|
+
key = next;
|
|
195
|
+
}
|
|
196
|
+
return labels;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
186
200
|
export class Store {
|
|
187
201
|
readonly dir: string;
|
|
188
202
|
state: TeamState;
|
package/src/terminal-ui.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function installTeamFooter(_pi: ExtensionAPI, ctx: ExtensionContext, host
|
|
|
95
95
|
redraw();
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
|
-
const next = await host.snapshot();
|
|
98
|
+
const next = await host.snapshot(false);
|
|
99
99
|
if (disposed) return;
|
|
100
100
|
members = next.members.filter(isLive); tasks = next.tasks;
|
|
101
101
|
const count = ids().length;
|
package/src/ui-types.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface PanelHost {
|
|
|
12
12
|
self: string;
|
|
13
13
|
/** True in a member session; false in a main session. */
|
|
14
14
|
child: boolean;
|
|
15
|
-
snapshot(): Promise<TeamSnapshot>;
|
|
15
|
+
snapshot(includeMessages?: boolean): Promise<TeamSnapshot>;
|
|
16
16
|
visit(id: string): Promise<void>;
|
|
17
17
|
requestTask(text: string): Promise<string>;
|
|
18
18
|
feedback(task: TaskView, text: string): Promise<string>;
|