paseo-terminal-integration 0.2.0 → 0.4.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
@@ -2,31 +2,32 @@
2
2
 
3
3
  Two-way bridge between agent chats and Paseo terminals.
4
4
 
5
- **Agent → terminal.** When a turn ends, runnable fenced code blocks in the agent's reply
6
- (`bash`, `sh`, `shell`, `console`, `zsh`, `fish`, `python`, `js`/`node`) get a **Run in terminal**
7
- card under the reply:
8
-
9
- - **Run in side terminal** runs the block in the workspace terminal "Agent commands" and shows it in
10
- the side panel; **Run silently** runs it without showing anything.
11
- - With **Send to agent** checked (the default), the command, exit code, and output go to the agent
12
- as a new message when the block finishes. If the agent is busy, delivery waits until its turn
13
- ends instead of interrupting it. Unchecked, **Send output** sends it later.
5
+ **Agent → terminal.** Runnable fenced code blocks in the agent's reply (`bash`, `sh`, `shell`,
6
+ `console`, `zsh`, `fish`, `python`, `js`/`node`) get run controls. On Paseo builds with inline code
7
+ block actions ([getpaseo/paseo#5874](https://github.com/getpaseo/paseo/pull/5874)) the controls sit
8
+ directly under each block, including in older messages as history loads, and become available when
9
+ the reply finishes. On other Paseo versions a **Run in terminal** card appears under the reply.
10
+
11
+ - **Run** runs the block in the workspace terminal "Agent commands", a regular Paseo terminal tab.
12
+ With inline controls, Run also opens that tab.
13
+ - **Run in background** runs it as a hidden process in the agent's working directory: no terminal
14
+ tab, and the runner keeps the last 5,000 lines instead of what fits on a terminal screen. Nothing
15
+ can be typed into it: stdin is empty, so password and confirmation prompts fail instead of
16
+ waiting. Background runs do not wait for the terminal.
17
+ - With **Send output to agent** checked (the default), the command, exit code, and output go to
18
+ the agent as a new message when the block finishes. If the agent is busy, delivery waits until
19
+ its turn ends instead of interrupting it. Unchecked, **Send output** sends it later.
14
20
  - **Stop** sends Ctrl-C; **Send output so far** reports a still-running command.
15
21
 
16
- **Terminal panel.** The side panel (Explorer) shows "Agent commands" as a full terminal: the same
17
- xterm.js renderer and live stream as Paseo's terminal tabs, with colors, the cursor, full-screen
18
- programs, and keyboard input. Click it and type to answer `sudo` passwords, `y/n` questions, or
19
- npm's "Press ENTER" without leaving the chat. Ctrl-C copies while text is selected and otherwise
20
- goes to the shell; Ctrl-Shift-C and Ctrl-Shift-V always copy and paste. Web links open in the
21
- browser when clicked. The card's **Terminal** button opens the panel without running anything. On
22
- phones the panel is a plain-text copy with an input line and Enter, Ctrl-C, Ctrl-D, and Tab keys,
23
- in a tab that opens only when you ask. **Settings → Plugins → Terminal integration** switches the
24
- panel to a workspace tab and sets the checkbox's default.
25
-
26
- Paseo gives every workspace terminal its own tab, so "Agent commands" also appears in the tab bar
27
- (in the background). Closing that tab ends the terminal; the next run starts a new one. Runs in
28
- one workspace are queued so a command never types into another one's stdin. Each block runs as a
29
- script in the agent's working directory, so `cd` and `export` do not persist between runs.
22
+ **Terminal beside the chat.** On Paseo builds with the terminal open-location setting, choose
23
+ **Settings → Layout → Open location → Opening a terminal → On the side** and "Agent commands"
24
+ opens beside the chat. Elsewhere, drag the tab onto the right or bottom edge of the chat to split
25
+ the workspace. A tab you moved stays where you put it while the terminal is open. Background runs
26
+ never change the layout.
27
+
28
+ Terminal runs in one workspace are queued so a command never types into another one's stdin.
29
+ Each block runs as a script in the agent's working directory, so `cd` and `export` do not persist
30
+ between runs. **Settings → Plugins → Terminal integration** sets the checkbox's default.
30
31
 
31
32
  **Terminal → agent.** In the composer's attachment menu, **Terminal output** attaches the latest
32
33
  lines of any terminal on the daemon. Type part of a name or path to filter, and a number to choose
@@ -39,36 +40,38 @@ Requires Paseo 0.10.1 or later and a macOS or Linux daemon host with `bash` (plu
39
40
 
40
41
  ```bash
41
42
  paseo plugin install npm:paseo-terminal-integration
43
+ # or from GitHub
44
+ paseo plugin install github:keefeere/paseo-terminal-integration
42
45
  ```
43
46
 
44
- Install from npm: GitHub installs skip npm dependencies, which the terminal panel needs.
45
-
46
47
  Plugins are trusted, unsandboxed code: this one types commands into terminals on the daemon host.
47
48
  Nothing runs until you press a button or submit `/run`.
48
49
 
49
50
  ## Commands
50
51
 
51
- | Command | Effect |
52
- | ----------------------------------------------- | ----------------------------------------------------------- |
53
- | `/run <command>` | Run a shell command and send its output to the agent |
54
- | `/blocks` | Add a run card for the latest reply (e.g. older chats) |
55
- | ⌘K / Ctrl+K → "Terminal: add run buttons to …" | Same as `/blocks` |
56
- | ⌘K / Ctrl+K → "Terminal: show … in the side panel" | Open the terminal panel in the Explorer |
52
+ | Command | Effect |
53
+ | ---------------------------------------------- | ------------------------------------------------------ |
54
+ | `/run <command>` | Run a shell command and send its output to the agent |
55
+ | `/blocks` | Add a run card for the latest reply (e.g. older chats) |
56
+ | ⌘K / Ctrl+K → "Terminal: add run buttons to …" | Same as `/blocks` |
57
+
58
+ The commands are available on Paseo versions without inline code block actions.
57
59
 
58
60
  ## Limitations
59
61
 
60
- - Output is read from the terminal screen, so very long lines arrive wrapped and only the last
62
+ - Terminal output is read from the screen, so very long lines arrive wrapped and only the last
61
63
  3,000 rows are visible. The agent receives at most 300 lines or 24,000 characters.
62
- - Run state lives in the plugin process; reloading the plugin forgets it (the cards stay).
63
- - The live panel opens its own connection to the daemon on the plugin host, using the address in
64
- `$PASEO_HOME/paseo.pid` and the local credential, as the `paseo` CLI does. A daemon listening on
65
- a Unix socket gets the plain-text panel instead.
66
- - Opening or resizing the panel resizes the PTY to fit it, as focusing a terminal tab does.
64
+ - Run state lives in the plugin process; reloading the plugin forgets it. Controls and cards stay,
65
+ and nothing is re-run.
67
66
  - A command that has not started within 20 seconds fails, for example when something else is
68
67
  running in "Agent commands".
69
68
 
70
69
  ## Develop
71
70
 
71
+ The inline code block action types are not in a published `@getpaseo/plugin` yet, so
72
+ `client/sdk-compat.ts` declares them. Delete it once the SDK exports them. Use npm 11 for
73
+ `npm install`: npm 10 crashes on this dependency tree.
74
+
72
75
  ```bash
73
76
  npm install
74
77
  npm run typecheck
@@ -77,6 +80,14 @@ paseo plugin reload terminal-integration
77
80
  paseo plugin logs terminal-integration
78
81
  ```
79
82
 
83
+ To release, bump the version and push the tag; GitHub Actions publishes it to npm through trusted
84
+ publishing, and Paseo Cafe picks up the new version from there:
85
+
86
+ ```bash
87
+ npm version patch # or minor; commits and tags vX.Y.Z
88
+ git push --follow-tags
89
+ ```
90
+
80
91
  ## License
81
92
 
82
93
  MIT
@@ -0,0 +1,367 @@
1
+ import { useRpc, useSettings } from "@getpaseo/plugin/client";
2
+ import type { PluginCodeBlockActionsProps } from "./sdk-compat";
3
+ import { Icon } from "@getpaseo/plugin/client/react-native";
4
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { useMemo } from "react";
6
+ import { Platform, Pressable, Text, View, type TextStyle } from "react-native";
7
+ import {
8
+ cancelRun,
9
+ inlineRunKey,
10
+ runStatus,
11
+ sendRunOutput,
12
+ startRun,
13
+ type RunMode,
14
+ type RunSnapshot,
15
+ } from "../shared/contracts";
16
+ import { runnableBlock } from "../shared/code-blocks";
17
+ import { preferences } from "../shared/settings";
18
+ import { revealStartedRun } from "./reveal-run";
19
+
20
+ type Theme = PluginCodeBlockActionsProps["theme"];
21
+ type Styles = ReturnType<typeof createStyles>;
22
+ const POLL_MS = 700;
23
+ const MONOSPACE = Platform.select({
24
+ ios: "Menlo",
25
+ android: "monospace",
26
+ default: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
27
+ });
28
+ interface Controls {
29
+ pending: boolean;
30
+ send: boolean | null;
31
+ error: string | null;
32
+ }
33
+ const INITIAL_CONTROLS: Controls = { pending: false, send: null, error: null };
34
+ function isActive(run: RunSnapshot): boolean {
35
+ return run.status === "queued" || run.status === "running" || run.sending;
36
+ }
37
+ export function CodeBlockActions(props: PluginCodeBlockActionsProps) {
38
+ const block = runnableBlock(props.code, props.language);
39
+ if (!block) return null;
40
+ return <RunnableActions {...props} command={block.code} />;
41
+ }
42
+ function RunnableActions({
43
+ agentId,
44
+ messageId,
45
+ blockIndex,
46
+ host,
47
+ theme,
48
+ layout,
49
+ language,
50
+ phase,
51
+ navigation,
52
+ command,
53
+ }: PluginCodeBlockActionsProps & { command: string }) {
54
+ const key = inlineRunKey(host.id, agentId, messageId, blockIndex);
55
+ const queryKey = useMemo(() => ["terminal-run-status", key], [key]);
56
+ const controlsKey = useMemo(() => ["terminal-run-controls", key], [key]);
57
+ const styles = useMemo(() => createStyles(theme, layout.compact), [theme, layout.compact]);
58
+ const queryClient = useQueryClient();
59
+ const fetchStatus = useRpc(runStatus);
60
+ const start = useRpc(startRun);
61
+ const cancel = useRpc(cancelRun);
62
+ const send = useRpc(sendRunOutput);
63
+ const settings = useSettings(preferences);
64
+ const { data: controls = INITIAL_CONTROLS } = useQuery<Controls>({
65
+ queryKey: controlsKey,
66
+ queryFn: () => INITIAL_CONTROLS,
67
+ initialData: INITIAL_CONTROLS,
68
+ enabled: false,
69
+ gcTime: Infinity,
70
+ });
71
+ const updateControls = (patch: Partial<Controls>) =>
72
+ queryClient.setQueryData<Controls>(controlsKey, (previous) => ({
73
+ ...INITIAL_CONTROLS,
74
+ ...previous,
75
+ ...patch,
76
+ }));
77
+ const sendToAgent =
78
+ controls.send ?? (settings.status === "ready" ? settings.values.sendToAgent : true);
79
+ const status = useQuery({
80
+ queryKey,
81
+ queryFn: () => fetchStatus({ keys: [key] }),
82
+ refetchInterval: (query) => (query.state.data?.runs.some(isActive) ? POLL_MS : false),
83
+ });
84
+ const run = status.data?.runs[0];
85
+ const running = run?.status === "queued" || run?.status === "running";
86
+ const disabled = controls.pending || (!!run && isActive(run)) || phase !== "complete";
87
+ async function act(action: () => Promise<unknown>) {
88
+ if (queryClient.getQueryData<Controls>(controlsKey)?.pending) return;
89
+ updateControls({ pending: true, error: null });
90
+ try {
91
+ await action();
92
+ await queryClient.invalidateQueries({ queryKey });
93
+ } catch (error) {
94
+ updateControls({ error: error instanceof Error ? error.message : String(error) });
95
+ } finally {
96
+ updateControls({ pending: false });
97
+ }
98
+ }
99
+ function runBlock(mode: RunMode) {
100
+ if (disabled) return;
101
+ void act(async () => {
102
+ const started = await start({
103
+ agentId,
104
+ key,
105
+ lang: language,
106
+ code: command,
107
+ send: sendToAgent,
108
+ mode,
109
+ });
110
+ queryClient.setQueryData(queryKey, { runs: [started] });
111
+ // Owned by this click, not by rendering or polling: remounts cannot steal focus.
112
+ void revealStartedRun(started, navigation?.openTerminal, fetchStatus).catch((error) =>
113
+ updateControls({ error: error instanceof Error ? error.message : String(error) }),
114
+ );
115
+ });
116
+ }
117
+ return (
118
+ <View style={styles.card}>
119
+ <View style={styles.actions}>
120
+ {running ? (
121
+ <ActionButton
122
+ styles={styles}
123
+ theme={theme}
124
+ icon="Square"
125
+ label="Stop"
126
+ tone="danger"
127
+ disabled={controls.pending}
128
+ onPress={() => void act(() => cancel({ key }))}
129
+ />
130
+ ) : (
131
+ <>
132
+ <ActionButton
133
+ styles={styles}
134
+ theme={theme}
135
+ icon="Play"
136
+ label="Run"
137
+ tone="primary"
138
+ disabled={disabled}
139
+ onPress={() => runBlock("terminal")}
140
+ />
141
+ <ActionButton
142
+ styles={styles}
143
+ theme={theme}
144
+ icon="EyeOff"
145
+ label="Run in background"
146
+ disabled={disabled}
147
+ onPress={() => runBlock("background")}
148
+ />
149
+ </>
150
+ )}
151
+ <Checkbox
152
+ styles={styles}
153
+ theme={theme}
154
+ label="Send output to agent"
155
+ checked={sendToAgent}
156
+ onChange={(value) => updateControls({ send: value })}
157
+ />
158
+ {run && !run.sent && !run.sending && (!running || !run.sendOnFinish) ? (
159
+ <ActionButton
160
+ styles={styles}
161
+ theme={theme}
162
+ icon="Send"
163
+ label={running ? "Send output so far" : "Send output"}
164
+ disabled={controls.pending}
165
+ onPress={() => void act(() => send({ key }))}
166
+ />
167
+ ) : null}
168
+ </View>
169
+ {phase !== "complete" ? (
170
+ <Text style={styles.muted}>Available when the response finishes</Text>
171
+ ) : null}
172
+ {controls.error ? <Text style={styles.danger}>{controls.error}</Text> : null}
173
+ {status.error ? <Text style={styles.danger}>{status.error.message}</Text> : null}
174
+ {run ? <RunStatus run={run} styles={styles} /> : null}
175
+ </View>
176
+ );
177
+ }
178
+
179
+ function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
180
+ const parts: string[] = [];
181
+ let tone: TextStyle = styles.muted;
182
+ if (run.status === "queued") parts.push("Queued, waiting for the terminal");
183
+ if (run.status === "running") {
184
+ parts.push(
185
+ `${run.mode === "background" ? "Running in background" : "Running"} · ${elapsed(run)}`,
186
+ );
187
+ }
188
+ if (run.status === "done") {
189
+ parts.push(`${run.exitCode === 0 ? "✓" : "✗"} exit ${run.exitCode}`);
190
+ tone = run.exitCode === 0 ? styles.success : styles.danger;
191
+ }
192
+ if (run.status === "canceled") parts.push("Stopped");
193
+ if (run.status === "failed") tone = styles.danger;
194
+ if (run.status !== "queued" && run.status !== "failed") {
195
+ parts.push(`${run.lineCount} ${run.lineCount === 1 ? "line" : "lines"}`);
196
+ }
197
+ if (run.sent) parts.push("sent to agent");
198
+ else if (run.sending) parts.push("sending when the agent is idle…");
199
+ else if (run.sendOnFinish && (run.status === "queued" || run.status === "running")) {
200
+ parts.push("output goes to the agent");
201
+ }
202
+
203
+ return (
204
+ <View style={styles.status}>
205
+ {parts.length > 0 ? <Text style={tone}>{parts.join(" · ")}</Text> : null}
206
+ {run.error ? <Text style={styles.danger}>{run.error}</Text> : null}
207
+ {run.tail.length > 0 ? (
208
+ <View style={styles.tail}>
209
+ {run.tail.map((line, index) => (
210
+ <Text key={index} style={styles.tailLine} numberOfLines={1}>
211
+ {line || " "}
212
+ </Text>
213
+ ))}
214
+ </View>
215
+ ) : null}
216
+ </View>
217
+ );
218
+ }
219
+
220
+ function Checkbox({
221
+ styles,
222
+ theme,
223
+ label,
224
+ checked,
225
+ onChange,
226
+ }: {
227
+ styles: Styles;
228
+ theme: Theme;
229
+ label: string;
230
+ checked: boolean;
231
+ onChange(checked: boolean): void;
232
+ }) {
233
+ return (
234
+ <Pressable
235
+ accessibilityRole="checkbox"
236
+ accessibilityLabel={label}
237
+ accessibilityState={{ checked }}
238
+ onPress={() => onChange(!checked)}
239
+ style={({ pressed }) => [styles.checkbox, { opacity: pressed ? 0.75 : 1 }]}
240
+ >
241
+ <Icon
242
+ name={checked ? "SquareCheck" : "Square"}
243
+ size={16}
244
+ color={checked ? theme.colors.accent : theme.colors.foregroundMuted}
245
+ />
246
+ <Text style={[styles.buttonText, { color: theme.colors.foreground }]}>{label}</Text>
247
+ </Pressable>
248
+ );
249
+ }
250
+
251
+ function ActionButton({
252
+ styles,
253
+ theme,
254
+ icon,
255
+ label,
256
+ tone = "default",
257
+ disabled = false,
258
+ onPress,
259
+ }: {
260
+ styles: Styles;
261
+ theme: Theme;
262
+ icon: string;
263
+ label: string;
264
+ tone?: "default" | "primary" | "danger";
265
+ disabled?: boolean;
266
+ onPress(): void;
267
+ }) {
268
+ const color =
269
+ tone === "primary"
270
+ ? theme.colors.accentForeground
271
+ : tone === "danger"
272
+ ? theme.colors.statusDanger
273
+ : theme.colors.foreground;
274
+ return (
275
+ <Pressable
276
+ accessibilityRole="button"
277
+ accessibilityLabel={label}
278
+ accessibilityState={{ disabled }}
279
+ disabled={disabled}
280
+ onPress={onPress}
281
+ style={({ pressed }) => [
282
+ styles.button,
283
+ tone === "primary" ? styles.primaryButton : null,
284
+ { opacity: disabled ? 0.5 : pressed ? 0.75 : 1 },
285
+ ]}
286
+ >
287
+ <Icon name={icon} size={14} color={color} />
288
+ <Text style={[styles.buttonText, { color }]}>{label}</Text>
289
+ </Pressable>
290
+ );
291
+ }
292
+
293
+ function elapsed(run: RunSnapshot): string {
294
+ const seconds = Math.max(0, Math.round((Date.now() - (run.startedAt ?? Date.now())) / 1000));
295
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
296
+ }
297
+
298
+ function createStyles(theme: Theme, compact: boolean) {
299
+ return {
300
+ card: {
301
+ gap: compact ? 8 : 10,
302
+ borderWidth: 1,
303
+ borderColor: theme.colors.border,
304
+ borderRadius: 10,
305
+ padding: compact ? 10 : 12,
306
+ backgroundColor: theme.colors.surface1,
307
+ },
308
+ header: {
309
+ flexDirection: "row" as const,
310
+ flexWrap: "wrap" as const,
311
+ alignItems: "center" as const,
312
+ gap: 6,
313
+ },
314
+ checkbox: {
315
+ flexDirection: "row" as const,
316
+ alignItems: "center" as const,
317
+ gap: 6,
318
+ paddingHorizontal: 4,
319
+ paddingVertical: compact ? 7 : 5,
320
+ },
321
+ title: { color: theme.colors.foreground, fontWeight: "600" as const },
322
+ muted: { color: theme.colors.foregroundMuted, flexShrink: 1 },
323
+ spacer: { flex: 1 },
324
+ success: { color: theme.colors.statusSuccess },
325
+ danger: { color: theme.colors.statusDanger },
326
+ block: { gap: 8 },
327
+ separated: {
328
+ borderTopWidth: 1,
329
+ borderTopColor: theme.colors.border,
330
+ paddingTop: compact ? 8 : 10,
331
+ },
332
+ codeRow: { flexDirection: "row" as const, gap: 8, alignItems: "flex-start" as const },
333
+ lang: {
334
+ color: theme.colors.foregroundMuted,
335
+ fontSize: 12,
336
+ paddingHorizontal: 6,
337
+ paddingVertical: 2,
338
+ borderRadius: 4,
339
+ backgroundColor: theme.colors.surface2,
340
+ overflow: "hidden" as const,
341
+ },
342
+ code: { color: theme.colors.foreground, fontFamily: MONOSPACE, fontSize: 13, flex: 1 },
343
+ actions: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 8 },
344
+ button: {
345
+ flexDirection: "row" as const,
346
+ alignItems: "center" as const,
347
+ gap: 6,
348
+ paddingHorizontal: 10,
349
+ paddingVertical: compact ? 7 : 5,
350
+ borderRadius: 6,
351
+ borderWidth: 1,
352
+ borderColor: theme.colors.border,
353
+ backgroundColor: theme.colors.surface2,
354
+ },
355
+ primaryButton: { backgroundColor: theme.colors.accent, borderColor: theme.colors.accent },
356
+ buttonText: { fontSize: 13 },
357
+ status: { gap: 6 },
358
+ tail: {
359
+ padding: 8,
360
+ borderRadius: 6,
361
+ backgroundColor: theme.colors.surface0,
362
+ borderWidth: 1,
363
+ borderColor: theme.colors.border,
364
+ },
365
+ tailLine: { color: theme.colors.foregroundMuted, fontFamily: MONOSPACE, fontSize: 12 },
366
+ };
367
+ }
@@ -0,0 +1,60 @@
1
+ import type { PluginCleanup } from "@getpaseo/plugin";
2
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
3
+ import { RunCard } from "./run-card";
4
+ import {
5
+ enableLegacyCards,
6
+ RUN_CARD_KIND,
7
+ RUN_CARD_VERSION,
8
+ runAdhoc,
9
+ runCardSchema,
10
+ scanBlocks,
11
+ } from "../shared/contracts";
12
+
13
+ // A function declaration: the host reads the default export before the module body has run.
14
+ export function contributeLegacy(client: PluginClientContext): PluginCleanup {
15
+ // COMPAT(legacyRunCards): added in the inline-actions fork, remove after 2027-04-01.
16
+ void client
17
+ .rpc(enableLegacyCards, {})
18
+ .catch((error) => console.error("Could not enable legacy terminal cards", error));
19
+ client.addTimelineRenderer({
20
+ kind: RUN_CARD_KIND,
21
+ version: RUN_CARD_VERSION,
22
+ schema: runCardSchema,
23
+ Component: RunCard,
24
+ });
25
+
26
+ client.addSlashCommand({
27
+ name: "run",
28
+ description: "Run a shell command in the workspace terminal and send its output to the agent",
29
+ argumentHint: "<command>",
30
+ context: "agent",
31
+ async onSubmit({ args, agent, rpc }) {
32
+ if (!args) throw new Error("Usage: /run <command>");
33
+ await rpc(runAdhoc, { agentId: agent.id, command: args });
34
+ },
35
+ });
36
+
37
+ async function addRunButtons(agentId: string, rpc: PluginClientContext["rpc"]) {
38
+ const { count } = await rpc(scanBlocks, { agentId });
39
+ if (count === 0) throw new Error("The latest reply has no runnable code blocks");
40
+ }
41
+
42
+ client.addSlashCommand({
43
+ name: "blocks",
44
+ description: "Add terminal run buttons for code blocks in the agent's latest reply",
45
+ argumentHint: "",
46
+ context: "agent",
47
+ onSubmit: ({ agent, rpc }) => addRunButtons(agent.id, rpc),
48
+ });
49
+
50
+ client.addCommandCenterItem({
51
+ id: "scan-latest-reply",
52
+ title: "Terminal: add run buttons to the latest reply",
53
+ icon: "SquareTerminal",
54
+ keywords: ["run", "code block", "terminal", "execute"],
55
+ context: "agent",
56
+ onSelect: ({ agent, rpc }) => addRunButtons(agent.id, rpc),
57
+ });
58
+
59
+ return () => {};
60
+ }
@@ -0,0 +1,24 @@
1
+ import type { PluginCodeBlockActionsProps } from "./sdk-compat";
2
+ import type { RunSnapshot } from "../shared/contracts";
3
+
4
+ type OpenTerminal = NonNullable<PluginCodeBlockActionsProps["navigation"]>["openTerminal"];
5
+ /** Called only by the initiating click; history mounts never acquire navigation intent. */
6
+ export async function revealStartedRun(
7
+ started: RunSnapshot,
8
+ openTerminal: OpenTerminal,
9
+ status: (input: { keys: string[] }) => Promise<{ runs: RunSnapshot[] }>,
10
+ ): Promise<void> {
11
+ if (started.mode !== "terminal") return;
12
+ if (!openTerminal)
13
+ throw new Error("Command started. Update Paseo to open its terminal automatically.");
14
+ let run = started;
15
+ while (!run.terminalId) {
16
+ if (run.status !== "queued" && run.status !== "running") return;
17
+ await new Promise((resolve) => setTimeout(resolve, 700));
18
+ const current = (await status({ keys: [run.key] })).runs[0];
19
+ if (!current || current.runId !== started.runId) return;
20
+ run = current;
21
+ }
22
+ if (run.status === "canceled" || run.status === "failed") return;
23
+ openTerminal({ workspaceId: run.workspaceId, terminalId: run.terminalId });
24
+ }