paseo-terminal-integration 0.5.4 → 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 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");
@@ -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 ? <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>
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
  }
@@ -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 ? <Text style={tone}>{parts.join(" · ")}</Text> : null}
213
- {run.error ? <Text style={styles.danger}>{run.error}</Text> : null}
214
- {run.tail.length > 0 ? (
215
- <View style={styles.tail}>
216
- {run.tail.map((line, index) => (
217
- <Text key={index} style={styles.tailLine} numberOfLines={1}>
218
- {line || " "}
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
+ }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-terminal-integration",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
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",
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";
@@ -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() }),