tmux-team 4.0.0 → 4.1.0

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.
@@ -14,7 +14,12 @@ import { cmdPreamble } from './preamble.js';
14
14
  // Test utilities
15
15
  // ─────────────────────────────────────────────────────────────
16
16
 
17
- function createMockUI(): UI & { errors: string[]; warnings: string[]; infos: string[]; jsonOutput: unknown[] } {
17
+ function createMockUI(): UI & {
18
+ errors: string[];
19
+ warnings: string[];
20
+ infos: string[];
21
+ jsonOutput: unknown[];
22
+ } {
18
23
  const mock = {
19
24
  errors: [] as string[],
20
25
  warnings: [] as string[],
@@ -48,7 +53,8 @@ function createDefaultConfig(): ResolvedConfig {
48
53
  pollInterval: 1,
49
54
  captureLines: 100,
50
55
  maxCaptureLines: 2000,
51
- preambleEvery: 3, pasteEnterDelayMs: 500,
56
+ preambleEvery: 3,
57
+ pasteEnterDelayMs: 500,
52
58
  },
53
59
  agents: {},
54
60
  paneRegistry: {
@@ -64,6 +70,13 @@ function createMockTmux(): Tmux {
64
70
  capture: vi.fn(() => ''),
65
71
  listPanes: vi.fn(() => []),
66
72
  getCurrentPaneId: vi.fn(() => null),
73
+ resolvePaneTarget: vi.fn((target: string) => target),
74
+ getAgentRegistry: vi.fn(() => ({ paneRegistry: {}, agents: {} })),
75
+ setAgentRegistration: vi.fn(),
76
+ clearAgentRegistration: vi.fn(() => false),
77
+ listTeams: vi.fn(() => ({})),
78
+ listTeamPanes: vi.fn(() => []),
79
+ removeTeam: vi.fn(() => ({ removed: 0, agents: [] })),
67
80
  };
68
81
  }
69
82
 
@@ -5,6 +5,7 @@
5
5
  import type { Context } from '../types.js';
6
6
  import { ExitCodes } from '../context.js';
7
7
  import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
8
+ import { getRegistryScope, registrationFromEntry } from '../registry.js';
8
9
 
9
10
  /**
10
11
  * Show preamble(s) for agent(s).
@@ -60,7 +61,7 @@ function showPreamble(ctx: Context, agentName?: string): void {
60
61
  * Set preamble for an agent (in local config).
61
62
  */
62
63
  function setPreamble(ctx: Context, agentName: string, preamble: string): void {
63
- const { ui, paths, flags, config } = ctx;
64
+ const { ui, paths, flags, config, tmux } = ctx;
64
65
 
65
66
  // Check if agent exists in pane registry
66
67
  if (!config.paneRegistry[agentName]) {
@@ -69,15 +70,19 @@ function setPreamble(ctx: Context, agentName: string, preamble: string): void {
69
70
  ctx.exit(ExitCodes.ERROR);
70
71
  }
71
72
 
72
- const localConfig = loadLocalConfigFile(paths);
73
-
74
- // Update preamble in local config
75
- const agentEntry = localConfig[agentName] as { pane: string; preamble?: string } | undefined;
76
- if (agentEntry) {
77
- agentEntry.preamble = preamble;
73
+ const pane = tmux.resolvePaneTarget(config.paneRegistry[agentName].pane);
74
+ if (!pane) {
75
+ ui.error(`Pane '${config.paneRegistry[agentName].pane}' not found. Is tmux running?`);
76
+ ctx.exit(ExitCodes.PANE_NOT_FOUND);
78
77
  }
79
78
 
80
- saveLocalConfigFile(paths, localConfig);
79
+ const nextEntry = { ...config.paneRegistry[agentName], pane, preamble };
80
+ tmux.setAgentRegistration(
81
+ pane,
82
+ getRegistryScope(ctx),
83
+ registrationFromEntry(agentName, nextEntry)
84
+ );
85
+ updateLegacyPreambleIfPresent(paths, agentName, preamble);
81
86
 
82
87
  if (flags.json) {
83
88
  ui.json({ agent: agentName, preamble, status: 'set' });
@@ -90,14 +95,28 @@ function setPreamble(ctx: Context, agentName: string, preamble: string): void {
90
95
  * Clear preamble for an agent (in local config).
91
96
  */
92
97
  function clearPreamble(ctx: Context, agentName: string): void {
93
- const { ui, paths, flags } = ctx;
94
-
95
- const localConfig = loadLocalConfigFile(paths);
96
- const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
97
-
98
- if (agentEntry?.preamble) {
99
- delete agentEntry.preamble;
100
- saveLocalConfigFile(paths, localConfig);
98
+ const { ui, paths, flags, config, tmux } = ctx;
99
+
100
+ const entry = config.paneRegistry[agentName];
101
+ const hasPreamble =
102
+ entry?.preamble !== undefined ||
103
+ config.agents[agentName]?.preamble !== undefined ||
104
+ legacyHasPreamble(paths, agentName);
105
+
106
+ if (entry && hasPreamble) {
107
+ const pane = tmux.resolvePaneTarget(entry.pane);
108
+ if (!pane) {
109
+ ui.error(`Pane '${entry.pane}' not found. Is tmux running?`);
110
+ ctx.exit(ExitCodes.PANE_NOT_FOUND);
111
+ }
112
+ const nextEntry = { ...entry, pane };
113
+ delete nextEntry.preamble;
114
+ tmux.setAgentRegistration(
115
+ pane,
116
+ getRegistryScope(ctx),
117
+ registrationFromEntry(agentName, nextEntry)
118
+ );
119
+ clearLegacyPreambleIfPresent(paths, agentName);
101
120
 
102
121
  if (flags.json) {
103
122
  ui.json({ agent: agentName, status: 'cleared' });
@@ -113,6 +132,32 @@ function clearPreamble(ctx: Context, agentName: string): void {
113
132
  }
114
133
  }
115
134
 
135
+ function updateLegacyPreambleIfPresent(
136
+ paths: Context['paths'],
137
+ agentName: string,
138
+ preamble: string
139
+ ): void {
140
+ const localConfig = loadLocalConfigFile(paths);
141
+ const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
142
+ if (!agentEntry) return;
143
+ agentEntry.preamble = preamble;
144
+ saveLocalConfigFile(paths, localConfig);
145
+ }
146
+
147
+ function clearLegacyPreambleIfPresent(paths: Context['paths'], agentName: string): void {
148
+ const localConfig = loadLocalConfigFile(paths);
149
+ const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
150
+ if (!agentEntry || !Object.prototype.hasOwnProperty.call(agentEntry, 'preamble')) return;
151
+ delete agentEntry.preamble;
152
+ saveLocalConfigFile(paths, localConfig);
153
+ }
154
+
155
+ function legacyHasPreamble(paths: Context['paths'], agentName: string): boolean {
156
+ const localConfig = loadLocalConfigFile(paths);
157
+ const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
158
+ return Boolean(agentEntry && Object.prototype.hasOwnProperty.call(agentEntry, 'preamble'));
159
+ }
160
+
116
161
  /**
117
162
  * Preamble command entry point.
118
163
  */
@@ -5,22 +5,26 @@
5
5
  import type { Context } from '../types.js';
6
6
  import { ExitCodes } from '../exits.js';
7
7
  import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
8
+ import { getRegistryScope } from '../registry.js';
8
9
 
9
10
  export function cmdRemove(ctx: Context, name: string): void {
10
- const { ui, config, paths, flags, exit } = ctx;
11
+ const { ui, config, paths, flags, tmux, exit } = ctx;
11
12
 
12
13
  if (!config.paneRegistry[name]) {
13
14
  ui.error(`Agent '${name}' not found.`);
14
15
  exit(ExitCodes.PANE_NOT_FOUND);
15
16
  }
16
17
 
17
- // Load existing config to preserve other agents' fields (preamble, deny, etc.)
18
- const localConfig = loadLocalConfigFile(paths);
19
- delete localConfig[name];
20
- saveLocalConfigFile(paths, localConfig);
18
+ const removedFromTmux = tmux.clearAgentRegistration(name, getRegistryScope(ctx));
19
+ if (!removedFromTmux) {
20
+ // Legacy fallback: remove from tmux-team.json when this scope still uses it.
21
+ const localConfig = loadLocalConfigFile(paths);
22
+ delete localConfig[name];
23
+ saveLocalConfigFile(paths, localConfig);
24
+ }
21
25
 
22
26
  if (flags.json) {
23
- ui.json({ removed: name });
27
+ ui.json({ removed: name, source: removedFromTmux ? 'tmux' : 'legacy' });
24
28
  } else {
25
29
  ui.success(`Removed agent '${name}'`);
26
30
  }
@@ -43,6 +43,25 @@ function createMockTmux(): Tmux & {
43
43
  getCurrentPaneId() {
44
44
  return null;
45
45
  },
46
+ resolvePaneTarget(target: string) {
47
+ return target;
48
+ },
49
+ getAgentRegistry() {
50
+ return { paneRegistry: {}, agents: {} };
51
+ },
52
+ setAgentRegistration() {},
53
+ clearAgentRegistration() {
54
+ return false;
55
+ },
56
+ listTeams() {
57
+ return {};
58
+ },
59
+ listTeamPanes() {
60
+ return [];
61
+ },
62
+ removeTeam() {
63
+ return { removed: 0, agents: [] };
64
+ },
46
65
  };
47
66
  return mock;
48
67
  }
@@ -239,7 +258,8 @@ describe('buildMessage (via cmdTalk)', () => {
239
258
  pollInterval: 0.1,
240
259
  captureLines: 100,
241
260
  maxCaptureLines: 2000,
242
- preambleEvery: 3, pasteEnterDelayMs: 500,
261
+ preambleEvery: 3,
262
+ pasteEnterDelayMs: 500,
243
263
  },
244
264
  };
245
265
 
@@ -551,7 +571,8 @@ describe('cmdTalk - --wait mode', () => {
551
571
  pollInterval: 0.01,
552
572
  captureLines: 100,
553
573
  maxCaptureLines: 2000,
554
- preambleEvery: 3, pasteEnterDelayMs: 500,
574
+ preambleEvery: 3,
575
+ pasteEnterDelayMs: 500,
555
576
  },
556
577
  },
557
578
  });
@@ -595,7 +616,8 @@ describe('cmdTalk - --wait mode', () => {
595
616
  pollInterval: 0.01,
596
617
  captureLines: 100,
597
618
  maxCaptureLines: 2000,
598
- preambleEvery: 3, pasteEnterDelayMs: 500,
619
+ preambleEvery: 3,
620
+ pasteEnterDelayMs: 500,
599
621
  },
600
622
  },
601
623
  });
@@ -626,7 +648,8 @@ describe('cmdTalk - --wait mode', () => {
626
648
  pollInterval: 0.02,
627
649
  captureLines: 100,
628
650
  maxCaptureLines: 2000,
629
- preambleEvery: 3, pasteEnterDelayMs: 500,
651
+ preambleEvery: 3,
652
+ pasteEnterDelayMs: 500,
630
653
  },
631
654
  },
632
655
  });
@@ -674,7 +697,8 @@ describe('cmdTalk - --wait mode', () => {
674
697
  pollInterval: 0.01,
675
698
  captureLines: 100,
676
699
  maxCaptureLines: 2000,
677
- preambleEvery: 3, pasteEnterDelayMs: 500,
700
+ preambleEvery: 3,
701
+ pasteEnterDelayMs: 500,
678
702
  },
679
703
  },
680
704
  });
@@ -710,7 +734,8 @@ describe('cmdTalk - --wait mode', () => {
710
734
  pollInterval: 0.01,
711
735
  captureLines: 100,
712
736
  maxCaptureLines: 2000,
713
- preambleEvery: 3, pasteEnterDelayMs: 500,
737
+ preambleEvery: 3,
738
+ pasteEnterDelayMs: 500,
714
739
  },
715
740
  },
716
741
  });
@@ -739,7 +764,8 @@ describe('cmdTalk - --wait mode', () => {
739
764
  pollInterval: 0.01,
740
765
  captureLines: 100,
741
766
  maxCaptureLines: 2000,
742
- preambleEvery: 3, pasteEnterDelayMs: 500,
767
+ preambleEvery: 3,
768
+ pasteEnterDelayMs: 500,
743
769
  },
744
770
  },
745
771
  });
