pi-extended-teams 2.1.16

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.
Files changed (130) hide show
  1. package/README.md +105 -0
  2. package/assets/pi-extended-teams-agent-navigation.png +0 -0
  3. package/assets/pi-extended-teams-in-action.png +0 -0
  4. package/extensions/agents/read-agent-report.ts +181 -0
  5. package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
  6. package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
  7. package/extensions/agents/read-agent.test.ts +3077 -0
  8. package/extensions/agents/read-agent.ts +1119 -0
  9. package/extensions/agents/write-agent.test.ts +513 -0
  10. package/extensions/agents/write-agent.ts +392 -0
  11. package/extensions/events/register-events.test.ts +465 -0
  12. package/extensions/events/register-events.ts +409 -0
  13. package/extensions/index.test.ts +1659 -0
  14. package/extensions/index.ts +1228 -0
  15. package/extensions/internal/agent-session-files.test.ts +164 -0
  16. package/extensions/internal/agent-session-files.ts +320 -0
  17. package/extensions/internal/debug.ts +44 -0
  18. package/extensions/internal/model-selection.ts +82 -0
  19. package/extensions/internal/pi-command.test.ts +191 -0
  20. package/extensions/internal/pi-command.ts +224 -0
  21. package/extensions/internal/pi-runtime-api.test.ts +43 -0
  22. package/extensions/internal/pi-runtime-api.ts +78 -0
  23. package/extensions/internal/schema.ts +19 -0
  24. package/extensions/internal/session-context-reference.test.ts +290 -0
  25. package/extensions/internal/session-context-reference.ts +430 -0
  26. package/extensions/internal/session-files.test.ts +225 -0
  27. package/extensions/internal/session-files.ts +273 -0
  28. package/extensions/internal/session-usage.ts +59 -0
  29. package/extensions/resources/spawn-resource-plan.test.ts +233 -0
  30. package/extensions/resources/spawn-resource-plan.ts +247 -0
  31. package/extensions/runtime/active-agent-sleep.test.ts +157 -0
  32. package/extensions/runtime/active-agent-sleep.ts +117 -0
  33. package/extensions/runtime/nested-read-agents.ts +22 -0
  34. package/extensions/runtime/pending-child-controller.test.ts +169 -0
  35. package/extensions/runtime/pending-child-controller.ts +280 -0
  36. package/extensions/runtime/types.ts +68 -0
  37. package/extensions/team/contracts.test.ts +111 -0
  38. package/extensions/team/lifecycle.test.ts +1095 -0
  39. package/extensions/team/lifecycle.ts +502 -0
  40. package/extensions/team/recipient-closure.test.ts +97 -0
  41. package/extensions/team/recipient-closure.ts +120 -0
  42. package/extensions/team/roster.test.ts +129 -0
  43. package/extensions/team/roster.ts +165 -0
  44. package/extensions/team/team-contracts.json +43 -0
  45. package/extensions/team/writer-screens.test.ts +50 -0
  46. package/extensions/team/writer-screens.ts +156 -0
  47. package/extensions/tools/agent-communication-tools.test.ts +306 -0
  48. package/extensions/tools/agent-communication-tools.ts +183 -0
  49. package/extensions/tools/coordination-tools.test.ts +670 -0
  50. package/extensions/tools/coordination-tools.ts +304 -0
  51. package/extensions/tools/delegation-guard.test.ts +95 -0
  52. package/extensions/tools/delegation-guard.ts +65 -0
  53. package/extensions/tools/file-claim-tools.test.ts +104 -0
  54. package/extensions/tools/file-claim-tools.ts +68 -0
  55. package/extensions/tools/model-tools.ts +53 -0
  56. package/extensions/tools/predefined-tools.test.ts +647 -0
  57. package/extensions/tools/predefined-tools.ts +331 -0
  58. package/extensions/tools/read-helper.test.ts +35 -0
  59. package/extensions/tools/task-runtime-tools.test.ts +334 -0
  60. package/extensions/tools/task-runtime-tools.ts +227 -0
  61. package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
  62. package/extensions/tools/team-tools.ts +1270 -0
  63. package/extensions/ui/agent-follow-view.test.ts +520 -0
  64. package/extensions/ui/agent-follow-view.ts +779 -0
  65. package/extensions/ui/agent-navigation.test.ts +52 -0
  66. package/extensions/ui/agent-navigation.ts +55 -0
  67. package/extensions/ui/ansi.ts +16 -0
  68. package/extensions/ui/extensions-command.test.ts +336 -0
  69. package/extensions/ui/extensions-command.ts +282 -0
  70. package/extensions/ui/favorite-models-command.test.ts +280 -0
  71. package/extensions/ui/favorite-models-command.ts +423 -0
  72. package/extensions/ui/frame.ts +77 -0
  73. package/extensions/ui/input.ts +17 -0
  74. package/extensions/ui/read-agent-status.test.ts +117 -0
  75. package/extensions/ui/read-agent-status.ts +125 -0
  76. package/extensions/ui/renderers.ts +243 -0
  77. package/extensions/ui/status-widget.test.ts +143 -0
  78. package/extensions/ui/status-widget.ts +440 -0
  79. package/extensions/ui-frame.test.ts +69 -0
  80. package/package.json +75 -0
  81. package/skills/teams.md +256 -0
  82. package/src/adapters/terminal-registry.ts +78 -0
  83. package/src/adapters/tmux-adapter.test.ts +276 -0
  84. package/src/adapters/tmux-adapter.ts +200 -0
  85. package/src/orchestration/index.ts +437 -0
  86. package/src/orchestration/orchestrator.test.ts +396 -0
  87. package/src/orchestration/types.ts +125 -0
  88. package/src/utils/atomic-json.ts +21 -0
  89. package/src/utils/claims.test.ts +137 -0
  90. package/src/utils/claims.ts +169 -0
  91. package/src/utils/hooks.test.ts +75 -0
  92. package/src/utils/hooks.ts +35 -0
  93. package/src/utils/lifecycle-tombstone.test.ts +105 -0
  94. package/src/utils/lifecycle-tombstone.ts +268 -0
  95. package/src/utils/lock.race.child.ts +44 -0
  96. package/src/utils/lock.race.test.ts +198 -0
  97. package/src/utils/lock.test.ts +90 -0
  98. package/src/utils/lock.ts +186 -0
  99. package/src/utils/messaging.test.ts +337 -0
  100. package/src/utils/messaging.ts +441 -0
  101. package/src/utils/model-resolution.test.ts +231 -0
  102. package/src/utils/model-resolution.ts +322 -0
  103. package/src/utils/models.test.ts +8 -0
  104. package/src/utils/models.ts +115 -0
  105. package/src/utils/paths.ts +81 -0
  106. package/src/utils/predefined-teams/types.ts +48 -0
  107. package/src/utils/predefined-teams.save-template.test.ts +74 -0
  108. package/src/utils/predefined-teams.test.ts +441 -0
  109. package/src/utils/predefined-teams.ts +471 -0
  110. package/src/utils/read-helper-queue.ts +99 -0
  111. package/src/utils/report-events.test.ts +154 -0
  112. package/src/utils/report-events.ts +222 -0
  113. package/src/utils/runtime.test.ts +314 -0
  114. package/src/utils/runtime.ts +261 -0
  115. package/src/utils/security.test.ts +43 -0
  116. package/src/utils/settings.test.ts +480 -0
  117. package/src/utils/settings.ts +645 -0
  118. package/src/utils/shared-memory.test.ts +80 -0
  119. package/src/utils/shared-memory.ts +81 -0
  120. package/src/utils/tasks.race.test.ts +44 -0
  121. package/src/utils/tasks.test.ts +229 -0
  122. package/src/utils/tasks.ts +396 -0
  123. package/src/utils/teams.ts +231 -0
  124. package/src/utils/terminal-adapter.ts +103 -0
  125. package/src/utils/thinking-levels.test.ts +56 -0
  126. package/src/utils/thinking-levels.ts +47 -0
  127. package/src/utils/workflow-metadata.test.ts +11 -0
  128. package/src/utils/workflow-metadata.ts +88 -0
  129. package/src/utils/write-queue.test.ts +251 -0
  130. package/src/utils/write-queue.ts +202 -0
