paseo-terminal-integration 0.5.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 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
@@ -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
+ }
@@ -3,6 +3,7 @@ import {
3
3
  ExternalLink,
4
4
  SettingsAction,
5
5
  SettingsCard,
6
+ SettingsInput,
6
7
  SettingsRow,
7
8
  SettingsSection,
8
9
  SettingsSelect,
@@ -39,6 +40,14 @@ function ReadySettingsScreen({
39
40
  }) {
40
41
  const { values, revision } = settings;
41
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.";
42
51
  const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(values), [draft, values]);
43
52
  const editorStyle = useMemo(
44
53
  () => ({
@@ -69,12 +78,25 @@ function ReadySettingsScreen({
69
78
  <SettingsSwitch
70
79
  label="Send output to the agent by default"
71
80
  hint={`Initial state of the "Send output to agent" checkbox on each code block.`}
72
- value={values.sendToAgent}
81
+ value={draft.sendToAgent}
73
82
  disabled={settings.saving}
74
83
  error={settings.saveError}
75
- onValueChange={(sendToAgent) =>
76
- void settings.save({ ...values, sendToAgent }, revision)
77
- }
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
+ }}
78
100
  />
79
101
  <SettingsRow
80
102
  label="Where the terminal appears"
@@ -186,14 +208,14 @@ function ReadySettingsScreen({
186
208
  </SettingsCard>
187
209
  </SettingsSection>
188
210
 
189
- <SettingsSection title="Save instruction settings">
211
+ <SettingsSection title="Save settings">
190
212
  <SettingsCard>
191
213
  <SettingsAction
192
- label="Apply to newly created agents"
193
- hint="Saving does not modify agents that already exist."
214
+ label="Apply changes"
215
+ hint="Output display changes immediately. Command instructions apply only to newly created agents."
194
216
  error={settings.saveError}
195
217
  actionLabel={settings.saving ? "Saving…" : "Save changes"}
196
- disabled={settings.saving || !dirty}
218
+ disabled={settings.saving || !dirty || collapseLinesError !== null}
197
219
  onPress={() => void settings.save(draft, revision)}
198
220
  />
199
221
  </SettingsCard>
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-terminal-integration",
3
- "version": "0.5.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,6 +20,7 @@
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/",
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 { fenced, trimBlankEdges } from "../shared/text";
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
- fenced(run.code, run.lang),
481
- "",
482
- output ? `Output${note}:` : "It printed no output.",
483
- ...(output ? ["", fenced(output, "text")] : []),
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,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
+ }
@@ -4,14 +4,22 @@ 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;
7
8
 
8
9
  export const preferences = defineSettings({
9
10
  id: "preferences",
10
11
  scope: "host",
11
- version: 2,
12
+ version: 3,
12
13
  schema: z.object({
13
14
  /** Initial state of a code block's "Send output to agent" checkbox. */
14
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),
15
23
  /** Add command-formatting and execution-topology guidance to newly created agents. */
16
24
  injectCommandInstructions: z.boolean().default(false),
17
25
  instructionTopology: z.enum(instructionTopologies).default("client-server"),