@@ -791,7 +817,8 @@ describe('cmdTalk - --wait mode', () => {
791
817
  pollInterval: 0.05,
792
818
  captureLines: 100,
793
819
  maxCaptureLines: 2000,
794
- preambleEvery: 3, pasteEnterDelayMs: 500,
820
+ preambleEvery: 3,
821
+ pasteEnterDelayMs: 500,
795
822
  },
796
823
  paneRegistry: {
797
824
  codex: { pane: '10.1' },
@@ -837,7 +864,8 @@ describe('cmdTalk - --wait mode', () => {
837
864
  pollInterval: 0.02,
838
865
  captureLines: 100,
839
866
  maxCaptureLines: 2000,
840
- preambleEvery: 3, pasteEnterDelayMs: 500,
867
+ preambleEvery: 3,
868
+ pasteEnterDelayMs: 500,
841
869
  },
842
870
  paneRegistry: {
843
871
  codex: { pane: '10.1' },
@@ -892,7 +920,8 @@ describe('cmdTalk - --wait mode', () => {
892
920
  pollInterval: 0.02,
893
921
  captureLines: 100,
894
922
  maxCaptureLines: 2000,
895
- preambleEvery: 3, pasteEnterDelayMs: 500,
923
+ preambleEvery: 3,
924
+ pasteEnterDelayMs: 500,
896
925
  },
897
926
  paneRegistry: {
898
927
  codex: { pane: '10.1' },
@@ -1003,7 +1032,8 @@ describe('cmdTalk - nonce collision handling', () => {
1003
1032
  pollInterval: 0.01,
1004
1033
  captureLines: 100,
1005
1034
  maxCaptureLines: 2000,
1006
- preambleEvery: 3, pasteEnterDelayMs: 500,
1035
+ preambleEvery: 3,
1036
+ pasteEnterDelayMs: 500,
1007
1037
  },
1008
1038
  },
1009
1039
  });
@@ -1058,7 +1088,8 @@ describe('cmdTalk - JSON output contract', () => {
1058
1088
  pollInterval: 0.01,
1059
1089
  captureLines: 100,
1060
1090
  maxCaptureLines: 2000,
1061
- preambleEvery: 3, pasteEnterDelayMs: 500,
1091
+ preambleEvery: 3,
1092
+ pasteEnterDelayMs: 500,
1062
1093
  },
1063
1094
  },
1064
1095
  });
@@ -1099,7 +1130,8 @@ describe('cmdTalk - JSON output contract', () => {
1099
1130
  pollInterval: 0.01,
1100
1131
  captureLines: 100,
1101
1132
  maxCaptureLines: 2000,
1102
- preambleEvery: 3, pasteEnterDelayMs: 500,
1133
+ preambleEvery: 3,
1134
+ pasteEnterDelayMs: 500,
1103
1135
  },
1104
1136
  },
1105
1137
  });
