paseo-terminal-integration 0.5.3 → 0.5.5
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 +9 -4
- package/README.md +5 -3
- package/client/code-block-actions.tsx +11 -20
- package/client/hide-run-report.ts +11 -0
- package/client/run-card.tsx +11 -20
- package/client/run-output.tsx +116 -0
- package/client/settings-screen.tsx +2 -26
- package/index.client.tsx +2 -8
- package/index.server.ts +2 -0
- package/package.json +1 -1
- package/server/runner.ts +30 -0
- package/shared/contracts.ts +12 -0
- package/shared/settings.ts +1 -10
- package/client/run-report.tsx +0 -143
package/OVERVIEW.md
CHANGED
|
@@ -10,10 +10,15 @@ accept passwords and confirmations. Background commands have no terminal or inpu
|
|
|
10
10
|
parallel, and retain more output. Commands only run after the user presses a run button or submits
|
|
11
11
|
the `/run` command.
|
|
12
12
|
|
|
13
|
-
The settings control whether completed output is sent to the agent by default
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
The settings control whether completed output is sent to the agent by default. Sent reports remain
|
|
14
|
+
available to the agent but are hidden from the visible timeline to avoid duplicating the inline run
|
|
15
|
+
status, recent output, and terminal. Optional command guidance can be added to newly created agents
|
|
16
|
+
for either a local-daemon or client-server setup. Both supplied instruction templates are editable
|
|
17
|
+
per host.
|
|
18
|
+
|
|
19
|
+
Output up to five lines opens automatically in the inline run controls. Longer output can be
|
|
20
|
+
expanded into a scrollable history panel after the run, including after its terminal tab is closed.
|
|
21
|
+
The retained history lasts until the plugin process is reloaded or prunes an older run.
|
|
17
22
|
|
|
18
23
|
The plugin requires Paseo 0.10.1 or later and a macOS or Linux daemon host with `bash`. Python and
|
|
19
24
|
Node.js code blocks also require their corresponding interpreter. Plugins are trusted code; this
|
package/README.md
CHANGED
|
@@ -17,9 +17,11 @@ 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
|
|
21
|
-
|
|
20
|
+
- Sent output remains available to the agent but is hidden from the visible timeline, since the
|
|
21
|
+
inline run controls and terminal already show the command status and recent output.
|
|
22
22
|
- **Stop** sends Ctrl-C; **Send output so far** reports a still-running command.
|
|
23
|
+
- Output up to 5 lines opens automatically. Longer output stays behind the status row; expand it
|
|
24
|
+
to browse the retained history in a scrollable panel, even after its terminal tab is closed.
|
|
23
25
|
|
|
24
26
|
**Terminal beside the chat.** On Paseo builds with the terminal open-location setting, choose
|
|
25
27
|
**Settings → Layout → Open location → Opening a terminal → On the side** and "Agent commands"
|
|
@@ -80,7 +82,7 @@ The commands are available on Paseo versions without inline code block actions.
|
|
|
80
82
|
- Terminal output is read from the screen, so very long lines arrive wrapped and only the last
|
|
81
83
|
3,000 rows are visible. The agent receives at most 300 lines or 24,000 characters.
|
|
82
84
|
- Run state lives in the plugin process; reloading the plugin forgets it. Controls and cards stay,
|
|
83
|
-
and nothing is re-run.
|
|
85
|
+
but their saved output is no longer available and nothing is re-run.
|
|
84
86
|
- A command that has not started within 20 seconds fails, for example when something else is
|
|
85
87
|
running in "Agent commands".
|
|
86
88
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { runnableBlock } from "../shared/code-blocks";
|
|
17
17
|
import { preferences } from "../shared/settings";
|
|
18
18
|
import { revealStartedRun } from "./reveal-run";
|
|
19
|
+
import { RunOutput } from "./run-output";
|
|
19
20
|
|
|
20
21
|
type Theme = PluginCodeBlockActionsProps["theme"];
|
|
21
22
|
type Styles = ReturnType<typeof createStyles>;
|
|
@@ -171,12 +172,12 @@ function RunnableActions({
|
|
|
171
172
|
) : null}
|
|
172
173
|
{controls.error ? <Text style={styles.danger}>{controls.error}</Text> : null}
|
|
173
174
|
{status.error ? <Text style={styles.danger}>{status.error.message}</Text> : null}
|
|
174
|
-
{run ? <RunStatus run={run} styles={styles} /> : null}
|
|
175
|
+
{run ? <RunStatus run={run} styles={styles} theme={theme} /> : null}
|
|
175
176
|
</View>
|
|
176
177
|
);
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
180
|
+
function RunStatus({ run, styles, theme }: { run: RunSnapshot; styles: Styles; theme: Theme }) {
|
|
180
181
|
const parts: string[] = [];
|
|
181
182
|
let tone: TextStyle = styles.muted;
|
|
182
183
|
if (run.status === "queued") parts.push("Queued, waiting for the terminal");
|
|
@@ -202,17 +203,15 @@ function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
|
202
203
|
|
|
203
204
|
return (
|
|
204
205
|
<View style={styles.status}>
|
|
205
|
-
{parts.length > 0 ?
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</Text>
|
|
213
|
-
))}
|
|
214
|
-
</View>
|
|
206
|
+
{parts.length > 0 ? (
|
|
207
|
+
<RunOutput
|
|
208
|
+
run={run}
|
|
209
|
+
summary={parts.join(" · ")}
|
|
210
|
+
summaryColor={tone.color as string}
|
|
211
|
+
theme={theme}
|
|
212
|
+
/>
|
|
215
213
|
) : null}
|
|
214
|
+
{run.error ? <Text style={styles.danger}>{run.error}</Text> : null}
|
|
216
215
|
</View>
|
|
217
216
|
);
|
|
218
217
|
}
|
|
@@ -355,13 +354,5 @@ function createStyles(theme: Theme, compact: boolean) {
|
|
|
355
354
|
primaryButton: { backgroundColor: theme.colors.accent, borderColor: theme.colors.accent },
|
|
356
355
|
buttonText: { fontSize: 13 },
|
|
357
356
|
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
357
|
};
|
|
367
358
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
|
|
2
|
+
import { RUN_REPORT_MARKER } from "../shared/run-report";
|
|
3
|
+
|
|
4
|
+
type UserMessageTransformer = PluginTimelineTransformerContribution<"user_message">["transform"];
|
|
5
|
+
|
|
6
|
+
/** Keep the full report available to the agent without repeating it in the visible timeline. */
|
|
7
|
+
export const hideRunReport: UserMessageTransformer = ({ item }) => {
|
|
8
|
+
if (item.text === RUN_REPORT_MARKER || item.text.startsWith(`${RUN_REPORT_MARKER}\n`)) {
|
|
9
|
+
return { items: [] };
|
|
10
|
+
}
|
|
11
|
+
};
|
package/client/run-card.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type RunSnapshot,
|
|
16
16
|
} from "../shared/contracts";
|
|
17
17
|
import { preferences } from "../shared/settings";
|
|
18
|
+
import { RunOutput } from "./run-output";
|
|
18
19
|
|
|
19
20
|
type Theme = PluginTimelineItemProps["theme"];
|
|
20
21
|
type Styles = ReturnType<typeof createStyles>;
|
|
@@ -175,7 +176,7 @@ export function RunCard({ item, agentId, theme, layout }: PluginTimelineItemProp
|
|
|
175
176
|
}
|
|
176
177
|
/>
|
|
177
178
|
</View>
|
|
178
|
-
{run ? <RunStatus run={run} styles={styles} /> : null}
|
|
179
|
+
{run ? <RunStatus run={run} styles={styles} theme={theme} /> : null}
|
|
179
180
|
</View>
|
|
180
181
|
);
|
|
181
182
|
})}
|
|
@@ -183,7 +184,7 @@ export function RunCard({ item, agentId, theme, layout }: PluginTimelineItemProp
|
|
|
183
184
|
);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
187
|
+
function RunStatus({ run, styles, theme }: { run: RunSnapshot; styles: Styles; theme: Theme }) {
|
|
187
188
|
const parts: string[] = [];
|
|
188
189
|
let tone: TextStyle = styles.muted;
|
|
189
190
|
if (run.status === "queued") parts.push("Queued, waiting for the terminal");
|
|
@@ -209,17 +210,15 @@ function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
|
209
210
|
|
|
210
211
|
return (
|
|
211
212
|
<View style={styles.status}>
|
|
212
|
-
{parts.length > 0 ?
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
</Text>
|
|
220
|
-
))}
|
|
221
|
-
</View>
|
|
213
|
+
{parts.length > 0 ? (
|
|
214
|
+
<RunOutput
|
|
215
|
+
run={run}
|
|
216
|
+
summary={parts.join(" · ")}
|
|
217
|
+
summaryColor={tone.color as string}
|
|
218
|
+
theme={theme}
|
|
219
|
+
/>
|
|
222
220
|
) : null}
|
|
221
|
+
{run.error ? <Text style={styles.danger}>{run.error}</Text> : null}
|
|
223
222
|
</View>
|
|
224
223
|
);
|
|
225
224
|
}
|
|
@@ -362,13 +361,5 @@ function createStyles(theme: Theme, compact: boolean) {
|
|
|
362
361
|
primaryButton: { backgroundColor: theme.colors.accent, borderColor: theme.colors.accent },
|
|
363
362
|
buttonText: { fontSize: 13 },
|
|
364
363
|
status: { gap: 6 },
|
|
365
|
-
tail: {
|
|
366
|
-
padding: 8,
|
|
367
|
-
borderRadius: 6,
|
|
368
|
-
backgroundColor: theme.colors.surface0,
|
|
369
|
-
borderWidth: 1,
|
|
370
|
-
borderColor: theme.colors.border,
|
|
371
|
-
},
|
|
372
|
-
tailLine: { color: theme.colors.foregroundMuted, fontFamily: MONOSPACE, fontSize: 12 },
|
|
373
364
|
};
|
|
374
365
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useRpc } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { Platform, Pressable, ScrollView, Text, View } from "react-native";
|
|
6
|
+
import { runOutput, type RunSnapshot } from "../shared/contracts";
|
|
7
|
+
|
|
8
|
+
const AUTO_EXPAND_LINES = 5;
|
|
9
|
+
const POLL_MS = 700;
|
|
10
|
+
const MONOSPACE = Platform.select({
|
|
11
|
+
ios: "Menlo",
|
|
12
|
+
android: "monospace",
|
|
13
|
+
default: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
interface OutputTheme {
|
|
17
|
+
colors: {
|
|
18
|
+
border: string;
|
|
19
|
+
foregroundMuted: string;
|
|
20
|
+
surface0: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function RunOutput({
|
|
25
|
+
run,
|
|
26
|
+
summary,
|
|
27
|
+
summaryColor,
|
|
28
|
+
theme,
|
|
29
|
+
}: {
|
|
30
|
+
run: RunSnapshot;
|
|
31
|
+
summary: string;
|
|
32
|
+
summaryColor: string;
|
|
33
|
+
theme: OutputTheme;
|
|
34
|
+
}) {
|
|
35
|
+
const automatic = run.lineCount > 0 && run.lineCount <= AUTO_EXPAND_LINES;
|
|
36
|
+
const [override, setOverride] = useState<boolean | null>(null);
|
|
37
|
+
useEffect(() => setOverride(null), [run.runId]);
|
|
38
|
+
const expanded = override ?? automatic;
|
|
39
|
+
const fetchOutput = useRpc(runOutput);
|
|
40
|
+
const active = run.status === "queued" || run.status === "running";
|
|
41
|
+
const output = useQuery({
|
|
42
|
+
queryKey: ["terminal-run-output", run.key, run.runId],
|
|
43
|
+
queryFn: () => fetchOutput({ key: run.key }),
|
|
44
|
+
enabled: expanded && run.lineCount > 0,
|
|
45
|
+
refetchInterval: active && expanded ? POLL_MS : false,
|
|
46
|
+
});
|
|
47
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
48
|
+
const lines = output.data?.lines ?? run.tail;
|
|
49
|
+
|
|
50
|
+
if (run.lineCount === 0) return <Text style={{ color: summaryColor }}>{summary}</Text>;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View style={styles.container}>
|
|
54
|
+
<Pressable
|
|
55
|
+
accessibilityRole="button"
|
|
56
|
+
accessibilityLabel={`${expanded ? "Collapse" : "Expand"} command output`}
|
|
57
|
+
accessibilityState={{ expanded }}
|
|
58
|
+
onPress={() => setOverride(!expanded)}
|
|
59
|
+
style={({ pressed }) => [styles.toggle, pressed && styles.pressed]}
|
|
60
|
+
>
|
|
61
|
+
<Icon name={expanded ? "ChevronDown" : "ChevronRight"} size={15} color={summaryColor} />
|
|
62
|
+
<Text style={[styles.summary, { color: summaryColor }]}>{summary}</Text>
|
|
63
|
+
</Pressable>
|
|
64
|
+
{expanded ? (
|
|
65
|
+
<View style={styles.output}>
|
|
66
|
+
{output.isPending && lines.length === 0 ? (
|
|
67
|
+
<Text style={styles.note}>Loading output…</Text>
|
|
68
|
+
) : null}
|
|
69
|
+
{output.error ? <Text style={styles.note}>{output.error.message}</Text> : null}
|
|
70
|
+
{output.data?.truncated ? (
|
|
71
|
+
<Text style={styles.note}>Earlier output is no longer available.</Text>
|
|
72
|
+
) : null}
|
|
73
|
+
{lines.length > 0 ? (
|
|
74
|
+
<ScrollView style={styles.viewport} nestedScrollEnabled>
|
|
75
|
+
<ScrollView horizontal contentContainerStyle={styles.outputContent}>
|
|
76
|
+
<Text selectable style={styles.outputText}>
|
|
77
|
+
{lines.join("\n")}
|
|
78
|
+
</Text>
|
|
79
|
+
</ScrollView>
|
|
80
|
+
</ScrollView>
|
|
81
|
+
) : null}
|
|
82
|
+
</View>
|
|
83
|
+
) : null}
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createStyles(theme: OutputTheme) {
|
|
89
|
+
return {
|
|
90
|
+
container: { gap: 6 },
|
|
91
|
+
toggle: { flexDirection: "row" as const, alignItems: "center" as const, gap: 4 },
|
|
92
|
+
pressed: { opacity: 0.7 },
|
|
93
|
+
summary: { flexShrink: 1 },
|
|
94
|
+
output: {
|
|
95
|
+
borderRadius: 6,
|
|
96
|
+
backgroundColor: theme.colors.surface0,
|
|
97
|
+
borderWidth: 1,
|
|
98
|
+
borderColor: theme.colors.border,
|
|
99
|
+
overflow: "hidden" as const,
|
|
100
|
+
},
|
|
101
|
+
viewport: { maxHeight: 280 },
|
|
102
|
+
outputContent: { padding: 8 },
|
|
103
|
+
outputText: {
|
|
104
|
+
color: theme.colors.foregroundMuted,
|
|
105
|
+
fontFamily: MONOSPACE,
|
|
106
|
+
fontSize: 12,
|
|
107
|
+
lineHeight: 18,
|
|
108
|
+
},
|
|
109
|
+
note: {
|
|
110
|
+
color: theme.colors.foregroundMuted,
|
|
111
|
+
fontSize: 12,
|
|
112
|
+
paddingHorizontal: 8,
|
|
113
|
+
paddingTop: 6,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
ExternalLink,
|
|
4
4
|
SettingsAction,
|
|
5
5
|
SettingsCard,
|
|
6
|
-
SettingsInput,
|
|
7
6
|
SettingsRow,
|
|
8
7
|
SettingsSection,
|
|
9
8
|
SettingsSelect,
|
|
@@ -40,14 +39,6 @@ function ReadySettingsScreen({
|
|
|
40
39
|
}) {
|
|
41
40
|
const { values, revision } = settings;
|
|
42
41
|
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
42
|
const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(values), [draft, values]);
|
|
52
43
|
const editorStyle = useMemo(
|
|
53
44
|
() => ({
|
|
@@ -83,21 +74,6 @@ function ReadySettingsScreen({
|
|
|
83
74
|
error={settings.saveError}
|
|
84
75
|
onValueChange={(sendToAgent) => setDraft((current) => ({ ...current, sendToAgent }))}
|
|
85
76
|
/>
|
|
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
77
|
<SettingsRow
|
|
102
78
|
label="Where the terminal appears"
|
|
103
79
|
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.`}
|
|
@@ -212,10 +188,10 @@ function ReadySettingsScreen({
|
|
|
212
188
|
<SettingsCard>
|
|
213
189
|
<SettingsAction
|
|
214
190
|
label="Apply changes"
|
|
215
|
-
hint="
|
|
191
|
+
hint="Command instructions apply only to newly created agents."
|
|
216
192
|
error={settings.saveError}
|
|
217
193
|
actionLabel={settings.saving ? "Saving…" : "Save changes"}
|
|
218
|
-
disabled={settings.saving || !dirty
|
|
194
|
+
disabled={settings.saving || !dirty}
|
|
219
195
|
onPress={() => void settings.save(draft, revision)}
|
|
220
196
|
/>
|
|
221
197
|
</SettingsCard>
|
package/index.client.tsx
CHANGED
|
@@ -3,8 +3,8 @@ import { contributeLegacy } from "./client/legacy";
|
|
|
3
3
|
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
|
+
import { hideRunReport } from "./client/hide-run-report";
|
|
6
7
|
import { SettingsScreen } from "./client/settings-screen";
|
|
7
|
-
import { RunReport, runReportSchema, transformRunReport } from "./client/run-report";
|
|
8
8
|
import { RUNNABLE_LANGUAGES } from "./shared/code-blocks";
|
|
9
9
|
import { terminalOutputSource } from "./shared/contracts";
|
|
10
10
|
|
|
@@ -12,13 +12,7 @@ export default function contribute(client: InlineActionsClient): PluginCleanup {
|
|
|
12
12
|
client.addTimelineTransformer({
|
|
13
13
|
id: "terminal-run-report",
|
|
14
14
|
query: { itemType: "user_message" },
|
|
15
|
-
transform:
|
|
16
|
-
});
|
|
17
|
-
client.addTimelineRenderer({
|
|
18
|
-
kind: "terminal-run-report",
|
|
19
|
-
version: 1,
|
|
20
|
-
schema: runReportSchema,
|
|
21
|
-
Component: RunReport,
|
|
15
|
+
transform: hideRunReport,
|
|
22
16
|
});
|
|
23
17
|
if (typeof client.addCodeBlockActions === "function") {
|
|
24
18
|
client.addCodeBlockActions({
|
package/index.server.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
cancelRun,
|
|
10
10
|
runAdhoc,
|
|
11
11
|
runKey,
|
|
12
|
+
runOutput,
|
|
12
13
|
runStatus,
|
|
13
14
|
scanBlocks,
|
|
14
15
|
searchTerminalOutput,
|
|
@@ -48,6 +49,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
48
49
|
|
|
49
50
|
server.handle(startRun, (input, { paseo }) => runnerFor(paseo).start(input));
|
|
50
51
|
server.handle(runStatus, ({ keys }, { paseo }) => ({ runs: runnerFor(paseo).status(keys) }));
|
|
52
|
+
server.handle(runOutput, ({ key }, { paseo }) => runnerFor(paseo).output(key));
|
|
51
53
|
server.handle(cancelRun, ({ key }, { paseo }) => runnerFor(paseo).cancel(key));
|
|
52
54
|
server.handle(sendRunOutput, ({ key }, { paseo }) => runnerFor(paseo).sendOutput(key));
|
|
53
55
|
|
package/package.json
CHANGED
package/server/runner.ts
CHANGED
|
@@ -29,6 +29,8 @@ const FINISHED_RUN_LIMIT = 200;
|
|
|
29
29
|
const BACKGROUND_MAX_LINES = 5_000;
|
|
30
30
|
const BACKGROUND_MAX_LINE_CHARS = 16_000;
|
|
31
31
|
const BACKGROUND_FLUSH_MS = 1_000;
|
|
32
|
+
const UI_OUTPUT_MAX_LINES = 5_000;
|
|
33
|
+
const UI_OUTPUT_MAX_CHARS = 1_000_000;
|
|
32
34
|
|
|
33
35
|
// sendKeys() expands only a few key names, so control characters are written as raw bytes.
|
|
34
36
|
const CTRL_C = "\x03";
|
|
@@ -146,6 +148,17 @@ export class TerminalRunner {
|
|
|
146
148
|
});
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
output(key: string): { lines: string[]; lineCount: number; truncated: boolean } | null {
|
|
152
|
+
const run = this.runs.get(key);
|
|
153
|
+
if (!run) return null;
|
|
154
|
+
const lines = boundedOutput(run.output, UI_OUTPUT_MAX_LINES, UI_OUTPUT_MAX_CHARS);
|
|
155
|
+
return {
|
|
156
|
+
lines,
|
|
157
|
+
lineCount: run.output.length,
|
|
158
|
+
truncated: run.truncated || lines.length < run.output.length,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
cancel(key: string): RunSnapshot | null {
|
|
150
163
|
const run = this.runs.get(key);
|
|
151
164
|
if (!run) return null;
|
|
@@ -375,6 +388,23 @@ export class TerminalRunner {
|
|
|
375
388
|
}
|
|
376
389
|
}
|
|
377
390
|
|
|
391
|
+
export function boundedOutput(
|
|
392
|
+
output: readonly string[],
|
|
393
|
+
maxLines: number,
|
|
394
|
+
maxChars: number,
|
|
395
|
+
): string[] {
|
|
396
|
+
const selected: string[] = [];
|
|
397
|
+
let characters = 0;
|
|
398
|
+
for (let index = output.length - 1; index >= 0 && selected.length < maxLines; index--) {
|
|
399
|
+
const line = output[index];
|
|
400
|
+
const remaining = maxChars - characters;
|
|
401
|
+
if (remaining <= 0) break;
|
|
402
|
+
selected.push(line.length <= remaining ? line : line.slice(-remaining));
|
|
403
|
+
characters += Math.min(line.length, remaining) + 1;
|
|
404
|
+
}
|
|
405
|
+
return selected.reverse();
|
|
406
|
+
}
|
|
407
|
+
|
|
378
408
|
/** Read through a call: TypeScript keeps `run.status` narrowed across the awaits in execute(). */
|
|
379
409
|
function finishedCleanly(run: Run): boolean {
|
|
380
410
|
return run.status === "done";
|
package/shared/contracts.ts
CHANGED
|
@@ -68,6 +68,18 @@ export const runStatus = defineRpc({
|
|
|
68
68
|
output: z.object({ runs: z.array(runSnapshotSchema) }),
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
export const runOutput = defineRpc({
|
|
72
|
+
name: "run.output",
|
|
73
|
+
input: z.object({ key: z.string() }),
|
|
74
|
+
output: z
|
|
75
|
+
.object({
|
|
76
|
+
lines: z.array(z.string()),
|
|
77
|
+
lineCount: z.number().int().nonnegative(),
|
|
78
|
+
truncated: z.boolean(),
|
|
79
|
+
})
|
|
80
|
+
.nullable(),
|
|
81
|
+
});
|
|
82
|
+
|
|
71
83
|
export const cancelRun = defineRpc({
|
|
72
84
|
name: "run.cancel",
|
|
73
85
|
input: z.object({ key: z.string() }),
|
package/shared/settings.ts
CHANGED
|
@@ -4,22 +4,13 @@ import { CLIENT_SERVER_INSTRUCTIONS, LOCAL_DAEMON_INSTRUCTIONS } from "./instruc
|
|
|
4
4
|
|
|
5
5
|
export const instructionTopologies = ["local-daemon", "client-server"] as const;
|
|
6
6
|
export type InstructionTopology = (typeof instructionTopologies)[number];
|
|
7
|
-
export const DEFAULT_COLLAPSE_OUTPUT_AFTER_LINES = 20;
|
|
8
|
-
|
|
9
7
|
export const preferences = defineSettings({
|
|
10
8
|
id: "preferences",
|
|
11
9
|
scope: "host",
|
|
12
|
-
version:
|
|
10
|
+
version: 4,
|
|
13
11
|
schema: z.object({
|
|
14
12
|
/** Initial state of a code block's "Send output to agent" checkbox. */
|
|
15
13
|
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
14
|
/** Add command-formatting and execution-topology guidance to newly created agents. */
|
|
24
15
|
injectCommandInstructions: z.boolean().default(false),
|
|
25
16
|
instructionTopology: z.enum(instructionTopologies).default("client-server"),
|
package/client/run-report.tsx
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
}
|