@genesislcap/ai-assistant 15.61.0 → 15.63.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.
Files changed (137) hide show
  1. package/dist/ai-assistant.api.json +18231 -11171
  2. package/dist/ai-assistant.d.ts +1235 -9
  3. package/dist/chat-driver.cjs +658 -50
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +657 -50
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +4759 -717
  8. package/dist/dts/autopilot/autopilot-driver.d.ts +278 -0
  9. package/dist/dts/autopilot/autopilot-driver.d.ts.map +1 -0
  10. package/dist/dts/autopilot/autopilot-session.d.ts +163 -0
  11. package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -0
  12. package/dist/dts/autopilot/autopilot-tools.d.ts +15 -0
  13. package/dist/dts/autopilot/autopilot-tools.d.ts.map +1 -0
  14. package/dist/dts/autopilot/decision-message.d.ts +62 -0
  15. package/dist/dts/autopilot/decision-message.d.ts.map +1 -0
  16. package/dist/dts/autopilot/hand-back/hand-back.controller.d.ts +69 -0
  17. package/dist/dts/autopilot/hand-back/hand-back.controller.d.ts.map +1 -0
  18. package/dist/dts/autopilot/hand-back/hand-back.d.ts +22 -0
  19. package/dist/dts/autopilot/hand-back/hand-back.d.ts.map +1 -0
  20. package/dist/dts/autopilot/index.d.ts +4 -0
  21. package/dist/dts/autopilot/index.d.ts.map +1 -0
  22. package/dist/dts/channel/ai-activity-bus.d.ts.map +1 -1
  23. package/dist/dts/channel/ai-activity-channel.d.ts +17 -0
  24. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  25. package/dist/dts/channel/autopilot-needs-user.d.ts +8 -0
  26. package/dist/dts/channel/autopilot-needs-user.d.ts.map +1 -0
  27. package/dist/dts/components/agent-picker/agent-picker.constants.d.ts +10 -0
  28. package/dist/dts/components/agent-picker/agent-picker.constants.d.ts.map +1 -1
  29. package/dist/dts/components/agent-picker/agent-picker.d.ts +4 -0
  30. package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -1
  31. package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -1
  32. package/dist/dts/components/ai-driver/ai-driver.d.ts +34 -1
  33. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  34. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +3 -1
  35. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
  36. package/dist/dts/components/chat-bubble/chat-bubble.d.ts +21 -0
  37. package/dist/dts/components/chat-bubble/chat-bubble.d.ts.map +1 -1
  38. package/dist/dts/components/chat-bubble/chat-bubble.styles.d.ts.map +1 -1
  39. package/dist/dts/components/chat-bubble/chat-bubble.template.d.ts.map +1 -1
  40. package/dist/dts/components/chat-driver/chat-driver.d.ts +5 -1
  41. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  42. package/dist/dts/components/chat-driver/interaction-controller.d.ts +27 -3
  43. package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
  44. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +10 -2
  45. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
  46. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -1
  47. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  48. package/dist/dts/config/config.d.ts +183 -0
  49. package/dist/dts/config/config.d.ts.map +1 -1
  50. package/dist/dts/config/define-stateful-agent.d.ts +31 -1
  51. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
  52. package/dist/dts/genesis/write-commit.d.ts +17 -7
  53. package/dist/dts/genesis/write-commit.d.ts.map +1 -1
  54. package/dist/dts/index.d.ts +7 -3
  55. package/dist/dts/index.d.ts.map +1 -1
  56. package/dist/dts/main/main.d.ts +128 -5
  57. package/dist/dts/main/main.d.ts.map +1 -1
  58. package/dist/dts/main/main.styles.d.ts.map +1 -1
  59. package/dist/dts/main/main.template.d.ts +2 -2
  60. package/dist/dts/main/main.template.d.ts.map +1 -1
  61. package/dist/dts/react.d.ts +10 -0
  62. package/dist/dts/state/debug-event-log.d.ts +1 -1
  63. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  64. package/dist/dts/state/decision-surface.d.ts +46 -0
  65. package/dist/dts/state/decision-surface.d.ts.map +1 -0
  66. package/dist/dts/state/interaction-context.d.ts +21 -1
  67. package/dist/dts/state/interaction-context.d.ts.map +1 -1
  68. package/dist/dts/state/interaction-registry.d.ts +63 -0
  69. package/dist/dts/state/interaction-registry.d.ts.map +1 -0
  70. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  71. package/dist/dts/state/persistence/session-snapshot.d.ts +21 -0
  72. package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
  73. package/dist/dts/state/run-steps.d.ts +66 -0
  74. package/dist/dts/state/run-steps.d.ts.map +1 -0
  75. package/dist/dts/types/ai-chat-widget.d.ts +7 -1
  76. package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
  77. package/dist/dts/types/interaction-context.d.ts +66 -1
  78. package/dist/dts/types/interaction-context.d.ts.map +1 -1
  79. package/dist/dts/types/interaction-interceptor.d.ts +60 -2
  80. package/dist/dts/types/interaction-interceptor.d.ts.map +1 -1
  81. package/dist/dts/types/interaction-surface.d.ts +195 -0
  82. package/dist/dts/types/interaction-surface.d.ts.map +1 -0
  83. package/dist/dts/utils/context-tokens.d.ts +6 -0
  84. package/dist/dts/utils/context-tokens.d.ts.map +1 -1
  85. package/dist/dts/utils/delegated-message.d.ts +13 -0
  86. package/dist/dts/utils/delegated-message.d.ts.map +1 -1
  87. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  88. package/dist/dts/utils/preserved-user-messages.d.ts +21 -0
  89. package/dist/dts/utils/preserved-user-messages.d.ts.map +1 -0
  90. package/dist/esm/autopilot/autopilot-driver.js +906 -0
  91. package/dist/esm/autopilot/autopilot-session.js +449 -0
  92. package/dist/esm/autopilot/autopilot-tools.js +68 -0
  93. package/dist/esm/autopilot/decision-message.js +206 -0
  94. package/dist/esm/autopilot/hand-back/hand-back.controller.js +47 -0
  95. package/dist/esm/autopilot/hand-back/hand-back.js +129 -0
  96. package/dist/esm/autopilot/index.js +4 -0
  97. package/dist/esm/channel/ai-activity-bus.js +7 -0
  98. package/dist/esm/channel/autopilot-needs-user.js +26 -0
  99. package/dist/esm/components/agent-picker/agent-picker.constants.js +10 -0
  100. package/dist/esm/components/agent-picker/agent-picker.js +11 -1
  101. package/dist/esm/components/agent-picker/agent-picker.template.js +2 -4
  102. package/dist/esm/components/ai-driver/ai-driver.js +2 -0
  103. package/dist/esm/components/ai-driver/base-driver-decorator.js +8 -0
  104. package/dist/esm/components/chat-bubble/chat-bubble.js +45 -0
  105. package/dist/esm/components/chat-bubble/chat-bubble.styles.js +26 -0
  106. package/dist/esm/components/chat-bubble/chat-bubble.template.js +13 -0
  107. package/dist/esm/components/chat-driver/chat-driver.js +53 -18
  108. package/dist/esm/components/chat-driver/interaction-controller.js +297 -12
  109. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +16 -2
  110. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +28 -11
  111. package/dist/esm/config/define-stateful-agent.js +34 -29
  112. package/dist/esm/genesis/write-outcome.js +2 -2
  113. package/dist/esm/index.js +3 -0
  114. package/dist/esm/main/main.js +334 -15
  115. package/dist/esm/main/main.styles.js +126 -0
  116. package/dist/esm/main/main.template.js +116 -11
  117. package/dist/esm/state/debug-event-log.js +28 -1
  118. package/dist/esm/state/decision-surface.js +209 -0
  119. package/dist/esm/state/interaction-context.js +89 -1
  120. package/dist/esm/state/interaction-registry.js +33 -0
  121. package/dist/esm/state/persistence/session-persister.js +23 -11
  122. package/dist/esm/state/persistence/session-snapshot.js +1 -1
  123. package/dist/esm/state/run-steps.js +77 -0
  124. package/dist/esm/types/interaction-surface.js +1 -0
  125. package/dist/esm/utils/context-tokens.js +23 -9
  126. package/dist/esm/utils/delegated-message.js +15 -0
  127. package/dist/esm/utils/history-transform.js +18 -7
  128. package/dist/esm/utils/preserved-user-messages.js +31 -0
  129. package/dist/react.cjs +7 -0
  130. package/dist/react.mjs +6 -0
  131. package/docs/styling.md +1 -1
  132. package/package.json +17 -17
  133. package/sandbox/README.md +38 -2
  134. package/sandbox/autopilot.ts +358 -0
  135. package/sandbox/controls.ts +24 -1
  136. package/sandbox/fixtures.ts +2 -0
  137. package/sandbox/sandbox.ts +188 -1
