@genesislcap/ai-assistant 15.59.0 → 15.61.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 (27) hide show
  1. package/dist/ai-assistant.d.ts +17 -0
  2. package/dist/custom-elements.json +646 -248
  3. package/dist/dts/components/message-feedback/index.d.ts +2 -0
  4. package/dist/dts/components/message-feedback/index.d.ts.map +1 -0
  5. package/dist/dts/components/message-feedback/message-feedback.d.ts +61 -0
  6. package/dist/dts/components/message-feedback/message-feedback.d.ts.map +1 -0
  7. package/dist/dts/components/message-feedback/message-feedback.styles.d.ts +2 -0
  8. package/dist/dts/components/message-feedback/message-feedback.styles.d.ts.map +1 -0
  9. package/dist/dts/components/message-feedback/message-feedback.template.d.ts +4 -0
  10. package/dist/dts/components/message-feedback/message-feedback.template.d.ts.map +1 -0
  11. package/dist/dts/main/main.d.ts +17 -1
  12. package/dist/dts/main/main.d.ts.map +1 -1
  13. package/dist/dts/main/main.styles.d.ts.map +1 -1
  14. package/dist/dts/main/main.template.d.ts.map +1 -1
  15. package/dist/dts/react.d.ts +10 -0
  16. package/dist/esm/components/message-feedback/index.js +1 -0
  17. package/dist/esm/components/message-feedback/message-feedback.js +241 -0
  18. package/dist/esm/components/message-feedback/message-feedback.styles.js +94 -0
  19. package/dist/esm/components/message-feedback/message-feedback.template.js +80 -0
  20. package/dist/esm/main/main.js +44 -1
  21. package/dist/esm/main/main.styles.js +27 -0
  22. package/dist/esm/main/main.template.js +13 -0
  23. package/dist/react.cjs +7 -0
  24. package/dist/react.mjs +6 -0
  25. package/package.json +17 -17
  26. package/sandbox/README.md +1 -1
  27. package/sandbox/sandbox.ts +35 -0
@@ -0,0 +1,80 @@
1
+ import { html, ref, when } from '@genesislcap/web-core';
2
+ import { DEBUG_LOG_NOTE_DEFAULT_MAX_LENGTH } from '../../utils/resolve-debug-log-note';
3
+ const thumbLabel = {
4
+ positive: 'Good response',
5
+ negative: 'Bad response',
6
+ };
7
+ const panelTitle = {
8
+ positive: 'What went well?',
9
+ negative: 'What went wrong?',
10
+ };
11
+ export const MessageFeedbackTemplate = (designSystemPrefix) => {
12
+ const buttonTag = `${designSystemPrefix}-button`;
13
+ const textareaTag = `${designSystemPrefix}-text-area`;
14
+ const iconTag = `${designSystemPrefix}-icon`;
15
+ const thumb = (rating) => html `
16
+ <button
17
+ type="button"
18
+ class="thumb"
19
+ part="message-feedback-button message-feedback-${rating}"
20
+ aria-label="${thumbLabel[rating]}"
21
+ title="${(x) => x.sentRating === rating && !x.open ? 'Thanks for your feedback' : thumbLabel[rating]}"
22
+ aria-pressed="${(x) => String(x.activeRating === rating)}"
23
+ ?disabled="${(x) => x.status === 'sending'}"
24
+ @click="${(x) => x.toggle(rating)}"
25
+ >
26
+ <${iconTag}
27
+ name="${rating === 'positive' ? 'thumbs-up' : 'thumbs-down'}"
28
+ variant="${(x) => (x.activeRating === rating ? 'solid' : 'regular')}"
29
+ ></${iconTag}>
30
+ </button>
31
+ `;
32
+ return html `
33
+ <div class="root" @keydown="${(x, c) => x.handleKeydown(c.event)}">
34
+ <div class="thumbs" part="message-feedback-thumbs">
35
+ ${thumb('positive')}${thumb('negative')}
36
+ </div>
37
+ <span class="visually-hidden" role="status" aria-live="polite">${(x) => x.announcement}</span>
38
+ ${when((x) => x.open && x.draftRating !== null, html `
39
+ <div class="panel" part="message-feedback-panel" role="group" aria-label="Send feedback">
40
+ <p class="panel-title" part="message-feedback-title">
41
+ ${(x) => panelTitle[x.draftRating]}
42
+ </p>
43
+ <${textareaTag}
44
+ ${ref('noteEl')}
45
+ class="note"
46
+ part="message-feedback-note"
47
+ rows="3"
48
+ resize="vertical"
49
+ aria-label="Feedback note (optional)"
50
+ placeholder="Add a note (optional)"
51
+ maxlength="${() => DEBUG_LOG_NOTE_DEFAULT_MAX_LENGTH}"
52
+ :value="${(x) => x.note}"
53
+ ?disabled="${(x) => x.status === 'sending'}"
54
+ @input="${(x, c) => x.noteInput(c.event.target.value)}"
55
+ ></${textareaTag}>
56
+ <p class="hint ${(x) => (x.status === 'error' ? 'is-error' : '')}" part="message-feedback-hint">
57
+ ${(x) => x.status === 'error' ? "Couldn't send your feedback. Try again." : x.description}
58
+ </p>
59
+ <div class="actions">
60
+ <${buttonTag}
61
+ part="message-feedback-cancel-button"
62
+ appearance="outline"
63
+ @click="${(x) => x.cancel()}"
64
+ >
65
+ Cancel
66
+ </${buttonTag}>
67
+ <${buttonTag}
68
+ part="message-feedback-send-button"
69
+ appearance="accent"
70
+ ?disabled="${(x) => x.status === 'sending'}"
71
+ @click="${(x) => x.send()}"
72
+ >
73
+ ${(x) => x.status === 'sending' ? 'Sending…' : x.status === 'error' ? 'Retry' : 'Send'}
74
+ </${buttonTag}>
75
+ </div>
76
+ </div>
77
+ `)}
78
+ </div>
79
+ `;
80
+ };
@@ -36,6 +36,7 @@ import { AiChatInteractionWrapper } from '../components/chat-interaction-wrapper
36
36
  import { AiChatMarkdown } from '../components/chat-markdown/chat-markdown';