@@ -0,0 +1,440 @@
1
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
2
+ import { dimAnsi, pink, purple } from "./ansi";
3
+ import { formatAnimatedProgress } from "./renderers";
4
+
5
+ export interface TeamActivityStatusEntry {
6
+ name: string;
7
+ role: "read" | "write";
8
+ status?: string;
9
+ detail?: string;
10
+ displayText?: string;
11
+ }
12
+
13
+ export type TeamActivityStatusCounts = Record<string, number>;
14
+
15
+ export interface TeamActivityStatusSnapshot {
16
+ activeCount: number;
17
+ readCount: number;
18
+ writeCount: number;
19
+ unreadCount: number;
20
+ entries: TeamActivityStatusEntry[];
21
+ statusCounts?: TeamActivityStatusCounts;
22
+ updatedAt: number;
23
+ }
24
+
25
+ const MAX_EXPANDED_ENTRIES = 10;
26
+ const AGGREGATE_PREVIEW_THRESHOLD = MAX_EXPANDED_ENTRIES;
27
+ const MAX_AGGREGATE_STATUS_PARTS = 4;
28
+ const PROGRESS_ANIMATION_FRAME_MS = 60;
29
+ const SINGLE_COLUMN_ASCII = /^[\x20-\x7E]*$/;
30
+ const SINGLE_COLUMN_STATUS_TEXT = /^[\x20-\x7E\u00B7]*$/;
31
+ const CONTEXT_USAGE_STATUS_SUFFIX = /^(?:\?|[\d.]+[kM]?) tok(?: \((?:\?|[\d.]+)%\))?$/;
32
+
33
+ function formatCountSummary(snapshot: TeamActivityStatusSnapshot): string {
34
+ const parts = [`${snapshot.activeCount} active`];
35
+ if (snapshot.readCount > 0) parts.push(`${snapshot.readCount} read`);
36
+ if (snapshot.writeCount > 0) parts.push(`${snapshot.writeCount} write`);
37
+ if (snapshot.unreadCount > 0) parts.push(`${snapshot.unreadCount} inbox`);
38
+ return parts.join(" · ");
39
+ }
40
+
41
+ function shouldUseAggregatePreview(snapshot: TeamActivityStatusSnapshot): boolean {
42
+ return snapshot.entries.length > AGGREGATE_PREVIEW_THRESHOLD;
43
+ }
44
+
45
+ function formatRoleSummary(snapshot: TeamActivityStatusSnapshot): string {
46
+ const parts: string[] = [];
47
+ if (snapshot.readCount > 0) parts.push(`${snapshot.readCount} read`);
48
+ if (snapshot.writeCount > 0) parts.push(`${snapshot.writeCount} write`);
49
+ return parts.length > 0 ? parts.join(" · ") : `${snapshot.activeCount} active`;
50
+ }
51
+
52
+ function formatStatusSummary(statusCounts: TeamActivityStatusCounts | undefined): string | undefined {
53
+ if (!statusCounts) return undefined;
54
+ const sorted = Object.entries(statusCounts)
55
+ .filter(([, count]) => count > 0)
56
+ .sort(([aLabel, aCount], [bLabel, bCount]) => bCount - aCount || aLabel.localeCompare(bLabel));
57
+ if (sorted.length === 0) return undefined;
58
+
59
+ const shown = sorted.slice(0, MAX_AGGREGATE_STATUS_PARTS).map(([label, count]) => `${count} ${label}`);
60
+ const remaining = sorted.length - shown.length;
61
+ if (remaining > 0) shown.push(`+${remaining} states`);
62
+ return shown.join(" · ");
63
+ }
64
+
65
+ function formatAggregatePreview(snapshot: TeamActivityStatusSnapshot): string {
66
+ const summary = formatStatusSummary(snapshot.statusCounts) || formatRoleSummary(snapshot);
67
+ return `${pink("summary")} ${purple(summary)} ${dimAnsi("↓ navigate")}`;
68
+ }
69
+
70
+ function formatExpandedEntry(entry: TeamActivityStatusEntry): string {
71
+ if (entry.displayText) return entry.displayText;
72
+ const status = entry.status ? ` ${purple(entry.status)}` : "";
73
+ const detail = entry.detail ? ` ${dimAnsi(entry.detail)}` : "";
74
+ return `${pink(entry.name)} ${purple(entry.role)}${status}${detail}`;
75
+ }
76
+
77
+ function formatHeader(snapshot: TeamActivityStatusSnapshot): string {
78
+ const summary = `${formatCountSummary(snapshot)} · ↓ navigate`;
79
+ return `${pink("agent activity")} ${dimAnsi(summary)}`;
80
+ }
81
+
82
+ interface ProgressTransition {
83
+ key: string;
84
+ authoritative: boolean;
85
+ entryRole: TeamActivityStatusEntry["role"];
86
+ entryName: string;
87
+ source: string;
88
+ branch: string;
89
+ prefix: string;
90
+ linePrefix: string;
91
+ linePrefixVisibleWidth: number;
92
+ previous: string;
93
+ previousSingleColumn: boolean;
94
+ previousVisibleWidth: number;
95
+ target: string;
96
+ targetSingleColumn: boolean;
97
+ targetVisibleWidth: number;
98
+ startedAt: number;
99
+ maxRenderedWidth: number;
100
+ }
101
+
102
+ function progressTransitionMaxRenderedWidth(linePrefixVisibleWidth: number, previousVisibleWidth: number, targetVisibleWidth: number): number {
103
+ return linePrefixVisibleWidth + Math.max(previousVisibleWidth, targetVisibleWidth + 3);
104
+ }
105
+
106
+ function currentTransitionRenderedWidth(
107
+ transition: ProgressTransition,
108
+ progressText: string,
109
+ singleColumn: boolean,
110
+ width: number
111
+ ): number {
112
+ return transition.maxRenderedWidth <= width
113
+ ? transition.maxRenderedWidth
114
+ : transition.linePrefixVisibleWidth + (singleColumn ? progressText.length : visibleWidth(progressText));
115
+ }
116
+
117
+ function splitProgressDisplay(displayText: string | undefined): { prefix: string; progress: string } | null {
118
+ if (!displayText) return null;
119
+ const delimiterIndex = displayText.lastIndexOf(" · ");
120
+ if (delimiterIndex < 0) return null;
121
+ const displayLength = displayText.length;
122
+ if (CONTEXT_USAGE_STATUS_SUFFIX.test(displayText.slice(delimiterIndex + 3))) return null;
123
+ let progressEnd = displayLength;
124
+ while (progressEnd > delimiterIndex + 3 && displayText.charCodeAt(progressEnd - 1) === 46) progressEnd--;
125
+ return {
126
+ prefix: displayText.slice(0, delimiterIndex + 3),
127
+ progress: displayText.slice(delimiterIndex + 3, progressEnd),
128
+ };
129
+ }
130
+
131
+ export function teamActivityStatusWidget(
132
+ getSnapshot: () => TeamActivityStatusSnapshot | null | undefined,
133
+ _getExpanded: () => boolean,
134
+ requestRender?: () => void
135
+ ) {
136
+ const transitions = new Map<string, ProgressTransition>();
137
+ let animationTimer: NodeJS.Timeout | null = null;
138
+ let summarySnapshot: TeamActivityStatusSnapshot | null | undefined;
139
+ let summaryUpdatedAt = -1;
140
+ let summaryWidth = -1;
141
+ let summaryLines: string[] = [];
142
+ let headerActiveCount = -1;
143
+ let headerReadCount = -1;
144
+ let headerWriteCount = -1;
145
+ let headerUnreadCount = -1;
146
+ let headerWidth = -1;
147
+ let headerLine = "";
148
+ let aggregateStatusCounts: TeamActivityStatusCounts = {};
149
+ let aggregateStatusCountSize = -1;
150
+ let aggregateActiveCount = -1;
151
+ let aggregateReadCount = -1;
152
+ let aggregateWriteCount = -1;
153
+ let aggregateWidth = -1;
154
+ let aggregateLine = "";
155
+ let remainingCount = -1;
156
+ let remainingWidth = -1;
157
+ let remainingLine = "";
158
+ let transitionHintSnapshot: TeamActivityStatusSnapshot | null | undefined;
159
+ let transitionHintUpdatedAt = -1;
160
+ const transitionHintEntries: TeamActivityStatusEntry[] = [];
161
+ const transitionHints: Array<ProgressTransition | undefined> = [];
162
+ const transitionRosterRoles: Array<TeamActivityStatusEntry["role"]> = [];
163
+ const transitionRosterNames: string[] = [];
164
+ const transitionRosterKeys: string[] = [];
165
+ let transitionRosterCount = -1;
166
+ let stableSnapshot: TeamActivityStatusSnapshot | null | undefined;
167
+ let stableUpdatedAt = -1;
168
+ let stableWidth = -1;
169
+ let stableLines: string[] | null = null;
170
+ // render consumes each result synchronously before animating the next entry.
171
+ const animatedResult: { text: string; active: boolean } = { text: "", active: false };
172
+ const animationResult = (text: string, active: boolean, width: number, renderedWidth?: number) => {
173
+ animatedResult.text = renderedWidth !== undefined && renderedWidth <= width
174
+ ? text
175
+ : truncateToWidth(text, width, "…", true);
176
+ animatedResult.active = active;
177
+ return animatedResult;
178
+ };
179
+
180
+ const animateEntry = (entry: TeamActivityStatusEntry, branch: string, now: number, width: number, transitionHint: ProgressTransition | undefined, hintIndex: number, hasCachedTransitionEntry: boolean): { text: string; active: boolean } => {
181
+ let key: string | undefined;
182
+ let transition = transitionHint;
183
+ if (!transition) {
184
+ key = `${entry.role}:${entry.name}`;
185
+ transition = transitions.get(key);
186
+ if (!hasCachedTransitionEntry || transition) transitionHints[hintIndex] = transition;
187
+ }
188
+ if (!entry.displayText) {
189
+ return animationResult(`${purple(branch)} ${formatExpandedEntry(entry)}`, false, width);
190
+ }
191
+
192
+ if (!transition || transition.source !== entry.displayText) {
193
+ const parsed = splitProgressDisplay(entry.displayText);
194
+ if (!parsed) {
195
+ return animationResult(`${purple(branch)} ${formatExpandedEntry(entry)}`, false, width);
196
+ }
197
+ const branchPrefix = `${purple(branch)} `;
198
+ const prefixVisibleWidth = SINGLE_COLUMN_STATUS_TEXT.test(parsed.prefix)
199
+ ? parsed.prefix.length
200
+ : visibleWidth(parsed.prefix);
201
+ const linePrefixVisibleWidth = prefixVisibleWidth + 3;
202
+ const progressSingleColumn = SINGLE_COLUMN_ASCII.test(parsed.progress);
203
+ const progressVisibleWidth = progressSingleColumn ? parsed.progress.length : visibleWidth(parsed.progress);
204
+ const transitionKey = transition
205
+ && transition.entryRole === entry.role
206
+ && transition.entryName === entry.name
207
+ ? transition.key
208
+ : key ?? `${entry.role}:${entry.name}`;
209
+ const sourceTransition = transition;
210
+
211
+ if (!transition) {
212
+ transition = {
213
+ key: transitionKey,
214
+ authoritative: true,
215
+ entryRole: entry.role,
216
+ entryName: entry.name,
217
+ source: entry.displayText,
218
+ branch,
219
+ prefix: parsed.prefix,
220
+ linePrefix: `${branchPrefix}${parsed.prefix}`,
221
+ linePrefixVisibleWidth,
222
+ previous: parsed.progress,
223
+ previousSingleColumn: progressSingleColumn,
224
+ previousVisibleWidth: progressVisibleWidth,
225
+ target: parsed.progress,
226
+ targetSingleColumn: progressSingleColumn,
227
+ targetVisibleWidth: progressVisibleWidth,
228
+ startedAt: now - 1000,
229
+ maxRenderedWidth: progressTransitionMaxRenderedWidth(linePrefixVisibleWidth, progressVisibleWidth, progressVisibleWidth),
230
+ };
231
+ } else if (transition.target !== parsed.progress) {
232
+ transition = {
233
+ key: transitionKey,
234
+ authoritative: true,
235
+ entryRole: entry.role,
236
+ entryName: entry.name,
237
+ source: entry.displayText,
238
+ branch,
239
+ prefix: parsed.prefix,
240
+ linePrefix: `${branchPrefix}${parsed.prefix}`,
241
+ linePrefixVisibleWidth,
242
+ previous: transition.target,
243
+ previousSingleColumn: transition.targetSingleColumn,
244
+ previousVisibleWidth: transition.targetVisibleWidth,
245
+ target: parsed.progress,
246
+ targetSingleColumn: progressSingleColumn,
247
+ targetVisibleWidth: progressVisibleWidth,
248
+ startedAt: now,
249
+ maxRenderedWidth: progressTransitionMaxRenderedWidth(linePrefixVisibleWidth, transition.targetVisibleWidth, progressVisibleWidth),
250
+ };
251
+ } else {
252
+ transition = {
253
+ ...transition,
254
+ key: transitionKey,
255
+ authoritative: true,
256
+ entryRole: entry.role,
257
+ entryName: entry.name,
258
+ source: entry.displayText,
259
+ branch,
260
+ prefix: parsed.prefix,
261
+ linePrefix: `${branchPrefix}${parsed.prefix}`,
262
+ linePrefixVisibleWidth,
263
+ maxRenderedWidth: progressTransitionMaxRenderedWidth(linePrefixVisibleWidth, transition.previousVisibleWidth, transition.targetVisibleWidth),
264
+ };
265
+ }
266
+ const replacedTransition = sourceTransition
267
+ ? sourceTransition.authoritative && sourceTransition.key === transition.key
268
+ ? sourceTransition
269
+ : transitions.get(transition.key)
270
+ : undefined;
271
+ if (replacedTransition && replacedTransition !== transition) replacedTransition.authoritative = false;
272
+ transitions.set(transition.key, transition);
273
+ transitionHints[hintIndex] = transition;
274
+ } else if (transition.branch !== branch) {
275
+ transition.branch = branch;
276
+ transition.linePrefix = `${purple(branch)} ${transition.prefix}`;
277
+ }
278
+
279
+ const elapsed = Math.max(0, now - transition.startedAt);
280
+ if (elapsed < 200) {
281
+ const remaining = Math.max(0, Math.ceil(transition.previous.length * (1 - elapsed / 200)));
282
+ const progressText = transition.previous.slice(0, remaining);
283
+ return animationResult(`${transition.linePrefix}${dimAnsi(progressText)}`, true, width, currentTransitionRenderedWidth(transition, progressText, transition.previousSingleColumn, width));
284
+ }
285
+ if (elapsed < 1000) {
286
+ const revealed = Math.floor(transition.target.length * ((elapsed - 200) / 800));
287
+ const progressText = transition.target.slice(0, revealed);
288
+ return animationResult(`${transition.linePrefix}${progressText}`, true, width, currentTransitionRenderedWidth(transition, progressText, transition.targetSingleColumn, width));
289
+ }
290
+ const progressText = formatAnimatedProgress(transition.target, now);
291
+ return animationResult(`${transition.linePrefix}${progressText}`, false, width, currentTransitionRenderedWidth(transition, progressText, transition.targetSingleColumn, width));
292
+ };
293
+
294
+ const stopAnimationTimer = () => {
295
+ if (animationTimer) clearInterval(animationTimer);
296
+ animationTimer = null;
297
+ };
298
+
299
+ return {
300
+ render(width: number): string[] {
301
+ const snapshot = getSnapshot();
302
+ if (!snapshot || width <= 0) {
303
+ stopAnimationTimer();
304
+ stableLines = null;
305
+ return [];
306
+ }
307
+ if (stableLines && stableSnapshot === snapshot && stableUpdatedAt === snapshot.updatedAt && stableWidth === width) {
308
+ return stableLines.slice();
309
+ }
310
+
311
+ const now = Date.now();
312
+ let animationActive = false;
313
+ if (summarySnapshot !== snapshot || summaryUpdatedAt !== snapshot.updatedAt || summaryWidth !== width) {
314
+ summarySnapshot = snapshot;
315
+ summaryUpdatedAt = snapshot.updatedAt;
316
+ summaryWidth = width;
317
+ if (headerActiveCount !== snapshot.activeCount
318
+ || headerReadCount !== snapshot.readCount
319
+ || headerWriteCount !== snapshot.writeCount
320
+ || headerUnreadCount !== snapshot.unreadCount
321
+ || headerWidth !== width) {
322
+ headerActiveCount = snapshot.activeCount;
323
+ headerReadCount = snapshot.readCount;
324
+ headerWriteCount = snapshot.writeCount;
325
+ headerUnreadCount = snapshot.unreadCount;
326
+ headerWidth = width;
327
+ headerLine = truncateToWidth(formatHeader(snapshot), width, "…", true);
328
+ }
329
+ summaryLines = [headerLine];
330
+ if (shouldUseAggregatePreview(snapshot)) {
331
+ const statusCounts = snapshot.statusCounts;
332
+ let statusCountSize = 0;
333
+ let sameStatusCounts = true;
334
+ if (statusCounts) {
335
+ for (const label in statusCounts) {
336
+ if (!Object.hasOwn(statusCounts, label)) continue;
337
+ statusCountSize++;
338
+ if (aggregateStatusCounts[label] !== statusCounts[label]) sameStatusCounts = false;
339
+ }
340
+ }
341
+ sameStatusCounts &&= statusCountSize === aggregateStatusCountSize;
342
+ if (!sameStatusCounts
343
+ || aggregateActiveCount !== snapshot.activeCount
344
+ || aggregateReadCount !== snapshot.readCount
345
+ || aggregateWriteCount !== snapshot.writeCount
346
+ || aggregateWidth !== width) {
347
+ aggregateStatusCounts = statusCounts ? { ...statusCounts } : {};
348
+ aggregateStatusCountSize = statusCountSize;
349
+ aggregateActiveCount = snapshot.activeCount;
350
+ aggregateReadCount = snapshot.readCount;
351
+ aggregateWriteCount = snapshot.writeCount;
352
+ aggregateWidth = width;
353
+ aggregateLine = truncateToWidth(formatAggregatePreview(snapshot), width, "…", true);
354
+ }
355
+ summaryLines.push(aggregateLine);
356
+ }
357
+ }
358
+ const lines = summaryLines.slice();
359
+
360
+ const entries = snapshot.entries;
361
+ const entryLength = entries.length;
362
+ const allEntriesVisible = entryLength <= MAX_EXPANDED_ENTRIES;
363
+ const entryCount = allEntriesVisible ? entryLength : MAX_EXPANDED_ENTRIES;
364
+ const lastVisibleEntryIndex = allEntriesVisible ? entryCount - 1 : -1;
365
+ const canReuseTransitionHints = transitionHintSnapshot === snapshot && transitionHintUpdatedAt === snapshot.updatedAt;
366
+ if (!canReuseTransitionHints) {
367
+ transitionHintSnapshot = snapshot;
368
+ transitionHintUpdatedAt = snapshot.updatedAt;
369
+ }
370
+ let rosterChanged = entryCount !== transitionRosterCount;
371
+ for (let index = 0; index < entryCount; index++) {
372
+ const entry = entries[index];
373
+ if (transitionRosterRoles[index] !== entry.role || transitionRosterNames[index] !== entry.name) {
374
+ transitionRosterRoles[index] = entry.role;
375
+ transitionRosterNames[index] = entry.name;
376
+ transitionRosterKeys[index] = `${entry.role}:${entry.name}`;
377
+ rosterChanged = true;
378
+ }
379
+ const branch = index === lastVisibleEntryIndex ? "└─" : "├─";
380
+ const hasCachedTransitionEntry = canReuseTransitionHints && transitionHintEntries[index] === entry;
381
+ let transitionHint = hasCachedTransitionEntry ? transitionHints[index] : undefined;
382
+ if (!hasCachedTransitionEntry) {
383
+ transitionHintEntries[index] = entry;
384
+ const candidate = transitionHints[index];
385
+ if (candidate
386
+ && candidate.entryRole === entry.role
387
+ && candidate.entryName === entry.name
388
+ && candidate.authoritative) {
389
+ transitionHint = candidate;
390
+ }
391
+ }
392
+ const animated = animateEntry(entry, branch, now, width, transitionHint, index, hasCachedTransitionEntry);
393
+ animationActive ||= animated.active;
394
+ lines.push(animated.text);
395
+ }
396
+ if (rosterChanged) {
397
+ const activeKeys = new Set<string>();
398
+ for (let index = 0; index < entryCount; index++) activeKeys.add(transitionRosterKeys[index]);
399
+ for (const key of transitions.keys()) {
400
+ if (activeKeys.has(key)) continue;
401
+ transitions.get(key)!.authoritative = false;
402
+ transitions.delete(key);
403
+ }
404
+ }
405
+ transitionRosterCount = entryCount;
406
+
407
+ const remaining = entryLength - entryCount;
408
+ if (remaining > 0) {
409
+ if (remainingCount !== remaining || remainingWidth !== width) {
410
+ remainingCount = remaining;
411
+ remainingWidth = width;
412
+ remainingLine = truncateToWidth(dimAnsi(`└─ … ${remaining} more active agent${remaining === 1 ? "" : "s"}`), width, "…", true);
413
+ }
414
+ lines.push(remainingLine);
415
+ }
416
+
417
+ if (animationActive && requestRender && !animationTimer) {
418
+ animationTimer = setInterval(requestRender, PROGRESS_ANIMATION_FRAME_MS);
419
+ } else if (!animationActive) {
420
+ stopAnimationTimer();
421
+ }
422
+
423
+ const border = purple("─".repeat(Math.max(0, width)));
424
+ const rendered = [...lines, border];
425
+ if (animationActive) {
426
+ stableLines = null;
427
+ } else {
428
+ stableSnapshot = snapshot;
429
+ stableUpdatedAt = snapshot.updatedAt;
430
+ stableWidth = width;
431
+ stableLines = rendered;
432
+ }
433
+ return rendered;
434
+ },
435
+ invalidate() {},
436
+ dispose() {
437
+ stopAnimationTimer();
438
+ },
439
+ };
440
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { visibleWidth } from "@mariozechner/pi-tui";
3
+ import { panelBgFill, framePanel, frameWidget, frameWidgetFullWidth, logWindowStart } from "./index.js";
4
+
5
+ const PANEL_BG = "\x1b[48;5;235m";
6
+ const RESET = "\x1b[0m";
7
+ const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
8
+
9
+ describe("panel UI framing", () => {
10
+ it("panelBgFill pads to the requested visible width with the dark background", () => {
11
+ const filled = panelBgFill("hi", 10);
12
+ expect(visibleWidth(filled)).toBe(10);
13
+ expect(filled.startsWith(PANEL_BG)).toBe(true);
14
+ expect(stripAnsi(filled)).toBe("hi ");
15
+ });
16
+
17
+ it("panelBgFill re-asserts the background after embedded foreground resets", () => {
18
+ const colored = `${"\x1b[38;5;213m"}name${RESET} ${"\x1b[2m"}detail${RESET}`;
19
+ const filled = panelBgFill(colored, 20);
20
+ const resets = (filled.match(/\x1b\[0m/g) || []).length;
21
+ const backgrounds = (filled.match(/\x1b\[48;5;235m/g) || []).length;
22
+ // Every reset (except the final terminator) is followed by a fresh bg code,
23
+ // plus the leading bg, so backgrounds always outnumber/equal interior resets.
24
+ expect(backgrounds).toBeGreaterThanOrEqual(resets);
25
+ expect(visibleWidth(filled)).toBe(20);
26
+ });
27
+
28
+ it("framePanel draws a rounded border with uniform width and a dark interior", () => {
29
+ const out = framePanel(["left", "right column"], 20);
30
+ expect(out).toHaveLength(4); // top + 2 content + bottom
31
+ const widths = out.map((l) => visibleWidth(l));
32
+ expect(new Set(widths).size).toBe(1); // every row identical width
33
+ expect(widths[0]).toBe(24); // innerWidth + 2 padding + 2 borders
34
+ expect(stripAnsi(out[0]).startsWith("╭")).toBe(true);
35
+ expect(stripAnsi(out[0]).endsWith("╮")).toBe(true);
36
+ expect(stripAnsi(out.at(-1)!).startsWith("╰")).toBe(true);
37
+ expect(stripAnsi(out[1]).startsWith("│")).toBe(true);
38
+ expect(stripAnsi(out[1]).endsWith("│")).toBe(true);
39
+ expect(out.every((l) => l.includes(PANEL_BG))).toBe(true);
40
+ });
41
+
42
+ it("framePanel truncates content wider than the inner width", () => {
43
+ const out = framePanel(["this line is much too long for the panel"], 12);
44
+ expect(new Set(out.map((l) => visibleWidth(l))).size).toBe(1);
45
+ expect(visibleWidth(out[1])).toBe(16); // innerWidth + 2 padding + 2 borders
46
+ expect(stripAnsi(out[1])).toContain("…");
47
+ });
48
+
49
+ it("frameWidget self-sizes the box to the widest content line", () => {
50
+ const out = frameWidget(["short", "a much longer line here"]);
51
+ const widest = visibleWidth("a much longer line here");
52
+ expect(visibleWidth(out[0])).toBe(widest + 4);
53
+ expect(new Set(out.map((l) => visibleWidth(l))).size).toBe(1);
54
+ });
55
+
56
+ it("frameWidgetFullWidth uses the full available render width", () => {
57
+ const out = frameWidgetFullWidth(["status"], 40);
58
+ expect(out).toHaveLength(3);
59
+ expect(out.every((line) => visibleWidth(line) === 40)).toBe(true);
60
+ expect(stripAnsi(out[0])).toBe(`╭${"─".repeat(38)}╮`);
61
+ });
62
+
63
+ it("logWindowStart anchors at the tail and scrolls upward by offset", () => {
64
+ expect(logWindowStart(30, 10, 0)).toBe(20);
65
+ expect(logWindowStart(30, 10, 5)).toBe(15);
66
+ expect(logWindowStart(30, 10, 500)).toBe(0);
67
+ expect(logWindowStart(5, 10, 5)).toBe(0);
68
+ });
69
+ });
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "pi-extended-teams",
3
+ "version": "2.1.16",
4
+ "description": "Control-first, session-connected subagents for Pi with live navigation and intent-tier routing",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/dantetekanem/pi-extended-teams.git"
8
+ },
9
+ "homepage": "https://github.com/dantetekanem/pi-extended-teams#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/dantetekanem/pi-extended-teams/issues"
12
+ },
13
+ "author": "dantetekanem",
14
+ "license": "MIT",
15
+ "keywords": [
16
+ "pi-package",
17
+ "ai-agent",
18
+ "multi-agent",
19
+ "agent-teams",
20
+ "agent-coordination",
21
+ "autonomous-agents",
22
+ "task-management",
23
+ "team-collaboration",
24
+ "agent-orchestration",
25
+ "coding-assistant",
26
+ "tmux"
27
+ ],
28
+ "main": "extensions/index.ts",
29
+ "exports": {
30
+ ".": "./extensions/index.ts",
31
+ "./orchestration": "./src/orchestration/index.ts",
32
+ "./orchestration/types": "./src/orchestration/types.ts",
33
+ "./extensions/*": "./extensions/*",
34
+ "./src/*": "./src/*",
35
+ "./skills/*": "./skills/*",
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "extensions",
40
+ "skills",
41
+ "src",
42
+ "assets",
43
+ "package.json",
44
+ "README.md"
45
+ ],
46
+ "dependencies": {
47
+ "uuid": "^11.1.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@mariozechner/pi-coding-agent": "*",
51
+ "@mariozechner/pi-tui": "*",
52
+ "@sinclair/typebox": "*"
53
+ },
54
+ "pi": {
55
+ "image": "https://raw.githubusercontent.com/dantetekanem/pi-extended-teams/main/assets/pi-extended-teams-in-action.png",
56
+ "extensions": [
57
+ "extensions/index.ts"
58
+ ],
59
+ "skills": [
60
+ "skills"
61
+ ]
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^25.3.0",
65
+ "ts-node": "^10.9.2",
66
+ "typescript": "^5.9.3",
67
+ "vitest": "^4.0.18"
68
+ },
69
+ "scripts": {
70
+ "typecheck": "tsc --noEmit",
71
+ "test": "vitest run",
72
+ "test:focused": "vitest run src/utils/claims.test.ts src/utils/tasks.test.ts src/utils/model-resolution.test.ts src/utils/settings.test.ts src/utils/write-queue.test.ts src/utils/shared-memory.test.ts extensions/index.test.ts extensions/events/register-events.test.ts extensions/internal/session-files.test.ts extensions/team/contracts.test.ts extensions/tools/delegation-guard.test.ts extensions/tools/agent-communication-tools.test.ts extensions/tools/read-helper.test.ts extensions/agents/read-agent.test.ts",
73
+ "build": "tsc --noEmit"
74
+ }
75
+ }