paseo-terminal-integration 0.3.0 → 0.5.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 +41 -17
- 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 +3 -1
- package/client/sdk-compat.ts +34 -0
- package/client/settings-screen.tsx +182 -19
- package/index.client.tsx +24 -55
- package/index.server.ts +26 -2
- package/instructions/client-server.md +13 -0
- package/instructions/local-daemon.md +11 -0
- package/package.json +31 -23
- package/server/attachments.ts +3 -1
- package/server/instructions.ts +28 -0
- package/server/runner.ts +10 -2
- package/shared/code-blocks.ts +13 -1
- package/shared/contracts.ts +19 -1
- package/shared/instructions.ts +31 -0
- package/shared/settings.ts +14 -2
package/README.md
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Two-way bridge between agent chats and Paseo terminals.
|
|
4
4
|
|
|
5
|
-
**Agent → terminal.**
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
8
10
|
|
|
9
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.
|
|
10
13
|
- **Run in background** runs it as a hidden process in the agent's working directory: no terminal
|
|
11
|
-
tab, and the
|
|
14
|
+
tab, and the runner keeps the last 5,000 lines instead of what fits on a terminal screen. Nothing
|
|
12
15
|
can be typed into it: stdin is empty, so password and confirmation prompts fail instead of
|
|
13
16
|
waiting. Background runs do not wait for the terminal.
|
|
14
17
|
- With **Send output to agent** checked (the default), the command, exit code, and output go to
|
|
@@ -16,17 +19,32 @@ card under the reply:
|
|
|
16
19
|
its turn ends instead of interrupting it. Unchecked, **Send output** sends it later.
|
|
17
20
|
- **Stop** sends Ctrl-C; **Send output so far** reports a still-running command.
|
|
18
21
|
|
|
19
|
-
**Terminal beside the chat.**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
new one in a new tab.
|
|
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.
|
|
25
27
|
|
|
26
28
|
Terminal runs in one workspace are queued so a command never types into another one's stdin.
|
|
27
29
|
Each block runs as a script in the agent's working directory, so `cd` and `export` do not persist
|
|
28
30
|
between runs. **Settings → Plugins → Terminal integration** sets the checkbox's default.
|
|
29
31
|
|
|
32
|
+
**Command guidance for agents.** The same settings screen can add command-formatting and execution
|
|
33
|
+
guidance to the system prompt of newly created agents. Turn on **Inject command instructions** and
|
|
34
|
+
choose one topology:
|
|
35
|
+
|
|
36
|
+
- **Local daemon** tells the agent that Run already executes on the local Paseo daemon host, so it
|
|
37
|
+
should not add SSH merely to reach that machine.
|
|
38
|
+
- **Client–server** tells the agent that the UI and daemon may be on different devices. Commands for
|
|
39
|
+
the daemon need no prefix; commands for another machine must include an explicit transport such
|
|
40
|
+
as SSH, using a destination supplied by the user rather than an invented host.
|
|
41
|
+
|
|
42
|
+
Both templates are editable and stored per host. The shipped defaults are also available as
|
|
43
|
+
[`local-daemon.md`](instructions/local-daemon.md) and
|
|
44
|
+
[`client-server.md`](instructions/client-server.md), and each editor has a restore-default action.
|
|
45
|
+
Changing or enabling the setting affects only agents created afterward; it does not rewrite the
|
|
46
|
+
system prompt of an existing agent.
|
|
47
|
+
|
|
30
48
|
**Terminal → agent.** In the composer's attachment menu, **Terminal output** attaches the latest
|
|
31
49
|
lines of any terminal on the daemon. Type part of a name or path to filter, and a number to choose
|
|
32
50
|
the line count (`build 500`; default 200).
|
|
@@ -47,23 +65,29 @@ Nothing runs until you press a button or submit `/run`.
|
|
|
47
65
|
|
|
48
66
|
## Commands
|
|
49
67
|
|
|
50
|
-
| Command
|
|
51
|
-
|
|
|
52
|
-
| `/run <command>`
|
|
53
|
-
| `/blocks`
|
|
54
|
-
| ⌘K / Ctrl+K → "Terminal: add run buttons to …"
|
|
68
|
+
| Command | Effect |
|
|
69
|
+
| ---------------------------------------------- | ------------------------------------------------------ |
|
|
70
|
+
| `/run <command>` | Run a shell command and send its output to the agent |
|
|
71
|
+
| `/blocks` | Add a run card for the latest reply (e.g. older chats) |
|
|
72
|
+
| ⌘K / Ctrl+K → "Terminal: add run buttons to …" | Same as `/blocks` |
|
|
73
|
+
|
|
74
|
+
The commands are available on Paseo versions without inline code block actions.
|
|
55
75
|
|
|
56
76
|
## Limitations
|
|
57
77
|
|
|
58
78
|
- Terminal output is read from the screen, so very long lines arrive wrapped and only the last
|
|
59
79
|
3,000 rows are visible. The agent receives at most 300 lines or 24,000 characters.
|
|
60
|
-
- Run state lives in the plugin process; reloading the plugin forgets it
|
|
61
|
-
|
|
80
|
+
- Run state lives in the plugin process; reloading the plugin forgets it. Controls and cards stay,
|
|
81
|
+
and nothing is re-run.
|
|
62
82
|
- A command that has not started within 20 seconds fails, for example when something else is
|
|
63
83
|
running in "Agent commands".
|
|
64
84
|
|
|
65
85
|
## Develop
|
|
66
86
|
|
|
87
|
+
The inline code block action types are not in a published `@getpaseo/plugin` yet, so
|
|
88
|
+
`client/sdk-compat.ts` declares them. Delete it once the SDK exports them. Use npm 11 for
|
|
89
|
+
`npm install`: npm 10 crashes on this dependency tree.
|
|
90
|
+
|
|
67
91
|
```bash
|
|
68
92
|
npm install
|
|
69
93
|
npm run typecheck
|
|
@@ -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
|
+
}
|
package/client/run-card.tsx
CHANGED
|
@@ -188,7 +188,9 @@ function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
|
188
188
|
let tone: TextStyle = styles.muted;
|
|
189
189
|
if (run.status === "queued") parts.push("Queued, waiting for the terminal");
|
|
190
190
|
if (run.status === "running") {
|
|
191
|
-
parts.push(
|
|
191
|
+
parts.push(
|
|
192
|
+
`${run.mode === "background" ? "Running in background" : "Running"} · ${elapsed(run)}`,
|
|
193
|
+
);
|
|
192
194
|
}
|
|
193
195
|
if (run.status === "done") {
|
|
194
196
|
parts.push(`${run.exitCode === 0 ? "✓" : "✗"} exit ${run.exitCode}`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// COMPAT(inlineCodeBlockActions): copies of the inline code-action SDK types until a published
|
|
2
|
+
// @getpaseo/plugin exports them; remove after 2027-04-01.
|
|
3
|
+
import type { ComponentType } from "react";
|
|
4
|
+
import type { PluginCleanup } from "@getpaseo/plugin";
|
|
5
|
+
import type { PluginClientContext, PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
6
|
+
|
|
7
|
+
type HostNavigation = NonNullable<PluginSurfaceProps["navigation"]>;
|
|
8
|
+
|
|
9
|
+
export interface PluginCodeBlockActionsProps extends Omit<PluginSurfaceProps, "navigation"> {
|
|
10
|
+
readonly navigation?: HostNavigation & {
|
|
11
|
+
readonly openTerminal?: (input: {
|
|
12
|
+
readonly workspaceId: string;
|
|
13
|
+
readonly terminalId: string;
|
|
14
|
+
readonly serverId?: string;
|
|
15
|
+
}) => void;
|
|
16
|
+
};
|
|
17
|
+
agentId: string;
|
|
18
|
+
messageId: string;
|
|
19
|
+
blockIndex: number;
|
|
20
|
+
code: string;
|
|
21
|
+
language: string;
|
|
22
|
+
phase: "streaming" | "complete";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PluginCodeBlockActionsContribution {
|
|
26
|
+
id: string;
|
|
27
|
+
languages: readonly string[];
|
|
28
|
+
Component: ComponentType<PluginCodeBlockActionsProps>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Older clients lack addCodeBlockActions; check for it before calling. */
|
|
32
|
+
export type InlineActionsClient = PluginClientContext & {
|
|
33
|
+
addCodeBlockActions?: (contribution: PluginCodeBlockActionsContribution) => PluginCleanup;
|
|
34
|
+
};
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import { useSettings, type PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
1
|
+
import { useSettings, type PluginSurfaceProps, type SettingsState } from "@getpaseo/plugin/client";
|
|
2
2
|
import {
|
|
3
|
+
ExternalLink,
|
|
4
|
+
SettingsAction,
|
|
3
5
|
SettingsCard,
|
|
4
6
|
SettingsRow,
|
|
5
7
|
SettingsSection,
|
|
8
|
+
SettingsSelect,
|
|
6
9
|
SettingsSwitch,
|
|
7
10
|
} from "@getpaseo/plugin/client/ui";
|
|
8
|
-
import {
|
|
11
|
+
import { useMemo, useState } from "react";
|
|
12
|
+
import { Text, TextInput, View } from "react-native";
|
|
9
13
|
import { TERMINAL_NAME } from "../shared/contracts";
|
|
10
|
-
import {
|
|
14
|
+
import { CLIENT_SERVER_INSTRUCTIONS, LOCAL_DAEMON_INSTRUCTIONS } from "../shared/instructions";
|
|
15
|
+
import { preferences, type InstructionTopology } from "../shared/settings";
|
|
16
|
+
|
|
17
|
+
const REPOSITORY = "https://github.com/keefeere/paseo-terminal-integration/blob/main";
|
|
18
|
+
|
|
19
|
+
type ReadySettings = Extract<SettingsState<typeof preferences.schema>, { status: "ready" }>;
|
|
11
20
|
|
|
12
21
|
export function SettingsScreen({ theme }: PluginSurfaceProps) {
|
|
13
22
|
const settings = useSettings(preferences);
|
|
@@ -17,24 +26,178 @@ export function SettingsScreen({ theme }: PluginSurfaceProps) {
|
|
|
17
26
|
settings.status === "loading" ? "Loading…" : `Settings are unavailable: ${settings.error}`;
|
|
18
27
|
return <Text style={{ color: theme.colors.foregroundMuted }}>{message}</Text>;
|
|
19
28
|
}
|
|
29
|
+
|
|
30
|
+
return <ReadySettingsScreen key={settings.revision} settings={settings} theme={theme} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ReadySettingsScreen({
|
|
34
|
+
settings,
|
|
35
|
+
theme,
|
|
36
|
+
}: {
|
|
37
|
+
settings: ReadySettings;
|
|
38
|
+
theme: PluginSurfaceProps["theme"];
|
|
39
|
+
}) {
|
|
20
40
|
const { values, revision } = settings;
|
|
41
|
+
const [draft, setDraft] = useState(values);
|
|
42
|
+
const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(values), [draft, values]);
|
|
43
|
+
const editorStyle = useMemo(
|
|
44
|
+
() => ({
|
|
45
|
+
minHeight: 220,
|
|
46
|
+
paddingHorizontal: 12,
|
|
47
|
+
paddingVertical: 10,
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderColor: theme.colors.border,
|
|
50
|
+
borderRadius: 8,
|
|
51
|
+
color: theme.colors.foreground,
|
|
52
|
+
backgroundColor: theme.colors.surface0,
|
|
53
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
54
|
+
fontSize: 12,
|
|
55
|
+
lineHeight: 18,
|
|
56
|
+
textAlignVertical: "top" as const,
|
|
57
|
+
}),
|
|
58
|
+
[theme],
|
|
59
|
+
);
|
|
60
|
+
const linkStyle = useMemo(() => ({ color: theme.colors.accent, fontSize: 12 }), [theme]);
|
|
61
|
+
|
|
62
|
+
const updateTopology = (instructionTopology: InstructionTopology) =>
|
|
63
|
+
setDraft((current) => ({ ...current, instructionTopology }));
|
|
21
64
|
|
|
22
65
|
return (
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
<>
|
|
67
|
+
<SettingsSection title="Code block actions">
|
|
68
|
+
<SettingsCard>
|
|
69
|
+
<SettingsSwitch
|
|
70
|
+
label="Send output to the agent by default"
|
|
71
|
+
hint={`Initial state of the "Send output to agent" checkbox on each code block.`}
|
|
72
|
+
value={values.sendToAgent}
|
|
73
|
+
disabled={settings.saving}
|
|
74
|
+
error={settings.saveError}
|
|
75
|
+
onValueChange={(sendToAgent) =>
|
|
76
|
+
void settings.save({ ...values, sendToAgent }, revision)
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
<SettingsRow
|
|
80
|
+
label="Where the terminal appears"
|
|
81
|
+
hint={`Commands run in the "${TERMINAL_NAME}" terminal tab. Choose Main panel or On the side in Settings → Layout → Open location → Terminals. Manually moved tabs keep their location.`}
|
|
82
|
+
/>
|
|
83
|
+
</SettingsCard>
|
|
84
|
+
</SettingsSection>
|
|
85
|
+
|
|
86
|
+
<SettingsSection title="Agent command instructions">
|
|
87
|
+
<SettingsCard>
|
|
88
|
+
<SettingsSwitch
|
|
89
|
+
label="Inject command instructions"
|
|
90
|
+
hint="Add the selected guidance to the system prompt of newly created agents. Existing agents are unchanged."
|
|
91
|
+
value={draft.injectCommandInstructions}
|
|
92
|
+
disabled={settings.saving}
|
|
93
|
+
onValueChange={(injectCommandInstructions) =>
|
|
94
|
+
setDraft((current) => ({ ...current, injectCommandInstructions }))
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
<SettingsSelect
|
|
98
|
+
label="Execution topology"
|
|
99
|
+
hint="Choose where clicking Run actually executes a command."
|
|
100
|
+
value={draft.instructionTopology}
|
|
101
|
+
options={[
|
|
102
|
+
{ label: "Local daemon", value: "local-daemon" },
|
|
103
|
+
{ label: "Client–server", value: "client-server" },
|
|
104
|
+
]}
|
|
105
|
+
disabled={settings.saving}
|
|
106
|
+
onValueChange={updateTopology}
|
|
107
|
+
/>
|
|
108
|
+
</SettingsCard>
|
|
109
|
+
</SettingsSection>
|
|
110
|
+
|
|
111
|
+
<SettingsSection title="Local daemon instructions">
|
|
112
|
+
<SettingsCard>
|
|
113
|
+
<SettingsRow
|
|
114
|
+
label="Instruction text"
|
|
115
|
+
hint="Used when Execution topology is Local daemon. Edit the host-scoped copy below."
|
|
116
|
+
>
|
|
117
|
+
<View style={{ gap: 8, width: "100%" }}>
|
|
118
|
+
<ExternalLink href={`${REPOSITORY}/instructions/local-daemon.md`}>
|
|
119
|
+
<Text style={linkStyle}>View shipped local-daemon.md</Text>
|
|
120
|
+
</ExternalLink>
|
|
121
|
+
<TextInput
|
|
122
|
+
multiline
|
|
123
|
+
accessibilityLabel="Local daemon command instructions"
|
|
124
|
+
value={draft.localDaemonInstructions}
|
|
125
|
+
editable={!settings.saving}
|
|
126
|
+
onChangeText={(localDaemonInstructions) =>
|
|
127
|
+
setDraft((current) => ({ ...current, localDaemonInstructions }))
|
|
128
|
+
}
|
|
129
|
+
style={editorStyle}
|
|
130
|
+
/>
|
|
131
|
+
</View>
|
|
132
|
+
</SettingsRow>
|
|
133
|
+
<SettingsAction
|
|
134
|
+
label="Restore local template"
|
|
135
|
+
hint="Replace the draft with the version shipped by the plugin."
|
|
136
|
+
actionLabel="Restore default"
|
|
137
|
+
disabled={
|
|
138
|
+
settings.saving || draft.localDaemonInstructions === LOCAL_DAEMON_INSTRUCTIONS
|
|
139
|
+
}
|
|
140
|
+
onPress={() =>
|
|
141
|
+
setDraft((current) => ({
|
|
142
|
+
...current,
|
|
143
|
+
localDaemonInstructions: LOCAL_DAEMON_INSTRUCTIONS,
|
|
144
|
+
}))
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
</SettingsCard>
|
|
148
|
+
</SettingsSection>
|
|
149
|
+
|
|
150
|
+
<SettingsSection title="Client–server instructions">
|
|
151
|
+
<SettingsCard>
|
|
152
|
+
<SettingsRow
|
|
153
|
+
label="Instruction text"
|
|
154
|
+
hint="Used when Execution topology is Client–server. Edit the host-scoped copy below."
|
|
155
|
+
>
|
|
156
|
+
<View style={{ gap: 8, width: "100%" }}>
|
|
157
|
+
<ExternalLink href={`${REPOSITORY}/instructions/client-server.md`}>
|
|
158
|
+
<Text style={linkStyle}>View shipped client-server.md</Text>
|
|
159
|
+
</ExternalLink>
|
|
160
|
+
<TextInput
|
|
161
|
+
multiline
|
|
162
|
+
accessibilityLabel="Client-server command instructions"
|
|
163
|
+
value={draft.clientServerInstructions}
|
|
164
|
+
editable={!settings.saving}
|
|
165
|
+
onChangeText={(clientServerInstructions) =>
|
|
166
|
+
setDraft((current) => ({ ...current, clientServerInstructions }))
|
|
167
|
+
}
|
|
168
|
+
style={editorStyle}
|
|
169
|
+
/>
|
|
170
|
+
</View>
|
|
171
|
+
</SettingsRow>
|
|
172
|
+
<SettingsAction
|
|
173
|
+
label="Restore client–server template"
|
|
174
|
+
hint="Replace the draft with the version shipped by the plugin."
|
|
175
|
+
actionLabel="Restore default"
|
|
176
|
+
disabled={
|
|
177
|
+
settings.saving || draft.clientServerInstructions === CLIENT_SERVER_INSTRUCTIONS
|
|
178
|
+
}
|
|
179
|
+
onPress={() =>
|
|
180
|
+
setDraft((current) => ({
|
|
181
|
+
...current,
|
|
182
|
+
clientServerInstructions: CLIENT_SERVER_INSTRUCTIONS,
|
|
183
|
+
}))
|
|
184
|
+
}
|
|
185
|
+
/>
|
|
186
|
+
</SettingsCard>
|
|
187
|
+
</SettingsSection>
|
|
188
|
+
|
|
189
|
+
<SettingsSection title="Save instruction settings">
|
|
190
|
+
<SettingsCard>
|
|
191
|
+
<SettingsAction
|
|
192
|
+
label="Apply to newly created agents"
|
|
193
|
+
hint="Saving does not modify agents that already exist."
|
|
194
|
+
error={settings.saveError}
|
|
195
|
+
actionLabel={settings.saving ? "Saving…" : "Save changes"}
|
|
196
|
+
disabled={settings.saving || !dirty}
|
|
197
|
+
onPress={() => void settings.save(draft, revision)}
|
|
198
|
+
/>
|
|
199
|
+
</SettingsCard>
|
|
200
|
+
</SettingsSection>
|
|
201
|
+
</>
|
|
39
202
|
);
|
|
40
203
|
}
|
package/index.client.tsx
CHANGED
|
@@ -1,66 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { contributeLegacy } from "./client/legacy";
|
|
1
3
|
import type { PluginCleanup } from "@getpaseo/plugin";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
+
import type { InlineActionsClient } from "./client/sdk-compat";
|
|
5
|
+
import { CodeBlockActions } from "./client/code-block-actions";
|
|
4
6
|
import { SettingsScreen } from "./client/settings-screen";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
import { RUNNABLE_LANGUAGES } from "./shared/code-blocks";
|
|
8
|
+
import { terminalOutputSource } from "./shared/contracts";
|
|
9
|
+
|
|
10
|
+
export default function contribute(client: InlineActionsClient): PluginCleanup {
|
|
11
|
+
if (typeof client.addCodeBlockActions === "function") {
|
|
12
|
+
client.addCodeBlockActions({
|
|
13
|
+
id: "run",
|
|
14
|
+
languages: RUNNABLE_LANGUAGES,
|
|
15
|
+
Component: CodeBlockActions,
|
|
16
|
+
});
|
|
17
|
+
// COMPAT(legacyRunCards): added in the inline-actions fork, remove after 2027-04-01.
|
|
18
|
+
client.addTimelineRenderer({
|
|
19
|
+
kind: "terminal-run-card",
|
|
20
|
+
version: 1,
|
|
21
|
+
schema: z.unknown(),
|
|
22
|
+
Component: () => null,
|
|
23
|
+
});
|
|
24
|
+
} else {
|
|
25
|
+
contributeLegacy(client);
|
|
26
|
+
}
|
|
23
27
|
client.addSettingsScreen({
|
|
24
28
|
id: "preferences",
|
|
25
29
|
title: "Terminal integration",
|
|
26
30
|
icon: "SquareTerminal",
|
|
27
31
|
Component: SettingsScreen,
|
|
28
32
|
});
|
|
29
|
-
|
|
30
33
|
client.addAttachmentSource(terminalOutputSource);
|
|
31
|
-
|
|
32
|
-
client.addSlashCommand({
|
|
33
|
-
name: "run",
|
|
34
|
-
description: "Run a shell command in the workspace terminal and send its output to the agent",
|
|
35
|
-
argumentHint: "<command>",
|
|
36
|
-
context: "agent",
|
|
37
|
-
async onSubmit({ args, agent, rpc }) {
|
|
38
|
-
if (!args) throw new Error("Usage: /run <command>");
|
|
39
|
-
await rpc(runAdhoc, { agentId: agent.id, command: args });
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
async function addRunButtons(agentId: string, rpc: PluginClientContext["rpc"]) {
|
|
44
|
-
const { count } = await rpc(scanBlocks, { agentId });
|
|
45
|
-
if (count === 0) throw new Error("The latest reply has no runnable code blocks");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
client.addSlashCommand({
|
|
49
|
-
name: "blocks",
|
|
50
|
-
description: "Add terminal run buttons for code blocks in the agent's latest reply",
|
|
51
|
-
argumentHint: "",
|
|
52
|
-
context: "agent",
|
|
53
|
-
onSubmit: ({ agent, rpc }) => addRunButtons(agent.id, rpc),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
client.addCommandCenterItem({
|
|
57
|
-
id: "scan-latest-reply",
|
|
58
|
-
title: "Terminal: add run buttons to the latest reply",
|
|
59
|
-
icon: "SquareTerminal",
|
|
60
|
-
keywords: ["run", "code block", "terminal", "execute"],
|
|
61
|
-
context: "agent",
|
|
62
|
-
onSelect: ({ agent, rpc }) => addRunButtons(agent.id, rpc),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
34
|
return () => {};
|
|
66
35
|
}
|
package/index.server.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
2
|
import { searchTerminals } from "./server/attachments";
|
|
3
3
|
import { appendCardForTurn, appendRunCard, scanLatestReply } from "./server/cards";
|
|
4
|
+
import { injectCommandInstructions, selectedCommandInstructions } from "./server/instructions";
|
|
4
5
|
import { cleanupScripts, TerminalRunner } from "./server/runner";
|
|
5
6
|
import { preferences } from "./shared/settings";
|
|
6
7
|
import {
|
|
8
|
+
enableLegacyCards,
|
|
7
9
|
cancelRun,
|
|
8
10
|
runAdhoc,
|
|
9
11
|
runKey,
|
|
@@ -15,12 +17,34 @@ import {
|
|
|
15
17
|
} from "./shared/contracts";
|
|
16
18
|
|
|
17
19
|
export default function contribute(server: PluginServerContext) {
|
|
18
|
-
server.registerSettings(preferences);
|
|
20
|
+
const settings = server.registerSettings(preferences);
|
|
19
21
|
let runner: TerminalRunner | null = null;
|
|
20
22
|
const runnerFor = (paseo: ConstructorParameters<typeof TerminalRunner>[0]) =>
|
|
21
23
|
(runner ??= new TerminalRunner(paseo));
|
|
22
24
|
|
|
23
|
-
server.
|
|
25
|
+
server.before("agent.create", async ({ request }) => {
|
|
26
|
+
const state = await settings.read();
|
|
27
|
+
if (state.status !== "ready") return;
|
|
28
|
+
const instructions = selectedCommandInstructions(state.values);
|
|
29
|
+
if (!instructions) return;
|
|
30
|
+
return {
|
|
31
|
+
...request,
|
|
32
|
+
config: {
|
|
33
|
+
...request.config,
|
|
34
|
+
systemPrompt: injectCommandInstructions(request.config.systemPrompt, instructions),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// COMPAT(legacyRunCards): only old clients opt into scanning; remove after 2027-04-01.
|
|
40
|
+
let legacyCardsRequested = false;
|
|
41
|
+
server.handle(enableLegacyCards, async () => {
|
|
42
|
+
legacyCardsRequested = true;
|
|
43
|
+
return { enabled: true };
|
|
44
|
+
});
|
|
45
|
+
server.on("agent.turn_ended", (event, { paseo }) => {
|
|
46
|
+
if (legacyCardsRequested) return appendCardForTurn(paseo, event);
|
|
47
|
+
});
|
|
24
48
|
|
|
25
49
|
server.handle(startRun, (input, { paseo }) => runnerFor(paseo).start(input));
|
|
26
50
|
server.handle(runStatus, ({ keys }, { paseo }) => ({ runs: runnerFor(paseo).status(keys) }));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Paseo Terminal Integration is installed in a client-server setup. The chat UI may be on a different device, but clicking Run never executes on that client device. It executes on this agent's Paseo daemon host, in this agent's working directory, under the daemon user's account.
|
|
2
|
+
|
|
3
|
+
When you offer commands that the user may want to run:
|
|
4
|
+
|
|
5
|
+
- Put each directly executable command in a fenced code block tagged with its interpreter, such as bash, sh, python, or node.
|
|
6
|
+
- Keep explanations, alternatives, warnings, and expected output outside executable code blocks.
|
|
7
|
+
- Make each executable block self-contained. Do not include a shell prompt such as "$ ", prose, or sample output in it.
|
|
8
|
+
- State clearly which machine a command targets whenever that may be ambiguous. Never describe Run as executing on the user's phone, browser, or desktop client.
|
|
9
|
+
- A command for the daemon host needs no transport prefix: clicking Run already executes it there.
|
|
10
|
+
- A command for another machine must include the transport explicitly, normally SSH from the daemon host. Use an SSH destination already provided by the user; do not invent a hostname, username, path, or credential. If the destination is unknown, explain what is needed instead of presenting a falsely runnable command.
|
|
11
|
+
- Quote remote commands so that expansion happens on the intended machine. Account for differences between the agent working directory and the remote working directory.
|
|
12
|
+
- Distinguish commands the user may run from commands you already ran. Do not claim that a suggested command has executed.
|
|
13
|
+
- Use Run in the terminal for commands that may need a password, confirmation, or other input. Background runs have no stdin.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Paseo Terminal Integration is installed on this agent's daemon host.
|
|
2
|
+
|
|
3
|
+
When you offer commands that the user may want to run:
|
|
4
|
+
|
|
5
|
+
- Put each directly executable command in a fenced code block tagged with its interpreter, such as bash, sh, python, or node.
|
|
6
|
+
- Keep explanations, alternatives, warnings, and expected output outside executable code blocks.
|
|
7
|
+
- Make each executable block self-contained. Do not include a shell prompt such as "$ ", prose, or sample output in it.
|
|
8
|
+
- Assume that clicking Run executes the block on the same machine as the Paseo daemon, in this agent's working directory, under the daemon user's account.
|
|
9
|
+
- Distinguish commands the user may run from commands you already ran. Do not claim that a suggested command has executed.
|
|
10
|
+
- Use Run in the terminal for commands that may need a password, confirmation, or other input. Background runs have no stdin.
|
|
11
|
+
- Do not add SSH merely to reach the daemon host: the command already runs there.
|
package/package.json
CHANGED
|
@@ -1,42 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-terminal-integration",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Run agent code blocks in a Paseo terminal and send the output back to the agent; attach terminal output to prompts",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agents",
|
|
7
|
+
"paseo",
|
|
8
|
+
"paseo-plugin",
|
|
9
|
+
"terminal"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/keefeere/paseo-terminal-integration#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/keefeere/paseo-terminal-integration/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Chechulin Serhii",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/keefeere/paseo-terminal-integration.git"
|
|
20
|
+
},
|
|
4
21
|
"files": [
|
|
5
22
|
"paseo-plugin.json",
|
|
6
23
|
"index.client.tsx",
|
|
7
24
|
"index.server.ts",
|
|
8
25
|
"client/",
|
|
26
|
+
"instructions/",
|
|
9
27
|
"server/",
|
|
10
|
-
"shared/"
|
|
28
|
+
"shared/",
|
|
29
|
+
"!**/*.test.ts"
|
|
11
30
|
],
|
|
12
31
|
"scripts": {
|
|
13
|
-
"typecheck": "tsc --noEmit"
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"lint": "oxlint",
|
|
35
|
+
"format": "oxfmt ."
|
|
14
36
|
},
|
|
15
37
|
"devDependencies": {
|
|
16
|
-
"@getpaseo/plugin": "0.10.
|
|
38
|
+
"@getpaseo/plugin": "0.10.2",
|
|
17
39
|
"@tanstack/react-query": "^5.90.11",
|
|
18
40
|
"@types/node": "^26.6.2",
|
|
19
41
|
"@types/react": "~19.2.0",
|
|
42
|
+
"oxfmt": "0.46.0",
|
|
43
|
+
"oxlint": "1.61.0",
|
|
20
44
|
"react": "19.1.0",
|
|
21
45
|
"react-native": "0.81.5",
|
|
22
46
|
"typescript": "^5.9.3",
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"license": "MIT",
|
|
27
|
-
"author": "Chechulin Serhii",
|
|
28
|
-
"repository": {
|
|
29
|
-
"type": "git",
|
|
30
|
-
"url": "git+https://github.com/keefeere/paseo-terminal-integration.git"
|
|
31
|
-
},
|
|
32
|
-
"homepage": "https://github.com/keefeere/paseo-terminal-integration#readme",
|
|
33
|
-
"bugs": {
|
|
34
|
-
"url": "https://github.com/keefeere/paseo-terminal-integration/issues"
|
|
35
|
-
},
|
|
36
|
-
"keywords": [
|
|
37
|
-
"paseo",
|
|
38
|
-
"paseo-plugin",
|
|
39
|
-
"terminal",
|
|
40
|
-
"agents"
|
|
41
|
-
]
|
|
47
|
+
"vitest": "^4.1.7",
|
|
48
|
+
"zod": "4.4.3"
|
|
49
|
+
}
|
|
42
50
|
}
|
package/server/attachments.ts
CHANGED
|
@@ -32,7 +32,9 @@ export async function searchTerminals(
|
|
|
32
32
|
|
|
33
33
|
const items = await Promise.all(
|
|
34
34
|
matches.map(async (entry) => {
|
|
35
|
-
const capture = await paseo.terminals
|
|
35
|
+
const capture = await paseo.terminals
|
|
36
|
+
.ref(entry)
|
|
37
|
+
.capture({ start: -lineCount, stripAnsi: true });
|
|
36
38
|
const lines = trimBlankEdges(withoutRunMarkers(capture.lines));
|
|
37
39
|
const name = entry.name || "Terminal";
|
|
38
40
|
const workspace = workspaceNames.get(entry.workspaceId);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { INSTRUCTION_PROMPT_MARKER } from "../shared/instructions";
|
|
2
|
+
import type { InstructionTopology } from "../shared/settings";
|
|
3
|
+
|
|
4
|
+
export interface CommandInstructionPreferences {
|
|
5
|
+
injectCommandInstructions: boolean;
|
|
6
|
+
instructionTopology: InstructionTopology;
|
|
7
|
+
localDaemonInstructions: string;
|
|
8
|
+
clientServerInstructions: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function selectedCommandInstructions(values: CommandInstructionPreferences): string | null {
|
|
12
|
+
if (!values.injectCommandInstructions) return null;
|
|
13
|
+
const text =
|
|
14
|
+
values.instructionTopology === "local-daemon"
|
|
15
|
+
? values.localDaemonInstructions
|
|
16
|
+
: values.clientServerInstructions;
|
|
17
|
+
return text.trim() || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function injectCommandInstructions(
|
|
21
|
+
systemPrompt: string | undefined,
|
|
22
|
+
instructions: string,
|
|
23
|
+
): string {
|
|
24
|
+
const existing = systemPrompt?.trim() ?? "";
|
|
25
|
+
if (existing.includes(INSTRUCTION_PROMPT_MARKER)) return existing;
|
|
26
|
+
const addition = `<${INSTRUCTION_PROMPT_MARKER}>\n${instructions.trim()}\n</${INSTRUCTION_PROMPT_MARKER}>`;
|
|
27
|
+
return existing ? `${existing}\n\n${addition}` : addition;
|
|
28
|
+
}
|
package/server/runner.ts
CHANGED
|
@@ -130,7 +130,9 @@ export class TerminalRunner {
|
|
|
130
130
|
// Background runs never touch the terminal, so they skip its queue.
|
|
131
131
|
void this.execute(run);
|
|
132
132
|
} else {
|
|
133
|
-
const lane = (this.lanes.get(run.workspaceId) ?? Promise.resolve()).then(() =>
|
|
133
|
+
const lane = (this.lanes.get(run.workspaceId) ?? Promise.resolve()).then(() =>
|
|
134
|
+
this.execute(run),
|
|
135
|
+
);
|
|
134
136
|
this.lanes.set(run.workspaceId, lane);
|
|
135
137
|
}
|
|
136
138
|
return snapshot(run);
|
|
@@ -380,6 +382,8 @@ function finishedCleanly(run: Run): boolean {
|
|
|
380
382
|
function snapshot(run: Run): RunSnapshot {
|
|
381
383
|
return {
|
|
382
384
|
runId: run.id,
|
|
385
|
+
workspaceId: run.workspaceId,
|
|
386
|
+
terminalId: run.terminal?.id ?? null,
|
|
383
387
|
key: run.key,
|
|
384
388
|
mode: run.mode,
|
|
385
389
|
status: run.status,
|
|
@@ -402,7 +406,8 @@ function snapshot(run: Run): RunSnapshot {
|
|
|
402
406
|
async function writeScripts(run: Run): Promise<string> {
|
|
403
407
|
const codeFile = await writeCode(run);
|
|
404
408
|
const wrapper = path.join(SCRIPT_DIR, `${run.id}.run.sh`);
|
|
405
|
-
const shell =
|
|
409
|
+
const shell =
|
|
410
|
+
run.interpreter === "bash" || run.interpreter === "zsh" || run.interpreter === "fish";
|
|
406
411
|
const echo = shell
|
|
407
412
|
? `sed 's/^/$ /' -- ${shellQuote(codeFile)}`
|
|
408
413
|
: `printf '# %s\\n' ${shellQuote(run.interpreter)}; cat -- ${shellQuote(codeFile)}`;
|
|
@@ -506,7 +511,10 @@ function signalGroup(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
|
506
511
|
}
|
|
507
512
|
}
|
|
508
513
|
|
|
514
|
+
// Terminal output cleaning deliberately matches ANSI and control bytes.
|
|
515
|
+
// oxlint-disable-next-line no-control-regex
|
|
509
516
|
const ANSI_ESCAPE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?|[@-Z\\-_])/g;
|
|
517
|
+
// oxlint-disable-next-line no-control-regex
|
|
510
518
|
const CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
511
519
|
|
|
512
520
|
/** Program output as a terminal would show it, without colors: `\r` rewrites the current line. */
|
package/shared/code-blocks.ts
CHANGED
|
@@ -73,7 +73,8 @@ export function runnableBlocks(markdown: string): CodeBlock[] {
|
|
|
73
73
|
function normalizeCommand(block: CodeBlock): string {
|
|
74
74
|
const lines = block.code.split("\n");
|
|
75
75
|
const prompted = lines.filter((line) => /^\s*\$ /.test(line));
|
|
76
|
-
const allPrompted =
|
|
76
|
+
const allPrompted =
|
|
77
|
+
prompted.length > 0 && prompted.length === lines.filter((l) => l.trim()).length;
|
|
77
78
|
if (!PROMPT_LANGUAGES.has(block.lang) && !allPrompted) return block.code;
|
|
78
79
|
if (prompted.length === 0) return block.code;
|
|
79
80
|
return prompted.map((line) => line.replace(/^\s*\$ /, "")).join("\n");
|
|
@@ -84,3 +85,14 @@ function stripIndent(line: string, indent: number): string {
|
|
|
84
85
|
while (index < indent && line[index] === " ") index++;
|
|
85
86
|
return line.slice(index);
|
|
86
87
|
}
|
|
88
|
+
|
|
89
|
+
export const RUNNABLE_LANGUAGES = Object.keys(INTERPRETERS);
|
|
90
|
+
|
|
91
|
+
/** Receives one already-parsed fence from Paseo, never scans Markdown. */
|
|
92
|
+
export function runnableBlock(code: string, language: string): CodeBlock | null {
|
|
93
|
+
const lang = language.toLowerCase();
|
|
94
|
+
if (!interpreterFor(lang)) return null;
|
|
95
|
+
const normalized = normalizeCommand({ code, lang }).trim();
|
|
96
|
+
if (!normalized || normalized.length > 8_000) return null;
|
|
97
|
+
return { lang, code: normalized };
|
|
98
|
+
}
|
package/shared/contracts.ts
CHANGED
|
@@ -3,6 +3,16 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
export const TERMINAL_NAME = "Agent commands";
|
|
5
5
|
|
|
6
|
+
export function inlineRunKey(
|
|
7
|
+
hostId: string,
|
|
8
|
+
agentId: string,
|
|
9
|
+
messageId: string,
|
|
10
|
+
blockIndex: number,
|
|
11
|
+
): string {
|
|
12
|
+
return JSON.stringify([hostId, agentId, messageId, blockIndex]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// COMPAT(legacyRunCards): added in the inline-actions fork, remove after 2027-04-01.
|
|
6
16
|
export const RUN_CARD_KIND = "terminal-run-card";
|
|
7
17
|
export const RUN_CARD_VERSION = 1;
|
|
8
18
|
|
|
@@ -22,6 +32,8 @@ export type RunMode = (typeof runModes)[number];
|
|
|
22
32
|
|
|
23
33
|
export const runSnapshotSchema = z.object({
|
|
24
34
|
runId: z.string(),
|
|
35
|
+
workspaceId: z.string(),
|
|
36
|
+
terminalId: z.string().nullable(),
|
|
25
37
|
key: z.string(),
|
|
26
38
|
mode: z.enum(runModes),
|
|
27
39
|
status: z.enum(["queued", "running", "done", "canceled", "failed"]),
|
|
@@ -43,7 +55,7 @@ export const startRun = defineRpc({
|
|
|
43
55
|
agentId: z.string(),
|
|
44
56
|
key: z.string(),
|
|
45
57
|
lang: z.string(),
|
|
46
|
-
code: z.string().min(1),
|
|
58
|
+
code: z.string().min(1).max(8_000),
|
|
47
59
|
send: z.boolean(),
|
|
48
60
|
mode: z.enum(runModes),
|
|
49
61
|
}),
|
|
@@ -80,6 +92,12 @@ export const scanBlocks = defineRpc({
|
|
|
80
92
|
output: z.object({ count: z.number().int() }),
|
|
81
93
|
});
|
|
82
94
|
|
|
95
|
+
export const enableLegacyCards = defineRpc({
|
|
96
|
+
name: "legacy.cards.enable",
|
|
97
|
+
input: z.object({}),
|
|
98
|
+
output: z.object({ enabled: z.boolean() }),
|
|
99
|
+
});
|
|
100
|
+
|
|
83
101
|
export const searchTerminalOutput = defineRpc({
|
|
84
102
|
name: "terminals.search",
|
|
85
103
|
input: z.object({ query: z.string() }),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const LOCAL_DAEMON_INSTRUCTIONS = `
|
|
2
|
+
Paseo Terminal Integration is installed on this agent's daemon host.
|
|
3
|
+
|
|
4
|
+
When you offer commands that the user may want to run:
|
|
5
|
+
|
|
6
|
+
- Put each directly executable command in a fenced code block tagged with its interpreter, such as bash, sh, python, or node.
|
|
7
|
+
- Keep explanations, alternatives, warnings, and expected output outside executable code blocks.
|
|
8
|
+
- Make each executable block self-contained. Do not include a shell prompt such as "$ ", prose, or sample output in it.
|
|
9
|
+
- Assume that clicking Run executes the block on the same machine as the Paseo daemon, in this agent's working directory, under the daemon user's account.
|
|
10
|
+
- Distinguish commands the user may run from commands you already ran. Do not claim that a suggested command has executed.
|
|
11
|
+
- Use Run in the terminal for commands that may need a password, confirmation, or other input. Background runs have no stdin.
|
|
12
|
+
- Do not add SSH merely to reach the daemon host: the command already runs there.
|
|
13
|
+
`.trim();
|
|
14
|
+
|
|
15
|
+
export const CLIENT_SERVER_INSTRUCTIONS = `
|
|
16
|
+
Paseo Terminal Integration is installed in a client-server setup. The chat UI may be on a different device, but clicking Run never executes on that client device. It executes on this agent's Paseo daemon host, in this agent's working directory, under the daemon user's account.
|
|
17
|
+
|
|
18
|
+
When you offer commands that the user may want to run:
|
|
19
|
+
|
|
20
|
+
- Put each directly executable command in a fenced code block tagged with its interpreter, such as bash, sh, python, or node.
|
|
21
|
+
- Keep explanations, alternatives, warnings, and expected output outside executable code blocks.
|
|
22
|
+
- Make each executable block self-contained. Do not include a shell prompt such as "$ ", prose, or sample output in it.
|
|
23
|
+
- State clearly which machine a command targets whenever that may be ambiguous. Never describe Run as executing on the user's phone, browser, or desktop client.
|
|
24
|
+
- A command for the daemon host needs no transport prefix: clicking Run already executes it there.
|
|
25
|
+
- A command for another machine must include the transport explicitly, normally SSH from the daemon host. Use an SSH destination already provided by the user; do not invent a hostname, username, path, or credential. If the destination is unknown, explain what is needed instead of presenting a falsely runnable command.
|
|
26
|
+
- Quote remote commands so that expansion happens on the intended machine. Account for differences between the agent working directory and the remote working directory.
|
|
27
|
+
- Distinguish commands the user may run from commands you already ran. Do not claim that a suggested command has executed.
|
|
28
|
+
- Use Run in the terminal for commands that may need a password, confirmation, or other input. Background runs have no stdin.
|
|
29
|
+
`.trim();
|
|
30
|
+
|
|
31
|
+
export const INSTRUCTION_PROMPT_MARKER = "paseo-terminal-integration-command-guidance";
|
package/shared/settings.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { defineSettings } from "@getpaseo/plugin";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { CLIENT_SERVER_INSTRUCTIONS, LOCAL_DAEMON_INSTRUCTIONS } from "./instructions";
|
|
4
|
+
|
|
5
|
+
export const instructionTopologies = ["local-daemon", "client-server"] as const;
|
|
6
|
+
export type InstructionTopology = (typeof instructionTopologies)[number];
|
|
3
7
|
|
|
4
8
|
export const preferences = defineSettings({
|
|
5
9
|
id: "preferences",
|
|
6
10
|
scope: "host",
|
|
7
|
-
version:
|
|
11
|
+
version: 2,
|
|
8
12
|
schema: z.object({
|
|
9
|
-
/** Initial state of a
|
|
13
|
+
/** Initial state of a code block's "Send output to agent" checkbox. */
|
|
10
14
|
sendToAgent: z.boolean().default(true),
|
|
15
|
+
/** Add command-formatting and execution-topology guidance to newly created agents. */
|
|
16
|
+
injectCommandInstructions: z.boolean().default(false),
|
|
17
|
+
instructionTopology: z.enum(instructionTopologies).default("client-server"),
|
|
18
|
+
localDaemonInstructions: z.string().default(LOCAL_DAEMON_INSTRUCTIONS),
|
|
19
|
+
clientServerInstructions: z.string().default(CLIENT_SERVER_INSTRUCTIONS),
|
|
11
20
|
}),
|
|
21
|
+
migrate(values) {
|
|
22
|
+
return values;
|
|
23
|
+
},
|
|
12
24
|
});
|