paseo-terminal-integration 0.5.4 → 0.5.6
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 +4 -0
- package/README.md +3 -1
- package/client/code-block-actions.tsx +12 -20
- package/client/run-card.tsx +12 -20
- package/client/run-output.tsx +120 -0
- package/index.server.ts +2 -0
- package/package.json +1 -1
- package/server/runner.ts +30 -0
- package/shared/contracts.ts +12 -0
package/OVERVIEW.md
CHANGED
|
@@ -16,6 +16,10 @@ status, recent output, and terminal. Optional command guidance can be added to n
|
|
|
16
16
|
for either a local-daemon or client-server setup. Both supplied instruction templates are editable
|
|
17
17
|
per host.
|
|
18
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.
|
|
22
|
+
|
|
19
23
|
The plugin requires Paseo 0.10.1 or later and a macOS or Linux daemon host with `bash`. Python and
|
|
20
24
|
Node.js code blocks also require their corresponding interpreter. Plugins are trusted code; this
|
|
21
25
|
plugin starts processes and types commands into terminals on the daemon host.
|
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ the reply finishes. On other Paseo versions a **Run in terminal** card appears u
|
|
|
20
20
|
- Sent output remains available to the agent but is hidden from the visible timeline, since the
|
|
21
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");
|
|
@@ -184,6 +185,7 @@ function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
|
184
185
|
parts.push(
|
|
185
186
|
`${run.mode === "background" ? "Running in background" : "Running"} · ${elapsed(run)}`,
|
|
186
187
|
);
|
|
188
|
+
tone = styles.success;
|
|
187
189
|
}
|
|
188
190
|
if (run.status === "done") {
|
|
189
191
|
parts.push(`${run.exitCode === 0 ? "✓" : "✗"} exit ${run.exitCode}`);
|
|
@@ -202,17 +204,15 @@ function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
|
202
204
|
|
|
203
205
|
return (
|
|
204
206
|
<View style={styles.status}>
|
|
205
|
-
{parts.length > 0 ?
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</Text>
|
|
213
|
-
))}
|
|
214
|
-
</View>
|
|
207
|
+
{parts.length > 0 ? (
|
|
208
|
+
<RunOutput
|
|
209
|
+
run={run}
|
|
210
|
+
summary={parts.join(" · ")}
|
|
211
|
+
summaryColor={tone.color as string}
|
|
212
|
+
theme={theme}
|
|
213
|
+
/>
|
|
215
214
|
) : null}
|
|
215
|
+
{run.error ? <Text style={styles.danger}>{run.error}</Text> : null}
|
|
216
216
|
</View>
|
|
217
217
|
);
|
|
218
218
|
}
|
|
@@ -355,13 +355,5 @@ function createStyles(theme: Theme, compact: boolean) {
|
|
|
355
355
|
primaryButton: { backgroundColor: theme.colors.accent, borderColor: theme.colors.accent },
|
|
356
356
|
buttonText: { fontSize: 13 },
|
|
357
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
358
|
};
|
|
367
359
|
}
|
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");
|
|
@@ -191,6 +192,7 @@ function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
|
191
192
|
parts.push(
|
|
192
193
|
`${run.mode === "background" ? "Running in background" : "Running"} · ${elapsed(run)}`,
|
|
193
194
|
);
|
|
195
|
+
tone = styles.success;
|
|
194
196
|
}
|
|
195
197
|
if (run.status === "done") {
|
|
196
198
|
parts.push(`${run.exitCode === 0 ? "✓" : "✗"} exit ${run.exitCode}`);
|
|
@@ -209,17 +211,15 @@ function RunStatus({ run, styles }: { run: RunSnapshot; styles: Styles }) {
|
|
|
209
211
|
|
|
210
212
|
return (
|
|
211
213
|
<View style={styles.status}>
|
|
212
|
-
{parts.length > 0 ?
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
</Text>
|
|
220
|
-
))}
|
|
221
|
-
</View>
|
|
214
|
+
{parts.length > 0 ? (
|
|
215
|
+
<RunOutput
|
|
216
|
+
run={run}
|
|
217
|
+
summary={parts.join(" · ")}
|
|
218
|
+
summaryColor={tone.color as string}
|
|
219
|
+
theme={theme}
|
|
220
|
+
/>
|
|
222
221
|
) : null}
|
|
222
|
+
{run.error ? <Text style={styles.danger}>{run.error}</Text> : null}
|
|
223
223
|
</View>
|
|
224
224
|
);
|
|
225
225
|
}
|
|
@@ -362,13 +362,5 @@ function createStyles(theme: Theme, compact: boolean) {
|
|
|
362
362
|
primaryButton: { backgroundColor: theme.colors.accent, borderColor: theme.colors.accent },
|
|
363
363
|
buttonText: { fontSize: 13 },
|
|
364
364
|
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
365
|
};
|
|
374
366
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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 { ActivityIndicator, 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
|
+
statusSuccess: string;
|
|
21
|
+
surface0: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function RunOutput({
|
|
26
|
+
run,
|
|
27
|
+
summary,
|
|
28
|
+
summaryColor,
|
|
29
|
+
theme,
|
|
30
|
+
}: {
|
|
31
|
+
run: RunSnapshot;
|
|
32
|
+
summary: string;
|
|
33
|
+
summaryColor: string;
|
|
34
|
+
theme: OutputTheme;
|
|
35
|
+
}) {
|
|
36
|
+
const automatic = run.lineCount > 0 && run.lineCount <= AUTO_EXPAND_LINES;
|
|
37
|
+
const [override, setOverride] = useState<boolean | null>(null);
|
|
38
|
+
useEffect(() => setOverride(null), [run.runId]);
|
|
39
|
+
const expanded = override ?? automatic;
|
|
40
|
+
const fetchOutput = useRpc(runOutput);
|
|
41
|
+
const active = run.status === "queued" || run.status === "running";
|
|
42
|
+
const output = useQuery({
|
|
43
|
+
queryKey: ["terminal-run-output", run.key, run.runId],
|
|
44
|
+
queryFn: () => fetchOutput({ key: run.key }),
|
|
45
|
+
enabled: expanded && run.lineCount > 0,
|
|
46
|
+
refetchInterval: active && expanded ? POLL_MS : false,
|
|
47
|
+
});
|
|
48
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
49
|
+
const lines = output.data?.lines ?? run.tail;
|
|
50
|
+
|
|
51
|
+
if (run.lineCount === 0) return <Text style={{ color: summaryColor }}>{summary}</Text>;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View style={styles.container}>
|
|
55
|
+
<Pressable
|
|
56
|
+
accessibilityRole="button"
|
|
57
|
+
accessibilityLabel={`${expanded ? "Collapse" : "Expand"} command output`}
|
|
58
|
+
accessibilityState={{ expanded }}
|
|
59
|
+
onPress={() => setOverride(!expanded)}
|
|
60
|
+
style={({ pressed }) => [styles.toggle, pressed && styles.pressed]}
|
|
61
|
+
>
|
|
62
|
+
{run.status === "running" ? (
|
|
63
|
+
<ActivityIndicator size="small" color={theme.colors.statusSuccess} />
|
|
64
|
+
) : null}
|
|
65
|
+
<Text style={[styles.summary, { color: summaryColor }]}>{summary}</Text>
|
|
66
|
+
<Icon name={expanded ? "ChevronDown" : "ChevronRight"} size={15} color={summaryColor} />
|
|
67
|
+
</Pressable>
|
|
68
|
+
{expanded ? (
|
|
69
|
+
<View style={styles.output}>
|
|
70
|
+
{output.isPending && lines.length === 0 ? (
|
|
71
|
+
<Text style={styles.note}>Loading output…</Text>
|
|
72
|
+
) : null}
|
|
73
|
+
{output.error ? <Text style={styles.note}>{output.error.message}</Text> : null}
|
|
74
|
+
{output.data?.truncated ? (
|
|
75
|
+
<Text style={styles.note}>Earlier output is no longer available.</Text>
|
|
76
|
+
) : null}
|
|
77
|
+
{lines.length > 0 ? (
|
|
78
|
+
<ScrollView style={styles.viewport} nestedScrollEnabled>
|
|
79
|
+
<ScrollView horizontal contentContainerStyle={styles.outputContent}>
|
|
80
|
+
<Text selectable style={styles.outputText}>
|
|
81
|
+
{lines.join("\n")}
|
|
82
|
+
</Text>
|
|
83
|
+
</ScrollView>
|
|
84
|
+
</ScrollView>
|
|
85
|
+
) : null}
|
|
86
|
+
</View>
|
|
87
|
+
) : null}
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createStyles(theme: OutputTheme) {
|
|
93
|
+
return {
|
|
94
|
+
container: { gap: 6 },
|
|
95
|
+
toggle: { flexDirection: "row" as const, alignItems: "center" as const, gap: 4 },
|
|
96
|
+
pressed: { opacity: 0.7 },
|
|
97
|
+
summary: { flex: 1 },
|
|
98
|
+
output: {
|
|
99
|
+
borderRadius: 6,
|
|
100
|
+
backgroundColor: theme.colors.surface0,
|
|
101
|
+
borderWidth: 1,
|
|
102
|
+
borderColor: theme.colors.border,
|
|
103
|
+
overflow: "hidden" as const,
|
|
104
|
+
},
|
|
105
|
+
viewport: { maxHeight: 280 },
|
|
106
|
+
outputContent: { padding: 8 },
|
|
107
|
+
outputText: {
|
|
108
|
+
color: theme.colors.foregroundMuted,
|
|
109
|
+
fontFamily: MONOSPACE,
|
|
110
|
+
fontSize: 12,
|
|
111
|
+
lineHeight: 18,
|
|
112
|
+
},
|
|
113
|
+
note: {
|
|
114
|
+
color: theme.colors.foregroundMuted,
|
|
115
|
+
fontSize: 12,
|
|
116
|
+
paddingHorizontal: 8,
|
|
117
|
+
paddingTop: 6,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
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() }),
|