tmux-ide 2.9.0-beta.47 → 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 +2764 -2425
- 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 +12 -2
- package/packages/daemon/dist/command-center/resources/agent-graph-overlay.js +2 -2
- package/packages/daemon/dist/command-center/resources/application-shell.js +22 -15
- package/packages/daemon/dist/command-center/resources/fleet-catalog.js +8 -4
- package/packages/daemon/dist/command-center/resources/fleet-resource-route.js +2 -2
- package/packages/daemon/dist/lib/daemon-embed.js +24 -3
- package/packages/daemon/dist/pane-team.js +28 -0
- package/packages/daemon/dist/terminal/attachments/agent-status-probe.js +33 -1
- package/packages/daemon/dist/terminal/attachments/claude-team-names.js +146 -0
- package/packages/daemon/dist/terminal/attachments/manual-pane-team.js +30 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +1 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +1 -0
- package/packages/daemon/dist/terminal/protocol/pane-display-name.js +40 -11
- package/packages/daemon/dist/terminal/protocol/session-descriptor-discovery.js +4 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +17 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +1 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.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-new-agent-owner.js +86 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +5 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-input.js +1 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-team-groups.js +22 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +24 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +7 -5
- package/packages/daemon/dist/tui/mirror/ui/agent-row.jsx +2 -2
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +6 -0
- package/packages/daemon/src/command-center/discovery.ts +18 -2
- package/packages/daemon/src/command-center/resources/agent-graph-overlay.ts +2 -1
- package/packages/daemon/src/command-center/resources/application-shell.ts +24 -18
- package/packages/daemon/src/command-center/resources/fleet-catalog.ts +11 -3
- package/packages/daemon/src/command-center/resources/fleet-resource-route.ts +5 -3
- package/packages/daemon/src/command-center/server.ts +1 -1
- package/packages/daemon/src/lib/daemon-embed.ts +31 -4
- package/packages/daemon/src/pane-team.ts +42 -0
- package/packages/daemon/src/terminal/attachments/agent-status-probe.ts +46 -1
- package/packages/daemon/src/terminal/attachments/claude-team-names.ts +201 -0
- package/packages/daemon/src/terminal/attachments/manual-pane-team.ts +31 -0
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +2 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +1 -0
- package/packages/daemon/src/terminal/mirror/trusted-inventory.ts +1 -0
- package/packages/daemon/src/terminal/protocol/pane-display-name.ts +51 -13
- package/packages/daemon/src/terminal/protocol/session-descriptor-discovery.ts +4 -4
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +19 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +1 -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-new-agent-dialog.tsx +97 -0
- package/packages/daemon/src/tui/mirror/runtime/application-new-agent-owner.ts +89 -0
- package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +6 -0
- package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +5 -4
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +23 -1
- package/packages/daemon/src/tui/mirror/runtime/application-team-groups.ts +40 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +27 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +10 -5
- package/packages/daemon/src/tui/mirror/ui/agent-row.tsx +7 -3
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +7 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PaneTeamMembership } from "@tmux-ide/contracts";
|
|
2
|
+
import { resolvePaneDisplayName } from "../../terminal/protocol/pane-display-name.ts";
|
|
1
3
|
import type { NativePaneIdentity } from "@tmux-ide/contracts";
|
|
2
4
|
import { hostname } from "node:os";
|
|
3
5
|
import { basename } from "node:path";
|
|
@@ -44,6 +46,7 @@ export interface ApplicationShellPanePresentationFacts {
|
|
|
44
46
|
readonly active: boolean;
|
|
45
47
|
readonly role: string | null;
|
|
46
48
|
readonly name: string | null;
|
|
49
|
+
readonly nameSource?: string | null;
|
|
47
50
|
readonly type: string | null;
|
|
48
51
|
/** Stable detected agent kind; private discovery fact, never a wire identity. */
|
|
49
52
|
readonly agentKind?: string | null;
|
|
@@ -69,6 +72,8 @@ export interface ApplicationShellPanePresentationFacts {
|
|
|
69
72
|
readonly agentStatusTextRaw?: string | null;
|
|
70
73
|
/** Raw `@agent_display_name`; sanitized + freshness-gated in the pure layer. */
|
|
71
74
|
readonly agentDisplayNameRaw?: string | null;
|
|
75
|
+
readonly teamMemberName?: string;
|
|
76
|
+
readonly team?: PaneTeamMembership;
|
|
72
77
|
/**
|
|
73
78
|
* Screen-scrape fallback verdict the discovery layer resolved for panes
|
|
74
79
|
* WITHOUT fresh authority. `null` means authority was fresh (scrape skipped);
|
|
@@ -384,6 +389,7 @@ function deprecatedStandalonePaneIdentities(
|
|
|
384
389
|
export function harnessForPane(
|
|
385
390
|
pane: ApplicationShellPanePresentationFacts,
|
|
386
391
|
): "codex" | "claude-code" | "custom" {
|
|
392
|
+
if (pane.teamMemberName) return "claude-code";
|
|
387
393
|
const detected = pane.agentKind?.toLowerCase();
|
|
388
394
|
if (detected === "codex") return "codex";
|
|
389
395
|
if (detected === "claude" || detected === "claude-code") return "claude-code";
|
|
@@ -393,42 +399,41 @@ export function harnessForPane(
|
|
|
393
399
|
return "custom";
|
|
394
400
|
}
|
|
395
401
|
|
|
396
|
-
const GENERIC_PANE_LABELS = new Set(["shell", "terminal", "tmux"]);
|
|
397
|
-
|
|
398
|
-
function isGenericPaneLabel(value: string | null | undefined): boolean {
|
|
399
|
-
const normalized = value?.trim().toLowerCase() ?? "";
|
|
400
|
-
return normalized.length === 0 || GENERIC_PANE_LABELS.has(normalized);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
402
|
/**
|
|
404
403
|
* A promoted configless pane can be stamped as `Terminal` before the native
|
|
405
404
|
* inventory's process-tree pass resolves a wrapped/version-shaped agent
|
|
406
405
|
* executable. Once that stronger identity exists, do not keep publishing the
|
|
407
406
|
* provisional generic label into the sidebar and pane chrome.
|
|
408
407
|
*/
|
|
409
|
-
function resolvedAgentLabel(
|
|
408
|
+
export function resolvedAgentLabel(
|
|
410
409
|
pane: ApplicationShellPanePresentationFacts,
|
|
411
410
|
presentation: ResolvedAgentPresentation,
|
|
412
411
|
index: number,
|
|
413
412
|
): string {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
const resolved = resolvePaneDisplayName({
|
|
414
|
+
semanticPaneId: pane.semanticPaneId ?? `agent.${index}`,
|
|
415
|
+
configuredName: pane.name,
|
|
416
|
+
configuredNameSource: pane.nameSource,
|
|
417
|
+
agentDisplayName: presentation.displayName,
|
|
418
|
+
teamMemberName: pane.teamMemberName,
|
|
419
|
+
currentCommand: pane.currentCommand,
|
|
420
|
+
title: pane.title,
|
|
421
|
+
hostName: hostname(),
|
|
422
|
+
});
|
|
423
|
+
if (["manual", "agent", "title"].includes(resolved.source))
|
|
424
|
+
return label(resolved.name, `Agent ${index + 1}`);
|
|
419
425
|
const harness = harnessForPane(pane);
|
|
420
426
|
if (harness === "codex") return "Codex";
|
|
421
427
|
if (harness === "claude-code") return "Claude Code";
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
return `Agent ${index + 1}`;
|
|
428
|
+
return resolved.source === "process"
|
|
429
|
+
? label(resolved.name, `Agent ${index + 1}`)
|
|
430
|
+
: `Agent ${index + 1}`;
|
|
427
431
|
}
|
|
428
432
|
|
|
429
433
|
const AGENT_STATE_STAMP = /^(?:working|blocked|done|idle):\d+$/u;
|
|
430
434
|
|
|
431
435
|
export function isAgentPane(pane: ApplicationShellPanePresentationFacts): boolean {
|
|
436
|
+
if (pane.teamMemberName || pane.team) return true;
|
|
432
437
|
// A well-formed @agent_state stamp IS the agent contract: any pane that
|
|
433
438
|
// self-reports is an agent pane, even when its command is a bare shell and
|
|
434
439
|
// no @ide_type/role metadata exists. Staleness only affects the status, not
|
|
@@ -627,6 +632,7 @@ function projectApplicationShellResourceV1Core(
|
|
|
627
632
|
// labels such as `Terminal` with the canonical harness name.
|
|
628
633
|
name: resolvedAgentLabel(pane, presentation, index),
|
|
629
634
|
harness: harnessForPane(pane),
|
|
635
|
+
...(pane.team ? { team: pane.team } : {}),
|
|
630
636
|
activity: presentation.activity,
|
|
631
637
|
paneId,
|
|
632
638
|
attention: presentation.attention,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
isAgentPane,
|
|
44
44
|
resolveAgentPresentation,
|
|
45
45
|
type ApplicationShellPanePresentationFacts,
|
|
46
|
+
resolvedAgentLabel,
|
|
46
47
|
} from "./application-shell.ts";
|
|
47
48
|
|
|
48
49
|
function digest(value: string): string {
|
|
@@ -123,16 +124,19 @@ function toPresentationPane(
|
|
|
123
124
|
return {
|
|
124
125
|
semanticPaneId: null,
|
|
125
126
|
index,
|
|
126
|
-
title: "",
|
|
127
|
+
title: pane.title ?? "",
|
|
127
128
|
currentCommand: pane.currentCommand,
|
|
128
129
|
active: pane.active,
|
|
129
130
|
role: null,
|
|
130
|
-
name: null,
|
|
131
|
+
name: pane.name ?? null,
|
|
132
|
+
nameSource: pane.nameSource,
|
|
131
133
|
type: null,
|
|
132
134
|
agentKind: pane.agentHintRaw,
|
|
133
135
|
agentStateRaw: pane.agentStateRaw,
|
|
134
136
|
agentStatusTextRaw: pane.agentStatusTextRaw,
|
|
135
137
|
agentDisplayNameRaw: pane.agentDisplayNameRaw,
|
|
138
|
+
teamMemberName: pane.teamMemberName,
|
|
139
|
+
team: pane.team,
|
|
136
140
|
// Authority-only: the fleet never scrapes an unopened session. `null` (not
|
|
137
141
|
// `undefined`) keeps `resolveAgentPresentation` on the ground-truth path
|
|
138
142
|
// while its scrape verdict resolves to `unknown` without any capture.
|
|
@@ -155,8 +159,12 @@ function projectSession(
|
|
|
155
159
|
const presentation = resolveAgentPresentation(presentationPane, nowSec);
|
|
156
160
|
agents.push({
|
|
157
161
|
agentId: fleetAgentIdForPane(session.name, pane),
|
|
158
|
-
name: fleetLabel(
|
|
162
|
+
name: fleetLabel(
|
|
163
|
+
resolvedAgentLabel(presentationPane, presentation, index),
|
|
164
|
+
`Agent ${index + 1}`,
|
|
165
|
+
),
|
|
159
166
|
harness: harnessForPane(presentationPane),
|
|
167
|
+
...(pane.team ? { team: pane.team } : {}),
|
|
160
168
|
activity: presentation.activity,
|
|
161
169
|
attention: presentation.attention,
|
|
162
170
|
statusSource: presentation.statusSource,
|
|
@@ -14,7 +14,9 @@ export interface FleetResourceRouteOptions {
|
|
|
14
14
|
/** Source of truth for which sessions the app created (`appCreated`). */
|
|
15
15
|
readonly registry: Pick<WorkspaceRegistry, "list">;
|
|
16
16
|
/** Daemon-generation-pinned tmux fleet read. Defaults only for legacy/test callers. */
|
|
17
|
-
readonly readFleet?: () =>
|
|
17
|
+
readonly readFleet?: () =>
|
|
18
|
+
| ReturnType<typeof readAdoptedFleet>
|
|
19
|
+
| Promise<ReturnType<typeof readAdoptedFleet>>;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -36,7 +38,7 @@ export function mountFleetResourceRoute(app: Hono, options: FleetResourceRouteOp
|
|
|
36
38
|
mismatchMessage: "Fleet catalog access requires owner authority",
|
|
37
39
|
});
|
|
38
40
|
|
|
39
|
-
app.get("/api/resources/fleet-catalog", (c) => {
|
|
41
|
+
app.get("/api/resources/fleet-catalog", async (c) => {
|
|
40
42
|
const gate = authorize(c);
|
|
41
43
|
if (gate) return gate;
|
|
42
44
|
|
|
@@ -45,7 +47,7 @@ export function mountFleetResourceRoute(app: Hono, options: FleetResourceRouteOp
|
|
|
45
47
|
// with a successful empty snapshot. Never expose raw socket/process errors.
|
|
46
48
|
let sessions: ReturnType<typeof readAdoptedFleet>;
|
|
47
49
|
try {
|
|
48
|
-
sessions = options.readFleet ? options.readFleet() : readAdoptedFleet(options.registry);
|
|
50
|
+
sessions = options.readFleet ? await options.readFleet() : readAdoptedFleet(options.registry);
|
|
49
51
|
} catch {
|
|
50
52
|
sessions = null;
|
|
51
53
|
}
|
|
@@ -219,7 +219,7 @@ export interface CreateAppOptions {
|
|
|
219
219
|
readonly paneCount: number;
|
|
220
220
|
}[];
|
|
221
221
|
/** Injectable daemon-generation-pinned adopted fleet projection. */
|
|
222
|
-
catalogFleet?: () => FleetSessionFacts[] | null
|
|
222
|
+
catalogFleet?: () => FleetSessionFacts[] | null | Promise<FleetSessionFacts[] | null>;
|
|
223
223
|
fleetPreviewCapture?: (
|
|
224
224
|
liveSessionId: string,
|
|
225
225
|
signal?: AbortSignal,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createClaudeTeamMembershipReader } from "../terminal/attachments/claude-team-names.ts";
|
|
1
3
|
import { PaneSourceDiscovery } from "./pane-source-discovery.ts";
|
|
2
4
|
import { createBackgroundNativeCapture } from "./background-native-capture.ts";
|
|
3
5
|
import { createOwnedViewerAdapterFactory } from "./owned-viewer-factory.ts";
|
|
@@ -842,7 +844,9 @@ async function startHttpServer({
|
|
|
842
844
|
readonly sessionName: string;
|
|
843
845
|
readonly paneCount: number;
|
|
844
846
|
}[];
|
|
845
|
-
catalogFleet: () =>
|
|
847
|
+
catalogFleet: () =>
|
|
848
|
+
| ReturnType<typeof readAdoptedFleet>
|
|
849
|
+
| Promise<ReturnType<typeof readAdoptedFleet>>;
|
|
846
850
|
fleetPreviewCapture: (
|
|
847
851
|
liveSessionId: string,
|
|
848
852
|
signal?: AbortSignal,
|
|
@@ -1127,6 +1131,9 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1127
1131
|
// observe one daemon-generation authority rather than whichever server a
|
|
1128
1132
|
// caller's ambient TMUX/PATH happens to select.
|
|
1129
1133
|
const tmuxAuthority = resolveWorkspacePaneTmuxAuthority();
|
|
1134
|
+
const readTeamMemberships = createClaudeTeamMembershipReader(
|
|
1135
|
+
join(resolveRuntimeNamespace().claudeDir, "teams"),
|
|
1136
|
+
);
|
|
1130
1137
|
const catalogTmuxRunner = createPinnedWorkspaceTmuxRunner(tmuxAuthority);
|
|
1131
1138
|
const initialTmuxProof = captureTmuxServerProof(catalogTmuxRunner);
|
|
1132
1139
|
const initialNativeIdentityParts = initialTmuxProof
|
|
@@ -1621,6 +1628,7 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1621
1628
|
agentStatusProbeFactory: ({ run }) =>
|
|
1622
1629
|
createTmuxAgentStatusProbe({
|
|
1623
1630
|
run,
|
|
1631
|
+
readTeamMemberships,
|
|
1624
1632
|
captureNative: (pane, signal) =>
|
|
1625
1633
|
backgroundCapture?.(
|
|
1626
1634
|
{
|
|
@@ -1900,8 +1908,8 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1900
1908
|
peekTerminalAttachmentRuntime,
|
|
1901
1909
|
paneStreamRuntime,
|
|
1902
1910
|
catalogLiveSessions: () => discoverLiveSessionSummaries(catalogTmuxRunner),
|
|
1903
|
-
catalogFleet: () =>
|
|
1904
|
-
readAdoptedFleet(
|
|
1911
|
+
catalogFleet: async () => {
|
|
1912
|
+
const fleet = readAdoptedFleet(
|
|
1905
1913
|
workspaceRegistry,
|
|
1906
1914
|
nativeGenerationTmuxRunner,
|
|
1907
1915
|
(sessionName, pane) =>
|
|
@@ -1913,7 +1921,26 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1913
1921
|
) ?? null)
|
|
1914
1922
|
: null,
|
|
1915
1923
|
() => observationSelector?.nativeServerEpoch ?? null,
|
|
1916
|
-
)
|
|
1924
|
+
);
|
|
1925
|
+
if (!fleet) return null;
|
|
1926
|
+
const names = await readTeamMemberships(
|
|
1927
|
+
fleet.flatMap((session) =>
|
|
1928
|
+
session.panes.map((pane) => ({
|
|
1929
|
+
runtimePaneId: pane.runtimePaneId,
|
|
1930
|
+
pid: pane.incarnation,
|
|
1931
|
+
})),
|
|
1932
|
+
),
|
|
1933
|
+
);
|
|
1934
|
+
return fleet.map((session) => ({
|
|
1935
|
+
...session,
|
|
1936
|
+
panes: session.panes.map((pane) => {
|
|
1937
|
+
const name = names.get(pane.runtimePaneId);
|
|
1938
|
+
return name
|
|
1939
|
+
? { ...pane, teamMemberName: name.name, team: pane.team ?? name.team }
|
|
1940
|
+
: pane;
|
|
1941
|
+
}),
|
|
1942
|
+
}));
|
|
1943
|
+
},
|
|
1917
1944
|
fleetPreviewCapture: createFleetPreviewCapture(fleetFactsTmuxRunner, {
|
|
1918
1945
|
serverEpoch: () => observationSelector?.nativeServerEpoch ?? null,
|
|
1919
1946
|
capture: (request, signal) =>
|
|
@@ -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,7 @@
|
|
|
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";
|
|
4
|
+
import type { ClaudeTeamNameReader } from "./claude-team-names.ts";
|
|
1
5
|
import type { NativePaneIdentity } from "@tmux-ide/contracts";
|
|
2
6
|
/**
|
|
3
7
|
* Agent-status probe for the application-shell inventory — the IO half of the
|
|
@@ -42,6 +46,8 @@ import {
|
|
|
42
46
|
|
|
43
47
|
/** The per-pane facts the pure projector consumes (see `ApplicationShellPanePresentationFacts`). */
|
|
44
48
|
export interface AgentStatusPaneFacts {
|
|
49
|
+
readonly teamMemberName?: string;
|
|
50
|
+
readonly team?: PaneTeamMembership;
|
|
45
51
|
/** Stable manifest id resolved from the pane hint, command, or process tree. */
|
|
46
52
|
readonly agentKind: string | null;
|
|
47
53
|
/** Raw `@agent_state` (`"<state>:<epoch>"`), or null when unset. */
|
|
@@ -93,6 +99,7 @@ const AGENT_OPTIONS_FORMAT = [
|
|
|
93
99
|
"#{@agent_display_name}",
|
|
94
100
|
"#{@agent_hint}",
|
|
95
101
|
"#{pane_pid}",
|
|
102
|
+
"#{@tmux_ide_team}",
|
|
96
103
|
AGENT_LINE_SENTINEL,
|
|
97
104
|
].join(AGENT_FIELD_SEPARATOR);
|
|
98
105
|
|
|
@@ -117,6 +124,7 @@ export const SCRAPE_CAPTURE_BUDGET = 4;
|
|
|
117
124
|
const SCRAPE_CACHE_MAX_ENTRIES = 1024;
|
|
118
125
|
|
|
119
126
|
interface RawPaneOptions {
|
|
127
|
+
readonly team?: PaneTeamMembership;
|
|
120
128
|
readonly stateRaw: string | null;
|
|
121
129
|
readonly statusTextRaw: string | null;
|
|
122
130
|
readonly displayNameRaw: string | null;
|
|
@@ -151,12 +159,13 @@ function parseAgentOptions(stdout: string): ReadonlyMap<string, RawPaneOptions>
|
|
|
151
159
|
if (line.length === 0) continue;
|
|
152
160
|
const fields = line.split(AGENT_FIELD_SEPARATOR);
|
|
153
161
|
// paneId, state, statusText, displayName, hint, pid, sentinel
|
|
154
|
-
if (fields.length
|
|
162
|
+
if (![7, 8].includes(fields.length) || fields.at(-1) !== AGENT_LINE_SENTINEL) continue;
|
|
155
163
|
const runtimePaneId = fields[0]!;
|
|
156
164
|
if (!RUNTIME_PANE_ID.test(runtimePaneId)) continue;
|
|
157
165
|
const pidText = fields[5]!;
|
|
158
166
|
const pid = /^[0-9]+$/u.test(pidText) ? Number(pidText) : null;
|
|
159
167
|
result.set(runtimePaneId, {
|
|
168
|
+
team: fields.length === 8 ? readManualPaneTeam(fields[6], pid) : undefined,
|
|
160
169
|
stateRaw: emptyToNull(fields[1]!),
|
|
161
170
|
statusTextRaw: emptyToNull(fields[2]!),
|
|
162
171
|
displayNameRaw: emptyToNull(fields[3]!),
|
|
@@ -168,6 +177,8 @@ function parseAgentOptions(stdout: string): ReadonlyMap<string, RawPaneOptions>
|
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
export interface TmuxAgentStatusProbeDeps {
|
|
180
|
+
readonly readTeamNames?: ClaudeTeamNameReader;
|
|
181
|
+
readonly readTeamMemberships?: ClaudeTeamMembershipReader;
|
|
171
182
|
/** null proves no native dispatch; failures must never retry through stock capture. */
|
|
172
183
|
readonly captureNative?: (
|
|
173
184
|
pane: AgentStatusProbePane,
|
|
@@ -302,6 +313,7 @@ export function createTmuxAgentStatusProbe(deps: TmuxAgentStatusProbeDeps): Agen
|
|
|
302
313
|
agentKind: string | null,
|
|
303
314
|
): void => {
|
|
304
315
|
facts.set(pane.runtimePaneId, {
|
|
316
|
+
...(raw?.team ? { team: raw.team } : {}),
|
|
305
317
|
agentKind,
|
|
306
318
|
agentStateRaw: raw?.stateRaw ?? null,
|
|
307
319
|
agentStatusTextRaw: raw?.statusTextRaw ?? null,
|
|
@@ -429,6 +441,39 @@ export function createTmuxAgentStatusProbe(deps: TmuxAgentStatusProbeDeps): Agen
|
|
|
429
441
|
emit(pane, raw, verdict, manifest.id);
|
|
430
442
|
}
|
|
431
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
|
+
|
|
463
|
+
if (deps.readTeamNames) {
|
|
464
|
+
const names = await deps.readTeamNames(
|
|
465
|
+
input.panes.flatMap((pane) => {
|
|
466
|
+
const pid = options.get(pane.runtimePaneId)?.pid;
|
|
467
|
+
return pid ? [{ runtimePaneId: pane.runtimePaneId, pid }] : [];
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
throwIfAborted(signal);
|
|
471
|
+
for (const [paneId, name] of names) {
|
|
472
|
+
const fact = facts.get(paneId);
|
|
473
|
+
if (fact) facts.set(paneId, { ...fact, teamMemberName: name });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
432
477
|
// Bound the cache: pane ids are server-unique and never re-probed after a
|
|
433
478
|
// pane dies, so evict the oldest verdicts past the ceiling.
|
|
434
479
|
if (stagedVerdicts.size > SCRAPE_CACHE_MAX_ENTRIES) {
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { PaneTeamMembership } from "@tmux-ide/contracts";
|
|
3
|
+
/** Read-only compatibility adapter for Claude Code's runtime team metadata.
|
|
4
|
+
* Names are presentation evidence, never agent identity or permission authority.
|
|
5
|
+
*/
|
|
6
|
+
import { open, readdir } from "node:fs/promises";
|
|
7
|
+
import { join, basename } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
readProcessTableAsync,
|
|
10
|
+
subtreeEntries,
|
|
11
|
+
type ProcEntry,
|
|
12
|
+
} from "../../tui/detect/process-tree.ts";
|
|
13
|
+
|
|
14
|
+
export interface ClaudeTeamMember {
|
|
15
|
+
readonly team: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly agentId: string;
|
|
18
|
+
readonly paneId: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TeamPaneProcess {
|
|
21
|
+
readonly runtimePaneId: string;
|
|
22
|
+
readonly pid: number;
|
|
23
|
+
}
|
|
24
|
+
const token = (value: unknown): value is string =>
|
|
25
|
+
typeof value === "string" &&
|
|
26
|
+
/^[a-zA-Z0-9_.@-]{1,160}$/u.test(value) &&
|
|
27
|
+
value !== "." &&
|
|
28
|
+
value !== "..";
|
|
29
|
+
|
|
30
|
+
export function parseClaudeTeamMembers(value: unknown, directory: string): ClaudeTeamMember[] {
|
|
31
|
+
if (!value || typeof value !== "object") return [];
|
|
32
|
+
const config = value as Record<string, unknown>;
|
|
33
|
+
if (
|
|
34
|
+
!token(directory) ||
|
|
35
|
+
config.name !== directory ||
|
|
36
|
+
!Array.isArray(config.members) ||
|
|
37
|
+
config.members.length > 128
|
|
38
|
+
)
|
|
39
|
+
return [];
|
|
40
|
+
return config.members.flatMap((value): ClaudeTeamMember[] => {
|
|
41
|
+
if (!value || typeof value !== "object") return [];
|
|
42
|
+
const m = value as Record<string, unknown>;
|
|
43
|
+
if (
|
|
44
|
+
!token(m.name) ||
|
|
45
|
+
m.name.length > 80 ||
|
|
46
|
+
!token(m.agentId) ||
|
|
47
|
+
m.backendType !== "tmux" ||
|
|
48
|
+
typeof m.tmuxPaneId !== "string" ||
|
|
49
|
+
!/^%\d+$/u.test(m.tmuxPaneId)
|
|
50
|
+
)
|
|
51
|
+
return [];
|
|
52
|
+
return [{ team: directory, name: m.name, agentId: m.agentId, paneId: m.tmuxPaneId }];
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Conservative argv recognition: flattened ps text is not a shell parser.
|
|
57
|
+
* Ambiguous/quoted arguments fall back to the native title rather than guessing.
|
|
58
|
+
*/
|
|
59
|
+
function matchesProcess(command: string, member: ClaudeTeamMember): boolean {
|
|
60
|
+
const args = command.trim().split(/\s+/u);
|
|
61
|
+
const executable = args.shift() ?? "";
|
|
62
|
+
if (basename(executable) !== "claude" && !/\/claude\/versions\/\d[\w.-]*$/u.test(executable))
|
|
63
|
+
return false;
|
|
64
|
+
for (const [flag, expected] of [
|
|
65
|
+
["--team-name", member.team],
|
|
66
|
+
["--agent-id", member.agentId],
|
|
67
|
+
["--agent-name", member.name],
|
|
68
|
+
]) {
|
|
69
|
+
const positions = args.flatMap((arg, index) =>
|
|
70
|
+
arg === flag || arg.startsWith(`${flag}=`) ? [index] : [],
|
|
71
|
+
);
|
|
72
|
+
if (positions.length !== 1) return false;
|
|
73
|
+
const i = positions[0]!;
|
|
74
|
+
if ((args[i] === flag ? args[i + 1] : args[i]!.slice(flag!.length + 1)) !== expected)
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveClaudeTeamMember(
|
|
81
|
+
pane: TeamPaneProcess,
|
|
82
|
+
members: readonly ClaudeTeamMember[],
|
|
83
|
+
processes: ProcEntry[],
|
|
84
|
+
): ClaudeTeamMember | null {
|
|
85
|
+
// A native pane id alone cannot bind a team: different servers reuse %N.
|
|
86
|
+
// Its live shell PID scopes the match to this host and this pane's subtree.
|
|
87
|
+
const subtree = subtreeEntries(processes, pane.pid);
|
|
88
|
+
const matches = members.filter(
|
|
89
|
+
(m) => m.paneId === pane.runtimePaneId && subtree.some((p) => matchesProcess(p.command, m)),
|
|
90
|
+
);
|
|
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;
|
|
105
|
+
}
|
|
106
|
+
export type ClaudeTeamMembershipReader = (
|
|
107
|
+
panes: readonly TeamPaneProcess[],
|
|
108
|
+
) => Promise<ReadonlyMap<string, ClaudePaneTeam>>;
|
|
109
|
+
|
|
110
|
+
async function readMembers(directory: string): Promise<ClaudeTeamMember[]> {
|
|
111
|
+
try {
|
|
112
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
113
|
+
if (entries.length > 128) return [];
|
|
114
|
+
const members: ClaudeTeamMember[] = [];
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (!entry.isDirectory() || !token(entry.name)) continue;
|
|
117
|
+
try {
|
|
118
|
+
const file = await open(join(directory, entry.name, "config.json"), "r");
|
|
119
|
+
try {
|
|
120
|
+
if (!(await file.stat()).isFile()) continue;
|
|
121
|
+
const bytes = Buffer.alloc(256 * 1024 + 1);
|
|
122
|
+
const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
|
|
123
|
+
if (bytesRead > 256 * 1024) continue;
|
|
124
|
+
members.push(
|
|
125
|
+
...parseClaudeTeamMembers(
|
|
126
|
+
JSON.parse(bytes.subarray(0, bytesRead).toString("utf8")),
|
|
127
|
+
entry.name,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
} finally {
|
|
131
|
+
await file.close();
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
/* Removal, atomic replacement and unknown schemas clear old facts. */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return members;
|
|
138
|
+
} catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type ClaudeTeamNameReader = (
|
|
144
|
+
panes: readonly TeamPaneProcess[],
|
|
145
|
+
) => Promise<ReadonlyMap<string, string>>;
|
|
146
|
+
|
|
147
|
+
export function createClaudeTeamMembershipReader(
|
|
148
|
+
directory: string,
|
|
149
|
+
deps: {
|
|
150
|
+
readMembers?: () => Promise<ClaudeTeamMember[]>;
|
|
151
|
+
readProcesses?: () => Promise<ProcEntry[]>;
|
|
152
|
+
now?: () => number;
|
|
153
|
+
} = {},
|
|
154
|
+
): ClaudeTeamMembershipReader {
|
|
155
|
+
const now = deps.now ?? Date.now;
|
|
156
|
+
let snapshot: { at: number; members: ClaudeTeamMember[]; processes: ProcEntry[] } | null = null;
|
|
157
|
+
let pending: Promise<void> | null = null;
|
|
158
|
+
return async (panes) => {
|
|
159
|
+
if (!panes.length) return new Map();
|
|
160
|
+
if (!snapshot || now() - snapshot.at >= 2000) {
|
|
161
|
+
pending ??= (async () => {
|
|
162
|
+
try {
|
|
163
|
+
const members = await (deps.readMembers?.() ?? readMembers(directory));
|
|
164
|
+
const processes = members.length
|
|
165
|
+
? await (deps.readProcesses?.() ?? readProcessTableAsync())
|
|
166
|
+
: [];
|
|
167
|
+
snapshot = { at: now(), members, processes };
|
|
168
|
+
} catch {
|
|
169
|
+
snapshot = { at: now(), members: [], processes: [] };
|
|
170
|
+
}
|
|
171
|
+
})().finally(() => {
|
|
172
|
+
pending = null;
|
|
173
|
+
});
|
|
174
|
+
await pending;
|
|
175
|
+
}
|
|
176
|
+
const current = snapshot!;
|
|
177
|
+
const names = new Map<string, ClaudePaneTeam>();
|
|
178
|
+
for (const pane of panes) {
|
|
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
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return names;
|
|
191
|
+
};
|
|
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
|
+
}
|
|
@@ -487,6 +487,7 @@ export interface NativeTerminalInventoryPaneSnapshot extends TrustedSemanticPane
|
|
|
487
487
|
readonly active: boolean;
|
|
488
488
|
readonly role: string | null;
|
|
489
489
|
readonly name: string | null;
|
|
490
|
+
readonly nameSource?: string | null;
|
|
490
491
|
readonly type: string | null;
|
|
491
492
|
/** Durable `@tmux_ide_mission` creation stamp, or null when unset. */
|
|
492
493
|
readonly missionStamp: string | null;
|
|
@@ -589,6 +590,7 @@ function projectTrustedMirrorInventory(
|
|
|
589
590
|
active: pane.active,
|
|
590
591
|
role: nullable(pane.role),
|
|
591
592
|
name: nullable(pane.name),
|
|
593
|
+
...(pane.nameSource ? { nameSource: nullable(pane.nameSource) } : {}),
|
|
592
594
|
type: nullable(pane.type),
|
|
593
595
|
missionStamp: nullable(pane.missionStamp),
|
|
594
596
|
dir: boundedWireValue(pane.dir, 4_096),
|
|
@@ -3530,6 +3530,7 @@ export class SessionChannel {
|
|
|
3530
3530
|
active: descriptor.paneActive && descriptor.windowActive,
|
|
3531
3531
|
role: descriptor.role,
|
|
3532
3532
|
name: descriptor.name,
|
|
3533
|
+
...(descriptor.nameSource ? { nameSource: descriptor.nameSource } : {}),
|
|
3533
3534
|
type: descriptor.type,
|
|
3534
3535
|
missionStamp: descriptor.missionStamp,
|
|
3535
3536
|
nativePaneBirthId: descriptor.nativePaneBirthId ?? null,
|
|
@@ -18,6 +18,7 @@ export interface TrustedMirrorPaneInventory {
|
|
|
18
18
|
readonly active: boolean;
|
|
19
19
|
readonly role: string | null;
|
|
20
20
|
readonly name: string | null;
|
|
21
|
+
readonly nameSource?: string | null;
|
|
21
22
|
readonly type: string | null;
|
|
22
23
|
readonly missionStamp: string | null;
|
|
23
24
|
readonly dir: string;
|