@tt-a1i/openpi 0.1.0

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.
Files changed (114) hide show
  1. package/README.md +643 -0
  2. package/SETUP.md +74 -0
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-package.png +0 -0
  5. package/assets/readme-hero-mobile.svg +72 -0
  6. package/assets/readme-hero.svg +118 -0
  7. package/assets/readme-runtime-mobile.svg +91 -0
  8. package/assets/readme-runtime.svg +111 -0
  9. package/extensions/ask-user/handoff.ts +205 -0
  10. package/extensions/ask-user/index.ts +1110 -0
  11. package/extensions/ask-user/limits.ts +89 -0
  12. package/extensions/ask-user/prompt.ts +76 -0
  13. package/extensions/background-terminals/index.ts +653 -0
  14. package/extensions/background-terminals/src/domain.ts +99 -0
  15. package/extensions/background-terminals/src/manager.ts +989 -0
  16. package/extensions/background-terminals/src/output.ts +84 -0
  17. package/extensions/background-terminals/src/prompt.ts +195 -0
  18. package/extensions/background-terminals/src/result-delivery.ts +43 -0
  19. package/extensions/background-terminals/src/runtime.ts +36 -0
  20. package/extensions/background-terminals/src/ui/output-view.ts +55 -0
  21. package/extensions/background-terminals/src/ui/ps.ts +642 -0
  22. package/extensions/background-terminals/src/ui/tool-result.ts +146 -0
  23. package/extensions/background-terminals/src/watch.ts +192 -0
  24. package/extensions/context-pivot/index.ts +222 -0
  25. package/extensions/copy-all/index.ts +65 -0
  26. package/extensions/cron/index.ts +173 -0
  27. package/extensions/cron/schedule.ts +127 -0
  28. package/extensions/file-mutation-display/index.ts +105 -0
  29. package/extensions/file-mutation-display/render.ts +107 -0
  30. package/extensions/file-search/index.ts +515 -0
  31. package/extensions/file-search/src/args.ts +129 -0
  32. package/extensions/file-search/src/binaries.ts +419 -0
  33. package/extensions/file-search/src/output.ts +142 -0
  34. package/extensions/file-search/src/process.ts +309 -0
  35. package/extensions/file-search/src/prompt.ts +53 -0
  36. package/extensions/git-info/index.ts +272 -0
  37. package/extensions/git-info/src/changed-files-view.ts +414 -0
  38. package/extensions/git-info/src/process.ts +107 -0
  39. package/extensions/git-info/src/refresh-coordinator.ts +13 -0
  40. package/extensions/git-info/src/runtime.ts +28 -0
  41. package/extensions/goal/controller.ts +794 -0
  42. package/extensions/goal/index.ts +521 -0
  43. package/extensions/goal/prompts.ts +122 -0
  44. package/extensions/goal/state.ts +763 -0
  45. package/extensions/goal/ui.ts +158 -0
  46. package/extensions/model-info/index.ts +234 -0
  47. package/extensions/plan-mode/bash-policy.ts +313 -0
  48. package/extensions/plan-mode/index.ts +539 -0
  49. package/extensions/post-edit/index.ts +129 -0
  50. package/extensions/sessions/LICENSE.upstream +21 -0
  51. package/extensions/sessions/git-stats.ts +226 -0
  52. package/extensions/sessions/index.ts +1092 -0
  53. package/extensions/sessions/sessions.ts +385 -0
  54. package/extensions/setup/index.ts +408 -0
  55. package/extensions/shared/activity-status.ts +65 -0
  56. package/extensions/shared/below-editor-navigation.ts +343 -0
  57. package/extensions/shared/child-session.ts +352 -0
  58. package/extensions/shared/context-utilization.ts +47 -0
  59. package/extensions/shared/dashboard-state.ts +102 -0
  60. package/extensions/shared/plan-mode-state.ts +65 -0
  61. package/extensions/shared/setup-config.ts +971 -0
  62. package/extensions/shared/subagent-roles.ts +22 -0
  63. package/extensions/shared/terminal-text.ts +38 -0
  64. package/extensions/shared/tool-call-timeout.ts +104 -0
  65. package/extensions/shared/worktree.ts +526 -0
  66. package/extensions/subagents/index.ts +1225 -0
  67. package/extensions/subagents/navigation.ts +121 -0
  68. package/extensions/subagents/src/agent-types.ts +543 -0
  69. package/extensions/subagents/src/backend.ts +63 -0
  70. package/extensions/subagents/src/backends/pi.ts +493 -0
  71. package/extensions/subagents/src/backends/stub.ts +296 -0
  72. package/extensions/subagents/src/by-the-way.ts +21 -0
  73. package/extensions/subagents/src/domain.ts +271 -0
  74. package/extensions/subagents/src/format.ts +48 -0
  75. package/extensions/subagents/src/manager.ts +769 -0
  76. package/extensions/subagents/src/prompt.ts +190 -0
  77. package/extensions/subagents/src/result-delivery.ts +20 -0
  78. package/extensions/subagents/src/runtime.ts +51 -0
  79. package/extensions/subagents/src/ui/takeover.ts +615 -0
  80. package/extensions/subagents/src/ui/transcript.ts +293 -0
  81. package/extensions/subagents/src/ui/wait-result.ts +89 -0
  82. package/extensions/suggestions/index.ts +172 -0
  83. package/extensions/suggestions/src/config.ts +12 -0
  84. package/extensions/suggestions/src/predictor.ts +147 -0
  85. package/extensions/suggestions/src/prompt.ts +20 -0
  86. package/extensions/suggestions/src/transcript.ts +233 -0
  87. package/extensions/suggestions/src/ui.ts +224 -0
  88. package/extensions/tasks/index.ts +512 -0
  89. package/extensions/tasks/tasks.ts +649 -0
  90. package/extensions/tasks/ui.ts +421 -0
  91. package/extensions/turn-time/index.ts +61 -0
  92. package/extensions/ui-customization/footer.ts +512 -0
  93. package/extensions/ui-customization/index.ts +217 -0
  94. package/extensions/workflows/acceptance.ts +298 -0
  95. package/extensions/workflows/artifacts.ts +225 -0
  96. package/extensions/workflows/controller.ts +210 -0
  97. package/extensions/workflows/dashboard.ts +1226 -0
  98. package/extensions/workflows/index.ts +1884 -0
  99. package/extensions/workflows/journal.ts +188 -0
  100. package/extensions/workflows/meta.ts +250 -0
  101. package/extensions/workflows/model.ts +423 -0
  102. package/extensions/workflows/navigation.ts +93 -0
  103. package/extensions/workflows/prompt.ts +212 -0
  104. package/extensions/workflows/replay-safety.ts +577 -0
  105. package/extensions/workflows/runner.ts +786 -0
  106. package/extensions/workflows/sandbox-child.cjs +402 -0
  107. package/extensions/workflows/sandbox.ts +397 -0
  108. package/extensions/workflows/serialization.ts +162 -0
  109. package/extensions/workflows/worktree-handoff.ts +216 -0
  110. package/package.json +87 -0
  111. package/scripts/prepare-effect-tsgo.mjs +16 -0
  112. package/skills/background-terminals/SKILL.md +30 -0
  113. package/skills/subagents/SKILL.md +15 -0
  114. package/themes/github-dark-default.json +89 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * OutputBuffer — bounded in-memory capture of one process stream.
