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.
Files changed (67) hide show
  1. package/bin/cli.js +2764 -2425
  2. package/package.json +2 -2
  3. package/packages/contracts/src/cohesion-fixture.ts +2 -0
  4. package/packages/contracts/src/fleet-catalog.ts +2 -0
  5. package/packages/contracts/src/index.ts +2 -0
  6. package/packages/contracts/src/pane-team.ts +32 -0
  7. package/packages/daemon/dist/command-center/discovery.js +12 -2
  8. package/packages/daemon/dist/command-center/resources/agent-graph-overlay.js +2 -2
  9. package/packages/daemon/dist/command-center/resources/application-shell.js +22 -15
  10. package/packages/daemon/dist/command-center/resources/fleet-catalog.js +8 -4
  11. package/packages/daemon/dist/command-center/resources/fleet-resource-route.js +2 -2
  12. package/packages/daemon/dist/lib/daemon-embed.js +24 -3
  13. package/packages/daemon/dist/pane-team.js +28 -0
  14. package/packages/daemon/dist/terminal/attachments/agent-status-probe.js +33 -1
  15. package/packages/daemon/dist/terminal/attachments/claude-team-names.js +146 -0
  16. package/packages/daemon/dist/terminal/attachments/manual-pane-team.js +30 -0
  17. package/packages/daemon/dist/terminal/attachments/native-runtime.js +1 -0
  18. package/packages/daemon/dist/terminal/mirror/session-channel.js +1 -0
  19. package/packages/daemon/dist/terminal/protocol/pane-display-name.js +40 -11
  20. package/packages/daemon/dist/terminal/protocol/session-descriptor-discovery.js +4 -5
  21. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +1 -1
  22. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +17 -1
  23. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +1 -0
  24. package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +1 -0
  25. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +3 -1
  26. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +3 -2
  27. package/packages/daemon/dist/tui/mirror/runtime/application-new-agent-owner.js +86 -0
  28. package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +5 -0
  29. package/packages/daemon/dist/tui/mirror/runtime/application-palette-input.js +1 -0
  30. package/packages/daemon/dist/tui/mirror/runtime/application-team-groups.js +22 -0
  31. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +24 -0
  32. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +7 -5
  33. package/packages/daemon/dist/tui/mirror/ui/agent-row.jsx +2 -2
  34. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +6 -0
  35. package/packages/daemon/src/command-center/discovery.ts +18 -2
  36. package/packages/daemon/src/command-center/resources/agent-graph-overlay.ts +2 -1
  37. package/packages/daemon/src/command-center/resources/application-shell.ts +24 -18
  38. package/packages/daemon/src/command-center/resources/fleet-catalog.ts +11 -3
  39. package/packages/daemon/src/command-center/resources/fleet-resource-route.ts +5 -3
  40. package/packages/daemon/src/command-center/server.ts +1 -1
  41. package/packages/daemon/src/lib/daemon-embed.ts +31 -4
  42. package/packages/daemon/src/pane-team.ts +42 -0
  43. package/packages/daemon/src/terminal/attachments/agent-status-probe.ts +46 -1
  44. package/packages/daemon/src/terminal/attachments/claude-team-names.ts +201 -0
  45. package/packages/daemon/src/terminal/attachments/manual-pane-team.ts +31 -0
  46. package/packages/daemon/src/terminal/attachments/native-runtime.ts +2 -0
  47. package/packages/daemon/src/terminal/mirror/session-channel.ts +1 -0
  48. package/packages/daemon/src/terminal/mirror/trusted-inventory.ts +1 -0
  49. package/packages/daemon/src/terminal/protocol/pane-display-name.ts +51 -13
  50. package/packages/daemon/src/terminal/protocol/session-descriptor-discovery.ts +4 -4
  51. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +1 -0
  52. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +19 -2
  53. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +3 -0
  54. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +1 -0
  55. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +3 -1
  56. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +5 -1
  57. package/packages/daemon/src/tui/mirror/runtime/application-new-agent-dialog.tsx +97 -0
  58. package/packages/daemon/src/tui/mirror/runtime/application-new-agent-owner.ts +89 -0
  59. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +6 -0
  60. package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +1 -0
  61. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +5 -4
  62. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +23 -1
  63. package/packages/daemon/src/tui/mirror/runtime/application-team-groups.ts +40 -0
  64. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +27 -0
  65. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +10 -5
  66. package/packages/daemon/src/tui/mirror/ui/agent-row.tsx +7 -3
  67. 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
- if (presentation.displayName !== undefined) {
415
- return label(presentation.displayName, `Agent ${index + 1}`);
416
- }
417
- if (!isGenericPaneLabel(pane.name)) return label(pane.name, `Agent ${index + 1}`);
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
- if (!isGenericPaneLabel(pane.title) && !isHostNameTitle(pane.title, hostname())) {
424
- return label(pane.title, `Agent ${index + 1}`);
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(presentation.displayName ?? pane.currentCommand, `Agent ${index + 1}`),
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?: () => ReturnType<typeof readAdoptedFleet>;
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: () => ReturnType<typeof readAdoptedFleet>;
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 !== 7 || fields[6] !== AGENT_LINE_SENTINEL) continue;
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;