opencode-courier 0.7.1 → 0.7.3
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/README.md +31 -15
- package/dist/index.js +1 -0
- package/dist/platform/version.js +2 -2
- package/dist/tree/mascot.d.ts +50 -0
- package/dist/tree/mascot.js +171 -0
- package/dist/tree/panel.d.ts +3 -1
- package/dist/tree/panel.js +17 -3
- package/dist/tree/ports.d.ts +8 -0
- package/dist/tree/rpc-handlers.d.ts +3 -0
- package/dist/tree/rpc.d.ts +8 -0
- package/dist/tree/rpc.js +2 -0
- package/dist/tree/tui.d.ts +104 -5
- package/dist/tui.js +735 -111
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ The edge cases on the live terminal UI, narrated ([what it shows](#demos)).
|
|
|
21
21
|
## Quickstart
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm install -g @opencode/cli@2.0.
|
|
24
|
+
npm install -g @opencode/cli@2.0.32 # OpenCode V2, at the version courier is tested on
|
|
25
25
|
opencode plugin add opencode-courier # by name, so OpenCode offers new releases
|
|
26
26
|
opencode # in any folder
|
|
27
27
|
```
|
|
@@ -43,9 +43,9 @@ sometimes stall on a request; if nothing moves for a few minutes, send the promp
|
|
|
43
43
|
|
|
44
44
|
Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
|
|
45
45
|
(`@opencode-ai/plugin`), and needs OpenCode V2. Each release is pinned to one V2 version and tested
|
|
46
|
-
against it end to end; the current one is tested on **OpenCode 2.0.
|
|
46
|
+
against it end to end; the current one is tested on **OpenCode 2.0.32**. CI also runs the live
|
|
47
47
|
suite on the newest OpenCode release, on `main` and nightly, so a host release that breaks the
|
|
48
|
-
plugin shows up there first. Older releases and their versions: [
|
|
48
|
+
plugin shows up there first. Older releases and their versions: [every release](docs/reference.md#the-opencode-version).
|
|
49
49
|
|
|
50
50
|
## Courier or the built-in subagent tool?
|
|
51
51
|
|
|
@@ -111,24 +111,23 @@ pinned to its version. OpenCode never checks it:
|
|
|
111
111
|
the plugin installs and loads on any V2 host, and on a version other than the pinned one writes one
|
|
112
112
|
line to the server log naming both ([more](docs/reference.md#the-opencode-version)).
|
|
113
113
|
|
|
114
|
+
<!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
|
|
114
115
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
115
116
|
|---|---|
|
|
117
|
+
| 0.7.3 | 2.0.32 |
|
|
118
|
+
| 0.7.2 | 2.0.31 |
|
|
116
119
|
| 0.7.1 | 2.0.30 |
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
| 0.2.2 | 2.0.24 |
|
|
121
|
-
| 0.2.1 | 2.0.23 |
|
|
122
|
-
| 0.2.0 | 2.0.22 |
|
|
123
|
-
| 0.1.6 | 0.0.0-beta-19271 (the beta line: `opencode2` from `@opencode-ai/cli`, and `@opencode-ai/plugin`) |
|
|
120
|
+
<!-- compat-table:end -->
|
|
121
|
+
|
|
122
|
+
Older releases and their versions: [every release](docs/reference.md#the-opencode-version).
|
|
124
123
|
|
|
125
124
|
The [terminal part](#in-the-terminal) relies on the host to provide its UI modules and is tested on
|
|
126
|
-
OpenCode 2.0.
|
|
125
|
+
OpenCode 2.0.32 only; the server part works as before on older V2 hosts from 2.0.4 on.
|
|
127
126
|
|
|
128
127
|
Check yours with `opencode --version`, and install the matching CLI with:
|
|
129
128
|
|
|
130
129
|
```bash
|
|
131
|
-
npm install -g @opencode/cli@2.0.
|
|
130
|
+
npm install -g @opencode/cli@2.0.32
|
|
132
131
|
```
|
|
133
132
|
|
|
134
133
|
### The plugin
|
|
@@ -271,17 +270,34 @@ courier 1 waiting on you · 2 running · group pair 1/2
|
|
|
271
270
|
```
|
|
272
271
|
|
|
273
272
|
`/courier tree` opens a panel beside the session with the tree under it, a row per session indented
|
|
274
|
-
by depth, with
|
|
273
|
+
by depth, with a mark and colour from your theme for its state (`✓` reported, a spinner `⠋` while it runs, `●` waiting on you, `!` stalled,
|
|
274
|
+
`✗` failed or interrupted), what it does, its report and its group's standing; a session waiting on you gets a line per
|
|
275
275
|
request or form (`shows a form: Web Search`), a stalled one since when its message waits, a session that started others gets
|
|
276
|
-
a bar of the parts reported, a leaf how long it ran, its last tool call and its cost. A tree that has
|
|
276
|
+
a bar of the parts reported, a leaf how long it ran, its last tool call, how full its context is (`ctx 31%`, in the warning colour past 80%) and its cost. A tree that has
|
|
277
277
|
started more than `maxTotal` sessions over time is listed live ones first, then newest, and the header
|
|
278
278
|
says how many older ones are left out; the footer counts them all. `↑`/`↓` select,
|
|
279
|
-
`enter` opens the session, `s` stops it and everything under it after you confirm, as `courier_stop`
|
|
279
|
+
`enter` or a click opens the session, a row under the pointer is highlighted, `s` stops it and everything under it after you confirm, as `courier_stop`
|
|
280
280
|
would, `x` forgets a `gone` session after you confirm, removing its worktree by `courier_cleanup`'s
|
|
281
281
|
rules, and `q` closes the panel; `/courier stop <session>` stops from the prompt. While open, the panel
|
|
282
282
|
follows you between sessions: into one of its tree it keeps that tree, the session on screen marked
|
|
283
283
|
`(on screen)`; elsewhere it shows that session's tree, or hides while you are in a session without one,
|
|
284
284
|
or on the home screen, and shows again once you are back in one; only `q` closes it for good.
|
|
285
|
+
A row whose report arrives flashes in your theme's success colour and fades back over 1.5 s, and the header's bar
|
|
286
|
+
grows to its new length; nothing in the footer moves. Next to the header's title digs Digby, the courier's mole, drawn
|
|
287
|
+
in your theme's colours, whose state follows the tree's: a breathing mound while all is idle, a trembling one while
|
|
288
|
+
children run, up with a blinking sign while one waits on you, slumped while one is stalled, popping up with a letter
|
|
289
|
+
for a report and raising mounds for a spawn. Where the panel has room the tree does not use, on an empty panel or 10
|
|
290
|
+
rows free below the last row, a larger Digby, 16 by 8, shows the same state there instead, paws, sign, letter and all
|
|
291
|
+
([more](docs/reference.md#in-the-terminal)).
|
|
292
|
+
To keep the panel still (`•` for a running row, no flash, the bar at once, Digby in each state's first frame), give the TUI part `"animations": false` in OpenCode's terminal config, `~/.config/opencode/cli.json`:
|
|
293
|
+
the TUI passes options to its plugins only from there, not from the server's `opencode.json`, and has no animations
|
|
294
|
+
setting a plugin can read ([more](docs/reference.md#in-the-terminal)).
|
|
295
|
+
|
|
296
|
+
```jsonc
|
|
297
|
+
{
|
|
298
|
+
"plugins": [{ "package": "opencode-courier", "options": { "animations": false } }]
|
|
299
|
+
}
|
|
300
|
+
```
|
|
285
301
|
|
|
286
302
|
`ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on you" or a click
|
|
287
303
|
on "waiting on you" opens the asks inbox: every permission request, question and form waiting
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,7 @@ export const courier = () => Plugin.define({
|
|
|
87
87
|
directory: ctx.location.directory,
|
|
88
88
|
projectID: ctx.location.project.id,
|
|
89
89
|
now: Date.now,
|
|
90
|
+
windows: async () => (await ctx.model.list()).data.map((model) => ({ providerID: model.providerID, id: model.id, context: model.limit.context })),
|
|
90
91
|
head: headOf,
|
|
91
92
|
dirty: dirtyOf,
|
|
92
93
|
pending: async (sessionID) => {
|
package/dist/platform/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
const
|
|
2
|
+
const VERSION_TABLE = "https://github.com/ivopogace/opencode-courier/blob/main/docs/reference.md#the-opencode-version";
|
|
3
3
|
/**
|
|
4
4
|
* Reads from the package's own package.json, next to `dist/` as to `src/`: `version` and the exact
|
|
5
5
|
* `@opencode/plugin` devDependency version, which the peer repeats. Throws if either is missing.
|
|
@@ -25,5 +25,5 @@ export function versionNotice(built, app) {
|
|
|
25
25
|
const running = version === undefined ? "this server reports no version" : `this server is ${version}`;
|
|
26
26
|
const channel = typeof app?.channel === "string" ? ` (channel ${app.channel})` : "";
|
|
27
27
|
return (`opencode-courier ${built.plugin} was built and tested against OpenCode ${built.opencode}; ${running}${channel}. ` +
|
|
28
|
-
`Its tools may fail: see the
|
|
28
|
+
`Its tools may fail: see every release and the OpenCode it is pinned to, ${VERSION_TABLE}`);
|
|
29
29
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pixel of Digby, the tree panel's mole, as the theme colour it takes: `b` body, `k` background (eyes), `g` mound,
|
|
3
|
+
* `m` muted (paws, dirt, post, arm), `a` accent (snout, letter), `w`, `e` and `s` warning, error and success (sign, dots, spark).
|
|
4
|
+
*/
|
|
5
|
+
export type Token = "b" | "k" | "g" | "m" | "a" | "w" | "e" | "s";
|
|
6
|
+
/** What Digby shows: the tree's state, or for a while after one arrives, a spawn or a report. */
|
|
7
|
+
export type MascotState = "idle" | "spawn" | "running" | "waiting" | "stalled" | "report" | "gone";
|
|
8
|
+
/** One cell of a drawing: `▀` the top pixel's colour over the bottom's, `▄` only a bottom pixel, `█` both alike, or a character. */
|
|
9
|
+
export type Cell = {
|
|
10
|
+
readonly char: string;
|
|
11
|
+
readonly fg?: Token;
|
|
12
|
+
readonly bg?: Token;
|
|
13
|
+
};
|
|
14
|
+
/** Digby's frames take one panel frame in this many, about every 450 ms. */
|
|
15
|
+
export declare const MASCOT_EVERY = 5;
|
|
16
|
+
/** He blinks on every this-many-th of his frames where his eyes are open. */
|
|
17
|
+
export declare const BLINK_EVERY = 11;
|
|
18
|
+
/**
|
|
19
|
+
* The 8×4 glyph's frames by state, pixel rows top to bottom; a state with several cycles through them, one per mascot frame.
|
|
20
|
+
* No muted pixel touches the mound, here or in the 16×16, as `text.muted` is close to `background.raised.max`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const GLYPH: Readonly<Record<MascotState, ReadonlyArray<ReadonlyArray<string>>>>;
|
|
23
|
+
/** The 8×4 glyph's pixels for `state` at mascot frame `frame`, its first frame with none: animations off. */
|
|
24
|
+
export declare function glyph(state: MascotState, frame?: number): ReadonlyArray<string>;
|
|
25
|
+
/** Pixel rows, two to a line of cells, by the half-block rule; an odd last row has nothing under it. */
|
|
26
|
+
export declare function cells(pixels: ReadonlyArray<string>): ReadonlyArray<ReadonlyArray<Cell>>;
|
|
27
|
+
/** How his eyes are drawn: one pixel each, `wide` two tall, `shut` two wide, or `none` (blinking). */
|
|
28
|
+
export type Eyes = "open" | "wide" | "shut" | "none";
|
|
29
|
+
/**
|
|
30
|
+
* The 16×16 Digby, his head raised by `rise` (0 in the hole, 1 fully up; below 0 out of sight) over a body in the hole,
|
|
31
|
+
* then the mound (its top row `top` wide, `shift` columns over) and the ground, both `mound`, drawn over it but for the hole.
|
|
32
|
+
*/
|
|
33
|
+
export declare function digby(rise: number, { top, eyes, slump, shift, mound }?: {
|
|
34
|
+
top?: number;
|
|
35
|
+
eyes?: Eyes;
|
|
36
|
+
slump?: boolean;
|
|
37
|
+
shift?: number;
|
|
38
|
+
mound?: "g" | "m";
|
|
39
|
+
}): ReadonlyArray<string>;
|
|
40
|
+
/** A spot of `pixels` with its first at row `row`, column `column`; `.` leaves what is under it. */
|
|
41
|
+
type Spot = readonly [row: number, column: number, pixels: string];
|
|
42
|
+
/** `pixels` with `spots` drawn over them. */
|
|
43
|
+
export declare function paint(pixels: ReadonlyArray<string>, spots: ReadonlyArray<Spot>): ReadonlyArray<string>;
|
|
44
|
+
/** The 16×16's frames by state but idle, each its pixels with the eyes open or, for a blink, not; the first reads alone. */
|
|
45
|
+
export declare const ART: Readonly<Record<Exclude<MascotState, "idle">, ReadonlyArray<(open: boolean) => ReadonlyArray<string>>>>;
|
|
46
|
+
/** The 16×16 idle Digby at mascot frame `frame`, its first with none: the mound breathing, a `z` drifting up beside it. */
|
|
47
|
+
export declare function idleArt(frame?: number): ReadonlyArray<ReadonlyArray<Cell>>;
|
|
48
|
+
/** The 16×16 Digby for `state` at mascot frame `frame`, its first frame with none: animations off. */
|
|
49
|
+
export declare function art(state: MascotState, frame?: number): ReadonlyArray<ReadonlyArray<Cell>>;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/** Digby's frames take one panel frame in this many, about every 450 ms. */
|
|
2
|
+
export const MASCOT_EVERY = 5;
|
|
3
|
+
/** He blinks on every this-many-th of his frames where his eyes are open. */
|
|
4
|
+
export const BLINK_EVERY = 11;
|
|
5
|
+
/**
|
|
6
|
+
* The 8×4 glyph's frames by state, pixel rows top to bottom; a state with several cycles through them, one per mascot frame.
|
|
7
|
+
* No muted pixel touches the mound, here or in the 16×16, as `text.muted` is close to `background.raised.max`.
|
|
8
|
+
*/
|
|
9
|
+
export const GLYPH = {
|
|
10
|
+
idle: [["........", "........", "..gggg..", "gggggggg"]],
|
|
11
|
+
spawn: [
|
|
12
|
+
["........", "...bb...", "g.bbbb.g", "gggggggg"],
|
|
13
|
+
[".g....g.", "...bb...", "ggbbbbgg", "gggggggg"],
|
|
14
|
+
],
|
|
15
|
+
running: [
|
|
16
|
+
["..g..g..", "...bb...", ".gbbbbg.", "gggggggg"],
|
|
17
|
+
[".g....g.", "........", ".gbbbbg.", "gggggggg"],
|
|
18
|
+
],
|
|
19
|
+
waiting: [
|
|
20
|
+
[".bbbbbbw", ".bkbbkbw", ".bbaabbm", "ggggggg."],
|
|
21
|
+
[".bbbbbbb", ".bkbbkbb", ".bbaabbm", "ggggggg."],
|
|
22
|
+
],
|
|
23
|
+
stalled: [
|
|
24
|
+
["..e.....", ".bbbbbb.", ".bkbbkb.", "gbbaabbg"],
|
|
25
|
+
["..e.e...", ".bbbbbb.", ".bkbbkb.", "gbbaabbg"],
|
|
26
|
+
["..e.e.e.", ".bbbbbb.", ".bkbbkb.", "gbbaabbg"],
|
|
27
|
+
],
|
|
28
|
+
report: [
|
|
29
|
+
["......aa", "bbbbbbaa", "bkbbkbm.", "bbaabbm."],
|
|
30
|
+
["s.....aa", "bbbbbbaa", "bkbbkbm.", "bbaabbm."],
|
|
31
|
+
],
|
|
32
|
+
gone: [["........", ".mmmmmm.", ".mkmmkm.", "mmmmmmmm"]],
|
|
33
|
+
};
|
|
34
|
+
/** The states in which his eyes are open, so he blinks. */
|
|
35
|
+
const OPEN_EYES = new Set(["waiting", "report"]);
|
|
36
|
+
/** The 8×4 glyph's pixels for `state` at mascot frame `frame`, its first frame with none: animations off. */
|
|
37
|
+
export function glyph(state, frame) {
|
|
38
|
+
const frames = GLYPH[state];
|
|
39
|
+
const pixels = frames[(frame ?? 0) % frames.length];
|
|
40
|
+
if (frame === undefined)
|
|
41
|
+
return pixels;
|
|
42
|
+
if (state === "idle" && breathesOut(frame))
|
|
43
|
+
return [...pixels.slice(0, 2), ".gggggg.", ...pixels.slice(3)];
|
|
44
|
+
return OPEN_EYES.has(state) && blinks(frame) ? pixels.map((row) => row.replaceAll("k", "b")) : pixels;
|
|
45
|
+
}
|
|
46
|
+
/** The mound's top row widens for two frames in four. */
|
|
47
|
+
const breathesOut = (frame) => Math.floor(frame / 2) % 2 === 1;
|
|
48
|
+
const blinks = (frame) => frame % BLINK_EVERY === BLINK_EVERY - 1;
|
|
49
|
+
/** Pixel rows, two to a line of cells, by the half-block rule; an odd last row has nothing under it. */
|
|
50
|
+
export function cells(pixels) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
for (let row = 0; row < pixels.length; row += 2) {
|
|
53
|
+
const top = pixels[row];
|
|
54
|
+
const bottom = pixels[row + 1] ?? "";
|
|
55
|
+
lines.push([...top].map((pixel, column) => cell(token(pixel), token(bottom[column]))));
|
|
56
|
+
}
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
const token = (pixel) => (pixel && pixel !== "." ? pixel : undefined);
|
|
60
|
+
function cell(top, bottom) {
|
|
61
|
+
if (!top && !bottom)
|
|
62
|
+
return { char: " " };
|
|
63
|
+
if (!top)
|
|
64
|
+
return { char: "▄", fg: bottom };
|
|
65
|
+
if (top === bottom)
|
|
66
|
+
return { char: "█", fg: top };
|
|
67
|
+
return bottom ? { char: "▀", fg: top, bg: bottom } : { char: "▀", fg: top };
|
|
68
|
+
}
|
|
69
|
+
/** The 16×16's size, in pixels. */
|
|
70
|
+
const SIZE = 16;
|
|
71
|
+
/** How far each of the head's rows leans right when he slumps. */
|
|
72
|
+
const SLUMP = [2, 2, 2, 1, 1, 0, 0];
|
|
73
|
+
/**
|
|
74
|
+
* The 16×16 Digby, his head raised by `rise` (0 in the hole, 1 fully up; below 0 out of sight) over a body in the hole,
|
|
75
|
+
* then the mound (its top row `top` wide, `shift` columns over) and the ground, both `mound`, drawn over it but for the hole.
|
|
76
|
+
*/
|
|
77
|
+
export function digby(rise, { top = 6, eyes = "open", slump = false, shift = 0, mound = "g" } = {}) {
|
|
78
|
+
const grid = Array.from({ length: SIZE }, () => new Array(SIZE).fill("."));
|
|
79
|
+
const span = (row, width, pixel, skip = (_column) => false, by = 0) => {
|
|
80
|
+
if (row < 0 || row >= SIZE)
|
|
81
|
+
return;
|
|
82
|
+
for (let column = (SIZE - width) / 2; column < (SIZE + width) / 2; column++)
|
|
83
|
+
if (!skip(column))
|
|
84
|
+
grid[row][column + by] = pixel;
|
|
85
|
+
};
|
|
86
|
+
const head = Math.round(10 - 8 * rise);
|
|
87
|
+
const lean = (at) => (slump ? SLUMP[at] : 0);
|
|
88
|
+
[4, 6, 8, 8, 8, 6, 4].forEach((width, at) => span(head + at, width, "b", undefined, lean(at)));
|
|
89
|
+
for (let row = head + 7; row <= 11; row++)
|
|
90
|
+
span(row, 4, "b");
|
|
91
|
+
const eyeColumns = eyes === "shut" ? [5, 6, 9, 10] : [6, 9];
|
|
92
|
+
if (eyes !== "none")
|
|
93
|
+
span(head + 3, 8, "k", (column) => !eyeColumns.includes(column), lean(3));
|
|
94
|
+
if (eyes === "wide")
|
|
95
|
+
span(head + 2, 8, "k", (column) => !eyeColumns.includes(column), lean(2));
|
|
96
|
+
span(head + 4, 2, "a", undefined, lean(4));
|
|
97
|
+
span(10, SIZE, ".", (column) => column >= 6 && column <= 9);
|
|
98
|
+
span(10, top, mound, (column) => column >= 6 && column <= 9, shift);
|
|
99
|
+
span(11, 10, mound, undefined, shift);
|
|
100
|
+
span(12, 12, mound, undefined, shift);
|
|
101
|
+
for (const row of [13, 14, 15])
|
|
102
|
+
span(row, SIZE, mound);
|
|
103
|
+
return grid.map((row) => row.join(""));
|
|
104
|
+
}
|
|
105
|
+
/** `pixels` with `spots` drawn over them. */
|
|
106
|
+
export function paint(pixels, spots) {
|
|
107
|
+
const grid = pixels.map((row) => [...row]);
|
|
108
|
+
for (const [row, column, drawn] of spots)
|
|
109
|
+
[...drawn].forEach((pixel, at) => {
|
|
110
|
+
if (pixel !== "." && grid[row]?.[column + at] !== undefined)
|
|
111
|
+
grid[row][column + at] = pixel;
|
|
112
|
+
});
|
|
113
|
+
return grid.map((row) => row.join(""));
|
|
114
|
+
}
|
|
115
|
+
/** A column of `length` pixels of `pixel` from row `row`. */
|
|
116
|
+
const post = (row, column, length, pixel) => Array.from({ length }, (_, at) => [row + at, column, pixel]);
|
|
117
|
+
/** The head's top row at `rise`, where running's snout pokes up. */
|
|
118
|
+
const headTop = (rise) => Math.round(10 - 8 * rise);
|
|
119
|
+
const running = (rise, shift, dirt) => paint(digby(rise, { shift }), [[headTop(rise), 7, "aa"], ...dirt]);
|
|
120
|
+
/** A child's head peeking out of a side mound at columns `column`–`column + 2`, `up` or half sunk. */
|
|
121
|
+
const peek = (column, up) => [
|
|
122
|
+
...(up ? [[8, column, ".b."], [9, column, "bkb"]] : [[9, column, ".b."]]),
|
|
123
|
+
[10, column, "gbg"],
|
|
124
|
+
[11, column, "ggg"],
|
|
125
|
+
];
|
|
126
|
+
const spawn = (up, dirt) => paint(digby(0.1), [...peek(0, up), ...peek(13, up), [12, 0, "gg"], [12, 14, "gg"], ...dirt]);
|
|
127
|
+
/** The sign on its post, which stops a pixel above the ground so the muted post does not sit on it. */
|
|
128
|
+
const sign = (shown) => [...(shown ? [[2, 13, "www"], [3, 13, "wkw"], [4, 13, "www"]] : []), ...post(5, 14, 7, "m")];
|
|
129
|
+
/** His paws, held a pixel above the mound's rim so the muted pixels do not touch it. */
|
|
130
|
+
const waiting = (shown) => (open) => paint(digby(1, { eyes: open ? "wide" : "none" }), [[8, 4, "mm"], [8, 10, "mm"], ...sign(shown)]);
|
|
131
|
+
const stalled = (dots) => () => paint(digby(0.7, { eyes: "shut", slump: true }), [[5, 0, dots]]);
|
|
132
|
+
/** The letter held up on his arm, which stops a pixel above the mound so it does not touch it, and the spark beside him. */
|
|
133
|
+
const report = (rise, spark) => (open) => paint(digby(rise, { eyes: open ? "open" : "none" }), [
|
|
134
|
+
[0, 12, "aaaa"],
|
|
135
|
+
[1, 12, "akka"],
|
|
136
|
+
[2, 12, "aaaa"],
|
|
137
|
+
...post(3, 13, 8, "m"),
|
|
138
|
+
...spark.map((row, at) => [1 + at, 0, row]),
|
|
139
|
+
]);
|
|
140
|
+
/** The 16×16's frames by state but idle, each its pixels with the eyes open or, for a blink, not; the first reads alone. */
|
|
141
|
+
export const ART = {
|
|
142
|
+
spawn: [
|
|
143
|
+
() => spawn(true, [[3, 1, "m"], [5, 3, "m"], [2, 14, "m"], [5, 12, "m"]]),
|
|
144
|
+
() => spawn(false, [[1, 0, "m"], [4, 2, "m"], [3, 15, "m"], [6, 13, "m"]]),
|
|
145
|
+
],
|
|
146
|
+
running: [
|
|
147
|
+
() => running(0.22, 0, [[5, 2, "m"], [7, 1, "m"], [4, 13, "m"], [6, 14, "m"]]),
|
|
148
|
+
() => running(0.12, 1, [[3, 1, "m"], [6, 3, "m"], [5, 12, "m"], [3, 15, "m"]]),
|
|
149
|
+
],
|
|
150
|
+
waiting: [waiting(true), waiting(false)],
|
|
151
|
+
stalled: [stalled("e.e.e"), stalled("e"), stalled("e.e")],
|
|
152
|
+
report: [report(1, [".s.", "sss", ".s."]), report(1.25, ["s.s", ".s.", "s.s"]), report(1.1, [".s.", "sss", ".s."])],
|
|
153
|
+
gone: [() => digby(-0.3, { mound: "m" })],
|
|
154
|
+
};
|
|
155
|
+
/** Where idle's `z` drifts up, by cell line, one step per mascot frame, then gone a frame. */
|
|
156
|
+
const Z_LINES = [3, 2, 1, undefined];
|
|
157
|
+
/** The 16×16 idle Digby at mascot frame `frame`, its first with none: the mound breathing, a `z` drifting up beside it. */
|
|
158
|
+
export function idleArt(frame) {
|
|
159
|
+
const lines = cells(digby(-0.3, { top: frame !== undefined && breathesOut(frame) ? 8 : 6 }));
|
|
160
|
+
const line = Z_LINES[(frame ?? 0) % Z_LINES.length];
|
|
161
|
+
if (line === undefined)
|
|
162
|
+
return lines;
|
|
163
|
+
return lines.map((cellsOf, at) => (at === line ? cellsOf.map((one, column) => (column === 13 ? { char: "z", fg: "m" } : one)) : cellsOf));
|
|
164
|
+
}
|
|
165
|
+
/** The 16×16 Digby for `state` at mascot frame `frame`, its first frame with none: animations off. */
|
|
166
|
+
export function art(state, frame) {
|
|
167
|
+
if (state === "idle")
|
|
168
|
+
return idleArt(frame);
|
|
169
|
+
const frames = ART[state];
|
|
170
|
+
return cells(frames[(frame ?? 0) % frames.length](!(frame !== undefined && OPEN_EYES.has(state) && blinks(frame))));
|
|
171
|
+
}
|
package/dist/tree/panel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CourierPorts, Pending, ShownForm } from "./ports.js";
|
|
1
|
+
import type { CourierPorts, ModelWindow, Pending, ShownForm } from "./ports.js";
|
|
2
2
|
import type { Tree, Wait } from "./rpc.js";
|
|
3
3
|
type Messages = Awaited<ReturnType<CourierPorts["session"]["context"]>>;
|
|
4
4
|
/** The last tool call in a session's messages, and when it was made. */
|
|
@@ -6,6 +6,8 @@ export declare function lastTool(messages: Messages): {
|
|
|
6
6
|
name: string;
|
|
7
7
|
at: number;
|
|
8
8
|
} | undefined;
|
|
9
|
+
/** How full a session's context is, as OpenCode's session view reads it (packages/tui/src/util/session.ts, v2.0.32): the last usage since the last compaction over its model's window. */
|
|
10
|
+
export declare function contextOf(messages: Messages, windows: ReadonlyArray<ModelWindow>): number | undefined;
|
|
9
11
|
/** A request as the panel says what a paused session waits on: a permission request, or its first question. */
|
|
10
12
|
export declare const waitOf: (pending: Pending) => Wait;
|
|
11
13
|
/** A form a session shows, as the panel says what it waits on. */
|
package/dist/tree/panel.js
CHANGED
|
@@ -15,6 +15,17 @@ export function lastTool(messages) {
|
|
|
15
15
|
}
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
|
+
/** How full a session's context is, as OpenCode's session view reads it (packages/tui/src/util/session.ts, v2.0.32): the last usage since the last compaction over its model's window. */
|
|
19
|
+
export function contextOf(messages, windows) {
|
|
20
|
+
const compacted = messages.findLastIndex((message) => message.type === "compaction" && message.status === "completed");
|
|
21
|
+
const last = messages.findLast((message, index) => index > compacted && message.type === "assistant" && message.tokens !== undefined);
|
|
22
|
+
if (last?.type !== "assistant" || !last.tokens)
|
|
23
|
+
return undefined;
|
|
24
|
+
const { input, output, reasoning, cache } = last.tokens;
|
|
25
|
+
const used = input + output + reasoning + cache.read + cache.write;
|
|
26
|
+
const window = windows.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)?.context;
|
|
27
|
+
return used > 0 && window ? used / window : undefined;
|
|
28
|
+
}
|
|
18
29
|
/** A request as the panel says what a paused session waits on: a permission request, or its first question. */
|
|
19
30
|
export const waitOf = (pending) => pending.type === "permission"
|
|
20
31
|
? { type: "permission", action: pending.action, resources: [...pending.resources] }
|
|
@@ -44,8 +55,8 @@ function stateOf(info, waits, stalled, running) {
|
|
|
44
55
|
return "stalled";
|
|
45
56
|
return running ? "running" : "idle";
|
|
46
57
|
}
|
|
47
|
-
/** One session of the panel: what it is doing, its report and group standing, its last tool call and
|
|
48
|
-
async function row(ports, { entry, depth, info, pending, forms, report }, stalled) {
|
|
58
|
+
/** One session of the panel: what it is doing, its report and group standing, its last tool call, context use and cost. */
|
|
59
|
+
async function row(ports, windows, { entry, depth, info, pending, forms, report }, stalled) {
|
|
49
60
|
const { sessionID } = entry;
|
|
50
61
|
const [messages, status, standing, stopped] = await Promise.all([
|
|
51
62
|
ports.session.context({ sessionID }).catch(() => []),
|
|
@@ -55,6 +66,7 @@ async function row(ports, { entry, depth, info, pending, forms, report }, stalle
|
|
|
55
66
|
]);
|
|
56
67
|
const running = info !== undefined && busy(info.time);
|
|
57
68
|
const tool = lastTool(messages);
|
|
69
|
+
const context = contextOf(messages, windows);
|
|
58
70
|
return {
|
|
59
71
|
sessionID,
|
|
60
72
|
parentID: entry.parentID,
|
|
@@ -70,6 +82,7 @@ async function row(ports, { entry, depth, info, pending, forms, report }, stalle
|
|
|
70
82
|
created: info?.time.created ?? entry.createdAt,
|
|
71
83
|
...(info && !running && info.time.idle !== undefined ? { idle: info.time.idle } : {}),
|
|
72
84
|
...(info ? { cost: info.cost } : {}),
|
|
85
|
+
...(context !== undefined ? { context } : {}),
|
|
73
86
|
...(tool ? { tool } : {}),
|
|
74
87
|
waits: [...pending.map(waitOf), ...forms.map(formWait)],
|
|
75
88
|
};
|
|
@@ -81,7 +94,8 @@ async function row(ports, { entry, depth, info, pending, forms, report }, stalle
|
|
|
81
94
|
export async function panel(ports, rootID) {
|
|
82
95
|
const { kept, leftOut } = await readTree(ports, rootID);
|
|
83
96
|
const stalled = await stalledAmong(ports, [rootID, ...kept.map((node) => node.entry.sessionID)]);
|
|
84
|
-
const
|
|
97
|
+
const windows = await ports.windows().catch(() => []);
|
|
98
|
+
const rows = await Promise.all(kept.map((node) => row(ports, windows, node, stalled.get(node.entry.sessionID))));
|
|
85
99
|
const own = stalled.get(rootID);
|
|
86
100
|
return { rows, ...(leftOut ? { truncated: leftOut } : {}), ...(own !== undefined ? { stalled: own } : {}) };
|
|
87
101
|
}
|
package/dist/tree/ports.d.ts
CHANGED
|
@@ -164,6 +164,8 @@ export interface CourierPorts {
|
|
|
164
164
|
readonly pending: (sessionID: string) => Promise<ReadonlyArray<Pending>>;
|
|
165
165
|
/** The forms a session shows, which only the person can answer: web search's, a plugin's; not the question tool's. */
|
|
166
166
|
readonly forms: (sessionID: string) => Promise<ReadonlyArray<ShownForm>>;
|
|
167
|
+
/** The context windows of the models available, by provider and model id, as the panel reads a session's context use. */
|
|
168
|
+
readonly windows: () => Promise<ReadonlyArray<ModelWindow>>;
|
|
167
169
|
readonly limits: Limits;
|
|
168
170
|
/** The `nativeChildren` option: a child started without isolate is created with OpenCode's own `parentID`. */
|
|
169
171
|
readonly nativeChildren?: boolean;
|
|
@@ -174,6 +176,12 @@ export interface CourierPorts {
|
|
|
174
176
|
readonly nudge: () => void;
|
|
175
177
|
readonly log: (message: string) => void;
|
|
176
178
|
}
|
|
179
|
+
/** A model's context window in tokens, as `CourierPorts.windows` lists it. */
|
|
180
|
+
export interface ModelWindow {
|
|
181
|
+
readonly providerID: string;
|
|
182
|
+
readonly id: string;
|
|
183
|
+
readonly context: number;
|
|
184
|
+
}
|
|
177
185
|
/** A form a session shows, as `CourierPorts.forms` lists it; `person/` keeps them. */
|
|
178
186
|
export interface ShownForm {
|
|
179
187
|
readonly id: string;
|
|
@@ -77,6 +77,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
77
77
|
readonly standing: string;
|
|
78
78
|
} | undefined;
|
|
79
79
|
readonly stopped?: boolean | undefined;
|
|
80
|
+
readonly context?: number | undefined;
|
|
80
81
|
readonly idle?: number | undefined;
|
|
81
82
|
readonly report?: {
|
|
82
83
|
readonly progressed: boolean;
|
|
@@ -118,6 +119,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
118
119
|
readonly standing: string;
|
|
119
120
|
} | undefined;
|
|
120
121
|
readonly stopped?: boolean | undefined;
|
|
122
|
+
readonly context?: number | undefined;
|
|
121
123
|
readonly idle?: number | undefined;
|
|
122
124
|
readonly report?: {
|
|
123
125
|
readonly progressed: boolean;
|
|
@@ -157,6 +159,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
157
159
|
readonly created: import("effect/Schema").Number;
|
|
158
160
|
readonly idle: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
159
161
|
readonly cost: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
162
|
+
readonly context: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
160
163
|
readonly tool: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
|
|
161
164
|
readonly name: import("effect/Schema").String;
|
|
162
165
|
readonly at: import("effect/Schema").Number;
|
package/dist/tree/rpc.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ declare const Row: Schema.Struct<{
|
|
|
63
63
|
readonly idle: Schema.optionalKey<Schema.Number>;
|
|
64
64
|
/** What it has cost so far, in US dollars. */
|
|
65
65
|
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
66
|
+
/** How full its context is, as a fraction of the model's window; left out when the model or its limit cannot be read. */
|
|
67
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
66
68
|
/** Its last tool call, and when it was made. */
|
|
67
69
|
readonly tool: Schema.optionalKey<Schema.Struct<{
|
|
68
70
|
readonly name: Schema.String;
|
|
@@ -116,6 +118,8 @@ declare const Tree: Schema.Struct<{
|
|
|
116
118
|
readonly idle: Schema.optionalKey<Schema.Number>;
|
|
117
119
|
/** What it has cost so far, in US dollars. */
|
|
118
120
|
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
121
|
+
/** How full its context is, as a fraction of the model's window; left out when the model or its limit cannot be read. */
|
|
122
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
119
123
|
/** Its last tool call, and when it was made. */
|
|
120
124
|
readonly tool: Schema.optionalKey<Schema.Struct<{
|
|
121
125
|
readonly name: Schema.String;
|
|
@@ -240,6 +244,7 @@ export declare const treeRpc: {
|
|
|
240
244
|
readonly standing: string;
|
|
241
245
|
} | undefined;
|
|
242
246
|
readonly stopped?: boolean | undefined;
|
|
247
|
+
readonly context?: number | undefined;
|
|
243
248
|
readonly idle?: number | undefined;
|
|
244
249
|
readonly report?: {
|
|
245
250
|
readonly progressed: boolean;
|
|
@@ -281,6 +286,7 @@ export declare const treeRpc: {
|
|
|
281
286
|
readonly standing: string;
|
|
282
287
|
} | undefined;
|
|
283
288
|
readonly stopped?: boolean | undefined;
|
|
289
|
+
readonly context?: number | undefined;
|
|
284
290
|
readonly idle?: number | undefined;
|
|
285
291
|
readonly report?: {
|
|
286
292
|
readonly progressed: boolean;
|
|
@@ -332,6 +338,8 @@ export declare const treeRpc: {
|
|
|
332
338
|
readonly idle: Schema.optionalKey<Schema.Number>;
|
|
333
339
|
/** What it has cost so far, in US dollars. */
|
|
334
340
|
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
341
|
+
/** How full its context is, as a fraction of the model's window; left out when the model or its limit cannot be read. */
|
|
342
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
335
343
|
/** Its last tool call, and when it was made. */
|
|
336
344
|
readonly tool: Schema.optionalKey<Schema.Struct<{
|
|
337
345
|
readonly name: Schema.String;
|
package/dist/tree/rpc.js
CHANGED
|
@@ -53,6 +53,8 @@ const Row = Schema.Struct({
|
|
|
53
53
|
idle: Schema.optionalKey(Schema.Number),
|
|
54
54
|
/** What it has cost so far, in US dollars. */
|
|
55
55
|
cost: Schema.optionalKey(Schema.Number),
|
|
56
|
+
/** How full its context is, as a fraction of the model's window; left out when the model or its limit cannot be read. */
|
|
57
|
+
context: Schema.optionalKey(Schema.Number),
|
|
56
58
|
/** Its last tool call, and when it was made. */
|
|
57
59
|
tool: Schema.optionalKey(Schema.Struct({ name: Schema.String, at: Schema.Number })),
|
|
58
60
|
waits: Schema.Array(Wait),
|