3
+ *
4
+ * Newest output is always retained; when the retained size exceeds the cap,
5
+ * whole chunks are evicted from the head and counted in `truncatedBytes`.
6
+ * A single chunk larger than the cap itself is trimmed to its tail (on a
7
+ * UTF-8 boundary) so retention is strictly bounded even for one giant write.
8
+ * An optional spill callback receives every chunk (in order, before any
9
+ * eviction) so the caller can keep a complete on-disk copy.
10
+ *
11
+ * Plain TS by design: this is push-based accumulation driven by node stream
12
+ * 'data' callbacks, not stream transformation.
13
+ */
14
+
15
+ import type { OutputView } from "./domain.ts";
16
+
17
+ export class OutputBuffer {
18
+ private chunks: string[] = [];
19
+ /** Bytes currently retained across `chunks`. */
20
+ private retainedBytes = 0;
21
+ /** Cached join of `chunks`; invalidated on push so 1Hz UI ticks are cheap. */
22
+ private cachedText: string | undefined = "";
23
+ /** Bumped on every push; lets the UI cache derived line layouts. */
24
+ version = 0;
25
+ totalBytes = 0;
26
+ truncatedBytes = 0;
27
+ spillPath?: string;
28
+
29
+ private readonly maxRetainedBytes: number;
30
+ private readonly spill?: (chunk: string) => unknown;
31
+
32
+ constructor(maxRetainedBytes: number, spill?: (chunk: string) => unknown) {
33
+ this.maxRetainedBytes = maxRetainedBytes;
34
+ this.spill = spill;
35
+ }
36
+
37
+ push(chunk: string) {
38
+ if (chunk.length === 0) return true;
39
+ let bytes = Buffer.byteLength(chunk, "utf8");
40
+ this.totalBytes += bytes;
41
+ const spillAccepted = this.spill?.(chunk) !== false;
42
+ if (bytes > this.maxRetainedBytes) {
43
+ // A single pathological chunk larger than the whole cap: everything
44
+ // retained so far precedes it in the stream, so evict all of it, then
45
+ // keep only the chunk's tail (cut on a UTF-8 code point boundary).
46
+ // Retention stays strictly bounded even for one giant write, and the
47
+ // retained view stays contiguous (no hole in the middle).
48
+ this.truncatedBytes += this.retainedBytes;
49
+ this.chunks = [];
50
+ this.retainedBytes = 0;
51
+ const raw = Buffer.from(chunk, "utf8");
52
+ let start = raw.length - this.maxRetainedBytes;
53
+ while (start < raw.length && (raw[start] & 0xc0) === 0x80) start++;
54
+ this.truncatedBytes += start;
55
+ chunk = raw.subarray(start).toString("utf8");
56
+ bytes = raw.length - start;
57
+ }
58
+ this.chunks.push(chunk);
59
+ this.retainedBytes += bytes;
60
+ while (
61
+ this.retainedBytes > this.maxRetainedBytes &&
62
+ this.chunks.length > 1
63
+ ) {
64
+ const evicted = this.chunks.shift();
65
+ if (evicted === undefined) break;
66
+ const evictedBytes = Buffer.byteLength(evicted, "utf8");
67
+ this.retainedBytes -= evictedBytes;
68
+ this.truncatedBytes += evictedBytes;
69
+ }
70
+ this.cachedText = undefined;
71
+ this.version++;
72
+ return spillAccepted;
73
+ }
74
+
75
+ view(): OutputView {
76
+ this.cachedText ??= this.chunks.join("");
77
+ return {
78
+ text: this.cachedText,
79
+ totalBytes: this.totalBytes,
80
+ truncatedBytes: this.truncatedBytes,
81
+ spillPath: this.spillPath,
82
+ };
83
+ }
84
+ }
@@ -0,0 +1,195 @@
1
+ /** All model-facing strings for the background-terminals tools. */
2
+
3
+ import {
4
+ DEFAULT_MAX_BYTES,
5
+ DEFAULT_MAX_LINES,
6
+ formatSize,
7
+ truncateTail,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import {
10
+ formatDuration,
11
+ formatElapsed,
12
+ formatExit,
13
+ type TerminalSnapshot,
14
+ } from "./domain.ts";
15
+ import { MAX_RUNNING, type KillResult } from "./manager.ts";
16
+
17
+ /** bg_status stdout tail. */
18
+ export const STATUS_STDOUT_MAX = 16 * 1024;
19
+ /** bg_status stderr tail. */
20
+ export const STATUS_STDERR_MAX = 8 * 1024;
21
+ /** Completion follow-up stdout tail. Keep this concise; /ps has the detailed view. */
22
+ export const RESULT_STDOUT_MAX = 8 * 1024;
23
+ /** Completion follow-up stderr tail. Keep this concise; /ps has the detailed view. */
24
+ export const RESULT_STDERR_MAX = 4 * 1024;
25
+ const STATUS_STDOUT_MAX_LINES = 400;
26
+ const STATUS_STDERR_MAX_LINES = 200;
27
+ const RESULT_STDOUT_MAX_LINES = 40;
28
+ const RESULT_STDERR_MAX_LINES = 20;
29
+
30
+ export const BG_START_TOOL_DESCRIPTION =
31
+ "Start a long-running shell command as a background terminal (executed via the platform shell — sh -c on POSIX, cmd.exe /d /s /c on Windows). " +
32
+ "Fire-and-forget: this returns immediately with an id, and you get a message with the final output when the process exits. " +
33
+ "The process receives NO stdin (immediate EOF) and there is no way to send input later — interactive commands will not work; use bg_kill to stop a stuck one. " +
34
+ `Terminals are session-scoped: they are killed when the session ends or reloads. Output shown to you is tail-truncated (stdout ${formatSize(STATUS_STDOUT_MAX)}, stderr ${formatSize(STATUS_STDERR_MAX)}); output is captured to a log file when one can be opened, and to the /ps viewer. ` +
35
+ "Set timeout_seconds for finite work such as builds or tests; omit it for servers and watchers. " +
36
+ `Max ${MAX_RUNNING} background terminals can run at once.`;
37
+
38
+ export const BG_START_PROMPT_SNIPPET =
39
+ "Run a long-lived shell command in the background (dev servers, builds, watchers); output is captured and you're notified on exit";
40
+
41
+ export const BG_START_PROMPT_GUIDELINES = [
42
+ "Use bg_start for commands expected to run long or indefinitely (servers, watch modes, long builds); use the regular bash tool for quick commands.",
43
+ "bg_start processes receive no stdin — never start a command that requires interactive input.",
44
+ "After bg_start, keep working; the exit result arrives automatically. Use bg_status only when you need current output before continuing.",
45
+ "For finite background work (builds, tests, migrations), give bg_start a realistic timeout_seconds. Omit timeout_seconds for servers and watch modes that should run indefinitely.",
46
+ ];
47
+
48
+ export const BG_START_PARAMETER_DESCRIPTIONS = {
49
+ command:
50
+ "Shell command line to run in the background (sh -c on POSIX, cmd.exe /d /s /c on Windows). It receives no stdin (EOF immediately); interactive commands will not work.",
51
+ title: "Short human-readable name shown in listings and the UI",
52
+ workingDir: "Working directory (default: current working directory)",
53
+ timeoutSeconds:
54
+ "Optional runtime limit in seconds (1 to 604800). On expiry the process tree is terminated and reported as timed out. Omit for servers and watchers.",
55
+ };
56
+
57
+ export const BG_STATUS_TOOL_DESCRIPTION =
58
+ "Peek at a background terminal's status and current output (tail-truncated) without blocking. If the terminal already exited, this returns its final state.";
59
+
60
+ export const BG_STATUS_PARAMETER_DESCRIPTIONS = {
61
+ id: 'Terminal id, e.g. "bt-1"',
62
+ };
63
+
64
+ export const BG_LIST_TOOL_DESCRIPTION =
65
+ "List all background terminals (running and settled) with pid, elapsed time, exit status, and output sizes.";
66
+
67
+ export const BG_KILL_TOOL_DESCRIPTION =
68
+ "Stop one or more running background terminals (SIGTERM to the whole process tree, escalating to SIGKILL). Returns each terminal's final state; already-settled ids are reported as such.";
69
+
70
+ export const BG_KILL_PARAMETER_DESCRIPTIONS = {
71
+ ids: 'Terminal ids to stop, e.g. ["bt-1"]',
72
+ };
73
+
74
+ /** Describes mid-run output watching, including the failure-coverage rule. */
75
+ export const BG_WATCH_TOOL_DESCRIPTION =
76
+ 'Wake yourself when a running background terminal\'s output matches one of several literal signatures, instead of waiting for it to exit. Use this for a process that does not exit on its own — a dev server printing "Ready", a watcher rebuilding, a long job logging progress — so you are notified mid-run rather than polling bg_status. Separate alternatives with |. A watch is one-shot: it fires once, then disarms. IMPORTANT: silence is not success — include failure signatures too, or a crash looks identical to still-working. Prefer "Ready in|Traceback|ERROR|FAILED|Killed". Exiting processes already notify you automatically, so do not add a watch just for completion.';
77
+
78
+ /** Model-facing schema descriptions for the watch target and pattern. */
79
+ export const BG_WATCH_PARAMETER_DESCRIPTIONS = {
80
+ id: 'Running terminal id to watch, e.g. "bt-1"',
81
+ pattern:
82
+ 'Pipe-separated literal signatures matched against stdout and stderr as they stream. Include failure signatures too, e.g. "Ready in|Traceback|ERROR|FAILED".',
83
+ };
84
+
85
+ /** Builds the bg_watch arm acknowledgement. */
86
+ export function buildWatchArmedResult(options: {
87
+ id: string;
88
+ title: string;
89
+ pattern: string;
90
+ }) {
91
+ return `Watching ${options.id} "${options.title}" for /${options.pattern}/. You'll get one message when it first matches (or when the terminal exits, as usual).`;
92
+ }
93
+
94
+ /** Builds the mid-run match notification delivered to the model. */
95
+ export function buildWatchMatchMessage(options: {
96
+ id: string;
97
+ title: string;
98
+ pattern: string;
99
+ stream: "stdout" | "stderr";
100
+ line: string;
101
+ }) {
102
+ return (
103
+ `Background terminal ${options.id} "${options.title}" matched /${options.pattern}/ on ${options.stream}:\n\n` +
104
+ `${options.line}\n\n` +
105
+ "(The terminal is still running; this watch has disarmed. It will still notify you when it exits.)"
106
+ );
107
+ }
108
+
109
+ export function buildStartResult(snap: TerminalSnapshot) {
110
+ return (
111
+ `Started background terminal ${snap.id} "${snap.title}" (pid ${snap.pid ?? "?"}, ${snap.cwd}).\n` +
112
+ `It runs in the background with no stdin${snap.timeoutAt === undefined ? "" : ` and will time out in ${formatDuration((snap.timeoutAt - snap.createdAt) / 1_000)}`}. You'll get a message when it exits, ` +
113
+ `or use bg_status(id: "${snap.id}") to peek, bg_kill to stop it, bg_list to see all.`
114
+ );
115
+ }
116
+
117
+ /** One metadata line: `bt-1 [running] "dev server" (pid 12345, 3m12s, exit -, /path)`. */
118
+ export function describeTerminal(snap: TerminalSnapshot) {
119
+ const details = [
120
+ `pid ${snap.pid ?? "?"}`,
121
+ formatElapsed(snap),
122
+ snap.status === "running" ? "exit -" : formatExit(snap),
123
+ snap.status === "running" && snap.timeoutAt !== undefined
124
+ ? `${formatDuration((snap.timeoutAt - Date.now()) / 1_000)} remaining`
125
+ : "",
126
+ snap.cwd,
127
+ `stdout ${formatSize(snap.stdout.totalBytes)}, stderr ${formatSize(snap.stderr.totalBytes)}`,
128
+ ].filter(Boolean);
129
+ return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
130
+ }
131
+
132
+ /** Tail-truncated labeled output section with a pointer at the full log. */
133
+ function outputSection(
134
+ label: string,
135
+ view: TerminalSnapshot["stdout"],
136
+ maxBytes: number,
137
+ maxLines: number,
138
+ ) {
139
+ if (view.totalBytes === 0) return `${label}: (empty)`;
140
+ const truncation = truncateTail(view.text, {
141
+ maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
142
+ maxLines: Math.min(maxLines, DEFAULT_MAX_LINES),
143
+ });
144
+ let text = `${label}:\n${truncation.content}`;
145
+ const shownBytes = truncation.outputBytes;
146
+ if (truncation.truncated || view.truncatedBytes > 0) {
147
+ const where = view.spillPath
148
+ ? `Read the full log at ${view.spillPath}`
149
+ : view.truncatedBytes > 0
150
+ ? `${formatSize(view.truncatedBytes)} of older output was dropped: no full log for this terminal`
151
+ : "This is a display cap, not a data loss; no full log is available for this terminal, so redirect the command's output to a file if you need the rest.";
152
+ text += `\n[${label} truncated: showing last ${formatSize(shownBytes)} of ${formatSize(view.totalBytes)}. ${where}]`;
153
+ }
154
+ return text;
155
+ }
156
+
157
+ export function buildStatusResult(snap: TerminalSnapshot) {
158
+ let text = describeTerminal(snap);
159
+ if (snap.errorText) text += `\nError: ${snap.errorText}`;
160
+ text += `\n\n${outputSection("stdout", snap.stdout, STATUS_STDOUT_MAX, STATUS_STDOUT_MAX_LINES)}`;
161
+ text += `\n\n${outputSection("stderr", snap.stderr, STATUS_STDERR_MAX, STATUS_STDERR_MAX_LINES)}`;
162
+ return text;
163
+ }
164
+
165
+ /** The async completion follow-up injected into the model's context. */
166
+ export function buildTerminalResultMessage(snap: TerminalSnapshot) {
167
+ const how =
168
+ snap.status === "timed_out"
169
+ ? "timed out"
170
+ : snap.status === "killed"
171
+ ? "was killed"
172
+ : `exited (${formatExit(snap)})`;
173
+ let text = `Background terminal ${snap.id} "${snap.title}" ${how} after ${formatElapsed(snap)}.`;
174
+ if (snap.errorText) text += `\nError: ${snap.errorText}`;
175
+ text += `\n\n${outputSection("stdout", snap.stdout, RESULT_STDOUT_MAX, RESULT_STDOUT_MAX_LINES)}`;
176
+ if (snap.stderr.totalBytes > 0) {
177
+ text += `\n\n${outputSection("stderr", snap.stderr, RESULT_STDERR_MAX, RESULT_STDERR_MAX_LINES)}`;
178
+ }
179
+ return text;
180
+ }
181
+
182
+ export function buildKillReport(results: ReadonlyArray<KillResult>) {
183
+ return results
184
+ .map((entry) => {
185
+ if (entry.killed) {
186
+ return `Killed ${entry.id} "${entry.title}" (${entry.exit}).`;
187
+ }
188
+ if (entry.wasRunning) {
189
+ // The natural exit won the race with the kill signal.
190
+ return `${entry.id} "${entry.title}" exited on its own before the kill landed (${entry.exit}).`;
191
+ }
192
+ return `${entry.id} "${entry.title}" was already ${entry.status} (${entry.exit}).`;
193
+ })
194
+ .join("\n");
195
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Deferred one-shot delivery map (same semantics as subagents'): a settled
3
+ * terminal's result is held here until it is either drained into a follow-up
4
+ * message or consumed by a tool call (bg_kill / bg_status) that already
5
+ * returned the settlement itself. Keyed by id, so double delivery is
6
+ * structurally impossible — whoever drains first wins.
7
+ */
8
+ export function createDeferredResultDelivery<T extends { id: string }>() {
9
+ const pending = new Map<string, T>();
10
+
11
+ return {
12
+ defer(result: T) {
13
+ pending.set(result.id, result);
14
+ },
15
+ consume(ids: Iterable<string>) {
16
+ for (const id of ids) pending.delete(id);
17
+ },
18
+ drain() {
19
+ const results = [...pending.values()];
20
+ pending.clear();
21
+ return results;
22
+ },
23
+ clear() {
24
+ pending.clear();
25
+ },
26
+ };
27
+ }
28
+
29
+ /**
30
+ * How a settled background result reaches the model.
31
+ *
32
+ * `followUp` + `triggerTurn` costs a whole turn, which is right when the model
33
+ * is idle and waiting on this result, and wrong for a backlog that piled up
34
+ * while it worked: waking once per stale process forces a turn each, and the
35
+ * model can only answer "that one already finished". `nextTurn` still puts the
36
+ * result in context — carried alongside the user's next message — without
37
+ * demanding a reply.
38
+ */
39
+ export function resultDeliveryOptions(wake: boolean) {
40
+ return wake
41
+ ? ({ deliverAs: "followUp", triggerTurn: true } as const)
42
+ : ({ deliverAs: "nextTurn" } as const);
43
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The async entry-point boundary: one ManagedRuntime shared by every tool
3
+ * handler, disposed on session_shutdown (which runs the manager finalizer →
4
+ * disposeAll → every process tree is killed).
5
+ */
6
+
7
+ import { Cause, Exit, ManagedRuntime, type Effect } from "effect";
8
+ import { TerminalManagerLive } from "./manager.ts";
9
+
10
+ export function createTerminalRuntime() {
11
+ return ManagedRuntime.make(TerminalManagerLive);
12
+ }
13
+
14
+ export type TerminalRuntime = ReturnType<typeof createTerminalRuntime>;
15
+
16
+ /**
17
+ * Run an effect from an async tool handler. Typed failures and defects are
18
+ * converted to thrown Errors (what pi's tool contract expects); interruption
19
+ * (tool AbortSignal) throws `interruptMessage`.
20
+ */
21
+ export async function runTool<A, E>(
22
+ runtime: TerminalRuntime,
23
+ effect: Effect.Effect<A, E>,
24
+ options: { signal?: AbortSignal; interruptMessage?: string } = {},
25
+ ) {
26
+ const exit = await runtime.runPromiseExit(
27
+ effect,
28
+ options.signal ? { signal: options.signal } : undefined,
29
+ );
30
+ if (Exit.isSuccess(exit)) return exit.value;
31
+ if (Cause.hasInterruptsOnly(exit.cause)) {
32
+ throw new Error(options.interruptMessage ?? "Operation was aborted.");
33
+ }
34
+ const [first] = Cause.prettyErrors(exit.cause);
35
+ throw new Error(first?.message ?? Cause.pretty(exit.cause));
36
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Output rendering for the /ps detail view: turns a captured stream's text
3
+ * into sanitized, wrapped display lines. Sanitization happens here — at
4
+ * render time, never at capture time — because raw ANSI/control characters
5
+ * desync the TUI renderer and smear the overlay.
6
+ */
7
+
8
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
9
+ import { sanitizeTerminalText } from "../../../shared/terminal-text.ts";
10
+
11
+ /** Shared terminal sanitizer, retained under the local rendering API name. */
12
+ export const sanitizeText = sanitizeTerminalText;
13
+
14
+ /** Split, sanitize, and wrap a stream's text into display lines. */
15
+ export function buildOutputLines(text: string, width: number) {
16
+ const safeWidth = Math.max(10, width);
17
+ const out: string[] = [];
18
+ for (const raw of text.split("\n")) {
19
+ // Carriage-return progress lines (npm, cargo): keep only the final state.
20
+ const segments = raw.split("\r");
21
+ const finalSegment = segments.at(-1) ?? "";
22
+ const lastSegment =
23
+ finalSegment || [...segments].reverse().find((segment) => segment) || "";
24
+ const clean = sanitizeText(lastSegment);
25
+ if (clean.length === 0) {
26
+ out.push("");
27
+ continue;
28
+ }
29
+ out.push(...wrapTextWithAnsi(clean, safeWidth));
30
+ }
31
+ // Drop one trailing empty line from a trailing "\n" so the tail pin sits
32
+ // on the last real output line.
33
+ if (out.length > 0 && out[out.length - 1] === "") out.pop();
34
+ return out;
35
+ }
36
+
37
+ /**
38
+ * Cache of wrapped lines keyed by (buffer version, width): a chatty process
39
+ * bumps the version per chunk, but renders between chunks (1Hz elapsed ticks,
40
+ * scrolling) must not re-wrap megabytes.
41
+ */
42
+ export function createOutputLineCache() {
43
+ let key: string | undefined;
44
+ let lines: string[] = [];
45
+ return {
46
+ get(text: string, version: number, width: number) {
47
+ const nextKey = `${version}:${width}`;
48
+ if (key !== nextKey) {
49
+ key = nextKey;
50
+ lines = buildOutputLines(text, width);
51
+ }
52
+ return lines;
53
+ },
54
+ };
55
+ }