@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,293 @@
1
+ /**
2
+ * Transcript rendering for the takeover view: turns a SubagentSnapshot's
3
+ * normalized transcript + live state into width-bounded TUI lines. The domain
4
+ * stream stays normalized and bounded; this renderer only formats its previews.
5
+ */
6
+
7
+ import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ Markdown,
10
+ truncateToWidth,
11
+ visibleWidth,
12
+ wrapTextWithAnsi,
13
+ type DefaultTextStyle,
14
+ type MarkdownOptions,
15
+ } from "@earendil-works/pi-tui";
16
+ import { sanitizeTerminalText } from "../../../shared/terminal-text.ts";
17
+ import type { SubagentSnapshot, TranscriptItem } from "../domain.ts";
18
+
19
+ const MAX_CACHED_WIDTHS_PER_ITEM = 2;
20
+
21
+ /**
22
+ * Strip raw ANSI codes, expand tabs, and drop control chars. Terminal-expanded
23
+ * tabs (and stray escapes) make lines wider than the width we declare to the
24
+ * TUI, which desyncs the renderer and smears the overlay.
25
+ */
26
+ export function sanitizeText(text: string): string {
27
+ return sanitizeTerminalText(text);
28
+ }
29
+
30
+ function singleLinePreview(text: string) {
31
+ // Keep meaningful whitespace inside parsed commands and paths; only fold
32
+ // physical line breaks so a preview remains one terminal row.
33
+ return sanitizeText(text).replace(/\r?\n/g, " ↵ ");
34
+ }
35
+
36
+ function compactPreview(text: string) {
37
+ return singleLinePreview(text).trim();
38
+ }
39
+
40
+ function stringField(value: Record<string, unknown>, field: string) {
41
+ const candidate = value[field];
42
+ return typeof candidate === "string" && candidate.length > 0
43
+ ? singleLinePreview(candidate)
44
+ : undefined;
45
+ }
46
+
47
+ function parsedArgs(preview: string) {
48
+ try {
49
+ const value: unknown = JSON.parse(preview);
50
+ return value && typeof value === "object" && !Array.isArray(value)
51
+ ? (value as Record<string, unknown>)
52
+ : undefined;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ /** Turn common tool arguments into a useful, bounded summary without retaining raw args. */
59
+ export function summarizeToolArgs(name: string, argsPreview?: string) {
60
+ if (!argsPreview) return undefined;
61
+
62
+ const fallback = compactPreview(argsPreview);
63
+ if (!fallback || fallback === "{}") return undefined;
64
+
65
+ const args = parsedArgs(fallback);
66
+ if (!args) return fallback;
67
+
68
+ const tool = name.toLowerCase();
69
+ if (tool === "bash") return stringField(args, "command") ?? fallback;
70
+ if (tool === "read" || tool === "write" || tool === "edit") {
71
+ return stringField(args, "path") ?? fallback;
72
+ }
73
+ if (tool === "rg" || tool === "fd") {
74
+ const pattern = stringField(args, "pattern");
75
+ const path = stringField(args, "path");
76
+ if (pattern && path) return `${pattern} · ${path}`;
77
+ return pattern ?? path ?? fallback;
78
+ }
79
+ return fallback;
80
+ }
81
+
82
+ function transcriptMarkdownTheme() {
83
+ const theme = getMarkdownTheme();
84
+ return {
85
+ ...theme,
86
+ // Markdown normalizes unordered lists to "- "; use a display bullet so
87
+ // transcript list syntax is never confused with unrendered source.
88
+ listBullet: (text: string) =>
89
+ theme.listBullet(text.replace(/^(?:[-+*]) /, "• ")),
90
+ };
91
+ }
92
+
93
+ function renderMarkdown(
94
+ text: string,
95
+ width: number,
96
+ defaultTextStyle?: DefaultTextStyle,
97
+ options?: MarkdownOptions,
98
+ ) {
99
+ const clean = sanitizeText(text).trim();
100
+ if (!clean) return [];
101
+ const markdown = new Markdown(
102
+ clean,
103
+ 0,
104
+ 0,
105
+ transcriptMarkdownTheme(),
106
+ defaultTextStyle,
107
+ options,
108
+ );
109
+ return markdown
110
+ .render(Math.max(1, width))
111
+ .map((line) => truncateToWidth(line, width));
112
+ }
113
+
114
+ function renderUserText(theme: Theme, text: string, width: number) {
115
+ const lines = renderMarkdown(
116
+ text,
117
+ Math.max(1, width - 2),
118
+ { color: (content: string) => theme.fg("userMessageText", content) },
119
+ { preserveOrderedListMarkers: true, preserveBackslashEscapes: true },
120
+ );
121
+ return lines.map((line, index) =>
122
+ truncateToWidth(
123
+ (index === 0 ? theme.fg("accent", "> ") : " ") + line,
124
+ width,
125
+ ),
126
+ );
127
+ }
128
+
129
+ function renderThinking(theme: Theme, text: string, width: number) {
130
+ const reasoning = sanitizeText(text).trim();
131
+ if (!reasoning) return [];
132
+ const out: string[] = [];
133
+ const prefix = theme.fg("dim", "~ ");
134
+ const defaultTextStyle = {
135
+ color: (content: string) => theme.fg("muted", content),
136
+ italic: true,
137
+ } satisfies DefaultTextStyle;
138
+ const lines = renderMarkdown(
139
+ reasoning,
140
+ Math.max(1, width - 2),
141
+ defaultTextStyle,
142
+ );
143
+ for (let i = 0; i < lines.length; i++) {
144
+ out.push(truncateToWidth((i === 0 ? prefix : " ") + lines[i], width));
145
+ }
146
+ return out;
147
+ }
148
+
149
+ function renderAssistantItem(
150
+ theme: Theme,
151
+ item: Extract<TranscriptItem, { kind: "assistant" }>,
152
+ width: number,
153
+ ) {
154
+ const out: string[] = [];
155
+ for (const part of item.parts) {
156
+ if (part.type === "text") {
157
+ out.push(...renderMarkdown(part.text, width));
158
+ } else if (part.type === "thinking") {
159
+ out.push(
160
+ ...renderThinking(
161
+ theme,
162
+ part.redacted ? "[redacted reasoning]" : part.text,
163
+ width,
164
+ ),
165
+ );
166
+ } else if (part.type === "toolCall") {
167
+ const preview = summarizeToolArgs(part.name, part.argsPreview);
168
+ const line =
169
+ theme.fg("muted", "→ ") +
170
+ theme.fg("toolTitle", part.name) +
171
+ (preview ? theme.fg("dim", ` ${preview}`) : "");
172
+ out.push(truncateToWidth(line, width));
173
+ }
174
+ }
175
+ return out;
176
+ }
177
+
178
+ function renderToolResultItem(
179
+ theme: Theme,
180
+ item: Extract<TranscriptItem, { kind: "toolResult" }>,
181
+ width: number,
182
+ ) {
183
+ const firstLine =
184
+ sanitizeText(item.outputPreview ?? "")
185
+ .split("\n")
186
+ .find((line) => line.trim()) ?? "";
187
+ const label = item.isError
188
+ ? theme.fg("error", " error: ")
189
+ : theme.fg("dim", " output: ");
190
+ return [
191
+ truncateToWidth(label + theme.fg("dim", firstLine || "(no output)"), width),
192
+ ];
193
+ }
194
+
195
+ function renderTranscriptItem(
196
+ theme: Theme,
197
+ item: TranscriptItem,
198
+ width: number,
199
+ ) {
200
+ if (item.kind === "user") return renderUserText(theme, item.text, width);
201
+ if (item.kind === "assistant") return renderAssistantItem(theme, item, width);
202
+ return renderToolResultItem(theme, item, width);
203
+ }
204
+
205
+ /**
206
+ * Caches finalized transcript items by identity and width. Live state remains
207
+ * uncached because it changes on every stream tick; callers clear this cache
208
+ * from their component's invalidate() when Pi changes theme.
209
+ */
210
+ export class TranscriptRenderer {
211
+ private itemCache = new WeakMap<TranscriptItem, Map<number, string[]>>();
212
+
213
+ render(snap: SubagentSnapshot, width: number, theme: Theme) {
214
+ const out: string[] = [];
215
+
216
+ for (const item of snap.transcript) {
217
+ const cached = this.itemCache.get(item)?.get(width);
218
+ const lines = cached ?? renderTranscriptItem(theme, item, width);
219
+ if (!cached) {
220
+ const widths = this.itemCache.get(item) ?? new Map<number, string[]>();
221
+ if (widths.size >= MAX_CACHED_WIDTHS_PER_ITEM) {
222
+ const oldestWidth = widths.keys().next().value;
223
+ if (oldestWidth !== undefined) widths.delete(oldestWidth);
224
+ }
225
+ widths.set(width, lines);
226
+ this.itemCache.set(item, widths);
227
+ }
228
+ if (lines.length > 0) out.push(...lines, "");
229
+ }
230
+ while (out.length > 0 && out[out.length - 1] === "") out.pop();
231
+
232
+ // Live streaming assistant buffers (cleared when the finalized message lands).
233
+ if (snap.liveAssistant) {
234
+ const { thinking, text } = snap.liveAssistant;
235
+ const before = out.length;
236
+ if (out.length > 0) out.push("");
237
+ if (thinking.trim()) out.push(...renderThinking(theme, thinking, width));
238
+ if (text.trim()) out.push(...renderMarkdown(text, width));
239
+ if (out.length === before + 1) out.pop();
240
+ }
241
+
242
+ // Live tool executions (present until the ToolEnd lands in the transcript).
243
+ for (const tool of snap.liveTools) {
244
+ if (out.length > 0) out.push("");
245
+ const marker = tool.done
246
+ ? tool.isError
247
+ ? theme.fg("error", "error")
248
+ : theme.fg("success", "done")
249
+ : theme.fg("warning", "running");
250
+ const args = summarizeToolArgs(tool.name, tool.argsPreview);
251
+ let line = `${theme.fg("toolTitle", tool.name)}${args ? theme.fg("dim", ` ${args}`) : ""} · ${marker}`;
252
+ const preview = tool.outputPreview && compactPreview(tool.outputPreview);
253
+ if (preview) line += theme.fg("dim", ` · ${preview}`);
254
+ out.push(truncateToWidth(line, width));
255
+ }
256
+
257
+ // Queued steering/follow-up messages: show them immediately so Enter
258
+ // visibly acknowledges the user's input instead of appearing to do nothing.
259
+ for (const message of snap.queued) {
260
+ if (out.length > 0) out.push("");
261
+ const prefix = theme.fg("warning", `> [queued ${message.kind}] `);
262
+ const wrapped = wrapTextWithAnsi(
263
+ sanitizeText(message.text),
264
+ Math.max(1, width - visibleWidth(prefix)),
265
+ );
266
+ for (let i = 0; i < wrapped.length; i++) {
267
+ out.push(
268
+ truncateToWidth(
269
+ (i === 0 ? prefix : " ".repeat(visibleWidth(prefix))) +
270
+ theme.fg("muted", wrapped[i]),
271
+ width,
272
+ ),
273
+ );
274
+ }
275
+ }
276
+
277
+ return out;
278
+ }
279
+
280
+ invalidate() {
281
+ this.itemCache = new WeakMap();
282
+ }
283
+ }
284
+
285
+ /** Render a subagent's conversation as width-bounded lines. */
286
+ export function buildTranscriptLines(
287
+ snap: SubagentSnapshot,
288
+ width: number,
289
+ theme: Theme,
290
+ renderer?: TranscriptRenderer,
291
+ ) {
292
+ return (renderer ?? new TranscriptRenderer()).render(snap, width, theme);
293
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ getMarkdownTheme,
3
+ keyHint,
4
+ type Theme,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { Markdown, Text } from "@earendil-works/pi-tui";
7
+ import { sanitizeText } from "./transcript.ts";
8
+
9
+ const COLLAPSED_MAX_LINES = 12;
10
+ const MAX_STATUS_ROWS = 4;
11
+
12
+ export interface WaitResultItem {
13
+ id: string;
14
+ title?: string;
15
+ status?: string;
16
+ }
17
+
18
+ export interface WaitResultDetails {
19
+ results?: WaitResultItem[];
20
+ }
21
+
22
+ export function buildWaitResultPreview(
23
+ content: string,
24
+ details: WaitResultDetails | undefined,
25
+ theme: Theme,
26
+ ) {
27
+ const results = details?.results ?? [];
28
+ const failed = results.filter((result) => result.status === "error").length;
29
+ const header =
30
+ theme.fg(failed > 0 ? "warning" : "success", "■") +
31
+ ` ${theme.fg("accent", theme.bold(`${results.length} subagent${results.length === 1 ? "" : "s"} settled`))}` +
32
+ (failed > 0 ? theme.fg("error", ` · ${failed} failed`) : "");
33
+ const lines = [header];
34
+
35
+ for (const result of results.slice(0, MAX_STATUS_ROWS)) {
36
+ const isFailure = result.status === "error";
37
+ const icon = theme.fg(
38
+ isFailure ? "error" : "success",
39
+ isFailure ? "x" : "✓",
40
+ );
41
+ lines.push(
42
+ ` ${icon} ${theme.fg("accent", result.id)}${result.title ? theme.fg("muted", ` · ${result.title}`) : ""}${theme.fg("dim", ` · ${result.status ?? "settled"}`)}`,
43
+ );
44
+ }
45
+ if (results.length > MAX_STATUS_ROWS) {
46
+ lines.push(theme.fg("dim", ` … ${results.length - MAX_STATUS_ROWS} more`));
47
+ }
48
+
49
+ const cleanLines = sanitizeText(content)
50
+ .split("\n")
51
+ .map((line) => line.trimEnd())
52
+ .filter((line) => line.trim() && line.trim() !== "---");
53
+ const leadingHeader = cleanLines[0]?.startsWith("## ") ? 1 : 0;
54
+ const body = cleanLines.slice(leadingHeader);
55
+ const available = Math.max(1, COLLAPSED_MAX_LINES - lines.length - 1);
56
+ for (const line of body.slice(0, available)) {
57
+ lines.push(theme.fg("toolOutput", line));
58
+ }
59
+
60
+ const hidden = Math.max(0, body.length - available);
61
+ if (hidden > 0) {
62
+ lines.push(
63
+ theme.fg(
64
+ "dim",
65
+ `… ${hidden} more line${hidden === 1 ? "" : "s"} · ${keyHint("app.tools.expand", "to expand")}`,
66
+ ),
67
+ );
68
+ }
69
+ return lines.join("\n");
70
+ }
71
+
72
+ export function renderWaitResult(
73
+ content: string,
74
+ details: WaitResultDetails | undefined,
75
+ expanded: boolean,
76
+ theme: Theme,
77
+ ) {
78
+ if (!expanded) {
79
+ return new Text(buildWaitResultPreview(content, details, theme), 0, 0);
80
+ }
81
+
82
+ const markdown = new Markdown(
83
+ sanitizeText(content),
84
+ 0,
85
+ 0,
86
+ getMarkdownTheme(),
87
+ );
88
+ return markdown;
89
+ }
@@ -0,0 +1,172 @@
1
+ import {
2
+ CustomEditor,
3
+ type ExtensionAPI,
4
+ type ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ loadSetupConfig,
8
+ SETUP_CONFIG_CHANGED_CHANNEL,
9
+ } from "../shared/setup-config.ts";
10
+ import { predictNextAction } from "./src/predictor.ts";
11
+ import {
12
+ createRunBoundary,
13
+ getRunEntries,
14
+ serializeRunTranscript,
15
+ } from "./src/transcript.ts";
16
+ import {
17
+ NextActionSuggestionEditor,
18
+ NextActionSuggestionState,
19
+ } from "./src/ui.ts";
20
+
21
+ const STATUS_KEY = "suggestions";
22
+ const SHUTDOWN_WAIT_MS = 1_000;
23
+
24
+ async function waitForCancellation(
25
+ task: Promise<void> | undefined,
26
+ timeoutMs: number,
27
+ ) {
28
+ if (!task) return;
29
+ let timeout: ReturnType<typeof setTimeout> | undefined;
30
+ try {
31
+ await Promise.race([
32
+ task.catch(() => undefined),
33
+ new Promise<void>((resolve) => {
34
+ timeout = setTimeout(resolve, timeoutMs);
35
+ }),
36
+ ]);
37
+ } finally {
38
+ if (timeout) clearTimeout(timeout);
39
+ }
40
+ }
41
+
42
+ interface ActivePrediction {
43
+ readonly controller: AbortController;
44
+ readonly task: Promise<void>;
45
+ }
46
+
47
+ export default function (pi: ExtensionAPI) {
48
+ const runBoundary = createRunBoundary();
49
+ const suggestionState = new NextActionSuggestionState();
50
+ let activePrediction: ActivePrediction | undefined;
51
+ let sessionActive = false;
52
+ let statusContext: ExtensionContext | undefined;
53
+ let requestEditorRender: (() => void) | undefined;
54
+
55
+ const updateStatus = () => {
56
+ statusContext?.ui.setStatus(
57
+ STATUS_KEY,
58
+ activePrediction
59
+ ? statusContext.ui.theme.fg("muted", "✦ predicting next input…")
60
+ : undefined,
61
+ );
62
+ };
63
+
64
+ const cancelPrediction = () => {
65
+ const current = activePrediction;
66
+ activePrediction = undefined;
67
+ suggestionState.cancel();
68
+ current?.controller.abort();
69
+ updateStatus();
70
+ requestEditorRender?.();
71
+ };
72
+
73
+ const installSuggestionEditor = (ctx: ExtensionContext) => {
74
+ if (ctx.mode !== "tui") return;
75
+ const previous = ctx.ui.getEditorComponent();
76
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => {
77
+ const base =
78
+ previous?.(tui, theme, keybindings) ??
79
+ new CustomEditor(tui, theme, keybindings);
80
+ requestEditorRender = () => tui.requestRender();
81
+ return new NextActionSuggestionEditor(
82
+ base,
83
+ keybindings,
84
+ suggestionState,
85
+ cancelPrediction,
86
+ requestEditorRender,
87
+ (text) => ctx.ui.theme.fg("dim", text),
88
+ );
89
+ });
90
+ };
91
+
92
+ pi.on("session_start", (_event, ctx) => {
93
+ sessionActive = ctx.mode === "tui";
94
+ statusContext = ctx;
95
+ runBoundary.reset();
96
+ cancelPrediction();
97
+ installSuggestionEditor(ctx);
98
+ });
99
+
100
+ pi.on("input", () => {
101
+ cancelPrediction();
102
+ return { action: "continue" };
103
+ });
104
+
105
+ pi.on("before_agent_start", (_event, ctx) => {
106
+ if (ctx.mode !== "tui") return;
107
+ cancelPrediction();
108
+ runBoundary.begin(ctx.sessionManager.getLeafId());
109
+ });
110
+
111
+ pi.on("agent_settled", (_event, ctx) => {
112
+ const run = runBoundary.settle();
113
+ if (!run || ctx.mode !== "tui" || !sessionActive) return;
114
+
115
+ const entries = getRunEntries(
116
+ ctx.sessionManager.getBranch(),
117
+ run.baselineLeafId,
118
+ );
119
+ if (entries.length === 0) return;
120
+
121
+ const setup = loadSetupConfig();
122
+ if (!setup.suggestions.enabled || !setup.suggestions.model) return;
123
+
124
+ cancelPrediction();
125
+ const token = suggestionState.begin();
126
+ const controller = new AbortController();
127
+ statusContext = ctx;
128
+ const task = (async () => {
129
+ try {
130
+ const suggestion = await predictNextAction({
131
+ modelRegistry: ctx.modelRegistry,
132
+ config: setup.suggestions.model!,
133
+ transcript: serializeRunTranscript(entries),
134
+ signal: controller.signal,
135
+ });
136
+ if (!sessionActive || controller.signal.aborted) return;
137
+ suggestionState.offer(
138
+ token,
139
+ suggestion,
140
+ ctx.ui.getEditorText().length === 0,
141
+ );
142
+ requestEditorRender?.();
143
+ } catch {
144
+ if (!controller.signal.aborted) {
145
+ suggestionState.offer(token, undefined, true);
146
+ }
147
+ }
148
+ })().finally(() => {
149
+ if (activePrediction?.controller === controller) {
150
+ activePrediction = undefined;
151
+ updateStatus();
152
+ }
153
+ });
154
+
155
+ activePrediction = { controller, task };
156
+ updateStatus();
157
+ void task;
158
+ });
159
+
160
+ pi.events.on(SETUP_CONFIG_CHANGED_CHANNEL, cancelPrediction);
161
+
162
+ pi.on("session_shutdown", async () => {
163
+ sessionActive = false;
164
+ runBoundary.reset();
165
+ const task = activePrediction?.task;
166
+ cancelPrediction();
167
+ await waitForCancellation(task, SHUTDOWN_WAIT_MS);
168
+ statusContext?.ui.setStatus(STATUS_KEY, undefined);
169
+ statusContext = undefined;
170
+ requestEditorRender = undefined;
171
+ });
172
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ loadSetupConfig,
3
+ type ReasoningLevel,
4
+ type SuggestionModelConfig,
5
+ } from "../../shared/setup-config.ts";
6
+
7
+ export type SuggestionConfig = SuggestionModelConfig;
8
+ export type { ReasoningLevel };
9
+
10
+ export function loadSuggestionConfig() {
11
+ return loadSetupConfig().suggestions.model;
12
+ }