@particle-academy/agent-integrations 0.11.1 → 0.13.0

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/README.md CHANGED
@@ -83,6 +83,26 @@ await host.callTool("sheet_set_cell", { address: "B3", value: 42 });
83
83
 
84
84
  Need both? `MicroMcpServer` extends `ToolRegistry`, so the same instance serves direct in-process calls **and** SSE-relayed remote agents at the same time. Bridges accept either — every `register*Bridge(host, …)` signature takes a `ToolHost`.
85
85
 
86
+ ## Connect an agent to your app — `<ConnectorButtons>`
87
+
88
+ The step *before* presence: getting your MCP server installed in the user's
89
+ agent. `<ConnectorButtons>` renders the right "Add to Claude / Cursor / VS Code"
90
+ affordance per client (each subtly different), and `writeMcpbBundle()` builds the
91
+ Claude Desktop `.mcpb`:
92
+
93
+ ```tsx
94
+ import { ConnectorButtons } from "@particle-academy/agent-integrations";
95
+ import "@particle-academy/agent-integrations/styles.css";
96
+
97
+ <ConnectorButtons serverName="Decksmith" mcpUrl="https://decksmith.dev/mcp" mcpbDownloadUrl="/decksmith.mcpb" />
98
+ ```
99
+
100
+ ```ts
101
+ import { writeMcpbBundle } from "@particle-academy/agent-integrations/connectors/build"; // Node, build-time
102
+ ```
103
+
104
+ Full guide + the per-client format gotchas: [`docs/connectors.md`](./docs/connectors.md).
105
+
86
106
  ## Architecture
87
107
 
