@flamingo-stack/openframe-frontend-core 0.0.200 → 0.0.201-snapshot.20260521220457

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 (70) hide show
  1. package/dist/{chunk-V2FNIPZJ.cjs → chunk-3B43AHYE.cjs} +2 -2
  2. package/dist/{chunk-TMD44IKJ.js.map → chunk-3B43AHYE.cjs.map} +1 -1
  3. package/dist/{chunk-TMD44IKJ.js → chunk-55HF462A.js} +2 -2
  4. package/dist/chunk-55HF462A.js.map +1 -0
  5. package/dist/{chunk-ZOM75JOY.js → chunk-IDULPYOU.js} +4458 -4245
  6. package/dist/chunk-IDULPYOU.js.map +1 -0
  7. package/dist/{chunk-C3M6R6JH.cjs → chunk-JIKTMXTZ.cjs} +1277 -1064
  8. package/dist/chunk-JIKTMXTZ.cjs.map +1 -0
  9. package/dist/components/chat/approval-request-message.d.ts.map +1 -1
  10. package/dist/components/chat/chat-container.d.ts.map +1 -1
  11. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  12. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  13. package/dist/components/chat/types/message.types.d.ts +34 -0
  14. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  15. package/dist/components/features/index.cjs +13 -3
  16. package/dist/components/features/index.cjs.map +1 -1
  17. package/dist/components/features/index.d.ts +1 -1
  18. package/dist/components/features/index.d.ts.map +1 -1
  19. package/dist/components/features/index.js +16 -6
  20. package/dist/components/index.cjs +15 -3
  21. package/dist/components/index.cjs.map +1 -1
  22. package/dist/components/index.js +16 -4
  23. package/dist/components/layout/title-block.d.ts.map +1 -1
  24. package/dist/components/navigation/index.cjs +3 -3
  25. package/dist/components/navigation/index.js +2 -2
  26. package/dist/components/providers/theme-provider.d.ts +69 -0
  27. package/dist/components/providers/theme-provider.d.ts.map +1 -0
  28. package/dist/components/ui/entity-image.d.ts +9 -0
  29. package/dist/components/ui/entity-image.d.ts.map +1 -0
  30. package/dist/components/ui/file-manager/index.cjs +50 -50
  31. package/dist/components/ui/file-manager/index.js +1 -1
  32. package/dist/components/ui/index.cjs +5 -3
  33. package/dist/components/ui/index.cjs.map +1 -1
  34. package/dist/components/ui/index.d.ts +1 -0
  35. package/dist/components/ui/index.d.ts.map +1 -1
  36. package/dist/components/ui/index.js +4 -2
  37. package/dist/components/ui/organization-card.d.ts.map +1 -1
  38. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  39. package/dist/index.cjs +15 -3
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.js +16 -4
  42. package/package.json +2 -1
  43. package/src/components/chat/approval-request-message.tsx +106 -92
  44. package/src/components/chat/chat-container.tsx +8 -4
  45. package/src/components/chat/chat-message-enhanced.tsx +51 -9
  46. package/src/components/chat/chat-message-list.tsx +27 -19
  47. package/src/components/chat/types/message.types.ts +35 -0
  48. package/src/components/features/index.ts +15 -1
  49. package/src/components/layout/title-block.tsx +6 -30
  50. package/src/components/providers/theme-provider.tsx +130 -0
  51. package/src/components/ui/button/button.tsx +1 -1
  52. package/src/components/ui/checkbox-block.tsx +13 -13
  53. package/src/components/ui/entity-image.tsx +56 -0
  54. package/src/components/ui/index.ts +1 -0
  55. package/src/components/ui/organization-card.tsx +4 -8
  56. package/src/components/ui/simple-markdown-renderer.tsx +248 -2
  57. package/src/stories/CheckboxBlock.stories.tsx +1 -3
  58. package/src/stories/OrganizationCard.stories.tsx +14 -0
  59. package/src/stories/Theme.stories.tsx +350 -0
  60. package/src/styles/README.md +271 -174
  61. package/src/styles/dark_theme.tokens.json +982 -0
  62. package/src/styles/light_theme.tokens.json +982 -0
  63. package/src/styles/ods-colors.css +225 -146
  64. package/src/styles/ods_color_tokens.json +1 -300
  65. package/dist/chunk-C3M6R6JH.cjs.map +0 -1
  66. package/dist/chunk-V2FNIPZJ.cjs.map +0 -1
  67. package/dist/chunk-ZOM75JOY.js.map +0 -1
  68. package/dist/components/features/organization-icon.d.ts +0 -80
  69. package/dist/components/features/organization-icon.d.ts.map +0 -1
  70. package/src/components/features/organization-icon.tsx +0 -175
