tmux-ide 2.9.0-beta.48 → 2.9.0-beta.49
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/bin/cli.js +2055 -1907
- package/package.json +2 -2
- package/packages/contracts/src/cohesion-fixture.ts +2 -0
- package/packages/contracts/src/fleet-catalog.ts +2 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/pane-team.ts +32 -0
- package/packages/daemon/dist/command-center/discovery.js +4 -1
- package/packages/daemon/dist/command-center/resources/application-shell.js +2 -1
- package/packages/daemon/dist/command-center/resources/fleet-catalog.js +2 -0
- package/packages/daemon/dist/lib/daemon-embed.js +7 -5
- package/packages/daemon/dist/pane-team.js +28 -0
- package/packages/daemon/dist/terminal/attachments/agent-status-probe.js +21 -1
- package/packages/daemon/dist/terminal/attachments/claude-team-names.js +22 -6
- package/packages/daemon/dist/terminal/attachments/manual-pane-team.js +30 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +1 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +3 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-team-groups.js +22 -0
- package/packages/daemon/dist/tui/mirror/ui/agent-row.jsx +2 -2
- package/packages/daemon/src/command-center/discovery.ts +6 -1
- package/packages/daemon/src/command-center/resources/application-shell.ts +4 -1
- package/packages/daemon/src/command-center/resources/fleet-catalog.ts +2 -0
- package/packages/daemon/src/lib/daemon-embed.ts +7 -5
- package/packages/daemon/src/pane-team.ts +42 -0
- package/packages/daemon/src/terminal/attachments/agent-status-probe.ts +29 -1
- package/packages/daemon/src/terminal/attachments/claude-team-names.ts +43 -8
- package/packages/daemon/src/terminal/attachments/manual-pane-team.ts +31 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +3 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +5 -1
- package/packages/daemon/src/tui/mirror/runtime/application-team-groups.ts +40 -0
- package/packages/daemon/src/tui/mirror/ui/agent-row.tsx +7 -3
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { runtimeTmuxArgs } from "./lib/runtime-namespace.ts";
|
|
4
|
+
import { manualPaneTeamStamp, PANE_TEAM_OPTION } from "./terminal/attachments/manual-pane-team.ts";
|
|
5
|
+
|
|
6
|
+
/** Explicit membership only. Never launches, stops, or messages an agent. */
|
|
7
|
+
export function assignPaneTeam(
|
|
8
|
+
options: {
|
|
9
|
+
paneId: string;
|
|
10
|
+
name: string | null;
|
|
11
|
+
socketPath?: string;
|
|
12
|
+
socketName?: string;
|
|
13
|
+
},
|
|
14
|
+
run: (args: string[]) => string = (args) =>
|
|
15
|
+
execFileSync("tmux", args, { encoding: "utf8", timeout: 5000 }),
|
|
16
|
+
) {
|
|
17
|
+
if (!/^%\d+$/u.test(options.paneId)) throw new Error("Use an exact pane ID such as %3");
|
|
18
|
+
if (options.socketPath && options.socketName) throw new Error("Choose one tmux socket selector");
|
|
19
|
+
if (options.socketPath && !isAbsolute(options.socketPath))
|
|
20
|
+
throw new Error("Socket path must be absolute");
|
|
21
|
+
if (options.socketName && !/^[a-zA-Z0-9_.-]+$/u.test(options.socketName))
|
|
22
|
+
throw new Error("Invalid socket name");
|
|
23
|
+
const args = (command: string[]) =>
|
|
24
|
+
options.socketPath
|
|
25
|
+
? ["-S", options.socketPath, ...command]
|
|
26
|
+
: options.socketName
|
|
27
|
+
? ["-L", options.socketName, ...command]
|
|
28
|
+
: runtimeTmuxArgs(command);
|
|
29
|
+
const pid = Number(
|
|
30
|
+
run(args(["display-message", "-p", "-t", options.paneId, "#{pane_pid}"])).trim(),
|
|
31
|
+
);
|
|
32
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error("Pane is no longer available");
|
|
33
|
+
const stamp = options.name === null ? null : manualPaneTeamStamp(options.name, pid);
|
|
34
|
+
run(
|
|
35
|
+
args(
|
|
36
|
+
stamp === null
|
|
37
|
+
? ["set-option", "-p", "-u", "-t", options.paneId, PANE_TEAM_OPTION]
|
|
38
|
+
: ["set-option", "-p", "-t", options.paneId, PANE_TEAM_OPTION, stamp],
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
return { paneId: options.paneId, team: options.name, source: stamp === null ? null : "manual" };
|
|
42
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readManualPaneTeam } from "./manual-pane-team.ts";
|
|
2
|
+
import type { PaneTeamMembership } from "@tmux-ide/contracts";
|
|
3
|
+
import type { ClaudeTeamMembershipReader } from "./claude-team-names.ts";
|
|
1
4
|
import type { ClaudeTeamNameReader } from "./claude-team-names.ts";
|
|
2
5
|
import type { NativePaneIdentity } from "@tmux-ide/contracts";
|
|
3
6
|
/**
|
|
@@ -44,6 +47,7 @@ import {
|
|
|
44
47
|
/** The per-pane facts the pure projector consumes (see `ApplicationShellPanePresentationFacts`). */
|
|
45
48
|
export interface AgentStatusPaneFacts {
|
|
46
49
|
readonly teamMemberName?: string;
|
|
50
|
+
readonly team?: PaneTeamMembership;
|
|
47
51
|
/** Stable manifest id resolved from the pane hint, command, or process tree. */
|
|
48
52
|
readonly agentKind: string | null;
|
|
49
53
|
/** Raw `@agent_state` (`"<state>:<epoch>"`), or null when unset. */
|
|
@@ -95,6 +99,7 @@ const AGENT_OPTIONS_FORMAT = [
|
|
|
95
99
|
"#{@agent_display_name}",
|
|
96
100
|
"#{@agent_hint}",
|
|
97
101
|
"#{pane_pid}",
|
|
102
|
+
"#{@tmux_ide_team}",
|
|
98
103
|
AGENT_LINE_SENTINEL,
|
|
99
104
|
].join(AGENT_FIELD_SEPARATOR);
|
|
100
105
|
|
|
@@ -119,6 +124,7 @@ export const SCRAPE_CAPTURE_BUDGET = 4;
|
|
|
119
124
|
const SCRAPE_CACHE_MAX_ENTRIES = 1024;
|
|
120
125
|
|
|
121
126
|
interface RawPaneOptions {
|
|
127
|
+
readonly team?: PaneTeamMembership;
|
|
122
128
|
readonly stateRaw: string | null;
|
|
123
129
|
readonly statusTextRaw: string | null;
|
|
124
130
|
readonly displayNameRaw: string | null;
|
|
@@ -153,12 +159,13 @@ function parseAgentOptions(stdout: string): ReadonlyMap<string, RawPaneOptions>
|
|
|
153
159
|
if (line.length === 0) continue;
|
|
154
160
|
const fields = line.split(AGENT_FIELD_SEPARATOR);
|
|
155
161
|
// paneId, state, statusText, displayName, hint, pid, sentinel
|
|
156
|
-
if (fields.length
|
|
162
|
+
if (![7, 8].includes(fields.length) || fields.at(-1) !== AGENT_LINE_SENTINEL) continue;
|
|
157
163
|
const runtimePaneId = fields[0]!;
|
|
158
164
|
if (!RUNTIME_PANE_ID.test(runtimePaneId)) continue;
|
|
159
165
|
const pidText = fields[5]!;
|
|
160
166
|
const pid = /^[0-9]+$/u.test(pidText) ? Number(pidText) : null;
|
|
161
167
|
result.set(runtimePaneId, {
|
|
168
|
+
team: fields.length === 8 ? readManualPaneTeam(fields[6], pid) : undefined,
|
|
162
169
|
stateRaw: emptyToNull(fields[1]!),
|
|
163
170
|
statusTextRaw: emptyToNull(fields[2]!),
|
|
164
171
|
displayNameRaw: emptyToNull(fields[3]!),
|
|
@@ -171,6 +178,7 @@ function parseAgentOptions(stdout: string): ReadonlyMap<string, RawPaneOptions>
|
|
|
171
178
|
|
|
172
179
|
export interface TmuxAgentStatusProbeDeps {
|
|
173
180
|
readonly readTeamNames?: ClaudeTeamNameReader;
|
|
181
|
+
readonly readTeamMemberships?: ClaudeTeamMembershipReader;
|
|
174
182
|
/** null proves no native dispatch; failures must never retry through stock capture. */
|
|
175
183
|
readonly captureNative?: (
|
|
176
184
|
pane: AgentStatusProbePane,
|
|
@@ -305,6 +313,7 @@ export function createTmuxAgentStatusProbe(deps: TmuxAgentStatusProbeDeps): Agen
|
|
|
305
313
|
agentKind: string | null,
|
|
306
314
|
): void => {
|
|
307
315
|
facts.set(pane.runtimePaneId, {
|
|
316
|
+
...(raw?.team ? { team: raw.team } : {}),
|
|
308
317
|
agentKind,
|
|
309
318
|
agentStateRaw: raw?.stateRaw ?? null,
|
|
310
319
|
agentStatusTextRaw: raw?.statusTextRaw ?? null,
|
|
@@ -432,6 +441,25 @@ export function createTmuxAgentStatusProbe(deps: TmuxAgentStatusProbeDeps): Agen
|
|
|
432
441
|
emit(pane, raw, verdict, manifest.id);
|
|
433
442
|
}
|
|
434
443
|
|
|
444
|
+
if (deps.readTeamMemberships) {
|
|
445
|
+
const memberships = await deps.readTeamMemberships(
|
|
446
|
+
input.panes.flatMap((pane) => {
|
|
447
|
+
const pid = options.get(pane.runtimePaneId)?.pid;
|
|
448
|
+
return pid ? [{ runtimePaneId: pane.runtimePaneId, pid }] : [];
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
throwIfAborted(signal);
|
|
452
|
+
for (const [paneId, member] of memberships) {
|
|
453
|
+
const fact = facts.get(paneId);
|
|
454
|
+
if (fact)
|
|
455
|
+
facts.set(paneId, {
|
|
456
|
+
...fact,
|
|
457
|
+
teamMemberName: member.name,
|
|
458
|
+
team: fact.team ?? member.team,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
435
463
|
if (deps.readTeamNames) {
|
|
436
464
|
const names = await deps.readTeamNames(
|
|
437
465
|
input.panes.flatMap((pane) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { PaneTeamMembership } from "@tmux-ide/contracts";
|
|
1
3
|
/** Read-only compatibility adapter for Claude Code's runtime team metadata.
|
|
2
4
|
* Names are presentation evidence, never agent identity or permission authority.
|
|
3
5
|
*/
|
|
@@ -75,19 +77,35 @@ function matchesProcess(command: string, member: ClaudeTeamMember): boolean {
|
|
|
75
77
|
return true;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
export function
|
|
80
|
+
export function resolveClaudeTeamMember(
|
|
79
81
|
pane: TeamPaneProcess,
|
|
80
82
|
members: readonly ClaudeTeamMember[],
|
|
81
83
|
processes: ProcEntry[],
|
|
82
|
-
):
|
|
84
|
+
): ClaudeTeamMember | null {
|
|
83
85
|
// A native pane id alone cannot bind a team: different servers reuse %N.
|
|
84
86
|
// Its live shell PID scopes the match to this host and this pane's subtree.
|
|
85
87
|
const subtree = subtreeEntries(processes, pane.pid);
|
|
86
88
|
const matches = members.filter(
|
|
87
89
|
(m) => m.paneId === pane.runtimePaneId && subtree.some((p) => matchesProcess(p.command, m)),
|
|
88
90
|
);
|
|
89
|
-
return matches.length === 1 ? matches[0]
|
|
91
|
+
return matches.length === 1 ? matches[0]! : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveClaudeTeamName(
|
|
95
|
+
pane: TeamPaneProcess,
|
|
96
|
+
members: readonly ClaudeTeamMember[],
|
|
97
|
+
processes: ProcEntry[],
|
|
98
|
+
): string | null {
|
|
99
|
+
return resolveClaudeTeamMember(pane, members, processes)?.name ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ClaudePaneTeam {
|
|
103
|
+
readonly name: string;
|
|
104
|
+
readonly team: PaneTeamMembership;
|
|
90
105
|
}
|
|
106
|
+
export type ClaudeTeamMembershipReader = (
|
|
107
|
+
panes: readonly TeamPaneProcess[],
|
|
108
|
+
) => Promise<ReadonlyMap<string, ClaudePaneTeam>>;
|
|
91
109
|
|
|
92
110
|
async function readMembers(directory: string): Promise<ClaudeTeamMember[]> {
|
|
93
111
|
try {
|
|
@@ -126,14 +144,14 @@ export type ClaudeTeamNameReader = (
|
|
|
126
144
|
panes: readonly TeamPaneProcess[],
|
|
127
145
|
) => Promise<ReadonlyMap<string, string>>;
|
|
128
146
|
|
|
129
|
-
export function
|
|
147
|
+
export function createClaudeTeamMembershipReader(
|
|
130
148
|
directory: string,
|
|
131
149
|
deps: {
|
|
132
150
|
readMembers?: () => Promise<ClaudeTeamMember[]>;
|
|
133
151
|
readProcesses?: () => Promise<ProcEntry[]>;
|
|
134
152
|
now?: () => number;
|
|
135
153
|
} = {},
|
|
136
|
-
):
|
|
154
|
+
): ClaudeTeamMembershipReader {
|
|
137
155
|
const now = deps.now ?? Date.now;
|
|
138
156
|
let snapshot: { at: number; members: ClaudeTeamMember[]; processes: ProcEntry[] } | null = null;
|
|
139
157
|
let pending: Promise<void> | null = null;
|
|
@@ -156,11 +174,28 @@ export function createClaudeTeamNameReader(
|
|
|
156
174
|
await pending;
|
|
157
175
|
}
|
|
158
176
|
const current = snapshot!;
|
|
159
|
-
const names = new Map<string,
|
|
177
|
+
const names = new Map<string, ClaudePaneTeam>();
|
|
160
178
|
for (const pane of panes) {
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
179
|
+
const member = resolveClaudeTeamMember(pane, current.members, current.processes);
|
|
180
|
+
if (member)
|
|
181
|
+
names.set(pane.runtimePaneId, {
|
|
182
|
+
name: member.name,
|
|
183
|
+
team: {
|
|
184
|
+
id: `team.${createHash("sha256").update(member.team).digest("hex").slice(0, 32)}`,
|
|
185
|
+
name: member.team.slice(0, 80),
|
|
186
|
+
source: "claude-code",
|
|
187
|
+
},
|
|
188
|
+
});
|
|
163
189
|
}
|
|
164
190
|
return names;
|
|
165
191
|
};
|
|
166
192
|
}
|
|
193
|
+
|
|
194
|
+
/** Compatibility API for callers interested only in the shared display name. */
|
|
195
|
+
export function createClaudeTeamNameReader(
|
|
196
|
+
directory: string,
|
|
197
|
+
deps: Parameters<typeof createClaudeTeamMembershipReader>[1] = {},
|
|
198
|
+
): ClaudeTeamNameReader {
|
|
199
|
+
const read = createClaudeTeamMembershipReader(directory, deps);
|
|
200
|
+
return async (panes) => new Map([...(await read(panes))].map(([id, value]) => [id, value.name]));
|
|
201
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { PaneTeamMembershipSchemaZ, type PaneTeamMembership } from "@tmux-ide/contracts";
|
|
3
|
+
|
|
4
|
+
export const PANE_TEAM_OPTION = "@tmux_ide_team";
|
|
5
|
+
|
|
6
|
+
/** Pane-local metadata survives layout moves, but never a replacement process. */
|
|
7
|
+
export function manualPaneTeamStamp(name: string, pid: number): string {
|
|
8
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error("Invalid pane process");
|
|
9
|
+
const team = PaneTeamMembershipSchemaZ.parse({
|
|
10
|
+
id: `team.${createHash("sha256").update(name).digest("hex").slice(0, 32)}`,
|
|
11
|
+
name,
|
|
12
|
+
source: "manual",
|
|
13
|
+
});
|
|
14
|
+
return `v1.${Buffer.from(JSON.stringify({ version: 1, pid, team })).toString("base64url")}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readManualPaneTeam(
|
|
18
|
+
raw: string | null | undefined,
|
|
19
|
+
pid: number | null,
|
|
20
|
+
): PaneTeamMembership | undefined {
|
|
21
|
+
if (!raw || raw.length > 2048 || !pid) return undefined;
|
|
22
|
+
try {
|
|
23
|
+
if (!/^v1\.[A-Za-z0-9_-]+$/u.test(raw)) return undefined;
|
|
24
|
+
const value = JSON.parse(Buffer.from(raw.slice(3), "base64url").toString("utf8"));
|
|
25
|
+
if (value.version !== 1 || value.pid !== pid) return undefined;
|
|
26
|
+
const parsed = PaneTeamMembershipSchemaZ.safeParse(value.team);
|
|
27
|
+
return parsed.success && parsed.data.source === "manual" ? parsed.data : undefined;
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -336,6 +336,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
|
|
|
336
336
|
theme={props.theme}
|
|
337
337
|
id={`home-agent:${key}`}
|
|
338
338
|
name={row().name}
|
|
339
|
+
team={row().team}
|
|
339
340
|
context={[row().machineLabel, row().serverLabel, row().sessionName]
|
|
340
341
|
.filter(Boolean)
|
|
341
342
|
.join(" · ")}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PaneTeamMembership } from "@tmux-ide/contracts";
|
|
1
2
|
import type { NativePaneIdentity } from "@tmux-ide/contracts";
|
|
2
3
|
import type {
|
|
3
4
|
AgentActivity,
|
|
@@ -10,6 +11,7 @@ import type { ApplicationHomeCatalogSession } from "./application-home-catalog.t
|
|
|
10
11
|
import { terminalAgentStatusLabel } from "./application-terminal-workspace-policy.ts";
|
|
11
12
|
|
|
12
13
|
export interface HomeAgentRow {
|
|
14
|
+
readonly team?: PaneTeamMembership;
|
|
13
15
|
readonly nativeIdentity: NativePaneIdentity | null;
|
|
14
16
|
readonly interactionEndpoint: Extract<InteractionPaneEndpoint, { kind: "pane" }> | null;
|
|
15
17
|
readonly key: string;
|
|
@@ -97,6 +99,7 @@ export function projectHomeAgentRows(
|
|
|
97
99
|
resource.attachability.semanticPaneId === agent.paneId,
|
|
98
100
|
)?.windowResourceId ?? null,
|
|
99
101
|
name: agent.name,
|
|
102
|
+
...(agent.team ? { team: agent.team } : {}),
|
|
100
103
|
harness: agent.harness,
|
|
101
104
|
activity: agent.activity,
|
|
102
105
|
attention: agent.attention,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { groupApplicationTeamRows } from "./application-team-groups.ts";
|
|
1
2
|
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
|
|
2
3
|
import type {
|
|
3
4
|
ApplicationMachineAgent,
|
|
@@ -84,6 +85,7 @@ export function projectHomeFleet(
|
|
|
84
85
|
? rows.filter((row) =>
|
|
85
86
|
[
|
|
86
87
|
row.name,
|
|
88
|
+
row.team?.name,
|
|
87
89
|
row.harness,
|
|
88
90
|
row.projectName,
|
|
89
91
|
row.sessionName,
|
|
@@ -100,7 +102,7 @@ export function projectHomeFleet(
|
|
|
100
102
|
? "loading"
|
|
101
103
|
: "unavailable"
|
|
102
104
|
: "live",
|
|
103
|
-
rows: matching,
|
|
105
|
+
rows: groupApplicationTeamRows(matching),
|
|
104
106
|
observedSessions,
|
|
105
107
|
totalSessions,
|
|
106
108
|
loadingSessions,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { groupApplicationTeamRows } from "./application-team-groups.ts";
|
|
2
|
+
import type { PaneTeamMembership } from "@tmux-ide/contracts";
|
|
1
3
|
import type { NativePaneIdentity } from "@tmux-ide/contracts";
|
|
2
4
|
import type { PaneInteractionEndpoint } from "../ui/pane-interaction-presentation.ts";
|
|
3
5
|
import type { InteractionPaneEndpoint } from "@tmux-ide/contracts";
|
|
@@ -31,6 +33,7 @@ import { Surface } from "../ui/surface.tsx";
|
|
|
31
33
|
import { useKeyboardRoute } from "../ui/keyboard-router.tsx";
|
|
32
34
|
|
|
33
35
|
export interface ApplicationMachineAgent {
|
|
36
|
+
readonly team?: PaneTeamMembership;
|
|
34
37
|
readonly nativeIdentity: NativePaneIdentity | null;
|
|
35
38
|
readonly interactionEndpoint: Extract<InteractionPaneEndpoint, { kind: "pane" }> | null;
|
|
36
39
|
readonly daemonInstanceId?: string;
|
|
@@ -168,7 +171,7 @@ export function ApplicationMachineSidebar(props: {
|
|
|
168
171
|
const preferenceKey = (group: ApplicationMachineGroup) => group.environmentId ?? group.id;
|
|
169
172
|
const rows = createMemo<readonly Row[]>(() => [
|
|
170
173
|
...props.model.groups().flatMap((group) =>
|
|
171
|
-
(group.agents ?? []).map((agent) => ({
|
|
174
|
+
groupApplicationTeamRows(group.agents ?? []).map((agent) => ({
|
|
172
175
|
key: JSON.stringify([
|
|
173
176
|
group.id,
|
|
174
177
|
"agent",
|
|
@@ -583,6 +586,7 @@ export function ApplicationMachineSidebar(props: {
|
|
|
583
586
|
theme={props.theme}
|
|
584
587
|
id={`machine:${row.key}`}
|
|
585
588
|
name={agent().name}
|
|
589
|
+
team={agent().team}
|
|
586
590
|
context={[
|
|
587
591
|
row.group.label,
|
|
588
592
|
row.group.sessions.find(
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
paneTeamGroupKey,
|
|
3
|
+
type PaneTeamMembership,
|
|
4
|
+
type TmuxServerScope,
|
|
5
|
+
} from "@tmux-ide/contracts";
|
|
6
|
+
|
|
7
|
+
interface TeamRow {
|
|
8
|
+
readonly team?: PaneTeamMembership;
|
|
9
|
+
readonly machineId?: string;
|
|
10
|
+
readonly server?: TmuxServerScope;
|
|
11
|
+
readonly daemonInstanceId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function applicationTeamKey(row: TeamRow): string | null {
|
|
15
|
+
return row.team
|
|
16
|
+
? paneTeamGroupKey(
|
|
17
|
+
row.team,
|
|
18
|
+
row.machineId ?? "local",
|
|
19
|
+
row.server?.serverId ?? "default",
|
|
20
|
+
row.server?.generation ?? row.daemonInstanceId ?? "",
|
|
21
|
+
)
|
|
22
|
+
: null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Group by membership, never by name, activity, or current tmux window. Stable within groups. */
|
|
26
|
+
export function groupApplicationTeamRows<T extends TeamRow>(rows: readonly T[]): T[] {
|
|
27
|
+
const groups = new Map<string, T[]>();
|
|
28
|
+
const ungrouped: T[] = [];
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
const key = applicationTeamKey(row);
|
|
31
|
+
if (key === null) {
|
|
32
|
+
ungrouped.push(row);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const group = groups.get(key) ?? [];
|
|
36
|
+
group.push(row);
|
|
37
|
+
groups.set(key, group);
|
|
38
|
+
}
|
|
39
|
+
return [...groups.values()].flat().concat(ungrouped);
|
|
40
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type PaneInteractionEvent,
|
|
6
6
|
} from "./pane-interaction-presentation.ts";
|
|
7
7
|
/* @jsxImportSource @opentui/solid */
|
|
8
|
-
import type { AgentActivity } from "@tmux-ide/contracts";
|
|
8
|
+
import type { AgentActivity, PaneTeamMembership } from "@tmux-ide/contracts";
|
|
9
9
|
import { Show } from "solid-js";
|
|
10
10
|
import type { SemanticThemeSnapshot } from "../theme.ts";
|
|
11
11
|
import { clipTerminal } from "../terminal-text.ts";
|
|
@@ -19,6 +19,7 @@ export function AgentRow(props: {
|
|
|
19
19
|
theme: SemanticThemeSnapshot;
|
|
20
20
|
id: string;
|
|
21
21
|
name: string;
|
|
22
|
+
team?: PaneTeamMembership;
|
|
22
23
|
context: string;
|
|
23
24
|
activity: AgentActivity;
|
|
24
25
|
interaction?: PaneInteractionEvent;
|
|
@@ -83,7 +84,7 @@ export function AgentRow(props: {
|
|
|
83
84
|
theme={props.theme}
|
|
84
85
|
id={props.id}
|
|
85
86
|
width={props.width}
|
|
86
|
-
label={props.name}
|
|
87
|
+
label={props.compact && props.team ? `${props.team.name} / ${props.name}` : props.name}
|
|
87
88
|
marker={
|
|
88
89
|
receipt()
|
|
89
90
|
? receiptMarker()
|
|
@@ -113,7 +114,10 @@ export function AgentRow(props: {
|
|
|
113
114
|
width={props.width}
|
|
114
115
|
fg={props.selected ? palette().foreground : props.theme.roles.text.muted}
|
|
115
116
|
>
|
|
116
|
-
{clipTerminal(
|
|
117
|
+
{clipTerminal(
|
|
118
|
+
` ${[props.team?.name, props.context].filter(Boolean).join(" · ")}`,
|
|
119
|
+
props.width,
|
|
120
|
+
)}
|
|
117
121
|
</text>
|
|
118
122
|
</Show>
|
|
119
123
|
</box>
|