openshain 0.2.0 → 0.4.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.
- package/NOTICE +4 -0
- package/dist/bin.js +4 -33
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +7 -6
- package/dist/commands/tools.js +1 -2
- package/dist/commands/work.d.ts +1 -9
- package/dist/commands/work.js +9 -22
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/labels.d.ts +1 -2
- package/dist/labels.js +3 -0
- package/dist/preview.d.ts +13 -0
- package/dist/preview.js +141 -0
- package/dist/report.d.ts +6 -0
- package/dist/{commands/run.js → report.js} +6 -38
- package/dist/tui/app.js +38 -5
- package/dist/tui/banner.d.ts +3 -8
- package/dist/tui/banner.js +2 -2
- package/dist/tui/controller.d.ts +30 -5
- package/dist/tui/controller.js +306 -77
- package/dist/tui/index.js +1 -1
- package/dist/tui/lines.d.ts +5 -3
- package/dist/tui/lines.js +31 -3
- package/dist/tui/markdown.d.ts +15 -0
- package/dist/tui/markdown.js +199 -0
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +1 -1
- package/dist/workspace.js +1 -1
- package/package.json +9 -7
- package/src/bin.ts +4 -38
- package/src/commands/init.ts +7 -6
- package/src/commands/tools.ts +7 -2
- package/src/commands/work.ts +10 -34
- package/src/index.ts +1 -10
- package/src/labels.ts +4 -2
- package/src/preview.ts +157 -0
- package/src/{commands/run.ts → report.ts} +6 -59
- package/src/tui/app.tsx +75 -13
- package/src/tui/banner.ts +4 -10
- package/src/tui/controller.ts +338 -77
- package/src/tui/index.ts +3 -1
- package/src/tui/lines.ts +36 -6
- package/src/tui/markdown.ts +214 -0
- package/src/usage.ts +1 -2
- package/src/workspace.ts +1 -1
- package/dist/commands/run.d.ts +0 -22
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { marked, type Token, type Tokens } from "marked";
|
|
2
|
+
import { displayWidth } from "../format.ts";
|
|
3
|
+
|
|
4
|
+
/** A piece of a row that carries one style. A row is a list of these, drawn left to right. */
|
|
5
|
+
export interface Span {
|
|
6
|
+
text: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
bold?: boolean;
|
|
9
|
+
italic?: boolean;
|
|
10
|
+
dim?: boolean;
|
|
11
|
+
strikethrough?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** What each part of a reply looks like on the screen. */
|
|
15
|
+
const STYLE = {
|
|
16
|
+
heading: { bold: true, color: "cyan" },
|
|
17
|
+
code: { color: "green" },
|
|
18
|
+
link: { color: "blue" },
|
|
19
|
+
quote: { dim: true },
|
|
20
|
+
rule: { dim: true },
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
const QUOTE_MARKER = "▎ ";
|
|
24
|
+
const CODE_MARKER = "│ ";
|
|
25
|
+
const BULLETS = ["•", "◦", "‣"] as const;
|
|
26
|
+
|
|
27
|
+
function styled(text: string, style: Omit<Span, "text">): Span {
|
|
28
|
+
return { text, ...style };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The inline tokens of one block, flattened into styled pieces. */
|
|
32
|
+
function inline(tokens: Token[] | undefined, style: Omit<Span, "text">): Span[] {
|
|
33
|
+
if (!tokens) return [];
|
|
34
|
+
const spans: Span[] = [];
|
|
35
|
+
for (const token of tokens) {
|
|
36
|
+
switch (token.type) {
|
|
37
|
+
case "strong":
|
|
38
|
+
spans.push(...inline(token.tokens, { ...style, bold: true }));
|
|
39
|
+
break;
|
|
40
|
+
case "em":
|
|
41
|
+
spans.push(...inline(token.tokens, { ...style, italic: true }));
|
|
42
|
+
break;
|
|
43
|
+
case "del":
|
|
44
|
+
spans.push(...inline(token.tokens, { ...style, strikethrough: true }));
|
|
45
|
+
break;
|
|
46
|
+
case "codespan":
|
|
47
|
+
spans.push(styled((token as Tokens.Codespan).text, { ...style, ...STYLE.code }));
|
|
48
|
+
break;
|
|
49
|
+
case "link": {
|
|
50
|
+
const link = token as Tokens.Link;
|
|
51
|
+
spans.push(...inline(link.tokens, style));
|
|
52
|
+
// The label alone hides where the link goes, so the address follows it.
|
|
53
|
+
if (link.href && link.href !== textOf(link.tokens)) {
|
|
54
|
+
spans.push(styled(` (${link.href})`, { ...style, ...STYLE.link }));
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "br":
|
|
59
|
+
spans.push(styled("\n", style));
|
|
60
|
+
break;
|
|
61
|
+
case "escape":
|
|
62
|
+
case "text":
|
|
63
|
+
spans.push(
|
|
64
|
+
...((token as Tokens.Text).tokens
|
|
65
|
+
? inline((token as Tokens.Text).tokens, style)
|
|
66
|
+
: [styled((token as Tokens.Text).text, style)]),
|
|
67
|
+
);
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
spans.push(styled((token as { raw: string }).raw, style));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return spans;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function textOf(tokens: Token[] | undefined): string {
|
|
77
|
+
return inline(tokens, {})
|
|
78
|
+
.map((s) => s.text)
|
|
79
|
+
.join("");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Breaks styled pieces into rows no wider than `width` display columns, the way the rest of the
|
|
84
|
+
* screen breaks plain text: at the character, so Japanese wraps where it should. Rows after the
|
|
85
|
+
* first start with `hanging`, which keeps a list item under its own marker.
|
|
86
|
+
*/
|
|
87
|
+
function wrap(spans: Span[], width: number, hanging = ""): Span[][] {
|
|
88
|
+
const limit = Math.max(4, width);
|
|
89
|
+
const rows: Span[][] = [];
|
|
90
|
+
let row: Span[] = [];
|
|
91
|
+
let used = 0;
|
|
92
|
+
const indent = () => (hanging === "" ? [] : [{ text: hanging }]);
|
|
93
|
+
const start = () => {
|
|
94
|
+
rows.push(row);
|
|
95
|
+
row = indent();
|
|
96
|
+
used = displayWidth(hanging);
|
|
97
|
+
};
|
|
98
|
+
for (const span of spans) {
|
|
99
|
+
for (const [i, part] of span.text.split("\n").entries()) {
|
|
100
|
+
// A line break inside a block starts a row of its own.
|
|
101
|
+
if (i > 0) start();
|
|
102
|
+
let piece = "";
|
|
103
|
+
for (const ch of part) {
|
|
104
|
+
const w = displayWidth(ch);
|
|
105
|
+
if (used + w > limit && (row.length > 0 || piece !== "")) {
|
|
106
|
+
if (piece !== "") row.push({ ...span, text: piece });
|
|
107
|
+
piece = "";
|
|
108
|
+
start();
|
|
109
|
+
}
|
|
110
|
+
piece += ch;
|
|
111
|
+
used += w;
|
|
112
|
+
}
|
|
113
|
+
if (piece !== "") row.push({ ...span, text: piece });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
rows.push(row);
|
|
117
|
+
return rows;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Puts `prefix` in front of every row, for a quote bar or a code bar. */
|
|
121
|
+
function prefixed(rows: Span[][], prefix: Span): Span[][] {
|
|
122
|
+
return rows.map((row) => [prefix, ...row]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function blockRows(tokens: Token[], width: number): Span[][] {
|
|
126
|
+
const rows: Span[][] = [];
|
|
127
|
+
for (const token of tokens) {
|
|
128
|
+
switch (token.type) {
|
|
129
|
+
case "space":
|
|
130
|
+
rows.push([]);
|
|
131
|
+
break;
|
|
132
|
+
case "heading":
|
|
133
|
+
rows.push(...wrap(inline(token.tokens, STYLE.heading), width));
|
|
134
|
+
break;
|
|
135
|
+
case "paragraph":
|
|
136
|
+
case "text":
|
|
137
|
+
rows.push(...wrap(inline(token.tokens ?? [], {}), width));
|
|
138
|
+
break;
|
|
139
|
+
case "code": {
|
|
140
|
+
const marker = styled(CODE_MARKER, STYLE.quote);
|
|
141
|
+
const body = (token as Tokens.Code).text.split("\n");
|
|
142
|
+
for (const line of body) {
|
|
143
|
+
rows.push(...prefixed(wrap([styled(line, STYLE.code)], width - 2), marker));
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "blockquote": {
|
|
148
|
+
const inner = blockRows((token as Tokens.Blockquote).tokens ?? [], width - 2);
|
|
149
|
+
rows.push(...prefixed(inner, styled(QUOTE_MARKER, STYLE.quote)));
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "list":
|
|
153
|
+
rows.push(...listRows(token as Tokens.List, width, 0));
|
|
154
|
+
break;
|
|
155
|
+
case "hr":
|
|
156
|
+
rows.push([styled("─".repeat(Math.max(4, width)), STYLE.rule)]);
|
|
157
|
+
break;
|
|
158
|
+
case "table":
|
|
159
|
+
// Aligning columns is its own piece of work; until then the source rows are shown as
|
|
160
|
+
// they were written, so nothing the model put in the table is lost.
|
|
161
|
+
for (const line of (token as Tokens.Table).raw.trimEnd().split("\n")) {
|
|
162
|
+
rows.push(...wrap([{ text: line }], width));
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
for (const line of ((token as { raw?: string }).raw ?? "").trimEnd().split("\n")) {
|
|
167
|
+
rows.push(...wrap([{ text: line }], width));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return rows;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function listRows(list: Tokens.List, width: number, depth: number): Span[][] {
|
|
175
|
+
const rows: Span[][] = [];
|
|
176
|
+
let number = Number(list.start || 1);
|
|
177
|
+
for (const item of list.items) {
|
|
178
|
+
const marker = list.ordered ? `${number++}. ` : `${BULLETS[depth % BULLETS.length]} `;
|
|
179
|
+
const indent = " ".repeat(displayWidth(marker));
|
|
180
|
+
const inner: Span[][] = [];
|
|
181
|
+
for (const token of item.tokens) {
|
|
182
|
+
if (token.type === "list") {
|
|
183
|
+
inner.push(...listRows(token as Tokens.List, width - displayWidth(marker), depth + 1));
|
|
184
|
+
} else {
|
|
185
|
+
inner.push(...blockRows([token], width - displayWidth(marker)));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const [i, row] of inner.entries()) {
|
|
189
|
+
rows.push([{ text: i === 0 ? marker : indent }, ...row]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return rows;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* How much of a reply is read as markdown. Reading it costs more than the square of its length
|
|
197
|
+
* (10,000 characters take about 0.14 seconds, 20,000 about 0.46, 140,000 over a minute), and the
|
|
198
|
+
* screen draws on one thread, so a longer reply would hold it. Above this the reply is shown as
|
|
199
|
+
* plain text: every character is still there, with its marks.
|
|
200
|
+
*/
|
|
201
|
+
const MAX_SOURCE = 20_000;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A reply as rows of styled pieces. The model writes markdown, so the screen shows the emphasis
|
|
205
|
+
* and the structure instead of the characters that mark them. What this does not draw yet is
|
|
206
|
+
* shown as it was written, never dropped.
|
|
207
|
+
*/
|
|
208
|
+
export function markdownRows(source: string, width: number): Span[][] {
|
|
209
|
+
if (source.length > MAX_SOURCE) return wrap([{ text: source }], width);
|
|
210
|
+
const rows = blockRows(marked.lexer(source), width);
|
|
211
|
+
while (rows.length > 0 && (rows[0]?.length ?? 0) === 0) rows.shift();
|
|
212
|
+
while (rows.length > 0 && (rows.at(-1)?.length ?? 0) === 0) rows.pop();
|
|
213
|
+
return rows.length > 0 ? rows : [[]];
|
|
214
|
+
}
|
package/src/usage.ts
CHANGED
package/src/workspace.ts
CHANGED
|
@@ -14,7 +14,7 @@ export async function findWorkspace(start: string): Promise<string> {
|
|
|
14
14
|
if (parent === dir) {
|
|
15
15
|
throw new OpenshainError(
|
|
16
16
|
"config",
|
|
17
|
-
`${CONFIG_FILE_NAME} が見つかりません。${resolve(start)} から上に向かって探しました。openshain init
|
|
17
|
+
`${CONFIG_FILE_NAME} が見つかりません。${resolve(start)} から上に向かって探しました。openshain init で作成します。`,
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
dir = parent;
|
package/dist/commands/run.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type AnyEvent, type Runtime, type RuntimeProviders, type Work, type WorkId } from "@openshain/core";
|
|
2
|
-
export interface DriveOptions {
|
|
3
|
-
write: (line: string) => void;
|
|
4
|
-
/** Answers the model's questions. Without it, a question leaves the work waiting and the run ends. */
|
|
5
|
-
ask?: (question: string) => Promise<string>;
|
|
6
|
-
/** Stops the run. The work stays where it is and can be resumed. */
|
|
7
|
-
signal?: AbortSignal;
|
|
8
|
-
}
|
|
9
|
-
export interface RunOptions extends DriveOptions {
|
|
10
|
-
workspaceRoot: string;
|
|
11
|
-
providers: RuntimeProviders;
|
|
12
|
-
objective: string;
|
|
13
|
-
}
|
|
14
|
-
/** Creates a work for the request and drives it. Exit code 0 when the work completed. */
|
|
15
|
-
export declare function run(options: RunOptions): Promise<number>;
|
|
16
|
-
/** Drives a work from its current state, printing one line per tool call, and closes with the report. */
|
|
17
|
-
export declare function drive(runtime: Runtime, workId: WorkId, options: DriveOptions): Promise<number>;
|
|
18
|
-
/** One line for a tool call, a rejection or a failure; nothing for the other events. `names` maps call ids to tool names. */
|
|
19
|
-
export declare function progressLine(event: AnyEvent, names: Map<string, string>): string | undefined;
|
|
20
|
-
/** The closing lines: what happened, what it cost, and who acts next. */
|
|
21
|
-
export declare function report(work: Work, events: AnyEvent[]): string[];
|
|
22
|
-
export declare function nextActor(work: Work): string;
|