@flamingo-stack/openframe-frontend-core 0.0.421 → 0.0.422-snapshot.20260713170838

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 (113) hide show
  1. package/dist/{chunk-5TL2VVEN.js → chunk-2Q2SKYCZ.js} +5 -5
  2. package/dist/{chunk-HBTKCED7.js → chunk-4ACSUJ7Q.js} +13 -6
  3. package/dist/chunk-4ACSUJ7Q.js.map +1 -0
  4. package/dist/{chunk-FHAG7O27.js → chunk-7W4474XS.js} +2 -2
  5. package/dist/{chunk-5WXUR7LF.js → chunk-B2255TIZ.js} +55 -23
  6. package/dist/chunk-B2255TIZ.js.map +1 -0
  7. package/dist/{chunk-RKNF2DHY.cjs → chunk-CBY3VQKJ.cjs} +59 -59
  8. package/dist/{chunk-RKNF2DHY.cjs.map → chunk-CBY3VQKJ.cjs.map} +1 -1
  9. package/dist/{chunk-WDZATJTF.cjs → chunk-FVI2ESVQ.cjs} +523 -491
  10. package/dist/chunk-FVI2ESVQ.cjs.map +1 -0
  11. package/dist/{chunk-UKIVJU3A.js → chunk-GR2CMONR.js} +4 -4
  12. package/dist/{chunk-PMFK7ZKC.cjs → chunk-HGBEKYXL.cjs} +5 -5
  13. package/dist/{chunk-PMFK7ZKC.cjs.map → chunk-HGBEKYXL.cjs.map} +1 -1
  14. package/dist/{chunk-LHJHMZYU.js → chunk-I4YIWY3O.js} +4 -4
  15. package/dist/{chunk-E6WANQ6K.cjs → chunk-IMK6RUEC.cjs} +13 -6
  16. package/dist/chunk-IMK6RUEC.cjs.map +1 -0
  17. package/dist/{chunk-E5HKIBRS.cjs → chunk-IRIPRXO5.cjs} +9 -9
  18. package/dist/{chunk-E5HKIBRS.cjs.map → chunk-IRIPRXO5.cjs.map} +1 -1
  19. package/dist/{chunk-TJ7HD4KI.js → chunk-JPZE2P4Y.js} +4 -4
  20. package/dist/{chunk-4PS4F4WL.cjs → chunk-KNO5CB3W.cjs} +12 -12
  21. package/dist/{chunk-4PS4F4WL.cjs.map → chunk-KNO5CB3W.cjs.map} +1 -1
  22. package/dist/{chunk-4XTIANMS.js → chunk-LK3KXZIN.js} +3 -3
  23. package/dist/{chunk-XVWDKBBF.js → chunk-MOBZMJAP.js} +2 -2
  24. package/dist/{chunk-5U6C3RXX.cjs → chunk-MSZ4WBCD.cjs} +11 -11
  25. package/dist/{chunk-5U6C3RXX.cjs.map → chunk-MSZ4WBCD.cjs.map} +1 -1
  26. package/dist/{chunk-ISUMLEJ6.js → chunk-NNF5XOMH.js} +3 -3
  27. package/dist/{chunk-YLSDXDD7.cjs → chunk-OTNL36LC.cjs} +37 -37
  28. package/dist/{chunk-YLSDXDD7.cjs.map → chunk-OTNL36LC.cjs.map} +1 -1
  29. package/dist/{chunk-RQXFDMRY.cjs → chunk-SMUKHJJM.cjs} +19 -19
  30. package/dist/{chunk-RQXFDMRY.cjs.map → chunk-SMUKHJJM.cjs.map} +1 -1
  31. package/dist/{chunk-JMHU37Y6.js → chunk-SVUDKNS7.js} +3 -3
  32. package/dist/{chunk-UZOYTAK7.cjs → chunk-TIFJIHV2.cjs} +82 -82
  33. package/dist/{chunk-UZOYTAK7.cjs.map → chunk-TIFJIHV2.cjs.map} +1 -1
  34. package/dist/{chunk-6HCWW4YM.cjs → chunk-TMSP5FYV.cjs} +26 -26
  35. package/dist/{chunk-6HCWW4YM.cjs.map → chunk-TMSP5FYV.cjs.map} +1 -1
  36. package/dist/{chunk-5UZULT35.cjs → chunk-WO2NOBF3.cjs} +31 -31
  37. package/dist/{chunk-5UZULT35.cjs.map → chunk-WO2NOBF3.cjs.map} +1 -1
  38. package/dist/{chunk-PCM4HJO4.cjs → chunk-WOHMJJKD.cjs} +30 -30
  39. package/dist/{chunk-PCM4HJO4.cjs.map → chunk-WOHMJJKD.cjs.map} +1 -1
  40. package/dist/{chunk-UHSVSDF5.js → chunk-WWS3R3UD.js} +2 -2
  41. package/dist/{chunk-ZXWSOOEH.js → chunk-YOEQWHUD.js} +2 -2
  42. package/dist/components/case-studies/index.cjs +9 -9
  43. package/dist/components/case-studies/index.js +3 -3
  44. package/dist/components/chat/chat-input.d.ts.map +1 -1
  45. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  47. package/dist/components/chat/index.cjs +3 -3
  48. package/dist/components/chat/index.js +2 -2
  49. package/dist/components/chat/types/api.types.d.ts +10 -0
  50. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  51. package/dist/components/contact/index.cjs +4 -4
  52. package/dist/components/contact/index.js +3 -3
  53. package/dist/components/docs/index.cjs +6 -6
  54. package/dist/components/docs/index.js +5 -5
  55. package/dist/components/embeds/index.cjs +4 -4
  56. package/dist/components/embeds/index.js +3 -3
  57. package/dist/components/faq/index.cjs +4 -4
  58. package/dist/components/faq/index.js +3 -3
  59. package/dist/components/features/index.cjs +3 -3
  60. package/dist/components/features/index.js +2 -2
  61. package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -1
  62. package/dist/components/help-center-pages/index.cjs +21 -21
  63. package/dist/components/help-center-pages/index.js +12 -12
  64. package/dist/components/index.cjs +153 -153
  65. package/dist/components/index.js +10 -10
  66. package/dist/components/navigation/index.cjs +3 -3
  67. package/dist/components/navigation/index.js +2 -2
  68. package/dist/components/onboarding-guides/index.cjs +6 -6
  69. package/dist/components/onboarding-guides/index.js +5 -5
  70. package/dist/components/related-content/index.cjs +4 -4
  71. package/dist/components/related-content/index.js +3 -3
  72. package/dist/components/tickets/index.cjs +6 -6
  73. package/dist/components/tickets/index.js +5 -5
  74. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  75. package/dist/components/ui/button/split-button.d.ts +6 -0
  76. package/dist/components/ui/button/split-button.d.ts.map +1 -1
  77. package/dist/components/ui/file-manager/index.cjs +50 -50
  78. package/dist/components/ui/file-manager/index.js +1 -1
  79. package/dist/components/ui/index.cjs +3 -3
  80. package/dist/components/ui/index.js +2 -2
  81. package/dist/components/ui/page-actions.d.ts.map +1 -1
  82. package/dist/index.cjs +3 -3
  83. package/dist/index.js +2 -2
  84. package/package.json +1 -1
  85. package/src/components/chat/chat-input.tsx +5 -1
  86. package/src/components/chat/hooks/use-nats-chat-adapter.ts +52 -3
  87. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +25 -3
  88. package/src/components/chat/types/api.types.ts +10 -0
  89. package/src/components/features/time-tracker/time-tracker-panel.tsx +1 -0
  90. package/src/components/ui/actions-menu.tsx +1 -0
  91. package/src/components/ui/button/split-button.tsx +18 -3
  92. package/src/components/ui/page-actions.tsx +16 -14
  93. package/src/stories/ApprovalRequestMessageClient.stories.tsx +89 -0
  94. package/src/stories/ChatMessageEnhanced.stories.tsx +219 -0
  95. package/src/stories/ChatTypingIndicator.stories.tsx +51 -0
  96. package/src/stories/ContextCompactionDisplay.stories.tsx +44 -0
  97. package/src/stories/ErrorMessageDisplay.stories.tsx +66 -0
  98. package/src/stories/ThinkingDisplay.stories.tsx +63 -0
  99. package/dist/chunk-5WXUR7LF.js.map +0 -1
  100. package/dist/chunk-E6WANQ6K.cjs.map +0 -1
  101. package/dist/chunk-HBTKCED7.js.map +0 -1
  102. package/dist/chunk-WDZATJTF.cjs.map +0 -1
  103. /package/dist/{chunk-5TL2VVEN.js.map → chunk-2Q2SKYCZ.js.map} +0 -0
  104. /package/dist/{chunk-FHAG7O27.js.map → chunk-7W4474XS.js.map} +0 -0
  105. /package/dist/{chunk-UKIVJU3A.js.map → chunk-GR2CMONR.js.map} +0 -0
  106. /package/dist/{chunk-LHJHMZYU.js.map → chunk-I4YIWY3O.js.map} +0 -0
  107. /package/dist/{chunk-TJ7HD4KI.js.map → chunk-JPZE2P4Y.js.map} +0 -0
  108. /package/dist/{chunk-4XTIANMS.js.map → chunk-LK3KXZIN.js.map} +0 -0
  109. /package/dist/{chunk-XVWDKBBF.js.map → chunk-MOBZMJAP.js.map} +0 -0
  110. /package/dist/{chunk-ISUMLEJ6.js.map → chunk-NNF5XOMH.js.map} +0 -0
  111. /package/dist/{chunk-JMHU37Y6.js.map → chunk-SVUDKNS7.js.map} +0 -0
  112. /package/dist/{chunk-UHSVSDF5.js.map → chunk-WWS3R3UD.js.map} +0 -0
  113. /package/dist/{chunk-ZXWSOOEH.js.map → chunk-YOEQWHUD.js.map} +0 -0
