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
@@ -2914,6 +2914,7 @@ export class SessionChannel {
2914
2914
  active: descriptor.paneActive && descriptor.windowActive,
2915
2915
  role: descriptor.role,
2916
2916
  name: descriptor.name,
2917
+ ...(descriptor.nameSource ? { nameSource: descriptor.nameSource } : {}),
2917
2918
  type: descriptor.type,
2918
2919
  missionStamp: descriptor.missionStamp,
2919
2920
  nativePaneBirthId: descriptor.nativePaneBirthId ?? null,
@@ -86,7 +86,14 @@ const GENERIC_SHELLS = new Set([
86
86
  const GENERIC_TITLES = new Set(["shell", "terminal", "tmux"]);
87
87
  function boundedName(value) {
88
88
  const name = value?.trim() ?? "";
89
- return name.length > 0 && name.length <= 80 && !/[\0\r\n\t]/u.test(name) ? name : null;
89
+ return name.length > 0 &&
90
+ name.length <= 80 &&
91
+ [...name].every((c) => {
92
+ const code = c.codePointAt(0);
93
+ return code > 31 && (code < 127 || code > 159);
94
+ })
95
+ ? name
96
+ : null;
90
97
  }
91
98
  function commandBasename(value) {
92
99
  const command = boundedName(value);
@@ -110,20 +117,20 @@ export function memorablePaneName(seed) {
110
117
  const second = stableHash(`${seed}:noun`);
111
118
  return `${ADJECTIVES[first % ADJECTIVES.length]}-${NOUNS[second % NOUNS.length]}`;
112
119
  }
113
- function meaningfulTitle(value, currentCommand, hostName) {
120
+ export function meaningfulPaneTitle(value, currentCommand, hostName) {
114
121
  const title = boundedName(value);
115
122
  if (!title || GENERIC_TITLES.has(title.toLowerCase()))
116
123
  return null;
124
+ if (["claude", "claude code", "codex", "node", "bun", currentCommand?.toLowerCase()].includes(title.toLowerCase()))
125
+ return null;
117
126
  if (title.startsWith("/") || title.startsWith("~") || title.includes("@"))
118
127
  return null;
119
128
  // Interactive shells commonly publish the machine hostname as their tmux
120
129
  // title. It identifies the computer, not the work happening in this pane,
121
130
  // so keep the memorable pane identity until a real foreground process runs.
122
- if (currentCommand &&
123
- GENERIC_SHELLS.has(currentCommand.toLowerCase()) &&
124
- (/^[a-z0-9][a-z0-9.-]*\.[a-z0-9-]{2,}$/iu.test(title) ||
125
- (hostName &&
126
- [hostName, hostName.split(".")[0]].some((host) => host?.toLowerCase() === title.toLowerCase()))))
131
+ if (/^[a-z0-9][a-z0-9.-]*\.[a-z0-9-]{2,}$/iu.test(title) ||
132
+ (hostName &&
133
+ [hostName, hostName.split(".")[0]].some((host) => host?.toLowerCase() === title.toLowerCase())))
127
134
  return null;
128
135
  return title;
129
136
  }
@@ -131,23 +138,45 @@ export function resolvePaneDisplayName(input) {
131
138
  const configuredName = boundedName(input.configuredName);
132
139
  const configuredSource = input.configuredNameSource?.trim().toLowerCase() ?? "";
133
140
  const generatedName = memorablePaneName(input.semanticPaneId);
141
+ const automaticHarnessName = [
142
+ "claude",
143
+ "claude code",
144
+ "codex",
145
+ "node",
146
+ "bun",
147
+ input.currentCommand?.toLowerCase(),
148
+ ].includes(configuredName?.toLowerCase() ?? "");
134
149
  const legacyConfiguredName = configuredName &&
135
150
  configuredName !== generatedName &&
151
+ !automaticHarnessName &&
136
152
  !GENERIC_TITLES.has(configuredName.toLowerCase())
137
153
  ? configuredName
138
154
  : null;
139
155
  if (configuredName && configuredSource === "manual")
140
156
  return { name: configuredName, source: "manual" };
141
- if (configuredName && (configuredSource === "agent" || input.paneType === "agent"))
157
+ const teamName = boundedName(input.teamMemberName);
158
+ if (teamName)
159
+ return { name: teamName, source: "agent" };
160
+ const reportedName = boundedName(input.agentDisplayName);
161
+ if (reportedName &&
162
+ !["claude", "claude code", "codex", input.currentCommand?.toLowerCase()].includes(reportedName.toLowerCase()))
163
+ return { name: reportedName, source: "agent" };
164
+ if (configuredName &&
165
+ !automaticHarnessName &&
166
+ (configuredSource === "agent" || input.paneType === "agent"))
142
167
  return { name: configuredName, source: "agent" };
143
168
  if (legacyConfiguredName && configuredSource !== "generated")
144
169
  return { name: legacyConfiguredName, source: "manual" };
145
170
  const command = commandBasename(input.currentCommand);
146
- if (command && !GENERIC_SHELLS.has(command.toLowerCase()))
147
- return { name: command, source: "process" };
148
- const title = meaningfulTitle(input.title, command, input.hostName);
171
+ const title = meaningfulPaneTitle(input.title, command, input.hostName);
149
172
  if (title)
150
173
  return { name: title, source: "title" };
174
+ if (reportedName)
175
+ return { name: reportedName, source: "agent" };
176
+ if (configuredName && configuredSource === "agent")
177
+ return { name: configuredName, source: "agent" };
178
+ if (command && !GENERIC_SHELLS.has(command.toLowerCase()))
179
+ return { name: command, source: "process" };
151
180
  return {
152
181
  name: configuredName && configuredSource === "generated" ? configuredName : generatedName,
153
182
  source: "generated",
@@ -26,6 +26,7 @@ export const SESSION_PANE_DESCRIPTOR_FORMAT = [
26
26
  "#{window_panes}",
27
27
  "#{session_windows}",
28
28
  "#{pane_birth_id}",
29
+ "#{qa:@tmux_ide_name_source}",
29
30
  ].join("\t");
30
31
  /** Decode the escapes emitted by tmux's `qa` format modifier. */
31
32
  export function decodeTmuxArgument(value) {
@@ -99,9 +100,9 @@ export function parseSessionPaneDescriptorReply(lines) {
99
100
  continue;
100
101
  }
101
102
  const encoded = utf8Line.split("\t");
102
- if (encoded.length !== 21)
103
+ if (encoded.length !== 21 && encoded.length !== 22)
103
104
  continue;
104
- const [runtimePaneId = "", semanticPaneId = "", role = "", type = "", currentCommand = "", cwd = "", windowIndexRaw = "", windowName = "", windowId = "", title = "", runtimeSessionId = "", paneIndexRaw = "", name = "", missionStamp = "", paneActiveRaw = "", windowActiveRaw = "", semanticWindowId = "", sessionName = "", windowPaneCountRaw = "", sessionWindowCountRaw = "", nativePaneBirthId = "",] = encoded.map(decodeTmuxArgument);
105
+ const [runtimePaneId = "", semanticPaneId = "", role = "", type = "", currentCommand = "", cwd = "", windowIndexRaw = "", windowName = "", windowId = "", title = "", runtimeSessionId = "", paneIndexRaw = "", name = "", missionStamp = "", paneActiveRaw = "", windowActiveRaw = "", semanticWindowId = "", sessionName = "", windowPaneCountRaw = "", sessionWindowCountRaw = "", nativePaneBirthId = "", nameSource = "",] = encoded.map(decodeTmuxArgument);
105
106
  if (!/^%(?:0|[1-9][0-9]*)$/u.test(runtimePaneId) || runtimePaneId.length > 32)
106
107
  continue;
107
108
  if (!/^\$(?:0|[1-9][0-9]*)$/u.test(runtimeSessionId) || runtimeSessionId.length > 32)
@@ -154,9 +155,7 @@ export function parseSessionPaneDescriptorReply(lines) {
154
155
  semanticWindowId: nonempty(semanticWindowId),
155
156
  paneIndex,
156
157
  name: nonempty(name),
157
- // Source metadata is persisted for mutations, while this hot inventory
158
- // stays wire-compatible. Generated names are recognized deterministically.
159
- nameSource: null,
158
+ nameSource: ["manual", "agent", "generated"].includes(nameSource) ? nameSource : null,
160
159
  missionStamp: nonempty(missionStamp),
161
160
  nativePaneBirthId: nonempty(nativePaneBirthId),
162
161
  paneActive: paneActiveRaw === "1",
@@ -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()))
@@ -1,3 +1,4 @@
1
+ import { createApplicationNewAgentOwner } from "./application-new-agent-owner.js";
1
2
  import { createEffect, createSignal, onCleanup, untrack } from "solid-js";
2
3
  import { createApplicationHomeAgentObserver, } from "./application-home-agent-observer.js";
3
4
  import { createHomeAgentSelectionOwner } from "./application-home-agent-selection.js";
@@ -156,12 +157,23 @@ export function createApplicationHomeNavigationOwner(options) {
156
157
  sessionOwner: () => options.sessionOwner(),
157
158
  selectPane: options.interaction.selectPane,
158
159
  });
160
+ const newAgent = createApplicationNewAgentOwner({
161
+ targetKey: () => JSON.stringify([
162
+ applicationGenerationNavigationKey(options.sessionOwner()?.snapshot() ?? null),
163
+ options.sessionOwner()?.snapshot()?.connection?.workspaceName,
164
+ options.sessionOwner()?.sessionName(),
165
+ ]),
166
+ workspace: () => options.sessionOwner()?.snapshot()?.connection?.workspaceName ?? null,
167
+ create: (draft) => options.interaction.newAgent(draft.name, draft.harness),
168
+ setNote: options.setNote,
169
+ });
159
170
  const paneRename = createApplicationPaneRenameOwner(options.interaction.renamePane, options.setNote);
160
171
  const homeAgents = createApplicationHomeAgentsOwner({
161
172
  catalog: options.catalog.snapshot,
162
173
  active: () => !options.fleetHome && options.activeSurface() === "home",
163
174
  inputActive: () => options.activeSurface() === "home" &&
164
175
  !paneRename.draft() &&
176
+ !newAgent.draft() &&
165
177
  !options.appearanceOpen?.() &&
166
178
  !options.shell().semantic?.focus.palette.open &&
167
179
  !options.shell().localPaletteOpen &&
@@ -222,13 +234,16 @@ export function createApplicationHomeNavigationOwner(options) {
222
234
  return null;
223
235
  if (options.sessionOwner()?.snapshot()?.status !== "live")
224
236
  return "Open a live session first";
225
- return command !== "new-window" && !options.focusedPane() ? "Select a live pane first" : null;
237
+ return command !== "new-window" && command !== "new-agent" && !options.focusedPane()
238
+ ? "Select a live pane first"
239
+ : null;
226
240
  },
227
241
  activeSurface: options.activeSurface,
228
242
  binding: options.binding,
229
243
  commandSource: applicationPaletteCommandSource,
230
244
  setSurface: options.setSurface,
231
245
  setNote: options.setNote,
246
+ openNewAgent: newAgent.begin,
232
247
  openAppearance: options.openAppearance,
233
248
  toggleSidebar: options.toggleSidebar,
234
249
  openSessions: options.openSessions,
@@ -245,6 +260,7 @@ export function createApplicationHomeNavigationOwner(options) {
245
260
  return {
246
261
  homeAgents: options.fleetHome ? { ...homeAgents, presentation: options.fleetHome } : homeAgents,
247
262
  paneRename,
263
+ newAgent,
248
264
  paletteCommands,
249
265
  paletteCommandList,
250
266
  openAgent,
@@ -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,
@@ -41,6 +41,7 @@ export function createApplicationHomeExperience(options) {
41
41
  paletteCommands: navigation.paletteCommands,
42
42
  blocked: () => options.paletteModalOpen() ||
43
43
  !!navigation.paneRename.draft() ||
44
+ !!navigation.newAgent.draft() ||
44
45
  paletteOpen() ||
45
46
  appearance.pickerOpen() ||
46
47
  machines.switching() ||
@@ -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,86 @@
1
+ import { createSignal } from "solid-js";
2
+ import { applicationPaneRenameKeyAction, applicationPaneRenamePaste, } from "./application-pane-rename-input.js";
3
+ /** Owns one reviewed workspace target; reconnects never retarget a launch. */
4
+ export function createApplicationNewAgentOwner(options) {
5
+ const [draft, setDraft] = createSignal(null);
6
+ const [busy, setBusy] = createSignal(false);
7
+ const [error, setError] = createSignal(null);
8
+ let target = "";
9
+ const cancel = () => {
10
+ if (!busy())
11
+ setDraft(null);
12
+ };
13
+ const submit = () => {
14
+ const current = draft();
15
+ if (!current || !current.name.trim() || busy() || error())
16
+ return;
17
+ if (target !== options.targetKey()) {
18
+ setError("Workspace changed. Close this dialog and open New agent again.");
19
+ return;
20
+ }
21
+ setBusy(true);
22
+ const expectedTarget = target;
23
+ void Promise.resolve()
24
+ .then(() => {
25
+ if (options.targetKey() !== expectedTarget)
26
+ throw new Error("Workspace changed");
27
+ return options.create({ ...current, name: current.name.trim() });
28
+ })
29
+ .then((message) => {
30
+ options.setNote(message);
31
+ setDraft(null);
32
+ })
33
+ .catch(() => {
34
+ setError("Creation was not confirmed. Check the workspace before trying again.");
35
+ })
36
+ .finally(() => setBusy(false));
37
+ };
38
+ const harness = (value) => {
39
+ const current = draft();
40
+ if (current && !busy())
41
+ setDraft({ ...current, harness: value });
42
+ };
43
+ return {
44
+ draft,
45
+ busy,
46
+ error,
47
+ cancel,
48
+ submit,
49
+ harness,
50
+ begin() {
51
+ const workspace = options.workspace();
52
+ if (!workspace || busy())
53
+ return;
54
+ target = options.targetKey();
55
+ setError(null);
56
+ setDraft({ name: "", harness: "claude", workspace });
57
+ },
58
+ handleKey(event) {
59
+ const current = draft();
60
+ if (!current)
61
+ return false;
62
+ if (busy())
63
+ return true;
64
+ if (event.name.toLowerCase() === "tab" && event.eventType !== "release") {
65
+ harness(current.harness === "claude" ? "codex" : "claude");
66
+ return true;
67
+ }
68
+ const action = applicationPaneRenameKeyAction(event, current.name);
69
+ if (action.kind === "cancel")
70
+ cancel();
71
+ else if (action.kind === "submit")
72
+ submit();
73
+ else if (action.kind === "update")
74
+ setDraft({ ...current, name: action.value });
75
+ return true;
76
+ },
77
+ handlePaste(bytes) {
78
+ const current = draft();
79
+ if (!current)
80
+ return false;
81
+ if (!busy())
82
+ setDraft({ ...current, name: applicationPaneRenamePaste(current.name, bytes) });
83
+ return true;
84
+ },
85
+ };
86
+ }
@@ -57,6 +57,11 @@ export function createApplicationPaletteCommandOwner(options) {
57
57
  options.setNote(unavailable);
58
58
  return;
59
59
  }
60
+ if (command === "new-agent") {
61
+ setOpen(false, source);
62
+ options.openNewAgent?.();
63
+ return;
64
+ }
60
65
  if (typeof command === "object" && command.fleet) {
61
66
  if (command.fleet.disabled || !options.openFleet) {
62
67
  options.setNote("That machine or session is unavailable.");
@@ -5,6 +5,7 @@ const BASE_COMMANDS = [
5
5
  "home",
6
6
  "terminals",
7
7
  "new-window",
8
+ "new-agent",
8
9
  "split-right",
9
10
  "split-down",
10
11
  "close-pane",
@@ -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
+ }
@@ -1125,6 +1125,30 @@ export function createApplicationTerminalInteractionController(options) {
1125
1125
  return `split unavailable: ${error instanceof Error ? error.message : String(error)}`;
1126
1126
  }
1127
1127
  },
1128
+ async newAgent(name, harness) {
1129
+ const active = options.generation();
1130
+ const workspaceName = active?.connection?.workspaceName;
1131
+ if (active?.status !== "live" || !active.client || !workspaceName)
1132
+ throw new Error("Open a live workspace first");
1133
+ const response = await active.client.dispatch({
1134
+ kind: "owner-action",
1135
+ name: "workspace.pane.create",
1136
+ input: {
1137
+ workspaceName,
1138
+ kind: "agent",
1139
+ displayTitle: name,
1140
+ harnessProfileId: harness,
1141
+ role: "implementer",
1142
+ placement: { kind: "window" },
1143
+ },
1144
+ operationId: createOperationId(),
1145
+ });
1146
+ if (options.generation() !== active ||
1147
+ response.kind !== "owner-action" ||
1148
+ response.result === null)
1149
+ throw new Error("Agent creation was not confirmed");
1150
+ return `Created ${name}`;
1151
+ },
1128
1152
  async newWindow() {
1129
1153
  const active = options.generation();
1130
1154
  const workspaceName = active?.connection?.workspaceName ?? null;
@@ -35,11 +35,13 @@ export function terminalPaneChromeLabel(paneId, focused, width, indicator, displ
35
35
  }
36
36
  export function terminalPaneDisplayTitle(paneId, indicator, displayName, displayNameSource) {
37
37
  const presentedName = displayName?.trim() || paneId;
38
- return indicator
39
- ? displayNameSource === "manual" && presentedName !== indicator.name.trim()
40
- ? `${presentedName} · ${indicator.name.trim()}`
41
- : indicator.name.trim() || presentedName
42
- : presentedName;
38
+ if (indicator?.nameResolved && indicator.name.trim())
39
+ return indicator.name.trim();
40
+ if (displayNameSource === "manual" ||
41
+ displayNameSource === "agent" ||
42
+ displayNameSource === "title")
43
+ return presentedName;
44
+ return indicator?.name.trim() || presentedName;
43
45
  }
44
46
  export function terminalWindowTitle(window) {
45
47
  return window.windowName ?? window.semanticWindowId ?? "window";
@@ -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>);
@@ -8,6 +8,12 @@ export const PALETTE_REFERENCE_COMMANDS = {
8
8
  };
9
9
  /** Presentation only: execution remains in the existing application owners. */
10
10
  export function applicationCommandDescription(command) {
11
+ if (command === "new-agent")
12
+ return {
13
+ id: command,
14
+ label: "New agent…",
15
+ detail: "Named Claude or Codex agent in this workspace",
16
+ };
11
17
  if (command === "help")
12
18
  return { id: command, label: "Using tmux-ide", detail: "Help and getting started" };
13
19
  if (command === "hide-sidebar" || command === "show-sidebar")
@@ -1,3 +1,6 @@
1
+ import { readManualPaneTeam } from "../terminal/attachments/manual-pane-team.ts";
2
+ import type { PaneTeamMembership } from "@tmux-ide/contracts";
3
+ import { decodeTmuxArgument } from "../terminal/protocol/session-descriptor-discovery.ts";
1
4
  import type { NativePaneIdentity } from "@tmux-ide/contracts";
2
5
  import { nativePaneIdentity } from "../lib/native-pane-identity.ts";
3
6
  import type { InteractionPaneEndpoint } from "@tmux-ide/contracts";
@@ -223,6 +226,10 @@ export function readAdoptedSessionNames(runTmux: TmuxRunner = _tmuxRunner): stri
223
226
 
224
227
  /** One live pane, with the raw agent-authority options gathered for the fleet. */
225
228
  export interface FleetPaneFacts {
229
+ readonly team?: PaneTeamMembership;
230
+ readonly title?: string;
231
+ readonly name?: string | null;
232
+ readonly nameSource?: string | null;
226
233
  readonly nativeIdentity?: NativePaneIdentity | null;
227
234
  readonly nativePaneBirthId?: string | null;
228
235
  readonly interactionEndpoint?: Extract<InteractionPaneEndpoint, { kind: "pane" }> | null;
@@ -237,6 +244,7 @@ export interface FleetPaneFacts {
237
244
  readonly agentStateRaw: string | null;
238
245
  readonly agentStatusTextRaw: string | null;
239
246
  readonly agentDisplayNameRaw: string | null;
247
+ readonly teamMemberName?: string;
240
248
  /** Durable `@agent_hint` agent identity, or null when the pane has none. */
241
249
  readonly agentHintRaw: string | null;
242
250
  }
@@ -271,6 +279,10 @@ const FLEET_PANE_FORMAT = [
271
279
  "#{@agent_display_name}",
272
280
  "#{@agent_hint}",
273
281
  "#{pane_birth_id}",
282
+ "#{qa:pane_title}",
283
+ "#{qa:@ide_name}",
284
+ "#{qa:@tmux_ide_name_source}",
285
+ "#{@tmux_ide_team}",
274
286
  FLEET_LINE_SENTINEL,
275
287
  ].join(FLEET_FIELD_SEPARATOR);
276
288
 
@@ -316,8 +328,8 @@ export function readAdoptedFleet(
316
328
  if (!line) continue;
317
329
  const fields = line.split(FLEET_FIELD_SEPARATOR);
318
330
  // session, pane, semantic pane, incarnation pid, active, command, path, state,
319
- // statusText, displayName, hint, sentinel
320
- if (fields.length !== 13 || fields[12] !== FLEET_LINE_SENTINEL) continue;
331
+ // statusText, displayName, hint, birth ID, quoted title/name/source, sentinel
332
+ if (![16, 17].includes(fields.length) || fields.at(-1) !== FLEET_LINE_SENTINEL) continue;
321
333
  const sessionName = fields[0]!;
322
334
  if (!adoptedSet.has(sessionName)) continue;
323
335
  const runtimePaneId = fields[1]!;
@@ -330,6 +342,10 @@ export function readAdoptedFleet(
330
342
  panesBySession.set(sessionName, panes);
331
343
  }
332
344
  panes.push({
345
+ team: fields.length === 17 ? readManualPaneTeam(fields[15], incarnation) : undefined,
346
+ nameSource: emptyToNull(decodeTmuxArgument(fields[14]!)),
347
+ title: decodeTmuxArgument(fields[12]!),
348
+ name: emptyToNull(decodeTmuxArgument(fields[13]!)),
333
349
  runtimePaneId,
334
350
  semanticPaneId: emptyToNull(fields[2]!),
335
351
  incarnation,
@@ -18,6 +18,7 @@ import {
18
18
  isAgentPane,
19
19
  paneIdentities,
20
20
  resolveAgentPresentation,
21
+ resolvedAgentLabel,
21
22
  type ApplicationShellSessionFacts,
22
23
  } from "./application-shell.ts";
23
24
 
@@ -143,7 +144,7 @@ export function projectApplicationShellAgentGraphOverlay(
143
144
  status: nodeStatus(presentation.detectStatus),
144
145
  statusSource: presentation.statusSource,
145
146
  attention: presentation.attention,
146
- label: nodeLabel(presentation.displayName ?? pane.name ?? pane.title),
147
+ label: nodeLabel(resolvedAgentLabel(pane, presentation, index)),
147
148
  });
148
149
  nodeWindowIds.add(windowId);
149
150
  if (pane.role === "lead") leadWindowIds.push(windowId);