canopy-ui 0.17.0 → 0.18.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/package.json +1 -1
- package/src/chat/protocol.ts +9 -2
- package/src/chat/turnStatus.test.ts +19 -0
- package/src/chat/turnStatus.ts +25 -0
package/package.json
CHANGED
package/src/chat/protocol.ts
CHANGED
|
@@ -222,13 +222,20 @@ export interface TurnStatus {
|
|
|
222
222
|
/** The agent is sitting on a dialog. A separate axis from `state`: a turn
|
|
223
223
|
* blocked on a question is still RUNNING as far as the turn is concerned. */
|
|
224
224
|
menu_pending: boolean;
|
|
225
|
-
/** Why a `failed` turn failed, in the runner's words. Null for every other
|
|
226
|
-
* state. Absent on an older server. */
|
|
227
225
|
/** For `queued_behind`: what it waits behind — `session` (the previous
|
|
228
226
|
* message in this conversation) or `agent` (another turn of the same
|
|
229
227
|
* agent). Absent on an older server. */
|
|
230
228
|
behind?: "session" | "agent" | null;
|
|
229
|
+
/** Why a `failed` turn failed, in the runner's words; or why a `cancelled`
|
|
230
|
+
* one was never run (an admission rule rejected it). Null otherwise, and
|
|
231
|
+
* for a turn someone stopped. Absent on an older server. */
|
|
231
232
|
detail?: string | null;
|
|
233
|
+
/** For `waiting_runner`: its runners are PAUSED by someone, not asleep.
|
|
234
|
+
* Absent on an older server. */
|
|
235
|
+
parked?: boolean;
|
|
236
|
+
/** For a `parked` turn: when the first of its runners resumes on its own
|
|
237
|
+
* (ISO). Null = only a person resuming one will run it. */
|
|
238
|
+
held_until?: string | null;
|
|
232
239
|
/** When a settled turn finished — lets the kit drop a failure the
|
|
233
240
|
* conversation has since moved past. Absent on an older server. */
|
|
234
241
|
finished_at?: string | null;
|
|
@@ -113,6 +113,25 @@ describe("turnNotice", () => {
|
|
|
113
113
|
expect(n?.text).toContain("jj-mbp is offline");
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
it("says a paused box is paused, and until when it holds the turn", () => {
|
|
117
|
+
const held = turnNotice(status({
|
|
118
|
+
state: "waiting_runner", stuck: true, claimed_by: null, parked: true,
|
|
119
|
+
held_until: "2026-10-12T21:00:00Z",
|
|
120
|
+
}));
|
|
121
|
+
expect(held?.text).toContain("jj-mbp is paused until");
|
|
122
|
+
expect(held?.text).toContain("It runs then.");
|
|
123
|
+
const open = turnNotice(status({ state: "waiting_runner", stuck: true, claimed_by: null, parked: true }));
|
|
124
|
+
expect(open?.text).toContain("runs only when someone resumes it");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("says why a rejected turn was never run, and nothing for one somebody stopped", () => {
|
|
128
|
+
const rejected = turnNotice(status({
|
|
129
|
+
state: "cancelled", settled: true, detail: "jj-mbp was paused by Jonathan Jackson",
|
|
130
|
+
}));
|
|
131
|
+
expect(rejected).toMatchObject({ tone: "error", text: "Not run — jj-mbp was paused by Jonathan Jackson" });
|
|
132
|
+
expect(turnNotice(status({ state: "cancelled", settled: true, detail: null }))).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
116
135
|
it("reads differently for the state waiting cannot fix", () => {
|
|
117
136
|
const n = turnNotice(status({ state: "unrouted", stuck: true, runners: [], claimed_by: null }));
|
|
118
137
|
// Not a warning you can wait out — an error somebody has to go and fix.
|
package/src/chat/turnStatus.ts
CHANGED
|
@@ -108,6 +108,14 @@ export interface TurnNotice {
|
|
|
108
108
|
* straight into the session makes messages but no Turn, so without this a
|
|
109
109
|
* day-old failure sat in red under a thread that was plainly working.
|
|
110
110
|
*/
|
|
111
|
+
/** A resume time as the reader's local "Oct 12, 3:00 PM". */
|
|
112
|
+
function formatWhen(iso: string): string {
|
|
113
|
+
const d = new Date(iso);
|
|
114
|
+
return Number.isNaN(d.getTime())
|
|
115
|
+
? iso
|
|
116
|
+
: d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
export function turnNotice(
|
|
112
120
|
status?: TurnStatus | null,
|
|
113
121
|
{ latestMessageAt }: { latestMessageAt?: string | null } = {},
|
|
@@ -124,6 +132,17 @@ export function turnNotice(
|
|
|
124
132
|
const need = status.requires?.length ? status.requires.map((r) => r.toUpperCase()).join(", ") : "";
|
|
125
133
|
switch (status.state) {
|
|
126
134
|
case "waiting_runner":
|
|
135
|
+
if (status.parked) {
|
|
136
|
+
return {
|
|
137
|
+
tone: "warn",
|
|
138
|
+
// Paused is somebody's decision, not a laptop lid: say whether it ends
|
|
139
|
+
// on its own.
|
|
140
|
+
text: status.held_until
|
|
141
|
+
? `Queued — ${runners || "its runner"} is paused until ${formatWhen(status.held_until)}. It runs then.`
|
|
142
|
+
: `Queued — ${runners || "its runner"} is paused, so this runs only when someone resumes it.`,
|
|
143
|
+
offer,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
127
146
|
return {
|
|
128
147
|
tone: "warn",
|
|
129
148
|
// Names the box, because the fix is physical: go and open it.
|
|
@@ -170,6 +189,12 @@ export function turnNotice(
|
|
|
170
189
|
text: status.detail ? `Could not finish — ${status.detail}` : "Could not finish.",
|
|
171
190
|
offer: null,
|
|
172
191
|
};
|
|
192
|
+
case "cancelled":
|
|
193
|
+
// Only a REJECTED turn carries a detail (an admission rule closed it before
|
|
194
|
+
// it ran); one somebody stopped needs no sentence.
|
|
195
|
+
return status.detail
|
|
196
|
+
? { tone: "error", text: `Not run — ${status.detail}`, offer: null }
|
|
197
|
+
: null;
|
|
173
198
|
case "missed":
|
|
174
199
|
return { tone: "warn", text: "Missed — nothing picked it up in time.", offer: null };
|
|
175
200
|
default:
|