tmux-team 4.1.0 → 4.2.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.
- package/README.md +31 -11
- package/package.json +15 -16
- package/src/cli.ts +5 -1
- package/src/commands/basic-commands.test.ts +247 -6
- package/src/commands/check.ts +20 -0
- package/src/commands/help.ts +5 -3
- package/src/commands/list.ts +14 -1
- package/src/commands/talk.test.ts +21 -0
- package/src/commands/talk.ts +20 -0
- package/src/commands/team.ts +201 -12
package/README.md
CHANGED
|
@@ -68,9 +68,12 @@ visible on both sides, use a [shared team](#shared-teams).
|
|
|
68
68
|
| `talk <agent> "msg"` | Send message and wait for response |
|
|
69
69
|
| `talk all "msg"` | Broadcast to all agents |
|
|
70
70
|
| `check <agent> [lines]` | Read agent's pane output |
|
|
71
|
-
| `list` | Show agents
|
|
71
|
+
| `list [team\|pane]` | Show current workspace agents, one shared team, or one pane's registrations |
|
|
72
72
|
| `migrate [--dry-run] [--cleanup]` | Move legacy `tmux-team.json` entries into tmux pane metadata |
|
|
73
|
-
| `team
|
|
73
|
+
| `team` | List shared team names |
|
|
74
|
+
| `team ls <team>` | List members of a shared team |
|
|
75
|
+
| `team add <team> <name> [pane]` | Add current or specified pane to a shared team |
|
|
76
|
+
| `team panes [--json]` | Inspect tmux panes grouped by scope |
|
|
74
77
|
| `team rm <team> --force` | Remove a shared team registration from every pane |
|
|
75
78
|
| `learn` | Show educational guide |
|
|
76
79
|
|
|
@@ -95,13 +98,31 @@ tmt config set pasteEnterDelayMs 500
|
|
|
95
98
|
Agent registrations live in tmux pane metadata, scoped per workspace by
|
|
96
99
|
default. The same-folder workflow never needs `--team`.
|
|
97
100
|
|
|
98
|
-
**List agents
|
|
101
|
+
**List agents and status:**
|
|
99
102
|
```bash
|
|
100
|
-
tmt ls
|
|
101
|
-
tmt ls
|
|
103
|
+
tmt ls # agents in this workspace
|
|
104
|
+
tmt ls myproject # members of a shared team
|
|
105
|
+
tmt ls 10.1 # registrations on a pane
|
|
106
|
+
tmt ls main.10.1 # shorthand for main:10.1
|
|
102
107
|
```
|
|
103
108
|
|
|
104
|
-
**
|
|
109
|
+
**Manage shared teams** with the `team` namespace:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
tmt team # list shared team names
|
|
113
|
+
tmt team ls myproject # list team members
|
|
114
|
+
tmt team add myproject claude # add current pane to a team
|
|
115
|
+
tmt team add myproject codex 1.1 # add a specific pane to a team
|
|
116
|
+
tmt team rm myproject --force # remove a team from every pane
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A single pane can belong to multiple teams. Commands never guess across teams:
|
|
120
|
+
`tmt talk codex` uses the current workspace, while `tmt talk codex --team
|
|
121
|
+
myproject` uses only that shared team. If an agent name appears in multiple
|
|
122
|
+
shared teams and is not in the current workspace, tmux-team asks you to specify
|
|
123
|
+
the team.
|
|
124
|
+
|
|
125
|
+
**Inspect every tmux pane** with `tmt team panes`. Output is grouped by scope —
|
|
105
126
|
shared teams first, then workspaces, then unregistered panes — and each
|
|
106
127
|
section's title lists the agents living there:
|
|
107
128
|
|
|
@@ -121,9 +142,8 @@ PANE TARGET CWD CMD
|
|
|
121
142
|
```
|
|
122
143
|
|
|
123
144
|
```bash
|
|
124
|
-
tmt team
|
|
125
|
-
tmt team
|
|
126
|
-
tmt team ls --json # { teams, panes } incl. each pane's registrations
|
|
145
|
+
tmt team panes # grouped pane inventory
|
|
146
|
+
tmt team panes --json # { teams, panes } incl. each pane's registrations
|
|
127
147
|
```
|
|
128
148
|
|
|
129
149
|
**Add an agent from any pane.** Targets can be `%pane_id`, `window.pane`, or
|
|
@@ -213,8 +233,8 @@ tmt add codex 1.1
|
|
|
213
233
|
```bash
|
|
214
234
|
tmt this frontend-claude --team acme-app # from ~/acme/frontend
|
|
215
235
|
tmt this backend-codex --team acme-app # from ~/acme/backend
|
|
216
|
-
tmt
|
|
217
|
-
tmt team ls
|
|
236
|
+
tmt team # list shared teams
|
|
237
|
+
tmt team ls acme-app # list members
|
|
218
238
|
tmt team rm acme-app --force # remove the team from every pane
|
|
219
239
|
```
|
|
220
240
|
|
package/package.json
CHANGED
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-team",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "CLI tool for AI agent collaboration in tmux - manage cross-pane communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tmux-team": "./bin/tmux-team",
|
|
8
8
|
"tmt": "./bin/tmux-team"
|
|
9
9
|
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"dev": "tsx src/cli.ts",
|
|
12
|
-
"tmt": "./bin/tmux-team",
|
|
13
|
-
"test": "pnpm test:run",
|
|
14
|
-
"test:watch": "vitest",
|
|
15
|
-
"test:run": "vitest run --coverage && node scripts/check-coverage.mjs --threshold 90 --branches 85",
|
|
16
|
-
"lint": "oxlint src/",
|
|
17
|
-
"lint:fix": "oxlint src/ --fix",
|
|
18
|
-
"format": "prettier --write src/",
|
|
19
|
-
"format:check": "prettier --check src/",
|
|
20
|
-
"type:check": "tsc --noEmit",
|
|
21
|
-
"check": "pnpm type:check && pnpm lint && pnpm format:check"
|
|
22
|
-
},
|
|
23
10
|
"keywords": [
|
|
24
11
|
"tmux",
|
|
25
12
|
"cli",
|
|
@@ -37,7 +24,6 @@
|
|
|
37
24
|
"engines": {
|
|
38
25
|
"node": ">=18"
|
|
39
26
|
},
|
|
40
|
-
"packageManager": "pnpm@9.15.4",
|
|
41
27
|
"os": [
|
|
42
28
|
"darwin",
|
|
43
29
|
"linux"
|
|
@@ -57,5 +43,18 @@
|
|
|
57
43
|
"prettier": "^3.7.4",
|
|
58
44
|
"typescript": "^5.3.0",
|
|
59
45
|
"vitest": "^1.2.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "tsx src/cli.ts",
|
|
49
|
+
"tmt": "./bin/tmux-team",
|
|
50
|
+
"test": "pnpm test:run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"test:run": "vitest run --coverage && node scripts/check-coverage.mjs --threshold 90 --branches 85",
|
|
53
|
+
"lint": "oxlint src/",
|
|
54
|
+
"lint:fix": "oxlint src/ --fix",
|
|
55
|
+
"format": "prettier --write src/",
|
|
56
|
+
"format:check": "prettier --check src/",
|
|
57
|
+
"type:check": "tsc --noEmit",
|
|
58
|
+
"check": "pnpm type:check && pnpm lint && pnpm format:check"
|
|
60
59
|
}
|
|
61
|
-
}
|
|
60
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -284,6 +284,98 @@ describe('basic commands', () => {
|
|
|
284
284
|
expect(tableCall[1][0][2]).toBe('-');
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
+
it('cmdList can list a shared team by positional name', () => {
|
|
288
|
+
const ctx = createCtx(testDir);
|
|
289
|
+
(ctx.tmux.listTeams as ReturnType<typeof vi.fn>).mockReturnValue({ egp: ['claude'] });
|
|
290
|
+
(ctx.tmux.listTeamPanes as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
291
|
+
{
|
|
292
|
+
pane: '%1',
|
|
293
|
+
target: 'main:1.0',
|
|
294
|
+
cwd: '/repo',
|
|
295
|
+
command: 'claude',
|
|
296
|
+
suggestedName: 'claude',
|
|
297
|
+
registrations: [{ scopeType: 'team', scope: 'egp', agent: 'claude' }],
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
cmdList(ctx, 'egp');
|
|
302
|
+
|
|
303
|
+
expect(ctx.ui.table).toHaveBeenCalledWith(
|
|
304
|
+
['NAME', 'PANE', 'TARGET', 'CWD', 'CMD', 'REMARK'],
|
|
305
|
+
[['claude', '%1', 'main:1.0', '/repo', 'claude', '-']]
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('cmdList can show pane status by pane target', () => {
|
|
310
|
+
const ctx = createCtx(testDir);
|
|
311
|
+
(ctx.tmux.resolvePaneTarget as ReturnType<typeof vi.fn>).mockImplementation((target: string) =>
|
|
312
|
+
target === 'main:1.0' ? '%1' : null
|
|
313
|
+
);
|
|
314
|
+
(ctx.tmux.listTeamPanes as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
315
|
+
{
|
|
316
|
+
pane: '%1',
|
|
317
|
+
target: 'main:1.0',
|
|
318
|
+
cwd: '/repo',
|
|
319
|
+
command: 'claude',
|
|
320
|
+
suggestedName: 'claude',
|
|
321
|
+
registrations: [
|
|
322
|
+
{ scopeType: 'workspace', scope: '/repo', agent: 'claude' },
|
|
323
|
+
{ scopeType: 'team', scope: 'egp', agent: 'reviewer', remark: 'strict' },
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
cmdList(ctx, 'main.1.0');
|
|
329
|
+
|
|
330
|
+
expect(ctx.tmux.resolvePaneTarget).toHaveBeenCalledWith('main.1.0');
|
|
331
|
+
expect(ctx.tmux.resolvePaneTarget).toHaveBeenCalledWith('main:1.0');
|
|
332
|
+
expect(ctx.ui.table).toHaveBeenCalledWith(
|
|
333
|
+
['SCOPE', 'NAME', 'REMARK'],
|
|
334
|
+
[
|
|
335
|
+
['workspace:/repo', 'claude', '-'],
|
|
336
|
+
['team:egp', 'reviewer', 'strict'],
|
|
337
|
+
]
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('cmdList outputs pane status JSON and handles unregistered panes', () => {
|
|
342
|
+
const ctx = createCtx(testDir, { flags: { json: true } });
|
|
343
|
+
(ctx.tmux.resolvePaneTarget as ReturnType<typeof vi.fn>).mockReturnValue('%9');
|
|
344
|
+
(ctx.tmux.listTeamPanes as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
345
|
+
{
|
|
346
|
+
pane: '%9',
|
|
347
|
+
target: 'main:9.0',
|
|
348
|
+
cwd: '/tmp',
|
|
349
|
+
command: 'zsh',
|
|
350
|
+
suggestedName: null,
|
|
351
|
+
registrations: [],
|
|
352
|
+
},
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
cmdList(ctx, '9.0');
|
|
356
|
+
|
|
357
|
+
expect((ctx.ui as any).jsonCalls).toEqual([
|
|
358
|
+
{
|
|
359
|
+
pane: {
|
|
360
|
+
pane: '%9',
|
|
361
|
+
target: 'main:9.0',
|
|
362
|
+
cwd: '/tmp',
|
|
363
|
+
command: 'zsh',
|
|
364
|
+
suggestedName: null,
|
|
365
|
+
registrations: [],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('cmdList errors when positional target is neither team nor pane', () => {
|
|
372
|
+
const ctx = createCtx(testDir);
|
|
373
|
+
(ctx.tmux.resolvePaneTarget as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
|
374
|
+
|
|
375
|
+
expect(() => cmdList(ctx, 'missing')).toThrow(`exit(${ExitCodes.PANE_NOT_FOUND})`);
|
|
376
|
+
expect(ctx.ui.error).toHaveBeenCalledWith("Pane or team 'missing' not found.");
|
|
377
|
+
});
|
|
378
|
+
|
|
287
379
|
it('cmdCheck captures pane output', () => {
|
|
288
380
|
const ctx = createCtx(testDir, {
|
|
289
381
|
config: { paneRegistry: { claude: { pane: '1.0' } } },
|
|
@@ -299,6 +391,31 @@ describe('basic commands', () => {
|
|
|
299
391
|
expect(() => cmdCheck(ctx, 'nope')).toThrow(`exit(${ExitCodes.PANE_NOT_FOUND})`);
|
|
300
392
|
});
|
|
301
393
|
|
|
394
|
+
it('cmdCheck points at shared teams when a missing agent is only registered there', () => {
|
|
395
|
+
const ctx = createCtx(testDir);
|
|
396
|
+
(ctx.tmux.listTeams as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
397
|
+
alpha: ['codex'],
|
|
398
|
+
beta: ['codex'],
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect(() => cmdCheck(ctx, 'codex')).toThrow(`exit(${ExitCodes.PANE_NOT_FOUND})`);
|
|
402
|
+
expect(ctx.ui.error).toHaveBeenCalledWith(
|
|
403
|
+
"Agent 'codex' is in multiple shared teams: alpha, beta. Specify one with --team <team>."
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('cmdCheck points at the single shared team when there is no ambiguity', () => {
|
|
408
|
+
const ctx = createCtx(testDir);
|
|
409
|
+
(ctx.tmux.listTeams as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
410
|
+
alpha: ['codex'],
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(() => cmdCheck(ctx, 'codex')).toThrow(`exit(${ExitCodes.PANE_NOT_FOUND})`);
|
|
414
|
+
expect(ctx.ui.error).toHaveBeenCalledWith(
|
|
415
|
+
"Agent 'codex' is in shared team 'alpha'. Specify it: tmt check codex --team alpha"
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
302
419
|
it('cmdCheck errors when tmux capture fails', () => {
|
|
303
420
|
const ctx = createCtx(testDir, {
|
|
304
421
|
config: { paneRegistry: { claude: { pane: '1.0' } } },
|
|
@@ -468,7 +585,18 @@ describe('basic commands', () => {
|
|
|
468
585
|
expect(ctx.ui.info).toHaveBeenCalledWith(`No legacy agents found in ${ctx.paths.localConfig}`);
|
|
469
586
|
});
|
|
470
587
|
|
|
471
|
-
it('cmdTeam lists
|
|
588
|
+
it('cmdTeam lists team names by default', () => {
|
|
589
|
+
const ctx = createCtx(testDir, { flags: { json: true } });
|
|
590
|
+
(ctx.tmux.listTeams as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
591
|
+
egp: ['claude', 'codex'],
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
cmdTeam(ctx, []);
|
|
595
|
+
|
|
596
|
+
expect((ctx.ui as any).jsonCalls).toEqual([{ teams: { egp: ['claude', 'codex'] } }]);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('cmdTeam panes lists all pane team/workspace scopes', () => {
|
|
472
600
|
const ctx = createCtx(testDir, { flags: { json: true } });
|
|
473
601
|
(ctx.tmux.listTeams as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
474
602
|
egp: ['claude', 'codex'],
|
|
@@ -487,7 +615,7 @@ describe('basic commands', () => {
|
|
|
487
615
|
},
|
|
488
616
|
]);
|
|
489
617
|
|
|
490
|
-
cmdTeam(ctx, ['
|
|
618
|
+
cmdTeam(ctx, ['panes']);
|
|
491
619
|
|
|
492
620
|
expect((ctx.ui as any).jsonCalls).toEqual([
|
|
493
621
|
{
|
|
@@ -512,8 +640,8 @@ describe('basic commands', () => {
|
|
|
512
640
|
it('cmdTeam shows empty state and errors on unknown subcommands', () => {
|
|
513
641
|
const ctx = createCtx(testDir);
|
|
514
642
|
|
|
515
|
-
cmdTeam(ctx, [
|
|
516
|
-
expect(ctx.ui.info).toHaveBeenCalledWith('No
|
|
643
|
+
cmdTeam(ctx, []);
|
|
644
|
+
expect(ctx.ui.info).toHaveBeenCalledWith('No shared teams found.');
|
|
517
645
|
|
|
518
646
|
expect(() => cmdTeam(ctx, ['wat'])).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
519
647
|
});
|
|
@@ -527,7 +655,117 @@ describe('basic commands', () => {
|
|
|
527
655
|
expect(ctx.ui.table).toHaveBeenCalledWith(['TEAM', 'AGENTS'], [['egp', 'claude']]);
|
|
528
656
|
});
|
|
529
657
|
|
|
530
|
-
it('cmdTeam
|
|
658
|
+
it('cmdTeam ls lists members for one team', () => {
|
|
659
|
+
const ctx = createCtx(testDir);
|
|
660
|
+
(ctx.tmux.listTeamPanes as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
661
|
+
{
|
|
662
|
+
pane: '%2',
|
|
663
|
+
target: 'main:2.0',
|
|
664
|
+
cwd: '/repo',
|
|
665
|
+
command: 'codex',
|
|
666
|
+
suggestedName: 'codex',
|
|
667
|
+
registrations: [{ scopeType: 'team', scope: 'egp', agent: 'codex', remark: 'review' }],
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
pane: '%1',
|
|
671
|
+
target: 'main:1.0',
|
|
672
|
+
cwd: '/repo',
|
|
673
|
+
command: 'claude',
|
|
674
|
+
suggestedName: 'claude',
|
|
675
|
+
registrations: [{ scopeType: 'team', scope: 'egp', agent: 'claude' }],
|
|
676
|
+
},
|
|
677
|
+
]);
|
|
678
|
+
|
|
679
|
+
cmdTeam(ctx, ['ls', 'egp']);
|
|
680
|
+
|
|
681
|
+
expect(ctx.ui.table).toHaveBeenCalledWith(
|
|
682
|
+
['NAME', 'PANE', 'TARGET', 'CWD', 'CMD', 'REMARK'],
|
|
683
|
+
[
|
|
684
|
+
['claude', '%1', 'main:1.0', '/repo', 'claude', '-'],
|
|
685
|
+
['codex', '%2', 'main:2.0', '/repo', 'codex', 'review'],
|
|
686
|
+
]
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('cmdTeam ls outputs JSON and reports empty teams', () => {
|
|
691
|
+
const ctx = createCtx(testDir, { flags: { json: true } });
|
|
692
|
+
(ctx.tmux.listTeamPanes as ReturnType<typeof vi.fn>).mockReturnValue([]);
|
|
693
|
+
|
|
694
|
+
cmdTeam(ctx, ['ls', 'missing']);
|
|
695
|
+
|
|
696
|
+
expect((ctx.ui as any).jsonCalls).toEqual([{ team: 'missing', members: [] }]);
|
|
697
|
+
|
|
698
|
+
const humanCtx = createCtx(testDir);
|
|
699
|
+
cmdTeam(humanCtx, ['ls', 'missing']);
|
|
700
|
+
expect(humanCtx.ui.info).toHaveBeenCalledWith(
|
|
701
|
+
'No agents in team "missing". Use \'tmt team add missing <name>\' to add one.'
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('cmdTeam add registers current pane in a team', () => {
|
|
706
|
+
const ctx = createCtx(testDir);
|
|
707
|
+
(ctx.tmux.getCurrentPaneId as ReturnType<typeof vi.fn>).mockReturnValue('%5');
|
|
708
|
+
(ctx.tmux.resolvePaneTarget as ReturnType<typeof vi.fn>).mockReturnValue('%5');
|
|
709
|
+
|
|
710
|
+
cmdTeam(ctx, ['add', 'egp', 'claude']);
|
|
711
|
+
|
|
712
|
+
expect(ctx.tmux.setAgentRegistration).toHaveBeenCalledWith(
|
|
713
|
+
'%5',
|
|
714
|
+
{ type: 'team', teamName: 'egp' },
|
|
715
|
+
{ name: 'claude' }
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('cmdTeam add supports explicit panes, remarks, JSON, and duplicate errors', () => {
|
|
720
|
+
const ctx = createCtx(testDir, { flags: { json: true } });
|
|
721
|
+
(ctx.tmux.resolvePaneTarget as ReturnType<typeof vi.fn>).mockReturnValue('%6');
|
|
722
|
+
|
|
723
|
+
cmdTeam(ctx, ['add', 'egp', 'codex', '1.2', 'reviewer']);
|
|
724
|
+
|
|
725
|
+
expect(ctx.tmux.setAgentRegistration).toHaveBeenCalledWith(
|
|
726
|
+
'%6',
|
|
727
|
+
{ type: 'team', teamName: 'egp' },
|
|
728
|
+
{ name: 'codex', remark: 'reviewer' }
|
|
729
|
+
);
|
|
730
|
+
expect((ctx.ui as any).jsonCalls).toEqual([
|
|
731
|
+
{ added: 'codex', team: 'egp', pane: '%6', remark: 'reviewer' },
|
|
732
|
+
]);
|
|
733
|
+
|
|
734
|
+
const duplicateCtx = createCtx(testDir);
|
|
735
|
+
(duplicateCtx.tmux.getAgentRegistry as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
736
|
+
paneRegistry: { codex: { pane: '%6' } },
|
|
737
|
+
agents: {},
|
|
738
|
+
});
|
|
739
|
+
expect(() => cmdTeam(duplicateCtx, ['add', 'egp', 'codex', '1.2'])).toThrow(
|
|
740
|
+
`exit(${ExitCodes.ERROR})`
|
|
741
|
+
);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('cmdTeam add requires a pane when outside tmux', () => {
|
|
745
|
+
const ctx = createCtx(testDir);
|
|
746
|
+
|
|
747
|
+
expect(() => cmdTeam(ctx, ['add', 'egp', 'codex'])).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
748
|
+
expect(ctx.ui.error).toHaveBeenCalledWith(
|
|
749
|
+
'Not running inside tmux. Provide a pane target: tmt team add <team> <name> <pane>'
|
|
750
|
+
);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('cmdTeam add validates arguments and pane targets', () => {
|
|
754
|
+
const ctx = createCtx(testDir);
|
|
755
|
+
expect(() => cmdTeam(ctx, ['add', 'egp'])).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
756
|
+
expect(ctx.ui.error).toHaveBeenCalledWith(
|
|
757
|
+
'Usage: tmux-team team add <team> <name> [pane] [remark]'
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const paneCtx = createCtx(testDir);
|
|
761
|
+
(paneCtx.tmux.resolvePaneTarget as ReturnType<typeof vi.fn>).mockReturnValue(null);
|
|
762
|
+
expect(() => cmdTeam(paneCtx, ['add', 'egp', 'codex', 'missing'])).toThrow(
|
|
763
|
+
`exit(${ExitCodes.PANE_NOT_FOUND})`
|
|
764
|
+
);
|
|
765
|
+
expect(paneCtx.ui.error).toHaveBeenCalledWith("Pane 'missing' not found. Is tmux running?");
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('cmdTeam panes table groups by team/workspace scope before pane order', () => {
|
|
531
769
|
const ctx = createCtx(testDir);
|
|
532
770
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
533
771
|
(ctx.tmux.listTeamPanes as ReturnType<typeof vi.fn>).mockReturnValue([
|
|
@@ -565,7 +803,7 @@ describe('basic commands', () => {
|
|
|
565
803
|
},
|
|
566
804
|
]);
|
|
567
805
|
|
|
568
|
-
cmdTeam(ctx, ['
|
|
806
|
+
cmdTeam(ctx, ['panes']);
|
|
569
807
|
|
|
570
808
|
expect(logSpy.mock.calls.map((call) => call[0])).toEqual([
|
|
571
809
|
'Team: alpha (gemini)',
|
|
@@ -610,6 +848,9 @@ describe('basic commands', () => {
|
|
|
610
848
|
const ctx = createCtx(testDir);
|
|
611
849
|
(ctx.tmux.listTeams as ReturnType<typeof vi.fn>).mockReturnValue({ egp: ['claude'] });
|
|
612
850
|
|
|
851
|
+
expect(() => cmdTeam(ctx, ['rm'])).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
852
|
+
expect(ctx.ui.error).toHaveBeenCalledWith('Usage: tmux-team team rm <team> --force');
|
|
853
|
+
|
|
613
854
|
expect(() => cmdTeam(ctx, ['rm', 'egp'])).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
614
855
|
|
|
615
856
|
(ctx.tmux.listTeams as ReturnType<typeof vi.fn>).mockReturnValue({});
|
package/src/commands/check.ts
CHANGED
|
@@ -6,11 +6,31 @@ import type { Context } from '../types.js';
|
|
|
6
6
|
import { ExitCodes } from '../exits.js';
|
|
7
7
|
import { colors } from '../ui.js';
|
|
8
8
|
|
|
9
|
+
function teamHintForMissingAgent(ctx: Context, target: string): string | null {
|
|
10
|
+
if (ctx.flags.team) return null;
|
|
11
|
+
|
|
12
|
+
const matches = Object.entries(ctx.tmux.listTeams())
|
|
13
|
+
.filter(([, agents]) => agents.includes(target))
|
|
14
|
+
.map(([teamName]) => teamName)
|
|
15
|
+
.sort();
|
|
16
|
+
|
|
17
|
+
if (matches.length === 0) return null;
|
|
18
|
+
if (matches.length === 1) {
|
|
19
|
+
return `Agent '${target}' is in shared team '${matches[0]}'. Specify it: tmt check ${target} --team ${matches[0]}`;
|
|
20
|
+
}
|
|
21
|
+
return `Agent '${target}' is in multiple shared teams: ${matches.join(', ')}. Specify one with --team <team>.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
export function cmdCheck(ctx: Context, target: string, lines?: number): void {
|
|
10
25
|
const { ui, config, tmux, flags, exit } = ctx;
|
|
11
26
|
|
|
12
27
|
if (!config.paneRegistry[target]) {
|
|
13
28
|
const available = Object.keys(config.paneRegistry).join(', ');
|
|
29
|
+
const teamHint = teamHintForMissingAgent(ctx, target);
|
|
30
|
+
if (teamHint) {
|
|
31
|
+
ui.error(teamHint);
|
|
32
|
+
exit(ExitCodes.PANE_NOT_FOUND);
|
|
33
|
+
}
|
|
14
34
|
ui.error(`Agent '${target}' not found. Available: ${available || 'none'}`);
|
|
15
35
|
exit(ExitCodes.PANE_NOT_FOUND);
|
|
16
36
|
}
|
package/src/commands/help.ts
CHANGED
|
@@ -47,13 +47,13 @@ ${colors.yellow('USAGE')}
|
|
|
47
47
|
${colors.yellow('COMMANDS')}
|
|
48
48
|
${colors.green('talk')} <target> <message> Send message to an agent (or "all")
|
|
49
49
|
${colors.green('check')} <target> [lines] Capture output from agent's pane
|
|
50
|
-
${colors.green('list')}
|
|
50
|
+
${colors.green('list')} [team|pane] List workspace, team, or pane status
|
|
51
51
|
${colors.green('add')} <name> <pane> [remark] Add a new agent
|
|
52
52
|
${colors.green('this')} <name> [remark] Register current pane as an agent
|
|
53
53
|
${colors.green('update')} <name> [options] Update an agent's config
|
|
54
54
|
${colors.green('remove')} <name> Remove an agent
|
|
55
55
|
${colors.green('migrate')} [--dry-run] Copy legacy JSON registry to tmux metadata
|
|
56
|
-
${colors.green('team')} [ls|rm]
|
|
56
|
+
${colors.green('team')} [ls|add|rm|panes] Manage shared teams
|
|
57
57
|
${colors.green('install')} [claude|codex] Install tmux-team for an AI agent
|
|
58
58
|
${colors.green('init')} Create empty tmux-team.json
|
|
59
59
|
${colors.green('config')} [show|set|clear] View/modify settings
|
|
@@ -88,13 +88,15 @@ ${colors.yellow('EXAMPLES')}${
|
|
|
88
88
|
tmux-team check codex ${colors.dim('← read response later')}`
|
|
89
89
|
}
|
|
90
90
|
tmux-team list --json
|
|
91
|
+
tmux-team list acme-app
|
|
92
|
+
tmux-team list main.1.0
|
|
91
93
|
tmux-team add codex 10.1 "Code review specialist"
|
|
92
94
|
|
|
93
95
|
${colors.yellow('CONFIG')}
|
|
94
96
|
Runtime: tmux pane metadata (agent registry)
|
|
95
97
|
Local: ./tmux-team.json (legacy registry + $config override)
|
|
96
98
|
Global: ~/.config/tmux-team/config.json (settings)
|
|
97
|
-
Teams: tmux pane metadata; team
|
|
99
|
+
Teams: tmux pane metadata; team panes shows cwd + workspace/team scopes
|
|
98
100
|
|
|
99
101
|
${colors.yellow('CHANGE MODE')}
|
|
100
102
|
tmux-team config set mode wait ${colors.dim('Enable wait mode (local)')}
|
package/src/commands/list.ts
CHANGED
|
@@ -3,9 +3,22 @@
|
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
import type { Context } from '../types.js';
|
|
6
|
+
import { listPaneStatus, listTeamMembers } from './team.js';
|
|
6
7
|
|
|
7
|
-
export function cmdList(ctx: Context): void {
|
|
8
|
+
export function cmdList(ctx: Context, target?: string): void {
|
|
8
9
|
const { ui, config, flags } = ctx;
|
|
10
|
+
|
|
11
|
+
if (target) {
|
|
12
|
+
const teams = ctx.tmux.listTeams();
|
|
13
|
+
if (teams[target]) {
|
|
14
|
+
listTeamMembers(ctx, [target]);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
listPaneStatus(ctx, target);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
const agents = Object.entries(config.paneRegistry);
|
|
10
23
|
|
|
11
24
|
if (flags.json) {
|
|
@@ -468,6 +468,27 @@ describe('cmdTalk - basic send', () => {
|
|
|
468
468
|
expect(ui.errors[0]).toContain("Agent 'unknown' not found");
|
|
469
469
|
});
|
|
470
470
|
|
|
471
|
+
it('requires explicit team when a missing agent appears in shared teams', async () => {
|
|
472
|
+
const tmux = createMockTmux();
|
|
473
|
+
vi.spyOn(tmux, 'listTeams').mockReturnValue({
|
|
474
|
+
frontend: ['codex'],
|
|
475
|
+
release: ['codex'],
|
|
476
|
+
});
|
|
477
|
+
const ui = createMockUI();
|
|
478
|
+
const ctx = createContext({
|
|
479
|
+
tmux,
|
|
480
|
+
ui,
|
|
481
|
+
paths: createTestPaths(testDir),
|
|
482
|
+
config: { paneRegistry: {} },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await expect(cmdTalk(ctx, 'codex', 'Hello')).rejects.toThrow('exit(3)');
|
|
486
|
+
|
|
487
|
+
expect(ui.errors).toEqual([
|
|
488
|
+
"Agent 'codex' is in multiple shared teams: frontend, release. Specify one with --team <team>.",
|
|
489
|
+
]);
|
|
490
|
+
});
|
|
491
|
+
|
|
471
492
|
it('outputs JSON when --json flag is set', async () => {
|
|
472
493
|
const tmux = createMockTmux();
|
|
473
494
|
const ui = createMockUI();
|
package/src/commands/talk.ts
CHANGED
|
@@ -319,6 +319,21 @@ function buildMessage(message: string, agentName: string, ctx: Context): string
|
|
|
319
319
|
return `[SYSTEM: ${preamble}]\n\n${message}`;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
function teamHintForMissingAgent(ctx: Context, target: string): string | null {
|
|
323
|
+
if (ctx.flags.team) return null;
|
|
324
|
+
|
|
325
|
+
const matches = Object.entries(ctx.tmux.listTeams())
|
|
326
|
+
.filter(([, agents]) => agents.includes(target))
|
|
327
|
+
.map(([teamName]) => teamName)
|
|
328
|
+
.sort();
|
|
329
|
+
|
|
330
|
+
if (matches.length === 0) return null;
|
|
331
|
+
if (matches.length === 1) {
|
|
332
|
+
return `Agent '${target}' is in shared team '${matches[0]}'. Specify it: tmt talk ${target} "..." --team ${matches[0]}`;
|
|
333
|
+
}
|
|
334
|
+
return `Agent '${target}' is in multiple shared teams: ${matches.join(', ')}. Specify one with --team <team>.`;
|
|
335
|
+
}
|
|
336
|
+
|
|
322
337
|
export async function cmdTalk(ctx: Context, target: string, message: string): Promise<void> {
|
|
323
338
|
const { ui, config, tmux, flags, exit } = ctx;
|
|
324
339
|
const waitEnabled = Boolean(flags.wait) || config.mode === 'wait';
|
|
@@ -389,6 +404,11 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
|
|
|
389
404
|
// Single agent
|
|
390
405
|
if (!config.paneRegistry[target]) {
|
|
391
406
|
const available = Object.keys(config.paneRegistry).join(', ');
|
|
407
|
+
const teamHint = teamHintForMissingAgent(ctx, target);
|
|
408
|
+
if (teamHint) {
|
|
409
|
+
ui.error(teamHint);
|
|
410
|
+
exit(ExitCodes.PANE_NOT_FOUND);
|
|
411
|
+
}
|
|
392
412
|
ui.error(`Agent '${target}' not found. Available: ${available || 'none'}`);
|
|
393
413
|
exit(ExitCodes.PANE_NOT_FOUND);
|
|
394
414
|
}
|
package/src/commands/team.ts
CHANGED
|
@@ -2,44 +2,57 @@
|
|
|
2
2
|
// team command - inspect pane team/workspace scope and manage explicit shared teams
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
|
-
import type { Context, TeamPaneInfo, TeamPaneRegistration } from '../types.js';
|
|
5
|
+
import type { Context, RegistryScope, TeamPaneInfo, TeamPaneRegistration } from '../types.js';
|
|
6
6
|
import { ExitCodes } from '../exits.js';
|
|
7
|
+
import { registrationFromEntry } from '../registry.js';
|
|
7
8
|
|
|
8
9
|
export function cmdTeam(ctx: Context, args: string[]): void {
|
|
9
|
-
const subcommand = args[0]
|
|
10
|
+
const subcommand = args[0];
|
|
10
11
|
|
|
11
12
|
switch (subcommand) {
|
|
13
|
+
case undefined:
|
|
14
|
+
listTeamNames(ctx, []);
|
|
15
|
+
break;
|
|
12
16
|
case 'ls':
|
|
13
17
|
case 'list':
|
|
14
|
-
|
|
18
|
+
listTeamMembers(ctx, args.slice(1));
|
|
19
|
+
break;
|
|
20
|
+
case 'add':
|
|
21
|
+
addTeamMember(ctx, args.slice(1));
|
|
15
22
|
break;
|
|
16
23
|
case 'rm':
|
|
17
24
|
case 'remove':
|
|
18
25
|
removeTeam(ctx, args.slice(1));
|
|
19
26
|
break;
|
|
27
|
+
case 'panes':
|
|
28
|
+
case 'inventory':
|
|
29
|
+
listPaneInventory(ctx, args.slice(1));
|
|
30
|
+
break;
|
|
20
31
|
default:
|
|
21
32
|
ctx.ui.error(`Unknown team subcommand: ${subcommand}`);
|
|
22
|
-
ctx.ui.error(
|
|
33
|
+
ctx.ui.error(
|
|
34
|
+
'Usage: tmux-team team [ls <team>|add <team> <name> [pane]|rm <team> --force|panes]'
|
|
35
|
+
);
|
|
23
36
|
ctx.exit(ExitCodes.ERROR);
|
|
24
37
|
}
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
function
|
|
40
|
+
function listTeamNames(ctx: Context, args: string[]): void {
|
|
28
41
|
const teams = ctx.tmux.listTeams();
|
|
29
|
-
const panes = ctx.tmux.listTeamPanes();
|
|
30
42
|
const summaryOnly = args.includes('--summary');
|
|
31
43
|
|
|
32
44
|
if (ctx.flags.json) {
|
|
33
|
-
ctx.ui.json({ teams
|
|
45
|
+
ctx.ui.json({ teams });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rows = Object.entries(teams).sort(([a], [b]) => a.localeCompare(b));
|
|
50
|
+
if (rows.length === 0) {
|
|
51
|
+
ctx.ui.info('No shared teams found.');
|
|
34
52
|
return;
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
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
56
|
ctx.ui.table(
|
|
44
57
|
['TEAM', 'AGENTS'],
|
|
45
58
|
rows.map(([teamName, agents]) => [teamName, agents.join(', ') || '-'])
|
|
@@ -47,6 +60,92 @@ function listTeams(ctx: Context, args: string[]): void {
|
|
|
47
60
|
return;
|
|
48
61
|
}
|
|
49
62
|
|
|
63
|
+
ctx.ui.table(
|
|
64
|
+
['TEAM', 'MEMBERS'],
|
|
65
|
+
rows.map(([teamName, agents]) => [teamName, String(agents.length)])
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function listTeamMembers(ctx: Context, args: string[]): void {
|
|
70
|
+
const teamName = args.find((arg) => !arg.startsWith('-'));
|
|
71
|
+
if (!teamName) {
|
|
72
|
+
listTeamNames(ctx, args);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rows = teamMemberRows(ctx.tmux.listTeamPanes(), teamName);
|
|
77
|
+
if (ctx.flags.json) {
|
|
78
|
+
ctx.ui.json({ team: teamName, members: rows.map(rowToMemberJson) });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (rows.length === 0) {
|
|
83
|
+
ctx.ui.info(
|
|
84
|
+
`No agents in team "${teamName}". Use 'tmt team add ${teamName} <name>' to add one.`
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ctx.ui.table(['NAME', 'PANE', 'TARGET', 'CWD', 'CMD', 'REMARK'], rows);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function addTeamMember(ctx: Context, args: string[]): void {
|
|
93
|
+
const [teamName, name, maybePane, ...remarkParts] = args.filter((arg) => !arg.startsWith('-'));
|
|
94
|
+
if (!teamName || !name) {
|
|
95
|
+
ctx.ui.error('Usage: tmux-team team add <team> <name> [pane] [remark]');
|
|
96
|
+
ctx.exit(ExitCodes.ERROR);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const scope: RegistryScope = { type: 'team', teamName };
|
|
100
|
+
const registry = ctx.tmux.getAgentRegistry(scope);
|
|
101
|
+
if (registry.paneRegistry[name]) {
|
|
102
|
+
ctx.ui.error(
|
|
103
|
+
`Agent '${name}' already exists in team '${teamName}'. Use 'tmux-team update --team ${teamName}' to modify.`
|
|
104
|
+
);
|
|
105
|
+
ctx.exit(ExitCodes.ERROR);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const targetPane = maybePane ?? ctx.tmux.getCurrentPaneId();
|
|
109
|
+
if (!targetPane) {
|
|
110
|
+
ctx.ui.error(
|
|
111
|
+
'Not running inside tmux. Provide a pane target: tmt team add <team> <name> <pane>'
|
|
112
|
+
);
|
|
113
|
+
ctx.exit(ExitCodes.ERROR);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const resolvedPane = ctx.tmux.resolvePaneTarget(targetPane);
|
|
117
|
+
if (!resolvedPane) {
|
|
118
|
+
ctx.ui.error(`Pane '${targetPane}' not found. Is tmux running?`);
|
|
119
|
+
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const remark = remarkParts.length > 0 ? remarkParts.join(' ') : undefined;
|
|
123
|
+
ctx.tmux.setAgentRegistration(
|
|
124
|
+
resolvedPane,
|
|
125
|
+
scope,
|
|
126
|
+
registrationFromEntry(name, {
|
|
127
|
+
pane: resolvedPane,
|
|
128
|
+
...(remark !== undefined && { remark }),
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (ctx.flags.json) {
|
|
133
|
+
ctx.ui.json({ added: name, team: teamName, pane: resolvedPane, remark });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ctx.ui.success(`Added agent '${name}' to team '${teamName}' at pane ${resolvedPane}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function listPaneInventory(ctx: Context, _args: string[]): void {
|
|
141
|
+
const teams = ctx.tmux.listTeams();
|
|
142
|
+
const panes = ctx.tmux.listTeamPanes();
|
|
143
|
+
|
|
144
|
+
if (ctx.flags.json) {
|
|
145
|
+
ctx.ui.json({ teams, panes });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
50
149
|
const groups = panesToGroups(panes);
|
|
51
150
|
if (groups.length === 0) {
|
|
52
151
|
ctx.ui.info('No tmux panes found.');
|
|
@@ -60,6 +159,96 @@ function listTeams(ctx: Context, args: string[]): void {
|
|
|
60
159
|
}
|
|
61
160
|
}
|
|
62
161
|
|
|
162
|
+
export function listPaneStatus(ctx: Context, target: string): void {
|
|
163
|
+
const resolvedPane = resolvePaneLike(ctx, target);
|
|
164
|
+
if (!resolvedPane) {
|
|
165
|
+
ctx.ui.error(`Pane or team '${target}' not found.`);
|
|
166
|
+
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const pane = ctx.tmux.listTeamPanes().find((item) => item.pane === resolvedPane);
|
|
170
|
+
if (!pane) {
|
|
171
|
+
ctx.ui.error(`Pane '${target}' not found.`);
|
|
172
|
+
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ctx.flags.json) {
|
|
176
|
+
ctx.ui.json({ pane });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`Pane: ${pane.pane}${pane.target ? ` (${pane.target})` : ''}`);
|
|
181
|
+
console.log(`CWD: ${pane.cwd ?? '-'}`);
|
|
182
|
+
console.log(`CMD: ${pane.command || '-'}`);
|
|
183
|
+
|
|
184
|
+
if (pane.registrations.length === 0) {
|
|
185
|
+
ctx.ui.info('No registrations on this pane.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
ctx.ui.table(
|
|
190
|
+
['SCOPE', 'NAME', 'REMARK'],
|
|
191
|
+
pane.registrations.map((registration) => [
|
|
192
|
+
registration.scopeType === 'team'
|
|
193
|
+
? `team:${registration.scope}`
|
|
194
|
+
: `workspace:${registration.scope}`,
|
|
195
|
+
registration.agent,
|
|
196
|
+
registration.remark ?? '-',
|
|
197
|
+
])
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolvePaneLike(ctx: Context, target: string): string | null {
|
|
202
|
+
const candidates = [target];
|
|
203
|
+
const dotted = target.match(/^([^.]+)\.([^.]+)\.([^.]+)$/);
|
|
204
|
+
if (dotted) {
|
|
205
|
+
candidates.push(`${dotted[1]}:${dotted[2]}.${dotted[3]}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const candidate of candidates) {
|
|
209
|
+
const resolved = ctx.tmux.resolvePaneTarget(candidate);
|
|
210
|
+
if (resolved) return resolved;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function teamMemberRows(panes: TeamPaneInfo[], teamName: string): string[][] {
|
|
217
|
+
const rows: string[][] = [];
|
|
218
|
+
for (const pane of panes) {
|
|
219
|
+
for (const registration of pane.registrations) {
|
|
220
|
+
if (registration.scopeType !== 'team' || registration.scope !== teamName) continue;
|
|
221
|
+
rows.push([
|
|
222
|
+
registration.agent,
|
|
223
|
+
pane.pane,
|
|
224
|
+
pane.target ?? '-',
|
|
225
|
+
pane.cwd ?? '-',
|
|
226
|
+
pane.command || '-',
|
|
227
|
+
registration.remark ?? '-',
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return rows.sort((a, b) => a[0].localeCompare(b[0]) || a[1].localeCompare(b[1]));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function rowToMemberJson(row: string[]): {
|
|
235
|
+
name: string;
|
|
236
|
+
pane: string;
|
|
237
|
+
target: string;
|
|
238
|
+
cwd: string;
|
|
239
|
+
command: string;
|
|
240
|
+
remark?: string;
|
|
241
|
+
} {
|
|
242
|
+
return {
|
|
243
|
+
name: row[0],
|
|
244
|
+
pane: row[1],
|
|
245
|
+
target: row[2],
|
|
246
|
+
cwd: row[3],
|
|
247
|
+
command: row[4],
|
|
248
|
+
...(row[5] !== '-' && { remark: row[5] }),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
63
252
|
interface PaneGroup {
|
|
64
253
|
key: string;
|
|
65
254
|
title: string;
|