opencode-courier 0.7.2 → 0.8.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/README.md +122 -135
- package/dist/index.js +24 -4
- package/dist/memory/domain.d.ts +119 -0
- package/dist/memory/domain.js +84 -0
- package/dist/memory/events.d.ts +26 -0
- package/dist/memory/events.js +46 -0
- package/dist/memory/forget.d.ts +10 -0
- package/dist/memory/forget.js +20 -0
- package/dist/memory/index.d.ts +10 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/note.d.ts +37 -0
- package/dist/memory/note.js +29 -0
- package/dist/memory/ports.d.ts +44 -0
- package/dist/memory/ports.js +1 -0
- package/dist/memory/recall.d.ts +27 -0
- package/dist/memory/recall.js +48 -0
- package/dist/memory/record.d.ts +7 -0
- package/dist/memory/record.js +92 -0
- package/dist/memory/rpc-handlers.d.ts +134 -0
- package/dist/memory/rpc-handlers.js +23 -0
- package/dist/memory/rpc.d.ts +227 -0
- package/dist/memory/rpc.js +60 -0
- package/dist/memory/store.d.ts +10 -0
- package/dist/memory/store.js +90 -0
- package/dist/memory/sweep.d.ts +4 -0
- package/dist/memory/sweep.js +12 -0
- package/dist/memory/text.d.ts +27 -0
- package/dist/memory/text.js +95 -0
- package/dist/memory/tools.d.ts +16 -0
- package/dist/memory/tools.js +78 -0
- package/dist/memory/tui.d.ts +73 -0
- package/dist/person/rpc.d.ts +4 -4
- package/dist/platform/json.d.ts +1 -0
- package/dist/platform/json.js +1 -0
- package/dist/platform/version.js +2 -2
- package/dist/rpc.d.ts +1 -0
- package/dist/rpc.js +1 -0
- package/dist/skill.js +10 -3
- package/dist/tree/context.d.ts +2 -2
- package/dist/tree/context.js +4 -3
- package/dist/tree/index.d.ts +6 -5
- package/dist/tree/index.js +5 -4
- 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 +12 -2
- package/dist/tree/roster.d.ts +17 -0
- package/dist/tree/roster.js +24 -0
- package/dist/tree/rpc-handlers.d.ts +21 -18
- package/dist/tree/rpc.d.ts +26 -18
- package/dist/tree/rpc.js +2 -0
- package/dist/tree/status.d.ts +5 -5
- package/dist/tree/subtree.d.ts +2 -2
- package/dist/tree/text.js +2 -1
- package/dist/tree/tui.d.ts +115 -8
- package/dist/tui.js +1269 -222
- package/package.json +4 -4
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { describeFailure } from "../kernel/protocol.js";
|
|
3
|
+
import { rethrow } from "../kernel/tool.js";
|
|
4
|
+
import { LESSON_MAX } from "./domain.js";
|
|
5
|
+
import { note } from "./note.js";
|
|
6
|
+
import { recall } from "./recall.js";
|
|
7
|
+
import { NOTE_EMPTY, NOTE_OFF, NOTE_TOOL, noteLeafRefusal, noteLongRefusal, noteText, noteUnrecordedRefusal, RECALL_OFF, RECALL_TOOL, recallListText, recallMissingText, recallRecordText } from "./text.js";
|
|
8
|
+
export const RecallInput = Schema.Struct({
|
|
9
|
+
sessionID: Schema.optional(Schema.String.annotate({ description: "The top session of a listed record, for that record in full; without it, one line per record of this project." })),
|
|
10
|
+
});
|
|
11
|
+
export const NoteInput = Schema.Struct({
|
|
12
|
+
text: Schema.String.annotate({
|
|
13
|
+
description: `The lesson, one line of at most ${LESSON_MAX} characters (longer is refused, not clipped): what the next orchestrator in this ` +
|
|
14
|
+
"project should know about running a tree like this one, how it was split, verified or briefed, or what the person decided, " +
|
|
15
|
+
"with the job it comes from and the reason.",
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
/** Adds `courier_recall` and `courier_note`, direct tools rather than code-mode ones. */
|
|
19
|
+
export function addMemoryTools(tools, ports) {
|
|
20
|
+
tools.add({
|
|
21
|
+
name: RECALL_TOOL,
|
|
22
|
+
options: { codemode: false },
|
|
23
|
+
description: "Look once at what earlier session trees in this project did, before you decide whether and how to split a task: " +
|
|
24
|
+
"one line per tree, newest first, with how its top split the task, how each part went (its status, how long it ran, " +
|
|
25
|
+
"what it cost, how full its context got) and whether a spawn was refused; with sessionID, one tree's record in full. " +
|
|
26
|
+
"Records hold no report bodies or files. Call it once at the start, not in a loop, and not to check on sessions you " +
|
|
27
|
+
"started: their reports wake you.",
|
|
28
|
+
input: RecallInput,
|
|
29
|
+
execute: async (input, context) => {
|
|
30
|
+
const result = await recall(ports.memory, ports.state, context.sessionID, input).catch(rethrow(RECALL_TOOL));
|
|
31
|
+
switch (result.kind) {
|
|
32
|
+
case "off":
|
|
33
|
+
return { content: RECALL_OFF, metadata: { memory: false } };
|
|
34
|
+
case "list":
|
|
35
|
+
return { content: recallListText(result.records, result.top), metadata: { projectID: result.projectID, top: result.top, records: result.records } };
|
|
36
|
+
case "record":
|
|
37
|
+
return { content: recallRecordText(result.record), metadata: { projectID: result.projectID, record: result.record } };
|
|
38
|
+
case "missing":
|
|
39
|
+
return { content: recallMissingText(result.sessionID), metadata: { projectID: result.projectID, sessionID: result.sessionID, found: false } };
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
tools.add({
|
|
44
|
+
name: NOTE_TOOL,
|
|
45
|
+
options: { codemode: false },
|
|
46
|
+
description: "Write the one lesson of your session tree for the next orchestrator in this project, once you have verified and " +
|
|
47
|
+
"integrated the work: what it should know about running a tree like this one that the record's facts do not say. How " +
|
|
48
|
+
"it was split (which parts collided, what was too small to hand off), how it was verified (what a part got wrong and " +
|
|
49
|
+
"what caught it), how the parts were briefed, or what the person decided. Name the job it comes from and the reason, " +
|
|
50
|
+
"so that a lesson for one kind of job can be told from one that holds everywhere. One line, at most " +
|
|
51
|
+
`${LESSON_MAX} characters; a longer one is refused, not clipped, so write it again shorter. Kept on your own tree's ` +
|
|
52
|
+
"record and listed by courier_recall after its facts, dated; a second call replaces the first. From the top session " +
|
|
53
|
+
"or one that started sessions, never a leaf. No code, no secrets, and not a rule that holds every time: that belongs " +
|
|
54
|
+
"in AGENTS.md or the skill, for the person to put there, so tell them instead.",
|
|
55
|
+
input: NoteInput,
|
|
56
|
+
execute: async (input, context) => {
|
|
57
|
+
const result = await note(ports.memory, context.sessionID, input).catch(rethrow(NOTE_TOOL));
|
|
58
|
+
// A refusal is the tool's failure, as courier_spawn's are, so the step ends on it and the model reads why.
|
|
59
|
+
const refuse = (text) => describeFailure(NOTE_TOOL, new Error(text));
|
|
60
|
+
switch (result.kind) {
|
|
61
|
+
case "noted": {
|
|
62
|
+
const { projectID, top, lesson, replaced } = result;
|
|
63
|
+
return { content: noteText(top, lesson, replaced), metadata: { projectID, top, lesson, ...(replaced ? { replaced } : {}) } };
|
|
64
|
+
}
|
|
65
|
+
case "off":
|
|
66
|
+
throw refuse(NOTE_OFF);
|
|
67
|
+
case "leaf":
|
|
68
|
+
throw refuse(noteLeafRefusal(result.depth, result.maxDepth));
|
|
69
|
+
case "empty":
|
|
70
|
+
throw refuse(NOTE_EMPTY);
|
|
71
|
+
case "long":
|
|
72
|
+
throw refuse(noteLongRefusal(result.length));
|
|
73
|
+
case "unrecorded":
|
|
74
|
+
throw refuse(noteUnrecordedRefusal(result.top));
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin/tui";
|
|
2
|
+
import { type Forgotten, type MemoryLesson, type MemoryPart, type MemoryRecord } from "./rpc.js";
|
|
3
|
+
type Context = Plugin.Context;
|
|
4
|
+
/** The command that opens the memory view: `/courier memory`, or the palette. */
|
|
5
|
+
export declare const MEMORY_COMMAND = "courier.memory";
|
|
6
|
+
/** The memory view's line of the `/courier` command's usage. */
|
|
7
|
+
export declare const MEMORY_USAGE = "/courier memory lists this project's tree records, to read or forget";
|
|
8
|
+
/** What the view says, in a toast, with the memory option off in the project asked. */
|
|
9
|
+
export declare const MEMORY_OFF = "Memory is off in this project: the plugin's memory option is false, so no record of a session tree is kept or listed here.";
|
|
10
|
+
/** What the view says, in a toast, when the project has no record: when none was written yet, or the last was forgotten. */
|
|
11
|
+
export declare const NO_RECORDS = "No record of a session tree in this project: one is written as each turn in a tree ends, and kept 90 days.";
|
|
12
|
+
/** The most characters of a lesson a list row's line under it shows; the record in full has it whole. */
|
|
13
|
+
export declare const LESSON_LINE_MAX = 160;
|
|
14
|
+
/** The most characters of a title a list row shows, so that "(this session)" after it fits the host's 61. */
|
|
15
|
+
export declare const ROW_TITLE_MAX = 46;
|
|
16
|
+
/** The group of the records moved on the current local day, over the others' local dates, as the host's Sessions dialog has it. */
|
|
17
|
+
export declare const TODAY = "Today";
|
|
18
|
+
/** The widest a row of the lesson in a record in full is; a longer lesson wraps over rows. */
|
|
19
|
+
export declare const LESSON_ROW = 90;
|
|
20
|
+
/** A record's name, as its rows and confirms show it: its title on one line, else its top session. */
|
|
21
|
+
export declare const nameOf: (record: MemoryRecord) => string;
|
|
22
|
+
/** A record's parts counted by status, in the order of `PART_STATUSES`: "3 parts: 2 done, 1 partial"; "no parts" with none. */
|
|
23
|
+
export declare function partsCounted(parts: ReadonlyArray<MemoryPart>): string;
|
|
24
|
+
/** The parts' costs summed, when any is known. */
|
|
25
|
+
export declare function costOf(record: MemoryRecord): number | undefined;
|
|
26
|
+
/** A list row's title: the tree's title, its top session's id without one; the open session's own tree says so. */
|
|
27
|
+
export declare function recordTitle(record: MemoryRecord, open?: string): string;
|
|
28
|
+
/** A list row's group: `Today` for a tree last moved on the local day of `now`, else that local date; the RPC's order, by the last move, keeps the groups newest first. */
|
|
29
|
+
export declare function recordCategory(record: MemoryRecord, now?: number): string;
|
|
30
|
+
/** A lesson as the view shows it: dated on the local day, from which depth when a session below the top wrote it, and its text. */
|
|
31
|
+
export declare function lessonHeading(lesson: MemoryLesson): string;
|
|
32
|
+
/** A list row's description: its parts by status, how long the tree ran, what its parts cost, and that it is not finished when it is not. */
|
|
33
|
+
export declare function recordDescription(record: MemoryRecord): string;
|
|
34
|
+
/** The lines under a list row: the lesson, dated and from which depth, clipped; none without a lesson. */
|
|
35
|
+
export declare function recordDetails(record: MemoryRecord): string[];
|
|
36
|
+
/** One part as the record in full describes it: its status, depth, isolation, group, duration, cost, context use and spawns refused. */
|
|
37
|
+
export declare function partLine(part: MemoryPart): string;
|
|
38
|
+
/** `text` in rows of at most `width` characters, broken at spaces where it can be. */
|
|
39
|
+
export declare function wrap(text: string, width: number): string[];
|
|
40
|
+
/** A row of a record in full: a fact of the tree, one of its parts, or a row of its lesson (0 its heading), which `x` forgets alone. */
|
|
41
|
+
export type Line = {
|
|
42
|
+
readonly kind: "fact";
|
|
43
|
+
readonly key: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: "part";
|
|
46
|
+
readonly sessionID: string;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: "lesson";
|
|
49
|
+
readonly row: number;
|
|
50
|
+
};
|
|
51
|
+
/** Where the cursor goes in a record in full after its lesson is forgotten: the lesson's first row, or the row that replaced it. */
|
|
52
|
+
export declare const lessonLine: (record: MemoryRecord) => Line;
|
|
53
|
+
/** The records the list's cursor tries, in order, after one is forgotten, kept or left: it, unless forgotten, then those after it, then those before it, nearest first. */
|
|
54
|
+
export declare function nearby(sessionIDs: ReadonlyArray<string>, sessionID: string, forgotten: boolean): string[];
|
|
55
|
+
/** The rows of a record in full, in three categories: the tree's facts, its parts and its lesson. */
|
|
56
|
+
export declare function detailOptions(record: MemoryRecord): Array<{
|
|
57
|
+
title: string;
|
|
58
|
+
value: Line;
|
|
59
|
+
category: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
}>;
|
|
62
|
+
/** What the confirm asks before a forget: the record named, with the local day it last moved, and what goes and what stays. */
|
|
63
|
+
export declare function forgetMessage(record: MemoryRecord, lesson: boolean): string;
|
|
64
|
+
/** The toast after a forget, as the server reports it. */
|
|
65
|
+
export declare function forgottenLine(record: MemoryRecord, outcome: Forgotten["outcome"]): string;
|
|
66
|
+
/**
|
|
67
|
+
* Adds the memory view: the project's records (the open session's location, else the TUI's, else the server's default) under
|
|
68
|
+
* `Today` and the local days they last moved; `enter` shows one in full, `x` forgets one or its lesson after a confirm, `esc` goes back and closes.
|
|
69
|
+
*/
|
|
70
|
+
export declare function addMemoryView(ctx: Context): {
|
|
71
|
+
open: () => Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
export {};
|
package/dist/person/rpc.d.ts
CHANGED
|
@@ -106,8 +106,8 @@ export declare const personRpc: {
|
|
|
106
106
|
readonly sessions: readonly {
|
|
107
107
|
readonly sessionID: string;
|
|
108
108
|
readonly title: string;
|
|
109
|
-
readonly parentID: string;
|
|
110
109
|
readonly depth: number;
|
|
110
|
+
readonly parentID: string;
|
|
111
111
|
readonly group?: string | undefined;
|
|
112
112
|
}[];
|
|
113
113
|
readonly permissions: readonly {
|
|
@@ -140,8 +140,8 @@ export declare const personRpc: {
|
|
|
140
140
|
readonly sessions: readonly {
|
|
141
141
|
readonly sessionID: string;
|
|
142
142
|
readonly title: string;
|
|
143
|
-
readonly parentID: string;
|
|
144
143
|
readonly depth: number;
|
|
144
|
+
readonly parentID: string;
|
|
145
145
|
readonly group?: string | undefined;
|
|
146
146
|
}[];
|
|
147
147
|
readonly permissions: readonly {
|
|
@@ -227,11 +227,11 @@ export declare const personRpc: {
|
|
|
227
227
|
}>;
|
|
228
228
|
readonly output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
229
229
|
readonly answered: boolean;
|
|
230
|
-
readonly by?: "
|
|
230
|
+
readonly by?: "message" | "result" | undefined;
|
|
231
231
|
readonly problem?: string | undefined;
|
|
232
232
|
}, {
|
|
233
233
|
readonly answered: boolean;
|
|
234
|
-
readonly by?: "
|
|
234
|
+
readonly by?: "message" | "result" | undefined;
|
|
235
235
|
readonly problem?: string | undefined;
|
|
236
236
|
}> & Schema.Struct<{
|
|
237
237
|
readonly answered: Schema.Boolean;
|
package/dist/platform/json.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Readers for untyped JSON, such as a webhook payload or a plugin option: the value when it has the type, else nothing. */
|
|
2
2
|
export declare const str: (value: unknown) => string | undefined;
|
|
3
3
|
export declare const num: (value: unknown) => number | undefined;
|
|
4
|
+
export declare const finite: (value: unknown) => number | undefined;
|
|
4
5
|
export declare const obj: (value: unknown) => Record<string, any>;
|
|
5
6
|
/** Epoch milliseconds from a session time, a `DateTime` in OpenCode's API and a number in tests; NaN for none. */
|
|
6
7
|
export declare function millis(value: unknown): number;
|
package/dist/platform/json.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Readers for untyped JSON, such as a webhook payload or a plugin option: the value when it has the type, else nothing. */
|
|
2
2
|
export const str = (value) => (typeof value === "string" ? value : undefined);
|
|
3
3
|
export const num = (value) => (typeof value === "number" && Number.isInteger(value) ? value : undefined);
|
|
4
|
+
export const finite = (value) => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
|
|
4
5
|
export const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
5
6
|
/** Epoch milliseconds from a session time, a `DateTime` in OpenCode's API and a number in tests; NaN for none. */
|
|
6
7
|
export function millis(value) {
|
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
|
}
|
package/dist/rpc.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { treeRpc, type Row, type Stopped, type Summary, type Tree, type TreeRpc, type Wait } from "./tree/rpc.js";
|
|
2
2
|
export { personRpc, type Answered, type Asks, type PersonRpc } from "./person/rpc.js";
|
|
3
|
+
export { memoryRpc, type Forgotten as MemoryForgotten, type MemoryLesson, type MemoryPart, type MemoryRecord, type MemoryRpc, type Records } from "./memory/rpc.js";
|
package/dist/rpc.js
CHANGED
package/dist/skill.js
CHANGED
|
@@ -10,8 +10,9 @@ export const SKILL_CONTENT = [
|
|
|
10
10
|
"How to split a task across OpenCode sessions with opencode-courier, and bring the results back up. Your brief, or the " +
|
|
11
11
|
"opencode-courier role line in your system prompt, names your depth in the session tree and its limits.",
|
|
12
12
|
"",
|
|
13
|
-
"1. Decide: split or do.
|
|
14
|
-
"
|
|
13
|
+
"1. Decide: split or do. First call courier_recall once: it lists how earlier trees in this project were split and how " +
|
|
14
|
+
"each part went, so weigh that; one call, before you decide, never in a loop. Split when the task has 2 or more " +
|
|
15
|
+
"independent, substantial parts touching separate files or areas; do it yourself when it is small, sequential or tightly coupled. Never start exactly one session for the whole " +
|
|
15
16
|
"task: it only adds a hop. What you read and write yourself stays in your context for every later turn, so keep the " +
|
|
16
17
|
"deciding, checking and integrating, and hand off the long reading and writing. An implementation task with one tightly " +
|
|
17
18
|
"coupled core usually still has independent parts: finding out what the change needs (a session with isolate: false " +
|
|
@@ -44,7 +45,13 @@ export const SKILL_CONTENT = [
|
|
|
44
45
|
"7. Integrate: merge each reported branch into your own checkout (git merge courier/<session>), resolve conflicts " +
|
|
45
46
|
"yourself, run the checks once more, then courier_cleanup the isolated sessions you are done with.",
|
|
46
47
|
"8. Report upward if a session started you: one courier_send to it, with one status for the whole (done only when every " +
|
|
47
|
-
"part is verified and integrated; partial, blocked or failed otherwise) and the artifacts. At the top,
|
|
48
|
+
"part is verified and integrated; partial, blocked or failed otherwise) and the artifacts. At the top, before you reply " +
|
|
49
|
+
"to the person: if there is something the next tree in this project should know about running a tree like this one (how " +
|
|
50
|
+
"it was split, what a part got wrong and what caught it, what a brief should have said, what the person decided), one " +
|
|
51
|
+
"courier_note with that one line; none when there is nothing. Name the job it comes from and the reason, so that a " +
|
|
52
|
+
"lesson for one kind of job is not taken for every job; the tool refuses a line over its limit rather than clipping " +
|
|
53
|
+
"it, so write it again shorter. A rule that holds every time goes to AGENTS.md or the skill, through the person, not " +
|
|
54
|
+
"into a note. Then reply to the person.",
|
|
48
55
|
"",
|
|
49
56
|
"When a part goes wrong:",
|
|
50
57
|
"- A failed turn (failed=\"...\") or a silent end (ended=\"without-report\", quoting the session's last reply): message " +
|
package/dist/tree/context.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface Shaped {
|
|
|
9
9
|
}>;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* The `context` hook: names a session's role in its tree in a system part, and hides courier_spawn
|
|
13
|
-
* from one at `maxDepth`. A session nobody spawned that has started none is pointed to the skill, if it could spawn.
|
|
12
|
+
* The `context` hook: names a session's role in its tree in a system part, and hides courier_spawn, and the tools
|
|
13
|
+
* named `hiddenFromLeaf`, from one at `maxDepth`. A session nobody spawned that has started none is pointed to the skill, if it could spawn.
|
|
14
14
|
*/
|
|
15
15
|
export declare function shapeContext(ports: ContextPorts, event: Shaped): Promise<void>;
|
package/dist/tree/context.js
CHANGED
|
@@ -4,8 +4,8 @@ import { ROLE_PREFIX, rolePart } from "./text.js";
|
|
|
4
4
|
/** How many sessions' depths an instance remembers. */
|
|
5
5
|
const ROLES_MAX = 1000;
|
|
6
6
|
/**
|
|
7
|
-
* The `context` hook: names a session's role in its tree in a system part, and hides courier_spawn
|
|
8
|
-
* from one at `maxDepth`. A session nobody spawned that has started none is pointed to the skill, if it could spawn.
|
|
7
|
+
* The `context` hook: names a session's role in its tree in a system part, and hides courier_spawn, and the tools
|
|
8
|
+
* named `hiddenFromLeaf`, from one at `maxDepth`. A session nobody spawned that has started none is pointed to the skill, if it could spawn.
|
|
9
9
|
*/
|
|
10
10
|
export async function shapeContext(ports, event) {
|
|
11
11
|
try {
|
|
@@ -13,7 +13,8 @@ export async function shapeContext(ports, event) {
|
|
|
13
13
|
if (depth === null && !event.tools.courier_spawn)
|
|
14
14
|
return;
|
|
15
15
|
if (depth !== null && depth >= ports.limits.maxDepth)
|
|
16
|
-
|
|
16
|
+
for (const tool of ["courier_spawn", ...(ports.hiddenFromLeaf ?? [])])
|
|
17
|
+
delete event.tools[tool];
|
|
17
18
|
if (!event.system.some((part) => part.text.startsWith(ROLE_PREFIX)))
|
|
18
19
|
event.system.push({ type: "text", text: rolePart(depth, ports.limits) });
|
|
19
20
|
}
|
package/dist/tree/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export { dirtyOf, headOf, inspectWorktree } from "./cleanup.js";
|
|
2
2
|
export { shapeContext } from "./context.js";
|
|
3
|
-
export { lineageIn, nativeRootOf, readLimits, readNativeChildren } from "./domain.js";
|
|
3
|
+
export { busy, lineageIn, nativeRootOf, readLimits, readNativeChildren } from "./domain.js";
|
|
4
4
|
export { together, treeEvents, waitsOn } from "./events.js";
|
|
5
5
|
export { deliverReleased } from "./group.js";
|
|
6
|
-
export
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
6
|
+
export { contextOf } from "./panel.js";
|
|
7
|
+
export type { CleanupPorts, CourierPorts, ModelWindow, Pending, PendingPrompt, RosterEntry, RosterStorage, ShownForm, SpawnGate, Waits, WatchPorts, WatchState, } from "./ports.js";
|
|
8
|
+
export { allEntries, answeringTop, childrenOf, indexedLineage, lineage, pruneExpired, reportState, RETENTION_MS, type ReportState } from "./roster.js";
|
|
9
|
+
export { subtree, type Below } from "./subtree.js";
|
|
9
10
|
export { registerTreeRpc } from "./rpc-handlers.js";
|
|
10
11
|
export { addTreeTools } from "./tools.js";
|
|
11
|
-
export { wakeMissed } from "./wake.js";
|
|
12
|
+
export { stalledAmong, wakeMissed } from "./wake.js";
|
package/dist/tree/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// What the rest of the plugin uses of the session tree: its tools, RPC, event module, context hook and tick jobs
|
|
2
|
-
// for `src/index.ts`,
|
|
2
|
+
// for `src/index.ts`, the lineage and subtree queries and types `person/` reads, and what `memory/` reads of each session.
|
|
3
3
|
export { dirtyOf, headOf, inspectWorktree } from "./cleanup.js";
|
|
4
4
|
export { shapeContext } from "./context.js";
|
|
5
|
-
export { lineageIn, nativeRootOf, readLimits, readNativeChildren } from "./domain.js";
|
|
5
|
+
export { busy, lineageIn, nativeRootOf, readLimits, readNativeChildren } from "./domain.js";
|
|
6
6
|
export { together, treeEvents, waitsOn } from "./events.js";
|
|
7
7
|
export { deliverReleased } from "./group.js";
|
|
8
|
-
export {
|
|
8
|
+
export { contextOf } from "./panel.js";
|
|
9
|
+
export { allEntries, answeringTop, childrenOf, indexedLineage, lineage, pruneExpired, reportState, RETENTION_MS } from "./roster.js";
|
|
9
10
|
export { subtree } from "./subtree.js";
|
|
10
11
|
export { registerTreeRpc } from "./rpc-handlers.js";
|
|
11
12
|
export { addTreeTools } from "./tools.js";
|
|
12
|
-
export { wakeMissed } from "./wake.js";
|
|
13
|
+
export { stalledAmong, wakeMissed } from "./wake.js";
|
|
@@ -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
|
}
|