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.
Files changed (34) hide show
  1. package/bin/cli.js +2055 -1907
  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 +4 -1
  8. package/packages/daemon/dist/command-center/resources/application-shell.js +2 -1
  9. package/packages/daemon/dist/command-center/resources/fleet-catalog.js +2 -0
  10. package/packages/daemon/dist/lib/daemon-embed.js +7 -5
  11. package/packages/daemon/dist/pane-team.js +28 -0
  12. package/packages/daemon/dist/terminal/attachments/agent-status-probe.js +21 -1
  13. package/packages/daemon/dist/terminal/attachments/claude-team-names.js +22 -6
  14. package/packages/daemon/dist/terminal/attachments/manual-pane-team.js +30 -0
  15. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +1 -1
  16. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +1 -0
  17. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +3 -1
  18. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +3 -2
  19. package/packages/daemon/dist/tui/mirror/runtime/application-team-groups.js +22 -0
  20. package/packages/daemon/dist/tui/mirror/ui/agent-row.jsx +2 -2
  21. package/packages/daemon/src/command-center/discovery.ts +6 -1
  22. package/packages/daemon/src/command-center/resources/application-shell.ts +4 -1
  23. package/packages/daemon/src/command-center/resources/fleet-catalog.ts +2 -0
  24. package/packages/daemon/src/lib/daemon-embed.ts +7 -5
  25. package/packages/daemon/src/pane-team.ts +42 -0
  26. package/packages/daemon/src/terminal/attachments/agent-status-probe.ts +29 -1
  27. package/packages/daemon/src/terminal/attachments/claude-team-names.ts +43 -8
  28. package/packages/daemon/src/terminal/attachments/manual-pane-team.ts +31 -0
  29. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +1 -0
  30. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +3 -0
  31. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +3 -1
  32. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +5 -1
  33. package/packages/daemon/src/tui/mirror/runtime/application-team-groups.ts +40 -0
  34. package/packages/daemon/src/tui/mirror/ui/agent-row.tsx +7 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.48",
3
+ "version": "2.9.0-beta.49",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,7 +64,7 @@
64
64
  "postinstall": "node scripts/postinstall.js",
65
65
  "docs": "turbo run dev --filter=@tmux-ide/docs",
66
66
  "demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
67
- "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/guided-tour-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/pane-interaction-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-new-agent-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
67
+ "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/guided-tour-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/pane-interaction-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-new-agent-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/agent-team-renderer.test.tsx",
68
68
  "test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
69
69
  "test:tui-live": "node scripts/tui-testdrive.mjs smoke",
70
70
  "test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
@@ -1,3 +1,4 @@
1
+ import { PaneTeamMembershipSchemaZ } from "./pane-team.ts";
1
2
  import { z } from "zod";
2
3
  import { CommandIdSchemaZ } from "./commands.ts";
