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 +48 -37
- package/client/code-block-actions.tsx +367 -0
- package/client/legacy.tsx +60 -0
- package/client/reveal-run.ts +24 -0
- package/client/run-card.tsx +20 -34
- package/client/sdk-compat.ts +34 -0
- package/client/settings-screen.tsx +7 -20
- package/index.client.tsx +26 -90
- package/index.server.ts +19 -17
- package/package.json +29 -28
- package/server/attachments.ts +3 -1
- package/server/runner.ts +183 -17
- package/shared/code-blocks.ts +13 -1
- package/shared/contracts.ts +26 -30
- package/shared/settings.ts +1 -8
- package/shared/text.ts +1 -1
- package/client/panel-opener.ts +0 -29
- package/client/terminal-panel.tsx +0 -273
- package/client/xterm-modules.d.ts +0 -13
- package/client/xterm-view.tsx +0 -222
- package/server/streams.ts +0 -228
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.**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
52
|
-
|
|
|
53
|
-
| `/run <command>`
|
|
54
|
-
| `/blocks`
|
|
55
|
-
| ⌘K / Ctrl+K → "Terminal: add run buttons to …"
|
|
56
|
-
|
|
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
|
-
-
|
|
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
|
|
63
|
-
|
|
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
|
+
}
|