tmux-ide 2.9.0-beta.47 → 2.9.0-beta.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +2764 -2425
- package/package.json +2 -2
- package/packages/contracts/src/cohesion-fixture.ts +2 -0
- package/packages/contracts/src/fleet-catalog.ts +2 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/pane-team.ts +32 -0
- package/packages/daemon/dist/command-center/discovery.js +12 -2
- package/packages/daemon/dist/command-center/resources/agent-graph-overlay.js +2 -2
- package/packages/daemon/dist/command-center/resources/application-shell.js +22 -15
- package/packages/daemon/dist/command-center/resources/fleet-catalog.js +8 -4
- package/packages/daemon/dist/command-center/resources/fleet-resource-route.js +2 -2
- package/packages/daemon/dist/lib/daemon-embed.js +24 -3
- package/packages/daemon/dist/pane-team.js +28 -0
- package/packages/daemon/dist/terminal/attachments/agent-status-probe.js +33 -1
- package/packages/daemon/dist/terminal/attachments/claude-team-names.js +146 -0
- package/packages/daemon/dist/terminal/attachments/manual-pane-team.js +30 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +1 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +1 -0
- package/packages/daemon/dist/terminal/protocol/pane-display-name.js +40 -11
- package/packages/daemon/dist/terminal/protocol/session-descriptor-discovery.js +4 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +17 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +1 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +1 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +3 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-new-agent-owner.js +86 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +5 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-input.js +1 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-team-groups.js +22 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +24 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +7 -5
- package/packages/daemon/dist/tui/mirror/ui/agent-row.jsx +2 -2
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +6 -0
- package/packages/daemon/src/command-center/discovery.ts +18 -2
- package/packages/daemon/src/command-center/resources/agent-graph-overlay.ts +2 -1
- package/packages/daemon/src/command-center/resources/application-shell.ts +24 -18
- package/packages/daemon/src/command-center/resources/fleet-catalog.ts +11 -3
- package/packages/daemon/src/command-center/resources/fleet-resource-route.ts +5 -3
- package/packages/daemon/src/command-center/server.ts +1 -1
- package/packages/daemon/src/lib/daemon-embed.ts +31 -4
- package/packages/daemon/src/pane-team.ts +42 -0
- package/packages/daemon/src/terminal/attachments/agent-status-probe.ts +46 -1
- package/packages/daemon/src/terminal/attachments/claude-team-names.ts +201 -0
- package/packages/daemon/src/terminal/attachments/manual-pane-team.ts +31 -0
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +2 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +1 -0
- package/packages/daemon/src/terminal/mirror/trusted-inventory.ts +1 -0
- package/packages/daemon/src/terminal/protocol/pane-display-name.ts +51 -13
- package/packages/daemon/src/terminal/protocol/session-descriptor-discovery.ts +4 -4
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +19 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +3 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +5 -1
- package/packages/daemon/src/tui/mirror/runtime/application-new-agent-dialog.tsx +97 -0
- package/packages/daemon/src/tui/mirror/runtime/application-new-agent-owner.ts +89 -0
- package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +6 -0
- package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +5 -4
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +23 -1
- package/packages/daemon/src/tui/mirror/runtime/application-team-groups.ts +40 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +27 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +10 -5
- package/packages/daemon/src/tui/mirror/ui/agent-row.tsx +7 -3
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-ide",
|
|
3
|
-
"version": "2.9.0-beta.
|
|
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,
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
|
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:
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
|
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),
|