3
4
  import {
@@ -42,6 +43,7 @@ const SessionSidebarItemSchemaZ = z
42
43
 
43
44
  const AgentSidebarItemSchemaZ = z
44
45
  .object({
46
+ team: PaneTeamMembershipSchemaZ.optional(),
45
47
  id: SemanticProductIdSchemaZ,
46
48
  name: LabelSchemaZ,
47
49
  harness: z.enum(["codex", "claude-code", "custom"]),
@@ -1,3 +1,4 @@
1
+ import { PaneTeamMembershipSchemaZ } from "./pane-team.ts";
1
2
  import {
2
3
  InteractionPaneEndpointSchemaZ,
3
4
  NativePaneIdentitySchemaZ,
@@ -121,6 +122,7 @@ export type FleetAgentHarness = z.infer<typeof FleetAgentHarnessSchemaZ>;
121
122
  export const FleetCatalogAgentEntryV1SchemaZ = z
122
123
  .object({
123
124
  agentId: FleetAgentIdSchemaZ,
125
+ team: PaneTeamMembershipSchemaZ.optional(),
124
126
  name: FleetLabelSchemaZ,
125
127
  harness: FleetAgentHarnessSchemaZ,
126
128
  activity: AgentActivitySchemaZ,
@@ -106,3 +106,5 @@ export * from "./tmux-server-interaction-events.ts";
106
106
  export * from "./automation-operations.ts";
107
107
  export * from "./native-interaction-journal.ts";
108
108
  export * from "./native-operation-identity.ts";
109
+
110
+ export * from "./pane-team.ts";
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+
3
+ /** Presentation membership; never grants control of a pane or a harness team. */
4
+ export const PaneTeamMembershipSchemaZ = z
5
+ .object({
6
+ id: z.string().regex(/^team\.[a-zA-Z0-9_-]{16,64}$/u),
7
+ name: z
8
+ .string()
9
+ .min(1)
10
+ .max(80)
11
+ .refine(
12
+ (value) =>
13
+ value === value.trim() &&
14
+ Array.from(value).every((character) => {
15
+ const code = character.codePointAt(0)!;
16
+ return code > 31 && (code < 127 || code > 159);
17
+ }),
18
+ ),
19
+ source: z.enum(["manual", "claude-code"]),
20
+ })
21
+ .strict();
22
+ export type PaneTeamMembership = z.infer<typeof PaneTeamMembershipSchemaZ>;
23
+
24
+ /** Scope imported identities to their machine and live server, not session/layout. */
25
+ export function paneTeamGroupKey(
26
+ team: PaneTeamMembership,
27
+ machineId: string,
28
+ serverId: string,
29
+ generation: string,
30
+ ): string {
31
+ return JSON.stringify([machineId, serverId, generation, team.source, team.id]);
32
+ }
@@ -1,3 +1,4 @@
1
+ import { readManualPaneTeam } from "../terminal/attachments/manual-pane-team.js";
1
2
  import { decodeTmuxArgument } from "../terminal/protocol/session-descriptor-discovery.js";
2
3
  import { nativePaneIdentity } from "../lib/native-pane-identity.js";
3
4
  import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
@@ -201,6 +202,7 @@ const FLEET_PANE_FORMAT = [
201
202
  "#{qa:pane_title}",
202
203
  "#{qa:@ide_name}",
203
204
  "#{qa:@tmux_ide_name_source}",
205
+ "#{@tmux_ide_team}",
204
206
  FLEET_LINE_SENTINEL,
205
207
  ].join(FLEET_FIELD_SEPARATOR);
206
208
  function emptyToNull(value) {
@@ -239,7 +241,7 @@ export function readAdoptedFleet(registry = getDefaultWorkspaceRegistry(), runTm
239
241
  const fields = line.split(FLEET_FIELD_SEPARATOR);
240
242
  // session, pane, semantic pane, incarnation pid, active, command, path, state,
241
243
  // statusText, displayName, hint, birth ID, quoted title/name/source, sentinel
242
- if (fields.length !== 16 || fields[15] !== FLEET_LINE_SENTINEL)
244
+ if (![16, 17].includes(fields.length) || fields.at(-1) !== FLEET_LINE_SENTINEL)
243
245
  continue;
244
246
  const sessionName = fields[0];
245
247
  if (!adoptedSet.has(sessionName))
@@ -256,6 +258,7 @@ export function readAdoptedFleet(registry = getDefaultWorkspaceRegistry(), runTm
256
258
  panesBySession.set(sessionName, panes);
257
259
  }
258
260
  panes.push({
261
+ team: fields.length === 17 ? readManualPaneTeam(fields[15], incarnation) : undefined,
259
262
  nameSource: emptyToNull(decodeTmuxArgument(fields[14])),
260
263
  title: decodeTmuxArgument(fields[12]),
261
264
  name: emptyToNull(decodeTmuxArgument(fields[13])),
@@ -254,7 +254,7 @@ export function resolvedAgentLabel(pane, presentation, index) {
254
254
  }
255
255
  const AGENT_STATE_STAMP = /^(?:working|blocked|done|idle):\d+$/u;
256
256
  export function isAgentPane(pane) {
257
- if (pane.teamMemberName)
257
+ if (pane.teamMemberName || pane.team)
258
258
  return true;
259
259
  // A well-formed @agent_state stamp IS the agent contract: any pane that
260
260
  // self-reports is an agent pane, even when its command is a bare shell and
@@ -416,6 +416,7 @@ function projectApplicationShellResourceV1Core(session, paneIds, nowSec) {
416
416
  // labels such as `Terminal` with the canonical harness name.
417
417
  name: resolvedAgentLabel(pane, presentation, index),
418
418
  harness: harnessForPane(pane),
419
+ ...(pane.team ? { team: pane.team } : {}),
419
420
  activity: presentation.activity,
420
421
  paneId,
421
422
  attention: presentation.attention,
@@ -106,6 +106,7 @@ function toPresentationPane(pane, index) {
106
106
  agentStatusTextRaw: pane.agentStatusTextRaw,
107
107
  agentDisplayNameRaw: pane.agentDisplayNameRaw,
108
108
  teamMemberName: pane.teamMemberName,
109
+ team: pane.team,
109
110
  // Authority-only: the fleet never scrapes an unopened session. `null` (not
110
111
  // `undefined`) keeps `resolveAgentPresentation` on the ground-truth path
111
112
  // while its scrape verdict resolves to `unknown` without any capture.
@@ -126,6 +127,7 @@ function projectSession(session, nowSec, remainingAgentBudget) {
126
127
  agentId: fleetAgentIdForPane(session.name, pane),
127
128
  name: fleetLabel(resolvedAgentLabel(presentationPane, presentation, index), `Agent ${index + 1}`),
128
129
  harness: harnessForPane(presentationPane),
130
+ ...(pane.team ? { team: pane.team } : {}),
129
131
  activity: presentation.activity,
130
132
  attention: presentation.attention,
131
133
  statusSource: presentation.statusSource,
@@ -7,7 +7,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  return path;
8
8
  };
9
9
  import { join } from "node:path";
10
- import { createClaudeTeamNameReader } from "../terminal/attachments/claude-team-names.js";
10
+ import { createClaudeTeamMembershipReader } from "../terminal/attachments/claude-team-names.js";
11
11
  import { PaneSourceDiscovery } from "./pane-source-discovery.js";
12
12
  import { createBackgroundNativeCapture } from "./background-native-capture.js";
13
13
  import { createOwnedViewerAdapterFactory } from "./owned-viewer-factory.js";
@@ -709,7 +709,7 @@ async function startEmbeddedDaemonGeneration(opts) {
709
709
  // observe one daemon-generation authority rather than whichever server a
710
710
  // caller's ambient TMUX/PATH happens to select.
711
711
  const tmuxAuthority = resolveWorkspacePaneTmuxAuthority();
712
- const readTeamNames = createClaudeTeamNameReader(join(resolveRuntimeNamespace().claudeDir, "teams"));
712
+ const readTeamMemberships = createClaudeTeamMembershipReader(join(resolveRuntimeNamespace().claudeDir, "teams"));
713
713
  const catalogTmuxRunner = createPinnedWorkspaceTmuxRunner(tmuxAuthority);
714
714
  const initialTmuxProof = captureTmuxServerProof(catalogTmuxRunner);
715
715
  const initialNativeIdentityParts = initialTmuxProof
@@ -1148,7 +1148,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1148
1148
  },
1149
1149
  agentStatusProbeFactory: ({ run }) => createTmuxAgentStatusProbe({
1150
1150
  run,
1151
- readTeamNames,
1151
+ readTeamMemberships,
1152
1152
  captureNative: (pane, signal) => backgroundCapture?.({
1153
1153
  paneId: pane.runtimePaneId,
1154
1154
  nativeIdentity: pane.nativeIdentity ?? null,
@@ -1393,7 +1393,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1393
1393
  : null, () => observationSelector?.nativeServerEpoch ?? null);
1394
1394
  if (!fleet)
1395
1395
  return null;
1396
- const names = await readTeamNames(fleet.flatMap((session) => session.panes.map((pane) => ({
1396
+ const names = await readTeamMemberships(fleet.flatMap((session) => session.panes.map((pane) => ({
1397
1397
  runtimePaneId: pane.runtimePaneId,
1398
1398
  pid: pane.incarnation,
1399
1399
  }))));
@@ -1401,7 +1401,9 @@ async function startEmbeddedDaemonGeneration(opts) {
1401
1401
  ...session,
1402
1402
  panes: session.panes.map((pane) => {
1403
1403
  const name = names.get(pane.runtimePaneId);
1404
- return name ? { ...pane, teamMemberName: name } : pane;
1404
+ return name
1405
+ ? { ...pane, teamMemberName: name.name, team: pane.team ?? name.team }
1406
+ : pane;
1405
1407
  }),
1406
1408
  }));
1407
1409
  },
@@ -0,0 +1,28 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { isAbsolute } from "node:path";
3
+ import { runtimeTmuxArgs } from "./lib/runtime-namespace.js";
4
+ import { manualPaneTeamStamp, PANE_TEAM_OPTION } from "./terminal/attachments/manual-pane-team.js";
5
+ /** Explicit membership only. Never launches, stops, or messages an agent. */
6
+ export function assignPaneTeam(options, run = (args) => execFileSync("tmux", args, { encoding: "utf8", timeout: 5000 })) {
7
+ if (!/^%\d+$/u.test(options.paneId))
8
+ throw new Error("Use an exact pane ID such as %3");
9
+ if (options.socketPath && options.socketName)
10
+ throw new Error("Choose one tmux socket selector");
11
+ if (options.socketPath && !isAbsolute(options.socketPath))
12
+ throw new Error("Socket path must be absolute");
13
+ if (options.socketName && !/^[a-zA-Z0-9_.-]+$/u.test(options.socketName))
14
+ throw new Error("Invalid socket name");
15
+ const args = (command) => options.socketPath
16
+ ? ["-S", options.socketPath, ...command]
17
+ : options.socketName
18
+ ? ["-L", options.socketName, ...command]
19
+ : runtimeTmuxArgs(command);
20
+ const pid = Number(run(args(["display-message", "-p", "-t", options.paneId, "#{pane_pid}"])).trim());
21
+ if (!Number.isSafeInteger(pid) || pid < 1)
22
+ throw new Error("Pane is no longer available");
23
+ const stamp = options.name === null ? null : manualPaneTeamStamp(options.name, pid);
24
+ run(args(stamp === null
25
+ ? ["set-option", "-p", "-u", "-t", options.paneId, PANE_TEAM_OPTION]
26
+ : ["set-option", "-p", "-t", options.paneId, PANE_TEAM_OPTION, stamp]));
27
+ return { paneId: options.paneId, team: options.name, source: stamp === null ? null : "manual" };
28
+ }
@@ -1,3 +1,4 @@
1
+ import { readManualPaneTeam } from "./manual-pane-team.js";
1
2
  /**
2
3
  * Agent-status probe for the application-shell inventory — the IO half of the
3
4
  * desktop projection's ground-truth agent detection.
@@ -40,6 +41,7 @@ const AGENT_OPTIONS_FORMAT = [
40
41
  "#{@agent_display_name}",
41
42
  "#{@agent_hint}",
42
43
  "#{pane_pid}",
44
+ "#{@tmux_ide_team}",
43
45
  AGENT_LINE_SENTINEL,
44
46
  ].join(AGENT_FIELD_SEPARATOR);
45
47
  /** Trailing non-empty lines captured for the scrape fallback. */
@@ -69,7 +71,7 @@ function parseAgentOptions(stdout) {
69
71
  continue;
70
72
  const fields = line.split(AGENT_FIELD_SEPARATOR);
71
73
  // paneId, state, statusText, displayName, hint, pid, sentinel
72
- if (fields.length !== 7 || fields[6] !== AGENT_LINE_SENTINEL)
74
+ if (![7, 8].includes(fields.length) || fields.at(-1) !== AGENT_LINE_SENTINEL)
73
75
  continue;
74
76
  const runtimePaneId = fields[0];
75
77
  if (!RUNTIME_PANE_ID.test(runtimePaneId))
@@ -77,6 +79,7 @@ function parseAgentOptions(stdout) {
77
79
  const pidText = fields[5];
78
80
  const pid = /^[0-9]+$/u.test(pidText) ? Number(pidText) : null;
79
81
  result.set(runtimePaneId, {
82
+ team: fields.length === 8 ? readManualPaneTeam(fields[6], pid) : undefined,
80
83
  stateRaw: emptyToNull(fields[1]),
81
84
  statusTextRaw: emptyToNull(fields[2]),
82
85
  displayNameRaw: emptyToNull(fields[3]),
@@ -166,6 +169,7 @@ export function createTmuxAgentStatusProbe(deps) {
166
169
  const candidates = [];
167
170
  const emit = (pane, raw, scrape, agentKind) => {
168
171
  facts.set(pane.runtimePaneId, {
172
+ ...(raw?.team ? { team: raw.team } : {}),
169
173
  agentKind,
170
174
  agentStateRaw: raw?.stateRaw ?? null,
171
175
  agentStatusTextRaw: raw?.statusTextRaw ?? null,
@@ -280,6 +284,22 @@ export function createTmuxAgentStatusProbe(deps) {
280
284
  });
281
285
  emit(pane, raw, verdict, manifest.id);
282
286
  }
287
+ if (deps.readTeamMemberships) {
288
+ const memberships = await deps.readTeamMemberships(input.panes.flatMap((pane) => {
289
+ const pid = options.get(pane.runtimePaneId)?.pid;
290
+ return pid ? [{ runtimePaneId: pane.runtimePaneId, pid }] : [];
291
+ }));
292
+ throwIfAborted(signal);
293
+ for (const [paneId, member] of memberships) {
294
+ const fact = facts.get(paneId);
295
+ if (fact)
296
+ facts.set(paneId, {
297
+ ...fact,
298
+ teamMemberName: member.name,
299
+ team: fact.team ?? member.team,
300
+ });
301
+ }
302
+ }
283
303
  if (deps.readTeamNames) {
284
304
  const names = await deps.readTeamNames(input.panes.flatMap((pane) => {
285
305
  const pid = options.get(pane.runtimePaneId)?.pid;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  /** Read-only compatibility adapter for Claude Code's runtime team metadata.
2
3
  * Names are presentation evidence, never agent identity or permission authority.
3
4
  */
@@ -53,12 +54,15 @@ function matchesProcess(command, member) {
53
54
  }
54
55
  return true;
55
56
  }
56
- export function resolveClaudeTeamName(pane, members, processes) {
57
+ export function resolveClaudeTeamMember(pane, members, processes) {
57
58
  // A native pane id alone cannot bind a team: different servers reuse %N.
58
59
  // Its live shell PID scopes the match to this host and this pane's subtree.
59
60
  const subtree = subtreeEntries(processes, pane.pid);
60
61
  const matches = members.filter((m) => m.paneId === pane.runtimePaneId && subtree.some((p) => matchesProcess(p.command, m)));
61
- return matches.length === 1 ? matches[0].name : null;
62
+ return matches.length === 1 ? matches[0] : null;
63
+ }
64
+ export function resolveClaudeTeamName(pane, members, processes) {
65
+ return resolveClaudeTeamMember(pane, members, processes)?.name ?? null;
62
66
  }
63
67
  async function readMembers(directory) {
64
68
  try {
@@ -94,7 +98,7 @@ async function readMembers(directory) {
94
98
  return [];
95
99
  }
96
100
  }
97
- export function createClaudeTeamNameReader(directory, deps = {}) {
101
+ export function createClaudeTeamMembershipReader(directory, deps = {}) {
98
102
  const now = deps.now ?? Date.now;
99
103
  let snapshot = null;
100
104
  let pending = null;
@@ -121,10 +125,22 @@ export function createClaudeTeamNameReader(directory, deps = {}) {
121
125
  const current = snapshot;
122
126
  const names = new Map();
123
127
  for (const pane of panes) {
124
- const name = resolveClaudeTeamName(pane, current.members, current.processes);
125
- if (name)
126
- names.set(pane.runtimePaneId, name);
128
+ const member = resolveClaudeTeamMember(pane, current.members, current.processes);
129
+ if (member)
130
+ names.set(pane.runtimePaneId, {
131
+ name: member.name,
132
+ team: {
133
+ id: `team.${createHash("sha256").update(member.team).digest("hex").slice(0, 32)}`,
134
+ name: member.team.slice(0, 80),
135
+ source: "claude-code",
136
+ },
137
+ });
127
138
  }
128
139
  return names;
129
140
  };
130
141
  }
142
+ /** Compatibility API for callers interested only in the shared display name. */
143
+ export function createClaudeTeamNameReader(directory, deps = {}) {
144
+ const read = createClaudeTeamMembershipReader(directory, deps);
145
+ return async (panes) => new Map([...(await read(panes))].map(([id, value]) => [id, value.name]));
146
+ }
@@ -0,0 +1,30 @@
1
+ import { createHash } from "node:crypto";
2
+ import { PaneTeamMembershipSchemaZ } from "@tmux-ide/contracts";
3
+ export const PANE_TEAM_OPTION = "@tmux_ide_team";
4
+ /** Pane-local metadata survives layout moves, but never a replacement process. */
5
+ export function manualPaneTeamStamp(name, pid) {
6
+ if (!Number.isSafeInteger(pid) || pid < 1)
7
+ throw new Error("Invalid pane process");
8
+ const team = PaneTeamMembershipSchemaZ.parse({
9
+ id: `team.${createHash("sha256").update(name).digest("hex").slice(0, 32)}`,
10
+ name,
11
+ source: "manual",
12
+ });
13
+ return `v1.${Buffer.from(JSON.stringify({ version: 1, pid, team })).toString("base64url")}`;
14
+ }
15
+ export function readManualPaneTeam(raw, pid) {
16
+ if (!raw || raw.length > 2048 || !pid)
17
+ return undefined;
18
+ try {
19
+ if (!/^v1\.[A-Za-z0-9_-]+$/u.test(raw))
20
+ return undefined;
21
+ const value = JSON.parse(Buffer.from(raw.slice(3), "base64url").toString("utf8"));
22
+ if (value.version !== 1 || value.pid !== pid)
23
+ return undefined;
24
+ const parsed = PaneTeamMembershipSchemaZ.safeParse(value.team);
25
+ return parsed.success && parsed.data.source === "manual" ? parsed.data : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
@@ -189,7 +189,7 @@ export function HomeAgentRoster(props) {
189
189
  {(key) => {
190
190
  const row = () => byKey().get(key);
191
191
  return (<box height={rowHeight()} width={width()} flexShrink={0} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
192
- <AgentRow surface="canvas" theme={props.theme} id={`home-agent:${key}`} name={row().name} context={[row().machineLabel, row().serverLabel, row().sessionName]
192
+ <AgentRow surface="canvas" theme={props.theme} id={`home-agent:${key}`} name={row().name} team={row().team} context={[row().machineLabel, row().serverLabel, row().sessionName]
193
193
  .filter(Boolean)
194
194
  .join(" · ")} width={width()} compact={rowHeight() === 1} interaction={props.interactionForAgent?.(row())} paneName={props.paneName} activity={row().activity} attention={row().attention} unavailable={row().paneId === null || stale(row())} selected={props.selection.selectedKey === key} focused={props.inputActive && !editing() && props.selection.selectedKey === key} hovered={hovered() === key} onOpen={(source) => {
195
195
  if (!props.inputActive || row().paneId === null || stale(row()))
@@ -30,6 +30,7 @@ export function projectHomeAgentRows(session, shell) {
30
30
  windowId: shell.resource.terminalInventory?.resources.find((resource) => resource.attachability.status === "available" &&
31
31
  resource.attachability.semanticPaneId === agent.paneId)?.windowResourceId ?? null,
32
32
  name: agent.name,
33
+ ...(agent.team ? { team: agent.team } : {}),
33
34
  harness: agent.harness,
34
35
  activity: agent.activity,
35
36
  attention: agent.attention,
@@ -1,3 +1,4 @@
1
+ import { groupApplicationTeamRows } from "./application-team-groups.js";
1
2
  import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
2
3
  import { createHomeAgentSelectionOwner } from "./application-home-agent-selection.js";
3
4
  export function projectHomeFleet(catalog, groups, filter) {
@@ -52,6 +53,7 @@ export function projectHomeFleet(catalog, groups, filter) {
52
53
  const matching = query
53
54
  ? rows.filter((row) => [
54
55
  row.name,
56
+ row.team?.name,
55
57
  row.harness,
56
58
  row.projectName,
57
59
  row.sessionName,
@@ -67,7 +69,7 @@ export function projectHomeFleet(catalog, groups, filter) {
67
69
  ? "loading"
68
70
  : "unavailable"
69
71
  : "live",
70
- rows: matching,
72
+ rows: groupApplicationTeamRows(matching),
71
73
  observedSessions,
72
74
  totalSessions,
73
75
  loadingSessions,
@@ -1,3 +1,4 @@
1
+ import { groupApplicationTeamRows } from "./application-team-groups.js";
1
2
  import { CHROME_ACTIONS, SIDEBAR_ACTIONS } from "../workspace/application-action-descriptions.js";
2
3
  import { AgentRow } from "../ui/agent-row.jsx";
3
4
  import { fleetHostColor } from "./fleet-presentation.js";
@@ -45,7 +46,7 @@ export function ApplicationMachineSidebar(props) {
45
46
  : row.session?.name === props.model.activeSessionName());
46
47
  const preferenceKey = (group) => group.environmentId ?? group.id;
47
48
  const rows = createMemo(() => [
48
- ...props.model.groups().flatMap((group) => (group.agents ?? []).map((agent) => ({
49
+ ...props.model.groups().flatMap((group) => groupApplicationTeamRows(group.agents ?? []).map((agent) => ({
49
50
  key: JSON.stringify([
50
51
  group.id,
51
52
  "agent",
@@ -374,7 +375,7 @@ export function ApplicationMachineSidebar(props) {
374
375
  : row.group.state === "ready"
375
376
  ? undefined
376
377
  : connectionDetail(row.group)} selected={Boolean(row.session && active(row))} focused={focused() && row.key === selectedKey()} onActivate={(source) => activate(row, source)}/>}>
377
- {(agent) => (<AgentRow theme={props.theme} id={`machine:${row.key}`} name={agent().name} context={[
378
+ {(agent) => (<AgentRow theme={props.theme} id={`machine:${row.key}`} name={agent().name} team={agent().team} context={[
378
379
  row.group.label,
379
380
  row.group.sessions.find((session) => session.server?.serverId === agent().server?.serverId)?.serverLabel,
380
381
  friendlySessionLabel(agent().sessionName),
@@ -0,0 +1,22 @@
1
+ import { paneTeamGroupKey, } from "@tmux-ide/contracts";
2
+ export function applicationTeamKey(row) {
3
+ return row.team
4
+ ? paneTeamGroupKey(row.team, row.machineId ?? "local", row.server?.serverId ?? "default", row.server?.generation ?? row.daemonInstanceId ?? "")
5
+ : null;
6
+ }
7
+ /** Group by membership, never by name, activity, or current tmux window. Stable within groups. */
8
+ export function groupApplicationTeamRows(rows) {
9
+ const groups = new Map();
10
+ const ungrouped = [];
11
+ for (const row of rows) {
12
+ const key = applicationTeamKey(row);
13
+ if (key === null) {
14
+ ungrouped.push(row);
15
+ continue;
16
+ }
17
+ const group = groups.get(key) ?? [];
18
+ group.push(row);
19
+ groups.set(key, group);
20
+ }
21
+ return [...groups.values()].flat().concat(ungrouped);
22
+ }
@@ -40,7 +40,7 @@ export function AgentRow(props) {
40
40
  event.stopPropagation();
41
41
  props.onOpen("mouse");
42
42
  }}>
43
- <NavigationRow surface={props.surface} theme={props.theme} id={props.id} width={props.width} label={props.name} marker={receipt()
43
+ <NavigationRow surface={props.surface} theme={props.theme} id={props.id} width={props.width} label={props.compact && props.team ? `${props.team.name} / ${props.name}` : props.name} marker={receipt()
44
44
  ? receiptMarker()
45
45
  : props.activity === "idle" && !props.attention && !props.unavailable
46
46
  ? " "
@@ -53,7 +53,7 @@ export function AgentRow(props) {
53
53
  : status()?.label} selected={props.selected} focused={props.focused} hovered={props.hovered} disabled={props.unavailable} tone={status()?.tone === "blocked" ? "warning" : "neutral"} onActivate={props.onOpen}/>
54
54
  <Show when={!props.compact}>
55
55
  <text height={1} width={props.width} fg={props.selected ? palette().foreground : props.theme.roles.text.muted}>
56
- {clipTerminal(` ${props.context}`, props.width)}
56
+ {clipTerminal(` ${[props.team?.name, props.context].filter(Boolean).join(" · ")}`, props.width)}
57
57
  </text>
58
58
  </Show>
59
59
  </box>);
@@ -1,3 +1,5 @@
1
+ import { readManualPaneTeam } from "../terminal/attachments/manual-pane-team.ts";
2
+ import type { PaneTeamMembership } from "@tmux-ide/contracts";
1
3
  import { decodeTmuxArgument } from "../terminal/protocol/session-descriptor-discovery.ts";
2
4
  import type { NativePaneIdentity } from "@tmux-ide/contracts";
3
5
  import { nativePaneIdentity } from "../lib/native-pane-identity.ts";
@@ -224,6 +226,7 @@ export function readAdoptedSessionNames(runTmux: TmuxRunner = _tmuxRunner): stri
224
226
 
225
227
  /** One live pane, with the raw agent-authority options gathered for the fleet. */
226
228
  export interface FleetPaneFacts {
229
+ readonly team?: PaneTeamMembership;
227
230
  readonly title?: string;
228
231
  readonly name?: string | null;
229
232
  readonly nameSource?: string | null;
@@ -279,6 +282,7 @@ const FLEET_PANE_FORMAT = [
279
282
  "#{qa:pane_title}",
280
283
  "#{qa:@ide_name}",
281
284
  "#{qa:@tmux_ide_name_source}",
285
+ "#{@tmux_ide_team}",
282
286
  FLEET_LINE_SENTINEL,
283
287
  ].join(FLEET_FIELD_SEPARATOR);
284
288
 
@@ -325,7 +329,7 @@ export function readAdoptedFleet(
325
329
  const fields = line.split(FLEET_FIELD_SEPARATOR);
326
330
  // session, pane, semantic pane, incarnation pid, active, command, path, state,
327
331
  // statusText, displayName, hint, birth ID, quoted title/name/source, sentinel
328
- if (fields.length !== 16 || fields[15] !== FLEET_LINE_SENTINEL) continue;
332
+ if (![16, 17].includes(fields.length) || fields.at(-1) !== FLEET_LINE_SENTINEL) continue;
329
333
  const sessionName = fields[0]!;
330
334
  if (!adoptedSet.has(sessionName)) continue;
331
335
  const runtimePaneId = fields[1]!;
@@ -338,6 +342,7 @@ export function readAdoptedFleet(
338
342
  panesBySession.set(sessionName, panes);
339
343
  }
340
344
  panes.push({
345
+ team: fields.length === 17 ? readManualPaneTeam(fields[15], incarnation) : undefined,
341
346
  nameSource: emptyToNull(decodeTmuxArgument(fields[14]!)),
342
347
  title: decodeTmuxArgument(fields[12]!),
343
348
  name: emptyToNull(decodeTmuxArgument(fields[13]!)),
@@ -1,3 +1,4 @@
1
+ import type { PaneTeamMembership } from "@tmux-ide/contracts";
1
2
  import { resolvePaneDisplayName } from "../../terminal/protocol/pane-display-name.ts";
2
3
  import type { NativePaneIdentity } from "@tmux-ide/contracts";
3
4
  import { hostname } from "node:os";
@@ -72,6 +73,7 @@ export interface ApplicationShellPanePresentationFacts {
72
73
  /** Raw `@agent_display_name`; sanitized + freshness-gated in the pure layer. */
73
74
  readonly agentDisplayNameRaw?: string | null;
74
75
  readonly teamMemberName?: string;
76
+ readonly team?: PaneTeamMembership;
75
77
  /**
76
78
  * Screen-scrape fallback verdict the discovery layer resolved for panes
77
79
  * WITHOUT fresh authority. `null` means authority was fresh (scrape skipped);
@@ -431,7 +433,7 @@ export function resolvedAgentLabel(
431
433
  const AGENT_STATE_STAMP = /^(?:working|blocked|done|idle):\d+$/u;
432
434
 
433
435
  export function isAgentPane(pane: ApplicationShellPanePresentationFacts): boolean {
434
- if (pane.teamMemberName) return true;
436
+ if (pane.teamMemberName || pane.team) return true;
435
437
  // A well-formed @agent_state stamp IS the agent contract: any pane that
436
438
  // self-reports is an agent pane, even when its command is a bare shell and
437
439
  // no @ide_type/role metadata exists. Staleness only affects the status, not
@@ -630,6 +632,7 @@ function projectApplicationShellResourceV1Core(
630
632
  // labels such as `Terminal` with the canonical harness name.
631
633
  name: resolvedAgentLabel(pane, presentation, index),
632
634
  harness: harnessForPane(pane),
635
+ ...(pane.team ? { team: pane.team } : {}),
633
636
  activity: presentation.activity,
634
637
  paneId,
635
638
  attention: presentation.attention,
@@ -136,6 +136,7 @@ function toPresentationPane(
136
136
  agentStatusTextRaw: pane.agentStatusTextRaw,
137
137
  agentDisplayNameRaw: pane.agentDisplayNameRaw,
138
138
  teamMemberName: pane.teamMemberName,
139
+ team: pane.team,
139
140
  // Authority-only: the fleet never scrapes an unopened session. `null` (not
140
141
  // `undefined`) keeps `resolveAgentPresentation` on the ground-truth path
141
142
  // while its scrape verdict resolves to `unknown` without any capture.
@@ -163,6 +164,7 @@ function projectSession(
163
164
  `Agent ${index + 1}`,
164
165
  ),
165
166
  harness: harnessForPane(presentationPane),
167
+ ...(pane.team ? { team: pane.team } : {}),
166
168
  activity: presentation.activity,
167
169
  attention: presentation.attention,
168
170
  statusSource: presentation.statusSource,
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { createClaudeTeamNameReader } from "../terminal/attachments/claude-team-names.ts";
2
+ import { createClaudeTeamMembershipReader } from "../terminal/attachments/claude-team-names.ts";
3
3
  import { PaneSourceDiscovery } from "./pane-source-discovery.ts";
4
4
  import { createBackgroundNativeCapture } from "./background-native-capture.ts";
5
5
  import { createOwnedViewerAdapterFactory } from "./owned-viewer-factory.ts";
@@ -1131,7 +1131,7 @@ async function startEmbeddedDaemonGeneration(
1131
1131
  // observe one daemon-generation authority rather than whichever server a
1132
1132
  // caller's ambient TMUX/PATH happens to select.
1133
1133
  const tmuxAuthority = resolveWorkspacePaneTmuxAuthority();
1134
- const readTeamNames = createClaudeTeamNameReader(
1134
+ const readTeamMemberships = createClaudeTeamMembershipReader(
1135
1135
  join(resolveRuntimeNamespace().claudeDir, "teams"),
1136
1136
  );
1137
1137
  const catalogTmuxRunner = createPinnedWorkspaceTmuxRunner(tmuxAuthority);
@@ -1628,7 +1628,7 @@ async function startEmbeddedDaemonGeneration(
1628
1628
  agentStatusProbeFactory: ({ run }) =>
1629
1629
  createTmuxAgentStatusProbe({
1630
1630
  run,
1631
- readTeamNames,
1631
+ readTeamMemberships,
1632
1632
  captureNative: (pane, signal) =>
1633
1633
  backgroundCapture?.(
1634
1634
  {
@@ -1923,7 +1923,7 @@ async function startEmbeddedDaemonGeneration(
1923
1923
  () => observationSelector?.nativeServerEpoch ?? null,
1924
1924
  );
1925
1925
  if (!fleet) return null;
1926
- const names = await readTeamNames(
1926
+ const names = await readTeamMemberships(
1927
1927
  fleet.flatMap((session) =>
1928
1928
  session.panes.map((pane) => ({
1929
1929
  runtimePaneId: pane.runtimePaneId,
@@ -1935,7 +1935,9 @@ async function startEmbeddedDaemonGeneration(
1935
1935
  ...session,
1936
1936
  panes: session.panes.map((pane) => {
1937
1937
  const name = names.get(pane.runtimePaneId);
1938
- return name ? { ...pane, teamMemberName: name } : pane;
1938
+ return name
1939
+ ? { ...pane, teamMemberName: name.name, team: pane.team ?? name.team }
1940
+ : pane;
1939
1941
  }),
1940
1942
  }));
1941
1943
  },