openshain 0.1.1 → 0.3.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/dist/bin.d.ts +2 -0
- package/dist/bin.js +108 -0
- package/dist/commands/init.d.ts +21 -0
- package/dist/commands/init.js +128 -0
- package/dist/commands/mcp.d.ts +10 -0
- package/dist/commands/mcp.js +17 -0
- package/dist/commands/tools.d.ts +8 -0
- package/dist/commands/tools.js +18 -0
- package/dist/commands/work.d.ts +15 -0
- package/dist/commands/work.js +87 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.js +103 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/labels.d.ts +13 -0
- package/dist/labels.js +59 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.js +73 -0
- package/dist/tui/app.d.ts +9 -0
- package/dist/tui/app.js +191 -0
- package/dist/tui/banner.d.ts +15 -0
- package/dist/tui/banner.js +29 -0
- package/dist/tui/controller.d.ts +59 -0
- package/dist/tui/controller.js +325 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.js +45 -0
- package/dist/tui/lines.d.ts +12 -0
- package/dist/tui/lines.js +68 -0
- package/dist/usage.d.ts +12 -0
- package/dist/usage.js +32 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.js +20 -0
- package/package.json +17 -7
- package/src/bin.ts +5 -39
- package/src/commands/init.ts +9 -9
- package/src/commands/tools.ts +7 -2
- package/src/commands/work.ts +3 -34
- package/src/index.ts +1 -10
- package/src/labels.ts +2 -2
- package/src/{commands/run.ts → report.ts} +6 -59
- package/src/tui/controller.ts +113 -75
- package/src/tui/index.ts +3 -1
- package/src/usage.ts +1 -2
- package/src/workspace.ts +1 -1
package/dist/report.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { pendingQuestions, } from "@openshain/core";
|
|
2
|
+
import { describeInput, truncate } from "./format.js";
|
|
3
|
+
import { failureLabel, rejectionLabel, statusLabel } from "./labels.js";
|
|
4
|
+
import { formatUsage, summarizeUsage } from "./usage.js";
|
|
5
|
+
/** One line for a tool call, a rejection or a failure; nothing for the other events. `names` maps call ids to tool names. */
|
|
6
|
+
export function progressLine(event, names) {
|
|
7
|
+
switch (event.type) {
|
|
8
|
+
case "tool.called": {
|
|
9
|
+
const { callId, name, input } = event.payload;
|
|
10
|
+
names.set(callId, name);
|
|
11
|
+
return `${name} ${describeInput(input)}`.trimEnd();
|
|
12
|
+
}
|
|
13
|
+
case "tool.rejected": {
|
|
14
|
+
const { name, code, reason } = event.payload;
|
|
15
|
+
return `${name} は拒否されました。${rejectionLabel(code)}。${reason}`;
|
|
16
|
+
}
|
|
17
|
+
case "tool.completed": {
|
|
18
|
+
const { callId, content, isError } = event.payload;
|
|
19
|
+
if (!isError)
|
|
20
|
+
return undefined;
|
|
21
|
+
return `${names.get(callId) ?? callId} は失敗しました。${truncate(firstLine(content))}`;
|
|
22
|
+
}
|
|
23
|
+
default:
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function firstLine(content) {
|
|
28
|
+
for (const part of content) {
|
|
29
|
+
if (part.type === "text")
|
|
30
|
+
return part.text.split("\n")[0] ?? "";
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
/** The closing lines: what happened, what it cost, and who acts next. */
|
|
35
|
+
export function report(work, events) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
switch (work.status) {
|
|
38
|
+
case "completed":
|
|
39
|
+
lines.push(`完了。${work.outcome?.summary ?? ""}`.trimEnd());
|
|
40
|
+
for (const artifact of work.outcome?.artifacts ?? []) {
|
|
41
|
+
lines.push(` 書き込み ${artifact.path}${artifact.missing ? " 完了時には読めなかった" : ""}${artifact.claimed ? " エージェントの申告(この Work の Tool は書いていない)" : ""}`);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case "failed":
|
|
45
|
+
lines.push(`失敗。${failureLabel(work.failure?.reason)}。${work.failure?.detail ?? ""}`.trimEnd());
|
|
46
|
+
break;
|
|
47
|
+
case "waiting_input":
|
|
48
|
+
lines.push("利用者の入力を待っています。");
|
|
49
|
+
for (const { question } of pendingQuestions(events))
|
|
50
|
+
lines.push(` 質問 ${question}`);
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
lines.push(`状態は ${statusLabel(work.status)} です。`);
|
|
54
|
+
}
|
|
55
|
+
lines.push(formatUsage(summarizeUsage(events)));
|
|
56
|
+
lines.push(nextActor(work));
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
export function nextActor(work) {
|
|
60
|
+
switch (work.status) {
|
|
61
|
+
case "completed":
|
|
62
|
+
case "cancelled":
|
|
63
|
+
return "次に動く人はいません。";
|
|
64
|
+
case "waiting_input":
|
|
65
|
+
return `次は利用者の番です。openshain の会話で /work resume ${work.id} を実行し、続きを依頼すると質問に答えられます。`;
|
|
66
|
+
case "waiting_approval":
|
|
67
|
+
return "次は利用者の番です。承認が要ります。";
|
|
68
|
+
case "failed":
|
|
69
|
+
return "次は利用者の番です。原因を修正して、もう一度依頼してください。";
|
|
70
|
+
default:
|
|
71
|
+
return "次は model の番です。";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Controller } from "./controller.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The whole terminal: a header, the conversation with the newest rows at the bottom, the input
|
|
4
|
+
* box, and a status line. The conversation scrolls inside the screen with the mouse wheel, PageUp
|
|
5
|
+
* and PageDown. The up and down arrows recall the lines sent before; the input is edited in place.
|
|
6
|
+
*/
|
|
7
|
+
export declare function App({ controller }: {
|
|
8
|
+
controller: Controller;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/tui/app.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { screenLines } from "./lines.js";
|
|
5
|
+
const COLORS = {
|
|
6
|
+
user: "cyan",
|
|
7
|
+
assistant: undefined,
|
|
8
|
+
progress: "gray",
|
|
9
|
+
notice: "yellow",
|
|
10
|
+
question: "magenta",
|
|
11
|
+
line: undefined,
|
|
12
|
+
logo: undefined,
|
|
13
|
+
banner: undefined,
|
|
14
|
+
blank: undefined,
|
|
15
|
+
};
|
|
16
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
17
|
+
/** Rows that are not history: the header, the input box (three rows) and the status line. */
|
|
18
|
+
const CHROME_ROWS = 5;
|
|
19
|
+
function statusText(state, scrolled) {
|
|
20
|
+
if (scrolled > 0)
|
|
21
|
+
return `↑ ${scrolled} 行上を表示中。End で最新へ、ホイールか PageUp と PageDown で移動`;
|
|
22
|
+
const { work, usage } = state.status;
|
|
23
|
+
const parts = [];
|
|
24
|
+
if (work)
|
|
25
|
+
parts.push(`${work.id} ${work.status}`);
|
|
26
|
+
parts.push(`model ${usage.modelCalls} 回、入力 ${usage.inputTokens}、出力 ${usage.outputTokens} トークン`);
|
|
27
|
+
parts.push("/help で使い方");
|
|
28
|
+
return parts.join(" · ");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The whole terminal: a header, the conversation with the newest rows at the bottom, the input
|
|
32
|
+
* box, and a status line. The conversation scrolls inside the screen with the mouse wheel, PageUp
|
|
33
|
+
* and PageDown. The up and down arrows recall the lines sent before; the input is edited in place.
|
|
34
|
+
*/
|
|
35
|
+
export function App({ controller }) {
|
|
36
|
+
const { exit } = useApp();
|
|
37
|
+
const { stdout } = useStdout();
|
|
38
|
+
const [state, setState] = useState(() => ({ ...controller.state() }));
|
|
39
|
+
const [input, setInput] = useState("");
|
|
40
|
+
/** Where the next character goes, counted in characters, not bytes. */
|
|
41
|
+
const [cursor, setCursor] = useState(0);
|
|
42
|
+
const [history, setHistory] = useState([]);
|
|
43
|
+
/** While the arrows walk the history: where we are, and what the input held before. */
|
|
44
|
+
const [recall, setRecall] = useState();
|
|
45
|
+
const [scroll, setScroll] = useState(0);
|
|
46
|
+
const [size, setSize] = useState({ columns: stdout.columns ?? 80, rows: stdout.rows ?? 24 });
|
|
47
|
+
const [tick, setTick] = useState(0);
|
|
48
|
+
useEffect(() => controller.subscribe(() => setState({ ...controller.state() })), [controller]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (state.closed)
|
|
51
|
+
exit();
|
|
52
|
+
}, [state.closed, exit]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const onResize = () => setSize({ columns: stdout.columns ?? 80, rows: stdout.rows ?? 24 });
|
|
55
|
+
stdout.on("resize", onResize);
|
|
56
|
+
return () => {
|
|
57
|
+
stdout.off("resize", onResize);
|
|
58
|
+
};
|
|
59
|
+
}, [stdout]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!state.busy)
|
|
62
|
+
return;
|
|
63
|
+
const timer = setInterval(() => setTick((t) => t + 1), 100);
|
|
64
|
+
return () => clearInterval(timer);
|
|
65
|
+
}, [state.busy]);
|
|
66
|
+
// One row is left to the terminal: drawing exactly its height makes it scroll on every redraw.
|
|
67
|
+
const height = Math.max(CHROME_ROWS + 1, size.rows - 1);
|
|
68
|
+
const width = Math.max(20, size.columns);
|
|
69
|
+
const paneRows = height - CHROME_ROWS;
|
|
70
|
+
const lines = useMemo(() => screenLines(state.entries, width).map((line, row) => ({ ...line, row })), [state.entries, width]);
|
|
71
|
+
const maxScroll = Math.max(0, lines.length - paneRows);
|
|
72
|
+
const scrolled = Math.min(scroll, maxScroll);
|
|
73
|
+
const end = lines.length - scrolled;
|
|
74
|
+
const visible = lines.slice(Math.max(0, end - paneRows), end);
|
|
75
|
+
const page = Math.max(1, paneRows - 1);
|
|
76
|
+
/** Replaces the input and puts the cursor after it. */
|
|
77
|
+
const replaceInput = (text) => {
|
|
78
|
+
setInput(text);
|
|
79
|
+
setCursor([...text].length);
|
|
80
|
+
};
|
|
81
|
+
useInput((ch, key) => {
|
|
82
|
+
// The terminal reports the mouse (SGR). The wheel scrolls the conversation; the rest is ignored.
|
|
83
|
+
if (/\[<\d+;\d+;\d+[Mm]/.test(ch)) {
|
|
84
|
+
let delta = 0;
|
|
85
|
+
for (const m of ch.matchAll(/\[<(6[45]);\d+;\d+M/g))
|
|
86
|
+
delta += m[1] === "64" ? 3 : -3;
|
|
87
|
+
if (delta !== 0)
|
|
88
|
+
setScroll((s) => Math.max(0, Math.min(maxScroll, s + delta)));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (key.ctrl && ch === "c") {
|
|
92
|
+
if (!controller.interrupt())
|
|
93
|
+
void controller.close();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (key.pageUp)
|
|
97
|
+
return setScroll((s) => Math.min(maxScroll, s + page));
|
|
98
|
+
if (key.pageDown)
|
|
99
|
+
return setScroll((s) => Math.max(0, s - page));
|
|
100
|
+
// Up and down walk the lines sent before; below the newest one is what was being typed.
|
|
101
|
+
if (key.upArrow) {
|
|
102
|
+
const index = (recall?.index ?? history.length) - 1;
|
|
103
|
+
if (index < 0)
|
|
104
|
+
return;
|
|
105
|
+
setRecall({ index, draft: recall?.draft ?? input });
|
|
106
|
+
replaceInput(history[index] ?? "");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (key.downArrow) {
|
|
110
|
+
if (!recall)
|
|
111
|
+
return;
|
|
112
|
+
const index = recall.index + 1;
|
|
113
|
+
if (index >= history.length) {
|
|
114
|
+
replaceInput(recall.draft);
|
|
115
|
+
setRecall(undefined);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setRecall({ ...recall, index });
|
|
119
|
+
replaceInput(history[index] ?? "");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Editing: the cursor moves with the arrows, Home and End (also Ctrl-A and Ctrl-E); Backspace
|
|
123
|
+
// deletes before it, Delete at it, Ctrl-U everything before it, Ctrl-K everything after it.
|
|
124
|
+
const chars = [...input];
|
|
125
|
+
const at = Math.min(cursor, chars.length);
|
|
126
|
+
if (key.leftArrow)
|
|
127
|
+
return setCursor(Math.max(0, at - 1));
|
|
128
|
+
if (key.rightArrow)
|
|
129
|
+
return setCursor(Math.min(chars.length, at + 1));
|
|
130
|
+
if (key.home || (key.ctrl && ch === "a"))
|
|
131
|
+
return setCursor(0);
|
|
132
|
+
if (key.end || (key.ctrl && ch === "e"))
|
|
133
|
+
return setCursor(chars.length);
|
|
134
|
+
if (key.backspace) {
|
|
135
|
+
if (at === 0)
|
|
136
|
+
return;
|
|
137
|
+
setInput([...chars.slice(0, at - 1), ...chars.slice(at)].join(""));
|
|
138
|
+
setCursor(at - 1);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (key.delete) {
|
|
142
|
+
setInput([...chars.slice(0, at), ...chars.slice(at + 1)].join(""));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.ctrl && ch === "u") {
|
|
146
|
+
setInput(chars.slice(at).join(""));
|
|
147
|
+
setCursor(0);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (key.ctrl && ch === "k")
|
|
151
|
+
return setInput(chars.slice(0, at).join(""));
|
|
152
|
+
// Ink hands a pasted or quickly typed chunk over whole, and a newline inside it does not set
|
|
153
|
+
// key.return. The line ends at the first newline; what follows becomes the next input.
|
|
154
|
+
const newline = key.return ? 0 : ch.search(/[\r\n]/);
|
|
155
|
+
if (newline >= 0) {
|
|
156
|
+
const line = [...chars.slice(0, at), ch.slice(0, newline), ...chars.slice(at)].join("");
|
|
157
|
+
if (line.trim() !== "")
|
|
158
|
+
setHistory((h) => (h.at(-1) === line ? h : [...h, line]));
|
|
159
|
+
setRecall(undefined);
|
|
160
|
+
replaceInput(ch.slice(newline + 1).replace(/^\n/, ""));
|
|
161
|
+
setScroll(0);
|
|
162
|
+
void controller.submit(line);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (key.tab || key.escape || key.ctrl || key.meta)
|
|
166
|
+
return;
|
|
167
|
+
setInput([...chars.slice(0, at), ch, ...chars.slice(at)].join(""));
|
|
168
|
+
setCursor(at + [...ch].length);
|
|
169
|
+
});
|
|
170
|
+
const asking = state.question !== undefined;
|
|
171
|
+
const chars = [...input];
|
|
172
|
+
const at = Math.min(cursor, chars.length);
|
|
173
|
+
const before = chars.slice(0, at).join("");
|
|
174
|
+
const under = chars[at] ?? "";
|
|
175
|
+
const after = chars.slice(at + 1).join("");
|
|
176
|
+
const bottom = state.busy
|
|
177
|
+
? `${SPINNER[tick % SPINNER.length]} 作業中。Ctrl-C で止める`
|
|
178
|
+
: statusText(state, scrolled);
|
|
179
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: [
|
|
180
|
+
"openshain",
|
|
181
|
+
state.status.company,
|
|
182
|
+
...(state.status.agentName ? [`社員エージェント ${state.status.agentName}`] : []),
|
|
183
|
+
state.status.model,
|
|
184
|
+
].join(" · ") }), _jsx(Box, { flexDirection: "column", height: paneRows, children: visible.map((line) => {
|
|
185
|
+
const color = COLORS[line.kind];
|
|
186
|
+
if (line.segments) {
|
|
187
|
+
return (_jsx(Text, { wrap: "truncate", children: line.segments.map((s) => (_jsx(Text, { color: s.color, children: s.text }, s.at))) }, line.row));
|
|
188
|
+
}
|
|
189
|
+
return (_jsx(Text, { wrap: "truncate", dimColor: line.kind === "banner", ...(color && { color }), children: line.text || " " }, line.row));
|
|
190
|
+
}) }), _jsxs(Box, { borderStyle: "round", borderColor: asking ? "magenta" : "gray", paddingX: 1, children: [_jsx(Text, { color: asking ? "magenta" : "cyan", children: asking ? "答え> " : "> " }), _jsx(Text, { children: before }), under === "" ? _jsx(Text, { dimColor: true, children: "\u258C" }) : _jsx(Text, { inverse: true, children: under }), _jsx(Text, { children: after })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: bottom })] }));
|
|
191
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The version of the openshain command, from its package.json. */
|
|
2
|
+
export declare const VERSION: string;
|
|
3
|
+
/**
|
|
4
|
+
* The wordmark as `oh-my-logo "openshain" --filled --block-font chrome` draws it, kept here so the
|
|
5
|
+
* screen needs neither the network nor another dependency to show it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LOGO_ROWS: readonly string[];
|
|
8
|
+
export interface Segment {
|
|
9
|
+
text: string;
|
|
10
|
+
color: string;
|
|
11
|
+
/** Position in the row; the screen keys by it. */
|
|
12
|
+
at: number;
|
|
13
|
+
}
|
|
14
|
+
/** One colored segment per character, so the gradient runs across the row. */
|
|
15
|
+
export declare function logoSegments(row: string): Segment[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import cli from "../../package.json" with { type: "json" };
|
|
2
|
+
/** The version of the openshain command, from its package.json. */
|
|
3
|
+
export const VERSION = cli.version;
|
|
4
|
+
/**
|
|
5
|
+
* The wordmark as `oh-my-logo "openshain" --filled --block-font chrome` draws it, kept here so the
|
|
6
|
+
* screen needs neither the network nor another dependency to show it.
|
|
7
|
+
*/
|
|
8
|
+
export const LOGO_ROWS = Object.freeze([
|
|
9
|
+
" ╔═╗ ╔═╗ ╔═╗ ╔╗╔ ╔═╗ ╦ ╦ ╔═╗ ╦ ╔╗╔",
|
|
10
|
+
" ║ ║ ╠═╝ ║╣ ║║║ ╚═╗ ╠═╣ ╠═╣ ║ ║║║",
|
|
11
|
+
" ╚═╝ ╩ ╚═╝ ╝╚╝ ╚═╝ ╩ ╩ ╩ ╩ ╩ ╝╚╝",
|
|
12
|
+
]);
|
|
13
|
+
/** oh-my-logo's grad-blue palette, from the left edge of the wordmark to the right. */
|
|
14
|
+
const GRADIENT = [
|
|
15
|
+
[78, 168, 255],
|
|
16
|
+
[127, 136, 255],
|
|
17
|
+
];
|
|
18
|
+
/** One colored segment per character, so the gradient runs across the row. */
|
|
19
|
+
export function logoSegments(row) {
|
|
20
|
+
const chars = [...row];
|
|
21
|
+
const last = Math.max(1, chars.length - 1);
|
|
22
|
+
return chars.map((text, at) => {
|
|
23
|
+
const t = at / last;
|
|
24
|
+
const [from, to] = GRADIENT;
|
|
25
|
+
const channel = (k) => Math.round(from[k] + (to[k] - from[k]) * t);
|
|
26
|
+
const hex = [channel(0), channel(1), channel(2)].map((v) => v.toString(16).padStart(2, "0"));
|
|
27
|
+
return { text, color: `#${hex.join("")}`, at };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type RuntimeProviders, type WorkId, WorkStore } from "@openshain/core";
|
|
2
|
+
import { statusLabel } from "../labels.ts";
|
|
3
|
+
/** logo and banner are the rows shown once when the screen opens: the wordmark, the version, the folder. */
|
|
4
|
+
export type EntryKind = "user" | "assistant" | "progress" | "notice" | "question" | "line" | "logo" | "banner";
|
|
5
|
+
export interface Entry {
|
|
6
|
+
id: number;
|
|
7
|
+
kind: EntryKind;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ControllerState {
|
|
11
|
+
/**
|
|
12
|
+
* Everything shown so far, in order. An entry never changes once added, and the array is
|
|
13
|
+
* replaced rather than mutated: the screen tells new entries apart by the array's identity.
|
|
14
|
+
*/
|
|
15
|
+
entries: Entry[];
|
|
16
|
+
busy: boolean;
|
|
17
|
+
/** A question a work is asking; the next line the person types answers it. */
|
|
18
|
+
question?: string;
|
|
19
|
+
closed: boolean;
|
|
20
|
+
status: {
|
|
21
|
+
company: string;
|
|
22
|
+
model: string;
|
|
23
|
+
/** The name the agent goes by in this conversation. */
|
|
24
|
+
agentName?: string;
|
|
25
|
+
work?: {
|
|
26
|
+
id: string;
|
|
27
|
+
status: string;
|
|
28
|
+
};
|
|
29
|
+
usage: {
|
|
30
|
+
modelCalls: number;
|
|
31
|
+
inputTokens: number;
|
|
32
|
+
outputTokens: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface Controller {
|
|
37
|
+
readonly sessionId: WorkId;
|
|
38
|
+
state(): ControllerState;
|
|
39
|
+
subscribe(listener: () => void): () => void;
|
|
40
|
+
/** A line the person typed: an answer, a slash command, or something to say. */
|
|
41
|
+
submit(line: string): Promise<void>;
|
|
42
|
+
/** Ctrl-C: stops the running work, taking back a question it waits on; false when nothing was running. */
|
|
43
|
+
interrupt(): boolean;
|
|
44
|
+
/** Stops whatever is running, then ends the session. A second call waits for the same close. */
|
|
45
|
+
close(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export interface ControllerOptions {
|
|
48
|
+
workspaceRoot: string;
|
|
49
|
+
providers: RuntimeProviders;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The state behind the screen: a session, the works it starts, and the lines to show. The
|
|
53
|
+
* conversation reaches the runtime only as an MCP client of the workspace's own server, the way
|
|
54
|
+
* any other agent does; the records are read directly for the closing lines.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createController(options: ControllerOptions): Promise<Controller>;
|
|
57
|
+
/** Lines that close a work in the screen: the CLI's closing lines without the summary, which the agent relays. */
|
|
58
|
+
export declare function workReport(store: WorkStore, workId: WorkId): Promise<string[]>;
|
|
59
|
+
export { statusLabel };
|