paseo-terminal-integration 0.4.0 → 0.5.1
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/OVERVIEW.md +20 -0
- package/README.md +18 -0
- package/client/run-report.tsx +143 -0
- package/client/settings-screen.tsx +204 -19
- package/index.client.tsx +12 -0
- package/index.server.ts +16 -1
- package/instructions/client-server.md +13 -0
- package/instructions/local-daemon.md +11 -0
- package/package.json +3 -1
- package/server/instructions.ts +28 -0
- package/server/runner.ts +9 -9
- package/shared/instructions.ts +31 -0
- package/shared/run-report.ts +88 -0
- package/shared/settings.ts +21 -1
package/OVERVIEW.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Terminal integration
|
|
2
|
+
|
|
3
|
+
Terminal integration connects agent replies to Paseo terminals. Runnable shell, Python, and
|
|
4
|
+
JavaScript code blocks get controls for running them in an interactive workspace terminal or as a
|
|
5
|
+
background process. Command results can be sent back to the agent, while terminal output can also
|
|
6
|
+
be attached manually to a prompt.
|
|
7
|
+
|
|
8
|
+
Interactive commands run in a regular terminal named "Agent commands" on the daemon host. They can
|
|
9
|
+
accept passwords and confirmations. Background commands have no terminal or input, can run in
|
|
10
|
+
parallel, and retain more output. Commands only run after the user presses a run button or submits
|
|
11
|
+
the `/run` command.
|
|
12
|
+
|
|
13
|
+
The settings control whether completed output is sent to the agent by default and how many lines
|
|
14
|
+
make a result start collapsed in the timeline. Optional command guidance can be added to newly
|
|
15
|
+
created agents for either a local-daemon or client-server setup. Both supplied instruction templates
|
|
16
|
+
are editable per host.
|
|
17
|
+
|
|
18
|
+
The plugin requires Paseo 0.10.1 or later and a macOS or Linux daemon host with `bash`. Python and
|
|
19
|
+
Node.js code blocks also require their corresponding interpreter. Plugins are trusted code; this
|
|
20
|
+
plugin starts processes and types commands into terminals on the daemon host.
|
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ the reply finishes. On other Paseo versions a **Run in terminal** card appears u
|
|
|
17
17
|
- With **Send output to agent** checked (the default), the command, exit code, and output go to
|
|
18
18
|
the agent as a new message when the block finishes. If the agent is busy, delivery waits until
|
|
19
19
|
its turn ends instead of interrupting it. Unchecked, **Send output** sends it later.
|
|
20
|
+
- Sent output longer than 20 lines starts collapsed. Change the threshold under
|
|
21
|
+
**Settings → Plugins → Terminal integration**, or set it to 0 to keep output expanded.
|
|
20
22
|
- **Stop** sends Ctrl-C; **Send output so far** reports a still-running command.
|
|
21
23
|
|
|
22
24
|
**Terminal beside the chat.** On Paseo builds with the terminal open-location setting, choose
|
|
@@ -29,6 +31,22 @@ Terminal runs in one workspace are queued so a command never types into another
|
|
|
29
31
|
Each block runs as a script in the agent's working directory, so `cd` and `export` do not persist
|
|
30
32
|
between runs. **Settings → Plugins → Terminal integration** sets the checkbox's default.
|
|
31
33
|
|
|
34
|
+
**Command guidance for agents.** The same settings screen can add command-formatting and execution
|
|
35
|
+
guidance to the system prompt of newly created agents. Turn on **Inject command instructions** and
|
|
36
|
+
choose one topology:
|
|
37
|
+
|
|
38
|
+
- **Local daemon** tells the agent that Run already executes on the local Paseo daemon host, so it
|
|
39
|
+
should not add SSH merely to reach that machine.
|
|
40
|
+
- **Client–server** tells the agent that the UI and daemon may be on different devices. Commands for
|
|
41
|
+
the daemon need no prefix; commands for another machine must include an explicit transport such
|
|
42
|
+
as SSH, using a destination supplied by the user rather than an invented host.
|
|
43
|
+
|
|
44
|
+
Both templates are editable and stored per host. The shipped defaults are also available as
|
|
45
|
+
[`local-daemon.md`](instructions/local-daemon.md) and
|
|
46
|
+
[`client-server.md`](instructions/client-server.md), and each editor has a restore-default action.
|
|
47
|
+
Changing or enabling the setting affects only agents created afterward; it does not rewrite the
|
|
48
|
+
system prompt of an existing agent.
|
|
49
|
+
|
|
32
50
|
**Terminal → agent.** In the composer's attachment menu, **Terminal output** attaches the latest
|
|
33
51
|
lines of any terminal on the daemon. Type part of a name or path to filter, and a number to choose
|
|
34
52
|
the line count (`build 500`; default 200).
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useSettings,
|
|
3
|
+
type PluginTimelineItemProps,
|
|
4
|
+
type PluginTimelineTransformerContribution,
|
|
5
|
+
} from "@getpaseo/plugin/client";
|
|
6
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
7
|
+
import { useMemo, useState } from "react";
|
|
8
|
+
import { Platform, Pressable, ScrollView, Text, View } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
parseRunReport,
|
|
11
|
+
runReportSchema,
|
|
12
|
+
wrapRunReportOutcome,
|
|
13
|
+
type RunReportData,
|
|
14
|
+
} from "../shared/run-report";
|
|
15
|
+
import { DEFAULT_COLLAPSE_OUTPUT_AFTER_LINES, preferences } from "../shared/settings";
|
|
16
|
+
|
|
17
|
+
type UserMessageTransformer = PluginTimelineTransformerContribution<"user_message">["transform"];
|
|
18
|
+
type Theme = PluginTimelineItemProps["theme"];
|
|
19
|
+
|
|
20
|
+
export const transformRunReport: UserMessageTransformer = ({ item }) => {
|
|
21
|
+
const data = parseRunReport(item.text);
|
|
22
|
+
if (!data) return;
|
|
23
|
+
return {
|
|
24
|
+
items: [{ type: "plugin", kind: "terminal-run-report", version: 1, data }],
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export { runReportSchema };
|
|
29
|
+
|
|
30
|
+
export function RunReport({ item, theme, layout }: PluginTimelineItemProps<RunReportData>) {
|
|
31
|
+
const settings = useSettings(preferences);
|
|
32
|
+
const threshold =
|
|
33
|
+
settings.status === "ready"
|
|
34
|
+
? settings.values.collapseOutputAfterLines
|
|
35
|
+
: DEFAULT_COLLAPSE_OUTPUT_AFTER_LINES;
|
|
36
|
+
const autoCollapsed = threshold > 0 && item.data.outputLineCount > threshold;
|
|
37
|
+
const [expandedOverride, setExpandedOverride] = useState<boolean | null>(null);
|
|
38
|
+
const expanded = expandedOverride ?? !autoCollapsed;
|
|
39
|
+
const styles = useMemo(() => createStyles(theme, layout.compact), [theme, layout.compact]);
|
|
40
|
+
|
|
41
|
+
if (!item.data.output) {
|
|
42
|
+
return (
|
|
43
|
+
<View style={styles.card}>
|
|
44
|
+
<Text style={styles.intro}>{item.data.intro}</Text>
|
|
45
|
+
<CodeBlock text={item.data.code} styles={styles} />
|
|
46
|
+
<Text style={styles.outputLabel}>{item.data.outputLabel}</Text>
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<View style={[styles.card, !expanded && styles.collapsedCard]}>
|
|
53
|
+
<Pressable
|
|
54
|
+
accessibilityRole="button"
|
|
55
|
+
accessibilityLabel={`${expanded ? "Collapse" : "Expand"} terminal output`}
|
|
56
|
+
accessibilityState={{ expanded }}
|
|
57
|
+
onPress={() => setExpandedOverride(!expanded)}
|
|
58
|
+
style={({ pressed }) => [styles.reportHeader, pressed && styles.pressed]}
|
|
59
|
+
>
|
|
60
|
+
<Icon
|
|
61
|
+
name={expanded ? "ChevronDown" : "ChevronRight"}
|
|
62
|
+
size={16}
|
|
63
|
+
color={theme.colors.foregroundMuted}
|
|
64
|
+
/>
|
|
65
|
+
<Text style={styles.reportTitle} numberOfLines={expanded ? undefined : 1}>
|
|
66
|
+
{expanded ? wrapRunReportOutcome(item.data.intro) : item.data.intro}
|
|
67
|
+
</Text>
|
|
68
|
+
<Text style={styles.lineCount}>
|
|
69
|
+
{item.data.outputLineCount} {item.data.outputLineCount === 1 ? "line" : "lines"}
|
|
70
|
+
</Text>
|
|
71
|
+
</Pressable>
|
|
72
|
+
{expanded ? (
|
|
73
|
+
<>
|
|
74
|
+
<CodeBlock text={item.data.code} styles={styles} />
|
|
75
|
+
<Text style={styles.outputLabel}>{item.data.outputLabel}</Text>
|
|
76
|
+
<CodeBlock text={item.data.output} styles={styles} />
|
|
77
|
+
</>
|
|
78
|
+
) : null}
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type Styles = ReturnType<typeof createStyles>;
|
|
84
|
+
|
|
85
|
+
function CodeBlock({ text, styles }: { text: string; styles: Styles }) {
|
|
86
|
+
return (
|
|
87
|
+
<ScrollView horizontal style={styles.codeBlock} contentContainerStyle={styles.codeContent}>
|
|
88
|
+
<Text selectable style={styles.code}>
|
|
89
|
+
{text}
|
|
90
|
+
</Text>
|
|
91
|
+
</ScrollView>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createStyles(theme: Theme, compact: boolean) {
|
|
96
|
+
return {
|
|
97
|
+
card: {
|
|
98
|
+
gap: 10,
|
|
99
|
+
padding: compact ? 12 : 14,
|
|
100
|
+
borderWidth: 1,
|
|
101
|
+
borderColor: theme.colors.border,
|
|
102
|
+
borderRadius: 12,
|
|
103
|
+
backgroundColor: theme.colors.surface1,
|
|
104
|
+
},
|
|
105
|
+
collapsedCard: { gap: 0, padding: 4 },
|
|
106
|
+
intro: { color: theme.colors.foreground, fontSize: 14, lineHeight: 20 },
|
|
107
|
+
reportHeader: {
|
|
108
|
+
minHeight: 36,
|
|
109
|
+
flexDirection: "row" as const,
|
|
110
|
+
alignItems: "center" as const,
|
|
111
|
+
gap: 6,
|
|
112
|
+
paddingHorizontal: 8,
|
|
113
|
+
borderRadius: 6,
|
|
114
|
+
},
|
|
115
|
+
pressed: { opacity: 0.7 },
|
|
116
|
+
reportTitle: {
|
|
117
|
+
flex: 1,
|
|
118
|
+
color: theme.colors.foreground,
|
|
119
|
+
fontSize: 13,
|
|
120
|
+
lineHeight: 18,
|
|
121
|
+
},
|
|
122
|
+
outputLabel: { flexShrink: 1, color: theme.colors.foreground, fontSize: 13 },
|
|
123
|
+
lineCount: { marginLeft: "auto" as const, color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
124
|
+
codeBlock: {
|
|
125
|
+
maxWidth: "100%" as const,
|
|
126
|
+
borderWidth: 1,
|
|
127
|
+
borderColor: theme.colors.border,
|
|
128
|
+
borderRadius: 8,
|
|
129
|
+
backgroundColor: theme.colors.surface0,
|
|
130
|
+
},
|
|
131
|
+
codeContent: { paddingHorizontal: 10, paddingVertical: 8 },
|
|
132
|
+
code: {
|
|
133
|
+
color: theme.colors.foreground,
|
|
134
|
+
fontSize: 12,
|
|
135
|
+
lineHeight: 18,
|
|
136
|
+
fontFamily: Platform.select({
|
|
137
|
+
ios: "Menlo",
|
|
138
|
+
android: "monospace",
|
|
139
|
+
default: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -1,13 +1,23 @@
|
|
|
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,
|
|
6
|
+
SettingsInput,
|
|
4
7
|
SettingsRow,
|
|
5
8
|
SettingsSection,
|
|
9
|
+
SettingsSelect,
|
|
6
10
|
SettingsSwitch,
|
|
7
11
|
} from "@getpaseo/plugin/client/ui";
|
|
8
|
-
import {
|
|
12
|
+
import { useMemo, useState } from "react";
|
|
13
|
+
import { Text, TextInput, View } from "react-native";
|
|
9
14
|
import { TERMINAL_NAME } from "../shared/contracts";
|
|
10
|
-
import {
|
|
15
|
+
import { CLIENT_SERVER_INSTRUCTIONS, LOCAL_DAEMON_INSTRUCTIONS } from "../shared/instructions";
|
|
16
|
+
import { preferences, type InstructionTopology } from "../shared/settings";
|
|
17
|
+
|
|
18
|
+
const REPOSITORY = "https://github.com/keefeere/paseo-terminal-integration/blob/main";
|
|
19
|
+
|
|
20
|
+
type ReadySettings = Extract<SettingsState<typeof preferences.schema>, { status: "ready" }>;
|
|
11
21
|
|
|
12
22
|
export function SettingsScreen({ theme }: PluginSurfaceProps) {
|
|
13
23
|
const settings = useSettings(preferences);
|
|
@@ -17,24 +27,199 @@ export function SettingsScreen({ theme }: PluginSurfaceProps) {
|
|
|
17
27
|
settings.status === "loading" ? "Loading…" : `Settings are unavailable: ${settings.error}`;
|
|
18
28
|
return <Text style={{ color: theme.colors.foregroundMuted }}>{message}</Text>;
|
|
19
29
|
}
|
|
30
|
+
|
|
31
|
+
return <ReadySettingsScreen key={settings.revision} settings={settings} theme={theme} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ReadySettingsScreen({
|
|
35
|
+
settings,
|
|
36
|
+
theme,
|
|
37
|
+
}: {
|
|
38
|
+
settings: ReadySettings;
|
|
39
|
+
theme: PluginSurfaceProps["theme"];
|
|
40
|
+
}) {
|
|
20
41
|
const { values, revision } = settings;
|
|
42
|
+
const [draft, setDraft] = useState(values);
|
|
43
|
+
const [collapseLinesText, setCollapseLinesText] = useState(
|
|
44
|
+
String(values.collapseOutputAfterLines),
|
|
45
|
+
);
|
|
46
|
+
const collapseLines = /^\d+$/.test(collapseLinesText) ? Number(collapseLinesText) : NaN;
|
|
47
|
+
const collapseLinesError =
|
|
48
|
+
Number.isInteger(collapseLines) && collapseLines >= 0 && collapseLines <= 5_000
|
|
49
|
+
? null
|
|
50
|
+
: "Enter a whole number from 0 to 5000.";
|
|
51
|
+
const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(values), [draft, values]);
|
|
52
|
+
const editorStyle = useMemo(
|
|
53
|
+
() => ({
|
|
54
|
+
minHeight: 220,
|
|
55
|
+
paddingHorizontal: 12,
|
|
56
|
+
paddingVertical: 10,
|
|
57
|
+
borderWidth: 1,
|
|
58
|
+
borderColor: theme.colors.border,
|
|
59
|
+
borderRadius: 8,
|
|
60
|
+
color: theme.colors.foreground,
|
|
61
|
+
backgroundColor: theme.colors.surface0,
|
|
62
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
63
|
+
fontSize: 12,
|
|
64
|
+
lineHeight: 18,
|
|
65
|
+
textAlignVertical: "top" as const,
|
|
66
|
+
}),
|
|
67
|
+
[theme],
|
|
68
|
+
);
|
|
69
|
+
const linkStyle = useMemo(() => ({ color: theme.colors.accent, fontSize: 12 }), [theme]);
|
|
70
|
+
|
|
71
|
+
const updateTopology = (instructionTopology: InstructionTopology) =>
|
|
72
|
+
setDraft((current) => ({ ...current, instructionTopology }));
|
|
21
73
|
|
|
22
74
|
return (
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
<>
|
|
76
|
+
<SettingsSection title="Code block actions">
|
|
77
|
+
<SettingsCard>
|
|
78
|
+
<SettingsSwitch
|
|
79
|
+
label="Send output to the agent by default"
|
|
80
|
+
hint={`Initial state of the "Send output to agent" checkbox on each code block.`}
|
|
81
|
+
value={draft.sendToAgent}
|
|
82
|
+
disabled={settings.saving}
|
|
83
|
+
error={settings.saveError}
|
|
84
|
+
onValueChange={(sendToAgent) => setDraft((current) => ({ ...current, sendToAgent }))}
|
|
85
|
+
/>
|
|
86
|
+
<SettingsInput
|
|
87
|
+
label="Collapse output after this many lines"
|
|
88
|
+
hint="Long output sent back to the agent starts collapsed. Set 0 to keep all output expanded."
|
|
89
|
+
initialValue={collapseLinesText}
|
|
90
|
+
placeholder="20"
|
|
91
|
+
disabled={settings.saving}
|
|
92
|
+
error={collapseLinesError}
|
|
93
|
+
onChangeText={(text) => {
|
|
94
|
+
setCollapseLinesText(text);
|
|
95
|
+
const value = /^\d+$/.test(text) ? Number(text) : NaN;
|
|
96
|
+
if (Number.isInteger(value) && value >= 0 && value <= 5_000) {
|
|
97
|
+
setDraft((current) => ({ ...current, collapseOutputAfterLines: value }));
|
|
98
|
+
}
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
<SettingsRow
|
|
102
|
+
label="Where the terminal appears"
|
|
103
|
+
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.`}
|
|
104
|
+
/>
|
|
105
|
+
</SettingsCard>
|
|
106
|
+
</SettingsSection>
|
|
107
|
+
|
|
108
|
+
<SettingsSection title="Agent command instructions">
|
|
109
|
+
<SettingsCard>
|
|
110
|
+
<SettingsSwitch
|
|
111
|
+
label="Inject command instructions"
|
|
112
|
+
hint="Add the selected guidance to the system prompt of newly created agents. Existing agents are unchanged."
|
|
113
|
+
value={draft.injectCommandInstructions}
|
|
114
|
+
disabled={settings.saving}
|
|
115
|
+
onValueChange={(injectCommandInstructions) =>
|
|
116
|
+
setDraft((current) => ({ ...current, injectCommandInstructions }))
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
<SettingsSelect
|
|
120
|
+
label="Execution topology"
|
|
121
|
+
hint="Choose where clicking Run actually executes a command."
|
|
122
|
+
value={draft.instructionTopology}
|
|
123
|
+
options={[
|
|
124
|
+
{ label: "Local daemon", value: "local-daemon" },
|
|
125
|
+
{ label: "Client–server", value: "client-server" },
|
|
126
|
+
]}
|
|
127
|
+
disabled={settings.saving}
|
|
128
|
+
onValueChange={updateTopology}
|
|
129
|
+
/>
|
|
130
|
+
</SettingsCard>
|
|
131
|
+
</SettingsSection>
|
|
132
|
+
|
|
133
|
+
<SettingsSection title="Local daemon instructions">
|
|
134
|
+
<SettingsCard>
|
|
135
|
+
<SettingsRow
|
|
136
|
+
label="Instruction text"
|
|
137
|
+
hint="Used when Execution topology is Local daemon. Edit the host-scoped copy below."
|
|
138
|
+
>
|
|
139
|
+
<View style={{ gap: 8, width: "100%" }}>
|
|
140
|
+
<ExternalLink href={`${REPOSITORY}/instructions/local-daemon.md`}>
|
|
141
|
+
<Text style={linkStyle}>View shipped local-daemon.md</Text>
|
|
142
|
+
</ExternalLink>
|
|
143
|
+
<TextInput
|
|
144
|
+
multiline
|
|
145
|
+
accessibilityLabel="Local daemon command instructions"
|
|
146
|
+
value={draft.localDaemonInstructions}
|
|
147
|
+
editable={!settings.saving}
|
|
148
|
+
onChangeText={(localDaemonInstructions) =>
|
|
149
|
+
setDraft((current) => ({ ...current, localDaemonInstructions }))
|
|
150
|
+
}
|
|
151
|
+
style={editorStyle}
|
|
152
|
+
/>
|
|
153
|
+
</View>
|
|
154
|
+
</SettingsRow>
|
|
155
|
+
<SettingsAction
|
|
156
|
+
label="Restore local template"
|
|
157
|
+
hint="Replace the draft with the version shipped by the plugin."
|
|
158
|
+
actionLabel="Restore default"
|
|
159
|
+
disabled={
|
|
160
|
+
settings.saving || draft.localDaemonInstructions === LOCAL_DAEMON_INSTRUCTIONS
|
|
161
|
+
}
|
|
162
|
+
onPress={() =>
|
|
163
|
+
setDraft((current) => ({
|
|
164
|
+
...current,
|
|
165
|
+
localDaemonInstructions: LOCAL_DAEMON_INSTRUCTIONS,
|
|
166
|
+
}))
|
|
167
|
+
}
|
|
168
|
+
/>
|
|
169
|
+
</SettingsCard>
|
|
170
|
+
</SettingsSection>
|
|
171
|
+
|
|
172
|
+
<SettingsSection title="Client–server instructions">
|
|
173
|
+
<SettingsCard>
|
|
174
|
+
<SettingsRow
|
|
175
|
+
label="Instruction text"
|
|
176
|
+
hint="Used when Execution topology is Client–server. Edit the host-scoped copy below."
|
|
177
|
+
>
|
|
178
|
+
<View style={{ gap: 8, width: "100%" }}>
|
|
179
|
+
<ExternalLink href={`${REPOSITORY}/instructions/client-server.md`}>
|
|
180
|
+
<Text style={linkStyle}>View shipped client-server.md</Text>
|
|
181
|
+
</ExternalLink>
|
|
182
|
+
<TextInput
|
|
183
|
+
multiline
|
|
184
|
+
accessibilityLabel="Client-server command instructions"
|
|
185
|
+
value={draft.clientServerInstructions}
|
|
186
|
+
editable={!settings.saving}
|
|
187
|
+
onChangeText={(clientServerInstructions) =>
|
|
188
|
+
setDraft((current) => ({ ...current, clientServerInstructions }))
|
|
189
|
+
}
|
|
190
|
+
style={editorStyle}
|
|
191
|
+
/>
|
|
192
|
+
</View>
|
|
193
|
+
</SettingsRow>
|
|
194
|
+
<SettingsAction
|
|
195
|
+
label="Restore client–server template"
|
|
196
|
+
hint="Replace the draft with the version shipped by the plugin."
|
|
197
|
+
actionLabel="Restore default"
|
|
198
|
+
disabled={
|
|
199
|
+
settings.saving || draft.clientServerInstructions === CLIENT_SERVER_INSTRUCTIONS
|
|
200
|
+
}
|
|
201
|
+
onPress={() =>
|
|
202
|
+
setDraft((current) => ({
|
|
203
|
+
...current,
|
|
204
|
+
clientServerInstructions: CLIENT_SERVER_INSTRUCTIONS,
|
|
205
|
+
}))
|
|
206
|
+
}
|
|
207
|
+
/>
|
|
208
|
+
</SettingsCard>
|
|
209
|
+
</SettingsSection>
|
|
210
|
+
|
|
211
|
+
<SettingsSection title="Save settings">
|
|
212
|
+
<SettingsCard>
|
|
213
|
+
<SettingsAction
|
|
214
|
+
label="Apply changes"
|
|
215
|
+
hint="Output display changes immediately. Command instructions apply only to newly created agents."
|
|
216
|
+
error={settings.saveError}
|
|
217
|
+
actionLabel={settings.saving ? "Saving…" : "Save changes"}
|
|
218
|
+
disabled={settings.saving || !dirty || collapseLinesError !== null}
|
|
219
|
+
onPress={() => void settings.save(draft, revision)}
|
|
220
|
+
/>
|
|
221
|
+
</SettingsCard>
|
|
222
|
+
</SettingsSection>
|
|
223
|
+
</>
|
|
39
224
|
);
|
|
40
225
|
}
|
package/index.client.tsx
CHANGED
|
@@ -4,10 +4,22 @@ import type { PluginCleanup } from "@getpaseo/plugin";
|
|
|
4
4
|
import type { InlineActionsClient } from "./client/sdk-compat";
|
|
5
5
|
import { CodeBlockActions } from "./client/code-block-actions";
|
|
6
6
|
import { SettingsScreen } from "./client/settings-screen";
|
|
7
|
+
import { RunReport, runReportSchema, transformRunReport } from "./client/run-report";
|
|
7
8
|
import { RUNNABLE_LANGUAGES } from "./shared/code-blocks";
|
|
8
9
|
import { terminalOutputSource } from "./shared/contracts";
|
|
9
10
|
|
|
10
11
|
export default function contribute(client: InlineActionsClient): PluginCleanup {
|
|
12
|
+
client.addTimelineTransformer({
|
|
13
|
+
id: "terminal-run-report",
|
|
14
|
+
query: { itemType: "user_message" },
|
|
15
|
+
transform: transformRunReport,
|
|
16
|
+
});
|
|
17
|
+
client.addTimelineRenderer({
|
|
18
|
+
kind: "terminal-run-report",
|
|
19
|
+
version: 1,
|
|
20
|
+
schema: runReportSchema,
|
|
21
|
+
Component: RunReport,
|
|
22
|
+
});
|
|
11
23
|
if (typeof client.addCodeBlockActions === "function") {
|
|
12
24
|
client.addCodeBlockActions({
|
|
13
25
|
id: "run",
|
package/index.server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 {
|
|
@@ -16,11 +17,25 @@ import {
|
|
|
16
17
|
} from "./shared/contracts";
|
|
17
18
|
|
|
18
19
|
export default function contribute(server: PluginServerContext) {
|
|
19
|
-
server.registerSettings(preferences);
|
|
20
|
+
const settings = server.registerSettings(preferences);
|
|
20
21
|
let runner: TerminalRunner | null = null;
|
|
21
22
|
const runnerFor = (paseo: ConstructorParameters<typeof TerminalRunner>[0]) =>
|
|
22
23
|
(runner ??= new TerminalRunner(paseo));
|
|
23
24
|
|
|
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
|
+
|
|
24
39
|
// COMPAT(legacyRunCards): only old clients opt into scanning; remove after 2027-04-01.
|
|
25
40
|
let legacyCardsRequested = false;
|
|
26
41
|
server.handle(enableLegacyCards, async () => {
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-terminal-integration",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Run agent code blocks in a Paseo terminal and send the output back to the agent; attach terminal output to prompts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"paseo-plugin.json",
|
|
23
|
+
"OVERVIEW.md",
|
|
23
24
|
"index.client.tsx",
|
|
24
25
|
"index.server.ts",
|
|
25
26
|
"client/",
|
|
27
|
+
"instructions/",
|
|
26
28
|
"server/",
|
|
27
29
|
"shared/",
|
|
28
30
|
"!**/*.test.ts"
|
|
@@ -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
|
@@ -7,7 +7,8 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
7
7
|
import type { PluginHandlerContext } from "@getpaseo/plugin/server";
|
|
8
8
|
import { interpreterFor, type Interpreter } from "../shared/code-blocks";
|
|
9
9
|
import { TERMINAL_NAME, type RunMode, type RunSnapshot } from "../shared/contracts";
|
|
10
|
-
import {
|
|
10
|
+
import { formatRunReport } from "../shared/run-report";
|
|
11
|
+
import { trimBlankEdges } from "../shared/text";
|
|
11
12
|
|
|
12
13
|
type Paseo = PluginHandlerContext["paseo"];
|
|
13
14
|
type TerminalHandle = ReturnType<Paseo["terminals"]["ref"]>;
|
|
@@ -474,14 +475,13 @@ function formatReport(run: Run): string {
|
|
|
474
475
|
run.mode === "background"
|
|
475
476
|
? "in the background, with no terminal"
|
|
476
477
|
: `in the Paseo terminal "${TERMINAL_NAME}"`;
|
|
477
|
-
return
|
|
478
|
-
`I ran this ${where} (cwd \`${run.cwd}\`), ${outcome}:`,
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
"",
|
|
482
|
-
output
|
|
483
|
-
|
|
484
|
-
].join("\n");
|
|
478
|
+
return formatRunReport({
|
|
479
|
+
intro: `I ran this ${where} (cwd \`${run.cwd}\`), ${outcome}:`,
|
|
480
|
+
language: run.lang,
|
|
481
|
+
code: run.code,
|
|
482
|
+
outputLabel: output ? `Output${note}:` : "It printed no output.",
|
|
483
|
+
output,
|
|
484
|
+
});
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
function shellQuote(value: string): 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";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fenced } from "./text";
|
|
3
|
+
|
|
4
|
+
export const RUN_REPORT_MARKER = "[paseo-terminal-integration-report]: # (v1)";
|
|
5
|
+
|
|
6
|
+
export const runReportSchema = z.object({
|
|
7
|
+
intro: z.string(),
|
|
8
|
+
language: z.string(),
|
|
9
|
+
code: z.string(),
|
|
10
|
+
outputLabel: z.string(),
|
|
11
|
+
output: z.string(),
|
|
12
|
+
outputLineCount: z.number().int().nonnegative(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type RunReportData = z.output<typeof runReportSchema>;
|
|
16
|
+
|
|
17
|
+
export function wrapRunReportOutcome(intro: string): string {
|
|
18
|
+
const splitAt = intro.lastIndexOf("), ");
|
|
19
|
+
return splitAt < 0 ? intro : `${intro.slice(0, splitAt + 1)}\n${intro.slice(splitAt + 3)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatRunReport(data: Omit<RunReportData, "outputLineCount">): string {
|
|
23
|
+
return [
|
|
24
|
+
RUN_REPORT_MARKER,
|
|
25
|
+
"",
|
|
26
|
+
data.intro,
|
|
27
|
+
"",
|
|
28
|
+
fenced(data.code, data.language),
|
|
29
|
+
"",
|
|
30
|
+
data.outputLabel,
|
|
31
|
+
...(data.output ? ["", fenced(data.output, "text")] : []),
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseRunReport(text: string): RunReportData | null {
|
|
36
|
+
const lines = text.split("\n");
|
|
37
|
+
if (lines[0] !== RUN_REPORT_MARKER) return null;
|
|
38
|
+
|
|
39
|
+
let cursor = skipBlankLines(lines, 1);
|
|
40
|
+
const intro = lines[cursor++];
|
|
41
|
+
if (!intro) return null;
|
|
42
|
+
|
|
43
|
+
cursor = skipBlankLines(lines, cursor);
|
|
44
|
+
const command = parseFence(lines, cursor);
|
|
45
|
+
if (!command) return null;
|
|
46
|
+
|
|
47
|
+
cursor = skipBlankLines(lines, command.next);
|
|
48
|
+
const outputLabel = lines[cursor++];
|
|
49
|
+
if (!outputLabel) return null;
|
|
50
|
+
|
|
51
|
+
cursor = skipBlankLines(lines, cursor);
|
|
52
|
+
let output = "";
|
|
53
|
+
if (cursor < lines.length) {
|
|
54
|
+
const outputFence = parseFence(lines, cursor);
|
|
55
|
+
if (!outputFence || outputFence.language !== "text") return null;
|
|
56
|
+
output = outputFence.content;
|
|
57
|
+
cursor = skipBlankLines(lines, outputFence.next);
|
|
58
|
+
}
|
|
59
|
+
if (cursor !== lines.length) return null;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
intro,
|
|
63
|
+
language: command.language,
|
|
64
|
+
code: command.content,
|
|
65
|
+
outputLabel,
|
|
66
|
+
output,
|
|
67
|
+
outputLineCount: output ? output.split("\n").length : 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function skipBlankLines(lines: readonly string[], from: number): number {
|
|
72
|
+
let cursor = from;
|
|
73
|
+
while (cursor < lines.length && lines[cursor] === "") cursor++;
|
|
74
|
+
return cursor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseFence(
|
|
78
|
+
lines: readonly string[],
|
|
79
|
+
from: number,
|
|
80
|
+
): { language: string; content: string; next: number } | null {
|
|
81
|
+
const match = /^(`{3,})([^`]*)$/.exec(lines[from] ?? "");
|
|
82
|
+
if (!match) return null;
|
|
83
|
+
const fence = match[1];
|
|
84
|
+
const language = match[2].trim();
|
|
85
|
+
const end = lines.indexOf(fence, from + 1);
|
|
86
|
+
if (end < 0) return null;
|
|
87
|
+
return { language, content: lines.slice(from + 1, end).join("\n"), next: end + 1 };
|
|
88
|
+
}
|
package/shared/settings.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
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];
|
|
7
|
+
export const DEFAULT_COLLAPSE_OUTPUT_AFTER_LINES = 20;
|
|
3
8
|
|
|
4
9
|
export const preferences = defineSettings({
|
|
5
10
|
id: "preferences",
|
|
6
11
|
scope: "host",
|
|
7
|
-
version:
|
|
12
|
+
version: 3,
|
|
8
13
|
schema: z.object({
|
|
9
14
|
/** Initial state of a code block's "Send output to agent" checkbox. */
|
|
10
15
|
sendToAgent: z.boolean().default(true),
|
|
16
|
+
/** Automatically collapse a sent run report when its output exceeds this many lines; 0 disables. */
|
|
17
|
+
collapseOutputAfterLines: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(0)
|
|
21
|
+
.max(5_000)
|
|
22
|
+
.default(DEFAULT_COLLAPSE_OUTPUT_AFTER_LINES),
|
|
23
|
+
/** Add command-formatting and execution-topology guidance to newly created agents. */
|
|
24
|
+
injectCommandInstructions: z.boolean().default(false),
|
|
25
|
+
instructionTopology: z.enum(instructionTopologies).default("client-server"),
|
|
26
|
+
localDaemonInstructions: z.string().default(LOCAL_DAEMON_INSTRUCTIONS),
|
|
27
|
+
clientServerInstructions: z.string().default(CLIENT_SERVER_INSTRUCTIONS),
|
|
11
28
|
}),
|
|
29
|
+
migrate(values) {
|
|
30
|
+
return values;
|
|
31
|
+
},
|
|
12
32
|
});
|