@@ -1141,7 +1173,8 @@ describe('cmdTalk - JSON output contract', () => {
1141
1173
  pollInterval: 0.01,
1142
1174
  captureLines: 100,
1143
1175
  maxCaptureLines: 2000,
1144
- preambleEvery: 3, pasteEnterDelayMs: 500,
1176
+ preambleEvery: 3,
1177
+ pasteEnterDelayMs: 500,
1145
1178
  },
1146
1179
  },
1147
1180
  });
@@ -1178,7 +1211,8 @@ describe('cmdTalk - JSON output contract', () => {
1178
1211
  pollInterval: 0.01,
1179
1212
  captureLines: 100,
1180
1213
  maxCaptureLines: 2000,
1181
- preambleEvery: 3, pasteEnterDelayMs: 500,
1214
+ preambleEvery: 3,
1215
+ pasteEnterDelayMs: 500,
1182
1216
  },
1183
1217
  },
1184
1218
  });
@@ -1228,7 +1262,8 @@ describe('cmdTalk - JSON output contract', () => {
1228
1262
  pollInterval: 0.02,
1229
1263
  captureLines: 100,
1230
1264
  maxCaptureLines: 2000,
1231
- preambleEvery: 3, pasteEnterDelayMs: 500,
1265
+ preambleEvery: 3,
1266
+ pasteEnterDelayMs: 500,
1232
1267
  },
1233
1268
  paneRegistry: {
1234
1269
  codex: { pane: '10.1' },
@@ -1310,7 +1345,16 @@ describe('cmdTalk - end marker detection', () => {
1310
1345
  ui,
1311
1346
  paths: createTestPaths(testDir),
1312
1347
  flags: { wait: true, json: true, timeout: 0.5 },
1313
- config: { defaults: { timeout: 0.5, pollInterval: 0.01, captureLines: 100, maxCaptureLines: 2000, preambleEvery: 3, pasteEnterDelayMs: 500 } },
1348
+ config: {
1349
+ defaults: {
1350
+ timeout: 0.5,
1351
+ pollInterval: 0.01,
1352
+ captureLines: 100,
1353
+ maxCaptureLines: 2000,
1354
+ preambleEvery: 3,
1355
+ pasteEnterDelayMs: 500,
1356
+ },
1357
+ },
1314
1358
  });
1315
1359
 
1316
1360
  await cmdTalk(ctx, 'claude', 'Test message');
@@ -1344,7 +1388,16 @@ describe('cmdTalk - end marker detection', () => {
1344
1388
  ui,
1345
1389
  paths: createTestPaths(testDir),
1346
1390
  flags: { wait: true, json: true, timeout: 0.5 },
1347
- config: { defaults: { timeout: 0.5, pollInterval: 0.01, captureLines: 100, maxCaptureLines: 2000, preambleEvery: 3, pasteEnterDelayMs: 500 } },
1391
+ config: {
1392
+ defaults: {
1393
+ timeout: 0.5,
1394
+ pollInterval: 0.01,
1395
+ captureLines: 100,
1396
+ maxCaptureLines: 2000,
1397
+ preambleEvery: 3,
1398
+ pasteEnterDelayMs: 500,
1399
+ },
1400
+ },
1348
1401
  });
1349
1402
 
1350
1403
  await cmdTalk(ctx, 'claude', 'Test');
@@ -1378,7 +1431,16 @@ Line 4 final`;
1378
1431
  ui,
1379
1432
  paths: createTestPaths(testDir),
1380
1433
  flags: { wait: true, json: true, timeout: 0.5 },
1381
- config: { defaults: { timeout: 0.5, pollInterval: 0.01, captureLines: 100, maxCaptureLines: 2000, preambleEvery: 3, pasteEnterDelayMs: 500 } },
1434
+ config: {
1435
+ defaults: {
1436
+ timeout: 0.5,
1437
+ pollInterval: 0.01,
1438
+ captureLines: 100,
1439
+ maxCaptureLines: 2000,
1440
+ preambleEvery: 3,
1441
+ pasteEnterDelayMs: 500,
1442
+ },
1443
+ },
1382
1444
  });
1383
1445
 
1384
1446
  await cmdTalk(ctx, 'claude', 'Test');
@@ -1409,7 +1471,16 @@ Line 4 final`;
1409
1471
  ui,
1410
1472
  paths: createTestPaths(testDir),
1411
1473
  flags: { wait: true, json: true, timeout: 0.5 },
1412
- config: { defaults: { timeout: 0.5, pollInterval: 0.01, captureLines: 100, maxCaptureLines: 2000, preambleEvery: 3, pasteEnterDelayMs: 500 } },
1474
+ config: {
1475
+ defaults: {
1476
+ timeout: 0.5,
1477
+ pollInterval: 0.01,
1478
+ captureLines: 100,
1479
+ maxCaptureLines: 2000,
1480
+ preambleEvery: 3,
1481
+ pasteEnterDelayMs: 500,
1482
+ },
1483
+ },
1413
1484
  });