package/dist/index.js CHANGED
@@ -100,6 +100,7 @@ import {
100
100
  CursorPaginationSimple,
101
101
  CveLink,
102
102
  DEFAULT_OS_PLATFORM,
103
+ DEFAULT_THEME,
103
104
  DashboardInfoCard,
104
105
  DataTable,
105
106
  DataTableBody,
@@ -142,6 +143,7 @@ import {
142
143
  DrawerTrigger,
143
144
  DropdownButton,
144
145
  DynamicThemeProvider,
146
+ EntityImage,
145
147
  EntitySummaryEditor,
146
148
  EntityVideoSection,
147
149
  ErrorBoundary,
@@ -258,7 +260,6 @@ import {
258
260
  OnboardingStepCard,
259
261
  OnboardingWalkthrough,
260
262
  OrganizationCard,
261
- OrganizationIcon,
262
263
  PageActions,
263
264
  PageContainer,
264
265
  PageError,
@@ -337,6 +338,8 @@ import {
337
338
  StatusIndicator,
338
339
  StickySectionNav,
339
340
  Switch,
341
+ THEME_ATTRIBUTE,
342
+ THEME_STORAGE_KEY,
340
343
  TMCG_ROLES,
341
344
  TMCG_ROLE_DISPLAY_NAMES,
342
345
  TMCG_SOCIAL_PLATFORMS,
@@ -362,6 +365,7 @@ import {
362
365
  TagsManager,
363
366
  TagsSelector,
364
367
  Textarea,
368
+ ThemeProvider,
365
369
  ThinkingDisplay,
366
370
  TicketAttachmentsList,
367
371
  TicketCard,
@@ -503,12 +507,14 @@ import {
503
507
  usePageActionsBottomPadding,
504
508
  useRealtimeChunkProcessor,
505
509
  useSectionNavigation,
510
+ useTheme,
511
+ useThemeToggle,
506
512
  validateAccessCode,
507
513
  validateAndConsumeAccessCode,
508
514
  validateEmailDomain,
509
515
  validateEmailDomainList,
510
516
  validatePhoneNumber
511
- } from "./chunk-ZOM75JOY.js";
517
+ } from "./chunk-IDULPYOU.js";
512
518
  import {
513
519
  CommandApprovalToast,
514
520
  GraphQLIntrospector,
@@ -622,7 +628,7 @@ import {
622
628
  SkeletonText,
623
629
  SplitButton,
624
630
  buttonVariants
625
- } from "./chunk-TMD44IKJ.js";
631
+ } from "./chunk-55HF462A.js";
626
632
  import {
627
633
  AboutIcon,
628
634
  AiRobotIcon,
@@ -924,6 +930,7 @@ export {
924
930
  CutVendorCostsIcon,
925
931
  CveLink,
926
932
  DEFAULT_OS_PLATFORM,
933
+ DEFAULT_THEME,
927
934
  DashboardIcon,
928
935
  DashboardInfoCard,
929
936
  DataTable,
@@ -991,6 +998,7 @@ export {
991
998
  EditProfileIcon,
992
999
  ElestioLogo,
993
1000
  EmptyVendorIcon,
1001
+ EntityImage,
994
1002
  EntitySummaryEditor,
995
1003
  EntityVideoSection,
996
1004
  ErrorBoundary,
@@ -1170,7 +1178,6 @@ export {
1170
1178
  OpenSourceIcon,
1171
1179
  OpenmspLogo,
1172
1180
  OrganizationCard,
1173
- OrganizationIcon,
1174
1181
  OrganizationsIcon,
1175
1182
  PLAY_ICON_PATH,
1176
1183
  PackageIcon,
@@ -1293,6 +1300,8 @@ export {
1293
1300
  StickySectionNav,
1294
1301
  SunIcon,
1295
1302
  Switch,
1303
+ THEME_ATTRIBUTE,
1304
+ THEME_STORAGE_KEY,
1296
1305
  TMCG_ROLES,
1297
1306
  TMCG_ROLE_DISPLAY_NAMES,
1298
1307
  TMCG_SOCIAL_PLATFORMS,
@@ -1320,6 +1329,7 @@ export {
1320
1329
  TagsSelector,
1321
1330
  TelegramIcon,
1322
1331
  Textarea,
1332
+ ThemeProvider,
1323
1333
  ThinkingDisplay,
1324
1334
  ThumbsDownIcon,
1325
1335
  ThumbsUpIcon,
@@ -1558,6 +1568,8 @@ export {
1558
1568
  useSectionNavigation,
1559
1569
  useSmUp,
1560
1570
  useTablePagination,
1571
+ useTheme,
1572
+ useThemeToggle,
1561
1573
  useThrottle,
1562
1574
  useToast,
1563
1575
  useValidatePlatform,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.200",
3
+ "version": "0.0.201-snapshot.20260521220457",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -211,6 +211,7 @@
211
211
  "recharts": "^3.1.0",
212
212
  "rehype-highlight": "^7.0.2",
213
213
  "rehype-raw": "^7.0.0",
214
+ "rehype-sanitize": "^6.0.0",
214
215
  "remark": "^15.0.1",
215
216
  "remark-breaks": "^4.0.0",
216
217
  "remark-gfm": "^4.0.1",
@@ -6,11 +6,77 @@ import { Button } from "../ui/button"
6
6
  import { Tag } from "../ui/tag"
7
7
  import { CheckCircle, XCircle } from "lucide-react"
8
8
  import type { ApprovalRequestMessageProps } from "./types"
9
+ import type { ApprovalRequestField } from "./types/message.types"
10
+
11
+ /**
12
+ * Stacked label/value rows for the approval card's structured field
13
+ * list. Labels are tiny uppercase muted text; values render as primary
14
+ * text with `whitespace-pre-wrap` so multi-line descriptions
15
+ * (`content`, `resolution`, etc.) keep their structure. Mirrored across
16
+ * the pending + resolved branches so an approved ticket reads the same
17
+ * way it did at decision time.
18
+ */
19
+ function ApprovalFieldList({ fields }: { fields: ApprovalRequestField[] }) {
20
+ return (
21
+ <dl className="flex flex-col gap-2.5 mt-1">
22
+ {fields.map((f, i) => (
23
+ <div key={i} className="flex flex-col gap-0.5">
24
+ <dt className="font-['DM_Sans'] font-semibold text-[11px] uppercase tracking-wide text-ods-text-tertiary leading-4">
25
+ {f.label}
26
+ </dt>
27
+ <dd className="font-['DM_Sans'] text-sm text-ods-text-primary leading-5 whitespace-pre-wrap break-words">
28
+ {f.value}
29
+ </dd>
30
+ </div>
31
+ ))}
32
+ </dl>
33
+ )
34
+ }
35
+
36
+ /**
37
+ * Shared body for both pending and resolved branches of
38
+ * `<ApprovalRequestMessage>`. The pending card adds Approve/Reject
39
+ * buttons below; the resolved card adds an Approved/Rejected `<Tag>`.
40
+ * Everything ABOVE the footer — command bar, icon, structured-fields
41
+ * stack, explanation paragraph — is identical, so the body lives here
42
+ * to prevent silent drift between the two render paths (a prior
43
+ * version already had a `break-words` vs `break-all` mismatch on the
44
+ * `<code>` element from an out-of-sync copy-paste edit).
45
+ */
46
+ function ApprovalCardBody({
47
+ data,
48
+ }: {
49
+ data: ApprovalRequestMessageProps['data']
50
+ }) {
51
+ return (
52
+ <div className="flex flex-col gap-1">
53
+ <div className="bg-ods-bg border border-ods-border rounded-md p-3 flex gap-2 items-start max-h-32 overflow-y-auto">
54
+ <code className="font-['DM_Sans'] font-medium text-sm text-ods-text-primary flex-1 leading-5 whitespace-pre-wrap break-words">
55
+ {data.command}
56
+ </code>
57
+ {data.icon && (
58
+ <div className="w-4 h-4 shrink-0 text-ods-text-tertiary">
59
+ {data.icon}
60
+ </div>
61
+ )}
62
+ </div>
63
+ {data.fields && data.fields.length > 0 ? (
64
+ <ApprovalFieldList fields={data.fields} />
65
+ ) : (
66
+ data.explanation && (
67
+ <p className="font-['DM_Sans'] font-medium text-sm text-ods-text-secondary leading-5 whitespace-pre-line break-words">
68
+ {data.explanation}
69
+ </p>
70
+ )
71
+ )}
72
+ </div>
73
+ )
74
+ }
9
75
 
10
76
  const ApprovalRequestMessage = forwardRef<HTMLDivElement, ApprovalRequestMessageProps>(
11
77
  ({ className, data, onApprove, onReject, status = 'pending', ...props }, ref) => {
12
78
  const [isProcessing, setIsProcessing] = useState(false)
13
-
79
+
14
80
  const handleApprove = async () => {
15
81
  setIsProcessing(true)
16
82
  try {
@@ -28,49 +94,7 @@ const ApprovalRequestMessage = forwardRef<HTMLDivElement, ApprovalRequestMessage
28
94
  setIsProcessing(false)
29
95
  }
30
96
  }
31
-
32
- if (status !== 'pending') {
33
- return (
34
- <div
35
- ref={ref}
36
- className={cn(
37
- "bg-ods-card border border-ods-border rounded-md p-4 mb-2 flex flex-col gap-4",
38
- className
39
- )}
40
- {...props}
41
- >
42
- {/* Command and icon section */}
43
- <div className="flex flex-col gap-1">
44
- <div className="bg-ods-bg border border-ods-border rounded-md p-3 flex gap-2 items-start max-h-32 overflow-y-auto">
45
- <code className="font-['DM_Sans'] font-medium text-sm text-ods-text-primary flex-1 leading-5 whitespace-pre-wrap break-words">
46
- {data.command}
47
- </code>
48
- {data.icon && (
49
- <div className="w-4 h-4 shrink-0 text-ods-text-tertiary">
50
- {data.icon}
51
- </div>
52
- )}
53
- </div>
54
-
55
- {data.explanation && (
56
- <p className="font-['DM_Sans'] font-medium text-sm text-ods-text-secondary leading-5 whitespace-pre-line break-words">
57
- {data.explanation}
58
- </p>
59
- )}
60
- </div>
61
-
62
- {/* Status indicator */}
63
- <div className="flex">
64
- <Tag
65
- label={status === 'approved' ? 'Approved' : 'Rejected'}
66
- variant={status === 'approved' ? 'success' : 'error'}
67
- icon={status === 'approved' ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
68
- />
69
- </div>
70
- </div>
71
- )
72
- }
73
-
97
+
74
98
  return (
75
99
  <div
76
100
  ref={ref}
@@ -80,55 +104,45 @@ const ApprovalRequestMessage = forwardRef<HTMLDivElement, ApprovalRequestMessage
80
104
  )}
81
105
  {...props}
82
106
  >
83
- {/* Command and icon section */}
84
- <div className="flex flex-col gap-1">
85
- <div className="bg-ods-bg border border-ods-border rounded-md p-3 flex gap-2 items-start max-h-32 overflow-y-auto">
86
- <code className="font-['DM_Sans'] font-medium text-sm text-ods-text-primary flex-1 leading-5 whitespace-pre-wrap break-all">
87
- {data.command}
88
- </code>
89
- {data.icon && (
90
- <div className="w-4 h-4 shrink-0 text-ods-text-tertiary">
91
- {data.icon}
92
- </div>
93
- )}
107
+ <ApprovalCardBody data={data} />
108
+ {status === 'pending' ? (
109
+ <div className="flex gap-4 items-center">
110
+ <Button
111
+ size="small-legacy"
112
+ variant="accent"
113
+ onClick={handleApprove}
114
+ disabled={isProcessing}
115
+ className={cn(
116
+ "bg-ods-accent hover:bg-ods-accent/90",
117
+ "font-mono font-medium md:!text-sm text-ods-bg uppercase tracking-[-0.28px]",
118
+ "px-2 py-1 h-auto"
119
+ )}
120
+ >
121
+ Approve
122
+ </Button>
123
+ <Button
124
+ size="small-legacy"
125
+ variant="outline"
126
+ onClick={handleReject}
127
+ disabled={isProcessing}
128
+ className={cn(
129
+ "bg-ods-card border-ods-border",
130
+ "font-mono font-medium md:!text-sm text-ods-text-primary uppercase tracking-[-0.28px]",
131
+ "hover:bg-ods-bg px-2 py-1 h-auto"
132
+ )}
133
+ >
134
+ Reject
135
+ </Button>
94
136
  </div>
95
-
96
- {data.explanation && (
97
- <p className="font-['DM_Sans'] font-medium text-sm text-ods-text-secondary leading-5">
98
- {data.explanation}
99
- </p>
100
- )}
101
- </div>
102
-
103
- {/* Approve/Reject buttons */}
104
- <div className="flex gap-4 items-center">
105
- <Button
106
- size="small-legacy"
107
- variant="accent"
108
- onClick={handleApprove}
109
- disabled={isProcessing}
110
- className={cn(
111
- "bg-ods-accent hover:bg-ods-accent/90",
112
- "font-mono font-medium md:!text-sm text-ods-bg uppercase tracking-[-0.28px]",
113
- "px-2 py-1 h-auto"
114
- )}
115
- >
116
- Approve
117
- </Button>
118
- <Button
119
- size="small-legacy"
120
- variant="outline"
121
- onClick={handleReject}
122
- disabled={isProcessing}
123
- className={cn(
124
- "bg-ods-card border-ods-border",
125
- "font-mono font-medium md:!text-sm text-ods-text-primary uppercase tracking-[-0.28px]",
126
- "hover:bg-ods-bg px-2 py-1 h-auto"
127
- )}
128
- >
129
- Reject
130
- </Button>
131
- </div>
137
+ ) : (
138
+ <div className="flex">
139
+ <Tag
140
+ label={status === 'approved' ? 'Approved' : 'Rejected'}
141
+ variant={status === 'approved' ? 'success' : 'error'}
142
+ icon={status === 'approved' ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
143
+ />
144
+ </div>
145
+ )}
132
146
  </div>
133
147
  )
134
148
  }
@@ -11,14 +11,18 @@ import type { ConnectionIndicatorProps, ChatContainerProps, ChatHeaderProps } fr
11
11
  const ConnectionIndicator: React.FC<ConnectionIndicatorProps> = ({ status }) => {
12
12
  const getStatusStyles = () => {
13
13
  switch (status) {
14
+ // ODS attention tokens — same scheme used by the rest of the chat
15
+ // shell (StatusBadge, error toast, etc.). Hex Tailwind palette
16
+ // (`bg-green-500` / `bg-red-500`) would diverge from the theme and
17
+ // is forbidden by the host's design-token policy.
14
18
  case 'connected':
15
- return 'bg-green-500'
19
+ return 'bg-ods-attention-green-success'
16
20
  case 'connecting':
17
- return 'bg-yellow-500 animate-pulse'
21
+ return 'bg-ods-attention-yellow-warning animate-pulse'
18
22
  case 'disconnected':
19
- return 'bg-red-500'
23
+ return 'bg-ods-attention-red-error'
20
24
  default:
21
- return 'bg-gray-500'
25
+ return 'bg-ods-text-tertiary'
22
26
  }
23
27
  }
24
28
 
@@ -111,8 +111,32 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
111
111
  // returns the cached node so React reuses the same instance.
112
112
  let rendered = seenRendered.get(key)
113
113
  if (!seenRendered.has(key)) {
114
+ // Always invoke render() — even when the metadata map has
115
+ // no entry for this marker. Fetch-mode card types
116
+ // (delivery_item, roadmap_item, internal_task, etc.) don't
117
+ // ship metadata in the SSE frame; they self-fetch by `id`
118
+ // via the host's list-API hook, so a minimal {type,id}
119
+ // ChatRef is all the renderer needs to mount the loader.
120
+ // For no-fetch types (hubspot_ticket_self, slack_message,
121
+ // …) without a refMatch the host's render() returns null
122
+ // and we fall through to the bare-cardId fallback in the
123
+ // `<a card://…>` override below.
124
+ //
125
+ // SYNTHETIC REF DEFAULTS: `ChatRef.title` and `ChatRef.url`
126
+ // are non-optional in the type; a bare `{type, id}` cast
127
+ // would lie to consumers that read those fields. Default
128
+ // `title` to the cardId (so any host renderer that prints
129
+ // `ref.title` shows the id rather than `undefined`) and
130
+ // `url` to null (matches the no-link semantics fetch-mode
131
+ // cards rely on — they resolve their own URL after fetch).
114
132
  const refMatch = refs[key]
115
- rendered = refMatch ? render(refMatch) : undefined
133
+ const refForRender: ChatRef = refMatch ?? {
134
+ type: cardType,
135
+ id: cardId,
136
+ title: cardId,
137
+ url: null,
138
+ }
139
+ rendered = render(refForRender)
116
140
  seenRendered.set(key, rendered)
117
141
  }
118
142
  if (React.isValidElement(rendered) && rendered.type === BlockCard) {
@@ -171,15 +195,33 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
171
195
  const key = `${cardType}:${cardId}`
172
196
  const inline = inlineByKey?.get(key)
173
197
  if (inline != null) return inline
174
- // No renderer, no ref, OR renderer returned null — fall back
175
- // to plain text title-only. Use any same-type ref's title if
176
- // available; otherwise the bare cardId. Never render the
177
- // literal `card://` URL.
198
+ // Three fallback cases — keep them DISTINCT, never blur them
199
+ // together. Mixing them up (which the old code did by reaching
200
+ // for "any same-type ref's title") makes LLM hallucinations
201
+ // LOOK like real cards, which the user can't tell apart from
202
+ // genuine references.
203
+ //
204
+ // (1) Exact ref present, renderer returned null:
205
+ // The marker is legit (server confirmed the row exists)
206
+ // but no compact-card type is registered for `cardType`.
207
+ // Render the ref's REAL title as plain text — accurate,
208
+ // just no rich UI.
209
+ //
210
+ // (2) Exact ref absent (refs map has no `${cardType}:${cardId}`):
211
+ // The LLM emitted a marker for an ID the server did NOT
212
+ // surface. Either the LLM hallucinated the id, or the
213
+ // refs map and the snapshot drifted (server-side bug
214
+ // worth fixing — see `MAX_ROWS_PER_ENTITY_GROUP` in
215
+ // `doc-chat-utils.ts:buildSourcesMeta`). Render the raw
216
+ // `cardId` so the breakage is VISIBLE; never borrow a
217
+ // title from an unrelated ref — that hides the bug and
218
+ // deceives the reader into thinking they're looking at
219
+ // a real card.
178
220
  const refMatch: ChatRef | undefined = refs[key]
179
- const fallbackTitle = (refMatch?.title)
180
- ?? Object.values(refs).find((r) => r.type === cardType)?.title
181
- ?? cardId
182
- return <span className="text-ods-text-primary">{fallbackTitle}</span>
221
+ if (refMatch) {
222
+ return <span className="text-ods-text-primary">{refMatch.title}</span>
223
+ }
224
+ return <span className="text-ods-text-secondary opacity-60">{cardId}</span>
183
225
  }
184
226
  }
185
227
  // Unified click rule — delegated to the host's `NavLinkAnchor`
@@ -441,25 +441,33 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
441
441
  <div ref={sentinelRef} className="h-px" />
442
442
  )}
443
443
  <div className="flex-1" />
444
- {messages.map((message, index) => (
445
- <ChatMessageEnhanced
446
- key={message.id}
447
- ref={getRegisterMessageEl(message.id)}
448
- role={message.role}
449
- name={message.name}
450
- content={message.content}
451
- timestamp={message.timestamp}
452
- isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
453
- avatar={showAvatars ? message.avatar : null}
454
- showAvatar={showAvatars}
455
- assistantType={message.assistantType || assistantType}
456
- authorType={message.authorType}
457
- assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
458
- chatRefs={message.chatRefs}
459
- renderEntityCard={renderEntityCard}
460
- NavLinkAnchor={NavLinkAnchor}
461
- />
462
- ))}
444
+ {messages.map((message, index) => {
445
+ // Hidden messages (synthetic continuation prompts the host
446
+ // injects after an approval card) are part of the API
447
+ // conversation history but never render. Skipping here
448
+ // keeps the visible thread coherent — see
449
+ // `Message.hidden` doc-comment in message.types.ts.
450
+ if (message.hidden) return null
451
+ return (
452
+ <ChatMessageEnhanced
453
+ key={message.id}
454
+ ref={getRegisterMessageEl(message.id)}
455
+ role={message.role}
456
+ name={message.name}
457
+ content={message.content}
458
+ timestamp={message.timestamp}
459
+ isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
460
+ avatar={showAvatars ? message.avatar : null}
461
+ showAvatar={showAvatars}
462
+ assistantType={message.assistantType || assistantType}
463
+ authorType={message.authorType}
464
+ assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
465
+ chatRefs={message.chatRefs}
466
+ renderEntityCard={renderEntityCard}
467
+ NavLinkAnchor={NavLinkAnchor}
468
+ />
469
+ )
470
+ })}
463
471
  </div>
464
472
  </div>
465
473
 
@@ -78,8 +78,24 @@ export interface ExecutingToolState {
78
78
 
79
79
  // ========== Approval Request Types ==========
80
80
 
81
+ export interface ApprovalRequestField {
82
+ /** Short label — e.g. "Subject", "Priority". Rendered in a muted
83
+ * caps style above the value. */
84
+ label: string
85
+ /** Free-text value. Wraps and line-breaks are preserved
86
+ * (`whitespace-pre-wrap`). */
87
+ value: string
88
+ }
89
+
81
90
  export interface ApprovalRequestData {
82
91
  command: string
92
+ /** Structured field list — preferred over `explanation`. When set,
93
+ * the approval card renders a vertical label/value stack with
94
+ * proper spacing. Falls back to `explanation` (a single paragraph)
95
+ * when omitted. Keep BOTH when you want hosts on older lib
96
+ * versions to still see the prose; new hosts should send only
97
+ * `fields`. */
98
+ fields?: ApprovalRequestField[]
83
99
  explanation?: string
84
100
  icon?: React.ReactNode
85
101
  requestId?: string
@@ -333,4 +349,23 @@ export interface Message {
333
349
  * whose body is a long article). The server is the sole decision-
334
350
  * maker — set on the metadata leading frame. */
335
351
  scrollAnchor?: ScrollAnchor
352
+ /** When true the message is part of the API conversation history (sent
353
+ * to the LLM so it has context) but is NOT rendered in the chat UI.
354
+ *
355
+ * Used for "synthetic continuation" turns: when the user clicks Approve
356
+ * on a tool proposal, the host auto-fires a follow-up `sendMessage`
357
+ * with `hidden: true` carrying a directive like "the user just
358
+ * approved <tool>; ask follow-up questions per protocol". The LLM's
359
+ * response IS rendered (as a normal assistant message); only the
360
+ * trigger prompt is suppressed so the chat reads naturally:
361
+ *
362
+ * user: "open a ticket"
363
+ * assistant: preamble + approval card
364
+ * [user clicks Approve]
365
+ * assistant: "Now to triage faster, can you share..." ← auto-fires
366
+ *
367
+ * Without this flag the trigger prompt would surface as a confusing
368
+ * bubble like "(continue per protocol)" between the approval card
369
+ * and the AI's follow-up. */
370
+ hidden?: boolean
336
371
  }
@@ -2,6 +2,21 @@
2
2
 
3
3
  // Feature Components exports
4
4
  export { DynamicThemeProvider, useDynamicTheme } from '../providers/dynamic-theme-provider'
5
+ // Canonical ODS theme system — thin wrapper over `next-themes`
6
+ // (manual light/dark, default dark, no-flash handled by next-themes).
7
+ // Headless by design: apps build their own toggle button via the lib's
8
+ // existing <Button> + `useThemeToggle()`.
9
+ export {
10
+ ThemeProvider,
11
+ useTheme,
12
+ useThemeToggle,
13
+ THEME_STORAGE_KEY,
14
+ THEME_ATTRIBUTE,
15
+ DEFAULT_THEME,
16
+ type Theme,
17
+ type ThemeProviderProps,
18
+ type UseThemeToggleResult,
19
+ } from '../providers/theme-provider'
5
20
  export * from './array-entry-manager'
6
21
  export * from './auth-providers-list'
7
22
  export * from './changelog-manager'
@@ -16,7 +31,6 @@ export * from './knowledge-base-links-manager'
16
31
  export * from './loading-provider'
17
32
  export * from './media-gallery-manager'
18
33
  export * from './more-about-button'
19
- export * from './organization-icon'
20
34
  export * from './os-type-badge'
21
35
  export * from './os-type-badge-group'
22
36
  export * from './parallax-image-showcase'
@@ -3,16 +3,10 @@
3
3
  import React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import type { ActionsMenuGroup } from '../ui/actions-menu'
6
+ import { EntityImage } from '../ui/entity-image'
6
7
  import { PageActions, type PageActionButton } from '../ui/page-actions'
7
8
  import { BackButton } from './back-button'
8
9
 
9
- function getInitials(name?: string): string {
10
- if (!name) return ''
11
- const words = name.trim().split(/\s+/)
12
- if (words.length === 1) return words[0].charAt(0).toUpperCase()
13
- return (words[0].charAt(0) + words[words.length - 1].charAt(0)).toUpperCase()
14
- }
15
-
16
10
  export interface TitleBlockProps {
17
11
  title?: string
18
12
  subtitle?: string
@@ -45,14 +39,6 @@ export function TitleBlock({
45
39
  className,
46
40
  }: TitleBlockProps) {
47
41
  const hasActions = actions && actions.length > 0
48
- const [imageFailed, setImageFailed] = React.useState(false)
49
-
50
- React.useEffect(() => {
51
- setImageFailed(false)
52
- }, [image?.src])
53
-
54
- const showImageFallback = !!image && (imageFailed || !image.src)
55
- const initials = getInitials(image?.alt || title)
56
42
 
57
43
  return (
58
44
  <div
@@ -83,21 +69,11 @@ export function TitleBlock({
83
69
  {(image || subtitle) ? (
84
70
  <div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
85
71
  {image && (
86
- showImageFallback ? (
87
- <div
88
- aria-label={image.alt}
89
- className="size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border bg-ods-bg flex items-center justify-center text-ods-text-secondary text-h4 select-none"
90
- >
91
- {initials || '?'}
92
- </div>
93
- ) : (
94
- <img
95
- src={image.src}
96
- alt={image.alt ?? ''}
97
- onError={() => setImageFailed(true)}
98
- className="size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border object-cover"
99
- />
100
- )
72
+ <EntityImage
73
+ src={image.src}
74
+ alt={image.alt}
75
+ fallbackText={image.alt || title}
76
+ />
101
77
  )}
102
78
  <div className="flex flex-col justify-center min-w-0 flex-1">
103
79
  {title && (