@vzlentin/harness 0.2.3 → 0.3.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.
@@ -0,0 +1,59 @@
1
+ /** The usage line of a mirror session, above the editor, as Pi's footer shows the usage of its own session. */
2
+ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
+ import * as coding from "@earendil-works/pi-coding-agent";
4
+ import type { ConversationView } from "@earendil-works/pi-durable";
5
+ import { type ChildInfo, childTitle } from "../dist/protocol.js";
6
+ import { lastAnswerUsage, usageTotals } from "../dist/view-state.js";
7
+
8
+ /**
9
+ * The title of the agent, the tokens and the cost of its model requests and tool results, and the context of its last
10
+ * answer that did not abort or fail, against the context window of its model, as Pi's footer shows them for its own
11
+ * session.
12
+ */
13
+ export function usageLine(
14
+ theme: Theme,
15
+ models: ExtensionContext["modelRegistry"],
16
+ child: ChildInfo,
17
+ view: ConversationView,
18
+ ): string {
19
+ const totals = usageTotals(view);
20
+ const last = lastAnswerUsage(view);
21
+ const model = models.find(child.provider, child.model);
22
+ const subscription = model !== undefined && models.isUsingOAuth(model);
23
+ const usage: string[] = [];
24
+
25
+ if (totals.input) usage.push(`↑${formatTokens(totals.input)}`);
26
+
27
+ if (totals.output) usage.push(`↓${formatTokens(totals.output)}`);
28
+
29
+ if (totals.cacheRead) usage.push(`R${formatTokens(totals.cacheRead)}`);
30
+
31
+ if (totals.cacheWrite) usage.push(`W${formatTokens(totals.cacheWrite)}`);
32
+
33
+ if (totals.cost || subscription) usage.push(`$${totals.cost.toFixed(3)}${subscription ? " (sub)" : ""}`);
34
+ const parts = [childTitle(child)];
35
+
36
+ if (usage.length > 0) parts.push(usage.join(" "));
37
+ const contextWindow = model?.contextWindow ?? 0;
38
+
39
+ if (contextWindow > 0) {
40
+ const percent = last === undefined ? 0 : (coding.calculateContextTokens(last) / contextWindow) * 100;
41
+ const context = `${percent.toFixed(1)}%/${formatTokens(contextWindow)}`;
42
+ parts.push(percent > 90 ? theme.fg("error", context) : percent > 70 ? theme.fg("warning", context) : context);
43
+ }
44
+
45
+ return theme.fg("dim", parts.join(" • "));
46
+ }
47
+
48
+ /** A token count as Pi's footer shows it: `950`, `1.2k`, `45k`, `1.5M`. */
49
+ function formatTokens(count: number): string {
50
+ if (count < 1000) return count.toString();
51
+
52
+ if (count < 10_000) return `${(count / 1000).toFixed(1)}k`;
53
+
54
+ if (count < 1_000_000) return `${Math.round(count / 1000)}k`;
55
+
56
+ if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
57
+
58
+ return `${Math.round(count / 1_000_000)}M`;
59
+ }
package/dist/chat.d.ts DELETED
@@ -1,48 +0,0 @@
1
- /**
2
- * A durable conversation drawn with Pi's own message and tool components.
3
- *
4
- * Adapted from `ChatView` in mavam/pi-agents, src/ui/attach.ts. MIT License, Copyright (c) 2026 Matthias Vallentin.
5
- */
6
- import type { ToolResultMessage } from "@earendil-works/pi-ai";
7
- import type { ConversationView } from "@earendil-works/pi-durable";
8
- import { Container, type TUI } from "@earendil-works/pi-tui";
9
- /**
10
- * A tool result as Pi shows its own. pi-durable adds the diagnostics of a call for the model as a `<harness>` block, such
11
- * as `Command exited with code 3`, and the card shows them as plain lines.
12
- */
13
- export declare function displayedResult(result: ToolResultMessage): ToolResultMessage;
14
- /** The transcript of one conversation view, updated in place as the view changes. */
15
- export declare class ChatView {
16
- readonly transcript: Container;
17
- /** The newest card for each call ID: providers can use a call ID again in a later turn. */
18
- private readonly tools;
19
- /** Every card shown, also the older ones whose call ID a later turn used again. */
20
- private readonly cards;
21
- /** The cards that have no final result yet. */
22
- private readonly running;
23
- /** The call IDs whose cards the streamed answer made; its entry takes them over. */
24
- private readonly streamingCalls;
25
- private readonly renderers;
26
- private renderedEntryIds;
27
- private streaming;
28
- /** Ctrl+O shows tool output in full, as in Pi. */
29
- private expanded;
30
- private readonly ui;
31
- private readonly cwd;
32
- constructor(ui: TUI, cwd: string);
33
- apply(view: ConversationView): void;
34
- /** Shows tool output in full, or collapses it again. */
35
- toggleExpanded(): void;
36
- /** Finishes the running cards: a running bash card keeps a timer until it gets a final result. */
37
- stopTimers(): void;
38
- private syncTranscript;
39
- private rebuild;
40
- private addEntry;
41
- private syncStreaming;
42
- private renderer;
43
- /** The newest card of a call, or a new card when the call has none. */
44
- private card;
45
- /** A new card of a call, also for a call ID that an earlier turn used. */
46
- private newCard;
47
- private addText;
48
- }
package/dist/chat.js DELETED
@@ -1,208 +0,0 @@
1
- import { AssistantMessageComponent, createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, getMarkdownTheme, ToolExecutionComponent, UserMessageComponent, } from "@earendil-works/pi-coding-agent";
2
- import { Container, Spacer, Text } from "@earendil-works/pi-tui";
3
- import { color } from "./theme.js";
4
- import { liveOf, userText } from "./view-state.js";
5
- /** Pi's renderers of the tools that children have under the same names. */
6
- const RENDERERS = new Map([
7
- ["read", createReadToolDefinition],
8
- ["bash", createBashToolDefinition],
9
- ["edit", createEditToolDefinition],
10
- ["write", createWriteToolDefinition],
11
- ["grep", createGrepToolDefinition],
12
- ["find", createFindToolDefinition],
13
- ["ls", createLsToolDefinition],
14
- ]);
15
- const DIAGNOSTICS = /^<harness>\n([\s\S]*)\n<\/harness>$/;
16
- /**
17
- * A tool result as Pi shows its own. pi-durable adds the diagnostics of a call for the model as a `<harness>` block, such
18
- * as `Command exited with code 3`, and the card shows them as plain lines.
19
- */
20
- export function displayedResult(result) {
21
- let changed = false;
22
- const content = result.content.map((block) => {
23
- const match = block.type === "text" ? DIAGNOSTICS.exec(block.text) : null;
24
- if (!match)
25
- return block;
26
- changed = true;
27
- const lines = (match[1] ?? "").split("\n").map((line) => line.replace(/^\[(error|warning|info)\] /, ""));
28
- return { type: "text", text: `\n${lines.join("\n")}` };
29
- });
30
- return changed ? { ...result, content } : result;
31
- }
32
- /** The transcript of one conversation view, updated in place as the view changes. */
33
- export class ChatView {
34
- transcript = new Container();
35
- /** The newest card for each call ID: providers can use a call ID again in a later turn. */
36
- tools = new Map();
37
- /** Every card shown, also the older ones whose call ID a later turn used again. */
38
- cards = [];
39
- /** The cards that have no final result yet. */
40
- running = new Set();
41
- /** The call IDs whose cards the streamed answer made; its entry takes them over. */
42
- streamingCalls = new Set();
43
- renderers = new Map();
44
- renderedEntryIds = [];
45
- streaming;
46
- /** Ctrl+O shows tool output in full, as in Pi. */
47
- expanded = false;
48
- ui;
49
- cwd;
50
- constructor(ui, cwd) {
51
- this.ui = ui;
52
- this.cwd = cwd;
53
- }
54
- apply(view) {
55
- const live = liveOf(view);
56
- this.syncTranscript(view.entries);
57
- const message = live.generation?.message;
58
- // A streamed answer without its entry was dropped, for example by a retry.
59
- if (message === undefined && this.streaming !== undefined)
60
- this.rebuild(view.entries);
61
- if (message !== undefined)
62
- this.syncStreaming(message);
63
- for (const slot of live.tools ?? []) {
64
- if (slot.status === "pending")
65
- continue;
66
- const component = this.card(slot.name, slot.callId);
67
- component.setArgsComplete();
68
- if (slot.status !== "running")
69
- continue;
70
- component.markExecutionStarted();
71
- if (slot.output !== undefined || slot.details !== undefined) {
72
- component.updateResult({ content: [{ type: "text", text: slot.output ?? "" }], details: slot.details, isError: false }, true);
73
- }
74
- }
75
- this.transcript.invalidate();
76
- }
77
- /** Shows tool output in full, or collapses it again. */
78
- toggleExpanded() {
79
- this.expanded = !this.expanded;
80
- for (const card of this.cards)
81
- card.setExpanded(this.expanded);
82
- this.transcript.invalidate();
83
- }
84
- /** Finishes the running cards: a running bash card keeps a timer until it gets a final result. */
85
- stopTimers() {
86
- for (const component of this.running)
87
- component.updateResult({ content: [], isError: false }, false);
88
- this.running.clear();
89
- }
90
- syncTranscript(entries) {
91
- // A compaction or a reset replaces the head of the active transcript.
92
- if (this.renderedEntryIds.some((id, index) => entries[index]?.id !== id))
93
- this.rebuild(entries);
94
- for (const entry of entries.slice(this.renderedEntryIds.length)) {
95
- this.addEntry(entry);
96
- this.renderedEntryIds.push(entry.id);
97
- }
98
- }
99
- rebuild(entries) {
100
- this.stopTimers();
101
- this.transcript.clear();
102
- this.cards.length = 0;
103
- this.tools.clear();
104
- this.streamingCalls.clear();
105
- this.renderedEntryIds = [];
106
- this.streaming = undefined;
107
- for (const entry of entries) {
108
- this.addEntry(entry);
109
- this.renderedEntryIds.push(entry.id);
110
- }
111
- }
112
- addEntry(entry) {
113
- const message = entry.model?.[0];
114
- if (entry.kind === "pi.user" && message?.role === "user") {
115
- this.transcript.addChild(new Spacer(1));
116
- this.transcript.addChild(new UserMessageComponent(userText(message.content), getMarkdownTheme()));
117
- }
118
- else if (entry.kind === "pi.assistant" && message?.role === "assistant") {
119
- const component = this.streaming ?? new AssistantMessageComponent(undefined, false, getMarkdownTheme());
120
- if (this.streaming === undefined)
121
- this.transcript.addChild(component);
122
- this.streaming = undefined;
123
- component.updateContent(message, false);
124
- // Only an answer that asks for tools runs its calls; an aborted, failed or cut answer never does.
125
- const ran = message.stopReason === "toolUse";
126
- for (const content of message.content) {
127
- if (content.type !== "toolCall")
128
- continue;
129
- const streamed = this.streamingCalls.has(content.id);
130
- if (!ran && !streamed)
131
- continue;
132
- const card = streamed
133
- ? this.card(content.name, content.id, content.arguments)
134
- : this.newCard(content.name, content.id, content.arguments);
135
- card.setArgsComplete();
136
- if (!ran) {
137
- const text = "Not run: the answer was interrupted.";
138
- card.updateResult({ content: [{ type: "text", text }], isError: true }, false);
139
- this.running.delete(card);
140
- }
141
- }
142
- this.streamingCalls.clear();
143
- }
144
- else if (entry.kind === "pi.tool-result" && message?.role === "toolResult") {
145
- const card = this.card(message.toolName, message.toolCallId);
146
- card.updateResult(displayedResult(message));
147
- this.running.delete(card);
148
- }
149
- else if (entry.kind === "pi.compaction") {
150
- this.addText(color("muted", "[compaction]"));
151
- if (message?.role === "user")
152
- this.addText(userText(message.content));
153
- }
154
- else if (entry.kind === "pi.reset") {
155
- this.addText(color("muted", "[new context]"));
156
- }
157
- }
158
- syncStreaming(message) {
159
- if (this.streaming === undefined) {
160
- this.streaming = new AssistantMessageComponent(undefined, false, getMarkdownTheme());
161
- this.transcript.addChild(this.streaming);
162
- }
163
- this.streaming.updateContent(message, true);
164
- for (const content of message.content) {
165
- if (content.type !== "toolCall")
166
- continue;
167
- if (this.streamingCalls.has(content.id))
168
- this.card(content.name, content.id, content.arguments);
169
- else
170
- this.newCard(content.name, content.id, content.arguments);
171
- this.streamingCalls.add(content.id);
172
- }
173
- }
174
- renderer(toolName) {
175
- let definition = this.renderers.get(toolName);
176
- if (definition === undefined) {
177
- const create = RENDERERS.get(toolName);
178
- if (create === undefined)
179
- return undefined;
180
- definition = create(this.cwd);
181
- this.renderers.set(toolName, definition);
182
- }
183
- return definition;
184
- }
185
- /** The newest card of a call, or a new card when the call has none. */
186
- card(toolName, toolCallId, args) {
187
- const existing = this.tools.get(toolCallId);
188
- if (existing === undefined)
189
- return this.newCard(toolName, toolCallId, args ?? {});
190
- if (args !== undefined)
191
- existing.updateArgs(args);
192
- return existing;
193
- }
194
- /** A new card of a call, also for a call ID that an earlier turn used. */
195
- newCard(toolName, toolCallId, args) {
196
- const component = new ToolExecutionComponent(toolName, toolCallId, args, {}, this.renderer(toolName), this.ui, this.cwd);
197
- component.setExpanded(this.expanded);
198
- this.transcript.addChild(component);
199
- this.cards.push(component);
200
- this.running.add(component);
201
- this.tools.set(toolCallId, component);
202
- return component;
203
- }
204
- addText(text) {
205
- this.transcript.addChild(new Spacer(1));
206
- this.transcript.addChild(new Text(text, 1, 0));
207
- }
208
- }
package/dist/chrome.d.ts DELETED
@@ -1,18 +0,0 @@
1
- import { type ConversationView } from "@earendil-works/pi-durable";
2
- import { type Component } from "@earendil-works/pi-tui";
3
- import type { ChildInfo } from "./protocol.ts";
4
- /** One line, cut to the width, with `hints` at the right end: the first hints go when they do not all fit. */
5
- export declare class Line implements Component {
6
- text: string;
7
- hints: readonly string[];
8
- style: (text: string) => string;
9
- render(width: number): string[];
10
- invalidate(): void;
11
- }
12
- /** `harness │ <title> │ <cwd> │ <state>`. */
13
- export declare function headerText(child: ChildInfo, title: string): string;
14
- /**
15
- * `<provider>/<model> · <thinking> │ <tokens> tok $<cost> │ <used>%/<window> ctx`. The context part shows when
16
- * `contextWindow` is known and the child has answered once.
17
- */
18
- export declare function footerText(child: ChildInfo, view: ConversationView, contextWindow: number | undefined): string;
package/dist/chrome.js DELETED
@@ -1,74 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { calculateContextTokens } from "@earendil-works/pi-coding-agent";
3
- import { UsageDoc } from "@earendil-works/pi-durable";
4
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
- import { bold, color } from "./theme.js";
6
- import { docOf } from "./view-state.js";
7
- /** One line, cut to the width, with `hints` at the right end: the first hints go when they do not all fit. */
8
- export class Line {
9
- text = "";
10
- hints = [];
11
- style = (text) => text;
12
- render(width) {
13
- for (let first = 0; first < this.hints.length; first++) {
14
- const right = `${this.hints.slice(first).join(" · ")} `;
15
- const gap = width - visibleWidth(this.text) - visibleWidth(right);
16
- if (gap >= 2)
17
- return [`${this.text}${" ".repeat(gap)}${this.style(right)}`];
18
- }
19
- return [truncateToWidth(this.text, width)];
20
- }
21
- invalidate() { }
22
- }
23
- /** `harness │ <title> │ <cwd> │ <state>`. */
24
- export function headerText(child, title) {
25
- const state = child.state === "busy"
26
- ? color("warning", "◉ busy")
27
- : child.state === "idle"
28
- ? color("success", "● idle")
29
- : color("error", "⊗ error");
30
- return ` ${[bold(color("accent", "harness")), title, color("muted", tildePath(child.cwd)), state].join(color("borderMuted", " │ "))}`;
31
- }
32
- /**
33
- * `<provider>/<model> · <thinking> │ <tokens> tok $<cost> │ <used>%/<window> ctx`. The context part shows when
34
- * `contextWindow` is known and the child has answered once.
35
- */
36
- export function footerText(child, view, contextWindow) {
37
- const usage = totalUsage(docOf(view, UsageDoc));
38
- const used = lastUsage(view.entries);
39
- const context = contextWindow === undefined || contextWindow === 0 || used === undefined
40
- ? []
41
- : [`${((100 * used) / contextWindow).toFixed(1)}%/${compactCount(contextWindow)} ctx`];
42
- return ` ${color("muted", [
43
- `${child.provider}/${child.model} · ${child.thinking}`,
44
- `${compactCount(usage.tokens)} tok $${usage.cost.toFixed(3)}`,
45
- ...context,
46
- ].join(" │ "))}`;
47
- }
48
- /** The tokens and the cost of every model request of the conversation. */
49
- function totalUsage(usage) {
50
- let tokens = 0;
51
- let cost = 0;
52
- for (const model of Object.values(usage?.models ?? {})) {
53
- tokens += model.totalTokens;
54
- cost += model.cost.total;
55
- }
56
- return { tokens, cost };
57
- }
58
- function compactCount(count) {
59
- return Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(count);
60
- }
61
- function tildePath(path) {
62
- const home = homedir();
63
- return home !== "" && (path === home || path.startsWith(`${home}/`)) ? `~${path.slice(home.length)}` : path;
64
- }
65
- /** The prompt and output tokens of the last answer, which fill the context window of the next request. */
66
- function lastUsage(entries) {
67
- for (const entry of entries.toReversed()) {
68
- const message = entry.model?.[0];
69
- if (entry.kind !== "pi.assistant" || message?.role !== "assistant")
70
- continue;
71
- return calculateContextTokens(message.usage);
72
- }
73
- return undefined;
74
- }
package/dist/picker.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import { type Component } from "@earendil-works/pi-tui";
2
- import type { ModelInfo } from "./protocol.ts";
3
- /**
4
- * A filter over a list of models, fuzzy as Pi's model selector: typing narrows the list and keeps the selected model
5
- * when it still matches, ↑↓ move, Enter picks, Esc closes.
6
- */
7
- export declare class ModelPicker implements Component {
8
- private readonly input;
9
- private readonly items;
10
- private readonly modelOf;
11
- private readonly onSelect;
12
- private readonly onCancel;
13
- private readonly border;
14
- private list;
15
- focused: boolean;
16
- constructor(models: readonly ModelInfo[], current: ModelInfo | undefined, border: (text: string) => string, onSelect: (model: ModelInfo) => void, onCancel: () => void);
17
- /** A frame with full-width lines, so the transcript does not show through. */
18
- render(width: number): string[];
19
- handleInput(data: string): void;
20
- invalidate(): void;
21
- private listOf;
22
- }
package/dist/picker.js DELETED
@@ -1,70 +0,0 @@
1
- import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
2
- import { fuzzyFilter, Input, matchesKey, SelectList, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
3
- /** The keys that the list of the model picker gets; the filter gets the others. */
4
- const LIST_KEYS = ["up", "down", "enter", "escape", "pageUp", "pageDown"];
5
- /**
6
- * A filter over a list of models, fuzzy as Pi's model selector: typing narrows the list and keeps the selected model
7
- * when it still matches, ↑↓ move, Enter picks, Esc closes.
8
- */
9
- export class ModelPicker {
10
- input = new Input();
11
- items;
12
- modelOf = new Map();
13
- onSelect;
14
- onCancel;
15
- border;
16
- list;
17
- focused = true;
18
- constructor(models, current, border, onSelect, onCancel) {
19
- this.items = models.map((model) => {
20
- const item = {
21
- value: `${model.provider}/${model.id}`,
22
- label: `${model.provider}/${model.id}`,
23
- description: model.name,
24
- };
25
- this.modelOf.set(item, model);
26
- return item;
27
- });
28
- this.border = border;
29
- this.onSelect = onSelect;
30
- this.onCancel = onCancel;
31
- this.input.focused = true;
32
- this.list = this.listOf(this.items);
33
- this.list.setSelectedIndex(current === undefined ? 0 : models.indexOf(current));
34
- }
35
- /** A frame with full-width lines, so the transcript does not show through. */
36
- render(width) {
37
- const inner = Math.max(1, width - 2);
38
- const body = [" Model", ...this.input.render(inner), ...this.list.render(inner)].map((line) => {
39
- const cut = truncateToWidth(line, inner);
40
- return `${this.border("│")}${cut}${" ".repeat(Math.max(0, inner - visibleWidth(cut)))}${this.border("│")}`;
41
- });
42
- return [this.border(`╭${"─".repeat(inner)}╮`), ...body, this.border(`╰${"─".repeat(inner)}╯`)];
43
- }
44
- handleInput(data) {
45
- if (LIST_KEYS.some((key) => matchesKey(data, key))) {
46
- this.list.handleInput(data);
47
- return;
48
- }
49
- this.input.handleInput(data);
50
- const selected = this.list.getSelectedItem();
51
- const query = this.input.getValue();
52
- const items = fuzzyFilter(this.items, query, (item) => `${item.label} ${item.description ?? ""}`);
53
- this.list = this.listOf(items);
54
- this.list.setSelectedIndex(selected === null ? 0 : items.indexOf(selected));
55
- }
56
- invalidate() {
57
- this.input.invalidate();
58
- this.list.invalidate();
59
- }
60
- listOf(items) {
61
- const list = new SelectList(items, 10, getSelectListTheme());
62
- list.onSelect = (item) => {
63
- const model = this.modelOf.get(item);
64
- if (model !== undefined)
65
- this.onSelect(model);
66
- };
67
- list.onCancel = this.onCancel;
68
- return list;
69
- }
70
- }
package/dist/theme.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- export declare function color(name: Parameters<Theme["fg"]>[0], text: string): string;
3
- export declare function bold(text: string): string;
package/dist/theme.js DELETED
@@ -1,19 +0,0 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
- // Pi gives its theme object only to extensions. Pi's cards color with the theme that `initTheme()` sets in this module,
10
- // so the viewer loads the same module by its path to color its own lines the same way. The path is not in the `exports`
11
- // of pi-coding-agent, so it holds only for the pinned version 1.3.0.
12
- // SAFETY: this module of pi-coding-agent 1.3.0, the version that package.json pins, exports the `theme` of Pi's cards.
13
- const piTheme = (await import(__rewriteRelativeImportExtension(new URL("./modes/interactive/theme/theme.js", import.meta.resolve("@earendil-works/pi-coding-agent")).href)));
14
- export function color(name, text) {
15
- return piTheme.theme.fg(name, text);
16
- }
17
- export function bold(text) {
18
- return piTheme.theme.bold(text);
19
- }
package/dist/viewer.d.ts DELETED
@@ -1,20 +0,0 @@
1
- import { type Terminal } from "@earendil-works/pi-tui";
2
- declare module "@earendil-works/pi-tui" {
3
- interface Keybindings {
4
- "app.scroll.lineUp": true;
5
- "app.scroll.lineDown": true;
6
- "app.scroll.pageUp": true;
7
- "app.scroll.pageDown": true;
8
- }
9
- }
10
- export type AttachOptions = {
11
- readonly terminal: Terminal;
12
- readonly env: NodeJS.ProcessEnv;
13
- };
14
- /**
15
- * Shows the conversation of the child `handle`, or of the project agent for `agent`, in `terminal` and sends
16
- * what the user types to it. It starts the host when none runs, and rejects before the terminal starts when the handle
17
- * is unknown. It resolves with the exit code: 0 after Ctrl+C or when the child is stopped, 1 when the host closes the
18
- * connection. The child keeps running.
19
- */
20
- export declare function attach(handle: string, options: AttachOptions): Promise<number>;