tmux-ide 2.9.0-beta.46 → 2.9.0-beta.47

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 CHANGED
@@ -9845,6 +9845,8 @@ var init_automation_operations = __esm({
9845
9845
  ))();
9846
9846
  AutomationPanesResponseSchemaZ = /* @__PURE__ */ (() => z73.object({
9847
9847
  version: z73.literal(1),
9848
+ // Resolved by the daemon from the invoking pane credential, never a title.
9849
+ source: AutomationPaneEndpointSchemaZ.nullable().optional(),
9848
9850
  panes: z73.array(
9849
9851
  z73.object({
9850
9852
  endpoint: AutomationPaneEndpointSchemaZ,
@@ -11906,7 +11908,7 @@ var require_package = __commonJS({
11906
11908
  "package.json"(exports, module) {
11907
11909
  module.exports = {
11908
11910
  name: "tmux-ide",
11909
- version: "2.9.0-beta.46",
11911
+ version: "2.9.0-beta.47",
11910
11912
  description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
11911
11913
  type: "module",
11912
11914
  bin: {
@@ -76442,6 +76444,8 @@ function mountAutomationRoutes(app, options) {
76442
76444
  `${base2}/panes`,
76443
76445
  route(async (c) => {
76444
76446
  const panes = [];
76447
+ const credential = c.req.header(PANE_SOURCE_CREDENTIAL_HEADER);
76448
+ let source = null;
76445
76449
  for (const scope of await options.owners.refresh()) {
76446
76450
  if (!scope.generation) continue;
76447
76451
  await options.owners.withOwner(
@@ -76456,6 +76460,18 @@ function mountAutomationRoutes(app, options) {
76456
76460
  pane.semanticPaneId
76457
76461
  );
76458
76462
  if (!endpoint) continue;
76463
+ if (credential) {
76464
+ const binding = owner.resolveInteractionSource(
76465
+ credential,
76466
+ endpoint.workspaceName,
76467
+ endpoint.semanticPaneId
76468
+ );
76469
+ if (binding && same4(binding.endpoint, endpoint)) {
76470
+ if (source && !same4(source, endpoint))
76471
+ throw new AutomationRequestError("invalid-source");
76472
+ source = endpoint;
76473
+ }
76474
+ }
76459
76475
  if (panes.length >= 4096) throw new AutomationRequestError("capacity");
76460
76476
  panes.push({
76461
76477
  endpoint,
@@ -76466,7 +76482,8 @@ function mountAutomationRoutes(app, options) {
76466
76482
  }
76467
76483
  );
76468
76484
  }
76469
- return c.json(AutomationPanesResponseSchemaZ.parse({ version: 1, panes }));
76485
+ if (credential && !source) throw new AutomationRequestError("invalid-source");
76486
+ return c.json(AutomationPanesResponseSchemaZ.parse({ version: 1, panes, source }));
76470
76487
  })
76471
76488
  );
76472
76489
  app.post(
@@ -91498,11 +91515,14 @@ var init_automation_client = __esm({
91498
91515
  // packages/daemon/src/automation.ts
91499
91516
  var automation_exports = {};
91500
91517
  __export(automation_exports, {
91518
+ AutomationInvocationIntentSchemaZ: () => AutomationInvocationIntentSchemaZ,
91501
91519
  invokingPaneCredential: () => invokingPaneCredential,
91502
91520
  localAutomationClient: () => localAutomationClient,
91503
91521
  readAutomationRequest: () => readAutomationRequest,
91522
+ resolveAutomationIntent: () => resolveAutomationIntent,
91504
91523
  runAutomationCli: () => runAutomationCli
91505
91524
  });
91525
+ import { z as z109 } from "zod";
91506
91526
  import { execFileSync as execFileSync22 } from "node:child_process";
91507
91527
  import { parseArgs } from "node:util";
91508
91528
  function invokingPaneCredential(env = process.env) {
@@ -91565,6 +91585,13 @@ async function readAutomationRequest(input) {
91565
91585
  throw new IdeError("Expected a JSON automation request on stdin", { code: "INVALID_REQUEST" });
91566
91586
  }
91567
91587
  }
91588
+ async function resolveAutomationIntent(input, client, options = {}) {
91589
+ const intent = AutomationInvocationIntentSchemaZ.parse(input);
91590
+ return AutomationOperationIntentSchemaZ.parse({
91591
+ ...intent,
91592
+ source: intent.source === void 0 ? (await client.discover(options)).source ?? null : intent.source
91593
+ });
91594
+ }
91568
91595
  async function runAutomationCli(args, dependencies = {}) {
91569
91596
  const { positionals: positionals2, values: values2 } = parseArgs({
91570
91597
  args: [...args],
@@ -91630,20 +91657,20 @@ async function runAutomationCli(args, dependencies = {}) {
91630
91657
  }
91631
91658
  return;
91632
91659
  }
91633
- const intent = AutomationOperationIntentSchemaZ.parse(input);
91660
+ const intent = await resolveAutomationIntent(input, client);
91634
91661
  if (command3 !== "reserve" && intent.kind !== command3)
91635
91662
  throw new IdeError("Automation command does not match intent kind", {
91636
91663
  code: "INVALID_REQUEST"
91637
91664
  });
91638
91665
  const reservation = await client.reserve(intent);
91639
- if (command3 === "reserve") output(reservation);
91666
+ if (command3 === "reserve") output({ ...reservation, intent });
91640
91667
  else output(await client.execute(reservation.handle, intent));
91641
91668
  } catch (error) {
91642
91669
  if (error instanceof AutomationInvocationError) throw new AutomationCliError(error);
91643
91670
  throw error;
91644
91671
  }
91645
91672
  }
91646
- var AutomationCliError;
91673
+ var AutomationCliError, AutomationInvocationIntentSchemaZ;
91647
91674
  var init_automation2 = __esm({
91648
91675
  "packages/daemon/src/automation.ts"() {
91649
91676
  "use strict";
@@ -91662,6 +91689,14 @@ var init_automation2 = __esm({
91662
91689
  return { ...super.toJSON(), handle: this.handle };
91663
91690
  }
91664
91691
  };
91692
+ AutomationInvocationIntentSchemaZ = z109.discriminatedUnion("kind", [
91693
+ AutomationOperationIntentSchemaZ.options[0].extend({
91694
+ source: AutomationOperationIntentSchemaZ.options[0].shape.source.optional()
91695
+ }),
91696
+ AutomationOperationIntentSchemaZ.options[1].extend({
91697
+ source: AutomationOperationIntentSchemaZ.options[1].shape.source.optional()
91698
+ })
91699
+ ]);
91665
91700
  }
91666
91701
  });
91667
91702
 
@@ -91673,7 +91708,7 @@ __export(mcp_exports, {
91673
91708
  });
91674
91709
  import { McpServer } from "@modelcontextprotocol/server";
91675
91710
  import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
91676
- import { z as z109 } from "zod";
91711
+ import { z as z110 } from "zod";
91677
91712
  async function call(work) {
91678
91713
  try {
91679
91714
  return result(await work());
@@ -91696,7 +91731,7 @@ function createTmuxIdeMcpServer(client) {
91696
91731
  "tmux_panes",
91697
91732
  {
91698
91733
  description: "Discover current panes across this daemon's tmux servers. Use returned endpoint objects unchanged; names and native pane numbers are not unique identities.",
91699
- inputSchema: z109.object({}).strict(),
91734
+ inputSchema: z110.object({}).strict(),
91700
91735
  annotations: { readOnlyHint: true, idempotentHint: true }
91701
91736
  },
91702
91737
  async (_args, context) => call(() => client.discover({ signal: context.mcpReq.signal }))
@@ -91704,17 +91739,25 @@ function createTmuxIdeMcpServer(client) {
91704
91739
  server.registerTool(
91705
91740
  "tmux_prepare",
91706
91741
  {
91707
- description: "Prepare a read or send and receive a generation-fenced operation handle. This does not send input. Save the handle before executing. A source claim requires this MCP process to hold that pane's valid credential; otherwise use source:null.",
91708
- inputSchema: z109.object({ intent: AutomationOperationIntentSchemaZ }).strict(),
91742
+ description: "Prepare a read or send and receive a generation-fenced operation handle. This does not send input. Save the handle before executing. Omit source to resolve this MCP process\u2019s verified pane automatically, or use source:null for an unbound caller. Save the returned intent unchanged with the handle.",
91743
+ inputSchema: z110.object({ intent: AutomationInvocationIntentSchemaZ }).strict(),
91709
91744
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
91710
91745
  },
91711
- async ({ intent }, context) => call(() => client.reserve(intent, { signal: context.mcpReq.signal }))
91746
+ async ({ intent }, context) => call(async () => {
91747
+ const resolved2 = await resolveAutomationIntent(intent, client, {
91748
+ signal: context.mcpReq.signal
91749
+ });
91750
+ return {
91751
+ ...await client.reserve(resolved2, { signal: context.mcpReq.signal }),
91752
+ intent: resolved2
91753
+ };
91754
+ })
91712
91755
  );
91713
91756
  server.registerTool(
91714
91757
  "tmux_execute",
91715
91758
  {
91716
91759
  description: "Execute a prepared read or send using its exact handle and unchanged intent. Sends may run terminal commands. On uncertainty, check status or retry this same handle; never prepare another operation. Read text is returned once; replay returns metadata only. A completed send is command completion, not proof the recipient consumed it.",
91717
- inputSchema: z109.object({
91760
+ inputSchema: z110.object({
91718
91761
  handle: AutomationOperationHandleSchemaZ,
91719
91762
  intent: AutomationOperationIntentSchemaZ
91720
91763
  }).strict(),
@@ -91726,7 +91769,7 @@ function createTmuxIdeMcpServer(client) {
91726
91769
  "tmux_operation_status",
91727
91770
  {
91728
91771
  description: "Look up a prepared operation without repeating its effect. Status contains no sent or captured text. Outcome-unknown includes expired retention and must not be interpreted as not executed.",
91729
- inputSchema: z109.object({ handle: AutomationOperationHandleSchemaZ }).strict(),
91772
+ inputSchema: z110.object({ handle: AutomationOperationHandleSchemaZ }).strict(),
91730
91773
  annotations: { readOnlyHint: true, idempotentHint: true }
91731
91774
  },
91732
91775
  async ({ handle }, context) => call(() => client.status(handle, { signal: context.mcpReq.signal }))
@@ -91735,9 +91778,9 @@ function createTmuxIdeMcpServer(client) {
91735
91778
  "tmux_interactions",
91736
91779
  {
91737
91780
  description: "Read at most one batch of scoped interaction metadata, optionally waiting for new events. Carry the returned cursor into the next call. Gaps explicitly mean missing history; these events contain no terminal contents.",
91738
- inputSchema: z109.object({
91781
+ inputSchema: z110.object({
91739
91782
  resume: TmuxInteractionCursorSchemaZ,
91740
- waitMs: z109.number().int().min(1).max(3e4).default(1e3)
91783
+ waitMs: z110.number().int().min(1).max(3e4).default(1e3)
91741
91784
  }).strict(),
91742
91785
  annotations: { readOnlyHint: true, idempotentHint: true }
91743
91786
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.46",
3
+ "version": "2.9.0-beta.47",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -93,6 +93,8 @@ const displayLabel = z
93
93
  export const AutomationPanesResponseSchemaZ = z
94
94
  .object({
95
95
  version: z.literal(1),
96
+ // Resolved by the daemon from the invoking pane credential, never a title.
97
+ source: AutomationPaneEndpointSchemaZ.nullable().optional(),
96
98
  panes: z
97
99
  .array(
98
100
  z
@@ -379,6 +379,7 @@ export function paneInteractionRelationshipLabel(
379
379
  export function reduceInteractionReceipt(
380
380
  previous: InteractionFeedState,
381
381
  raw: InteractionJournalEntry,
382
+ project: (entry: InteractionJournalEntry) => boolean = () => true,
382
383
  ): InteractionFeedState {
383
384
  const receipt = InteractionJournalEntrySchemaZ.parse(raw);
384
385
  const ownerKey = receiptOwnerKey(receipt);
@@ -427,6 +428,7 @@ export function reduceInteractionReceipt(
427
428
  const destination = evidence?.endpoints.destination;
428
429
  const operationKind = interactionActivityOperationKind(receipt);
429
430
  if (
431
+ !project(receipt) ||
430
432
  interactionIsViewerActivity(receipt) ||
431
433
  (receipt.type === "interaction.receipt" && receipt.target.kind !== "pane") ||
432
434
  (operationKind !== "workspace.pane.send" && operationKind !== "workspace.pane.read") ||
@@ -468,6 +470,7 @@ export function interactionForPane(
468
470
  state: InteractionFeedState,
469
471
  endpoint: ResolvedInteractionEndpoint,
470
472
  nativeIdentity?: NativePaneIdentity | null,
473
+ project: (entry: InteractionJournalEntry) => boolean = () => true,
471
474
  ): PaneInteractionProjection | null {
472
475
  const semantic = state.panes[interactionPaneEndpointKey(endpoint)] ?? null;
473
476
  if (!nativeIdentity) return semantic;
@@ -481,7 +484,12 @@ export function interactionForPane(
481
484
  // At most INTERACTION_ACTIVITY_LIMIT entries. Current aliases are never written back
482
485
  // into retained history; a linked physical pane can appear in several sessions.
483
486
  for (const entry of state.activity) {
484
- if (entry.type !== "interaction.evidence" || interactionIsViewerActivity(entry)) continue;
487
+ if (
488
+ entry.type !== "interaction.evidence" ||
489
+ interactionIsViewerActivity(entry) ||
490
+ !project(entry)
491
+ )
492
+ continue;
485
493
  const evidence = entry.evidence;
486
494
  const destination = evidence.endpoints.destination;
487
495
  if (destination.kind !== "native-pane" || interactionNativePaneEndpointKey(destination) !== key)
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import { parseArgs } from "node:util";
3
4
  import { AutomationExecuteRequestSchemaZ, AutomationOperationHandleSchemaZ, AutomationOperationIntentSchemaZ, DAEMON_WIRE_PROTOCOL_VERSION, TmuxInteractionCursorSchemaZ, } from "@tmux-ide/contracts";
@@ -94,6 +95,24 @@ export async function readAutomationRequest(input) {
94
95
  throw new IdeError("Expected a JSON automation request on stdin", { code: "INVALID_REQUEST" });
95
96
  }
96
97
  }
98
+ /** Omission requests discovery; explicit null deliberately keeps the caller unbound. */
99
+ export const AutomationInvocationIntentSchemaZ = z.discriminatedUnion("kind", [
100
+ AutomationOperationIntentSchemaZ.options[0].extend({
101
+ source: AutomationOperationIntentSchemaZ.options[0].shape.source.optional(),
102
+ }),
103
+ AutomationOperationIntentSchemaZ.options[1].extend({
104
+ source: AutomationOperationIntentSchemaZ.options[1].shape.source.optional(),
105
+ }),
106
+ ]);
107
+ export async function resolveAutomationIntent(input, client, options = {}) {
108
+ const intent = AutomationInvocationIntentSchemaZ.parse(input);
109
+ return AutomationOperationIntentSchemaZ.parse({
110
+ ...intent,
111
+ source: intent.source === undefined
112
+ ? ((await client.discover(options)).source ?? null)
113
+ : intent.source,
114
+ });
115
+ }
97
116
  /** The CLI is a thin adapter; it never falls back to raw tmux after an uncertain effect. */
98
117
  export async function runAutomationCli(args, dependencies = {}) {
99
118
  const { positionals, values } = parseArgs({
@@ -156,14 +175,14 @@ export async function runAutomationCli(args, dependencies = {}) {
156
175
  }
157
176
  return;
158
177
  }
159
- const intent = AutomationOperationIntentSchemaZ.parse(input);
178
+ const intent = await resolveAutomationIntent(input, client);
160
179
  if (command !== "reserve" && intent.kind !== command)
161
180
  throw new IdeError("Automation command does not match intent kind", {
162
181
  code: "INVALID_REQUEST",
163
182
  });
164
183
  const reservation = await client.reserve(intent);
165
184
  if (command === "reserve")
166
- output(reservation);
185
+ output({ ...reservation, intent });
167
186
  else
168
187
  output(await client.execute(reservation.handle, intent));
169
188
  }
@@ -124,6 +124,8 @@ export function mountAutomationRoutes(app, options) {
124
124
  };
125
125
  app.get(`${base}/panes`, route(async (c) => {
126
126
  const panes = [];
127
+ const credential = c.req.header(PANE_SOURCE_CREDENTIAL_HEADER);
128
+ let source = null;
127
129
  for (const scope of await options.owners.refresh()) {
128
130
  if (!scope.generation)
129
131
  continue;
@@ -136,6 +138,14 @@ export function mountAutomationRoutes(app, options) {
136
138
  const endpoint = owner.interactionEvidence?.captureAuthoredEndpoint(pane.workspaceName, pane.semanticPaneId);
137
139
  if (!endpoint)
138
140
  continue;
141
+ if (credential) {
142
+ const binding = owner.resolveInteractionSource(credential, endpoint.workspaceName, endpoint.semanticPaneId);
143
+ if (binding && same(binding.endpoint, endpoint)) {
144
+ if (source && !same(source, endpoint))
145
+ throw new AutomationRequestError("invalid-source");
146
+ source = endpoint;
147
+ }
148
+ }
139
149
  if (panes.length >= 4096)
140
150
  throw new AutomationRequestError("capacity");
141
151
  panes.push({
@@ -146,7 +156,9 @@ export function mountAutomationRoutes(app, options) {
146
156
  }
147
157
  });
148
158
  }
149
- return c.json(AutomationPanesResponseSchemaZ.parse({ version: 1, panes }));
159
+ if (credential && !source)
160
+ throw new AutomationRequestError("invalid-source");
161
+ return c.json(AutomationPanesResponseSchemaZ.parse({ version: 1, panes, source }));
150
162
  }));
151
163
  app.post(`${base}/reserve`, route(async (c) => {
152
164
  const request = AutomationReserveRequestSchemaZ.parse(await boundedJson(c.req.raw));
@@ -3,7 +3,7 @@ import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/s
3
3
  import { z } from "zod";
4
4
  import { AutomationOperationHandleSchemaZ, AutomationOperationIntentSchemaZ, TmuxInteractionCursorSchemaZ, } from "@tmux-ide/contracts";
5
5
  import { AutomationInvocationError, } from "@tmux-ide/daemon-client/automation-client";
6
- import { localAutomationClient } from "./automation.js";
6
+ import { localAutomationClient, AutomationInvocationIntentSchemaZ, resolveAutomationIntent, } from "./automation.js";
7
7
  const result = (value) => ({
8
8
  content: [{ type: "text", text: JSON.stringify(value) }],
9
9
  });
@@ -33,10 +33,18 @@ export function createTmuxIdeMcpServer(client) {
33
33
  annotations: { readOnlyHint: true, idempotentHint: true },
34
34
  }, async (_args, context) => call(() => client.discover({ signal: context.mcpReq.signal })));
35
35
  server.registerTool("tmux_prepare", {
36
- description: "Prepare a read or send and receive a generation-fenced operation handle. This does not send input. Save the handle before executing. A source claim requires this MCP process to hold that pane's valid credential; otherwise use source:null.",
37
- inputSchema: z.object({ intent: AutomationOperationIntentSchemaZ }).strict(),
36
+ description: "Prepare a read or send and receive a generation-fenced operation handle. This does not send input. Save the handle before executing. Omit source to resolve this MCP process’s verified pane automatically, or use source:null for an unbound caller. Save the returned intent unchanged with the handle.",
37
+ inputSchema: z.object({ intent: AutomationInvocationIntentSchemaZ }).strict(),
38
38
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
39
- }, async ({ intent }, context) => call(() => client.reserve(intent, { signal: context.mcpReq.signal })));
39
+ }, async ({ intent }, context) => call(async () => {
40
+ const resolved = await resolveAutomationIntent(intent, client, {
41
+ signal: context.mcpReq.signal,
42
+ });
43
+ return {
44
+ ...(await client.reserve(resolved, { signal: context.mcpReq.signal })),
45
+ intent: resolved,
46
+ };
47
+ }));
40
48
  server.registerTool("tmux_execute", {
41
49
  description: "Execute a prepared read or send using its exact handle and unchanged intent. Sends may run terminal commands. On uncertainty, check status or retry this same handle; never prepare another operation. Read text is returned once; replay returns metadata only. A completed send is command completion, not proof the recipient consumed it.",
42
50
  inputSchema: z
@@ -1,3 +1,4 @@
1
+ import { receiptIsHeaderWorthy } from "../ui/pane-interaction-presentation.js";
1
2
  import { applicationMachineAuthorityManager } from "./application-machine-authority.js";
2
3
  import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.js";
3
4
  import { createEffect, createSignal, onCleanup } from "solid-js";
@@ -73,7 +74,7 @@ export function createApplicationPaneActivityOwner(sources, subscribe = subscrib
73
74
  const snapshot = feed;
74
75
  setPanes(Object.assign(new Map(current), {
75
76
  forPane: (endpoint, nativeIdentity) => {
76
- const value = interactionForPane(snapshot, endpoint, nativeIdentity);
77
+ const value = interactionForPane(snapshot, endpoint, nativeIdentity, receiptIsHeaderWorthy);
77
78
  return value && interactionPresenceIsFresh(value, Date.now()) ? value : undefined;
78
79
  },
79
80
  }));
@@ -84,7 +85,7 @@ export function createApplicationPaneActivityOwner(sources, subscribe = subscrib
84
85
  const deadlines = [
85
86
  ...current.map(([, value]) => Date.parse(value.at) + INTERACTION_PRESENCE_MS),
86
87
  ...feed.activity
87
- .filter((entry) => entry.type === "interaction.evidence")
88
+ .filter((entry) => entry.type === "interaction.evidence" && receiptIsHeaderWorthy(entry))
88
89
  .map((entry) => Date.parse(interactionActivityAt(entry)) + INTERACTION_PRESENCE_MS),
89
90
  ].filter((deadline) => deadline >= now && deadline <= now + INTERACTION_PRESENCE_MS);
90
91
  if (deadlines.length)
@@ -132,7 +133,7 @@ export function createApplicationPaneActivityOwner(sources, subscribe = subscrib
132
133
  if (receipt.type === "interaction.evidence" ||
133
134
  receipt.operationKind === "workspace.pane.read" ||
134
135
  receipt.operationKind === "workspace.pane.send")
135
- feed = reduceInteractionReceipt(feed, receipt);
136
+ feed = reduceInteractionReceipt(feed, receipt, receiptIsHeaderWorthy);
136
137
  }
137
138
  entry.attempts = 0;
138
139
  publish();
@@ -258,6 +258,9 @@ export function ApplicationTerminalWorkspace(props) {
258
258
  const paneContextMenu = paneMenu.state;
259
259
  const [interactionDetails, setInteractionDetails] = createSignal(null);
260
260
  const paneName = (endpoint) => {
261
+ const registeredName = props.interactionPaneName?.(endpoint);
262
+ if (registeredName)
263
+ return registeredName;
261
264
  const current = props.interactionEndpoints?.().get(endpoint.semanticPaneId);
262
265
  return current && interactionPaneEndpointKey(current) === interactionPaneEndpointKey(endpoint)
263
266
  ? props.agentIndicators?.().get(endpoint.semanticPaneId)?.name
@@ -81,9 +81,13 @@ export function paneInteractionPresentation(event, name = () => undefined) {
81
81
  // caller consumed captured output. Keep that limit visible on every surface.
82
82
  if (event.effect.kind === "unknown" && event.phase === "observed") {
83
83
  return {
84
- label: read ? "Read command · reader unknown" : "Send command · sender unknown",
84
+ label: source
85
+ ? `${read ? "Read" : "Send"} command from ${source}`
86
+ : read
87
+ ? "Read command · reader unknown"
88
+ : "Send command · sender unknown",
85
89
  compactLabel: read ? "Read command" : "Send command",
86
- source: "Unknown",
90
+ source: source ?? "Unknown",
87
91
  target,
88
92
  pending,
89
93
  failed,
@@ -179,3 +183,27 @@ export function nameForCurrentEndpoint(rows, endpoint) {
179
183
  const matches = rows.filter((row) => row.interactionEndpoint && interactionPaneEndpointKey(row.interactionEndpoint) === key);
180
184
  return matches.length === 1 ? matches[0].name : undefined;
181
185
  }
186
+ /** Chrome is reserved for completed, attributed interactions between distinct panes.
187
+ * Unknown commands remain in Activity; hiding them is not sender attribution. */
188
+ export function paneInteractionIsHeaderWorthy(event) {
189
+ const source = event.sourceEndpoint;
190
+ const target = paneInteractionDisplayDestination(event);
191
+ return (event.phase === "observed" &&
192
+ event.effect.kind !== "no-input" &&
193
+ (event.effect.kind !== "unknown" || event.origin !== "external") &&
194
+ event.origin !== "tui" &&
195
+ source !== null &&
196
+ (!target || interactionPaneEndpointKey(source) !== interactionPaneEndpointKey(target)));
197
+ }
198
+ export function receiptIsHeaderWorthy(receipt) {
199
+ const source = receipt.evidence?.endpoints.source;
200
+ const target = receipt.evidence?.endpoints.destination;
201
+ return ((receipt.type === "interaction.evidence" ||
202
+ (receipt.phase === "observed" && receipt.origin !== "tui")) &&
203
+ (receipt.evidence?.effect.kind !== "unknown" ||
204
+ (receipt.type === "interaction.receipt" && receipt.origin !== "external")) &&
205
+ receipt.evidence?.effect.kind !== "no-input" &&
206
+ source?.kind === "pane" &&
207
+ (target?.kind !== "pane" ||
208
+ interactionPaneEndpointKey(source) !== interactionPaneEndpointKey(target)));
209
+ }
@@ -50,7 +50,7 @@ export function createPaneInteractionMarker(event, theme) {
50
50
  export function PaneInteraction(props) {
51
51
  const value = () => paneInteractionPresentation(props.event, props.paneName);
52
52
  const marker = createPaneInteractionMarker(() => props.event, () => props.theme);
53
- const detailsWidth = () => (props.onDetails && props.width >= 32 ? 9 : 0);
53
+ const detailsWidth = () => (props.onDetails && props.width >= 20 ? 9 : 0);
54
54
  return (<box width={props.width} height={1} flexShrink={0} flexDirection="row">
55
55
  <Badge theme={props.theme} width={Math.max(1, props.width - detailsWidth())} label={value().label} marker={marker()} tone={value().failed
56
56
  ? "warning"
@@ -3,7 +3,7 @@ import { PaneModeControl } from "../ui/pane-mode-control.jsx";
3
3
  import { paneInteractionPresence } from "@tmux-ide/core";
4
4
  import { statusPresentation } from "../ui/status-presentation.js";
5
5
  import { PaneInteraction } from "../ui/pane-interaction.jsx";
6
- import { paneInteractionPresentation } from "../ui/pane-interaction-presentation.js";
6
+ import { paneInteractionPresentation, paneInteractionIsHeaderWorthy, } from "../ui/pane-interaction-presentation.js";
7
7
  import { createMemo, createSignal, Show } from "solid-js";
8
8
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
9
9
  import { AgentBadge, IconButton, componentPalette } from "../ui/index.js";
@@ -29,7 +29,9 @@ export function PaneTitleBar(props) {
29
29
  const viewMode = () => statusPresentation({ scrollback: props.scrollback, expanded: props.zoomed });
30
30
  const status = () => presentation()?.tone;
31
31
  const statusLabel = () => (presentation()?.action ? undefined : presentation()?.label);
32
- const presence = () => (props.interaction ? paneInteractionPresence(props.interaction) : null);
32
+ const presence = () => props.interaction && paneInteractionIsHeaderWorthy(props.interaction)
33
+ ? paneInteractionPresence(props.interaction)
34
+ : null;
33
35
  const activityLabel = () => props.interaction ? paneInteractionPresentation(props.interaction, props.paneName).label : "";
34
36
  const activityWidth = () => {
35
37
  const available = safeWidth() -
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import { parseArgs } from "node:util";
3
4
  import {
@@ -12,6 +13,7 @@ import {
12
13
  createAutomationClient,
13
14
  AutomationInvocationError,
14
15
  type AutomationClient,
16
+ type AutomationRequestOptions,
15
17
  } from "@tmux-ide/daemon-client/automation-client";
16
18
  import {
17
19
  canonicalDaemonUrl,
@@ -130,6 +132,30 @@ export async function readAutomationRequest(
130
132
  }
131
133
  }
132
134
 
135
+ /** Omission requests discovery; explicit null deliberately keeps the caller unbound. */
136
+ export const AutomationInvocationIntentSchemaZ = z.discriminatedUnion("kind", [
137
+ AutomationOperationIntentSchemaZ.options[0].extend({
138
+ source: AutomationOperationIntentSchemaZ.options[0].shape.source.optional(),
139
+ }),
140
+ AutomationOperationIntentSchemaZ.options[1].extend({
141
+ source: AutomationOperationIntentSchemaZ.options[1].shape.source.optional(),
142
+ }),
143
+ ]);
144
+ export async function resolveAutomationIntent(
145
+ input: unknown,
146
+ client: AutomationClient,
147
+ options: AutomationRequestOptions = {},
148
+ ) {
149
+ const intent = AutomationInvocationIntentSchemaZ.parse(input);
150
+ return AutomationOperationIntentSchemaZ.parse({
151
+ ...intent,
152
+ source:
153
+ intent.source === undefined
154
+ ? ((await client.discover(options)).source ?? null)
155
+ : intent.source,
156
+ });
157
+ }
158
+
133
159
  /** The CLI is a thin adapter; it never falls back to raw tmux after an uncertain effect. */
134
160
  export async function runAutomationCli(
135
161
  args: readonly string[],
@@ -204,13 +230,13 @@ export async function runAutomationCli(
204
230
  }
205
231
  return;
206
232
  }
207
- const intent = AutomationOperationIntentSchemaZ.parse(input);
233
+ const intent = await resolveAutomationIntent(input, client);
208
234
  if (command !== "reserve" && intent.kind !== command)
209
235
  throw new IdeError("Automation command does not match intent kind", {
210
236
  code: "INVALID_REQUEST",
211
237
  });
212
238
  const reservation = await client.reserve(intent);
213
- if (command === "reserve") output(reservation);
239
+ if (command === "reserve") output({ ...reservation, intent });
214
240
  else output(await client.execute(reservation.handle, intent));
215
241
  } catch (error) {
216
242
  if (error instanceof AutomationInvocationError) throw new AutomationCliError(error);
@@ -169,6 +169,8 @@ export function mountAutomationRoutes(app: Hono, options: AutomationRoutesOption
169
169
  `${base}/panes`,
170
170
  route(async (c) => {
171
171
  const panes: AutomationPanesResponse["panes"] = [];
172
+ const credential = c.req.header(PANE_SOURCE_CREDENTIAL_HEADER);
173
+ let source: Endpoint | null = null;
172
174
  for (const scope of await options.owners.refresh()) {
173
175
  if (!scope.generation) continue;
174
176
  await options.owners.withOwner(
@@ -183,6 +185,18 @@ export function mountAutomationRoutes(app: Hono, options: AutomationRoutesOption
183
185
  pane.semanticPaneId,
184
186
  );
185
187
  if (!endpoint) continue;
188
+ if (credential) {
189
+ const binding = owner.resolveInteractionSource(
190
+ credential,
191
+ endpoint.workspaceName,
192
+ endpoint.semanticPaneId,
193
+ );
194
+ if (binding && same(binding.endpoint, endpoint)) {
195
+ if (source && !same(source, endpoint))
196
+ throw new AutomationRequestError("invalid-source");
197
+ source = endpoint;
198
+ }
199
+ }
186
200
  if (panes.length >= 4096) throw new AutomationRequestError("capacity");
187
201
  panes.push({
188
202
  endpoint,
@@ -193,7 +207,8 @@ export function mountAutomationRoutes(app: Hono, options: AutomationRoutesOption
193
207
  },
194
208
  );
195
209
  }
196
- return c.json(AutomationPanesResponseSchemaZ.parse({ version: 1, panes }));
210
+ if (credential && !source) throw new AutomationRequestError("invalid-source");
211
+ return c.json(AutomationPanesResponseSchemaZ.parse({ version: 1, panes, source }));
197
212
  }),
198
213
  );
199
214
  app.post(
@@ -11,7 +11,11 @@ import {
11
11
  AutomationInvocationError,
12
12
  type AutomationClient,
13
13
  } from "@tmux-ide/daemon-client/automation-client";
14
- import { localAutomationClient } from "./automation.ts";
14
+ import {
15
+ localAutomationClient,
16
+ AutomationInvocationIntentSchemaZ,
17
+ resolveAutomationIntent,
18
+ } from "./automation.ts";
15
19
 
16
20
  const result = (value: unknown) => ({
17
21
  content: [{ type: "text" as const, text: JSON.stringify(value) }],
@@ -53,12 +57,20 @@ export function createTmuxIdeMcpServer(client: AutomationClient): McpServer {
53
57
  "tmux_prepare",
54
58
  {
55
59
  description:
56
- "Prepare a read or send and receive a generation-fenced operation handle. This does not send input. Save the handle before executing. A source claim requires this MCP process to hold that pane's valid credential; otherwise use source:null.",
57
- inputSchema: z.object({ intent: AutomationOperationIntentSchemaZ }).strict(),
60
+ "Prepare a read or send and receive a generation-fenced operation handle. This does not send input. Save the handle before executing. Omit source to resolve this MCP process’s verified pane automatically, or use source:null for an unbound caller. Save the returned intent unchanged with the handle.",
61
+ inputSchema: z.object({ intent: AutomationInvocationIntentSchemaZ }).strict(),
58
62
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
59
63
  },
60
64
  async ({ intent }, context) =>
61
- call(() => client.reserve(intent, { signal: context.mcpReq.signal })),
65
+ call(async () => {
66
+ const resolved = await resolveAutomationIntent(intent, client, {
67
+ signal: context.mcpReq.signal,
68
+ });
69
+ return {
70
+ ...(await client.reserve(resolved, { signal: context.mcpReq.signal })),
71
+ intent: resolved,
72
+ };
73
+ }),
62
74
  );
63
75
  server.registerTool(
64
76
  "tmux_execute",
@@ -1,3 +1,4 @@
1
+ import { receiptIsHeaderWorthy } from "../ui/pane-interaction-presentation.ts";
1
2
  import { applicationMachineAuthorityManager } from "./application-machine-authority.ts";
2
3
  import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.ts";
3
4
  import type {
@@ -164,7 +165,12 @@ export function createApplicationPaneActivityOwner(
164
165
  endpoint: Extract<InteractionPaneEndpoint, { kind: "pane" }>,
165
166
  nativeIdentity?: NativePaneIdentity | null,
166
167
  ) => {
167
- const value = interactionForPane(snapshot, endpoint, nativeIdentity);
168
+ const value = interactionForPane(
169
+ snapshot,
170
+ endpoint,
171
+ nativeIdentity,
172
+ receiptIsHeaderWorthy,
173
+ );
168
174
  return value && interactionPresenceIsFresh(value, Date.now()) ? value : undefined;
169
175
  },
170
176
  }),
@@ -181,7 +187,7 @@ export function createApplicationPaneActivityOwner(
181
187
  const deadlines = [
182
188
  ...current.map(([, value]) => Date.parse(value.at) + INTERACTION_PRESENCE_MS),
183
189
  ...feed.activity
184
- .filter((entry) => entry.type === "interaction.evidence")
190
+ .filter((entry) => entry.type === "interaction.evidence" && receiptIsHeaderWorthy(entry))
185
191
  .map((entry) => Date.parse(interactionActivityAt(entry)) + INTERACTION_PRESENCE_MS),
186
192
  ].filter((deadline) => deadline >= now && deadline <= now + INTERACTION_PRESENCE_MS);
187
193
  if (deadlines.length)
@@ -237,7 +243,7 @@ export function createApplicationPaneActivityOwner(
237
243
  receipt.operationKind === "workspace.pane.read" ||
238
244
  receipt.operationKind === "workspace.pane.send"
239
245
  )
240
- feed = reduceInteractionReceipt(feed, receipt);
246
+ feed = reduceInteractionReceipt(feed, receipt, receiptIsHeaderWorthy);
241
247
  }
242
248
  entry.attempts = 0;
243
249
  publish();
@@ -608,6 +608,13 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
608
608
  interactionObservation={props.interactionObservation}
609
609
  paneInteractions={props.paneInteractions}
610
610
  interactionEndpoints={interactionEndpoints}
611
+ interactionPaneName={(endpoint) =>
612
+ nameForCurrentEndpoint(
613
+ props.machineSidebar?.groups().flatMap((group) => group.agents ?? []) ??
614
+ [],
615
+ endpoint,
616
+ )
617
+ }
611
618
  nativePaneIdentities={nativePaneIdentities}
612
619
  onSelectPane={props.onSelectPane}
613
620
  onSelectWindowLink={props.onSelectWindowLink}
@@ -156,6 +156,7 @@ export function beginApplicationMouseIngress(
156
156
  }
157
157
 
158
158
  export interface ApplicationTerminalWorkspaceProps {
159
+ readonly interactionPaneName?: (endpoint: PaneInteractionEndpoint) => string | undefined;
159
160
  readonly nativePaneIdentities?: Accessor<ReadonlyMap<string, NativePaneIdentity>>;
160
161
  readonly interactionEndpoints?: Accessor<
161
162
  ReadonlyMap<string, Extract<InteractionPaneEndpoint, { kind: "pane" }>>
@@ -530,6 +531,8 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
530
531
  epoch: number;
531
532
  } | null>(null);
532
533
  const paneName = (endpoint: PaneInteractionEndpoint) => {
534
+ const registeredName = props.interactionPaneName?.(endpoint);
535
+ if (registeredName) return registeredName;
533
536
  const current = props.interactionEndpoints?.().get(endpoint.semanticPaneId);
534
537
  return current && interactionPaneEndpointKey(current) === interactionPaneEndpointKey(endpoint)
535
538
  ? props.agentIndicators?.().get(endpoint.semanticPaneId)?.name
@@ -132,9 +132,13 @@ export function paneInteractionPresentation(
132
132
  // caller consumed captured output. Keep that limit visible on every surface.
133
133
  if (event.effect.kind === "unknown" && event.phase === "observed") {
134
134
  return {
135
- label: read ? "Read command · reader unknown" : "Send command · sender unknown",
135
+ label: source
136
+ ? `${read ? "Read" : "Send"} command from ${source}`
137
+ : read
138
+ ? "Read command · reader unknown"
139
+ : "Send command · sender unknown",
136
140
  compactLabel: read ? "Read command" : "Send command",
137
- source: "Unknown",
141
+ source: source ?? "Unknown",
138
142
  target,
139
143
  pending,
140
144
  failed,
@@ -255,3 +259,32 @@ export function nameForCurrentEndpoint(
255
259
  );
256
260
  return matches.length === 1 ? matches[0]!.name : undefined;
257
261
  }
262
+
263
+ /** Chrome is reserved for completed, attributed interactions between distinct panes.
264
+ * Unknown commands remain in Activity; hiding them is not sender attribution. */
265
+ export function paneInteractionIsHeaderWorthy(event: PaneInteractionEvent): boolean {
266
+ const source = event.sourceEndpoint;
267
+ const target = paneInteractionDisplayDestination(event);
268
+ return (
269
+ event.phase === "observed" &&
270
+ event.effect.kind !== "no-input" &&
271
+ (event.effect.kind !== "unknown" || event.origin !== "external") &&
272
+ event.origin !== "tui" &&
273
+ source !== null &&
274
+ (!target || interactionPaneEndpointKey(source) !== interactionPaneEndpointKey(target))
275
+ );
276
+ }
277
+ export function receiptIsHeaderWorthy(receipt: InteractionJournalEntry): boolean {
278
+ const source = receipt.evidence?.endpoints.source;
279
+ const target = receipt.evidence?.endpoints.destination;
280
+ return (
281
+ (receipt.type === "interaction.evidence" ||
282
+ (receipt.phase === "observed" && receipt.origin !== "tui")) &&
283
+ (receipt.evidence?.effect.kind !== "unknown" ||
284
+ (receipt.type === "interaction.receipt" && receipt.origin !== "external")) &&
285
+ receipt.evidence?.effect.kind !== "no-input" &&
286
+ source?.kind === "pane" &&
287
+ (target?.kind !== "pane" ||
288
+ interactionPaneEndpointKey(source) !== interactionPaneEndpointKey(target))
289
+ );
290
+ }
@@ -70,7 +70,7 @@ export function PaneInteraction(props: PaneInteractionProps) {
70
70
  () => props.event,
71
71
  () => props.theme,
72
72
  );
73
- const detailsWidth = () => (props.onDetails && props.width >= 32 ? 9 : 0);
73
+ const detailsWidth = () => (props.onDetails && props.width >= 20 ? 9 : 0);
74
74
  return (
75
75
  <box width={props.width} height={1} flexShrink={0} flexDirection="row">
76
76
  <Badge
@@ -4,7 +4,10 @@ import { PaneModeControl } from "../ui/pane-mode-control.tsx";
4
4
  import { paneInteractionPresence, type PaneInteractionProjection } from "@tmux-ide/core";
5
5
  import { statusPresentation } from "../ui/status-presentation.ts";
6
6
  import { PaneInteraction } from "../ui/pane-interaction.tsx";
7
- import { paneInteractionPresentation } from "../ui/pane-interaction-presentation.ts";
7
+ import {
8
+ paneInteractionPresentation,
9
+ paneInteractionIsHeaderWorthy,
10
+ } from "../ui/pane-interaction-presentation.ts";
8
11
  import type { AgentActivity } from "@tmux-ide/contracts";
9
12
  import { createMemo, createSignal, Show } from "solid-js";
10
13
 
@@ -90,7 +93,10 @@ export function PaneTitleBar(props: PaneTitleBarProps) {
90
93
  statusPresentation({ scrollback: props.scrollback, expanded: props.zoomed });
91
94
  const status = () => presentation()?.tone;
92
95
  const statusLabel = () => (presentation()?.action ? undefined : presentation()?.label);
93
- const presence = () => (props.interaction ? paneInteractionPresence(props.interaction) : null);
96
+ const presence = () =>
97
+ props.interaction && paneInteractionIsHeaderWorthy(props.interaction)
98
+ ? paneInteractionPresence(props.interaction)
99
+ : null;
94
100
  const activityLabel = () =>
95
101
  props.interaction ? paneInteractionPresentation(props.interaction, props.paneName).label : "";
96
102
  const activityWidth = () => {