canopy-ui 0.8.0 → 0.10.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/ChatPanel.pending.test.tsx +79 -1
- package/src/chat/ChatPanel.tsx +42 -13
- package/src/chat/agui.fixture.json +425 -5
- package/src/chat/agui.test.ts +5 -1
- package/src/chat/agui.ts +13 -0
- package/src/chat/index.ts +12 -0
- package/src/chat/protocol.ts +58 -0
- package/src/chat/sessionReducer.ts +12 -0
- package/src/chat/turnStatus.test.ts +139 -0
- package/src/chat/turnStatus.ts +148 -0
- package/src/chat/useSessionSocket.protocol.test.tsx +124 -0
- package/src/chat/useSessionSocket.ts +79 -1
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
15
15
|
import { cleanup, render, screen } from "@testing-library/react";
|
|
16
16
|
|
|
17
17
|
import { ChatPanel } from "./ChatPanel";
|
|
18
|
-
import type { Message, SessionState } from "./protocol";
|
|
18
|
+
import type { Message, SessionState, TurnStatus } from "./protocol";
|
|
19
19
|
|
|
20
20
|
afterEach(cleanup);
|
|
21
21
|
|
|
@@ -134,3 +134,81 @@ describe("the pending-reply row", () => {
|
|
|
134
134
|
expect(screen.queryByText("no messages yet")).toBeNull();
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
function status(over: Partial<TurnStatus> = {}): TurnStatus {
|
|
139
|
+
return {
|
|
140
|
+
state: "working",
|
|
141
|
+
agent_slug: "hal",
|
|
142
|
+
runners: ["jj-mbp"],
|
|
143
|
+
claimed_by: "jj-mbp",
|
|
144
|
+
pinned: false,
|
|
145
|
+
cloud_runner: null,
|
|
146
|
+
cloud_runner_id: null,
|
|
147
|
+
last_seen_at: null,
|
|
148
|
+
menu_pending: false,
|
|
149
|
+
settled: false,
|
|
150
|
+
stuck: false,
|
|
151
|
+
...over,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe("the server's turn status", () => {
|
|
156
|
+
it("replaces the spinner with a reason when nothing will pick the turn up", () => {
|
|
157
|
+
// The regression the whole projection exists for. A client cannot know
|
|
158
|
+
// this: it pressed send, and from here an offline runner and a slow one
|
|
159
|
+
// are identical.
|
|
160
|
+
panel({
|
|
161
|
+
state: state({
|
|
162
|
+
turn_status: status({ state: "waiting_runner", stuck: true, claimed_by: null }),
|
|
163
|
+
}),
|
|
164
|
+
awaitingReply: true,
|
|
165
|
+
});
|
|
166
|
+
expect(screen.queryByTestId("pending-reply")).toBeNull();
|
|
167
|
+
expect(screen.getByRole("status").textContent).toContain("jj-mbp is offline");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("reads differently when waiting cannot possibly help", () => {
|
|
171
|
+
panel({
|
|
172
|
+
state: state({
|
|
173
|
+
turn_status: status({ state: "unrouted", stuck: true, runners: [], claimed_by: null }),
|
|
174
|
+
}),
|
|
175
|
+
awaitingReply: true,
|
|
176
|
+
});
|
|
177
|
+
expect(screen.getByRole("status").textContent).toContain("no runner is set up to run hal");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("names the box in the spinner once a runner really has it", () => {
|
|
181
|
+
panel({ state: state({ turn_status: status({ state: "working" }) }) });
|
|
182
|
+
expect(screen.getByText("Thinking on jj-mbp…")).not.toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("clears a stuck spinner even if the stream frame that ends it never arrived", () => {
|
|
186
|
+
// `awaitingReply` only clears on a stream frame; a dropped socket used to
|
|
187
|
+
// leave it spinning forever. The server's settled flag is the backstop.
|
|
188
|
+
panel({
|
|
189
|
+
state: state({ turn_status: status({ state: "done", settled: true }) }),
|
|
190
|
+
awaitingReply: true,
|
|
191
|
+
});
|
|
192
|
+
expect(screen.queryByTestId("pending-reply")).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("stays silent when the host has its own, richer banner for the same fact", () => {
|
|
196
|
+
// canopy's chat page raises <PlacementBanner> here, with buttons to wait
|
|
197
|
+
// or continue elsewhere. Our sentence underneath would restate it as a
|
|
198
|
+
// dead end directly below the thing you can actually press.
|
|
199
|
+
panel({
|
|
200
|
+
state: state({
|
|
201
|
+
turn_status: status({ state: "waiting_runner", stuck: true, claimed_by: null }),
|
|
202
|
+
}),
|
|
203
|
+
banner: <div>continue on another runner?</div>,
|
|
204
|
+
});
|
|
205
|
+
expect(screen.queryByRole("status")).toBeNull();
|
|
206
|
+
expect(screen.getByText("continue on another runner?")).not.toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("leaves a blocked agent entirely to the menu", () => {
|
|
210
|
+
panel({ state: state({ turn_status: status({ state: "blocked", stuck: true }) }) });
|
|
211
|
+
expect(screen.queryByRole("status")).toBeNull();
|
|
212
|
+
expect(screen.queryByTestId("pending-reply")).toBeNull();
|
|
213
|
+
});
|
|
214
|
+
});
|
package/src/chat/ChatPanel.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { MessageList } from "./MessageList";
|
|
|
7
7
|
import { PresenceChips } from "./PresenceChips";
|
|
8
8
|
import { SendBox, type PendingAttachment } from "./SendBox";
|
|
9
9
|
import { isDraftIdle, msUntilDraftIdle, type DraftStorage } from "./drafts";
|
|
10
|
+
import { agentHasFloor as computeAgentHasFloor, pendingLabel as computePendingLabel, turnNotice } from "./turnStatus";
|
|
10
11
|
import { useStickyBottom } from "./useStickyBottom";
|
|
11
12
|
|
|
12
13
|
export interface ChatPanelProps {
|
|
@@ -119,19 +120,31 @@ export function ChatPanel({
|
|
|
119
120
|
// the real runner path, and the only signal was a 12px chip in the header,
|
|
120
121
|
// which on a phone is the far corner of the screen from your thumb.
|
|
121
122
|
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
const agentHasFloor =
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
// Three sources now, in `turnStatus.ts` — see `agentHasFloor` there for the
|
|
124
|
+
// order of authority. The server's `turn_status` wins when present because
|
|
125
|
+
// it is the only one that can say a turn is queued behind an offline box or
|
|
126
|
+
// routed nowhere at all; `awaitingReply` still covers the instant between
|
|
127
|
+
// pressing send and the first frame, which nothing server-side can.
|
|
128
|
+
const status = state.turn_status ?? null;
|
|
129
|
+
const agentHasFloor = computeAgentHasFloor({
|
|
130
|
+
status,
|
|
131
|
+
activity: state.activity,
|
|
132
|
+
awaitingReply,
|
|
133
|
+
});
|
|
131
134
|
const showPendingReply = inFlightMessage == null && agentHasFloor;
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
+
const pendingLabel = computePendingLabel({ status, activity: state.activity });
|
|
136
|
+
// The sentence for an ask that is going NOWHERE. Rendered instead of the
|
|
137
|
+
// bubble, never beside it: a spinner over a turn nothing will pick up is the
|
|
138
|
+
// precise lie this projection exists to remove.
|
|
139
|
+
//
|
|
140
|
+
// Suppressed entirely when the host supplies its own `banner`. A host banner
|
|
141
|
+
// IS this host's richer answer for the same situation — canopy's chat page
|
|
142
|
+
// raises <PlacementBanner> for an offline runner, with buttons to wait or
|
|
143
|
+
// continue elsewhere, and <MenuPrompt> for a dialog. Rendering our sentence
|
|
144
|
+
// underneath would state the same fact twice and put a dead-end restatement
|
|
145
|
+
// directly below the thing you can actually press. The fallback is for hosts
|
|
146
|
+
// with no banner of their own, which is exactly the embedded widget.
|
|
147
|
+
const notice = banner ? null : turnNotice(status);
|
|
135
148
|
|
|
136
149
|
// Sticky-bottom scroll: dep changes on (a) new message arrival and (b)
|
|
137
150
|
// streaming text growth on the last message. length-only (cheap) instead
|
|
@@ -188,7 +201,23 @@ export function ChatPanel({
|
|
|
188
201
|
onStop={onStop}
|
|
189
202
|
stopState={state.stopState}
|
|
190
203
|
onTakeOver={onTakeOver}
|
|
191
|
-
banner={
|
|
204
|
+
banner={
|
|
205
|
+
notice ? (
|
|
206
|
+
<p
|
|
207
|
+
role="status"
|
|
208
|
+
className={
|
|
209
|
+
notice.tone === "error"
|
|
210
|
+
? "text-[12px] text-destructive"
|
|
211
|
+
: "text-[12px] text-warning"
|
|
212
|
+
}
|
|
213
|
+
>
|
|
214
|
+
{notice.text}
|
|
215
|
+
{notice.offer ? ` A runner admin can send it to ${notice.offer.name}.` : ""}
|
|
216
|
+
</p>
|
|
217
|
+
) : (
|
|
218
|
+
banner
|
|
219
|
+
)
|
|
220
|
+
}
|
|
192
221
|
disabledReason={disabledReason}
|
|
193
222
|
attachments={attachments}
|
|
194
223
|
onAttach={onAttach}
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"agui": [
|
|
11
11
|
{
|
|
12
|
+
"type": "TEXT_MESSAGE_START",
|
|
12
13
|
"metadata": {
|
|
13
14
|
"canopy": {
|
|
14
15
|
"turn_index": 4
|
|
15
16
|
}
|
|
16
17
|
},
|
|
17
|
-
"type": "TEXT_MESSAGE_START",
|
|
18
18
|
"messageId": "m1",
|
|
19
19
|
"role": "assistant"
|
|
20
20
|
}
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"agui": [
|
|
48
48
|
{
|
|
49
|
+
"type": "TEXT_MESSAGE_END",
|
|
49
50
|
"metadata": {
|
|
50
51
|
"canopy": {
|
|
51
52
|
"plaintext": "hello"
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
|
-
"type": "TEXT_MESSAGE_END",
|
|
55
55
|
"messageId": "m1"
|
|
56
56
|
}
|
|
57
57
|
]
|
|
@@ -67,12 +67,12 @@
|
|
|
67
67
|
},
|
|
68
68
|
"agui": [
|
|
69
69
|
{
|
|
70
|
+
"type": "TEXT_MESSAGE_CHUNK",
|
|
70
71
|
"metadata": {
|
|
71
72
|
"canopy": {
|
|
72
73
|
"turn_index": 3
|
|
73
74
|
}
|
|
74
75
|
},
|
|
75
|
-
"type": "TEXT_MESSAGE_CHUNK",
|
|
76
76
|
"messageId": "u1",
|
|
77
77
|
"role": "user",
|
|
78
78
|
"delta": "hi there"
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
},
|
|
97
97
|
"agui": [
|
|
98
98
|
{
|
|
99
|
+
"type": "TOOL_CALL_START",
|
|
99
100
|
"metadata": {
|
|
100
101
|
"canopy": {
|
|
101
102
|
"turn_index": 5,
|
|
@@ -107,7 +108,6 @@
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
},
|
|
110
|
-
"type": "TOOL_CALL_START",
|
|
111
111
|
"toolCallId": "t9",
|
|
112
112
|
"toolCallName": "list_insights",
|
|
113
113
|
"parentMessageId": "m1"
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"agui": [
|
|
141
141
|
{
|
|
142
|
+
"type": "TOOL_CALL_RESULT",
|
|
142
143
|
"metadata": {
|
|
143
144
|
"canopy": {
|
|
144
145
|
"turn_index": 6,
|
|
@@ -150,7 +151,6 @@
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
},
|
|
153
|
-
"type": "TOOL_CALL_RESULT",
|
|
154
154
|
"messageId": "t9",
|
|
155
155
|
"toolCallId": "t9",
|
|
156
156
|
"content": "[]"
|
|
@@ -231,5 +231,425 @@
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"canopy": {
|
|
237
|
+
"event": "session.state",
|
|
238
|
+
"data": {
|
|
239
|
+
"messages": [
|
|
240
|
+
{
|
|
241
|
+
"id": "41",
|
|
242
|
+
"turn_index": 0,
|
|
243
|
+
"role": "user",
|
|
244
|
+
"content": {},
|
|
245
|
+
"plaintext": "what is stale?",
|
|
246
|
+
"status": "complete",
|
|
247
|
+
"error_detail": null,
|
|
248
|
+
"started_at": null,
|
|
249
|
+
"completed_at": null,
|
|
250
|
+
"created_at": "2026-09-18T12:00:00+00:00"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": "42",
|
|
254
|
+
"turn_index": 1,
|
|
255
|
+
"role": "assistant",
|
|
256
|
+
"content": {},
|
|
257
|
+
"plaintext": "Three insights are.",
|
|
258
|
+
"status": "complete",
|
|
259
|
+
"error_detail": null,
|
|
260
|
+
"started_at": null,
|
|
261
|
+
"completed_at": null,
|
|
262
|
+
"created_at": "2026-09-18T12:00:05+00:00"
|
|
263
|
+
}
|
|
264
|
+
],
|
|
265
|
+
"active_draft": null,
|
|
266
|
+
"participants": [
|
|
267
|
+
{
|
|
268
|
+
"user_id": 7,
|
|
269
|
+
"role": "editor"
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
"presence_user_ids": [
|
|
273
|
+
7
|
|
274
|
+
],
|
|
275
|
+
"current_user_id": 7,
|
|
276
|
+
"menu": null,
|
|
277
|
+
"turn_status": {
|
|
278
|
+
"state": "working",
|
|
279
|
+
"agent_slug": "hal",
|
|
280
|
+
"runners": [
|
|
281
|
+
"jj-mbp"
|
|
282
|
+
],
|
|
283
|
+
"claimed_by": "jj-mbp",
|
|
284
|
+
"pinned": false,
|
|
285
|
+
"cloud_runner": null,
|
|
286
|
+
"cloud_runner_id": null,
|
|
287
|
+
"last_seen_at": null,
|
|
288
|
+
"menu_pending": false,
|
|
289
|
+
"settled": false,
|
|
290
|
+
"stuck": false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"agui": [
|
|
295
|
+
{
|
|
296
|
+
"type": "MESSAGES_SNAPSHOT",
|
|
297
|
+
"metadata": {
|
|
298
|
+
"canopy": {
|
|
299
|
+
"frame": {
|
|
300
|
+
"event": "session.state",
|
|
301
|
+
"data": {
|
|
302
|
+
"messages": [
|
|
303
|
+
{
|
|
304
|
+
"id": "41",
|
|
305
|
+
"turn_index": 0,
|
|
306
|
+
"role": "user",
|
|
307
|
+
"content": {},
|
|
308
|
+
"plaintext": "what is stale?",
|
|
309
|
+
"status": "complete",
|
|
310
|
+
"error_detail": null,
|
|
311
|
+
"started_at": null,
|
|
312
|
+
"completed_at": null,
|
|
313
|
+
"created_at": "2026-09-18T12:00:00+00:00"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"id": "42",
|
|
317
|
+
"turn_index": 1,
|
|
318
|
+
"role": "assistant",
|
|
319
|
+
"content": {},
|
|
320
|
+
"plaintext": "Three insights are.",
|
|
321
|
+
"status": "complete",
|
|
322
|
+
"error_detail": null,
|
|
323
|
+
"started_at": null,
|
|
324
|
+
"completed_at": null,
|
|
325
|
+
"created_at": "2026-09-18T12:00:05+00:00"
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
"active_draft": null,
|
|
329
|
+
"participants": [
|
|
330
|
+
{
|
|
331
|
+
"user_id": 7,
|
|
332
|
+
"role": "editor"
|
|
333
|
+
}
|
|
334
|
+
],
|
|
335
|
+
"presence_user_ids": [
|
|
336
|
+
7
|
|
337
|
+
],
|
|
338
|
+
"current_user_id": 7,
|
|
339
|
+
"menu": null,
|
|
340
|
+
"turn_status": {
|
|
341
|
+
"state": "working",
|
|
342
|
+
"agent_slug": "hal",
|
|
343
|
+
"runners": [
|
|
344
|
+
"jj-mbp"
|
|
345
|
+
],
|
|
346
|
+
"claimed_by": "jj-mbp",
|
|
347
|
+
"pinned": false,
|
|
348
|
+
"cloud_runner": null,
|
|
349
|
+
"cloud_runner_id": null,
|
|
350
|
+
"last_seen_at": null,
|
|
351
|
+
"menu_pending": false,
|
|
352
|
+
"settled": false,
|
|
353
|
+
"stuck": false
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
"messages": [
|
|
360
|
+
{
|
|
361
|
+
"id": "41",
|
|
362
|
+
"role": "user",
|
|
363
|
+
"content": "what is stale?"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"id": "42",
|
|
367
|
+
"role": "assistant",
|
|
368
|
+
"content": "Three insights are."
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"canopy": {
|
|
376
|
+
"event": "session.page_action",
|
|
377
|
+
"data": {
|
|
378
|
+
"id": "9f1c",
|
|
379
|
+
"name": "scrollToRow",
|
|
380
|
+
"args": {
|
|
381
|
+
"id": 4471
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
"agui": [
|
|
386
|
+
{
|
|
387
|
+
"type": "CUSTOM",
|
|
388
|
+
"name": "canopy.session.page_action",
|
|
389
|
+
"value": {
|
|
390
|
+
"id": "9f1c",
|
|
391
|
+
"name": "scrollToRow",
|
|
392
|
+
"args": {
|
|
393
|
+
"id": 4471
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
"canopy": {
|
|
401
|
+
"event": "session.turn_status",
|
|
402
|
+
"data": {
|
|
403
|
+
"status": {
|
|
404
|
+
"state": "waiting_runner",
|
|
405
|
+
"agent_slug": "hal",
|
|
406
|
+
"runners": [
|
|
407
|
+
"jj-mbp"
|
|
408
|
+
],
|
|
409
|
+
"claimed_by": null,
|
|
410
|
+
"pinned": false,
|
|
411
|
+
"cloud_runner": "cloud-1",
|
|
412
|
+
"cloud_runner_id": "8f2e",
|
|
413
|
+
"last_seen_at": null,
|
|
414
|
+
"menu_pending": false,
|
|
415
|
+
"settled": false,
|
|
416
|
+
"stuck": true
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
"agui": [
|
|
421
|
+
{
|
|
422
|
+
"type": "CUSTOM",
|
|
423
|
+
"name": "canopy.session.turn_status",
|
|
424
|
+
"value": {
|
|
425
|
+
"status": {
|
|
426
|
+
"state": "waiting_runner",
|
|
427
|
+
"agent_slug": "hal",
|
|
428
|
+
"runners": [
|
|
429
|
+
"jj-mbp"
|
|
430
|
+
],
|
|
431
|
+
"claimed_by": null,
|
|
432
|
+
"pinned": false,
|
|
433
|
+
"cloud_runner": "cloud-1",
|
|
434
|
+
"cloud_runner_id": "8f2e",
|
|
435
|
+
"last_seen_at": null,
|
|
436
|
+
"menu_pending": false,
|
|
437
|
+
"settled": false,
|
|
438
|
+
"stuck": true
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"canopy": {
|
|
446
|
+
"event": "session.stop",
|
|
447
|
+
"data": {
|
|
448
|
+
"state": "failed"
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
"agui": [
|
|
452
|
+
{
|
|
453
|
+
"type": "CUSTOM",
|
|
454
|
+
"name": "canopy.session.stop",
|
|
455
|
+
"value": {
|
|
456
|
+
"state": "failed"
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"canopy": {
|
|
463
|
+
"event": "session.activity",
|
|
464
|
+
"data": {
|
|
465
|
+
"state": "working"
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
"agui": [
|
|
469
|
+
{
|
|
470
|
+
"type": "ACTIVITY_SNAPSHOT",
|
|
471
|
+
"messageId": "t1",
|
|
472
|
+
"activityType": "canopy.session",
|
|
473
|
+
"content": {
|
|
474
|
+
"state": "working"
|
|
475
|
+
},
|
|
476
|
+
"replace": true
|
|
477
|
+
}
|
|
478
|
+
]
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"canopy": {
|
|
482
|
+
"event": "session.menu",
|
|
483
|
+
"data": {
|
|
484
|
+
"menu": {
|
|
485
|
+
"source": "hook",
|
|
486
|
+
"question": "Proceed?",
|
|
487
|
+
"observed_at": 1758196800,
|
|
488
|
+
"options": [
|
|
489
|
+
{
|
|
490
|
+
"label": "Yes"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"label": "No"
|
|
494
|
+
}
|
|
495
|
+
]
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
"agui": [
|
|
500
|
+
{
|
|
501
|
+
"type": "CUSTOM",
|
|
502
|
+
"name": "canopy.menu",
|
|
503
|
+
"value": {
|
|
504
|
+
"source": "hook",
|
|
505
|
+
"question": "Proceed?",
|
|
506
|
+
"observed_at": 1758196800,
|
|
507
|
+
"options": [
|
|
508
|
+
{
|
|
509
|
+
"label": "Yes"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"label": "No"
|
|
513
|
+
}
|
|
514
|
+
]
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
]
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"canopy": {
|
|
521
|
+
"event": "session.error",
|
|
522
|
+
"data": {
|
|
523
|
+
"code": "draft_conflict",
|
|
524
|
+
"message": "stale version"
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
"agui": [
|
|
528
|
+
{
|
|
529
|
+
"type": "RUN_ERROR",
|
|
530
|
+
"message": "stale version",
|
|
531
|
+
"code": "draft_conflict"
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"canopy": {
|
|
537
|
+
"event": "chat.stream_cancelled",
|
|
538
|
+
"data": {
|
|
539
|
+
"message_id": "m1",
|
|
540
|
+
"partial_len": 12
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
"agui": [
|
|
544
|
+
{
|
|
545
|
+
"type": "CUSTOM",
|
|
546
|
+
"name": "canopy.chat.stream_cancelled",
|
|
547
|
+
"value": {
|
|
548
|
+
"message_id": "m1",
|
|
549
|
+
"partial_len": 12
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
]
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
"canopy": {
|
|
556
|
+
"event": "chat.stream_error",
|
|
557
|
+
"data": {
|
|
558
|
+
"message_id": "m1",
|
|
559
|
+
"detail": "runner went away"
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
"agui": [
|
|
563
|
+
{
|
|
564
|
+
"type": "RUN_ERROR",
|
|
565
|
+
"metadata": {
|
|
566
|
+
"canopy": {
|
|
567
|
+
"frame": {
|
|
568
|
+
"event": "chat.stream_error",
|
|
569
|
+
"data": {
|
|
570
|
+
"message_id": "m1",
|
|
571
|
+
"detail": "runner went away"
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
"message": "runner went away",
|
|
577
|
+
"code": "stream_error"
|
|
578
|
+
}
|
|
579
|
+
]
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"canopy": {
|
|
583
|
+
"event": "draft.committed",
|
|
584
|
+
"data": {
|
|
585
|
+
"draft_id": "d1",
|
|
586
|
+
"user_message_id": "u2"
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
"agui": [
|
|
590
|
+
{
|
|
591
|
+
"type": "CUSTOM",
|
|
592
|
+
"name": "canopy.draft.committed",
|
|
593
|
+
"value": {
|
|
594
|
+
"draft_id": "d1",
|
|
595
|
+
"user_message_id": "u2"
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
"canopy": {
|
|
602
|
+
"event": "draft.discarded",
|
|
603
|
+
"data": {
|
|
604
|
+
"draft_id": "d1"
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
"agui": [
|
|
608
|
+
{
|
|
609
|
+
"type": "CUSTOM",
|
|
610
|
+
"name": "canopy.draft.discarded",
|
|
611
|
+
"value": {
|
|
612
|
+
"draft_id": "d1"
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
"canopy": {
|
|
619
|
+
"event": "draft.lock_changed",
|
|
620
|
+
"data": {
|
|
621
|
+
"draft_id": "d1",
|
|
622
|
+
"holder_user_id": 7,
|
|
623
|
+
"expires_at": "2026-09-18T12:01:00+00:00"
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
"agui": [
|
|
627
|
+
{
|
|
628
|
+
"type": "CUSTOM",
|
|
629
|
+
"name": "canopy.draft.lock_changed",
|
|
630
|
+
"value": {
|
|
631
|
+
"draft_id": "d1",
|
|
632
|
+
"holder_user_id": 7,
|
|
633
|
+
"expires_at": "2026-09-18T12:01:00+00:00"
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
"canopy": {
|
|
640
|
+
"event": "page.invalidate",
|
|
641
|
+
"data": {
|
|
642
|
+
"uri": "item://"
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
"agui": [
|
|
646
|
+
{
|
|
647
|
+
"type": "CUSTOM",
|
|
648
|
+
"name": "canopy.page.invalidate",
|
|
649
|
+
"value": {
|
|
650
|
+
"uri": "item://"
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
]
|
|
234
654
|
}
|
|
235
655
|
]
|
package/src/chat/agui.test.ts
CHANGED
|
@@ -277,7 +277,11 @@ describe('the socket actually asks for it', () => {
|
|
|
277
277
|
it('is wired to the hook’s public option', () => {
|
|
278
278
|
const source = hookSource()
|
|
279
279
|
|
|
280
|
-
expect(source).toContain('protocol=ag-ui')
|
|
280
|
+
// NOT `expect(source).toContain('protocol=ag-ui')`, which this used to say.
|
|
281
|
+
// It held the whole time the flag was being dropped by any URL builder that
|
|
282
|
+
// ignores its path — i.e. by canopy-web's widget and by ace-web. Whether the
|
|
283
|
+
// flag reaches the socket is asserted where it can be seen, against the URL
|
|
284
|
+
// a socket really opens: `useSessionSocket.protocol.test.tsx`.
|
|
281
285
|
expect(source).toContain('fromAgui')
|
|
282
286
|
// And the default stays canopy's own vocabulary, which is what lets an
|
|
283
287
|
// existing consumer — ace-web installs this package from npm — notice
|