@@ -303,6 +303,7 @@ export function TimeTrackerPanel({
303
303
  'aria-label': 'Open my time in a new view',
304
304
  onClick: onOpenMyTimeMenu,
305
305
  disabled: !onOpenMyTimeMenu,
306
+ openInNewTab: true,
306
307
  }}
307
308
  >
308
309
  Open My Time
@@ -90,6 +90,7 @@ const SecondaryAction: React.FC<{ action: ActionsMenuItemIconAction }> = ({ acti
90
90
 
91
91
  const classes = cn(
92
92
  SECONDARY_ACTION_CLASSES,
93
+ action.openInNewTab && "max-md:hidden",
93
94
  action.disabled && "cursor-not-allowed opacity-60 pointer-events-none",
94
95
  );
95
96
 
@@ -32,6 +32,7 @@ const splitHalfVariants = cva(splitHalfBase, {
32
32
  small: "h-6 md:h-8 px-[var(--spacing-system-xs)] text-h5 [&_svg]:h-3 [&_svg]:w-3 md:[&_svg]:h-4 md:[&_svg]:w-4",
33
33
  },
34
34
  side: { main: "", icon: "" },
35
+ soloOnMobile: { true: "", false: "" },
35
36
  },
36
37
  compoundVariants: [
37
38
  // Rounded corners + per-variant seam. The icon-side's left border is the divider.
@@ -55,8 +56,12 @@ const splitHalfVariants = cva(splitHalfBase, {
55
56
  // Icon half: per Figma, narrower than main height (default: 40×48; small: 32×32).
56
57
  { side: "icon", size: "default", class: "w-10 px-0" },
57
58
  { side: "icon", size: "small", class: "w-6 md:w-8 px-0" },
59
+
60
+ { side: "icon", soloOnMobile: true, class: "max-md:hidden" },
61
+ { side: "main", soloOnMobile: true, class: "max-md:rounded-r-md" },
62
+ { variant: "outline", side: "main", soloOnMobile: true, class: "max-md:border-r" },
58
63
  ],
59
- defaultVariants: { variant: "accent", size: "default", side: "main" },
64
+ defaultVariants: { variant: "accent", size: "default", side: "main", soloOnMobile: false },
60
65
  })
61
66
 
62
67
  type SplitButtonVariant = NonNullable<VariantProps<typeof splitHalfVariants>["variant"]>
@@ -68,6 +73,11 @@ interface SplitButtonIconAction {
68
73
  "aria-label": string
69
74
  onClick?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>
70
75
  href?: string
76
+ /**
77
+ * Opens `href` with `target="_blank"`. Also marks the half as a new-tab
78
+ * affordance, which hides it below `md` (set it for `onClick` handlers that
79
+ * open a new tab/window themselves).
80
+ */
71
81
  openInNewTab?: boolean
72
82
  prefetch?: boolean
73
83
  disabled?: boolean
@@ -101,6 +111,7 @@ interface HalfOptions {
101
111
  variant: SplitButtonVariant
102
112
  size: SplitButtonSize
103
113
  side: "main" | "icon"
114
+ soloOnMobile?: boolean
104
115
  href?: string
105
116
  openInNewTab?: boolean
106
117
  prefetch?: boolean
@@ -113,8 +124,8 @@ interface HalfOptions {
113
124
  children: React.ReactNode
114
125
  }
115
126
 
116
- function Half({ variant, size, side, href, openInNewTab, prefetch, onClick, disabled, grow, type = "button", ariaLabel, children }: HalfOptions) {
117
- const classes = cn(splitHalfVariants({ variant, size, side }), grow && "flex-1")
127
+ function Half({ variant, size, side, soloOnMobile, href, openInNewTab, prefetch, onClick, disabled, grow, type = "button", ariaLabel, children }: HalfOptions) {
128
+ const classes = cn(splitHalfVariants({ variant, size, side, soloOnMobile }), grow && "flex-1")
118
129
 
119
130
  if (href) {
120
131
  return (
@@ -169,6 +180,8 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
169
180
  },
170
181
  ref,
171
182
  ) {
183
+ const soloOnMobile = !!iconAction.openInNewTab
184
+
172
185
  return (
173
186
  <div
174
187
  ref={ref}
@@ -180,6 +193,7 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
180
193
  variant={variant}
181
194
  size={size}
182
195
  side="main"
196
+ soloOnMobile={soloOnMobile}
183
197
  href={href}
184
198
  openInNewTab={openInNewTab}
185
199
  prefetch={prefetch}
@@ -197,6 +211,7 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
197
211
  variant={variant}
198
212
  size={size}
199
213
  side="icon"
214
+ soloOnMobile={soloOnMobile}
200
215
  href={iconAction.href}
201
216
  openInNewTab={iconAction.openInNewTab}
202
217
  prefetch={iconAction.prefetch}
@@ -245,6 +245,12 @@ function IconButtonsVariant({
245
245
  const singleAction = isSingleAction ? actions[0] : null
246
246
  const useSingleActionMobile = isSingleAction && !hasMenuActions
247
247
 
248
+ const mobileMenuGroups = [
249
+ { items: actions.flatMap(actionToMenuItems) },
250
+ ...(menuActions ?? []),
251
+ ]
252
+ const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0)
253
+
248
254
  return (
249
255
  <>
250
256
  {/* Desktop: every action as an icon button + optional overflow menu */}
@@ -262,14 +268,9 @@ function IconButtonsVariant({
262
268
  <div className={cn('flex md:hidden', className)}>
263
269
  {useSingleActionMobile && singleAction ? (
264
270
  renderActionButton(singleAction, { iconOnly: true })
265
- ) : (
266
- <ActionsMenuDropdown
267
- groups={[
268
- { items: actions.flatMap(actionToMenuItems) },
269
- ...(menuActions ?? []),
270
- ]}
271
- />
272
- )}
271
+ ) : hasMobileMenuItems ? (
272
+ <ActionsMenuDropdown groups={mobileMenuGroups} />
273
+ ) : null}
273
274
  </div>
274
275
  </>
275
276
  )
@@ -328,6 +329,12 @@ function MenuPrimaryVariant({
328
329
  const desktopActions = actions.filter(a => !a.showOnlyMobile)
329
330
  const hasMenuActions = menuActions.some(g => g.items.length > 0)
330
331
 
332
+ const mobileMenuGroups = [
333
+ { items: actions.flatMap(actionToMenuItems) },
334
+ ...menuActions,
335
+ ]
336
+ const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0)
337
+
331
338
  return (
332
339
  <>
333
340
  <div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
@@ -341,12 +348,7 @@ function MenuPrimaryVariant({
341
348
  </div>
342
349
 
343
350
  <div className={cn('flex md:hidden', className)}>
344
- <ActionsMenuDropdown
345
- groups={[
346
- { items: actions.flatMap(actionToMenuItems) },
347
- ...menuActions,
348
- ]}
349
- />
351
+ {hasMobileMenuItems ? <ActionsMenuDropdown groups={mobileMenuGroups} /> : null}
350
352
  </div>
351
353
  </>
352
354
  )
@@ -0,0 +1,89 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import type React from "react";
3
+
4
+ import { ApprovalRequestMessage } from "../components/chat/approval-request-message";
5
+ import type { ApprovalRequestData } from "../components/chat/types";
6
+
7
+ /**
8
+ * CLIENT (end-user Fae desktop app) variant of the single-command approval
9
+ * card — Figma node 203-11947 ("fae-approval-block").
10
+ *
11
+ * Same component and props as the Admin card, driven by `variant="client"`.
12
+ * The end client sees ONLY the BE-generated title (`explanation`) plus the
13
+ * Approve/Reject buttons, or the full-text resolved pill
14
+ * ("APPROVED BY JOHN SMITH"). The raw command is never rendered.
15
+ */
16
+
17
+ const baseData: ApprovalRequestData = {
18
+ command: 'brew install --cask slack',
19
+ requestId: "req-client-1",
20
+ approvalType: "CLIENT",
21
+ // BE-generated human-readable title — the ONLY content the client sees.
22
+ explanation: "Install Slack via Homebrew Cask on macOS",
23
+ };
24
+
25
+ const plainDecorator =
26
+ (width?: number) => (Story: React.ComponentType) => (
27
+ <div style={{ maxWidth: width ?? 400, background: "var(--color-bg)" }}>
28
+ <Story />
29
+ </div>
30
+ );
31
+
32
+ const meta = {
33
+ title: "Chat/Client/ApprovalRequestMessage",
34
+ component: ApprovalRequestMessage,
35
+ tags: ["autodocs"],
36
+ args: { variant: "client" },
37
+ parameters: {
38
+ docs: {
39
+ description: {
40
+ component:
41
+ "CLIENT (Fae end-user) single-command approval card — title-only with Approve/Reject or a full-text status pill ('APPROVED BY {NAME}'). The raw command is never shown. Gated by variant='client' (Figma 203-11947).",
42
+ },
43
+ },
44
+ },
45
+ decorators: [plainDecorator()],
46
+ } satisfies Meta<typeof ApprovalRequestMessage>;
47
+
48
+ export default meta;
49
+ type Story = StoryObj<typeof meta>;
50
+
51
+ /** Pending — BE title + Approve / Reject buttons. No command block. */
52
+ export const Pending: Story = {
53
+ args: {
54
+ data: baseData,
55
+ status: "pending",
56
+ onApprove: () => {},
57
+ onReject: () => {},
58
+ },
59
+ };
60
+
61
+ /** Approved with resolver — green "APPROVED BY JOHN SMITH" pill. */
62
+ export const ApprovedByUser: Story = {
63
+ args: { data: baseData, status: "approved", resolvedByName: "John Smith" },
64
+ };
65
+
66
+ /** Approved without a resolver name — plain "APPROVED" pill (system action). */
67
+ export const Approved: Story = {
68
+ args: { data: baseData, status: "approved" },
69
+ };
70
+
71
+ /** Rejected with resolver — red "REJECTED BY JOHN SMITH" pill. */
72
+ export const RejectedByUser: Story = {
73
+ args: { data: baseData, status: "rejected", resolvedByName: "John Smith" },
74
+ };
75
+
76
+ /** Cancelled — grey status pill. */
77
+ export const Cancelled: Story = {
78
+ args: { data: baseData, status: "cancelled" },
79
+ };
80
+
81
+ /** Missing BE title — falls back to the "Approval required" placeholder. */
82
+ export const NoTitleFallback: Story = {
83
+ args: {
84
+ data: { ...baseData, explanation: undefined },
85
+ status: "pending",
86
+ onApprove: () => {},
87
+ onReject: () => {},
88
+ },
89
+ };
@@ -0,0 +1,219 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import type React from "react";
3
+
4
+ import { ChatMessageEnhanced } from "../components/chat/chat-message-enhanced";
5
+ import { ChatTypingIndicator } from "../components/chat/chat-typing-indicator";
6
+ import type { MessageSegment } from "../components/chat/types";
7
+
8
+ /**
9
+ * `<ChatMessageEnhanced>` — THE chat message renderer used by
10
+ * ChatMessageList / EmbeddableChat. Renders the avatar + name + timestamp
11
+ * header (Fae pink / Mingo cyan / user grey) and every message-segment type:
12
+ * text (markdown), thinking, tool_execution, approval_request/batch, error,
13
+ * context_compaction.
14
+ *
15
+ * The Fae conversation scenes reproduce the Fae desktop-app flow: greeting →
16
+ * user request → approval block → typing dots (Figma "fae-approval-block").
17
+ */
18
+
19
+ // Fixed time-of-day so the header always reads "2:47 PM" — messages dated
20
+ // today render time-only (see formatMessageTimestamp).
21
+ const at247 = () => {
22
+ const d = new Date();
23
+ d.setHours(14, 47, 0, 0);
24
+ return d;
25
+ };
26
+
27
+ const panelDecorator = (Story: React.ComponentType) => (
28
+ <div
29
+ style={{ maxWidth: 640, background: "var(--color-bg)" }}
30
+ className="p-[var(--spacing-system-m)]"
31
+ >
32
+ <Story />
33
+ </div>
34
+ );
35
+
36
+ const meta = {
37
+ title: "Chat/ChatMessageEnhanced",
38
+ component: ChatMessageEnhanced,
39
+ tags: ["autodocs"],
40
+ parameters: {
41
+ docs: {
42
+ description: {
43
+ component:
44
+ "Single chat message row — avatar + colored author name + timestamp header, plus the full segment renderer (text/thinking/tool/approval/error/compaction). Used by ChatMessageList in both the admin (Mingo) and client (Fae) chats.",
45
+ },
46
+ },
47
+ },
48
+ decorators: [panelDecorator],
49
+ } satisfies Meta<typeof ChatMessageEnhanced>;
50
+
51
+ export default meta;
52
+ type Story = StoryObj<typeof meta>;
53
+
54
+ /** Assistant (Fae) text message — pink mono name + timestamp. */
55
+ export const AssistantFae: Story = {
56
+ args: {
57
+ role: "assistant",
58
+ name: "Fae",
59
+ content: "Good morning! How can I help you today?",
60
+ timestamp: at247(),
61
+ },
62
+ };
63
+
64
+ /** Assistant (Mingo) text message — cyan mono name. */
65
+ export const AssistantMingo: Story = {
66
+ args: {
67
+ role: "assistant",
68
+ name: "Mingo",
69
+ assistantType: "mingo",
70
+ content: "I've pulled the device list for Acme Corp — 3 endpoints are offline.",
71
+ timestamp: at247(),
72
+ },
73
+ };
74
+
75
+ /** User message — grey mono name, no avatar block until user avatars ship. */
76
+ export const UserMessage: Story = {
77
+ args: {
78
+ role: "user",
79
+ name: "John Smith",
80
+ content: "hey can u install slack on my mac",
81
+ timestamp: at247(),
82
+ },
83
+ };
84
+
85
+ /**
86
+ * Fae flow, step 1 — greeting, user request, and the assistant "typing"
87
+ * row (empty message + `<ChatTypingIndicator>`, as the Fae desktop host
88
+ * renders it while the reply streams in).
89
+ */
90
+ export const FaeConversationTyping: Story = {
91
+ args: { role: "assistant", content: "" },
92
+ render: () => (
93
+ <div className="flex flex-col">
94
+ <ChatMessageEnhanced
95
+ role="assistant"
96
+ name="Fae"
97
+ content="Good morning! How can I help you today?"
98
+ timestamp={at247()}
99
+ />
100
+ <ChatMessageEnhanced
101
+ role="user"
102
+ name="John Smith"
103
+ content="hey can u install slack on my mac"
104
+ timestamp={at247()}
105
+ />
106
+ <ChatMessageEnhanced role="assistant" name="Fae" content="" isTyping timestamp={at247()} />
107
+ <ChatTypingIndicator size="sm" />
108
+ </div>
109
+ ),
110
+ };
111
+
112
+ const approvedSegments: MessageSegment[] = [
113
+ { type: "text", text: "On it! Just need your quick approval here." },
114
+ {
115
+ type: "approval_request",
116
+ data: {
117
+ command: "brew install --cask slack",
118
+ requestId: "req-1",
119
+ approvalType: "CLIENT",
120
+ explanation: "Install Slack via Homebrew Cask on macOS",
121
+ },
122
+ status: "approved",
123
+ resolvedByName: "John Smith",
124
+ },
125
+ ];
126
+
127
+ /**
128
+ * Fae flow, step 2 — the reply carries an `approval_request` segment
129
+ * rendered as the CLIENT card with the resolved "APPROVED BY JOHN SMITH"
130
+ * pill (`approvalVariant="client"`), then Fae keeps typing.
131
+ */
132
+ export const FaeConversationApproved: Story = {
133
+ args: { role: "assistant", content: "" },
134
+ render: () => (
135
+ <div className="flex flex-col">
136
+ <ChatMessageEnhanced
137
+ role="assistant"
138
+ name="Fae"
139
+ content="Good morning! How can I help you today?"
140
+ timestamp={at247()}
141
+ />
142
+ <ChatMessageEnhanced
143
+ role="user"
144
+ name="John Smith"
145
+ content="hey can u install slack on my mac"
146
+ timestamp={at247()}
147
+ />
148
+ <ChatMessageEnhanced
149
+ role="assistant"
150
+ name="Fae"
151
+ content={approvedSegments}
152
+ approvalVariant="client"
153
+ timestamp={at247()}
154
+ />
155
+ <ChatMessageEnhanced role="assistant" name="Fae" content="" isTyping timestamp={at247()} />
156
+ <ChatTypingIndicator size="sm" />
157
+ </div>
158
+ ),
159
+ };
160
+
161
+ /** Pending approval inside a message — Approve/Reject buttons (client card). */
162
+ export const PendingApprovalSegment: Story = {
163
+ args: {
164
+ role: "assistant",
165
+ name: "Fae",
166
+ approvalVariant: "client",
167
+ timestamp: at247(),
168
+ content: [
169
+ { type: "text", text: "On it! Just need your quick approval here." },
170
+ {
171
+ type: "approval_request",
172
+ data: {
173
+ command: "brew install --cask slack",
174
+ requestId: "req-2",
175
+ approvalType: "CLIENT",
176
+ explanation: "Install Slack via Homebrew Cask on macOS",
177
+ },
178
+ status: "pending",
179
+ onApprove: () => {},
180
+ onReject: () => {},
181
+ },
182
+ ] satisfies MessageSegment[],
183
+ },
184
+ };
185
+
186
+ /** Every remaining segment type in one message — thinking, tool execution, context compaction, error. */
187
+ export const AllSegmentTypes: Story = {
188
+ args: {
189
+ role: "assistant",
190
+ name: "Fae",
191
+ timestamp: at247(),
192
+ content: [
193
+ {
194
+ type: "thinking",
195
+ text: "The user wants Slack installed. Homebrew is present on the endpoint, so `brew install --cask slack` is the cleanest path.",
196
+ },
197
+ { type: "text", text: "Installing Slack now." },
198
+ {
199
+ type: "tool_execution",
200
+ data: {
201
+ type: "EXECUTED_TOOL",
202
+ integratedToolType: "OPENFRAME_RMM",
203
+ toolFunction: "run_script",
204
+ toolTitle: "Install Slack via Homebrew Cask",
205
+ parameters: { command: "brew install --cask slack" },
206
+ result: "🍺 slack was successfully installed!",
207
+ success: true,
208
+ },
209
+ },
210
+ { type: "context_compaction", status: "completed" },
211
+ {
212
+ type: "error",
213
+ title: "Notification delivery failed",
214
+ details: "Slack webhook returned 429 — the completion ping was skipped. The install itself succeeded.",
215
+ },
216
+ { type: "text", text: "Done — Slack is installed and ready to use." },
217
+ ] satisfies MessageSegment[],
218
+ },
219
+ };
@@ -0,0 +1,51 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import type React from "react";
3
+
4
+ import { ChatTypingIndicator } from "../components/chat/chat-typing-indicator";
5
+
6
+ /**
7
+ * Three pulsing dots shown while the assistant reply streams in — the
8
+ * "Fae is typing" affordance under the message header. Also rendered
9
+ * inside `<ChatInput>` (size="sm") while a send is in flight.
10
+ */
11
+
12
+ const plainDecorator = (Story: React.ComponentType) => (
13
+ <div style={{ maxWidth: 400, background: "var(--color-bg)" }} className="p-4">
14
+ <Story />
15
+ </div>
16
+ );
17
+
18
+ const meta = {
19
+ title: "Chat/ChatTypingIndicator",
20
+ component: ChatTypingIndicator,
21
+ tags: ["autodocs"],
22
+ parameters: {
23
+ docs: {
24
+ description: {
25
+ component:
26
+ "Pulsing three-dot typing indicator. Sizes sm/md/lg; optional 'Assistant is typing' label; dot color overridable via dotClassName.",
27
+ },
28
+ },
29
+ },
30
+ decorators: [plainDecorator],
31
+ } satisfies Meta<typeof ChatTypingIndicator>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ /** Default — medium dots, no label. */
37
+ export const Default: Story = {};
38
+
39
+ /** Small — as rendered inside ChatInput while a send is in flight. */
40
+ export const Small: Story = { args: { size: "sm" } };
41
+
42
+ /** Large dots. */
43
+ export const Large: Story = { args: { size: "lg" } };
44
+
45
+ /** With the "Assistant is typing" label. */
46
+ export const WithText: Story = { args: { showText: true } };
47
+
48
+ /** Muted dots via dotClassName override. */
49
+ export const MutedDots: Story = {
50
+ args: { dotClassName: "bg-ods-text-secondary" },
51
+ };
@@ -0,0 +1,44 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import type React from "react";
3
+
4
+ import { ContextCompactionDisplay } from "../components/chat/context-compaction-display";
5
+
6
+ /**
7
+ * Inline status pill for `context_compaction` message segments — shown when
8
+ * the conversation hits the context limit and earlier messages get
9
+ * summarized. `started` shows a dots loader, `completed` a green check.
10
+ */
11
+
12
+ const plainDecorator = (Story: React.ComponentType) => (
13
+ <div style={{ maxWidth: 640, background: "var(--color-bg)" }} className="p-4">
14
+ <Story />
15
+ </div>
16
+ );
17
+
18
+ const meta = {
19
+ title: "Chat/ContextCompactionDisplay",
20
+ component: ContextCompactionDisplay,
21
+ tags: ["autodocs"],
22
+ parameters: {
23
+ docs: {
24
+ description: {
25
+ component:
26
+ "Renders a `context_compaction` message segment — 'Context limit reached…' with a dots loader while running, 'Earlier context summarized.' with a check once done.",
27
+ },
28
+ },
29
+ },
30
+ decorators: [plainDecorator],
31
+ } satisfies Meta<typeof ContextCompactionDisplay>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ /** In progress — summarizing earlier messages. */
37
+ export const Started: Story = {
38
+ args: { status: "started" },
39
+ };
40
+
41
+ /** Finished — earlier context summarized. */
42
+ export const Completed: Story = {
43
+ args: { status: "completed" },
44
+ };
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import type React from "react";
3
+
4
+ import { ErrorMessageDisplay } from "../components/chat/error-message-display";
5
+
6
+ /**
7
+ * Inline error card for `error` message segments — mono uppercase title
8
+ * with an alert icon; optional details expand under the chevron.
9
+ * `type` tints the icon: error (red) / warning (yellow) / info (grey).
10
+ */
11
+
12
+ const plainDecorator = (Story: React.ComponentType) => (
13
+ <div style={{ maxWidth: 640, background: "var(--color-bg)" }} className="p-4">
14
+ <Story />
15
+ </div>
16
+ );
17
+
18
+ const meta = {
19
+ title: "Chat/ErrorMessageDisplay",
20
+ component: ErrorMessageDisplay,
21
+ tags: ["autodocs"],
22
+ parameters: {
23
+ docs: {
24
+ description: {
25
+ component:
26
+ "Renders an `error` message segment — collapsed mono title row, expandable details. Icon tint follows type: error / warning / info.",
27
+ },
28
+ },
29
+ },
30
+ decorators: [plainDecorator],
31
+ } satisfies Meta<typeof ErrorMessageDisplay>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ /** Error with expandable details. */
37
+ export const WithDetails: Story = {
38
+ args: {
39
+ title: "Tool execution failed",
40
+ details:
41
+ "run_script exited with code 1: brew not found on this endpoint. Install Homebrew first or use the direct .dmg fallback.",
42
+ },
43
+ };
44
+
45
+ /** Title only — no chevron, row is not clickable. */
46
+ export const TitleOnly: Story = {
47
+ args: { title: "Stream disconnected — retrying" },
48
+ };
49
+
50
+ /** Warning tint. */
51
+ export const Warning: Story = {
52
+ args: {
53
+ type: "warning",
54
+ title: "Approval expired",
55
+ details: "The approval request timed out after 15 minutes and was cancelled automatically.",
56
+ },
57
+ };
58
+
59
+ /** Info tint. */
60
+ export const Info: Story = {
61
+ args: {
62
+ type: "info",
63
+ title: "Session resumed",
64
+ details: "Reconnected to the stream; missed chunks were replayed from history.",
65
+ },
66
+ };
@@ -0,0 +1,63 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import type React from "react";
3
+
4
+ import { ThinkingDisplay } from "../components/chat/thinking-display";
5
+
6
+ /**
7
+ * Collapsible "Thought" block for `thinking` message segments — muted
8
+ * markdown inside a bordered card, one line collapsed, expandable via the
9
+ * chevron. While the segment is still streaming the label reads "Thinking"
10
+ * and the collapse transition is disabled.
11
+ */
12
+
13
+ const SHORT_THOUGHT =
14
+ "The user wants Slack installed. Homebrew is present, so `brew install --cask slack` is the cleanest path.";
15
+
16
+ const LONG_THOUGHT = `The user asked to install Slack on their Mac.
17
+
18
+ **Options considered:**
19
+
20
+ 1. Direct \`.dmg\` download — needs manual mounting and copying, hard to automate.
21
+ 2. Homebrew Cask — \`brew install --cask slack\`, idempotent and scriptable.
22
+ 3. Mac App Store via \`mas\` — requires the user to be signed in to the store.
23
+
24
+ Homebrew is already present on this endpoint (verified in the device inventory), so option 2 is the cleanest path. The install needs client approval because it modifies installed software.`;
25
+
26
+ const plainDecorator = (Story: React.ComponentType) => (
27
+ <div style={{ maxWidth: 640, background: "var(--color-bg)" }} className="p-4">
28
+ <Story />
29
+ </div>
30
+ );
31
+
32
+ const meta = {
33
+ title: "Chat/ThinkingDisplay",
34
+ component: ThinkingDisplay,
35
+ tags: ["autodocs"],
36
+ parameters: {
37
+ docs: {
38
+ description: {
39
+ component:
40
+ "Renders a `thinking` message segment — collapsed single-line 'Thought' card that expands to the full muted-markdown reasoning. Streaming state shows 'Thinking' with a dots loader.",
41
+ },
42
+ },
43
+ },
44
+ decorators: [plainDecorator],
45
+ } satisfies Meta<typeof ThinkingDisplay>;
46
+
47
+ export default meta;
48
+ type Story = StoryObj<typeof meta>;
49
+
50
+ /** Finished thought — collapsed to one line, expandable. */
51
+ export const Thought: Story = {
52
+ args: { text: SHORT_THOUGHT },
53
+ };
54
+
55
+ /** Streaming — "Thinking" label, transition disabled while tokens arrive. */
56
+ export const Streaming: Story = {
57
+ args: { text: SHORT_THOUGHT, isStreaming: true },
58
+ };
59
+
60
+ /** Long markdown thought — headings, list, inline code; expand to read. */
61
+ export const LongMarkdown: Story = {
62
+ args: { text: LONG_THOUGHT },
63
+ };