opencode-courier 0.13.1 → 0.13.2
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 +6 -6
- package/dist/tree/domain.d.ts +4 -4
- package/dist/tree/domain.js +3 -3
- package/dist/tree/panel.d.ts +2 -2
- package/dist/tree/panel.js +9 -6
- package/dist/tree/rpc-handlers.d.ts +126 -0
- package/dist/tree/rpc.d.ts +204 -0
- package/dist/tree/rpc.js +2 -0
- package/dist/tree/running.js +1 -1
- package/dist/tree/subtree.d.ts +7 -0
- package/dist/tree/subtree.js +35 -11
- package/dist/tree/tui.d.ts +8 -0
- package/dist/tree/wake.js +16 -6
- package/dist/tui.js +134 -79
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ A narrated tour of the terminal UI: the tree panel, the asks inbox, failing chil
|
|
|
18
18
|
## Quickstart
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm install -g @opencode/cli@2.2.
|
|
21
|
+
npm install -g @opencode/cli@2.2.2 # OpenCode V2, the version courier is tested on
|
|
22
22
|
opencode plugin add opencode-courier
|
|
23
23
|
opencode
|
|
24
24
|
```
|
|
@@ -62,10 +62,10 @@ split a task and how to check and merge the parts. Every detail: [the reference]
|
|
|
62
62
|
## Install
|
|
63
63
|
|
|
64
64
|
Requires OpenCode V2: the `opencode` command from `@opencode/cli`. Each release is tested against
|
|
65
|
-
one OpenCode version; this one is tested on **OpenCode 2.2.
|
|
65
|
+
one OpenCode version; this one is tested on **OpenCode 2.2.2** (check with `opencode --version`).
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
npm install -g @opencode/cli@2.2.
|
|
68
|
+
npm install -g @opencode/cli@2.2.2
|
|
69
69
|
opencode plugin add opencode-courier
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -78,13 +78,13 @@ name, without a version: OpenCode offers updates only for an entry without an ex
|
|
|
78
78
|
<!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
|
|
79
79
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
80
80
|
|---|---|
|
|
81
|
+
| 0.13.2 | 2.2.2 |
|
|
81
82
|
| 0.13.1 | 2.2.1 |
|
|
82
83
|
| 0.13.0 | 2.2.0 |
|
|
83
|
-
| 0.11.1, 0.12.0 | 2.1.4 |
|
|
84
84
|
<!-- compat-table:end -->
|
|
85
85
|
|
|
86
86
|
Every release: [the full table](docs/reference.md#the-opencode-version). The server part loads on
|
|
87
|
-
any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.
|
|
87
|
+
any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.2 only.
|
|
88
88
|
|
|
89
89
|
## Options
|
|
90
90
|
|
|
@@ -128,7 +128,7 @@ Parameters and behaviour: [the reference](docs/reference.md#the-tools).
|
|
|
128
128
|
## In the terminal
|
|
129
129
|
|
|
130
130
|
- **Footer:** `courier 1 waiting on you · 2 running · group pair 1/2`.
|
|
131
|
-
- **`/courier tree`:** a panel with the tree. `enter` opens a session, `s` stops it, `q` closes.
|
|
131
|
+
- **`/courier tree`:** a panel with the tree, the session it was opened in as its first row. `enter` opens a session, `s` stops it, `q` closes.
|
|
132
132
|
- **`/courier stop <session>`:** stops a session and everything under it.
|
|
133
133
|
- **`ctrl+x k`** or **`/asks`:** every ask waiting on you in the tree, answered in a dialog.
|
|
134
134
|
- **`/courier memory`:** the project's tree records. `p` pins one, `x` forgets one.
|
package/dist/tree/domain.d.ts
CHANGED
|
@@ -24,11 +24,11 @@ export declare function readNativeChildren(options: unknown): {
|
|
|
24
24
|
};
|
|
25
25
|
/** A session's times, as `session.get` gives them. */
|
|
26
26
|
type Times = Awaited<ReturnType<Context["session"]["get"]>>["time"];
|
|
27
|
-
/** A session's times as `busy` reads them. */
|
|
28
|
-
export type BusyTimes = Pick<Times, "updated" | "idle">;
|
|
27
|
+
/** A session's times as `busy` reads them; `busy`, when its latest execution started, is stamped since OpenCode 2.2.2. */
|
|
28
|
+
export type BusyTimes = Pick<Times, "updated" | "idle" | "busy">;
|
|
29
29
|
/**
|
|
30
|
-
* Whether a session may run by its times: it has never finished a turn, or
|
|
31
|
-
*
|
|
30
|
+
* Whether a session may run by its times: it has never finished a turn, or its latest execution started after the last one ended
|
|
31
|
+
* (`busy`; without it `updated`, which a rename, an archive or a waiting inbox item also stamp); only its transcript then tells.
|
|
32
32
|
*/
|
|
33
33
|
export declare const busy: (time: BusyTimes) => boolean;
|
|
34
34
|
/** A stored message as `turnEnd` reads it: an assistant message's `error` says how its step ended. */
|
package/dist/tree/domain.js
CHANGED
|
@@ -24,10 +24,10 @@ export function readNativeChildren(options) {
|
|
|
24
24
|
return { nativeChildren: false, problems: [`nativeChildren must be true or false, not ${JSON.stringify(value)}; using false`] };
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Whether a session may run by its times: it has never finished a turn, or
|
|
28
|
-
*
|
|
27
|
+
* Whether a session may run by its times: it has never finished a turn, or its latest execution started after the last one ended
|
|
28
|
+
* (`busy`; without it `updated`, which a rename, an archive or a waiting inbox item also stamp); only its transcript then tells.
|
|
29
29
|
*/
|
|
30
|
-
export const busy = (time) => time.idle === undefined || millis(time.updated) > millis(time.idle);
|
|
30
|
+
export const busy = (time) => time.idle === undefined || millis(time.busy ?? time.updated) > millis(time.idle);
|
|
31
31
|
/** The messages the person's switch of agent or model, move or shell command appends, at rest or mid-turn alike. */
|
|
32
32
|
const ASIDES = new Set(["agent-switched", "model-switched", "location-switched", "shell"]);
|
|
33
33
|
/**
|
package/dist/tree/panel.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export declare const waitOf: (pending: Pending) => Wait;
|
|
|
13
13
|
/** A form a session shows, as the panel says what it waits on. */
|
|
14
14
|
export declare const formWait: (form: ShownForm) => Wait;
|
|
15
15
|
/**
|
|
16
|
-
* The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`,
|
|
17
|
-
*
|
|
16
|
+
* The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, live ones first,
|
|
17
|
+
* then the newest, each as `row` reads it; `top` the session itself, `truncated` the ones left out, `stalled` its stall.
|
|
18
18
|
*/
|
|
19
19
|
export declare function panel(ports: CourierPorts, rootID: string): Promise<Tree>;
|
|
20
20
|
export {};
|
package/dist/tree/panel.js
CHANGED
|
@@ -2,7 +2,7 @@ import { startedAs } from "./domain.js";
|
|
|
2
2
|
import { lastStatus, stoppedSincePrompt } from "./report.js";
|
|
3
3
|
import { standingIn } from "./status.js";
|
|
4
4
|
import { childStore } from "./store.js";
|
|
5
|
-
import { readTree } from "./subtree.js";
|
|
5
|
+
import { readTop, readTree } from "./subtree.js";
|
|
6
6
|
import { stalledAmong } from "./wake.js";
|
|
7
7
|
/** The last tool call in a session's messages, and when it was made. */
|
|
8
8
|
export function lastTool(messages) {
|
|
@@ -91,14 +91,17 @@ async function row(ports, windows, { entry, depth, info, running, endedAt, pendi
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
|
-
* The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`,
|
|
95
|
-
*
|
|
94
|
+
* The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, live ones first,
|
|
95
|
+
* then the newest, each as `row` reads it; `top` the session itself, `truncated` the ones left out, `stalled` its stall.
|
|
96
96
|
*/
|
|
97
97
|
export async function panel(ports, rootID) {
|
|
98
|
-
const { kept, leftOut } = await readTree(ports, rootID);
|
|
98
|
+
const [{ kept, leftOut }, root] = await Promise.all([readTree(ports, rootID), readTop(ports, rootID)]);
|
|
99
99
|
const stalled = await stalledAmong(ports, [rootID, ...kept.map((node) => node.entry.sessionID)]);
|
|
100
100
|
const windows = await ports.models().catch(() => []);
|
|
101
|
-
const rows = await Promise.all(
|
|
101
|
+
const [rows, top] = await Promise.all([
|
|
102
|
+
Promise.all(kept.map((node) => row(ports, windows, node, stalled.get(node.entry.sessionID)))),
|
|
103
|
+
root && row(ports, windows, root, stalled.get(rootID)),
|
|
104
|
+
]);
|
|
102
105
|
const own = stalled.get(rootID);
|
|
103
|
-
return { rows, ...(leftOut ? { truncated: leftOut } : {}), ...(own !== undefined ? { stalled: own } : {}) };
|
|
106
|
+
return { rows, ...(top ? { top } : {}), ...(leftOut ? { truncated: leftOut } : {}), ...(own !== undefined ? { stalled: own } : {}) };
|
|
104
107
|
}
|
|
@@ -96,6 +96,48 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
96
96
|
readonly resumes?: number | undefined;
|
|
97
97
|
}[];
|
|
98
98
|
readonly stalled?: number | undefined;
|
|
99
|
+
readonly top?: {
|
|
100
|
+
readonly sessionID: string;
|
|
101
|
+
readonly title: string;
|
|
102
|
+
readonly depth: number;
|
|
103
|
+
readonly isolated: boolean;
|
|
104
|
+
readonly parentID: string;
|
|
105
|
+
readonly created: number;
|
|
106
|
+
readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
|
|
107
|
+
readonly waits: readonly ({
|
|
108
|
+
readonly type: "permission";
|
|
109
|
+
readonly action: string;
|
|
110
|
+
readonly resources: readonly string[];
|
|
111
|
+
} | {
|
|
112
|
+
readonly type: "question";
|
|
113
|
+
readonly question: string;
|
|
114
|
+
} | {
|
|
115
|
+
readonly type: "form";
|
|
116
|
+
readonly title: string;
|
|
117
|
+
})[];
|
|
118
|
+
readonly stopped?: boolean | undefined;
|
|
119
|
+
readonly stalled?: number | undefined;
|
|
120
|
+
readonly group?: {
|
|
121
|
+
readonly name: string;
|
|
122
|
+
readonly standing: string;
|
|
123
|
+
} | undefined;
|
|
124
|
+
readonly cost?: number | undefined;
|
|
125
|
+
readonly context?: number | undefined;
|
|
126
|
+
readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
127
|
+
readonly report?: {
|
|
128
|
+
readonly progressed: boolean;
|
|
129
|
+
readonly state: "ended" | "reported" | "owed";
|
|
130
|
+
readonly status?: string | undefined;
|
|
131
|
+
} | undefined;
|
|
132
|
+
readonly idle?: number | undefined;
|
|
133
|
+
readonly native?: boolean | undefined;
|
|
134
|
+
readonly model?: string | undefined;
|
|
135
|
+
readonly tool?: {
|
|
136
|
+
readonly name: string;
|
|
137
|
+
readonly at: number;
|
|
138
|
+
} | undefined;
|
|
139
|
+
readonly resumes?: number | undefined;
|
|
140
|
+
} | undefined;
|
|
99
141
|
readonly truncated?: number | undefined;
|
|
100
142
|
}, {
|
|
101
143
|
readonly rows: readonly {
|
|
@@ -141,6 +183,48 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
141
183
|
readonly resumes?: number | undefined;
|
|
142
184
|
}[];
|
|
143
185
|
readonly stalled?: number | undefined;
|
|
186
|
+
readonly top?: {
|
|
187
|
+
readonly sessionID: string;
|
|
188
|
+
readonly title: string;
|
|
189
|
+
readonly depth: number;
|
|
190
|
+
readonly isolated: boolean;
|
|
191
|
+
readonly parentID: string;
|
|
192
|
+
readonly created: number;
|
|
193
|
+
readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
|
|
194
|
+
readonly waits: readonly ({
|
|
195
|
+
readonly type: "permission";
|
|
196
|
+
readonly action: string;
|
|
197
|
+
readonly resources: readonly string[];
|
|
198
|
+
} | {
|
|
199
|
+
readonly type: "question";
|
|
200
|
+
readonly question: string;
|
|
201
|
+
} | {
|
|
202
|
+
readonly type: "form";
|
|
203
|
+
readonly title: string;
|
|
204
|
+
})[];
|
|
205
|
+
readonly stopped?: boolean | undefined;
|
|
206
|
+
readonly stalled?: number | undefined;
|
|
207
|
+
readonly group?: {
|
|
208
|
+
readonly name: string;
|
|
209
|
+
readonly standing: string;
|
|
210
|
+
} | undefined;
|
|
211
|
+
readonly cost?: number | undefined;
|
|
212
|
+
readonly context?: number | undefined;
|
|
213
|
+
readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
214
|
+
readonly report?: {
|
|
215
|
+
readonly progressed: boolean;
|
|
216
|
+
readonly state: "ended" | "reported" | "owed";
|
|
217
|
+
readonly status?: string | undefined;
|
|
218
|
+
} | undefined;
|
|
219
|
+
readonly idle?: number | undefined;
|
|
220
|
+
readonly native?: boolean | undefined;
|
|
221
|
+
readonly model?: string | undefined;
|
|
222
|
+
readonly tool?: {
|
|
223
|
+
readonly name: string;
|
|
224
|
+
readonly at: number;
|
|
225
|
+
} | undefined;
|
|
226
|
+
readonly resumes?: number | undefined;
|
|
227
|
+
} | undefined;
|
|
144
228
|
readonly truncated?: number | undefined;
|
|
145
229
|
}> & import("effect/Schema").Struct<{
|
|
146
230
|
readonly rows: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
@@ -185,6 +269,48 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
185
269
|
readonly title: import("effect/Schema").String;
|
|
186
270
|
}>]>>;
|
|
187
271
|
}>>;
|
|
272
|
+
readonly top: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
|
|
273
|
+
readonly sessionID: import("effect/Schema").String;
|
|
274
|
+
readonly parentID: import("effect/Schema").String;
|
|
275
|
+
readonly title: import("effect/Schema").String;
|
|
276
|
+
readonly depth: import("effect/Schema").Int;
|
|
277
|
+
readonly state: import("effect/Schema").Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
|
|
278
|
+
readonly stalled: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
279
|
+
readonly outcome: import("effect/Schema").optionalKey<import("effect/Schema").Literals<readonly ["succeeded", "failed", "interrupted"]>>;
|
|
280
|
+
readonly resumes: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
281
|
+
readonly stopped: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
|
|
282
|
+
readonly report: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
|
|
283
|
+
readonly state: import("effect/Schema").Literals<readonly ["owed", "reported", "ended"]>;
|
|
284
|
+
readonly status: import("effect/Schema").optionalKey<import("effect/Schema").String>;
|
|
285
|
+
readonly progressed: import("effect/Schema").Boolean;
|
|
286
|
+
}>>;
|
|
287
|
+
readonly group: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
|
|
288
|
+
readonly name: import("effect/Schema").String;
|
|
289
|
+
readonly standing: import("effect/Schema").String;
|
|
290
|
+
}>>;
|
|
291
|
+
readonly isolated: import("effect/Schema").Boolean;
|
|
292
|
+
readonly native: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
|
|
293
|
+
readonly model: import("effect/Schema").optionalKey<import("effect/Schema").String>;
|
|
294
|
+
readonly created: import("effect/Schema").Number;
|
|
295
|
+
readonly idle: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
296
|
+
readonly cost: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
297
|
+
readonly context: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
298
|
+
readonly tool: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
|
|
299
|
+
readonly name: import("effect/Schema").String;
|
|
300
|
+
readonly at: import("effect/Schema").Number;
|
|
301
|
+
}>>;
|
|
302
|
+
readonly waits: import("effect/Schema").$Array<import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
|
|
303
|
+
readonly type: import("effect/Schema").Literal<"permission">;
|
|
304
|
+
readonly action: import("effect/Schema").String;
|
|
305
|
+
readonly resources: import("effect/Schema").$Array<import("effect/Schema").String>;
|
|
306
|
+
}>, import("effect/Schema").Struct<{
|
|
307
|
+
readonly type: import("effect/Schema").Literal<"question">;
|
|
308
|
+
readonly question: import("effect/Schema").String;
|
|
309
|
+
}>, import("effect/Schema").Struct<{
|
|
310
|
+
readonly type: import("effect/Schema").Literal<"form">;
|
|
311
|
+
readonly title: import("effect/Schema").String;
|
|
312
|
+
}>]>>;
|
|
313
|
+
}>>;
|
|
188
314
|
readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
|
|
189
315
|
readonly stalled: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
190
316
|
}>;
|
package/dist/tree/rpc.d.ts
CHANGED
|
@@ -149,6 +149,66 @@ declare const Tree: Schema.Struct<{
|
|
|
149
149
|
readonly title: Schema.String;
|
|
150
150
|
}>]>>;
|
|
151
151
|
}>>;
|
|
152
|
+
/** The panel's own session, at depth 0; absent when OpenCode no longer knows it, and from an older server. */
|
|
153
|
+
readonly top: Schema.optionalKey<Schema.Struct<{
|
|
154
|
+
readonly sessionID: Schema.String;
|
|
155
|
+
readonly parentID: Schema.String;
|
|
156
|
+
readonly title: Schema.String;
|
|
157
|
+
/** A child of the panel's session is at depth 1. */
|
|
158
|
+
readonly depth: Schema.Int;
|
|
159
|
+
/**
|
|
160
|
+
* `waiting` on a request to the person or a form it shows them; `stalled` when a courier message waits in its inbox and
|
|
161
|
+
* waking it again did not start a turn; `gone` when OpenCode no longer knows it.
|
|
162
|
+
*/
|
|
163
|
+
readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
|
|
164
|
+
/** Since when its oldest message that did not wake it has waited, when it is stalled. */
|
|
165
|
+
readonly stalled: Schema.optionalKey<Schema.Number>;
|
|
166
|
+
/** How its last turn ended. */
|
|
167
|
+
readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
|
|
168
|
+
/** When the provider limit that failed its last turn lifts; left out once that has passed, or when it runs again or was stopped. */
|
|
169
|
+
readonly resumes: Schema.optionalKey<Schema.Number>;
|
|
170
|
+
/** Set when courier_stop stopped it since its last prompt. */
|
|
171
|
+
readonly stopped: Schema.optionalKey<Schema.Boolean>;
|
|
172
|
+
/** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
|
|
173
|
+
readonly report: Schema.optionalKey<Schema.Struct<{
|
|
174
|
+
readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
|
|
175
|
+
readonly status: Schema.optionalKey<Schema.String>;
|
|
176
|
+
readonly progressed: Schema.Boolean;
|
|
177
|
+
}>>;
|
|
178
|
+
/** Its join group, and its standing there: `held`, `out`, how it left, or `released`. */
|
|
179
|
+
readonly group: Schema.optionalKey<Schema.Struct<{
|
|
180
|
+
readonly name: Schema.String;
|
|
181
|
+
readonly standing: Schema.String;
|
|
182
|
+
}>>;
|
|
183
|
+
readonly isolated: Schema.Boolean;
|
|
184
|
+
/** Set when it is OpenCode's subagent of its parent (a native child), which OpenCode's session lists leave out. */
|
|
185
|
+
readonly native: Schema.optionalKey<Schema.Boolean>;
|
|
186
|
+
/** The model it was started on, `provider/model` with `#variant` when one is set, when that is not its parent's. */
|
|
187
|
+
readonly model: Schema.optionalKey<Schema.String>;
|
|
188
|
+
readonly created: Schema.Number;
|
|
189
|
+
/** When its last turn ended, if it is not running. */
|
|
190
|
+
readonly idle: Schema.optionalKey<Schema.Number>;
|
|
191
|
+
/** What it has cost so far, in US dollars. */
|
|
192
|
+
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
193
|
+
/** How full its context is, as a fraction of the model's window; left out when the model or its limit cannot be read. */
|
|
194
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
195
|
+
/** Its last tool call, and when it was made. */
|
|
196
|
+
readonly tool: Schema.optionalKey<Schema.Struct<{
|
|
197
|
+
readonly name: Schema.String;
|
|
198
|
+
readonly at: Schema.Number;
|
|
199
|
+
}>>;
|
|
200
|
+
readonly waits: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
|
|
201
|
+
readonly type: Schema.Literal<"permission">;
|
|
202
|
+
readonly action: Schema.String;
|
|
203
|
+
readonly resources: Schema.$Array<Schema.String>;
|
|
204
|
+
}>, Schema.Struct<{
|
|
205
|
+
readonly type: Schema.Literal<"question">;
|
|
206
|
+
readonly question: Schema.String;
|
|
207
|
+
}>, Schema.Struct<{
|
|
208
|
+
readonly type: Schema.Literal<"form">;
|
|
209
|
+
readonly title: Schema.String;
|
|
210
|
+
}>]>>;
|
|
211
|
+
}>>;
|
|
152
212
|
/** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
|
|
153
213
|
readonly truncated: Schema.optionalKey<Schema.Int>;
|
|
154
214
|
/** Since when a courier message has waited in the panel's own session, when that session is stalled. */
|
|
@@ -275,6 +335,48 @@ export declare const treeRpc: {
|
|
|
275
335
|
readonly resumes?: number | undefined;
|
|
276
336
|
}[];
|
|
277
337
|
readonly stalled?: number | undefined;
|
|
338
|
+
readonly top?: {
|
|
339
|
+
readonly sessionID: string;
|
|
340
|
+
readonly title: string;
|
|
341
|
+
readonly depth: number;
|
|
342
|
+
readonly isolated: boolean;
|
|
343
|
+
readonly parentID: string;
|
|
344
|
+
readonly created: number;
|
|
345
|
+
readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
|
|
346
|
+
readonly waits: readonly ({
|
|
347
|
+
readonly type: "permission";
|
|
348
|
+
readonly action: string;
|
|
349
|
+
readonly resources: readonly string[];
|
|
350
|
+
} | {
|
|
351
|
+
readonly type: "question";
|
|
352
|
+
readonly question: string;
|
|
353
|
+
} | {
|
|
354
|
+
readonly type: "form";
|
|
355
|
+
readonly title: string;
|
|
356
|
+
})[];
|
|
357
|
+
readonly stopped?: boolean | undefined;
|
|
358
|
+
readonly stalled?: number | undefined;
|
|
359
|
+
readonly group?: {
|
|
360
|
+
readonly name: string;
|
|
361
|
+
readonly standing: string;
|
|
362
|
+
} | undefined;
|
|
363
|
+
readonly cost?: number | undefined;
|
|
364
|
+
readonly context?: number | undefined;
|
|
365
|
+
readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
366
|
+
readonly report?: {
|
|
367
|
+
readonly progressed: boolean;
|
|
368
|
+
readonly state: "ended" | "reported" | "owed";
|
|
369
|
+
readonly status?: string | undefined;
|
|
370
|
+
} | undefined;
|
|
371
|
+
readonly idle?: number | undefined;
|
|
372
|
+
readonly native?: boolean | undefined;
|
|
373
|
+
readonly model?: string | undefined;
|
|
374
|
+
readonly tool?: {
|
|
375
|
+
readonly name: string;
|
|
376
|
+
readonly at: number;
|
|
377
|
+
} | undefined;
|
|
378
|
+
readonly resumes?: number | undefined;
|
|
379
|
+
} | undefined;
|
|
278
380
|
readonly truncated?: number | undefined;
|
|
279
381
|
}, {
|
|
280
382
|
readonly rows: readonly {
|
|
@@ -320,6 +422,48 @@ export declare const treeRpc: {
|
|
|
320
422
|
readonly resumes?: number | undefined;
|
|
321
423
|
}[];
|
|
322
424
|
readonly stalled?: number | undefined;
|
|
425
|
+
readonly top?: {
|
|
426
|
+
readonly sessionID: string;
|
|
427
|
+
readonly title: string;
|
|
428
|
+
readonly depth: number;
|
|
429
|
+
readonly isolated: boolean;
|
|
430
|
+
readonly parentID: string;
|
|
431
|
+
readonly created: number;
|
|
432
|
+
readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
|
|
433
|
+
readonly waits: readonly ({
|
|
434
|
+
readonly type: "permission";
|
|
435
|
+
readonly action: string;
|
|
436
|
+
readonly resources: readonly string[];
|
|
437
|
+
} | {
|
|
438
|
+
readonly type: "question";
|
|
439
|
+
readonly question: string;
|
|
440
|
+
} | {
|
|
441
|
+
readonly type: "form";
|
|
442
|
+
readonly title: string;
|
|
443
|
+
})[];
|
|
444
|
+
readonly stopped?: boolean | undefined;
|
|
445
|
+
readonly stalled?: number | undefined;
|
|
446
|
+
readonly group?: {
|
|
447
|
+
readonly name: string;
|
|
448
|
+
readonly standing: string;
|
|
449
|
+
} | undefined;
|
|
450
|
+
readonly cost?: number | undefined;
|
|
451
|
+
readonly context?: number | undefined;
|
|
452
|
+
readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
453
|
+
readonly report?: {
|
|
454
|
+
readonly progressed: boolean;
|
|
455
|
+
readonly state: "ended" | "reported" | "owed";
|
|
456
|
+
readonly status?: string | undefined;
|
|
457
|
+
} | undefined;
|
|
458
|
+
readonly idle?: number | undefined;
|
|
459
|
+
readonly native?: boolean | undefined;
|
|
460
|
+
readonly model?: string | undefined;
|
|
461
|
+
readonly tool?: {
|
|
462
|
+
readonly name: string;
|
|
463
|
+
readonly at: number;
|
|
464
|
+
} | undefined;
|
|
465
|
+
readonly resumes?: number | undefined;
|
|
466
|
+
} | undefined;
|
|
323
467
|
readonly truncated?: number | undefined;
|
|
324
468
|
}> & Schema.Struct<{
|
|
325
469
|
readonly rows: Schema.$Array<Schema.Struct<{
|
|
@@ -381,6 +525,66 @@ export declare const treeRpc: {
|
|
|
381
525
|
readonly title: Schema.String;
|
|
382
526
|
}>]>>;
|
|
383
527
|
}>>;
|
|
528
|
+
/** The panel's own session, at depth 0; absent when OpenCode no longer knows it, and from an older server. */
|
|
529
|
+
readonly top: Schema.optionalKey<Schema.Struct<{
|
|
530
|
+
readonly sessionID: Schema.String;
|
|
531
|
+
readonly parentID: Schema.String;
|
|
532
|
+
readonly title: Schema.String;
|
|
533
|
+
/** A child of the panel's session is at depth 1. */
|
|
534
|
+
readonly depth: Schema.Int;
|
|
535
|
+
/**
|
|
536
|
+
* `waiting` on a request to the person or a form it shows them; `stalled` when a courier message waits in its inbox and
|
|
537
|
+
* waking it again did not start a turn; `gone` when OpenCode no longer knows it.
|
|
538
|
+
*/
|
|
539
|
+
readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
|
|
540
|
+
/** Since when its oldest message that did not wake it has waited, when it is stalled. */
|
|
541
|
+
readonly stalled: Schema.optionalKey<Schema.Number>;
|
|
542
|
+
/** How its last turn ended. */
|
|
543
|
+
readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
|
|
544
|
+
/** When the provider limit that failed its last turn lifts; left out once that has passed, or when it runs again or was stopped. */
|
|
545
|
+
readonly resumes: Schema.optionalKey<Schema.Number>;
|
|
546
|
+
/** Set when courier_stop stopped it since its last prompt. */
|
|
547
|
+
readonly stopped: Schema.optionalKey<Schema.Boolean>;
|
|
548
|
+
/** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
|
|
549
|
+
readonly report: Schema.optionalKey<Schema.Struct<{
|
|
550
|
+
readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
|
|
551
|
+
readonly status: Schema.optionalKey<Schema.String>;
|
|
552
|
+
readonly progressed: Schema.Boolean;
|
|
553
|
+
}>>;
|
|
554
|
+
/** Its join group, and its standing there: `held`, `out`, how it left, or `released`. */
|
|
555
|
+
readonly group: Schema.optionalKey<Schema.Struct<{
|
|
556
|
+
readonly name: Schema.String;
|
|
557
|
+
readonly standing: Schema.String;
|
|
558
|
+
}>>;
|
|
559
|
+
readonly isolated: Schema.Boolean;
|
|
560
|
+
/** Set when it is OpenCode's subagent of its parent (a native child), which OpenCode's session lists leave out. */
|
|
561
|
+
readonly native: Schema.optionalKey<Schema.Boolean>;
|
|
562
|
+
/** The model it was started on, `provider/model` with `#variant` when one is set, when that is not its parent's. */
|
|
563
|
+
readonly model: Schema.optionalKey<Schema.String>;
|
|
564
|
+
readonly created: Schema.Number;
|
|
565
|
+
/** When its last turn ended, if it is not running. */
|
|
566
|
+
readonly idle: Schema.optionalKey<Schema.Number>;
|
|
567
|
+
/** What it has cost so far, in US dollars. */
|
|
568
|
+
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
569
|
+
/** How full its context is, as a fraction of the model's window; left out when the model or its limit cannot be read. */
|
|
570
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
571
|
+
/** Its last tool call, and when it was made. */
|
|
572
|
+
readonly tool: Schema.optionalKey<Schema.Struct<{
|
|
573
|
+
readonly name: Schema.String;
|
|
574
|
+
readonly at: Schema.Number;
|
|
575
|
+
}>>;
|
|
576
|
+
readonly waits: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
|
|
577
|
+
readonly type: Schema.Literal<"permission">;
|
|
578
|
+
readonly action: Schema.String;
|
|
579
|
+
readonly resources: Schema.$Array<Schema.String>;
|
|
580
|
+
}>, Schema.Struct<{
|
|
581
|
+
readonly type: Schema.Literal<"question">;
|
|
582
|
+
readonly question: Schema.String;
|
|
583
|
+
}>, Schema.Struct<{
|
|
584
|
+
readonly type: Schema.Literal<"form">;
|
|
585
|
+
readonly title: Schema.String;
|
|
586
|
+
}>]>>;
|
|
587
|
+
}>>;
|
|
384
588
|
/** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
|
|
385
589
|
readonly truncated: Schema.optionalKey<Schema.Int>;
|
|
386
590
|
/** Since when a courier message has waited in the panel's own session, when that session is stalled. */
|
package/dist/tree/rpc.js
CHANGED
|
@@ -68,6 +68,8 @@ const Row = Schema.Struct({
|
|
|
68
68
|
/** The sessions under a session, as courier_tree lists them, in the order a tree is drawn. */
|
|
69
69
|
const Tree = Schema.Struct({
|
|
70
70
|
rows: Schema.Array(Row),
|
|
71
|
+
/** The panel's own session, at depth 0; absent when OpenCode no longer knows it, and from an older server. */
|
|
72
|
+
top: Schema.optionalKey(Row),
|
|
71
73
|
/** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
|
|
72
74
|
truncated: Schema.optionalKey(Count),
|
|
73
75
|
/** Since when a courier message has waited in the panel's own session, when that session is stalled. */
|
package/dist/tree/running.js
CHANGED
|
@@ -27,7 +27,7 @@ function resolve(session, sessionID, time, now, load, cached) {
|
|
|
27
27
|
return Promise.resolve(rest(millis(time.idle)));
|
|
28
28
|
const known = answers.get(session) ?? new Map();
|
|
29
29
|
answers.set(session, known);
|
|
30
|
-
const stamp =
|
|
30
|
+
const stamp = [time.updated, time.idle, time.busy].map(millis).join(" ");
|
|
31
31
|
const found = known.get(sessionID);
|
|
32
32
|
if (cached && found?.stamp === stamp && now < found.until)
|
|
33
33
|
return found.turn;
|
package/dist/tree/subtree.d.ts
CHANGED
|
@@ -37,6 +37,13 @@ export interface Listed extends Looked {
|
|
|
37
37
|
export type ReadPorts = Pick<CourierPorts, "storage" | "session" | "pending" | "forms" | "now" | "limits">;
|
|
38
38
|
/** Every session under a session, without those past retention, each looked up, in the order a tree is drawn. */
|
|
39
39
|
export declare function lookUp(ports: ReadPorts, rootID: string): Promise<Looked[]>;
|
|
40
|
+
/** One session looked up, as `lookUp` does each; `info` when the caller already has it. */
|
|
41
|
+
export declare function look(ports: ReadPorts, node: Below, known?: Info): Promise<Looked>;
|
|
42
|
+
/**
|
|
43
|
+
* The session a subtree is read from, looked up and listed as its own sessions are, at depth 0: its roster entry when it
|
|
44
|
+
* was spawned, else one made of OpenCode's info of it; none when OpenCode does not know it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readTop(ports: ReadPorts, rootID: string): Promise<Listed | undefined>;
|
|
40
47
|
/**
|
|
41
48
|
* The listing of at most `max` of the sessions under a session: the live ones first, then the newest, with what is
|
|
42
49
|
* above them, in the order a tree is drawn (`cut`); `leftOut` counts the rest.
|
package/dist/tree/subtree.js
CHANGED
|
@@ -37,17 +37,41 @@ function drawn(byParent, parentID) {
|
|
|
37
37
|
/** Every session under a session, without those past retention, each looked up, in the order a tree is drawn. */
|
|
38
38
|
export async function lookUp(ports, rootID) {
|
|
39
39
|
const { nodes } = await subtree(ports.storage, rootID, { now: ports.now() });
|
|
40
|
-
return Promise.all(nodes.map(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
return Promise.all(nodes.map((node) => look(ports, node)));
|
|
41
|
+
}
|
|
42
|
+
/** One session looked up, as `lookUp` does each; `info` when the caller already has it. */
|
|
43
|
+
export async function look(ports, node, known) {
|
|
44
|
+
const [info, pending, forms] = await Promise.all([
|
|
45
|
+
known ?? ports.session.get({ sessionID: node.entry.sessionID }).catch(() => undefined),
|
|
46
|
+
ports.pending(node.entry.sessionID).catch(() => []),
|
|
47
|
+
ports.forms(node.entry.sessionID).catch(() => []),
|
|
48
|
+
]);
|
|
49
|
+
const turn = info === undefined ? undefined : await turnOf(ports.session, node.entry.sessionID, info.time);
|
|
50
|
+
const running = turn?.running ?? false;
|
|
51
|
+
// A form outlives its session's turn only when its withdrawal was missed, as while no instance followed the events.
|
|
52
|
+
return { ...node, info, running, ...(turn?.endedAt !== undefined ? { endedAt: turn.endedAt } : {}), pending, forms: running ? forms : [] };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The session a subtree is read from, looked up and listed as its own sessions are, at depth 0: its roster entry when it
|
|
56
|
+
* was spawned, else one made of OpenCode's info of it; none when OpenCode does not know it.
|
|
57
|
+
*/
|
|
58
|
+
export async function readTop(ports, rootID) {
|
|
59
|
+
const info = await ports.session.get({ sessionID: rootID }).catch(() => undefined);
|
|
60
|
+
if (!info)
|
|
61
|
+
return undefined;
|
|
62
|
+
const children = childStore(ports.storage);
|
|
63
|
+
const spawned = await children.indexedEntry(rootID).catch(() => undefined);
|
|
64
|
+
const entry = spawned ?? {
|
|
65
|
+
sessionID: rootID,
|
|
66
|
+
parentID: info.parentID ?? "",
|
|
67
|
+
title: info.title ?? "",
|
|
68
|
+
directory: info.location.directory,
|
|
69
|
+
isolated: false,
|
|
70
|
+
createdAt: info.time.created,
|
|
71
|
+
};
|
|
72
|
+
const looked = await look(ports, { entry, depth: 0 }, info);
|
|
73
|
+
const report = await reportOf(children, rootID).catch(() => undefined);
|
|
74
|
+
return { ...looked, report, live: isLive(looked.running, looked.pending.length + looked.forms.length, report) };
|
|
51
75
|
}
|
|
52
76
|
/**
|
|
53
77
|
* The listing of at most `max` of the sessions under a session: the live ones first, then the newest, with what is
|
package/dist/tree/tui.d.ts
CHANGED
|
@@ -130,6 +130,14 @@ export declare const activityLine: (row: Row, now: number) => string;
|
|
|
130
130
|
export declare function stalledLine(since: number, now: number): string;
|
|
131
131
|
/** The lines a row shows under its title: an orchestrator's bar, a paused or stalled session's wait, or else a live leaf's activity. */
|
|
132
132
|
export declare function detailLines(row: Row, rows: ReadonlyArray<Row>, now: number): string[];
|
|
133
|
+
/**
|
|
134
|
+
* Each row's tree guides, by session, for rows in the order a tree is drawn, below depth `base`: before its mark ("│ ├ "),
|
|
135
|
+
* and before its detail lines, which carry a line down to its first child ("│ │ ").
|
|
136
|
+
*/
|
|
137
|
+
export declare function guides(rows: ReadonlyArray<Row>, base: number): Map<string, {
|
|
138
|
+
row: string;
|
|
139
|
+
detail: string;
|
|
140
|
+
}>;
|
|
133
141
|
/** How often the footer and the panel ask again with no event: the scheduler flags a stalled session without one. */
|
|
134
142
|
export declare const REREAD_MS = 15000;
|
|
135
143
|
/**
|
package/dist/tree/wake.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { millis } from "../platform/json.js";
|
|
1
2
|
import { busy, endsIdle } from "./domain.js";
|
|
2
3
|
import { wakeStore } from "./store.js";
|
|
3
4
|
/** How long a courier message may wait in an idle session's inbox before its wake counts as missed. */
|
|
@@ -86,12 +87,13 @@ export async function wakeMissed(ports, claimed) {
|
|
|
86
87
|
await Promise.all(due.map((wake) => wakeAgain(ports, store, wake, now).finally(() => claimed.delete(claimOf(wake)))));
|
|
87
88
|
}
|
|
88
89
|
const claimOf = (wake) => `wake ${wake.sessionID} ${wake.inboxID}`;
|
|
90
|
+
/** Since when a message has waited in an idle session: it was enqueued, seen waiting as the turn ended, or sent again, whichever is last. */
|
|
91
|
+
const waitingSince = (wake) => Math.max(wake.idleAt ?? wake.at, wake.rewoken ?? 0);
|
|
89
92
|
/** Whether a message has waited in an idle session past `WAKE_MS`, and since a wake again, if one was sent; a stalled one is done. */
|
|
90
93
|
export function missed(wake, now) {
|
|
91
94
|
if (wake.running || wake.stalled !== undefined)
|
|
92
95
|
return false;
|
|
93
|
-
|
|
94
|
-
return now - since >= WAKE_MS;
|
|
96
|
+
return now - waitingSince(wake) >= WAKE_MS;
|
|
95
97
|
}
|
|
96
98
|
async function wakeAgain(ports, store, wake, now) {
|
|
97
99
|
try {
|
|
@@ -100,7 +102,7 @@ async function wakeAgain(ports, store, wake, now) {
|
|
|
100
102
|
if (!current || !missed(current, now))
|
|
101
103
|
return;
|
|
102
104
|
// A shutdown during a step reads as an end, and the turn the next boot resumes may start unseen: that turn takes the message.
|
|
103
|
-
if (await runsNow(ports,
|
|
105
|
+
if (await runsNow(ports, current)) {
|
|
104
106
|
const { idleAt: _, ...running } = current;
|
|
105
107
|
return await store.put({ ...running, running: true });
|
|
106
108
|
}
|
|
@@ -127,10 +129,18 @@ async function wakeAgain(ports, store, wake, now) {
|
|
|
127
129
|
ports.log(`courier wake: could not wake ${wake.sessionID} again for ${wake.inboxID}: ${String(error)}`);
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Whether a session shows a turn running that takes the message: by `time.busy` (2.2.2), an execution started since the message was
|
|
134
|
+
* last left waiting and not ended; without it, by its times and its transcript read now. One that cannot be read does not, and is woken again.
|
|
135
|
+
*/
|
|
136
|
+
async function runsNow(ports, wake) {
|
|
137
|
+
const { sessionID } = wake;
|
|
132
138
|
const info = await ports.session.get({ sessionID }).catch(() => undefined);
|
|
133
|
-
if (!info
|
|
139
|
+
if (!info)
|
|
140
|
+
return false;
|
|
141
|
+
if (info.time.busy !== undefined)
|
|
142
|
+
return busy(info.time) && millis(info.time.busy) >= waitingSince(wake);
|
|
143
|
+
if (!busy(info.time))
|
|
134
144
|
return false;
|
|
135
145
|
const messages = await ports.session.context({ sessionID }).catch(() => []);
|
|
136
146
|
return messages.length > 0 && !endsIdle(messages);
|
package/dist/tui.js
CHANGED
|
@@ -1468,6 +1468,7 @@ var Row = Schema3.Struct({
|
|
|
1468
1468
|
});
|
|
1469
1469
|
var Tree = Schema3.Struct({
|
|
1470
1470
|
rows: Schema3.Array(Row),
|
|
1471
|
+
top: Schema3.optionalKey(Row),
|
|
1471
1472
|
truncated: Schema3.optionalKey(Count3),
|
|
1472
1473
|
stalled: Schema3.optionalKey(Schema3.Number)
|
|
1473
1474
|
});
|
|
@@ -1841,6 +1842,31 @@ function detailLines(row, rows, now) {
|
|
|
1841
1842
|
return [bar(started)];
|
|
1842
1843
|
return row.state === "gone" ? [] : [activityLine(row, now)];
|
|
1843
1844
|
}
|
|
1845
|
+
function guides(rows, base) {
|
|
1846
|
+
const byID = new Map(rows.map((row) => [row.sessionID, row]));
|
|
1847
|
+
const later = new Set;
|
|
1848
|
+
const parents = new Set;
|
|
1849
|
+
rows.forEach((row, index) => {
|
|
1850
|
+
if (rows.slice(index + 1).some((next) => next.parentID === row.parentID))
|
|
1851
|
+
later.add(row.sessionID);
|
|
1852
|
+
parents.add(row.parentID);
|
|
1853
|
+
});
|
|
1854
|
+
const above = (row) => {
|
|
1855
|
+
const parent = row.depth > base + 1 ? byID.get(row.parentID) : undefined;
|
|
1856
|
+
return parent ? [...above(parent), parent] : [];
|
|
1857
|
+
};
|
|
1858
|
+
const line = (row) => later.has(row.sessionID) ? "\u2502 " : " ";
|
|
1859
|
+
const branch = (row) => later.has(row.sessionID) ? "\u251C " : "\u2514 ";
|
|
1860
|
+
return new Map(rows.map((row) => {
|
|
1861
|
+
const through = above(row).map(line).join("");
|
|
1862
|
+
const below = row.depth > base;
|
|
1863
|
+
const down = parents.has(row.sessionID) ? "\u2502 " : " ";
|
|
1864
|
+
return [row.sessionID, {
|
|
1865
|
+
row: through + (below ? branch(row) : ""),
|
|
1866
|
+
detail: through + (below ? line(row) : "") + down
|
|
1867
|
+
}];
|
|
1868
|
+
}));
|
|
1869
|
+
}
|
|
1844
1870
|
function courierOf(ctx) {
|
|
1845
1871
|
const courier = ctx.client.rpc(treeRpc);
|
|
1846
1872
|
const location = (sessionID) => ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default();
|
|
@@ -2084,6 +2110,11 @@ function TreePanel(props) {
|
|
|
2084
2110
|
tree: view.tree
|
|
2085
2111
|
};
|
|
2086
2112
|
const rows = () => latest()?.tree?.rows ?? [];
|
|
2113
|
+
const top = () => latest()?.tree?.top;
|
|
2114
|
+
const listed = () => {
|
|
2115
|
+
const own = top();
|
|
2116
|
+
return own ? [own, ...rows()] : rows();
|
|
2117
|
+
};
|
|
2087
2118
|
createEffect(() => {
|
|
2088
2119
|
const tree = latest()?.tree;
|
|
2089
2120
|
if (tree)
|
|
@@ -2115,7 +2146,7 @@ function TreePanel(props) {
|
|
|
2115
2146
|
const [selectedID, setSelectedID] = createSignal();
|
|
2116
2147
|
let placed = false;
|
|
2117
2148
|
createEffect(() => {
|
|
2118
|
-
const found = placed ? undefined :
|
|
2149
|
+
const found = placed ? undefined : listed().find((row) => row.sessionID === input.sessionID);
|
|
2119
2150
|
if (!found)
|
|
2120
2151
|
return;
|
|
2121
2152
|
placed = true;
|
|
@@ -2124,24 +2155,24 @@ function TreePanel(props) {
|
|
|
2124
2155
|
let at = 0;
|
|
2125
2156
|
const selected = createMemo(() => {
|
|
2126
2157
|
const id = selectedID();
|
|
2127
|
-
const found =
|
|
2128
|
-
const index = found >= 0 ? found : Math.min(at,
|
|
2158
|
+
const found = listed().findIndex((row) => row.sessionID === id);
|
|
2159
|
+
const index = found >= 0 ? found : Math.min(at, listed().length - 1);
|
|
2129
2160
|
at = Math.max(index, 0);
|
|
2130
2161
|
return index;
|
|
2131
2162
|
});
|
|
2132
2163
|
createEffect(() => {
|
|
2133
|
-
const row =
|
|
2164
|
+
const row = listed()[selected()];
|
|
2134
2165
|
if (row && row.sessionID !== selectedID())
|
|
2135
2166
|
setSelectedID(row.sessionID);
|
|
2136
2167
|
});
|
|
2137
|
-
const current = () =>
|
|
2168
|
+
const current = () => listed()[selected()];
|
|
2138
2169
|
let scroll;
|
|
2139
2170
|
createEffect(() => {
|
|
2140
2171
|
if (selected() >= 0)
|
|
2141
2172
|
scroll?.scrollChildIntoView(`courier-row-${selected()}`);
|
|
2142
2173
|
});
|
|
2143
2174
|
const move = (by) => {
|
|
2144
|
-
const next =
|
|
2175
|
+
const next = listed()[Math.max(0, Math.min(listed().length - 1, selected() + by))];
|
|
2145
2176
|
if (next)
|
|
2146
2177
|
setSelectedID(next.sessionID);
|
|
2147
2178
|
};
|
|
@@ -2157,7 +2188,7 @@ function TreePanel(props) {
|
|
|
2157
2188
|
const [hovered, setHovered] = createSignal();
|
|
2158
2189
|
createEffect(() => {
|
|
2159
2190
|
const id = hovered();
|
|
2160
|
-
if (id !== undefined && !
|
|
2191
|
+
if (id !== undefined && !listed().some((row) => row.sessionID === id))
|
|
2161
2192
|
setHovered(undefined);
|
|
2162
2193
|
});
|
|
2163
2194
|
let pressed;
|
|
@@ -2165,9 +2196,19 @@ function TreePanel(props) {
|
|
|
2165
2196
|
setSelectedID(row.sessionID);
|
|
2166
2197
|
openRow(row);
|
|
2167
2198
|
};
|
|
2199
|
+
const ownRow = (row, key) => {
|
|
2200
|
+
if (row.sessionID !== view.root)
|
|
2201
|
+
return false;
|
|
2202
|
+
ctx.ui.toast.show({
|
|
2203
|
+
title: "courier",
|
|
2204
|
+
message: `${key} acts on a session under "${oneLine3(row.title)}", the panel's own.`,
|
|
2205
|
+
variant: "warning"
|
|
2206
|
+
});
|
|
2207
|
+
return true;
|
|
2208
|
+
};
|
|
2168
2209
|
const stopSelected = () => {
|
|
2169
2210
|
const row = current();
|
|
2170
|
-
if (row)
|
|
2211
|
+
if (row && !ownRow(row, "s"))
|
|
2171
2212
|
confirmStop(ctx, courier, location, view.root, {
|
|
2172
2213
|
sessionID: row.sessionID,
|
|
2173
2214
|
title: row.title,
|
|
@@ -2176,7 +2217,7 @@ function TreePanel(props) {
|
|
|
2176
2217
|
};
|
|
2177
2218
|
const forgetSelected = () => {
|
|
2178
2219
|
const row = current();
|
|
2179
|
-
if (!row)
|
|
2220
|
+
if (!row || ownRow(row, "x"))
|
|
2180
2221
|
return;
|
|
2181
2222
|
if (row.state !== "gone")
|
|
2182
2223
|
return ctx.ui.toast.show({
|
|
@@ -2261,7 +2302,9 @@ function TreePanel(props) {
|
|
|
2261
2302
|
const glyphShown = () => height() >= GLYPH_ROWS && size().width >= GLYPH_COLUMNS;
|
|
2262
2303
|
const artShown = () => empty() && height() >= ART_ROWS;
|
|
2263
2304
|
const [listHeight, setListHeight] = createSignal(0);
|
|
2264
|
-
const
|
|
2305
|
+
const lines = (row) => detailLines(row, row.depth ? rows() : [], now());
|
|
2306
|
+
const guided = createMemo(() => guides(listed(), top() ? 0 : 1));
|
|
2307
|
+
const treeLines = () => listed().reduce((count, row) => count + 1 + lines(row).length, 0);
|
|
2265
2308
|
const belowShown = () => rows().length > 0 && listHeight() - treeLines() >= BELOW_ROWS;
|
|
2266
2309
|
const headerGlyph = () => glyphShown() && !artShown() && !belowShown();
|
|
2267
2310
|
const mascot = createMemo(() => mascotState(rows(), {
|
|
@@ -2317,9 +2360,9 @@ function TreePanel(props) {
|
|
|
2317
2360
|
return theme.text.muted;
|
|
2318
2361
|
};
|
|
2319
2362
|
return (() => {
|
|
2320
|
-
var _el$6 = _$createElement("box"), _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$
|
|
2363
|
+
var _el$6 = _$createElement("box"), _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$17 = _$createElement("scrollbox"), _el$22 = _$createElement("text");
|
|
2321
2364
|
_$insertNode(_el$6, _el$7);
|
|
2322
|
-
_$insertNode(_el$6, _el$
|
|
2365
|
+
_$insertNode(_el$6, _el$17);
|
|
2323
2366
|
_$insertNode(_el$6, _el$22);
|
|
2324
2367
|
_$setProp(_el$6, "flexDirection", "column");
|
|
2325
2368
|
_$setProp(_el$6, "flexGrow", 1);
|
|
@@ -2394,7 +2437,7 @@ function TreePanel(props) {
|
|
|
2394
2437
|
_$effect((_$p) => _$setProp(_el$12, "fg", theme.text.muted, _$p));
|
|
2395
2438
|
return _el$11;
|
|
2396
2439
|
}
|
|
2397
|
-
}), _el$
|
|
2440
|
+
}), _el$17);
|
|
2398
2441
|
_$insert(_el$6, _$createComponent(Show, {
|
|
2399
2442
|
get when() {
|
|
2400
2443
|
return latest()?.error;
|
|
@@ -2406,10 +2449,10 @@ function TreePanel(props) {
|
|
|
2406
2449
|
_$effect((_$p) => _$setProp(_el$13, "fg", theme.text.feedback.error.base, _$p));
|
|
2407
2450
|
return _el$13;
|
|
2408
2451
|
}
|
|
2409
|
-
}), _el$
|
|
2452
|
+
}), _el$17);
|
|
2410
2453
|
_$insert(_el$6, _$createComponent(Show, {
|
|
2411
2454
|
get when() {
|
|
2412
|
-
return latest()?.tree?.stalled;
|
|
2455
|
+
return _$memo(() => !!!top())() && latest()?.tree?.stalled;
|
|
2413
2456
|
},
|
|
2414
2457
|
get children() {
|
|
2415
2458
|
var _el$15 = _$createElement("text"), _el$16 = _$createTextNode(`This session is stalled: `);
|
|
@@ -2418,49 +2461,18 @@ function TreePanel(props) {
|
|
|
2418
2461
|
_$effect((_$p) => _$setProp(_el$15, "fg", theme.text.feedback.error.base, _$p));
|
|
2419
2462
|
return _el$15;
|
|
2420
2463
|
}
|
|
2421
|
-
}), _el$
|
|
2422
|
-
_$
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
},
|
|
2426
|
-
get children() {
|
|
2427
|
-
return [(() => {
|
|
2428
|
-
var _el$17 = _$createElement("text");
|
|
2429
|
-
_$insertNode(_el$17, _$createTextNode(`No session started with courier_spawn under this one.`));
|
|
2430
|
-
_$effect((_$p) => _$setProp(_el$17, "fg", theme.text.muted, _$p));
|
|
2431
|
-
return _el$17;
|
|
2432
|
-
})(), _$createComponent(Show, {
|
|
2433
|
-
get when() {
|
|
2434
|
-
return artShown();
|
|
2435
|
-
},
|
|
2436
|
-
get children() {
|
|
2437
|
-
var _el$19 = _$createElement("box");
|
|
2438
|
-
_$setProp(_el$19, "flexDirection", "column");
|
|
2439
|
-
_$setProp(_el$19, "flexShrink", 0);
|
|
2440
|
-
_$setProp(_el$19, "paddingTop", 1);
|
|
2441
|
-
_$insert(_el$19, _$createComponent(Index, {
|
|
2442
|
-
get each() {
|
|
2443
|
-
return art2();
|
|
2444
|
-
},
|
|
2445
|
-
children: (line) => drawn(line)
|
|
2446
|
-
}));
|
|
2447
|
-
return _el$19;
|
|
2448
|
-
}
|
|
2449
|
-
})];
|
|
2450
|
-
}
|
|
2451
|
-
}), _el$20);
|
|
2452
|
-
_$use((value) => scroll = value, _el$20);
|
|
2453
|
-
_$setProp(_el$20, "flexGrow", 1);
|
|
2454
|
-
_$setProp(_el$20, "onSizeChange", function() {
|
|
2464
|
+
}), _el$17);
|
|
2465
|
+
_$use((value) => scroll = value, _el$17);
|
|
2466
|
+
_$setProp(_el$17, "flexGrow", 1);
|
|
2467
|
+
_$setProp(_el$17, "onSizeChange", function() {
|
|
2455
2468
|
setListHeight(this.height);
|
|
2456
2469
|
});
|
|
2457
|
-
_$insert(_el$
|
|
2470
|
+
_$insert(_el$17, _$createComponent(For, {
|
|
2458
2471
|
get each() {
|
|
2459
|
-
return
|
|
2472
|
+
return listed();
|
|
2460
2473
|
},
|
|
2461
2474
|
children: (row, index) => {
|
|
2462
2475
|
const chosen = () => index() === selected();
|
|
2463
|
-
const indent = " ".repeat(row.depth - 1);
|
|
2464
2476
|
return (() => {
|
|
2465
2477
|
var _el$24 = _$createElement("box"), _el$25 = _$createElement("box"), _el$26 = _$createElement("text"), _el$27 = _$createElement("text"), _el$28 = _$createTextNode(` `), _el$29 = _$createElement("text"), _el$30 = _$createElement("text");
|
|
2466
2478
|
_$insertNode(_el$24, _el$25);
|
|
@@ -2486,8 +2498,20 @@ function TreePanel(props) {
|
|
|
2486
2498
|
_$setProp(_el$25, "flexShrink", 0);
|
|
2487
2499
|
_$setProp(_el$26, "wrapMode", "none");
|
|
2488
2500
|
_$setProp(_el$26, "flexShrink", 0);
|
|
2489
|
-
_$insert(_el$26,
|
|
2490
|
-
_$insert(_el$
|
|
2501
|
+
_$insert(_el$26, () => chosen() ? "\u25B8 " : " ");
|
|
2502
|
+
_$insert(_el$25, _$createComponent(Show, {
|
|
2503
|
+
get when() {
|
|
2504
|
+
return guided().get(row.sessionID)?.row;
|
|
2505
|
+
},
|
|
2506
|
+
children: (guide) => (() => {
|
|
2507
|
+
var _el$32 = _$createElement("text");
|
|
2508
|
+
_$setProp(_el$32, "wrapMode", "none");
|
|
2509
|
+
_$setProp(_el$32, "flexShrink", 0);
|
|
2510
|
+
_$insert(_el$32, guide);
|
|
2511
|
+
_$effect((_$p) => _$setProp(_el$32, "fg", theme.text.muted, _$p));
|
|
2512
|
+
return _el$32;
|
|
2513
|
+
})()
|
|
2514
|
+
}), _el$27);
|
|
2491
2515
|
_$insertNode(_el$27, _el$28);
|
|
2492
2516
|
_$setProp(_el$27, "wrapMode", "none");
|
|
2493
2517
|
_$setProp(_el$27, "flexShrink", 0);
|
|
@@ -2509,32 +2533,32 @@ function TreePanel(props) {
|
|
|
2509
2533
|
return at();
|
|
2510
2534
|
},
|
|
2511
2535
|
get children() {
|
|
2512
|
-
var _el$
|
|
2513
|
-
_$insertNode(_el$
|
|
2514
|
-
_$setProp(_el$
|
|
2515
|
-
_$setProp(_el$
|
|
2516
|
-
_$effect((_$p) => _$setProp(_el$
|
|
2517
|
-
return _el$
|
|
2536
|
+
var _el$33 = _$createElement("text");
|
|
2537
|
+
_$insertNode(_el$33, _$createTextNode(` \xB7 `));
|
|
2538
|
+
_$setProp(_el$33, "wrapMode", "none");
|
|
2539
|
+
_$setProp(_el$33, "flexShrink", 0);
|
|
2540
|
+
_$effect((_$p) => _$setProp(_el$33, "fg", theme.text.muted, _$p));
|
|
2541
|
+
return _el$33;
|
|
2518
2542
|
}
|
|
2519
2543
|
}), (() => {
|
|
2520
|
-
var _el$
|
|
2521
|
-
_$setProp(_el$
|
|
2522
|
-
_$setProp(_el$
|
|
2523
|
-
_$insert(_el$
|
|
2524
|
-
_$effect((_$p) => _$setProp(_el$
|
|
2525
|
-
return _el$
|
|
2544
|
+
var _el$35 = _$createElement("text");
|
|
2545
|
+
_$setProp(_el$35, "wrapMode", "none");
|
|
2546
|
+
_$setProp(_el$35, "flexShrink", 0);
|
|
2547
|
+
_$insert(_el$35, () => part.text);
|
|
2548
|
+
_$effect((_$p) => _$setProp(_el$35, "fg", partColor(row, part.kind), _$p));
|
|
2549
|
+
return _el$35;
|
|
2526
2550
|
})()]
|
|
2527
2551
|
}), null);
|
|
2528
2552
|
_$insert(_el$24, _$createComponent(For, {
|
|
2529
2553
|
get each() {
|
|
2530
|
-
return
|
|
2554
|
+
return lines(row);
|
|
2531
2555
|
},
|
|
2532
2556
|
children: (line) => (() => {
|
|
2533
|
-
var _el$
|
|
2534
|
-
_$insertNode(_el$
|
|
2535
|
-
_$setProp(_el$
|
|
2536
|
-
_$insert(_el$
|
|
2537
|
-
_$insert(_el$
|
|
2557
|
+
var _el$36 = _$createElement("text"), _el$37 = _$createTextNode(` `);
|
|
2558
|
+
_$insertNode(_el$36, _el$37);
|
|
2559
|
+
_$setProp(_el$36, "wrapMode", "none");
|
|
2560
|
+
_$insert(_el$36, () => guided().get(row.sessionID)?.detail ?? " ", null);
|
|
2561
|
+
_$insert(_el$36, _$createComponent(Show, {
|
|
2538
2562
|
get when() {
|
|
2539
2563
|
return _$memo(() => row.context !== undefined)() && Math.round(row.context * 100) > CONTEXT_WARN * 100;
|
|
2540
2564
|
},
|
|
@@ -2545,18 +2569,18 @@ function TreePanel(props) {
|
|
|
2545
2569
|
return line.split(" \xB7 ");
|
|
2546
2570
|
},
|
|
2547
2571
|
children: (part, at) => [_$memo(() => at() ? " \xB7 " : ""), _$memo(() => _$memo(() => part === `ctx ${Math.round(row.context * 100)}%`)() ? (() => {
|
|
2548
|
-
var _el$
|
|
2549
|
-
_$insert(_el$
|
|
2550
|
-
_$effect((_$p) => _$setProp(_el$
|
|
2572
|
+
var _el$38 = _$createElement("span");
|
|
2573
|
+
_$insert(_el$38, part);
|
|
2574
|
+
_$effect((_$p) => _$setProp(_el$38, "style", {
|
|
2551
2575
|
fg: theme.text.feedback.warning.base
|
|
2552
2576
|
}, _$p));
|
|
2553
|
-
return _el$
|
|
2577
|
+
return _el$38;
|
|
2554
2578
|
})() : part)]
|
|
2555
2579
|
});
|
|
2556
2580
|
}
|
|
2557
2581
|
}), null);
|
|
2558
|
-
_$effect((_$p) => _$setProp(_el$
|
|
2559
|
-
return _el$
|
|
2582
|
+
_$effect((_$p) => _$setProp(_el$36, "fg", theme.text.muted, _$p));
|
|
2583
|
+
return _el$36;
|
|
2560
2584
|
})()
|
|
2561
2585
|
}), null);
|
|
2562
2586
|
_$effect((_p$) => {
|
|
@@ -2580,7 +2604,38 @@ function TreePanel(props) {
|
|
|
2580
2604
|
})();
|
|
2581
2605
|
}
|
|
2582
2606
|
}), null);
|
|
2583
|
-
_$insert(_el$
|
|
2607
|
+
_$insert(_el$17, _$createComponent(Show, {
|
|
2608
|
+
get when() {
|
|
2609
|
+
return _$memo(() => !!latest()?.tree)() && !rows().length;
|
|
2610
|
+
},
|
|
2611
|
+
get children() {
|
|
2612
|
+
return [(() => {
|
|
2613
|
+
var _el$18 = _$createElement("text"), _el$19 = _$createTextNode(`No session started with courier_spawn under this one.`);
|
|
2614
|
+
_$insertNode(_el$18, _el$19);
|
|
2615
|
+
_$insert(_el$18, () => top() ? " " : "", _el$19);
|
|
2616
|
+
_$effect((_$p) => _$setProp(_el$18, "fg", theme.text.muted, _$p));
|
|
2617
|
+
return _el$18;
|
|
2618
|
+
})(), _$createComponent(Show, {
|
|
2619
|
+
get when() {
|
|
2620
|
+
return artShown();
|
|
2621
|
+
},
|
|
2622
|
+
get children() {
|
|
2623
|
+
var _el$20 = _$createElement("box");
|
|
2624
|
+
_$setProp(_el$20, "flexDirection", "column");
|
|
2625
|
+
_$setProp(_el$20, "flexShrink", 0);
|
|
2626
|
+
_$setProp(_el$20, "paddingTop", 1);
|
|
2627
|
+
_$insert(_el$20, _$createComponent(Index, {
|
|
2628
|
+
get each() {
|
|
2629
|
+
return art2();
|
|
2630
|
+
},
|
|
2631
|
+
children: (line) => drawn(line)
|
|
2632
|
+
}));
|
|
2633
|
+
return _el$20;
|
|
2634
|
+
}
|
|
2635
|
+
})];
|
|
2636
|
+
}
|
|
2637
|
+
}), null);
|
|
2638
|
+
_$insert(_el$17, _$createComponent(Show, {
|
|
2584
2639
|
get when() {
|
|
2585
2640
|
return belowShown();
|
|
2586
2641
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-courier",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ivo Pogace <71701666+ivopogace@users.noreply.github.com>",
|
|
@@ -56,17 +56,17 @@
|
|
|
56
56
|
"test:e2e": "e2e/run.sh"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@opencode/plugin": "2.2.
|
|
60
|
-
"effect": "4.0.
|
|
59
|
+
"@opencode/plugin": "2.2.2",
|
|
60
|
+
"effect": "4.0.6"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@opencode/plugin": "2.2.
|
|
64
|
-
"@opencode/theme": "2.2.
|
|
63
|
+
"@opencode/plugin": "2.2.2",
|
|
64
|
+
"@opencode/theme": "2.2.2",
|
|
65
65
|
"@opentui/core": "0.7.0",
|
|
66
66
|
"@opentui/solid": "0.7.0",
|
|
67
67
|
"@types/bun": "1.4.2",
|
|
68
68
|
"@types/node": "26.6.4",
|
|
69
|
-
"effect": "4.0.
|
|
69
|
+
"effect": "4.0.6",
|
|
70
70
|
"solid-js": "1.9.17",
|
|
71
71
|
"typescript": "5.9.3"
|
|
72
72
|
}
|