88
108
  ```
@@ -215,3 +235,9 @@ Agents (Claude Desktop, Cline, custom) connect to the same channel via your auth
215
235
  ## License
216
236
 
217
237
  MIT
238
+
239
+ ---
240
+
241
+ ## ⭐ Star Fancy UI
242
+
243
+ If this package is useful to you, a quick ⭐ on the repo really helps us build a better kit. Thank you!
@@ -3,7 +3,7 @@ import { B as Bridge } from '../types-CCSBGW9T.cjs';
3
3
  import '../types-aOQLTW0E.cjs';
4
4
 
5
5
  /**
6
- * A shell/profile an agent can switch the terminal to. Mirrors fancy-term's
6
+ * A shell/profile an agent can switch a terminal to. Mirrors fancy-term's
7
7
  * `ShellProfile` (kept local so the bridge never imports fancy-term).
8
8
  */
9
9
  type TerminalShell = {
@@ -12,36 +12,63 @@ type TerminalShell = {
12
12
  icon?: string;
13
13
  };
14
14
  /**
15
- * Host-provided window into a terminal surface (e.g. a fancy-term `<Terminal>`'s
16
- * `TerminalHandle`). The bridge never touches the DOM it reads + writes through
17
- * these functions, so it works with any terminal the host wires up.
15
+ * One terminal the bridge can drive. A Human+ app often hosts **several**
16
+ * terminals on a screen (a build pane, a server pane, an agent scratch shell);
17
+ * each is a `TerminalRef` with a stable `id` so an agent can read/write any of
18
+ * them — not just "its own". Wire the function fields to that terminal's
19
+ * fancy-term `TerminalHandle`.
18
20
  */
19
- type TerminalBridgeAdapter = {
20
- /** fancy-screens screen id (optional) so activity events know which screen the terminal lives in. */
21
- screenId?: string;
22
- /** Read the visible terminal buffer as text (wire to `TerminalHandle.getBuffer`). */
21
+ type TerminalRef = {
22
+ /** Stable id used to address this terminal (`terminal_list` enumerates them). */
23
+ id: string;
24
+ /** Human label, e.g. "Build", "Server". Defaults to the id. */
25
+ label?: string;
26
+ /** True for the focused terminal — the default target when no id is passed. */
27
+ active?: boolean;
28
+ /** Read the visible buffer as text (wire to `TerminalHandle.getBuffer`). */
23
29
  getBuffer: () => string;
24
- /** Write raw data / keystrokes to the terminal (wire to `TerminalHandle.write`). */
30
+ /** Write raw data / keystrokes (wire to `TerminalHandle.write`). */
25
31
  write: (data: string) => void;
26
- /** Run a command. Defaults to writing `${command}\r` (submit to a PTY); override to call a real command runner. */
32
+ /** Run a command. Defaults to writing `${command}\r`; override for a real runner. */
27
33
  runCommand?: (command: string) => void | Promise<void>;
28
- /** Optional: clear the terminal viewport (wire to `TerminalHandle.clear`). */
34
+ /** Clear the viewport (wire to `TerminalHandle.clear`). */
29
35
  clear?: () => void;
30
- /** Optional: the shells the host offers (cmd, powershell, git-bash, …). Enables `terminal_list_shells`. */
36
+ /** Current text selection (wire to `TerminalHandle.getSelection`). */
37
+ getSelection?: () => string;
38
+ /** Shells this terminal offers (cmd, PowerShell, …). */
31
39
  listShells?: () => TerminalShell[];
32
- /** Optional: switch the active shell by id (wire to `TerminalHandle.setShell` + a host backend reconnect). Enables `terminal_set_shell`. */
40
+ /** Switch this terminal's active shell by id. */
33
41
  setShell?: (id: string) => void | Promise<void>;
34
- /** Optional: the currently active shell id (wire to `TerminalHandle.getShell`). */
42
+ /** This terminal's active shell id. */
35
43
  getShell?: () => string | undefined;
36
44
  };
45
+ /**
46
+ * Single-terminal adapter (back-compat). A `TerminalRef` without the
47
+ * `id`/`label`/`active` bookkeeping — pass it as `{ adapter }` and the bridge
48
+ * treats it as the one (active) terminal. Use `{ terminals }` for multiple.
49
+ */
50
+ type TerminalBridgeAdapter = Omit<TerminalRef, "id" | "label" | "active"> & {
51
+ /** fancy-screens screen id (optional) so activity events know which screen the terminal lives in. */
52
+ screenId?: string;
53
+ };
37
54
  type StagedKind = "write" | "run";
38
55
  type Staged = {
39
56
  id: string;
40
57
  kind: StagedKind;
41
58
  data: string;
59
+ terminalId: string;
42
60
  };
43
61
  type TerminalBridgeOptions = {
44
- adapter: TerminalBridgeAdapter;
62
+ /** A single terminal (back-compat). Mutually exclusive with `terminals`. */
63
+ adapter?: TerminalBridgeAdapter;
64
+ /**
65
+ * The live list of terminals on the screen. Use this when the app hosts more
66
+ * than one terminal so an agent can `terminal_list` then target any of them by
67
+ * id — i.e. reach into another terminal in the same screen.
68
+ */
69
+ terminals?: () => TerminalRef[];
70
+ /** fancy-screens screen id for activity events (defaults to `adapter.screenId`). */
71
+ screenId?: string;
45
72
  agent?: {
46
73
  id: string;
47
74
  name?: string;
@@ -66,15 +93,20 @@ type TerminalBridge = Bridge & {
66
93
  pending: () => Staged[];
67
94
  };
68
95
  /**
69
- * registerTerminalBridge — MCP access to a terminal surface. An agent reads the
70
- * visible buffer (`terminal_read`), writes input (`terminal_write`), and runs
71
- * commands (`terminal_run`) through the host adapter; every mutation broadcasts
72
- * an `AgentActivity` event. With `pendingMode`, destructive actions are staged
73
- * for human confirmation (`terminal_confirm` / `terminal_reject` /
74
- * `terminal_pending`). When the adapter offers shells, the agent can also list
75
- * (`terminal_list_shells`) and switch (`terminal_set_shell`) the active shell —
76
- * cmd, PowerShell, Git Bash, etc. Tool prefix `terminal_*`.
96
+ * registerTerminalBridge — MCP access to one **or many** terminal surfaces on a
97
+ * screen. An agent reads the visible buffer (`terminal_read`), writes input
98
+ * (`terminal_write`), and runs commands (`terminal_run`) through the host; every
99
+ * mutation broadcasts an `AgentActivity` event. With `pendingMode`, destructive
100
+ * actions are staged for human confirmation (`terminal_confirm` / `terminal_reject`
101
+ * / `terminal_pending`).
102
+ *
103
+ * **Multi-terminal:** pass `{ terminals }` (vs a single `{ adapter }`) and every
104
+ * tool takes an optional `terminal` id; `terminal_list` enumerates them. This is
105
+ * how an agent **reaches into another terminal in the same screen** rather than
106
+ * being stuck in one. When a terminal offers shells, the agent can also list
107
+ * (`terminal_list_shells`) and switch (`terminal_set_shell`) its shell. Tool
108
+ * prefix `terminal_*`.
77
109
  */
78
110
  declare function registerTerminalBridge(host: ToolHost, options: TerminalBridgeOptions): TerminalBridge;
79
111
 
80
- export { type TerminalBridge, type TerminalBridgeAdapter, type TerminalBridgeOptions, type TerminalShell, registerTerminalBridge };
112
+ export { type TerminalBridge, type TerminalBridgeAdapter, type TerminalBridgeOptions, type TerminalRef, type TerminalShell, registerTerminalBridge };
@@ -3,7 +3,7 @@ import { B as Bridge } from '../types-DIVNcIQO.js';
3
3
  import '../types-aOQLTW0E.js';
4
4
 
5
5
  /**
6
- * A shell/profile an agent can switch the terminal to. Mirrors fancy-term's
6
+ * A shell/profile an agent can switch a terminal to. Mirrors fancy-term's
7
7
  * `ShellProfile` (kept local so the bridge never imports fancy-term).
8
8
  */
9
9
  type TerminalShell = {
@@ -12,36 +12,63 @@ type TerminalShell = {
12
12
  icon?: string;
13
13
  };
14
14
  /**
15
- * Host-provided window into a terminal surface (e.g. a fancy-term `<Terminal>`'s
16
- * `TerminalHandle`). The bridge never touches the DOM it reads + writes through
17
- * these functions, so it works with any terminal the host wires up.
15
+ * One terminal the bridge can drive. A Human+ app often hosts **several**
16
+ * terminals on a screen (a build pane, a server pane, an agent scratch shell);
17
+ * each is a `TerminalRef` with a stable `id` so an agent can read/write any of
18
+ * them — not just "its own". Wire the function fields to that terminal's
19
+ * fancy-term `TerminalHandle`.
18
20
  */
19
- type TerminalBridgeAdapter = {
20
- /** fancy-screens screen id (optional) so activity events know which screen the terminal lives in. */
21
- screenId?: string;
22
- /** Read the visible terminal buffer as text (wire to `TerminalHandle.getBuffer`). */
21
+ type TerminalRef = {
22
+ /** Stable id used to address this terminal (`terminal_list` enumerates them). */
23
+ id: string;
24
+ /** Human label, e.g. "Build", "Server". Defaults to the id. */
25
+ label?: string;
26
+ /** True for the focused terminal — the default target when no id is passed. */
27
+ active?: boolean;
28
+ /** Read the visible buffer as text (wire to `TerminalHandle.getBuffer`). */
23
29
  getBuffer: () => string;
24
- /** Write raw data / keystrokes to the terminal (wire to `TerminalHandle.write`). */
30
+ /** Write raw data / keystrokes (wire to `TerminalHandle.write`). */
25
31
  write: (data: string) => void;
26
- /** Run a command. Defaults to writing `${command}\r` (submit to a PTY); override to call a real command runner. */
32
+ /** Run a command. Defaults to writing `${command}\r`; override for a real runner. */
27
33
  runCommand?: (command: string) => void | Promise<void>;
28
- /** Optional: clear the terminal viewport (wire to `TerminalHandle.clear`). */
34
+ /** Clear the viewport (wire to `TerminalHandle.clear`). */
29
35
  clear?: () => void;
30
- /** Optional: the shells the host offers (cmd, powershell, git-bash, …). Enables `terminal_list_shells`. */
36
+ /** Current text selection (wire to `TerminalHandle.getSelection`). */
37
+ getSelection?: () => string;
38
+ /** Shells this terminal offers (cmd, PowerShell, …). */
31
39
  listShells?: () => TerminalShell[];
32
- /** Optional: switch the active shell by id (wire to `TerminalHandle.setShell` + a host backend reconnect). Enables `terminal_set_shell`. */
40
+ /** Switch this terminal's active shell by id. */
33
41
  setShell?: (id: string) => void | Promise<void>;
34
- /** Optional: the currently active shell id (wire to `TerminalHandle.getShell`). */
42
+ /** This terminal's active shell id. */
35
43
  getShell?: () => string | undefined;
36
44
  };
45
+ /**
46
+ * Single-terminal adapter (back-compat). A `TerminalRef` without the
47
+ * `id`/`label`/`active` bookkeeping — pass it as `{ adapter }` and the bridge
48
+ * treats it as the one (active) terminal. Use `{ terminals }` for multiple.
49
+ */
50
+ type TerminalBridgeAdapter = Omit<TerminalRef, "id" | "label" | "active"> & {
51
+ /** fancy-screens screen id (optional) so activity events know which screen the terminal lives in. */
52
+ screenId?: string;
53
+ };
37
54
  type StagedKind = "write" | "run";
38
55
  type Staged = {
39
56
  id: string;
40
57
  kind: StagedKind;
41
58
  data: string;
59
+ terminalId: string;
42
60
  };
43
61
  type TerminalBridgeOptions = {
44
- adapter: TerminalBridgeAdapter;
62
+ /** A single terminal (back-compat). Mutually exclusive with `terminals`. */
63
+ adapter?: TerminalBridgeAdapter;
64
+ /**
65
+ * The live list of terminals on the screen. Use this when the app hosts more
66
+ * than one terminal so an agent can `terminal_list` then target any of them by
67
+ * id — i.e. reach into another terminal in the same screen.
68
+ */
69
+ terminals?: () => TerminalRef[];
70
+ /** fancy-screens screen id for activity events (defaults to `adapter.screenId`). */
71
+ screenId?: string;
45
72
  agent?: {
46
73
  id: string;
47
74
  name?: string;
@@ -66,15 +93,20 @@ type TerminalBridge = Bridge & {
66
93
  pending: () => Staged[];
67
94
  };
68
95
  /**
69
- * registerTerminalBridge — MCP access to a terminal surface. An agent reads the
70
- * visible buffer (`terminal_read`), writes input (`terminal_write`), and runs
71
- * commands (`terminal_run`) through the host adapter; every mutation broadcasts
72
- * an `AgentActivity` event. With `pendingMode`, destructive actions are staged
73
- * for human confirmation (`terminal_confirm` / `terminal_reject` /
74
- * `terminal_pending`). When the adapter offers shells, the agent can also list
75
- * (`terminal_list_shells`) and switch (`terminal_set_shell`) the active shell —
76
- * cmd, PowerShell, Git Bash, etc. Tool prefix `terminal_*`.
96
+ * registerTerminalBridge — MCP access to one **or many** terminal surfaces on a
97
+ * screen. An agent reads the visible buffer (`terminal_read`), writes input
98
+ * (`terminal_write`), and runs commands (`terminal_run`) through the host; every
99
+ * mutation broadcasts an `AgentActivity` event. With `pendingMode`, destructive
100
+ * actions are staged for human confirmation (`terminal_confirm` / `terminal_reject`
101
+ * / `terminal_pending`).
102
+ *
103
+ * **Multi-terminal:** pass `{ terminals }` (vs a single `{ adapter }`) and every
104
+ * tool takes an optional `terminal` id; `terminal_list` enumerates them. This is
105
+ * how an agent **reaches into another terminal in the same screen** rather than
106
+ * being stuck in one. When a terminal offers shells, the agent can also list
107
+ * (`terminal_list_shells`) and switch (`terminal_set_shell`) its shell. Tool
108
+ * prefix `terminal_*`.
77
109
  */
78
110
  declare function registerTerminalBridge(host: ToolHost, options: TerminalBridgeOptions): TerminalBridge;
79
111
 
80
- export { type TerminalBridge, type TerminalBridgeAdapter, type TerminalBridgeOptions, type TerminalShell, registerTerminalBridge };
112
+ export { type TerminalBridge, type TerminalBridgeAdapter, type TerminalBridgeOptions, type TerminalRef, type TerminalShell, registerTerminalBridge };
@@ -126,19 +126,39 @@ function serialize(entry) {
126
126
  var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
127
127
  var truncate = (s, n = 60) => s.length > n ? s.slice(0, n) + "\u2026" : s;
128
128
  function registerTerminalBridge(host, options) {
129
- const { adapter } = options;
130
129
  const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
131
130
  const pendingMode = options.pendingMode ?? false;
131
+ const screenId = options.screenId ?? options.adapter?.screenId;
132
132
  const disposers = [];
133
133
  const staged = /* @__PURE__ */ new Map();
134
134
  let seq = 0;
135
135
  ensureUndoToolsRegistered(host);
136
- const target = (label) => ({
136
+ const listTerminals = () => {
137
+ if (options.terminals) return options.terminals();
138
+ if (options.adapter) return [{ id: "terminal", label: "Terminal", active: true, ...options.adapter }];
139
+ return [];
140
+ };
141
+ const resolve = (id) => {
142
+ const list = listTerminals();
143
+ if (typeof id === "string" && id !== "") return list.find((t) => t.id === id);
144
+ return list.find((t) => t.active) ?? list[0];
145
+ };
146
+ const anyMulti = !!options.terminals;
147
+ const canClear = anyMulti || !!options.adapter?.clear;
148
+ const canShells = anyMulti || !!options.adapter?.listShells;
149
+ const canSetShell = anyMulti || !!options.adapter?.setShell;
150
+ const target = (label, terminalId) => ({
137
151
  kind: "terminal",
138
- screenId: adapter.screenId,
139
- elementId: adapter.screenId ?? "terminal",
152
+ screenId,
153
+ elementId: terminalId ?? screenId ?? "terminal",
140
154
  label: label ?? "terminal"
141
155
  });
156
+ const TERMINAL_ARG = {
157
+ terminal: {
158
+ type: "string",
159
+ description: "Terminal id to target (call terminal_list for ids). Omit for the active / only terminal."
160
+ }
161
+ };
142
162
  const reg = (name, description, properties, required, handler, isMutation, resolveTarget) => {
143
163
  const wrapped = async (args) => {
144
164
  try {
@@ -151,7 +171,7 @@ function registerTerminalBridge(host, options) {
151
171
  toolName: name,
152
172
  agent,
153
173
  kind: "terminal",
154
- screenId: adapter.screenId,
174
+ screenId,
155
175
  resolveTarget: ({ args, result }) => resolveTarget?.(args, result) ?? target()
156
176
  }) : wrapped;
157
177
  disposers.push(
@@ -165,38 +185,66 @@ function registerTerminalBridge(host, options) {
165
185
  )
166
186
  );
167
187
  };
168
- async function exec(kind, data) {
188
+ const need = (args) => {
189
+ const t = resolve(args.terminal);
190
+ if (!t) {
191
+ const ids = listTerminals().map((x) => x.id).join(", ") || "(none)";
192
+ throw new Error(
193
+ typeof args.terminal === "string" && args.terminal ? `Unknown terminal '${args.terminal}'. Available: ${ids}. Use terminal_list.` : "No terminal available."
194
+ );
195
+ }
196
+ return t;
197
+ };
198
+ async function exec(t, kind, data) {
169
199
  if (kind === "run") {
170
- if (adapter.runCommand) await adapter.runCommand(data);
171
- else adapter.write(data + "\r");
200
+ if (t.runCommand) await t.runCommand(data);
201
+ else t.write(data + "\r");
172
202
  } else {
173
- adapter.write(data);
203
+ t.write(data);
174
204
  }
175
205
  }
176
- async function stageOrExec(kind, data) {
206
+ async function stageOrExec(t, kind, data) {
177
207
  if (!pendingMode) {
178
- await exec(kind, data);
179
- return textResult(`${kind === "run" ? "ran" : "wrote"}: ${truncate(data)}`, { kind, data, executed: true });
208
+ await exec(t, kind, data);
209
+ return textResult(`${kind === "run" ? "ran" : "wrote"} on ${t.id}: ${truncate(data)}`, {
210
+ kind,
211
+ data,
212
+ terminal: t.id,
213
+ executed: true
214
+ });
180
215
  }
181
216
  const id = `t${++seq}`;
182
- const entry = { id, kind, data };
217
+ const entry = { id, kind, data, terminalId: t.id };
183
218
  staged.set(id, entry);
184
219
  options.onPending?.(entry);
185
220
  return textResult(
186
- `Staged ${kind} (id ${id}) \u2014 awaiting human confirmation: ${truncate(data)}`,
221
+ `Staged ${kind} on ${t.id} (id ${id}) \u2014 awaiting human confirmation: ${truncate(data)}`,
187
222
  { ...entry, pending: true }
188
223
  );
189
224
  }
225
+ reg(
226
+ "terminal_list",
227
+ "List the terminals on this screen (id, label, which is active) \u2014 so you can reach into another terminal, not just the active one. Pass the chosen id as `terminal` to the other tools.",
228
+ {},
229
+ [],
230
+ () => {
231
+ const list = listTerminals().map((t) => ({ id: t.id, label: t.label ?? t.id, active: !!t.active }));
232
+ const text = list.length ? list.map((t) => `${t.active ? "* " : " "}${t.id} \u2014 ${t.label}`).join("\n") : "(no terminals)";
233
+ return textResult(text, { terminals: list });
234
+ },
235
+ false
236
+ );
190
237
  reg(
191
238
  "terminal_read",
192
- "Read the visible terminal buffer as text \u2014 what the user sees. Pass `tail` for only the last N lines.",
193
- { tail: { type: "number", description: "Return only the last N lines." } },
239
+ "Read a terminal's visible buffer as text \u2014 what the user sees. Pass `tail` for only the last N lines, `terminal` to read a specific one.",
240
+ { ...TERMINAL_ARG, tail: { type: "number", description: "Return only the last N lines." } },
194
241
  [],
195
242
  (args) => {
196
- let buf = adapter.getBuffer();
243
+ const t = need(args);
244
+ let buf = t.getBuffer();
197
245
  const tail = typeof args.tail === "number" ? args.tail : void 0;
198
246
  if (tail && tail > 0) buf = buf.split("\n").slice(-tail).join("\n");
199
- return textResult(buf, { buffer: buf });
247
+ return textResult(buf, { buffer: buf, terminal: t.id });
200
248
  },
201
249
  false
202
250
  );
@@ -208,7 +256,7 @@ function registerTerminalBridge(host, options) {
208
256
  () => {
209
257
  const list = [...staged.values()];
210
258
  return textResult(
211
- list.length ? list.map((s) => `${s.id}: ${s.kind} ${truncate(s.data)}`).join("\n") : "(none)",
259
+ list.length ? list.map((s) => `${s.id}: ${s.kind} on ${s.terminalId} ${truncate(s.data)}`).join("\n") : "(none)",
212
260
  { pending: list }
213
261
  );
214
262
  },
@@ -216,20 +264,21 @@ function registerTerminalBridge(host, options) {
216
264
  );
217
265
  reg(
218
266
  "terminal_write",
219
- "Write raw data / keystrokes to the terminal (input, control chars, ANSI). In pendingMode this stages instead of executing.",
220
- { data: { type: "string", description: "Raw bytes to write." } },
267
+ "Write raw data / keystrokes to a terminal (input, control chars, ANSI). Pass `terminal` to target a specific one. In pendingMode this stages instead of executing.",
268
+ { ...TERMINAL_ARG, data: { type: "string", description: "Raw bytes to write." } },
221
269
  ["data"],
222
- (args) => stageOrExec("write", String(args.data)),
223
- true
270
+ (args) => stageOrExec(need(args), "write", String(args.data)),
271
+ true,
272
+ (args) => target(`write:${String(args.terminal ?? "")}`, resolve(args.terminal)?.id)
224
273
  );
225
274
  reg(
226
275
  "terminal_run",
227
- "Run a shell command \u2014 writes the command followed by Enter (or the host's command runner). In pendingMode this stages it for confirmation.",
228
- { command: { type: "string", description: "The command line to run." } },
276
+ "Run a shell command in a terminal \u2014 writes the command + Enter (or the host's runner). Pass `terminal` to target a specific one. In pendingMode this stages it for confirmation.",
277
+ { ...TERMINAL_ARG, command: { type: "string", description: "The command line to run." } },
229
278
  ["command"],
230
- (args) => stageOrExec("run", String(args.command)),
279
+ (args) => stageOrExec(need(args), "run", String(args.command)),
231
280
  true,
232
- (args) => target(truncate(String(args.command ?? "")))
281
+ (args) => target(truncate(String(args.command ?? "")), resolve(args.terminal)?.id)
233
282
  );
234
283
  reg(
235
284
  "terminal_confirm",
@@ -240,11 +289,17 @@ function registerTerminalBridge(host, options) {
240
289
  const id = String(args.id);
241
290
  const entry = staged.get(id);
242
291
  if (!entry) return errorResult(`No staged command ${id}`);
292
+ const t = resolve(entry.terminalId);
293
+ if (!t) return errorResult(`Terminal '${entry.terminalId}' is gone \u2014 cannot run ${id}`);
243
294
  staged.delete(id);
244
- await exec(entry.kind, entry.data);
245
- return textResult(`Confirmed ${id}: ${entry.kind} ${truncate(entry.data)}`, { ...entry, executed: true });
295
+ await exec(t, entry.kind, entry.data);
296
+ return textResult(`Confirmed ${id}: ${entry.kind} on ${t.id} ${truncate(entry.data)}`, { ...entry, executed: true });
246
297
  },
247
- true
298
+ true,
299
+ (args) => {
300
+ const e = staged.get(String(args.id));
301
+ return target(`confirm:${String(args.id ?? "")}`, e?.terminalId);
302
+ }
248
303
  );
249
304
  reg(
250
305
  "terminal_reject",
@@ -258,51 +313,58 @@ function registerTerminalBridge(host, options) {
258
313
  },
259
314
  false
260
315
  );
261
- if (adapter.clear) {
316
+ if (canClear) {
262
317
  reg(
263
318
  "terminal_clear",
264
- "Clear the terminal viewport.",
265
- {},
319
+ "Clear a terminal's viewport. Pass `terminal` to target a specific one.",
320
+ { ...TERMINAL_ARG },
266
321
  [],
267
- () => {
268
- adapter.clear();
269
- return textResult("cleared");
322
+ (args) => {
323
+ const t = need(args);
324
+ if (!t.clear) return errorResult(`Terminal '${t.id}' can't be cleared.`);
325
+ t.clear();
326
+ return textResult(`cleared ${t.id}`, { terminal: t.id });
270
327
  },
