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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.47",
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-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,5 @@
1
+ import { readManualPaneTeam } from "../terminal/attachments/manual-pane-team.js";
2
+ import { decodeTmuxArgument } from "../terminal/protocol/session-descriptor-discovery.js";
1
3
  import { nativePaneIdentity } from "../lib/native-pane-identity.js";
2
4
  import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
3
5
  import { runtimeTmuxArgs } from "../lib/runtime-namespace.js";
@@ -197,6 +199,10 @@ const FLEET_PANE_FORMAT = [
197
199
  "#{@agent_display_name}",
198
200
  "#{@agent_hint}",
199
201
  "#{pane_birth_id}",
202
+ "#{qa:pane_title}",
203
+ "#{qa:@ide_name}",
204
+ "#{qa:@tmux_ide_name_source}",
205
+ "#{@tmux_ide_team}",
200
206
  FLEET_LINE_SENTINEL,
201
207
  ].join(FLEET_FIELD_SEPARATOR);
202
208
  function emptyToNull(value) {
@@ -234,8 +240,8 @@ export function readAdoptedFleet(registry = getDefaultWorkspaceRegistry(), runTm
234
240
  continue;
235
241
  const fields = line.split(FLEET_FIELD_SEPARATOR);
236
242
  // session, pane, semantic pane, incarnation pid, active, command, path, state,
237
- // statusText, displayName, hint, sentinel
238
- if (fields.length !== 13 || fields[12] !== FLEET_LINE_SENTINEL)
243
+ // statusText, displayName, hint, birth ID, quoted title/name/source, sentinel
244
+ if (![16, 17].includes(fields.length) || fields.at(-1) !== FLEET_LINE_SENTINEL)
239
245
  continue;
240
246
  const sessionName = fields[0];
241
247
  if (!adoptedSet.has(sessionName))
@@ -252,6 +258,10 @@ export function readAdoptedFleet(registry = getDefaultWorkspaceRegistry(), runTm
252
258
  panesBySession.set(sessionName, panes);
253
259
  }
254
260
  panes.push({
261
+ team: fields.length === 17 ? readManualPaneTeam(fields[15], incarnation) : undefined,
262
+ nameSource: emptyToNull(decodeTmuxArgument(fields[14])),
263
+ title: decodeTmuxArgument(fields[12]),
264
+ name: emptyToNull(decodeTmuxArgument(fields[13])),
255
265
  runtimePaneId,
256
266
  semanticPaneId: emptyToNull(fields[2]),
257
267
  incarnation,
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AGENT_GRAPH_LABEL_MAX_LENGTH, projectAgentGraphOverlay, } from "@tmux-ide/contracts";
3
- import { isAgentPane, paneIdentities, resolveAgentPresentation, } from "./application-shell.js";
3
+ import { isAgentPane, paneIdentities, resolveAgentPresentation, resolvedAgentLabel, } from "./application-shell.js";
4
4
  /**
5
5
  * The `@ide_role` values (besides `lead`) that a session lead is inferred to
6
6
  * relate to. Kept in lockstep with the roles {@link isAgentPane} recognizes.
@@ -104,7 +104,7 @@ export function projectApplicationShellAgentGraphOverlay(input) {
104
104
  status: nodeStatus(presentation.detectStatus),
105
105
  statusSource: presentation.statusSource,
106
106
  attention: presentation.attention,
107
- label: nodeLabel(presentation.displayName ?? pane.name ?? pane.title),
107
+ label: nodeLabel(resolvedAgentLabel(pane, presentation, index)),
108
108
  });
109
109
  nodeWindowIds.add(windowId);
110
110
  if (pane.role === "lead")
@@ -1,3 +1,4 @@
1
+ import { resolvePaneDisplayName } from "../../terminal/protocol/pane-display-name.js";
1
2
  import { hostname } from "node:os";
2
3
  import { basename } from "node:path";
3
4
  import { ApplicationShellProjectionInputV1WireSchemaZ, ApplicationShellProjectionInputV2SchemaZ, ApplicationShellProjectionInputV3SchemaZ, CANONICAL_SURFACE_REGISTRY, SemanticProductIdSchemaZ, TerminalAttachmentSemanticPaneIdSchemaZ, TerminalAttachmentSemanticWindowIdSchemaZ, projectApplicationShellV1, resolveAgentStatusPresentation, } from "@tmux-ide/contracts";
@@ -209,6 +210,8 @@ function deprecatedStandalonePaneIdentities(panes) {
209
210
  });
210
211
  }
211
212
  export function harnessForPane(pane) {
213
+ if (pane.teamMemberName)
214
+ return "claude-code";
212
215
  const detected = pane.agentKind?.toLowerCase();
213
216
  if (detected === "codex")
214
217
  return "codex";
@@ -221,35 +224,38 @@ export function harnessForPane(pane) {
221
224
  return "claude-code";
222
225
  return "custom";
223
226
  }
224
- const GENERIC_PANE_LABELS = new Set(["shell", "terminal", "tmux"]);
225
- function isGenericPaneLabel(value) {
226
- const normalized = value?.trim().toLowerCase() ?? "";
227
- return normalized.length === 0 || GENERIC_PANE_LABELS.has(normalized);
228
- }
229
227
  /**
230
228
  * A promoted configless pane can be stamped as `Terminal` before the native
231
229
  * inventory's process-tree pass resolves a wrapped/version-shaped agent
232
230
  * executable. Once that stronger identity exists, do not keep publishing the
233
231
  * provisional generic label into the sidebar and pane chrome.
234
232
  */
235
- function resolvedAgentLabel(pane, presentation, index) {
236
- if (presentation.displayName !== undefined) {
237
- return label(presentation.displayName, `Agent ${index + 1}`);
238
- }
239
- if (!isGenericPaneLabel(pane.name))
240
- return label(pane.name, `Agent ${index + 1}`);
233
+ export function resolvedAgentLabel(pane, presentation, index) {
234
+ const resolved = resolvePaneDisplayName({
235
+ semanticPaneId: pane.semanticPaneId ?? `agent.${index}`,
236
+ configuredName: pane.name,
237
+ configuredNameSource: pane.nameSource,
238
+ agentDisplayName: presentation.displayName,
239
+ teamMemberName: pane.teamMemberName,
240
+ currentCommand: pane.currentCommand,
241
+ title: pane.title,
242
+ hostName: hostname(),
243
+ });
244
+ if (["manual", "agent", "title"].includes(resolved.source))
245
+ return label(resolved.name, `Agent ${index + 1}`);
241
246
  const harness = harnessForPane(pane);
242
247
  if (harness === "codex")
243
248
  return "Codex";
244
249
  if (harness === "claude-code")
245
250
  return "Claude Code";
246
- if (!isGenericPaneLabel(pane.title) && !isHostNameTitle(pane.title, hostname())) {
247
- return label(pane.title, `Agent ${index + 1}`);
248
- }
249
- return `Agent ${index + 1}`;
251
+ return resolved.source === "process"
252
+ ? label(resolved.name, `Agent ${index + 1}`)
253
+ : `Agent ${index + 1}`;
250
254
  }
251
255
  const AGENT_STATE_STAMP = /^(?:working|blocked|done|idle):\d+$/u;
252
256
  export function isAgentPane(pane) {
257
+ if (pane.teamMemberName || pane.team)
258
+ return true;
253
259
  // A well-formed @agent_state stamp IS the agent contract: any pane that
254
260
  // self-reports is an agent pane, even when its command is a bare shell and
255
261
  // no @ide_type/role metadata exists. Staleness only affects the status, not
@@ -410,6 +416,7 @@ function projectApplicationShellResourceV1Core(session, paneIds, nowSec) {
410
416
  // labels such as `Terminal` with the canonical harness name.
411
417
  name: resolvedAgentLabel(pane, presentation, index),
412
418
  harness: harnessForPane(pane),
419
+ ...(pane.team ? { team: pane.team } : {}),
413
420
  activity: presentation.activity,
414
421
  paneId,
415
422
  attention: presentation.attention,
@@ -25,7 +25,7 @@
25
25
  import { createHash } from "node:crypto";
26
26
  import { basename } from "node:path";
27
27
  import { FLEET_CATALOG_RESOURCE_VERSION, FLEET_LABEL_MAX_LENGTH, FLEET_MAX_AGENTS_PER_SESSION, FLEET_MAX_PANES_PER_SESSION, FLEET_MAX_SESSIONS, FLEET_MAX_TOTAL_AGENTS, FleetCatalogResourceV1SchemaZ, } from "@tmux-ide/contracts";
28
- import { harnessForPane, isAgentPane, resolveAgentPresentation, } from "./application-shell.js";
28
+ import { harnessForPane, isAgentPane, resolveAgentPresentation, resolvedAgentLabel, } from "./application-shell.js";
29
29
  function digest(value) {
30
30
  return createHash("sha256").update(value).digest("hex").slice(0, 20);
31
31
  }
@@ -94,16 +94,19 @@ function toPresentationPane(pane, index) {
94
94
  return {
95
95
  semanticPaneId: null,
96
96
  index,
97
- title: "",
97
+ title: pane.title ?? "",
98
98
  currentCommand: pane.currentCommand,
99
99
  active: pane.active,
100
100
  role: null,
101
- name: null,
101
+ name: pane.name ?? null,
102
+ nameSource: pane.nameSource,
102
103
  type: null,
103
104
  agentKind: pane.agentHintRaw,
104
105
  agentStateRaw: pane.agentStateRaw,
105
106
  agentStatusTextRaw: pane.agentStatusTextRaw,
106
107
  agentDisplayNameRaw: pane.agentDisplayNameRaw,
108
+ teamMemberName: pane.teamMemberName,
109
+ team: pane.team,
107
110
  // Authority-only: the fleet never scrapes an unopened session. `null` (not
108
111
  // `undefined`) keeps `resolveAgentPresentation` on the ground-truth path
109
112
  // while its scrape verdict resolves to `unknown` without any capture.
@@ -122,8 +125,9 @@ function projectSession(session, nowSec, remainingAgentBudget) {
122
125
  const presentation = resolveAgentPresentation(presentationPane, nowSec);
123
126
  agents.push({
124
127
  agentId: fleetAgentIdForPane(session.name, pane),
125
- name: fleetLabel(presentation.displayName ?? pane.currentCommand, `Agent ${index + 1}`),
128
+ name: fleetLabel(resolvedAgentLabel(presentationPane, presentation, index), `Agent ${index + 1}`),
126
129
  harness: harnessForPane(presentationPane),
130
+ ...(pane.team ? { team: pane.team } : {}),
127
131
  activity: presentation.activity,
128
132
  attention: presentation.attention,
129
133
  statusSource: presentation.statusSource,
@@ -19,7 +19,7 @@ export function mountFleetResourceRoute(app, options) {
19
19
  unavailableMessage: "Fleet catalog capability is unavailable",
20
20
  mismatchMessage: "Fleet catalog access requires owner authority",
21
21
  });
22
- app.get("/api/resources/fleet-catalog", (c) => {
22
+ app.get("/api/resources/fleet-catalog", async (c) => {
23
23
  const gate = authorize(c);
24
24
  if (gate)
25
25
  return gate;
@@ -28,7 +28,7 @@ export function mountFleetResourceRoute(app, options) {
28
28
  // with a successful empty snapshot. Never expose raw socket/process errors.
29
29
  let sessions;
30
30
  try {
31
- sessions = options.readFleet ? options.readFleet() : readAdoptedFleet(options.registry);
31
+ sessions = options.readFleet ? await options.readFleet() : readAdoptedFleet(options.registry);
32
32
  }
33
33
  catch {
34
34
  sessions = null;
@@ -6,6 +6,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ import { join } from "node:path";
10
+ import { createClaudeTeamMembershipReader } from "../terminal/attachments/claude-team-names.js";
9
11
  import { PaneSourceDiscovery } from "./pane-source-discovery.js";
10
12
  import { createBackgroundNativeCapture } from "./background-native-capture.js";
11
13
  import { createOwnedViewerAdapterFactory } from "./owned-viewer-factory.js";
@@ -707,6 +709,7 @@ async function startEmbeddedDaemonGeneration(opts) {
707
709
  // observe one daemon-generation authority rather than whichever server a
708
710
  // caller's ambient TMUX/PATH happens to select.
709
711
  const tmuxAuthority = resolveWorkspacePaneTmuxAuthority();
712
+ const readTeamMemberships = createClaudeTeamMembershipReader(join(resolveRuntimeNamespace().claudeDir, "teams"));
710
713
  const catalogTmuxRunner = createPinnedWorkspaceTmuxRunner(tmuxAuthority);
711
714
  const initialTmuxProof = captureTmuxServerProof(catalogTmuxRunner);
712
715
  const initialNativeIdentityParts = initialTmuxProof
@@ -1145,6 +1148,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1145
1148
  },
1146
1149
  agentStatusProbeFactory: ({ run }) => createTmuxAgentStatusProbe({
1147
1150
  run,
1151
+ readTeamMemberships,
1148
1152
  captureNative: (pane, signal) => backgroundCapture?.({
1149
1153
  paneId: pane.runtimePaneId,
1150
1154
  nativeIdentity: pane.nativeIdentity ?? null,
@@ -1383,9 +1387,26 @@ async function startEmbeddedDaemonGeneration(opts) {
1383
1387
  peekTerminalAttachmentRuntime,
1384
1388
  paneStreamRuntime,
1385
1389
  catalogLiveSessions: () => discoverLiveSessionSummaries(catalogTmuxRunner),
1386
- catalogFleet: () => readAdoptedFleet(workspaceRegistry, nativeGenerationTmuxRunner, (sessionName, pane) => pane.semanticPaneId
1387
- ? (interactionEvidence?.captureInventoryEndpoint(sessionName, pane.runtimePaneId, pane.semanticPaneId) ?? null)
1388
- : null, () => observationSelector?.nativeServerEpoch ?? null),
1390
+ catalogFleet: async () => {
1391
+ const fleet = readAdoptedFleet(workspaceRegistry, nativeGenerationTmuxRunner, (sessionName, pane) => pane.semanticPaneId
1392
+ ? (interactionEvidence?.captureInventoryEndpoint(sessionName, pane.runtimePaneId, pane.semanticPaneId) ?? null)
1393
+ : null, () => observationSelector?.nativeServerEpoch ?? null);
1394
+ if (!fleet)
1395
+ return null;
1396
+ const names = await readTeamMemberships(fleet.flatMap((session) => session.panes.map((pane) => ({
1397
+ runtimePaneId: pane.runtimePaneId,
1398
+ pid: pane.incarnation,
1399
+ }))));
1400
+ return fleet.map((session) => ({
1401
+ ...session,
1402
+ panes: session.panes.map((pane) => {
1403
+ const name = names.get(pane.runtimePaneId);
1404
+ return name
1405
+ ? { ...pane, teamMemberName: name.name, team: pane.team ?? name.team }
1406
+ : pane;
1407
+ }),
1408
+ }));
1409
+ },
1389
1410
  fleetPreviewCapture: createFleetPreviewCapture(fleetFactsTmuxRunner, {
1390
1411
  serverEpoch: () => observationSelector?.nativeServerEpoch ?? null,
1391
1412
  capture: (request, signal) => backgroundCapture?.(request, signal) ?? Promise.resolve(null),
@@ -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,34 @@ 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
+ }
303
+ if (deps.readTeamNames) {
304
+ const names = await deps.readTeamNames(input.panes.flatMap((pane) => {
305
+ const pid = options.get(pane.runtimePaneId)?.pid;
306
+ return pid ? [{ runtimePaneId: pane.runtimePaneId, pid }] : [];
307
+ }));
308
+ throwIfAborted(signal);
309
+ for (const [paneId, name] of names) {
310
+ const fact = facts.get(paneId);
311
+ if (fact)
312
+ facts.set(paneId, { ...fact, teamMemberName: name });
313
+ }
314
+ }
283
315
  // Bound the cache: pane ids are server-unique and never re-probed after a
284
316
  // pane dies, so evict the oldest verdicts past the ceiling.
285
317
  if (stagedVerdicts.size > SCRAPE_CACHE_MAX_ENTRIES) {
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Read-only compatibility adapter for Claude Code's runtime team metadata.
3
+ * Names are presentation evidence, never agent identity or permission authority.
4
+ */
5
+ import { open, readdir } from "node:fs/promises";
6
+ import { join, basename } from "node:path";
7
+ import { readProcessTableAsync, subtreeEntries, } from "../../tui/detect/process-tree.js";
8
+ const token = (value) => typeof value === "string" &&
9
+ /^[a-zA-Z0-9_.@-]{1,160}$/u.test(value) &&
10
+ value !== "." &&
11
+ value !== "..";
12
+ export function parseClaudeTeamMembers(value, directory) {
13
+ if (!value || typeof value !== "object")
14
+ return [];
15
+ const config = value;
16
+ if (!token(directory) ||
17
+ config.name !== directory ||
18
+ !Array.isArray(config.members) ||
19
+ config.members.length > 128)
20
+ return [];
21
+ return config.members.flatMap((value) => {
22
+ if (!value || typeof value !== "object")
23
+ return [];
24
+ const m = value;
25
+ if (!token(m.name) ||
26
+ m.name.length > 80 ||
27
+ !token(m.agentId) ||
28
+ m.backendType !== "tmux" ||
29
+ typeof m.tmuxPaneId !== "string" ||
30
+ !/^%\d+$/u.test(m.tmuxPaneId))
31
+ return [];
32
+ return [{ team: directory, name: m.name, agentId: m.agentId, paneId: m.tmuxPaneId }];
33
+ });
34
+ }
35
+ /** Conservative argv recognition: flattened ps text is not a shell parser.
36
+ * Ambiguous/quoted arguments fall back to the native title rather than guessing.
37
+ */
38
+ function matchesProcess(command, member) {
39
+ const args = command.trim().split(/\s+/u);
40
+ const executable = args.shift() ?? "";
41
+ if (basename(executable) !== "claude" && !/\/claude\/versions\/\d[\w.-]*$/u.test(executable))
42
+ return false;
43
+ for (const [flag, expected] of [
44
+ ["--team-name", member.team],
45
+ ["--agent-id", member.agentId],
46
+ ["--agent-name", member.name],
47
+ ]) {
48
+ const positions = args.flatMap((arg, index) => arg === flag || arg.startsWith(`${flag}=`) ? [index] : []);
49
+ if (positions.length !== 1)
50
+ return false;
51
+ const i = positions[0];
52
+ if ((args[i] === flag ? args[i + 1] : args[i].slice(flag.length + 1)) !== expected)
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+ export function resolveClaudeTeamMember(pane, members, processes) {
58
+ // A native pane id alone cannot bind a team: different servers reuse %N.
59
+ // Its live shell PID scopes the match to this host and this pane's subtree.
60
+ const subtree = subtreeEntries(processes, pane.pid);
61
+ const matches = members.filter((m) => m.paneId === pane.runtimePaneId && subtree.some((p) => matchesProcess(p.command, m)));
62
+ return matches.length === 1 ? matches[0] : null;
63
+ }
64
+ export function resolveClaudeTeamName(pane, members, processes) {
65
+ return resolveClaudeTeamMember(pane, members, processes)?.name ?? null;
66
+ }
67
+ async function readMembers(directory) {
68
+ try {
69
+ const entries = await readdir(directory, { withFileTypes: true });
70
+ if (entries.length > 128)
71
+ return [];
72
+ const members = [];
73
+ for (const entry of entries) {
74
+ if (!entry.isDirectory() || !token(entry.name))
75
+ continue;
76
+ try {
77
+ const file = await open(join(directory, entry.name, "config.json"), "r");
78
+ try {
79
+ if (!(await file.stat()).isFile())
80
+ continue;
81
+ const bytes = Buffer.alloc(256 * 1024 + 1);
82
+ const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
83
+ if (bytesRead > 256 * 1024)
84
+ continue;
85
+ members.push(...parseClaudeTeamMembers(JSON.parse(bytes.subarray(0, bytesRead).toString("utf8")), entry.name));
86
+ }
87
+ finally {
88
+ await file.close();
89
+ }
90
+ }
91
+ catch {
92
+ /* Removal, atomic replacement and unknown schemas clear old facts. */
93
+ }
94
+ }
95
+ return members;
96
+ }
97
+ catch {
98
+ return [];
99
+ }
100
+ }
101
+ export function createClaudeTeamMembershipReader(directory, deps = {}) {
102
+ const now = deps.now ?? Date.now;
103
+ let snapshot = null;
104
+ let pending = null;
105
+ return async (panes) => {
106
+ if (!panes.length)
107
+ return new Map();
108
+ if (!snapshot || now() - snapshot.at >= 2000) {
109
+ pending ??= (async () => {
110
+ try {
111
+ const members = await (deps.readMembers?.() ?? readMembers(directory));
112
+ const processes = members.length
113
+ ? await (deps.readProcesses?.() ?? readProcessTableAsync())
114
+ : [];
115
+ snapshot = { at: now(), members, processes };
116
+ }
117
+ catch {
118
+ snapshot = { at: now(), members: [], processes: [] };
119
+ }
120
+ })().finally(() => {
121
+ pending = null;
122
+ });
123
+ await pending;
124
+ }
125
+ const current = snapshot;
126
+ const names = new Map();
127
+ for (const pane of panes) {
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
+ });
138
+ }
139
+ return names;
140
+ };
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
+ }
@@ -364,6 +364,7 @@ function projectTrustedMirrorInventory(trusted, workspaceName, expectedSessionNa
364
364
  active: pane.active,
365
365
  role: nullable(pane.role),
366
366
  name: nullable(pane.name),
367
+ ...(pane.nameSource ? { nameSource: nullable(pane.nameSource) } : {}),
367
368
  type: nullable(pane.type),
368
369
  missionStamp: nullable(pane.missionStamp),
369
370
  dir: boundedWireValue(pane.dir, 4_096),