@xenosystem/blocks 0.6.0 → 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.
@@ -0,0 +1,1439 @@
1
+ import "../chunk-2KG3PWR4.js";
2
+
3
+ // src/agent/agent/panel.ts
4
+ import { createElement } from "react";
5
+ import { createRoot } from "react-dom/client";
6
+ import {
7
+ bindConfig,
8
+ configBool,
9
+ configString,
10
+ isRecord
11
+ } from "@xenosystem/block-sdk";
12
+
13
+ // src/agent/agent/unified-diff.ts
14
+ var HUNK_HEADER = /^@@+ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@+(.*)$/;
15
+ var FILE_HEADER = /^(diff --git |index |--- |\+\+\+ |new file mode |deleted file mode |similarity index |rename (from|to) |old mode |new mode |Binary files )/;
16
+ function hash(text) {
17
+ let h = 2166136261;
18
+ for (let i = 0; i < text.length; i += 1) {
19
+ h ^= text.charCodeAt(i);
20
+ h = Math.imul(h, 16777619) >>> 0;
21
+ }
22
+ return h.toString(16).padStart(8, "0");
23
+ }
24
+ function parseUnifiedDiff(path, diff) {
25
+ const hunks = [];
26
+ const ids = /* @__PURE__ */ new Set();
27
+ let current = null;
28
+ let oldLine = 0;
29
+ let newLine = 0;
30
+ const close = () => {
31
+ if (!current) return;
32
+ const body = current.lines.map((l) => `${l.kind[0]}${l.text}`).join("\n");
33
+ let id = `${path}#${hash(`${current.header ?? ""}
34
+ ${body}`)}`;
35
+ if (ids.has(id)) {
36
+ id = `${id}-${ids.size}`;
37
+ }
38
+ ids.add(id);
39
+ current.id = id;
40
+ hunks.push(current);
41
+ current = null;
42
+ };
43
+ for (const raw of diff.split("\n")) {
44
+ const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
45
+ const header = HUNK_HEADER.exec(line);
46
+ if (header) {
47
+ close();
48
+ oldLine = Number(header[1]);
49
+ newLine = Number(header[3]);
50
+ current = {
51
+ id: "",
52
+ oldStart: oldLine,
53
+ // ⚠️ An absent count means 1, per the format. Defaulting it to 0 would render a
54
+ // single-line hunk as an empty one — present in the list, empty on screen.
55
+ oldLines: header[2] === void 0 ? 1 : Number(header[2]),
56
+ newStart: newLine,
57
+ newLines: header[4] === void 0 ? 1 : Number(header[4]),
58
+ lines: [],
59
+ header: line
60
+ };
61
+ continue;
62
+ }
63
+ if (!current) {
64
+ continue;
65
+ }
66
+ if (line.startsWith("\\")) {
67
+ continue;
68
+ }
69
+ if (line.startsWith("+")) {
70
+ current.lines.push({ kind: "added", newLineNumber: newLine, text: line.slice(1) });
71
+ newLine += 1;
72
+ } else if (line.startsWith("-")) {
73
+ current.lines.push({ kind: "removed", oldLineNumber: oldLine, text: line.slice(1) });
74
+ oldLine += 1;
75
+ } else if (line.startsWith(" ")) {
76
+ current.lines.push({
77
+ kind: "context",
78
+ oldLineNumber: oldLine,
79
+ newLineNumber: newLine,
80
+ text: line.slice(1)
81
+ });
82
+ oldLine += 1;
83
+ newLine += 1;
84
+ } else if (line === "") {
85
+ if (current.lines.filter((l) => l.kind !== "added").length >= current.oldLines) continue;
86
+ current.lines.push({ kind: "context", oldLineNumber: oldLine, newLineNumber: newLine, text: "" });
87
+ oldLine += 1;
88
+ newLine += 1;
89
+ } else if (FILE_HEADER.test(line)) {
90
+ close();
91
+ }
92
+ }
93
+ close();
94
+ return hunks;
95
+ }
96
+ function countChanges(hunks) {
97
+ let additions = 0;
98
+ let deletions = 0;
99
+ for (const hunk of hunks) {
100
+ for (const line of hunk.lines) {
101
+ if (line.kind === "added") additions += 1;
102
+ else if (line.kind === "removed") deletions += 1;
103
+ }
104
+ }
105
+ return { additions, deletions };
106
+ }
107
+
108
+ // src/agent/agent/projections.ts
109
+ var AGENT_FANOUT_PORTS = [
110
+ "runs",
111
+ "records",
112
+ "elicitations",
113
+ "withdraw",
114
+ "diffModel",
115
+ "terminalData",
116
+ "terminalSession"
117
+ ];
118
+ var RUN_STATUS = {
119
+ idle: "requested",
120
+ queued: "queued",
121
+ running: "running",
122
+ // The two that had no run-panel equivalent until the panel gained them, and the reason it did.
123
+ "waiting-for-user": "awaiting-decision",
124
+ "waiting-for-permission": "awaiting-permission",
125
+ succeeded: "succeeded",
126
+ failed: "failed",
127
+ cancelled: "cancelled"
128
+ };
129
+ function turnStatusToRunStatus(state) {
130
+ return RUN_STATUS[state];
131
+ }
132
+ function levelFor(role) {
133
+ return role === "error" ? "error" : "info";
134
+ }
135
+ var AgentProjector = class {
136
+ now;
137
+ /** turnId → sessionId. The address a run action comes back on. */
138
+ turnSession = /* @__PURE__ */ new Map();
139
+ /** callId → the step path inside its run, so a nested call can be patched in place. */
140
+ callPath = /* @__PURE__ */ new Map();
141
+ /** messageId → seen, so the second event for a message patches rather than re-appends. */
142
+ messages = /* @__PURE__ */ new Set();
143
+ /** askId → sessionId. */
144
+ askSession = /* @__PURE__ */ new Map();
145
+ /** terminalId → sessionId. */
146
+ terminalSession = /* @__PURE__ */ new Map();
147
+ /**
148
+ * File path → the change set it belongs to.
149
+ *
150
+ * ⚠️ **Replaced wholesale on every `patch` event, because the diff panel's model is too.** The
151
+ * map and the thing it describes move together, so a decision can never address a change set the
152
+ * user is no longer looking at.
153
+ */
154
+ patchPaths = /* @__PURE__ */ new Map();
155
+ /**
156
+ * Answers that addressed nothing known, since the last clear.
157
+ *
158
+ * Surfaced rather than swallowed, for the reason `xeno.core.console` surfaces its dropped
159
+ * patches: a mis-wired return path and a dead one look identical from the outside, and this is
160
+ * the one number that tells them apart.
161
+ */
162
+ dropped = 0;
163
+ constructor(options = {}) {
164
+ this.now = options.now ?? (() => Date.now());
165
+ }
166
+ /** How many answers were dropped for want of a correlation. */
167
+ get droppedAnswers() {
168
+ return this.dropped;
169
+ }
170
+ /** Forget everything. Used when the host replaces or clears the session index. */
171
+ reset() {
172
+ this.turnSession.clear();
173
+ this.callPath.clear();
174
+ this.messages.clear();
175
+ this.askSession.clear();
176
+ this.terminalSession.clear();
177
+ this.patchPaths = /* @__PURE__ */ new Map();
178
+ this.dropped = 0;
179
+ }
180
+ /** How many asks are still outstanding. */
181
+ get pendingAsks() {
182
+ return this.askSession.size;
183
+ }
184
+ /** Which session an ask belongs to, or `undefined`. */
185
+ sessionForAsk(askId) {
186
+ return this.askSession.get(askId);
187
+ }
188
+ /** Which session a turn belongs to, or `undefined`. */
189
+ sessionForTurn(turnId) {
190
+ return this.turnSession.get(turnId);
191
+ }
192
+ /** Which session a terminal belongs to, or `undefined`. */
193
+ sessionForTerminal(terminalId) {
194
+ return this.terminalSession.get(terminalId);
195
+ }
196
+ /** Which change set a reviewed file belongs to, or `undefined`. */
197
+ patchForPath(path) {
198
+ return this.patchPaths.get(path);
199
+ }
200
+ /** Record that an ask has been settled, so a second answer for it is dropped rather than resent. */
201
+ settleAsk(askId) {
202
+ this.askSession.delete(askId);
203
+ }
204
+ /** Count an answer that could not be correlated. */
205
+ dropAnswer() {
206
+ this.dropped += 1;
207
+ }
208
+ /**
209
+ * Project one event.
210
+ *
211
+ * @param event - The event.
212
+ * @returns Zero or more emissions, in the order they must be delivered. **Order matters**: a
213
+ * console record must be appended before it is patched, and a step must be announced before it
214
+ * is addressed — both panels drop an update for something they do not hold.
215
+ */
216
+ project(event) {
217
+ switch (event.type) {
218
+ case "session":
219
+ return [];
220
+ case "session-closed": {
221
+ return [];
222
+ }
223
+ case "turn": {
224
+ const fresh = !this.turnSession.has(event.turnId);
225
+ this.turnSession.set(event.turnId, event.sessionId);
226
+ const status = turnStatusToRunStatus(event.state);
227
+ if (fresh) {
228
+ return [
229
+ {
230
+ portId: "runs",
231
+ value: {
232
+ append: [
233
+ {
234
+ id: event.turnId,
235
+ label: event.label ?? event.turnId,
236
+ status,
237
+ // The run panel sorts on this and requires it. A turn with no reported start
238
+ // is stamped on arrival rather than defaulted to the epoch, which would file
239
+ // it at the bottom of the list — exactly where nobody looks for the thing they
240
+ // just asked for.
241
+ startedAt: event.startedAt ?? this.now(),
242
+ endedAt: event.endedAt,
243
+ error: event.error,
244
+ source: event.sessionId
245
+ }
246
+ ]
247
+ }
248
+ }
249
+ ];
250
+ }
251
+ return [
252
+ {
253
+ portId: "runs",
254
+ value: {
255
+ patch: {
256
+ [event.turnId]: {
257
+ status,
258
+ ...event.label === void 0 ? {} : { label: event.label },
259
+ ...event.endedAt === void 0 ? {} : { endedAt: event.endedAt },
260
+ ...event.error === void 0 ? {} : { error: event.error }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ ];
266
+ }
267
+ case "tool": {
268
+ this.turnSession.set(event.turnId, event.sessionId);
269
+ const known = this.callPath.get(event.callId);
270
+ const status = turnStatusToRunStatus(event.state);
271
+ if (!known) {
272
+ const parentPath = event.parentCallId ? this.callPath.get(event.parentCallId) : void 0;
273
+ const path = [...parentPath ?? [], event.callId];
274
+ this.callPath.set(event.callId, path);
275
+ return [
276
+ {
277
+ portId: "runs",
278
+ value: {
279
+ appendSteps: [
280
+ {
281
+ runId: event.turnId,
282
+ path: parentPath,
283
+ steps: [
284
+ {
285
+ id: event.callId,
286
+ label: event.label ?? event.callId,
287
+ status,
288
+ startedAt: event.startedAt,
289
+ endedAt: event.endedAt,
290
+ progress: event.progress,
291
+ detail: event.detail,
292
+ error: event.error
293
+ }
294
+ ]
295
+ }
296
+ ]
297
+ }
298
+ }
299
+ ];
300
+ }
301
+ return [
302
+ {
303
+ portId: "runs",
304
+ value: {
305
+ patchSteps: [
306
+ {
307
+ runId: event.turnId,
308
+ path: known,
309
+ patch: {
310
+ status,
311
+ ...event.label === void 0 ? {} : { label: event.label },
312
+ ...event.progress === void 0 ? {} : { progress: event.progress },
313
+ ...event.detail === void 0 ? {} : { detail: event.detail },
314
+ ...event.endedAt === void 0 ? {} : { endedAt: event.endedAt },
315
+ ...event.error === void 0 ? {} : { error: event.error }
316
+ }
317
+ }
318
+ ]
319
+ }
320
+ }
321
+ ];
322
+ }
323
+ case "message": {
324
+ const fresh = !this.messages.has(event.messageId);
325
+ const delta = {};
326
+ if (fresh) {
327
+ this.messages.add(event.messageId);
328
+ delta.append = [
329
+ {
330
+ id: event.messageId,
331
+ ts: event.ts ?? this.now(),
332
+ level: levelFor(event.role),
333
+ message: event.text ?? "",
334
+ source: event.sessionId,
335
+ scope: event.role
336
+ }
337
+ ];
338
+ if (event.append !== void 0) {
339
+ delta.patch = { [event.messageId]: { appendMessage: event.append } };
340
+ }
341
+ return [{ portId: "records", value: delta }];
342
+ }
343
+ const patch = {};
344
+ if (event.text !== void 0) patch.message = event.text;
345
+ if (event.append !== void 0) patch.appendMessage = event.append;
346
+ if (Object.keys(patch).length === 0) return [];
347
+ return [{ portId: "records", value: { patch: { [event.messageId]: patch } } }];
348
+ }
349
+ case "ask": {
350
+ this.askSession.set(event.ask.id, event.sessionId);
351
+ return [{ portId: "elicitations", value: event.ask }];
352
+ }
353
+ case "ask-withdraw": {
354
+ this.askSession.delete(event.askId);
355
+ return [{ portId: "withdraw", value: event.askId }];
356
+ }
357
+ case "patch": {
358
+ const paths = /* @__PURE__ */ new Map();
359
+ const files = event.files.map((file) => {
360
+ paths.set(file.path, { sessionId: event.sessionId, patchId: event.patchId });
361
+ return this.projectFile(file);
362
+ });
363
+ this.patchPaths = paths;
364
+ return [
365
+ {
366
+ portId: "diffModel",
367
+ value: {
368
+ files,
369
+ status: "ready",
370
+ oldLabel: event.oldLabel,
371
+ newLabel: event.newLabel
372
+ }
373
+ }
374
+ ];
375
+ }
376
+ case "terminal": {
377
+ this.terminalSession.set(event.terminalId, event.sessionId);
378
+ const out = [];
379
+ if (event.session !== void 0) {
380
+ out.push({ portId: "terminalSession", value: event.session });
381
+ }
382
+ if (event.data !== void 0) {
383
+ out.push({ portId: "terminalData", value: { id: event.terminalId, data: event.data } });
384
+ }
385
+ return out;
386
+ }
387
+ default: {
388
+ return [];
389
+ }
390
+ }
391
+ }
392
+ /** One file of a change set → the diff panel's file shape. */
393
+ projectFile(file) {
394
+ if (file.omitted !== void 0 || file.diff === void 0) {
395
+ return {
396
+ path: file.path,
397
+ oldPath: file.oldPath,
398
+ status: file.status,
399
+ language: file.language,
400
+ hunks: [],
401
+ omitted: file.omitted ?? "unreadable"
402
+ };
403
+ }
404
+ const hunks = parseUnifiedDiff(file.path, file.diff);
405
+ if (hunks.length === 0) {
406
+ return {
407
+ path: file.path,
408
+ oldPath: file.oldPath,
409
+ status: file.status,
410
+ language: file.language,
411
+ hunks: [],
412
+ omitted: "unreadable"
413
+ };
414
+ }
415
+ const { additions, deletions } = countChanges(hunks);
416
+ return {
417
+ path: file.path,
418
+ oldPath: file.oldPath,
419
+ status: file.status,
420
+ language: file.language,
421
+ hunks,
422
+ additions,
423
+ deletions
424
+ };
425
+ }
426
+ };
427
+
428
+ // src/agent/agent/types.ts
429
+ function isBusy(turn) {
430
+ return turn === "queued" || turn === "running";
431
+ }
432
+ function isWaitingOnUser(turn) {
433
+ return turn === "waiting-for-user" || turn === "waiting-for-permission";
434
+ }
435
+
436
+ // src/agent/agent/controller.ts
437
+ var AgentController = class {
438
+ host;
439
+ projector;
440
+ sessions = [];
441
+ activeSessionId = null;
442
+ connection = "idle";
443
+ connectionMessage = null;
444
+ catalog = { providers: [] };
445
+ draft = "";
446
+ /** Last delta revision applied. A delta whose rev has not advanced is dropped. */
447
+ rev = -1;
448
+ listeners = /* @__PURE__ */ new Set();
449
+ /**
450
+ * Memoized view state.
451
+ *
452
+ * `useSyncExternalStore` compares by identity and loops forever if `getState` returns a fresh
453
+ * object each call. Every mutation clears this; nothing else may.
454
+ */
455
+ snapshot = null;
456
+ constructor(options) {
457
+ this.host = options.host;
458
+ this.projector = new AgentProjector({ now: options.now });
459
+ }
460
+ /* ── Subscription ──────────────────────────────────────────────────────── */
461
+ subscribe = (listener) => {
462
+ this.listeners.add(listener);
463
+ return () => this.listeners.delete(listener);
464
+ };
465
+ getState = () => {
466
+ if (!this.snapshot) {
467
+ const active = this.sessions.find((s) => s.id === this.activeSessionId) ?? null;
468
+ const turn = active?.turn ?? "idle";
469
+ const busy = isBusy(turn);
470
+ const activeProvider = this.catalog.providers.find((p) => p.id === (active?.providerId ?? this.catalog.activeProviderId)) ?? null;
471
+ this.snapshot = {
472
+ sessions: this.sessions,
473
+ active,
474
+ turn,
475
+ connection: this.connection,
476
+ // A turn's own failure sentence wins over the connection's: it is the more specific fact,
477
+ // and it is the one the user is looking at.
478
+ message: (turn === "failed" ? active?.message : null) ?? this.connectionMessage,
479
+ providers: this.catalog.providers,
480
+ activeProvider,
481
+ activeModelId: active?.modelId ?? this.catalog.activeModelId ?? null,
482
+ draft: this.draft,
483
+ canSubmit: this.canSubmit(),
484
+ busy,
485
+ pendingAsks: this.projector.pendingAsks
486
+ };
487
+ }
488
+ return this.snapshot;
489
+ };
490
+ notify() {
491
+ this.snapshot = null;
492
+ for (const listener of this.listeners) listener();
493
+ }
494
+ /* ── Fan-out ───────────────────────────────────────────────────────────── */
495
+ fanOut(emissions) {
496
+ for (const emission of emissions) this.host.emit(emission.portId, emission.value);
497
+ }
498
+ /** Emit the one thing the host acts on. */
499
+ intent(intent) {
500
+ this.host.emit("intent", intent);
501
+ }
502
+ /* ── Host-pushed state ─────────────────────────────────────────────────── */
503
+ /**
504
+ * Apply a session delta.
505
+ *
506
+ * The host is authoritative: sessions are upserted, never merged cleverly, and the panel keeps no
507
+ * "better" older value. A panel that second-guesses its host is a second source of truth.
508
+ *
509
+ * @param delta - The delta.
510
+ */
511
+ ingest(delta) {
512
+ if (typeof delta.rev === "number") {
513
+ if (delta.rev <= this.rev) return;
514
+ this.rev = delta.rev;
515
+ }
516
+ if (delta.clear) {
517
+ this.sessions = [];
518
+ this.activeSessionId = null;
519
+ this.projector.reset();
520
+ this.notify();
521
+ return;
522
+ }
523
+ if (delta.replace) {
524
+ this.sessions = [...delta.replace].sort(orderSessions);
525
+ this.projector.reset();
526
+ if (this.activeSessionId && !this.sessions.some((s) => s.id === this.activeSessionId)) {
527
+ this.activeSessionId = null;
528
+ }
529
+ this.notify();
530
+ return;
531
+ }
532
+ let touched = false;
533
+ for (const event of delta.events ?? []) {
534
+ if (event.type === "session") {
535
+ this.upsertSession(event.session);
536
+ touched = true;
537
+ } else if (event.type === "session-closed") {
538
+ this.sessions = this.sessions.filter((s) => s.id !== event.sessionId);
539
+ if (this.activeSessionId === event.sessionId) this.activeSessionId = null;
540
+ touched = true;
541
+ } else if (event.type === "turn") {
542
+ touched = this.setTurnState(event.sessionId, event.state) || touched;
543
+ } else if (event.type === "ask" || event.type === "ask-withdraw") {
544
+ touched = true;
545
+ }
546
+ this.fanOut(this.projector.project(event));
547
+ }
548
+ if (!this.activeSessionId && this.sessions.length > 0) {
549
+ this.activeSessionId = this.sessions[0].id;
550
+ touched = true;
551
+ }
552
+ if (touched) this.notify();
553
+ }
554
+ upsertSession(next) {
555
+ const at = this.sessions.findIndex((s) => s.id === next.id);
556
+ if (at < 0) {
557
+ this.sessions = [...this.sessions, next].sort(orderSessions);
558
+ return;
559
+ }
560
+ const defined = Object.fromEntries(Object.entries(next).filter(([, v]) => v !== void 0));
561
+ const merged = { ...this.sessions[at], ...defined };
562
+ const sessions = [...this.sessions];
563
+ sessions[at] = merged;
564
+ this.sessions = sessions.sort(orderSessions);
565
+ }
566
+ setTurnState(sessionId, turn) {
567
+ const at = this.sessions.findIndex((s) => s.id === sessionId);
568
+ if (at < 0) return false;
569
+ if (this.sessions[at].turn === turn) return false;
570
+ const sessions = [...this.sessions];
571
+ sessions[at] = { ...sessions[at], turn };
572
+ this.sessions = sessions;
573
+ return true;
574
+ }
575
+ /** Install the provider catalogue. The host decides what is selectable; the panel renders it. */
576
+ setCatalog(catalog) {
577
+ this.catalog = { ...catalog, providers: catalog.providers ?? [] };
578
+ this.notify();
579
+ }
580
+ /**
581
+ * Report the connection out of band.
582
+ *
583
+ * Separate from the session delta so a host never has to express "not connected" as an empty
584
+ * session list — the distinction `xeno.core.runs` and `xeno.core.diff` both draw, and for the
585
+ * same reason: zero sessions means "there are none" only when someone looked.
586
+ */
587
+ setStatus(status, message) {
588
+ this.connection = status;
589
+ this.connectionMessage = message ?? null;
590
+ this.notify();
591
+ }
592
+ /* ── The composer ──────────────────────────────────────────────────────── */
593
+ setDraft(text) {
594
+ this.draft = text;
595
+ this.notify();
596
+ }
597
+ /**
598
+ * Would {@link submit} do anything?
599
+ *
600
+ * 🔴 The view's `disabled` derives from THIS, so an enabled button cannot be a button that
601
+ * refuses. The affordance and the guard are one predicate.
602
+ *
603
+ * ⚠️ `connection === 'idle'` does NOT block. Idle means *the host has never reported a
604
+ * connection state*, and inventing a blocker out of silence would make the panel unusable in
605
+ * every host that does not wire the optional `status` port. Silence is not a refusal; the host
606
+ * refuses if it must.
607
+ */
608
+ canSubmit() {
609
+ if (!this.activeSessionId) return false;
610
+ if (this.draft.trim() === "") return false;
611
+ if (this.connection === "connecting" || this.connection === "failed") return false;
612
+ const active = this.sessions.find((s) => s.id === this.activeSessionId);
613
+ return !isBusy(active?.turn ?? "idle");
614
+ }
615
+ /**
616
+ * Ask the host to run the draft.
617
+ *
618
+ * 🔴 **The draft is NOT cleared here.** The panel does not know the turn started — the host may
619
+ * refuse, the provider may be unavailable, a permission check may reject it. Clearing on emit
620
+ * would show success before anything settled, which is a claim the panel cannot back; the draft
621
+ * clears when the host reports a `running` turn. (`panel-template`'s rule, applied to the one
622
+ * place in this catalog where a user would notice losing their text.)
623
+ *
624
+ * @returns Whether an intent was emitted.
625
+ */
626
+ submit() {
627
+ if (!this.canSubmit()) return false;
628
+ const active = this.sessions.find((s) => s.id === this.activeSessionId);
629
+ this.intent({
630
+ type: "prompt",
631
+ sessionId: active.id,
632
+ text: this.draft,
633
+ providerId: active.providerId ?? this.catalog.activeProviderId,
634
+ modelId: active.modelId ?? this.catalog.activeModelId
635
+ });
636
+ return true;
637
+ }
638
+ /**
639
+ * Ask the host to steer the running turn.
640
+ *
641
+ * ⚠️ **Steering lands at the agent's next decision point, not instantly.** That was measured
642
+ * against real Claude Code and recorded in the ADE spec, and it is why this is a separate verb
643
+ * from {@link submit} rather than "submit while busy": the two have different latencies and
644
+ * different failure modes, and a control that hid the difference would be lying about which one
645
+ * the user got.
646
+ *
647
+ * @returns Whether an intent was emitted.
648
+ */
649
+ steer() {
650
+ const active = this.sessions.find((s) => s.id === this.activeSessionId);
651
+ if (!active || this.draft.trim() === "") return false;
652
+ if (!isBusy(active.turn ?? "idle")) return false;
653
+ this.intent({ type: "steer", sessionId: active.id, text: this.draft });
654
+ return true;
655
+ }
656
+ /** Ask the host to stop the running turn. */
657
+ cancel() {
658
+ const active = this.sessions.find((s) => s.id === this.activeSessionId);
659
+ if (!active) return false;
660
+ this.intent({ type: "cancel", sessionId: active.id });
661
+ return true;
662
+ }
663
+ /** Show another session. Panel-local AND an intent — the host may need to attach to it. */
664
+ selectSession(sessionId) {
665
+ if (!this.sessions.some((s) => s.id === sessionId)) return false;
666
+ this.activeSessionId = sessionId;
667
+ this.notify();
668
+ this.intent({ type: "select-session", sessionId });
669
+ return true;
670
+ }
671
+ /** Ask the host to start a session. The panel does not create one — it has no way to. */
672
+ newSession(providerId, modelId) {
673
+ this.intent({
674
+ type: "new-session",
675
+ providerId: providerId ?? this.catalog.activeProviderId,
676
+ modelId: modelId ?? this.catalog.activeModelId
677
+ });
678
+ }
679
+ /**
680
+ * Ask the host to switch the active session's agent.
681
+ *
682
+ * Refused for a provider the host declared unavailable: the panel must not offer what the host
683
+ * will not honour, which is the rule `xeno.core.consent` applies to durations.
684
+ *
685
+ * @returns Whether an intent was emitted.
686
+ */
687
+ selectProvider(providerId, modelId) {
688
+ const provider = this.catalog.providers.find((p) => p.id === providerId);
689
+ if (!provider || provider.unavailable !== void 0) return false;
690
+ if (!this.activeSessionId) return false;
691
+ this.intent({ type: "select-provider", sessionId: this.activeSessionId, providerId, modelId });
692
+ return true;
693
+ }
694
+ /* ── Answers coming back from the canonical panels ─────────────────────── */
695
+ /**
696
+ * A consent decision or an elicitation result, from `xeno.core.consent`.
697
+ *
698
+ * 🔴 **Dropped when the id correlates to nothing.** A decision routed to a guessed session
699
+ * applies a human's approval to work they never saw. Both of consent's output ports arrive here
700
+ * because both are answers to an ask this panel minted, and the panel forwards each verbatim —
701
+ * it does not read `decision`, and must not: interpreting an allow/deny would make this a second
702
+ * place where a grant is decided.
703
+ *
704
+ * @param answer - The decision or result, verbatim.
705
+ * @returns Whether an intent was emitted.
706
+ */
707
+ answer(answer) {
708
+ if (!answer || typeof answer !== "object") return false;
709
+ const id = answer.id;
710
+ if (typeof id !== "string") return false;
711
+ const sessionId = this.projector.sessionForAsk(id);
712
+ if (!sessionId) {
713
+ this.projector.dropAnswer();
714
+ return false;
715
+ }
716
+ this.projector.settleAsk(id);
717
+ this.intent({ type: "answer", sessionId, askId: id, answer });
718
+ this.notify();
719
+ return true;
720
+ }
721
+ /**
722
+ * A hunk decision, from `xeno.core.diff`.
723
+ *
724
+ * @param decision - `{path, hunkId, action, comment?}`, verbatim.
725
+ * @returns Whether an intent was emitted.
726
+ */
727
+ patchDecision(decision) {
728
+ if (!decision || typeof decision !== "object") return false;
729
+ const path = decision.path;
730
+ if (typeof path !== "string") return false;
731
+ const owner = this.projector.patchForPath(path);
732
+ if (!owner) {
733
+ this.projector.dropAnswer();
734
+ return false;
735
+ }
736
+ this.intent({
737
+ type: "patch-decision",
738
+ sessionId: owner.sessionId,
739
+ patchId: owner.patchId,
740
+ decision
741
+ });
742
+ return true;
743
+ }
744
+ /**
745
+ * A run action, from `xeno.core.runs`.
746
+ *
747
+ * ⚠️ The targeted step is read from `stepPath`'s LAST element, falling back to `stepId`. Both are
748
+ * emitted together by that panel and mean the same thing at depth 1; only the path survives
749
+ * nesting, which is the case an agent's subagent calls produce.
750
+ *
751
+ * @param action - `{runId, action, stepId?, stepPath?}`, verbatim.
752
+ * @returns Whether an intent was emitted.
753
+ */
754
+ runAction(action) {
755
+ if (!action || typeof action !== "object") return false;
756
+ const record = action;
757
+ if (typeof record.runId !== "string") return false;
758
+ if (record.action !== "cancel" && record.action !== "retry") return false;
759
+ const sessionId = this.projector.sessionForTurn(record.runId);
760
+ if (!sessionId) {
761
+ this.projector.dropAnswer();
762
+ return false;
763
+ }
764
+ const path = Array.isArray(record.stepPath) ? record.stepPath : void 0;
765
+ const last = path && path.length > 0 ? path[path.length - 1] : void 0;
766
+ const callId = typeof last === "string" ? last : typeof record.stepId === "string" ? record.stepId : void 0;
767
+ this.intent({ type: "run-action", sessionId, turnId: record.runId, action: record.action, callId });
768
+ return true;
769
+ }
770
+ /**
771
+ * A terminal intent, from `xeno.core.terminal`.
772
+ *
773
+ * @param intent - The terminal panel's intent, verbatim.
774
+ * @returns Whether an intent was emitted.
775
+ */
776
+ terminalIntent(intent) {
777
+ if (!intent || typeof intent !== "object") return false;
778
+ const id = intent.id;
779
+ if (typeof id !== "string") return false;
780
+ const sessionId = this.projector.sessionForTerminal(id);
781
+ if (!sessionId) {
782
+ this.projector.dropAnswer();
783
+ return false;
784
+ }
785
+ this.intent({ type: "terminal", sessionId, terminal: intent });
786
+ return true;
787
+ }
788
+ /** Answers that could not be correlated, since the last reset. Surfaced, never swallowed. */
789
+ get droppedAnswers() {
790
+ return this.projector.droppedAnswers;
791
+ }
792
+ /* ── Lifecycle ─────────────────────────────────────────────────────────── */
793
+ /**
794
+ * Serialize.
795
+ *
796
+ * 🔴 **Preferences plus the unsent draft, and nothing else.** No transcript, no run list, no
797
+ * session content — that is host state which has moved on, and `.xapp` is a plain JSON file that
798
+ * may contain nothing sensitive.
799
+ */
800
+ serialize() {
801
+ return { activeSessionId: this.activeSessionId, draft: this.draft };
802
+ }
803
+ /** Restore preferences. Sessions are NOT restored; the host re-pushes them. */
804
+ deserialize(state) {
805
+ if (!state || typeof state !== "object") return;
806
+ const s = state;
807
+ if (typeof s.activeSessionId === "string" || s.activeSessionId === null) {
808
+ this.activeSessionId = s.activeSessionId ?? null;
809
+ }
810
+ if (typeof s.draft === "string") this.draft = s.draft;
811
+ this.notify();
812
+ }
813
+ /** Release everything. */
814
+ dispose() {
815
+ this.listeners.clear();
816
+ this.sessions = [];
817
+ this.projector.reset();
818
+ this.snapshot = null;
819
+ }
820
+ };
821
+ function orderSessions(a, b) {
822
+ const at = a.createdAt;
823
+ const bt = b.createdAt;
824
+ if (at === void 0 && bt === void 0) return 0;
825
+ if (at === void 0) return 1;
826
+ if (bt === void 0) return -1;
827
+ return bt - at;
828
+ }
829
+
830
+ // src/agent/agent/manifest.ts
831
+ import { WELL_KNOWN_PORT_SCHEMAS } from "@xenosystem/block-sdk";
832
+ var AGENT_PANEL_ID = "xeno.core.agent";
833
+ var AGENT_SESSION_SCHEMA = "xeno.agentsession@1";
834
+ var AGENT_CATALOG_SCHEMA = "xeno.agentcatalog@1";
835
+ var AGENT_INTENT_SCHEMA = "xeno.agentintent@1";
836
+ var CONSENT_ELICITATION_SCHEMA = "xeno.elicitation@1";
837
+ var CONSENT_ELICITATION_RESULT_SCHEMA = "xeno.elicitationresult@1";
838
+ var agentManifest = {
839
+ id: AGENT_PANEL_ID,
840
+ version: "0.1.0",
841
+ title: "Agent",
842
+ icon: "bot",
843
+ description: "One agent surface for every XENO app: the composer, the lane and provider picker, and the session switcher \u2014 plus the translation from one session-event stream into the canonical panels that render the tool timeline, the log, the permission queue, the diff and the terminal. Holds no agent logic and runs nothing.",
844
+ defaultSlot: "right",
845
+ inputs: [
846
+ {
847
+ id: "session",
848
+ name: "Session Events",
849
+ type: "object",
850
+ schema: AGENT_SESSION_SCHEMA,
851
+ description: "A delta: {rev?, events?, replace?, clear?}. Events are session/turn/message/tool/ask/patch/terminal \u2014 see the package README for the shape and INTEGRATION.md for the host obligation. Fan-in: one wire per agent host.",
852
+ multiple: true
853
+ },
854
+ {
855
+ id: "catalog",
856
+ name: "Provider Catalog",
857
+ type: "object",
858
+ schema: AGENT_CATALOG_SCHEMA,
859
+ description: '{providers[], activeProviderId?, activeModelId?}. A provider carries a `lane` (cloud|local|acp) and an `unavailable` SENTENCE \u2014 never a boolean, because "why can I not pick this?" is the whole question a greyed-out row raises.',
860
+ multiple: false
861
+ },
862
+ {
863
+ id: "status",
864
+ name: "Connection",
865
+ type: "object",
866
+ description: '{status, message?} \u2014 idle | connecting | ready | failed, out of band, so a host never has to express "not connected" as an empty session list.',
867
+ multiple: false
868
+ },
869
+ {
870
+ id: "decision",
871
+ name: "Consent Decision",
872
+ type: "object",
873
+ schema: WELL_KNOWN_PORT_SCHEMAS.CONSENT_DECISION,
874
+ description: "The consent panel's `decision` output, routed back so it can be correlated to the session that asked. Forwarded VERBATIM \u2014 this panel never reads allow/deny, because interpreting it would make this a second place where a grant is decided.",
875
+ multiple: true
876
+ },
877
+ {
878
+ id: "result",
879
+ name: "Elicitation Result",
880
+ type: "object",
881
+ schema: CONSENT_ELICITATION_RESULT_SCHEMA,
882
+ description: "The consent panel's `result` output \u2014 a text/path/choice answer, or a cancellation carrying no value at all. Correlated and forwarded verbatim.",
883
+ multiple: true
884
+ },
885
+ {
886
+ id: "diffDecision",
887
+ name: "Hunk Decision",
888
+ type: "object",
889
+ schema: WELL_KNOWN_PORT_SCHEMAS.DIFF_DECISION_INTENT,
890
+ description: "The diff panel's `decision` output. \u{1F534} Dropped when its path belongs to no change set currently on screen \u2014 forwarding it would apply a review of one proposal to another.",
891
+ multiple: true
892
+ },
893
+ {
894
+ id: "runAction",
895
+ name: "Run Action",
896
+ type: "object",
897
+ description: "The runs panel's `action` output: {runId, action, stepId?, stepPath?}. The run id IS the turn id; the targeted tool call is `stepPath`'s last element.",
898
+ multiple: true
899
+ },
900
+ {
901
+ id: "terminalIntent",
902
+ name: "Terminal Intent",
903
+ type: "object",
904
+ schema: WELL_KNOWN_PORT_SCHEMAS.TERMINAL_INTENT,
905
+ description: "The terminal panel's `intent` output, correlated to the session that owns the instance and forwarded verbatim.",
906
+ multiple: true
907
+ }
908
+ ],
909
+ outputs: [
910
+ {
911
+ id: "intent",
912
+ name: "Intent",
913
+ type: "object",
914
+ schema: AGENT_INTENT_SCHEMA,
915
+ description: "The ONE thing the host acts on: prompt | cancel | steer | select-session | new-session | select-provider | answer | patch-decision | run-action | terminal. Every answer arriving from a canonical panel leaves here carrying the session id that panel could not know."
916
+ },
917
+ {
918
+ id: "runs",
919
+ name: "Runs Delta",
920
+ type: "object",
921
+ description: "For `xeno.core.runs` \u2192 `runs`. A turn is a RUN; a tool call is a STEP, nested by `parentCallId`, addressed by path. Progress travels as `patchSteps`, never as a re-push of the run."
922
+ },
923
+ {
924
+ id: "records",
925
+ name: "Log Delta",
926
+ type: "object",
927
+ description: "For `xeno.core.console` \u2192 `records`. A message opens with `append` and streams with `patch.appendMessage` \u2014 the update-by-id path a token-streamed reply has no other representation in."
928
+ },
929
+ {
930
+ id: "elicitations",
931
+ name: "Elicitations",
932
+ type: "object",
933
+ schema: CONSENT_ELICITATION_SCHEMA,
934
+ description: "For `xeno.core.consent` \u2192 `elicitations`. Forwarded verbatim; whether an ask can be rendered is that panel\u2019s decision, not this one\u2019s."
935
+ },
936
+ {
937
+ id: "withdraw",
938
+ name: "Withdraw",
939
+ type: "string",
940
+ description: "For `xeno.core.consent` \u2192 `withdraw`. An ask the agent no longer needs answered, so the queue is not left holding a question nobody is waiting on."
941
+ },
942
+ {
943
+ id: "diffModel",
944
+ name: "Diff Model",
945
+ type: "object",
946
+ schema: WELL_KNOWN_PORT_SCHEMAS.DIFF_MODEL,
947
+ description: 'For `xeno.core.diff` \u2192 `model`. Unified-diff text is parsed HERE (the panel ships no engine by charter). A file with no readable diff is reported `omitted: "unreadable"`, never as identical.'
948
+ },
949
+ {
950
+ id: "terminalData",
951
+ name: "Terminal Data",
952
+ type: "object",
953
+ schema: WELL_KNOWN_PORT_SCHEMAS.TERMINAL_DATA,
954
+ description: "For `xeno.core.terminal` \u2192 `data`. Raw bytes, escape sequences intact \u2014 never pre-parsed, because the emulator is the parser."
955
+ },
956
+ {
957
+ id: "terminalSession",
958
+ name: "Terminal Session",
959
+ type: "object",
960
+ description: "For `xeno.core.terminal` \u2192 `session`. Emitted BEFORE any data for that instance: a write into a terminal the panel has not been told about has nowhere to land."
961
+ }
962
+ ],
963
+ commands: [
964
+ {
965
+ id: "get_sessions",
966
+ title: "Get Sessions",
967
+ description: 'Return every session with its turn state, newest first, plus the connection status. The STATUS travels with the count so a caller cannot read 0 as "none" when the truth is "not connected".',
968
+ parameters: {}
969
+ },
970
+ {
971
+ id: "get_status",
972
+ title: "Get Status",
973
+ description: "Connection, active session, turn state, pending asks, and how many panel answers were dropped for want of a correlation.",
974
+ parameters: {}
975
+ },
976
+ {
977
+ id: "select_session",
978
+ title: "Select Session",
979
+ description: "Show another session. Returns the active session id, or null.",
980
+ parameters: { sessionId: { type: "string", description: "Session id", required: true } }
981
+ },
982
+ {
983
+ id: "cancel_turn",
984
+ title: "Cancel Turn",
985
+ description: "Ask the host to stop the active session\u2019s turn. Stopping only ever reduces what happens, which is why it is the one mutating verb here.",
986
+ parameters: {}
987
+ }
988
+ // 🔴 WHAT IS DELIBERATELY ABSENT, and why each one is a refusal rather than an omission:
989
+ //
990
+ // - **No `submit_prompt`.** An agent that can start turns can start them forever — an unbounded
991
+ // self-driving loop with a billing meter attached and no human decision point anywhere in it.
992
+ // Every other agent-facing verb in this catalog costs at most one wrong row; this one costs
993
+ // credits until somebody notices.
994
+ // - **No `set_draft`.** Writing into the human's composer is putting words in their mouth: the
995
+ // text sits where they expect their own to be, and the next keystroke sends it.
996
+ // - **No `answer_ask`.** An agent answering its own permission prompt is precisely what
997
+ // `xeno.core.consent` exists to prevent, and routing around that panel would defeat it just
998
+ // as thoroughly as editing it.
999
+ // - **No patch accept/reject.** Same reasoning as `xeno.core.diff`: the review step exists to
1000
+ // check the agent, so the agent must not be able to operate the thing that checks it.
1001
+ ],
1002
+ config: [
1003
+ {
1004
+ key: "submitOnEnter",
1005
+ label: "Enter sends",
1006
+ type: "boolean",
1007
+ defaultValue: true,
1008
+ description: "Enter submits and Shift+Enter inserts a newline. Off swaps the two."
1009
+ },
1010
+ {
1011
+ key: "showLane",
1012
+ label: "Show lane",
1013
+ type: "boolean",
1014
+ defaultValue: true,
1015
+ description: "Show whether the turn runs in the cloud, locally, or through ACP. Where the code and the credits go is not a detail."
1016
+ },
1017
+ {
1018
+ key: "showSessions",
1019
+ label: "Show session switcher",
1020
+ type: "boolean",
1021
+ defaultValue: true,
1022
+ description: "Hide it in a host that owns session selection in its own chrome."
1023
+ },
1024
+ {
1025
+ key: "emptyHint",
1026
+ label: "Empty hint",
1027
+ type: "text",
1028
+ description: "Say what would produce a session, not that there are none.",
1029
+ placeholder: "Start a session to talk to an agent."
1030
+ }
1031
+ ],
1032
+ capabilities: ["storage.local"],
1033
+ sdk: "^1.1.0"
1034
+ };
1035
+
1036
+ // src/agent/agent/react/AgentPanelView.tsx
1037
+ import { useSyncExternalStore } from "react";
1038
+ import {
1039
+ Badge,
1040
+ EmptyState,
1041
+ Row,
1042
+ RowList,
1043
+ ScrollArea,
1044
+ StatusBadge,
1045
+ StatusBar,
1046
+ TextButton,
1047
+ Toolbar,
1048
+ ToolbarGroup
1049
+ } from "@xenosystem/workbench/primitives/react";
1050
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1051
+ var LANE_LABEL = {
1052
+ cloud: "Cloud",
1053
+ local: "Local",
1054
+ acp: "ACP"
1055
+ };
1056
+ var TURN = {
1057
+ idle: { label: "Idle", tone: "neutral" },
1058
+ queued: { label: "Queued", tone: "neutral" },
1059
+ running: { label: "Working", tone: "info" },
1060
+ "waiting-for-user": { label: "Needs an answer", tone: "warning" },
1061
+ "waiting-for-permission": { label: "Needs permission", tone: "warning" },
1062
+ succeeded: { label: "Done", tone: "success" },
1063
+ failed: { label: "Failed", tone: "error" },
1064
+ cancelled: { label: "Cancelled", tone: "neutral" }
1065
+ };
1066
+ var COLUMN = { display: "flex", flexDirection: "column", height: "100%", minHeight: 0 };
1067
+ var COMPOSER = {
1068
+ width: "100%",
1069
+ minHeight: 64,
1070
+ resize: "vertical",
1071
+ background: "transparent",
1072
+ color: "inherit",
1073
+ border: "none",
1074
+ outline: "none",
1075
+ font: "inherit",
1076
+ padding: "8px 10px"
1077
+ };
1078
+ function AgentPanelView({
1079
+ controller,
1080
+ submitOnEnter = true,
1081
+ showLane = true,
1082
+ showSessions = true,
1083
+ emptyHint
1084
+ }) {
1085
+ const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
1086
+ if (state.connection === "failed") {
1087
+ return /* @__PURE__ */ jsx(ErrorPane, { message: state.message });
1088
+ }
1089
+ if (state.connection === "connecting") {
1090
+ return /* @__PURE__ */ jsx(EmptyState, { title: "Connecting\u2026", hint: "Waiting for the agent host." });
1091
+ }
1092
+ if (state.sessions.length === 0) {
1093
+ return /* @__PURE__ */ jsxs("div", { style: COLUMN, children: [
1094
+ /* @__PURE__ */ jsx(EmptyState, { title: "No sessions", hint: emptyHint ?? "Start a session to talk to an agent." }),
1095
+ /* @__PURE__ */ jsx(
1096
+ Toolbar,
1097
+ {
1098
+ right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx(TextButton, { onClick: () => controller.newSession(), children: "New session" }) })
1099
+ }
1100
+ )
1101
+ ] });
1102
+ }
1103
+ const turn = TURN[state.turn];
1104
+ const lane = state.activeProvider?.lane;
1105
+ return /* @__PURE__ */ jsxs("div", { style: COLUMN, children: [
1106
+ /* @__PURE__ */ jsx(
1107
+ Toolbar,
1108
+ {
1109
+ left: /* @__PURE__ */ jsxs(ToolbarGroup, { children: [
1110
+ /* @__PURE__ */ jsx(StatusBadge, { tone: turn.tone, children: turn.label }),
1111
+ showLane && lane ? /* @__PURE__ */ jsx(Badge, { children: LANE_LABEL[lane] ?? lane }) : null,
1112
+ state.pendingAsks > 0 ? /* @__PURE__ */ jsx(Badge, { children: `${state.pendingAsks} waiting` }) : null
1113
+ ] }),
1114
+ right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx(TextButton, { onClick: () => controller.newSession(), children: "New session" }) })
1115
+ }
1116
+ ),
1117
+ showSessions ? /* @__PURE__ */ jsx(ScrollArea, { children: /* @__PURE__ */ jsx(RowList, { children: state.sessions.map((session) => /* @__PURE__ */ jsx(
1118
+ Row,
1119
+ {
1120
+ noIcon: true,
1121
+ label: session.title ?? session.id,
1122
+ selected: session.id === state.active?.id,
1123
+ onClick: () => controller.selectSession(session.id),
1124
+ trailing: /* @__PURE__ */ jsx(Badge, { children: TURN[session.turn ?? "idle"].label })
1125
+ },
1126
+ session.id
1127
+ )) }) }) : null,
1128
+ state.providers.length > 1 ? /* @__PURE__ */ jsx(RowList, { children: state.providers.map((provider) => /* @__PURE__ */ jsx(
1129
+ Row,
1130
+ {
1131
+ noIcon: true,
1132
+ label: provider.label,
1133
+ meta: provider.unavailable ?? (showLane ? LANE_LABEL[provider.lane] ?? provider.lane : void 0),
1134
+ disabled: provider.unavailable !== void 0,
1135
+ selected: provider.id === state.activeProvider?.id,
1136
+ onClick: () => controller.selectProvider(provider.id)
1137
+ },
1138
+ provider.id
1139
+ )) }) : null,
1140
+ /* @__PURE__ */ jsx(
1141
+ "textarea",
1142
+ {
1143
+ style: COMPOSER,
1144
+ value: state.draft,
1145
+ placeholder: state.busy ? "Steer the running turn\u2026" : "Ask the agent\u2026",
1146
+ onChange: (event) => controller.setDraft(event.target.value),
1147
+ onKeyDown: (event) => {
1148
+ if (event.key !== "Enter") return;
1149
+ const wantsSubmit = submitOnEnter ? !event.shiftKey : event.shiftKey;
1150
+ if (!wantsSubmit) return;
1151
+ event.preventDefault();
1152
+ if (state.busy) controller.steer();
1153
+ else controller.submit();
1154
+ }
1155
+ }
1156
+ ),
1157
+ /* @__PURE__ */ jsx(
1158
+ StatusBar,
1159
+ {
1160
+ left: state.message ?? state.active?.cwd ?? void 0,
1161
+ right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: state.busy ? /* @__PURE__ */ jsxs(Fragment, { children: [
1162
+ /* @__PURE__ */ jsx(TextButton, { onClick: () => controller.steer(), children: "Steer" }),
1163
+ /* @__PURE__ */ jsx(TextButton, { onClick: () => controller.cancel(), children: "Stop" })
1164
+ ] }) : (
1165
+ // `disabled` derives from the SAME predicate `submit()` enforces, so an enabled
1166
+ // button can never be a button that refuses.
1167
+ /* @__PURE__ */ jsx(TextButton, { disabled: !state.canSubmit, onClick: () => controller.submit(), children: "Send" })
1168
+ ) })
1169
+ }
1170
+ )
1171
+ ] });
1172
+ }
1173
+ function ErrorPane({ message }) {
1174
+ return /* @__PURE__ */ jsx(EmptyState, { title: "Not connected", hint: message ?? "The agent host reported a failure." });
1175
+ }
1176
+
1177
+ // src/agent/agent/panel.ts
1178
+ var CONNECTIONS = ["idle", "connecting", "ready", "failed"];
1179
+ function createAgentPanel(options = {}) {
1180
+ return {
1181
+ manifest: agentManifest,
1182
+ activate(host) {
1183
+ const initial = host.config ?? {};
1184
+ const controller = new AgentController({
1185
+ host: { emit: (portId, value) => host.emit(portId, value) },
1186
+ now: options.now
1187
+ });
1188
+ const resolve = (config) => ({
1189
+ submitOnEnter: configBool(config, "submitOnEnter", true),
1190
+ showLane: configBool(config, "showLane", true),
1191
+ showSessions: configBool(config, "showSessions", true),
1192
+ emptyHint: configString(config, "emptyHint", "Start a session to talk to an agent.")
1193
+ });
1194
+ let renderConfig = resolve(initial);
1195
+ let unrender = null;
1196
+ let root = null;
1197
+ let currentEl = null;
1198
+ const draw = (el) => {
1199
+ unrender?.();
1200
+ if (options.render) {
1201
+ unrender = options.render(el, { controller, config: renderConfig });
1202
+ return;
1203
+ }
1204
+ root = createRoot(el);
1205
+ root.render(createElement(AgentPanelView, { controller, ...renderConfig }));
1206
+ unrender = () => {
1207
+ root?.unmount();
1208
+ root = null;
1209
+ };
1210
+ };
1211
+ const unbindConfig = bindConfig(host, (config) => {
1212
+ renderConfig = resolve(config);
1213
+ if (currentEl) draw(currentEl);
1214
+ });
1215
+ return {
1216
+ render(el) {
1217
+ currentEl = el;
1218
+ draw(el);
1219
+ },
1220
+ onInput(portId, value) {
1221
+ switch (portId) {
1222
+ case "session": {
1223
+ if (!isRecord(value)) return;
1224
+ if (value.events !== void 0 && !Array.isArray(value.events)) return;
1225
+ if (value.replace !== void 0 && !Array.isArray(value.replace)) return;
1226
+ controller.ingest(value);
1227
+ return;
1228
+ }
1229
+ case "catalog": {
1230
+ if (!isRecord(value) || !Array.isArray(value.providers)) return;
1231
+ controller.setCatalog(value);
1232
+ return;
1233
+ }
1234
+ case "status": {
1235
+ if (!isRecord(value)) return;
1236
+ const status = value.status;
1237
+ if (typeof status !== "string" || !CONNECTIONS.includes(status)) return;
1238
+ controller.setStatus(
1239
+ status,
1240
+ typeof value.message === "string" ? value.message : void 0
1241
+ );
1242
+ return;
1243
+ }
1244
+ // The four return paths. Each guards inside the controller and DROPS an answer it
1245
+ // cannot correlate — a decision routed to a guessed session applies a human's approval
1246
+ // to work they never saw.
1247
+ case "decision":
1248
+ case "result":
1249
+ controller.answer(value);
1250
+ return;
1251
+ case "diffDecision":
1252
+ controller.patchDecision(value);
1253
+ return;
1254
+ case "runAction":
1255
+ controller.runAction(value);
1256
+ return;
1257
+ case "terminalIntent":
1258
+ controller.terminalIntent(value);
1259
+ return;
1260
+ default:
1261
+ return;
1262
+ }
1263
+ },
1264
+ async onCommand(commandId, params) {
1265
+ const state = controller.getState();
1266
+ switch (commandId) {
1267
+ case "get_sessions": {
1268
+ return {
1269
+ connection: state.connection,
1270
+ count: state.sessions.length,
1271
+ sessions: state.sessions.map((session) => ({
1272
+ id: session.id,
1273
+ title: session.title ?? null,
1274
+ providerId: session.providerId ?? null,
1275
+ lane: session.lane ?? null,
1276
+ turn: session.turn ?? "idle",
1277
+ active: session.id === state.active?.id
1278
+ }))
1279
+ };
1280
+ }
1281
+ case "get_status": {
1282
+ return {
1283
+ connection: state.connection,
1284
+ message: state.message,
1285
+ activeSessionId: state.active?.id ?? null,
1286
+ turn: state.turn,
1287
+ busy: state.busy,
1288
+ pendingAsks: state.pendingAsks,
1289
+ // Surfaced rather than swallowed: a mis-wired return path and a dead one look
1290
+ // identical from the outside, and this is the one number that tells them apart.
1291
+ droppedAnswers: controller.droppedAnswers
1292
+ };
1293
+ }
1294
+ case "select_session": {
1295
+ if (typeof params.sessionId !== "string") return null;
1296
+ controller.selectSession(params.sessionId);
1297
+ return controller.getState().active?.id ?? null;
1298
+ }
1299
+ case "cancel_turn": {
1300
+ return controller.cancel();
1301
+ }
1302
+ default:
1303
+ return;
1304
+ }
1305
+ },
1306
+ serialize() {
1307
+ return controller.serialize();
1308
+ },
1309
+ deserialize(state) {
1310
+ controller.deserialize(state);
1311
+ },
1312
+ dispose() {
1313
+ unbindConfig();
1314
+ currentEl = null;
1315
+ unrender?.();
1316
+ unrender = null;
1317
+ controller.dispose();
1318
+ }
1319
+ };
1320
+ }
1321
+ };
1322
+ }
1323
+ var agentPanel = createAgentPanel();
1324
+
1325
+ // src/agent/agent/wiring.ts
1326
+ var AGENT_WIRING_TARGETS = {
1327
+ RUNS: "xeno.core.runs",
1328
+ CONSOLE: "xeno.core.console",
1329
+ CONSENT: "xeno.core.consent",
1330
+ DIFF: "xeno.core.diff",
1331
+ TERMINAL: "xeno.core.terminal"
1332
+ };
1333
+ var AGENT = AGENT_PANEL_ID;
1334
+ var { RUNS, CONSOLE, CONSENT, DIFF, TERMINAL } = AGENT_WIRING_TARGETS;
1335
+ var AGENT_PANEL_WIRING = Object.freeze([
1336
+ // ── The tool timeline ──────────────────────────────────────────────────
1337
+ {
1338
+ from: { panel: AGENT, port: "runs" },
1339
+ to: { panel: RUNS, port: "runs" },
1340
+ why: "Turns become runs and tool calls become nested steps. Without it there is no timeline at all \u2014 the panel that can render a subagent three levels down never receives one.",
1341
+ optional: true
1342
+ },
1343
+ {
1344
+ from: { panel: RUNS, port: "action" },
1345
+ to: { panel: AGENT, port: "runAction" },
1346
+ why: "Cancel and Retry on a run or one of its steps. Without it those buttons emit into nothing and read as a broken agent rather than a missing wire.",
1347
+ optional: true
1348
+ },
1349
+ // ── The transcript ─────────────────────────────────────────────────────
1350
+ {
1351
+ from: { panel: AGENT, port: "records" },
1352
+ to: { panel: CONSOLE, port: "records" },
1353
+ why: "The conversation itself, streamed by update-by-id. Without it the agent replies into nothing visible.",
1354
+ optional: true
1355
+ },
1356
+ // ── Permissions and questions ──────────────────────────────────────────
1357
+ {
1358
+ from: { panel: AGENT, port: "elicitations" },
1359
+ to: { panel: CONSENT, port: "elicitations" },
1360
+ why: "Every permission the agent needs, and every question it asks. Without it a turn that stops on a grant looks like a hung agent.",
1361
+ optional: true
1362
+ },
1363
+ {
1364
+ from: { panel: AGENT, port: "withdraw" },
1365
+ to: { panel: CONSENT, port: "withdraw" },
1366
+ why: "An ask the agent no longer needs. Without it the queue keeps a question nobody is waiting on, in front of the ones that matter.",
1367
+ optional: true
1368
+ },
1369
+ {
1370
+ from: { panel: CONSENT, port: "decision" },
1371
+ to: { panel: AGENT, port: "decision" },
1372
+ why: "Permission answers. \u{1F534} Without it the agent never learns it was allowed, and the user watches an Allow they already pressed change nothing.",
1373
+ optional: true
1374
+ },
1375
+ {
1376
+ from: { panel: CONSENT, port: "result" },
1377
+ to: { panel: AGENT, port: "result" },
1378
+ why: "Text, path and choice answers. A SECOND port because a permission and an answer partition \u2014 wiring only `decision` silently drops every question the agent asked.",
1379
+ optional: true
1380
+ },
1381
+ // ── Review ─────────────────────────────────────────────────────────────
1382
+ {
1383
+ from: { panel: AGENT, port: "diffModel" },
1384
+ to: { panel: DIFF, port: "model" },
1385
+ why: "Proposed edits, parsed from unified diff here because the diff panel ships no engine by charter.",
1386
+ optional: true
1387
+ },
1388
+ {
1389
+ from: { panel: DIFF, port: "decision" },
1390
+ to: { panel: AGENT, port: "diffDecision" },
1391
+ why: "Accept and reject, correlated back to the change set that produced them. \u26A0\uFE0F Only meaningful with the diff panel\u2019s `review` config on; it is off by default.",
1392
+ optional: true
1393
+ },
1394
+ // ── The terminal ───────────────────────────────────────────────────────
1395
+ {
1396
+ from: { panel: AGENT, port: "terminalSession" },
1397
+ to: { panel: TERMINAL, port: "session" },
1398
+ why: "Instance lifecycle. Must be wired for `terminalData` to land anywhere \u2014 the panel drops a write for an instance it has not been told about.",
1399
+ optional: true
1400
+ },
1401
+ {
1402
+ from: { panel: AGENT, port: "terminalData" },
1403
+ to: { panel: TERMINAL, port: "data" },
1404
+ why: "The agent\u2019s PTY output, escape sequences intact.",
1405
+ optional: true
1406
+ },
1407
+ {
1408
+ from: { panel: TERMINAL, port: "intent" },
1409
+ to: { panel: AGENT, port: "terminalIntent" },
1410
+ why: "What the HUMAN types, plus resize and stop. Without it the terminal renders and swallows every keystroke.",
1411
+ optional: true
1412
+ }
1413
+ ]);
1414
+ function wiringFor(panelId) {
1415
+ return AGENT_PANEL_WIRING.filter((w) => w.from.panel === panelId || w.to.panel === panelId);
1416
+ }
1417
+ export {
1418
+ AGENT_CATALOG_SCHEMA,
1419
+ AGENT_FANOUT_PORTS,
1420
+ AGENT_INTENT_SCHEMA,
1421
+ AGENT_PANEL_ID,
1422
+ AGENT_PANEL_WIRING,
1423
+ AGENT_SESSION_SCHEMA,
1424
+ AGENT_WIRING_TARGETS,
1425
+ AgentController,
1426
+ AgentPanelView,
1427
+ AgentProjector,
1428
+ CONSENT_ELICITATION_RESULT_SCHEMA,
1429
+ CONSENT_ELICITATION_SCHEMA,
1430
+ agentManifest,
1431
+ agentPanel,
1432
+ countChanges,
1433
+ createAgentPanel,
1434
+ isBusy,
1435
+ isWaitingOnUser,
1436
+ parseUnifiedDiff,
1437
+ turnStatusToRunStatus,
1438
+ wiringFor
1439
+ };