271
- true
328
+ true,
329
+ (args) => target(`clear:${String(args.terminal ?? "")}`, resolve(args.terminal)?.id)
272
330
  );
273
331
  }
274
- if (adapter.listShells) {
332
+ if (canShells) {
275
333
  reg(
276
334
  "terminal_list_shells",
277
- "List the shells the host can switch to (cmd, PowerShell, Git Bash, \u2026) \u2014 id + label, with the active one marked.",
278
- {},
335
+ "List the shells a terminal can switch to (cmd, PowerShell, Git Bash, \u2026) \u2014 id + label, active one marked. Pass `terminal` to target a specific one.",
336
+ { ...TERMINAL_ARG },
279
337
  [],
280
- () => {
281
- const shells = adapter.listShells();
282
- const active = adapter.getShell?.();
338
+ (args) => {
339
+ const t = need(args);
340
+ if (!t.listShells) return errorResult(`Terminal '${t.id}' has no switchable shells.`);
341
+ const shells = t.listShells();
342
+ const active = t.getShell?.();
283
343
  const text = shells.length ? shells.map((s) => `${s.id === active ? "* " : " "}${s.id} \u2014 ${s.label}`).join("\n") : "(none)";
284
- return textResult(text, { shells, active });
344
+ return textResult(text, { shells, active, terminal: t.id });
285
345
  },
286
346
  false
287
347
  );
288
348
  }
289
- if (adapter.setShell) {
349
+ if (canSetShell) {
290
350
  reg(
291
351
  "terminal_set_shell",
292
- "Switch the active shell by id (e.g. 'powershell', 'git-bash'). Call terminal_list_shells first for valid ids. The host reconnects its backend to the chosen shell.",
293
- { id: { type: "string", description: "Shell id to switch to." } },
352
+ "Switch a terminal's active shell by id (e.g. 'powershell', 'git-bash'). Call terminal_list_shells first for valid ids. Pass `terminal` to target a specific one.",
353
+ { ...TERMINAL_ARG, id: { type: "string", description: "Shell id to switch to." } },
294
354
  ["id"],
295
355
  async (args) => {
356
+ const t = need(args);
357
+ if (!t.setShell) return errorResult(`Terminal '${t.id}' can't switch shells.`);
296
358
  const id = String(args.id);
297
- const shells = adapter.listShells?.();
359
+ const shells = t.listShells?.();
298
360
  if (shells && shells.length && !shells.some((s) => s.id === id)) {
299
- return errorResult(`Unknown shell '${id}'. Use terminal_list_shells for valid ids.`);
361
+ return errorResult(`Unknown shell '${id}' for ${t.id}. Use terminal_list_shells for valid ids.`);
300
362
  }
301
- await adapter.setShell(id);
302
- return textResult(`Switched shell to ${id}`, { shell: id });
363
+ await t.setShell(id);
364
+ return textResult(`Switched ${t.id} shell to ${id}`, { shell: id, terminal: t.id });
303
365
  },
304
366
  true,
305
- (args) => target(`shell:${String(args.id ?? "")}`)
367
+ (args) => target(`shell:${String(args.id ?? "")}`, resolve(args.terminal)?.id)
306
368
  );
307
369
  }
308
370
  return {
@@ -316,8 +378,9 @@ function registerTerminalBridge(host, options) {
316
378
  confirm: (id) => {
317
379
  const e = staged.get(id);
318
380
  if (e) {
381
+ const t = resolve(e.terminalId);
319
382
  staged.delete(id);
320
- void exec(e.kind, e.data);
383
+ if (t) void exec(t, e.kind, e.data);
321
384
  }
322
385
  },
323
386
  reject: (id) => {