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 +57 -14
- package/package.json +1 -1
- package/packages/contracts/src/automation-operations.ts +2 -0
- package/packages/core/src/interaction-receipts.ts +9 -1
- package/packages/daemon/dist/automation.js +21 -2
- package/packages/daemon/dist/command-center/automation.js +13 -1
- package/packages/daemon/dist/mcp.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/application-pane-activity-owner.js +4 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +3 -0
- package/packages/daemon/dist/tui/mirror/ui/pane-interaction-presentation.js +30 -2
- package/packages/daemon/dist/tui/mirror/ui/pane-interaction.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +4 -2
- package/packages/daemon/src/automation.ts +28 -2
- package/packages/daemon/src/command-center/automation.ts +16 -1
- package/packages/daemon/src/mcp.ts +16 -4
- package/packages/daemon/src/tui/mirror/runtime/application-pane-activity-owner.ts +9 -3
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +7 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +3 -0
- package/packages/daemon/src/tui/mirror/ui/pane-interaction-presentation.ts +35 -2
- package/packages/daemon/src/tui/mirror/ui/pane-interaction.tsx +1 -1
- package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +8 -2
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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.
|
|
91708
|
-
inputSchema:
|
|
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(() =>
|
|
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:
|
|
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:
|
|
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:
|
|
91781
|
+
inputSchema: z110.object({
|
|
91739
91782
|
resume: TmuxInteractionCursorSchemaZ,
|
|
91740
|
-
waitMs:
|
|
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
|
@@ -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 (
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
37
|
-
inputSchema: z.object({ intent:
|
|
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(() =>
|
|
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:
|
|
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 >=
|
|
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 = () =>
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
57
|
-
inputSchema: z.object({ intent:
|
|
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(() =>
|
|
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(
|
|
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:
|
|
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 >=
|
|
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 {
|
|
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 = () =>
|
|
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 = () => {
|