1414
1485
 
1415
1486
  await cmdTalk(ctx, 'claude', 'Test');
@@ -1446,7 +1517,16 @@ Line 4 final`;
1446
1517
  ui,
1447
1518
  paths: createTestPaths(testDir),
1448
1519
  flags: { wait: true, json: true, timeout: 0.5 },
1449
- config: { defaults: { timeout: 0.5, pollInterval: 0.01, captureLines: 100, maxCaptureLines: 2000, preambleEvery: 3, pasteEnterDelayMs: 500 } },
1520
+ config: {
1521
+ defaults: {
1522
+ timeout: 0.5,
1523
+ pollInterval: 0.01,
1524
+ captureLines: 100,
1525
+ maxCaptureLines: 2000,
1526
+ preambleEvery: 3,
1527
+ pasteEnterDelayMs: 500,
1528
+ },
1529
+ },
1450
1530
  });
1451
1531
 
1452
1532
  await cmdTalk(ctx, 'claude', 'Test');
@@ -1482,7 +1562,16 @@ Line 4 final`;
1482
1562
  ui,
1483
1563
  paths: createTestPaths(testDir),
1484
1564
  flags: { wait: true, json: true, timeout: 0.5 },
1485
- config: { defaults: { timeout: 0.5, pollInterval: 0.01, captureLines: 200, maxCaptureLines: 2000, preambleEvery: 3, pasteEnterDelayMs: 500 } },
1565
+ config: {
1566
+ defaults: {
1567
+ timeout: 0.5,
1568
+ pollInterval: 0.01,
1569
+ captureLines: 200,
1570
+ maxCaptureLines: 2000,
1571
+ preambleEvery: 3,
1572
+ pasteEnterDelayMs: 500,
1573
+ },
1574
+ },
1486
1575
  });