@@ -0,0 +1,358 @@
1
+ /**
2
+ * The autopilot UI in the sandbox (GENC-1660, Part 1 UI track). See `README.md`, "Checking the
3
+ * autopilot UI".
4
+ *
5
+ * Two halves, both driving the element through its ordinary inputs:
6
+ *
7
+ * - `SandboxAutopilotHost` stands in for `AutopilotDriver` behind the slim bar, the banner and the
8
+ * badge. Its buttons really move it: Pause pauses, Resume resumes, Keep paused keeps it paused.
9
+ * - The demo widget (`sandbox-confirm`) is a real registered widget with a DOM-free controller,
10
+ * opened through the driver's `presentInteraction` and claimed by a real interceptor, so `heldBy`
11
+ * moves through a real `InteractionHold` and the element decides what to show for itself.
12
+ */
13
+ import type { ChatMessage } from '@genesislcap/foundation-ai';
14
+ import { customElement, GenesisElement, html, observable, when } from '@genesislcap/web-core';
15
+ import type { AutopilotHost, AutopilotHostState } from '../src/autopilot/autopilot-driver';
16
+ import type { AiDriver } from '../src/components/ai-driver/ai-driver';
17
+ import type { AutopilotCopy } from '../src/config/config';
18
+ import { registerInteraction } from '../src/state/interaction-registry';
19
+ import type { AiChatInteraction } from '../src/types/ai-chat-widget';
20
+ import type { InteractionContext } from '../src/types/interaction-context';
21
+ import type { InteractionHold } from '../src/types/interaction-interceptor';
22
+
23
+ // ─── The stub host ───────────────────────────────────────────────────────────
24
+
25
+ /** What the panel sets. `off` is no autopilot at all: the element shows exactly what it does today. */
26
+ export type SandboxAutopilotMode = 'off' | 'running' | 'paused' | 'self-paused';
27
+
28
+ const COPY: AutopilotCopy = {
29
+ running: 'Building from your plan…',
30
+ paused: 'Paused',
31
+ selfPaused: "Waiting for your answer: I'll carry on after this",
32
+ handBackLead: 'I need your input on this one:',
33
+ toggleLabel: 'Autopilot',
34
+ description: "Answers the assistant's questions on your behalf, so the work keeps going.",
35
+ pauseLabel: 'Pause autopilot',
36
+ resumeLabel: 'Resume autopilot',
37
+ resumeDecideLabel: 'Resume autopilot and force it to decide this one',
38
+ keepPausedLabel: 'Keep paused',
39
+ markLabel: 'Done in autopilot mode',
40
+ checkInMessage: "I've done a lot in one go, so I've stopped to check in. Shall I carry on?",
41
+ carryOnLabel: 'Carry on',
42
+ tooLongMessage: 'This conversation has grown too long for me to carry on as it is.',
43
+ compactLabel: 'Compact and carry on',
44
+ failedMessage: 'Something went wrong on that last step.',
45
+ tryAgainLabel: 'Try again',
46
+ sendLabel: 'Send',
47
+ budgetReason: 'This one is taking more working out than I expected, so over to you.',
48
+ errorReason: 'Something went wrong while I was deciding this one.',
49
+ noDecisionReason: "I couldn't settle this one.",
50
+ watchdogReason: 'This is taking longer than expected, so I need you to look at it.',
51
+ };
52
+
53
+ const HAND_BACK_REASON = "your brief doesn't say whether quantities can be decimals.";
54
+
55
+ /**
56
+ * An `AutopilotHost` whose mode the panel sets and whose buttons move it, as the real one's do.
57
+ * `onChange` tells the panel, so its controls follow a click on the slim bar.
58
+ */
59
+ export class SandboxAutopilotHost implements AutopilotHost {
60
+ readonly copy = COPY;
61
+ private readonly listeners = new Set<() => void>();
62
+
63
+ constructor(
64
+ private mode: SandboxAutopilotMode,
65
+ private status: string,
66
+ private needsUser: boolean,
67
+ /** The pending demo widget's id, which a self-pause hands back. */
68
+ private interactionId: () => string | undefined,
69
+ private readonly onChange: (mode: SandboxAutopilotMode) => void,
70
+ /**
71
+ * Whether something is happening, as the real driver reports it: derived by the panel from the
72
+ * simulated state and the demo widget. Running and idle, the composer comes back.
73
+ */
74
+ private readonly working: () => boolean,
75
+ ) {}
76
+
77
+ get state(): AutopilotHostState {
78
+ const selfPaused = this.mode === 'self-paused';
79
+ // A self-pause is the autopilot's own, for one interaction: it is still running.
80
+ const mode = (
81
+ {
82
+ off: 'unavailable',
83
+ running: 'running',
84
+ paused: 'paused',
85
+ 'self-paused': 'running',
86
+ } as const
87
+ )[this.mode];
88
+ const status =
89
+ this.status ||
90
+ (selfPaused ? COPY.selfPaused : this.mode === 'running' ? COPY.running : COPY.paused);
91
+ return {
92
+ mode,
93
+ ...(selfPaused
94
+ ? {
95
+ pause: {
96
+ scope: 'this-interaction' as const,
97
+ interactionId: this.interactionId(),
98
+ reason: HAND_BACK_REASON,
99
+ },
100
+ }
101
+ : this.mode === 'paused'
102
+ ? { pause: { scope: 'until-resumed' as const } }
103
+ : {}),
104
+ status: this.mode === 'off' ? '' : status,
105
+ needsUser: this.needsUser || selfPaused,
106
+ working: this.mode !== 'off' && this.working(),
107
+ };
108
+ }
109
+
110
+ /** The panel moved: take its values and tell the element. */
111
+ update(mode: SandboxAutopilotMode, status: string, needsUser: boolean): void {
112
+ this.mode = mode;
113
+ this.status = status;
114
+ this.needsUser = needsUser;
115
+ this.notify();
116
+ }
117
+
118
+ subscribe(listener: () => void): () => void {
119
+ this.listeners.add(listener);
120
+ return () => {
121
+ this.listeners.delete(listener);
122
+ };
123
+ }
124
+
125
+ pause(): void {
126
+ this.move('paused');
127
+ }
128
+
129
+ resume(): void {
130
+ this.move('running');
131
+ }
132
+
133
+ stop(): void {
134
+ this.move('paused');
135
+ }
136
+
137
+ keepPaused(): void {
138
+ this.move('paused');
139
+ }
140
+
141
+ private move(mode: SandboxAutopilotMode): void {
142
+ if (this.mode === 'off') return;
143
+ this.mode = mode;
144
+ this.onChange(mode);
145
+ this.notify();
146
+ }
147
+
148
+ notify(): void {
149
+ for (const fn of this.listeners) fn();
150
+ }
151
+ }
152
+
153
+ // ─── The demo widget: Appendix A's confirm button ────────────────────────────
154
+
155
+ export const DEMO_WIDGET = 'sandbox-confirm';
156
+
157
+ interface ConfirmData {
158
+ question: string;
159
+ label: string;
160
+ }
161
+
162
+ /** DOM-free: the button and a claimant both reach `press()`. */
163
+ class ConfirmController {
164
+ constructor(
165
+ readonly data: ConfirmData,
166
+ private readonly ctx: InteractionContext,
167
+ ) {
168
+ ctx.exposeDecision({ options: [data.label], choose: () => this.press() });
169
+ }
170
+
171
+ press(): void {
172
+ this.ctx.resolve({ status: 'approved', payload: { pressed: true } });
173
+ }
174
+ }
175
+
176
+ /** Register the demo widget, with how it behaves while a claimant holds it. */
177
+ export const registerDemoWidget = (whileAutopilot: 'visible' | 'hidden'): void => {
178
+ registerInteraction(DEMO_WIDGET, {
179
+ controller: (data, ctx) => new ConfirmController(data as ConfirmData, ctx),
180
+ whileAutopilot,
181
+ asText: (data, result) => {
182
+ const d = data as ConfirmData;
183
+ return result ? `${d.question} → ${d.label}` : d.question;
184
+ },
185
+ });
186
+ };
187
+
188
+ const template = html<SandboxConfirm>`
189
+ <p class="question">${(x) => x.data?.question}</p>
190
+ ${when(
191
+ (x) => x.interaction?.status === 'resolved',
192
+ html<SandboxConfirm>`
193
+ <span>✓ ${(x) => x.data?.label}</span>
194
+ `,
195
+ )}
196
+ ${when(
197
+ (x) => x.interaction?.status === 'pending',
198
+ html<SandboxConfirm>`
199
+ <rapid-button
200
+ appearance="accent"
201
+ ?hidden=${(x) => x.claimantOwns}
202
+ @click=${(x) => {
203
+ x.controller?.press();
204
+ return true;
205
+ }}
206
+ >
207
+ ${(x) => x.data?.label}
208
+ </rapid-button>
209
+ ${when(
210
+ (x) => x.claimantOwns,
211
+ html<SandboxConfirm>`
212
+ <small class="held">Held by ${(x) => x.heldBy}: its button is the claimant's.</small>
213
+ `,
214
+ )}
215
+ `,
216
+ )}
217
+ `;
218
+
219
+ /**
220
+ * The view. It renders from `heldBy` (through `claimantOwns`, so a button the controller did not
221
+ * expose is never hidden) and calls the controller; it never resolves anything itself.
222
+ */
223
+ @customElement({
224
+ name: DEMO_WIDGET,
225
+ template,
226
+ styles: `
227
+ :host { display: block; }
228
+ .question { margin: 0 0 8px; }
229
+ .held { display: block; opacity: 0.7; font-style: italic; }
230
+ `,
231
+ })
232
+ export class SandboxConfirm extends GenesisElement {
233
+ interactionId = '';
234
+ @observable interaction?: AiChatInteraction<ConfirmData>;
235
+ @observable heldBy = 'user';
236
+ @observable claimantOwns = false;
237
+ private unsubscribe?: () => void;
238
+
239
+ get data(): ConfirmData | undefined {
240
+ return this.interaction?.data;
241
+ }
242
+
243
+ get controller(): ConfirmController | undefined {
244
+ return this.interaction?.status === 'pending'
245
+ ? (this.interaction.interactionContext.controller as ConfirmController)
246
+ : undefined;
247
+ }
248
+
249
+ interactionChanged(): void {
250
+ this.unsubscribe?.();
251
+ this.unsubscribe = undefined;
252
+ if (this.interaction?.status !== 'pending') return;
253
+ const ctx = this.interaction.interactionContext;
254
+ const read = () => {
255
+ this.heldBy = ctx.heldBy;
256
+ this.claimantOwns = ctx.claimantOwns(this.data?.label ?? '');
257
+ };
258
+ read();
259
+ this.unsubscribe = ctx.subscribe(read);
260
+ }
261
+
262
+ disconnectedCallback(): void {
263
+ this.unsubscribe?.();
264
+ super.disconnectedCallback();
265
+ }
266
+ }
267
+
268
+ // ─── Claiming it through a real interceptor ──────────────────────────────────
269
+
270
+ /** The id the sandbox's interceptor claims under: the same one the real autopilot uses. */
271
+ const CLAIMANT = 'autopilot';
272
+
273
+ /**
274
+ * Install an interceptor on `driver` that claims the demo widget when `claim()` says so, and keeps
275
+ * its hold so the panel can hand it to the person and back. Returns the remover and the hold.
276
+ */
277
+ export const installDemoClaimant = (
278
+ driver: AiDriver,
279
+ claim: () => boolean,
280
+ /** Told when the demo widget opens or closes, as the real autopilot notifies its host. */
281
+ onChange: () => void,
282
+ ): {
283
+ remove: () => void;
284
+ hold: () => InteractionHold | undefined;
285
+ pendingId: () => string | undefined;
286
+ } => {
287
+ let hold: InteractionHold | undefined;
288
+ let pendingId: string | undefined;
289
+ const remove =
290
+ driver.addInteractionInterceptor?.({
291
+ id: CLAIMANT,
292
+ onInteractionOpened: (opened) => {
293
+ if (opened.componentName !== DEMO_WIDGET) return undefined;
294
+ hold = opened.hold;
295
+ pendingId = opened.interactionId;
296
+ opened.ctx.onDispose(() => {
297
+ if (pendingId === opened.interactionId) {
298
+ hold = undefined;
299
+ pendingId = undefined;
300
+ onChange();
301
+ }
302
+ });
303
+ // After the claim lands, so the host reads the widget's id and holder as they now are.
304
+ queueMicrotask(onChange);
305
+ return claim() ? 'claim' : undefined;
306
+ },
307
+ }) ?? (() => undefined);
308
+ return { remove, hold: () => hold, pendingId: () => pendingId };
309
+ };
310
+
311
+ /** Open the demo widget, as a working agent's tool would ask. */
312
+ export const openDemoWidget = (driver: AiDriver): void => {
313
+ void driver
314
+ .presentInteraction?.(DEMO_WIDGET, { question: 'Deploy the app now?', label: 'Deploy' })
315
+ .catch(() => undefined);
316
+ };
317
+
318
+ // ─── Delegated messages, for the Transcript seed ─────────────────────────────
319
+
320
+ /**
321
+ * Each kind of delegated message: narration before a widget the autopilot answered, the "I chose"
322
+ * echo, and a written answer given on the user's behalf. All render as the assistant, with the mark.
323
+ * Unstamped: the fixtures stamp it like every other seed.
324
+ */
325
+ export const autopilotTranscript = (): ChatMessage[] => [
326
+ { role: 'user', content: 'Build me a trade blotter from my plan.' },
327
+ {
328
+ role: 'assistant',
329
+ content: "Your brief has no external feeds, so I'll skip Data Feeds.",
330
+ delegate: 'autopilot',
331
+ excludeFromProvider: true,
332
+ },
333
+ {
334
+ role: 'assistant',
335
+ content: '',
336
+ interaction: {
337
+ interactionId: 'sandbox-answered',
338
+ componentName: DEMO_WIDGET,
339
+ data: { question: 'Add data feeds?', label: 'No, skip this step' },
340
+ resolved: { status: 'approved', payload: { pressed: true } },
341
+ answeredBy: 'autopilot',
342
+ },
343
+ },
344
+ {
345
+ role: 'assistant',
346
+ content: 'I chose: Approve trade, Reject trade',
347
+ delegate: 'autopilot',
348
+ excludeFromProvider: true,
349
+ },
350
+ { role: 'assistant', content: 'Which currency should notional amounts use?' },
351
+ {
352
+ role: 'user',
353
+ content: 'GBP — the brief prices everything in sterling.',
354
+ delegate: 'autopilot',
355
+ delegateFraming: "(Answered on the user's behalf, from their brief.)",
356
+ },
357
+ { role: 'assistant', content: 'Using GBP for every notional field.' },
358
+ ];
@@ -59,6 +59,28 @@ const el = <K extends keyof HTMLElementTagNameMap>(
59
59
  return node;
60
60
  };