37
37
  import { AiFlowingWavesIndicator } from '../components/flowing-waves-indicator';
38
38
  import { AiHaloOverlay } from '../components/halo-overlay';
39
+ import { isRateableReply, MessageFeedback, replyKey, } from '../components/message-feedback/message-feedback';
39
40
  import { asOrchestrator, OrchestratingDriver, } from '../components/orchestrating-driver/orchestrating-driver';
40
41
  import { AiPlasmaOrbIndicator } from '../components/plasma-orb-indicator';
41
42
  import { SettingsModalController, } from '../components/settings-modal';
@@ -165,7 +166,7 @@ const TIMESTAMP_CLOCK_INTERVAL_MS = 30000;
165
166
  */
166
167
  export { budgetPercentOf, formatBlockedReason } from './budget-controller';
167
168
  // Register supporting components when the main component module is imported.
168
- avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiWavesIndicator, AiFlowingWavesIndicator, AiPlasmaOrbIndicator, AiChatBubble, AiActivityHalo, ChatSuggestions, AgentPicker);
169
+ avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiWavesIndicator, AiFlowingWavesIndicator, AiPlasmaOrbIndicator, AiChatBubble, AiActivityHalo, ChatSuggestions, AgentPicker, MessageFeedback);
169
170
  /**
170
171
  * Foundation AI Assistant component.
171
172
  *
@@ -3681,6 +3682,48 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3681
3682
  const stem = this.debugLogAgentName.toLowerCase().replace(/\s+/g, '-');
3682
3683
  return `${stem}-${new Date().toISOString().replace(/:/g, '-')}.json`;
3683
3684
  }
3685
+ /**
3686
+ * The most recent rateable reply, whose thumbs stay visible without a hover.
3687
+ *
3688
+ * @internal
3689
+ */
3690
+ get latestReply() {
3691
+ for (let i = this.messages.length - 1; i >= 0; i -= 1) {
3692
+ if (isRateableReply(this.messages[i]))
3693
+ return this.messages[i];
3694
+ }
3695
+ return undefined;
3696
+ }
3697
+ /** @internal */
3698
+ get messageFeedbackDescription() {
3699
+ var _a, _b, _c;
3700
+ return ((_c = (_b = (_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.messageFeedback) === null || _b === void 0 ? void 0 : _b.description) !== null && _c !== void 0 ? _c : 'Sends your rating and note on this reply.');
3701
+ }
3702
+ /**
3703
+ * Hand a rated reply to the host's `ui.messageFeedback.send`, with a getter for the same
3704
+ * lifetime log the agent-log Send button submits — built only if the host asks for it.
3705
+ * Rejects when the host does, or does not settle in time.
3706
+ *
3707
+ * @internal
3708
+ */
3709
+ submitMessageFeedback(reply, rating, note) {
3710
+ return __awaiter(this, void 0, void 0, function* () {
3711
+ var _a, _b, _c;
3712
+ const config = (_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.messageFeedback;
3713
+ if (!config)
3714
+ throw new Error('ui.messageFeedback is not configured');
3715
+ yield settleWithin(config.send({
3716
+ rating,
3717
+ reply,
3718
+ replyKey: replyKey(reply),
3719
+ message: note,
3720
+ filename: this.debugLogFilename(),
3721
+ agentName: (_b = reply.agentName) !== null && _b !== void 0 ? _b : this.debugLogAgentName,
3722
+ host: window.location.host,
3723
+ getLogJson: () => __awaiter(this, void 0, void 0, function* () { return JSON.stringify(yield this.getLifetimeDebugLog(), null, 2); }),
3724
+ }), (_c = config.timeoutMs) !== null && _c !== void 0 ? _c : DEBUG_LOG_SUBMIT_TIMEOUT_MS, 'messageFeedback.send');
3725
+ });
3726
+ }
3684
3727
  /**
3685
3728
  * Back to idle: nothing in flight, no flash, no open compose box, no note. Called on
3686
3729
  * disconnect and on a session switch / Clear (a note typed against one conversation
@@ -459,6 +459,33 @@ const baseStyles = css `
459
459
  }
460
460
  }
461
461
 
462
+ /* Revealed like the 'hover' timestamps, with space reserved so the bubble never reflows.
463
+ Always shown on the latest reply, once rated, and while the note panel is open. */
464
+ .message-feedback {
465
+ opacity: 0%;
466
+ transition: opacity 0.12s ease-out;
467
+ }
468
+
469
+ .message-row:hover .message-feedback,
470
+ .message-row:focus-within .message-feedback,
471
+ .message-feedback[latest],
472
+ .message-feedback[open],
473
+ .message-feedback[rated] {
474
+ opacity: 100%;
475
+ }
476
+
477
+ @media (hover: none) {
478
+ .message-feedback {
479
+ opacity: 100%;
480
+ }
481
+ }
482
+
483
+ @media (prefers-reduced-motion: reduce) {
484
+ .message-feedback {
485
+ transition: none;
486
+ }
487
+ }
488
+
462
489
  .payload {
463
490
  font-family: monospace;
464
491
  white-space: pre-wrap;
@@ -17,6 +17,7 @@
17
17
  import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
18
18
  import { classNames, html, ref, repeat, slotted, when } from '@genesislcap/web-core';
19
19
  import { MARKDOWN_EXPORTPARTS } from '../components/chat-markdown/chat-markdown';
20
+ import { isRateableReply, MESSAGE_FEEDBACK_EXPORTPARTS, } from '../components/message-feedback/message-feedback';
20
21
  import { SettingsModalTemplate } from '../components/settings-modal';
21
22
  import { SUGGESTIONS_EXPORTPARTS } from '../suggestions/chat-suggestions';
22
23
  import { formatAbsoluteTimestamp, formatMessageTimestamp, messageTimestampDateTime, } from '../utils/format-message-timestamp';
@@ -651,6 +652,18 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
651
652
  `)}
652
653
  ${when((m) => m.interaction, interactionWrapperTemplate)}
653
654
  </div>
655
+ ${when((m, c) => { var _a; return isRateableReply(m) && ((_a = c.parent.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.messageFeedback) != null; }, html `
656
+ <foundation-ai-message-feedback
657
+ class="message-feedback"
658
+ part="message-feedback"
659
+ exportparts="${MESSAGE_FEEDBACK_EXPORTPARTS}"
660
+ ?latest="${(m, c) => m === c.parent.latestReply}"
661
+ :designSystemPrefix="${(_, c) => c.parent.designSystemPrefix}"
662
+ :description="${(_, c) => c.parent.messageFeedbackDescription}"
663
+ :submit="${(_, c) => c.parent.submitMessageFeedback.bind(c.parent)}"
664
+ :message="${(m) => m}"
665
+ ></foundation-ai-message-feedback>
666
+ `)}
654
667
  </div>
655
668
  </div>
656
669
  `)}
package/dist/react.cjs CHANGED
@@ -13,6 +13,7 @@ const { AiChatInteractionWrapper: AiChatInteractionWrapperWC } = require('./esm/
13
13
  const { AiChatMarkdown: AiChatMarkdownWC } = require('./esm/components/chat-markdown/chat-markdown.js');
14
14
  const { AiFlowingWavesIndicator: AiFlowingWavesIndicatorWC } = require('./esm/components/flowing-waves-indicator.js');
15
15
  const { AiHaloOverlay: AiHaloOverlayWC } = require('./esm/components/halo-overlay.js');
16
+ const { MessageFeedback: MessageFeedbackWC } = require('./esm/components/message-feedback/message-feedback.js');
16
17
  const { AiPlasmaOrbIndicator: AiPlasmaOrbIndicatorWC } = require('./esm/components/plasma-orb-indicator.js');
17
18
  const { FoundationAiPopoutManager: FoundationAiPopoutManagerWC } = require('./esm/components/popout-manager/popout-manager.js');
18
19
  const { AiWavesIndicator: AiWavesIndicatorWC } = require('./esm/components/waves-indicator.js');
@@ -140,6 +141,11 @@ const AiChatMarkdown = React.forwardRef(function AiChatMarkdown(props, ref) {
140
141
  return React.createElement(customElements.getName(AiChatMarkdownWC) ?? 'ai-chat-markdown', { ...rest, ref }, children);
141
142
  });
142
143
 
144
+ const MessageFeedback = React.forwardRef(function MessageFeedback(props, ref) {
145
+ const { children, ...rest } = props;
146
+ return React.createElement(customElements.getName(MessageFeedbackWC) ?? 'foundation-ai-message-feedback', { ...rest, ref }, children);
147
+ });
148
+
143
149
  const FoundationAiPopoutManager = React.forwardRef(function FoundationAiPopoutManager(props, ref) {
144
150
  const { children, ...rest } = props;
145
151
  return React.createElement(customElements.getName(FoundationAiPopoutManagerWC) ?? 'foundation-ai-popout-manager', { ...rest, ref }, children);
@@ -174,6 +180,7 @@ module.exports = {
174
180
  AiChatBubble,
175
181
  AiChatInteractionWrapper,
176
182
  AiChatMarkdown,
183
+ MessageFeedback,
177
184
  FoundationAiPopoutManager,
178
185
  GenesisWriteReview,
179
186
  };
package/dist/react.mjs CHANGED
@@ -11,6 +11,7 @@ import { AiChatInteractionWrapper as AiChatInteractionWrapperWC } from './esm/co
11
11
  import { AiChatMarkdown as AiChatMarkdownWC } from './esm/components/chat-markdown/chat-markdown.js';
12
12
  import { AiFlowingWavesIndicator as AiFlowingWavesIndicatorWC } from './esm/components/flowing-waves-indicator.js';
13
13
  import { AiHaloOverlay as AiHaloOverlayWC } from './esm/components/halo-overlay.js';
14
+ import { MessageFeedback as MessageFeedbackWC } from './esm/components/message-feedback/message-feedback.js';
14
15
  import { AiPlasmaOrbIndicator as AiPlasmaOrbIndicatorWC } from './esm/components/plasma-orb-indicator.js';
15
16
  import { FoundationAiPopoutManager as FoundationAiPopoutManagerWC } from './esm/components/popout-manager/popout-manager.js';
16
17
  import { AiWavesIndicator as AiWavesIndicatorWC } from './esm/components/waves-indicator.js';
@@ -138,6 +139,11 @@ export const AiChatMarkdown = React.forwardRef(function AiChatMarkdown(props, re
138
139
  return React.createElement(customElements.getName(AiChatMarkdownWC) ?? 'ai-chat-markdown', { ...rest, ref }, children);
139
140
  });
140
141
 
142
+ export const MessageFeedback = React.forwardRef(function MessageFeedback(props, ref) {
143
+ const { children, ...rest } = props;
144
+ return React.createElement(customElements.getName(MessageFeedbackWC) ?? 'foundation-ai-message-feedback', { ...rest, ref }, children);
145
+ });
146
+
141
147
  export const FoundationAiPopoutManager = React.forwardRef(function FoundationAiPopoutManager(props, ref) {
142
148
  const { children, ...rest } = props;
143
149
  return React.createElement(customElements.getName(FoundationAiPopoutManagerWC) ?? 'foundation-ai-popout-manager', { ...rest, ref }, children);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "15.59.0",
4
+ "version": "15.61.0",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -92,31 +92,31 @@
92
92
  }
93
93
  },
94
94
  "devDependencies": {
95
- "@genesislcap/foundation-comms": "15.59.0",
96
- "@genesislcap/foundation-testing": "15.59.0",
97
- "@genesislcap/genx": "15.59.0",
98
- "@genesislcap/ts-builder": "15.59.0",
99
- "@genesislcap/uvu-playwright-builder": "15.59.0",
100
- "@genesislcap/vite-builder": "15.59.0",
101
- "@genesislcap/webpack-builder": "15.59.0",
95
+ "@genesislcap/foundation-comms": "15.61.0",
96
+ "@genesislcap/foundation-testing": "15.61.0",
97
+ "@genesislcap/genx": "15.61.0",
98
+ "@genesislcap/ts-builder": "15.61.0",
99
+ "@genesislcap/uvu-playwright-builder": "15.61.0",
100
+ "@genesislcap/vite-builder": "15.61.0",
101
+ "@genesislcap/webpack-builder": "15.61.0",
102
102
  "@types/dompurify": "^3.0.5",
103
103
  "@types/marked": "^5.0.2",
104
104
  "esbuild": "0.25.12"
105
105
  },
106
106
  "dependencies": {
107
- "@genesislcap/foundation-ai": "15.59.0",
108
- "@genesislcap/foundation-logger": "15.59.0",
109
- "@genesislcap/foundation-notifications": "15.59.0",
110
- "@genesislcap/foundation-redux": "15.59.0",
111
- "@genesislcap/foundation-ui": "15.59.0",
112
- "@genesislcap/foundation-utils": "15.59.0",
113
- "@genesislcap/rapid-design-system": "15.59.0",
114
- "@genesislcap/web-core": "15.59.0",
107
+ "@genesislcap/foundation-ai": "15.61.0",
108
+ "@genesislcap/foundation-logger": "15.61.0",
109
+ "@genesislcap/foundation-notifications": "15.61.0",
110
+ "@genesislcap/foundation-redux": "15.61.0",
111
+ "@genesislcap/foundation-ui": "15.61.0",
112
+ "@genesislcap/foundation-utils": "15.61.0",
113
+ "@genesislcap/rapid-design-system": "15.61.0",
114
+ "@genesislcap/web-core": "15.61.0",
115
115
  "dompurify": "^3.3.1",
116
116
  "marked": "^17.0.3"
117
117
  },
118
118
  "peerDependencies": {
119
- "@genesislcap/foundation-comms": "^15.59.0"
119
+ "@genesislcap/foundation-comms": "^15.61.0"
120
120
  },
121
121
  "peerDependenciesMeta": {
122
122
  "@genesislcap/foundation-comms": {
package/sandbox/README.md CHANGED
@@ -31,7 +31,7 @@ The panel covers, roughly in the order you'll want them:
31
31
  | **Context window** | **Jump to state** (the four gate states in one click), the simulated window and fill, `chatConfig.context`, a flow-in-progress toggle, live readouts of the gate / thresholds / compaction plan, and Compact |
32
32
  | **Simulated state** | Forces `idle` / busy / cancelling / blocked / compacting / restoring onto the element |
33
33
  | **Transcript** | Seeds an empty, short, tool-and-markdown-heavy, 28-turn, ~20k-token, or ~22k-token tool-loop conversation, and how far back to shift its timestamps |
34
- | **Chrome** | Header title, popout mode, suggestions, the tool-call / reasoning / narration toggles, the timestamp mode, animations |
34
+ | **Chrome** | Header title, popout mode, suggestions, the tool-call / reasoning / narration toggles, the timestamp mode, animations, per-reply thumbs (`messageFeedback`, with a stub send that succeeds or fails) |
35
35
  | **Stage** | Frame size (floored at 400px — see below), light/dark, remount, full reset |
36
36
 
37
37
  Panel state persists in `localStorage`, so a reload picks up where you left off. **Reset sandbox**
@@ -137,6 +137,7 @@ interface SandboxState {
137
137
  showNarration: boolean;
138
138
  showTimestamps: TimestampDisplay;
139
139
  animations: boolean;
140
+ messageFeedback: 'off' | 'succeeds' | 'fails';
140
141
  // Context window (GENC-1567)
141
142
  contextPreset: ContextPresetName | typeof CUSTOM_PRESET;
142
143
  contextWindow: number;
@@ -182,6 +183,7 @@ const DEFAULT_STATE: SandboxState = {
182
183
  showNarration: true,
183
184
  showTimestamps: 'always',
184
185
  animations: true,
186
+ messageFeedback: 'succeeds',
185
187
  // Chosen so the `huge` (~20k) and `loop` (~22k) seeds actually clear the tail
186
188
  // budget these produce (~12.8k) and so have something to compact. Verified, not
187
189
  // guessed — a slightly roomier pairing leaves the tail larger than either seed,
@@ -334,6 +336,9 @@ const ACCEPTED_FILES: Record<SandboxState['attachments'], string | undefined> =
334
336
  images: 'image/png,image/jpeg,image/gif,image/webp,.txt,.md,.csv,.json,text/*',
335
337
  };
336
338
 
339
+ /** Long enough to see the thumbs' Sending… state. */
340
+ const STUB_FEEDBACK_SEND_MS = 600;
341
+
337
342
  const buildChatConfig = (s: SandboxState): ChatConfig => ({
338
343
  ui: {
339
344
  acceptedFiles: ACCEPTED_FILES[s.attachments],
@@ -348,6 +353,25 @@ const buildChatConfig = (s: SandboxState): ChatConfig => ({
348
353
  // `undefined` disables the feature outright (no cog control, nothing enabled);
349
354
  // `userConfigurable` turns the defaults on and adds the multiselect to the cog.
350
355
  animations: s.animations ? { userConfigurable: true } : undefined,
356
+ messageFeedback:
357
+ s.messageFeedback === 'off'
358
+ ? undefined
359
+ : {
360
+ send: async ({ rating, reply, replyKey, message, filename, getLogJson }) => {
361
+ await new Promise((resolve) => {
362
+ setTimeout(resolve, STUB_FEEDBACK_SEND_MS);
363
+ });
364
+ console.info('[sandbox] message feedback', {
365
+ rating,
366
+ reply: reply.content,
367
+ replyKey,
368
+ note: message,
369
+ filename,
370
+ logBytes: (await getLogJson()).length,
371
+ });
372
+ if (s.messageFeedback === 'fails') throw new Error('Sandbox transport refused');
373
+ },
374
+ },
351
375
  },
352
376
  context: {
353
377
  enabled: s.contextGateEnabled,
@@ -969,6 +993,17 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
969
993
  hint: 'Also settable from the cog. Whichever you moved last wins; moving other panel controls will not disturb a cog choice.',
970
994
  },
971
995
  { kind: 'toggle', key: 'animations', label: 'Animations' },
996
+ {
997
+ kind: 'select',
998
+ key: 'messageFeedback',
999
+ label: 'Message feedback',
1000
+ options: [
1001
+ { value: 'off', label: 'Off' },
1002
+ { value: 'succeeds', label: 'Send succeeds' },
1003
+ { value: 'fails', label: 'Send fails' },
1004
+ ],
1005
+ hint: 'chatConfig.ui.messageFeedback — thumbs on each reply; the stub send logs to the console.',
1006
+ },
972
1007
  ],
973
1008
  },
974
1009
  {