1487
1576
 
1488
1577
  await cmdTalk(ctx, 'claude', 'Test');
@@ -186,7 +186,10 @@ function extractWithExpandableCapture(
186
186
 
187
187
  if (instructionLineIndex !== -1 && instructionLineIndex < endMarkerLineIndex) {
188
188
  // Instruction visible: extract from after instruction to marker
189
- const response = lines.slice(instructionLineIndex + 1, endMarkerLineIndex).join('\n').trim();
189
+ const response = lines
190
+ .slice(instructionLineIndex + 1, endMarkerLineIndex)
191
+ .join('\n')
192
+ .trim();
190
193
  return { response, truncated: false };
191
194
  }
192
195
 
@@ -619,7 +622,8 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
619
622
  );
620
623
 
621
624
  // Clean Gemini CLI UI artifacts
622
- const response = target === 'gemini' ? cleanGeminiResponse(extractedResponse) : extractedResponse;
625
+ const response =
626
+ target === 'gemini' ? cleanGeminiResponse(extractedResponse) : extractedResponse;
623
627
 
624
628
  if (!flags.json && isTTY) {
625
629
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
@@ -883,7 +887,8 @@ async function cmdTalkAllWait(
883
887
  );
884
888
 
885
889
  // Clean Gemini CLI UI artifacts
886
- const response = state.agent === 'gemini' ? cleanGeminiResponse(extractedResponse) : extractedResponse;
890
+ const response =
891
+ state.agent === 'gemini' ? cleanGeminiResponse(extractedResponse) : extractedResponse;
887
892
  state.response = response;
888
893
  state.truncated = truncated;
889
894
  state.status = 'completed';
@@ -0,0 +1,172 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // team command - inspect pane team/workspace scope and manage explicit shared teams
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { Context, TeamPaneInfo, TeamPaneRegistration } from '../types.js';
6
+ import { ExitCodes } from '../exits.js';
7
+
8
+ export function cmdTeam(ctx: Context, args: string[]): void {
9
+ const subcommand = args[0] ?? 'ls';
10
+
11
+ switch (subcommand) {
12
+ case 'ls':
13
+ case 'list':
14
+ listTeams(ctx, args.slice(1));
15
+ break;
16
+ case 'rm':
17
+ case 'remove':
18
+ removeTeam(ctx, args.slice(1));
19
+ break;
20
+ default:
21
+ ctx.ui.error(`Unknown team subcommand: ${subcommand}`);
22
+ ctx.ui.error('Usage: tmux-team team [ls [--summary]|rm <team> --force]');
23
+ ctx.exit(ExitCodes.ERROR);
24
+ }
25
+ }
26
+
27
+ function listTeams(ctx: Context, args: string[]): void {
28
+ const teams = ctx.tmux.listTeams();
29
+ const panes = ctx.tmux.listTeamPanes();
30
+ const summaryOnly = args.includes('--summary');
31
+
32
+ if (ctx.flags.json) {
33
+ ctx.ui.json({ teams, panes });
34
+ return;
35
+ }
36
+
37
+ if (summaryOnly) {
38
+ const rows = Object.entries(teams).sort(([a], [b]) => a.localeCompare(b));
39
+ if (rows.length === 0) {
40
+ ctx.ui.info('No shared teams found.');
41
+ return;
42
+ }
43
+ ctx.ui.table(
44
+ ['TEAM', 'AGENTS'],
45
+ rows.map(([teamName, agents]) => [teamName, agents.join(', ') || '-'])
46
+ );
47
+ return;
48
+ }
49
+
50
+ const groups = panesToGroups(panes);
51
+ if (groups.length === 0) {
52
+ ctx.ui.info('No tmux panes found.');
53
+ return;
54
+ }
55
+
56
+ for (const [index, group] of groups.entries()) {
57
+ if (index > 0) console.log('');
58
+ console.log(group.title);
59
+ ctx.ui.table(['PANE', 'TARGET', 'CWD', 'CMD'], group.rows);
60
+ }
61
+ }
62
+
63
+ interface PaneGroup {
64
+ key: string;
65
+ title: string;
66
+ agents: Set<string>;
67
+ rows: string[][];
68
+ }
69
+
70
+ function panesToGroups(panes: TeamPaneInfo[]): PaneGroup[] {
71
+ const groups = new Map<string, PaneGroup>();
72
+
73
+ for (const pane of panes) {
74
+ if (pane.registrations.length === 0) {
75
+ addPaneToGroup(groups, '2:', 'Unregistered panes', pane);
76
+ continue;
77
+ }
78
+
79
+ for (const registration of pane.registrations) {
80
+ addPaneToGroup(
81
+ groups,
82
+ scopeSortKey(registration),
83
+ groupTitle(registration),
84
+ pane,
85
+ registration
86
+ );
87
+ }
88
+ }
89
+
90
+ return [...groups.values()]
91
+ .sort((a, b) => a.key.localeCompare(b.key))
92
+ .map((group) => ({
93
+ ...group,
94
+ title:
95
+ group.agents.size > 0
96
+ ? `${group.title} (${[...group.agents].sort().join(', ')})`
97
+ : group.title,
98
+ rows: group.rows.sort((a, b) => a[1].localeCompare(b[1])),
99
+ }));
100
+ }
101
+
102
+ function addPaneToGroup(
103
+ groups: Map<string, PaneGroup>,
104
+ key: string,
105
+ title: string,
106
+ pane: TeamPaneInfo,
107
+ registration?: TeamPaneRegistration
108
+ ): void {
109
+ const group = groups.get(key) ?? { key, title, agents: new Set<string>(), rows: [] };
110
+ if (registration) group.agents.add(formatAgent(registration));
111
+ group.rows.push([pane.pane, pane.target ?? '-', pane.cwd ?? '-', pane.command || '-']);
112
+ groups.set(key, group);
113
+ }
114
+
115
+ function scopeSortKey(registration: TeamPaneRegistration): string {
116
+ if (registration.scopeType === 'team') return `0:${registration.scope}`;
117
+ if (registration.scopeType === 'workspace') return `1:${registration.scope}`;
118
+ return '2:';
119
+ }
120
+
121
+ function groupTitle(registration: TeamPaneRegistration): string {
122
+ if (registration.scopeType === 'team') return `Team: ${registration.scope}`;
123
+ return `Workspace: ${registration.scope}`;
124
+ }
125
+
126
+ function formatAgent(registration: TeamPaneRegistration): string {
127
+ return registration.remark
128
+ ? `${registration.agent} (${registration.remark})`
129
+ : registration.agent;
130
+ }
131
+
132
+ function removeTeam(ctx: Context, args: string[]): void {
133
+ const teamName = args.find((arg) => !arg.startsWith('-'));
134
+ const dryRun = args.includes('--dry-run');
135
+ const force = ctx.flags.force || args.includes('--force') || args.includes('-f');
136
+
137
+ if (!teamName) {
138
+ ctx.ui.error('Usage: tmux-team team rm <team> --force');
139
+ ctx.exit(ExitCodes.ERROR);
140
+ }
141
+
142
+ const teams = ctx.tmux.listTeams();
143
+ const agents = teams[teamName] ?? [];
144
+ if (agents.length === 0) {
145
+ ctx.ui.error(`Team '${teamName}' not found.`);
146
+ ctx.exit(ExitCodes.PANE_NOT_FOUND);
147
+ }
148
+
149
+ if (ctx.flags.json && dryRun) {
150
+ ctx.ui.json({ team: teamName, dryRun: true, agents, removed: 0 });
151
+ return;
152
+ }
153
+
154
+ if (dryRun) {
155
+ ctx.ui.info(`Would remove team '${teamName}' from ${agents.length} agent(s).`);
156
+ ctx.ui.table(['TEAM', 'AGENTS'], [[teamName, agents.join(', ')]]);
157
+ return;
158
+ }
159
+
160
+ if (!force) {
161
+ ctx.ui.error(`Refusing to remove team '${teamName}' without --force.`);
162
+ ctx.exit(ExitCodes.ERROR);
163
+ }
164
+
165
+ const result = ctx.tmux.removeTeam(teamName);
166
+ if (ctx.flags.json) {
167
+ ctx.ui.json({ team: teamName, removed: result.removed, agents: result.agents });
168
+ return;
169
+ }
170
+
171
+ ctx.ui.success(`Removed team '${teamName}' from ${result.removed} pane(s).`);
172
+ }