61
61
 
62
+ /**
63
+ * Which groups are open, by title, so a reload keeps the panel as it was left. Separate from the
64
+ * sandbox state: it is panel chrome, not something the assistant is driven by.
65
+ */
66
+ export const GROUPS_OPEN_KEY = 'ai-assistant-sandbox:groups-open';
67
+
68
+ const readGroupsOpen = (): Record<string, boolean> => {
69
+ try {
70
+ return JSON.parse(localStorage.getItem(GROUPS_OPEN_KEY) ?? '{}') ?? {};
71
+ } catch {
72
+ return {};
73
+ }
74
+ };
75
+
76
+ const writeGroupOpen = (title: string, open: boolean): void => {
77
+ try {
78
+ localStorage.setItem(GROUPS_OPEN_KEY, JSON.stringify({ ...readGroupsOpen(), [title]: open }));
79
+ } catch {
80
+ // Storage unavailable: the panel still works, it just forgets.
81
+ }
82
+ };
83
+
62
84
  const withHint = (row: HTMLElement, hint?: string): HTMLElement => {
63
85
  if (hint) {
64
86
  const small = el('small', 'hint');
@@ -98,7 +120,8 @@ export function renderControls<S extends Record<string, any>>(
98
120
 
99
121
  for (const group of groups) {
100
122
  const details = el('details', 'group');
101
- details.open = group.open ?? true;
123
+ details.open = readGroupsOpen()[group.title] ?? group.open ?? true;
124
+ details.addEventListener('toggle', () => writeGroupOpen(group.title, details.open));
102
125
  const summary = el('summary');
103
126
  summary.textContent = group.title;
104
127
  details.appendChild(summary);
@@ -11,6 +11,7 @@ import type {
11
11
  ChatRequestOptions,
12
12
  } from '@genesislcap/foundation-ai';
13
13
  import type { AgentConfig } from '../src/config/config';
14
+ import { autopilotTranscript } from './autopilot';
14
15
 
15
16
  /** The stub ignores it; it exists only to keep the driver's warning out of the console. */
16
17
  const STUB_PROMPT = 'Sandbox stub. Replies are canned.';
@@ -328,6 +329,7 @@ export const TRANSCRIPTS = {
328
329
  huge: HUGE,
329
330
  loop: LOOP,
330
331
  wall: WALL,
332
+ autopilot: stamped(autopilotTranscript()),
331
333
  } as const;
332
334
 
333
335
  export type TranscriptName = keyof typeof TRANSCRIPTS;
@@ -14,7 +14,14 @@ import type { FoundationAiAssistant } from '../src/main/main';
14
14
  import type { PopoutMode } from '../src/main/main.types';
15
15
  import { WebStorageSessionProvider } from '../src/state/persistence';
16
16
  import { estimateContextTokens } from '../src/utils/context-tokens';
17
- import { renderControls, type ControlGroup } from './controls';
17
+ import {
18
+ installDemoClaimant,
19
+ openDemoWidget,
20
+ registerDemoWidget,
21
+ SandboxAutopilotHost,
22
+ type SandboxAutopilotMode,
23
+ } from './autopilot';
24
+ import { GROUPS_OPEN_KEY, renderControls, type ControlGroup } from './controls';
18
25
  import {
19
26
  buildAgents,
20
27
  buildAttachmentChips,
@@ -151,6 +158,15 @@ interface SandboxState {
151
158
  budgetMeter: 'off' | 'single' | 'segmented' | 'segmented-remainder';
152
159
  // Simulated runtime
153
160
  runtime: 'idle' | 'loading' | 'cancelling' | 'blocked' | 'compacting' | 'restoring';
161
+ // Autopilot (GENC-1660)
162
+ autopilotMode: SandboxAutopilotMode;
163
+ autopilotHidesComposer: boolean;
164
+ autopilotStatus: string;
165
+ autopilotNeedsUser: boolean;
166
+ demoHeldBy: 'user' | 'autopilot';
167
+ demoWhileAutopilot: 'visible' | 'hidden';
168
+ delegateMarker: 'subtle' | 'none';
169
+ showBubble: boolean;
154
170
  transcript: TranscriptName;
155
171
  transcriptAge: TranscriptAge;
156
172
  // Stage
@@ -198,6 +214,14 @@ const DEFAULT_STATE: SandboxState = {
198
214
  flowInProgress: false,
199
215
  budgetMeter: 'off',
200
216
  runtime: 'idle',
217
+ autopilotMode: 'off',
218
+ autopilotHidesComposer: true,
219
+ autopilotStatus: '',
220
+ autopilotNeedsUser: false,
221
+ demoHeldBy: 'autopilot',
222
+ demoWhileAutopilot: 'hidden',
223
+ delegateMarker: 'subtle',
224
+ showBubble: false,
201
225
  transcript: 'rich',
202
226
  transcriptAge: 'fresh',
203
227
  width: 480,
@@ -353,6 +377,7 @@ const buildChatConfig = (s: SandboxState): ChatConfig => ({
353
377
  // `undefined` disables the feature outright (no cog control, nothing enabled);
354
378
  // `userConfigurable` turns the defaults on and adds the multiselect to the cog.
355
379
  animations: s.animations ? { userConfigurable: true } : undefined,
380
+ delegatedMessages: { marker: s.delegateMarker },
356
381
  messageFeedback:
357
382
  s.messageFeedback === 'off'
358
383
  ? undefined
@@ -550,6 +575,78 @@ const applyRuntime = (s: SandboxState): void => {
550
575
  assistant.showLoadingIndicator = s.runtime === 'loading' || s.runtime === 'cancelling';
551
576
  };
552
577
 
578
+ // ─── Autopilot (GENC-1660) ───────────────────────────────────────────────────
579
+ //
580
+ // The stub host goes on as `autopilotHostOverride`, the element's ordinary input for one; the demo
581
+ // widget is claimed by a real interceptor on the element's own driver. See `autopilot.ts`.
582
+
583
+ let autopilotHost: SandboxAutopilotHost | undefined;
584
+ let demoClaimant: ReturnType<typeof installDemoClaimant> | undefined;
585
+ let demoDriver: unknown;
586
+ let demoRegisteredAs: string | undefined;
587
+ let bubble: HTMLElement | undefined;
588
+
589
+ /** The id a per-call override is filed under, as an agent's tool would file its own. */
590
+ const SANDBOX_OVERRIDE = 'sandbox-autopilot';
591
+
592
+ const applyAutopilot = (s: SandboxState): void => {
593
+ if (demoRegisteredAs !== s.demoWhileAutopilot) {
594
+ registerDemoWidget(s.demoWhileAutopilot);
595
+ demoRegisteredAs = s.demoWhileAutopilot;
596
+ }
597
+ autopilotHost ??= new SandboxAutopilotHost(
598
+ s.autopilotMode,
599
+ s.autopilotStatus,
600
+ s.autopilotNeedsUser,
601
+ () => demoClaimant?.pendingId(),
602
+ // A click on the slim bar or the banner moves the panel too.
603
+ (mode) => {
604
+ state.autopilotMode = mode;
605
+ saveState(state);
606
+ refreshReadouts();
607
+ },
608
+ // Working, as the real driver says it: the simulated state is Busy, or the demo widget is open
609
+ // and the autopilot holds it (deciding it).
610
+ () =>
611
+ state.runtime === 'loading' ||
612
+ state.runtime === 'cancelling' ||
613
+ (state.demoHeldBy === 'autopilot' && !!demoClaimant?.pendingId()),
614
+ );
615
+ autopilotHost.update(s.autopilotMode, s.autopilotStatus, s.autopilotNeedsUser);
616
+ const host = s.autopilotMode === 'off' ? undefined : autopilotHost;
617
+ if (assistant.autopilotHostOverride !== host) assistant.autopilotHostOverride = host;
618
+
619
+ // The agent's own rule hiding the composer while it works — bootstrap's
620
+ // `chatInputDuringExecution: 'hidden'` — as the per-call override a tool would file.
621
+ const actions = (assistant as any)._sessionRef?.actions.aiAssistant;
622
+ actions?.removeInputOverride({ id: SANDBOX_OVERRIDE });
623
+ if (s.autopilotHidesComposer) actions?.addInputOverride({ id: SANDBOX_OVERRIDE, mode: 'hidden' });
624
+
625
+ // Re-installed whenever the element's driver changes (a remount, or new agents).
626
+ const driver = (assistant as any).driver;
627
+ if (driver !== demoDriver) {
628
+ demoClaimant?.remove();
629
+ demoClaimant = driver
630
+ ? installDemoClaimant(
631
+ driver,
632
+ () => state.demoHeldBy === 'autopilot',
633
+ () => autopilotHost?.notify(),
634
+ )
635
+ : undefined;
636
+ demoDriver = driver;
637
+ }
638
+ demoClaimant?.hold()?.set(s.demoHeldBy === 'autopilot' ? 'claimant' : 'user');
639
+
640
+ // The collapsed assistant, for the "needs you" badge.
641
+ if (s.showBubble && !bubble) {
642
+ bubble = document.createElement('foundation-ai-chat-bubble');
643
+ document.body.appendChild(bubble);
644
+ } else if (!s.showBubble && bubble) {
645
+ bubble.remove();
646
+ bubble = undefined;
647
+ }
648
+ };
649
+
553
650
  // FAST's `baseLayerLuminance` drives the whole neutral ramp. These are
554
651
  // `StandardLuminance.LightMode` / `DarkMode` from `@microsoft/fast-components`,
555
652
  // inlined so the sandbox doesn't reach past the design system for two numbers.
@@ -610,6 +707,7 @@ function apply(): void {
610
707
  applyContext(state);
611
708
  applyBudgetMeter(state);
612
709
  applyRuntime(state);
710
+ applyAutopilot(state);
613
711
  applyGradientSend(state.gradientSend);
614
712
  applyDivider(state.showDivider);
615
713
  assistant.attachments = buildAttachmentChips(state.chips);
@@ -910,6 +1008,93 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
910
1008
  },
911
1009
  ],
912
1010
  },
1011
+ {
1012
+ title: 'Autopilot',
1013
+ open: false,
1014
+ controls: [
1015
+ {
1016
+ kind: 'select',
1017
+ key: 'autopilotMode',
1018
+ label: 'Mode',
1019
+ featured: true,
1020
+ hint: 'Unavailable is the UI as it is today. Self-paused hands the demo widget back, with its banner.',
1021
+ options: [
1022
+ { value: 'off', label: 'Unavailable (no autopilot)' },
1023
+ { value: 'running', label: 'Running' },
1024
+ { value: 'paused', label: 'Paused' },
1025
+ { value: 'self-paused', label: 'Self-paused (handed one back)' },
1026
+ ],
1027
+ },
1028
+ {
1029
+ kind: 'toggle',
1030
+ key: 'autopilotHidesComposer',
1031
+ label: 'Agent hides the composer while working',
1032
+ hint: "Bootstrap's chatInputDuringExecution: 'hidden'. Only while busy: set Simulated state to Busy. Paused then shows the slim bar; off, it shows the composer with the Autopilot pill.",
1033
+ },
1034
+ {
1035
+ kind: 'text',
1036
+ key: 'autopilotStatus',
1037
+ label: 'Status text',
1038
+ placeholder: 'From the copy',
1039
+ },
1040
+ {
1041
+ kind: 'toggle',
1042
+ key: 'autopilotNeedsUser',
1043
+ label: 'Needs you',
1044
+ hint: 'Drives the badge on the collapsed bubble.',
1045
+ },
1046
+ {
1047
+ kind: 'toggle',
1048
+ key: 'showBubble',
1049
+ label: 'Show the collapsed bubble',
1050
+ hint: 'The floating orb, bottom right, where the badge appears.',
1051
+ },
1052
+ {
1053
+ kind: 'action',
1054
+ label: 'Open the demo widget',
1055
+ hint: 'A confirm button with a controller, as a tool would ask. One at a time.',
1056
+ run: () => openDemoWidget((assistant as any).driver),
1057
+ },
1058
+ {
1059
+ kind: 'select',
1060
+ key: 'demoHeldBy',
1061
+ label: 'Demo widget held by',
1062
+ hint: 'Moved through the real hold, so the element decides what to show.',
1063
+ options: [
1064
+ { value: 'autopilot', label: 'The autopilot' },
1065
+ { value: 'user', label: 'The person' },
1066
+ ],
1067
+ },
1068
+ {
1069
+ kind: 'select',
1070
+ key: 'demoWhileAutopilot',
1071
+ label: 'Demo widget while held',
1072
+ hint: 'Its registration: hidden drops the whole bubble, visible hides only its button.',
1073
+ options: [
1074
+ { value: 'hidden', label: 'Hidden' },
1075
+ { value: 'visible', label: 'Visible' },
1076
+ ],
1077
+ },
1078
+ {
1079
+ kind: 'select',
1080
+ key: 'delegateMarker',
1081
+ label: 'Delegated mark',
1082
+ hint: 'Seed the Autopilot transcript to see each kind of delegated message.',
1083
+ options: [
1084
+ { value: 'subtle', label: 'Subtle' },
1085
+ { value: 'none', label: 'None' },
1086
+ ],
1087
+ },
1088
+ {
1089
+ kind: 'readout',
1090
+ label: 'Element',
1091
+ render: () =>
1092
+ assistant
1093
+ ? `held by ${assistant.activeHeldBy} · halo ${assistant.showHalo} · slim bar ${assistant.slimBarVisible ? 'on' : 'off'}`
1094
+ : '—',
1095
+ },
1096
+ ],
1097
+ },
913
1098
  {
914
1099
  title: 'Transcript',
915
1100
  controls: [
@@ -922,6 +1107,7 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
922
1107
  { value: 'short', label: 'Short' },
923
1108
  { value: 'rich', label: 'Tools, markdown, agent switch' },
924
1109
  { value: 'long', label: 'Long (28 turns)' },
1110
+ { value: 'autopilot', label: 'Autopilot: delegated messages' },
925
1111
  { value: 'huge', label: 'Huge (~20k tokens)' },
926
1112
  { value: 'loop', label: 'Tool loop (~22k, no user turns inside)' },
927
1113
  { value: 'wall', label: 'Wall (~24k in one indivisible tool result)' },
@@ -1040,6 +1226,7 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
1040
1226
  label: 'Reset sandbox',
1041
1227
  run: () => {
1042
1228
  localStorage.removeItem(STORAGE_KEY);
1229
+ localStorage.removeItem(GROUPS_OPEN_KEY);
1043
1230
  sessionStorage.clear();
1044
1231
  location.reload();
1045
1232
  },