ai-design-system 0.1.74 → 0.1.77

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 (57) hide show
  1. package/components/ai-elements/ExpoAppPreview/ExpoAppPreview.tsx +112 -0
  2. package/components/ai-elements/ExpoAppPreview/index.ts +2 -0
  3. package/components/ai-elements/ExpoAppPreview/interfaces.ts +58 -0
  4. package/components/ai-elements/checkpoint.tsx +86 -0
  5. package/components/ai-elements/edge.tsx +41 -5
  6. package/components/blocks/AIConversation/AIConversation.stories.tsx +43 -0
  7. package/components/blocks/AIConversation/AIConversation.tsx +118 -77
  8. package/components/blocks/PreviewCanvas/index.ts +2 -0
  9. package/components/blocks/PreviewCanvas/interfaces.ts +21 -0
  10. package/components/blocks/PreviewCanvas/preview-layout.ts +112 -0
  11. package/components/blocks/WorkflowCanvas/WorkflowCanvas.stories.tsx +60 -0
  12. package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +12 -1
  13. package/components/blocks/WorkflowCanvas/index.ts +2 -2
  14. package/components/blocks/index.ts +5 -0
  15. package/components/composites/AuthCard/AuthCard.stories.tsx +65 -0
  16. package/components/composites/AuthCard/AuthCard.tsx +360 -0
  17. package/components/composites/AuthCard/index.ts +2 -0
  18. package/components/composites/DevicePreviewNode/DevicePreviewNode.stories.tsx +214 -0
  19. package/components/composites/DevicePreviewNode/DevicePreviewNode.tsx +250 -0
  20. package/components/composites/DevicePreviewNode/ExpoAppPreview.stories.tsx +228 -0
  21. package/components/composites/DevicePreviewNode/index.ts +12 -0
  22. package/components/composites/DevicePreviewNode/interfaces.ts +98 -0
  23. package/components/composites/DevicePreviewToolbar/DevicePreviewToolbar.stories.tsx +81 -0
  24. package/components/composites/DevicePreviewToolbar/DevicePreviewToolbar.tsx +127 -0
  25. package/components/composites/DevicePreviewToolbar/index.ts +6 -0
  26. package/components/composites/DevicePreviewToolbar/interfaces.ts +38 -0
  27. package/components/composites/PromptInput/PromptInput.tsx +17 -0
  28. package/components/composites/index.ts +12 -0
  29. package/components/features/AuthFeature/AuthFeature.behaviors.stories.tsx +80 -0
  30. package/components/features/AuthFeature/AuthFeature.mocks.ts +41 -0
  31. package/components/features/AuthFeature/AuthFeature.stories.tsx +102 -0
  32. package/components/features/AuthFeature/AuthFeature.tsx +186 -0
  33. package/components/features/AuthFeature/README.md +33 -0
  34. package/components/features/AuthFeature/index.ts +10 -0
  35. package/components/features/AuthFeature/useAuthFeature.d.ts +57 -0
  36. package/components/features/AuthFeature/useAuthFeature.mock.ts +113 -0
  37. package/components/features/ChatPanel/ChatPanel.stories.tsx +2 -0
  38. package/components/features/ChatPanel/ChatPanel.tsx +21 -0
  39. package/components/features/ChatPanel/useChatPanel.d.ts +3 -0
  40. package/components/features/ChatPanel/useChatPanel.mock.ts +12 -0
  41. package/components/features/NodeEditor/NodeEditor.stories.tsx +409 -0
  42. package/components/features/NodeEditor/NodeEditor.tsx +5 -0
  43. package/components/features/NodeEditor/PREVIEW_MANIFEST_MENTAL_MODEL.md +197 -0
  44. package/components/features/NodeEditor/bookgeek.preview.json +311 -0
  45. package/components/features/PageLayout/PageLayout.stories.tsx +2 -2
  46. package/components/features/index.ts +5 -0
  47. package/components/index.ts +15 -3
  48. package/dist/index.cjs +1139 -98
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.css +131 -0
  51. package/dist/index.d.ts +410 -39
  52. package/dist/index.js +1135 -102
  53. package/dist/index.js.map +1 -1
  54. package/{components/blocks/WorkflowCanvas → lib/workflow}/interfaces.ts +4 -1
  55. package/lib/workflow/layout-engine.ts +105 -0
  56. package/package.json +1 -1
  57. package/components/blocks/WorkflowCanvas/layout-engine.ts +0 -77
@@ -0,0 +1,112 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+ import type { ExpoAppPreviewProps } from "./interfaces"
4
+
5
+ /**
6
+ * ExpoAppPreview Block
7
+ *
8
+ * A generic, full-bleed sandboxed iframe container for rendering an Expo /
9
+ * React Native app compiled to web. It is intentionally unopinionated about
10
+ * where the app is served from: `src` may point at a Metro dev server (live
11
+ * reload during development) or a static export (published app).
12
+ *
13
+ * ## Security model
14
+ * The default sandbox retains same-origin (`allow-same-origin`) per the
15
+ * waypoint preview contract. As documented on the props, `allow-scripts`
16
+ * combined with `allow-same-origin` is not a security boundary against the
17
+ * embedded app; pass a sandbox without `allow-same-origin` for untrusted
18
+ * content.
19
+ *
20
+ * ## Host <-> app messaging
21
+ * `onMessage` forwards `message` events whose `event.source` is this iframe's
22
+ * window. It is an optional passthrough of a browser primitive — standalone
23
+ * previews render without it.
24
+ */
25
+ export const ExpoAppPreview = React.memo<ExpoAppPreviewProps>(
26
+ ({
27
+ src,
28
+ title = "App preview",
29
+ sandbox = "allow-scripts allow-same-origin allow-forms allow-popups",
30
+ allow,
31
+ loading = false,
32
+ loadingLabel = "Loading app…",
33
+ isEmpty = false,
34
+ emptyLabel = "No preview available",
35
+ error = null,
36
+ errorLabel = "Preview unavailable",
37
+ onLoad,
38
+ onError,
39
+ onMessage,
40
+ registerIframe,
41
+ className,
42
+ iframeClassName,
43
+ ...props
44
+ }) => {
45
+ const iframeRef = React.useRef<HTMLIFrameElement>(null)
46
+ const onMessageRef = React.useRef(onMessage)
47
+ onMessageRef.current = onMessage
48
+
49
+ React.useEffect(() => {
50
+ if (!registerIframe) return undefined
51
+ registerIframe(iframeRef.current)
52
+ return () => registerIframe(null)
53
+ }, [registerIframe])
54
+
55
+ React.useEffect(() => {
56
+ if (!onMessageRef.current) return undefined
57
+ const handler = (event: MessageEvent): void => {
58
+ if (event.source !== iframeRef.current?.contentWindow) return
59
+ onMessageRef.current?.(event)
60
+ }
61
+ window.addEventListener("message", handler)
62
+ return () => window.removeEventListener("message", handler)
63
+ }, [])
64
+
65
+ return (
66
+ <div
67
+ className={cn(
68
+ "relative h-full w-full min-h-0 overflow-hidden bg-background",
69
+ className,
70
+ )}
71
+ {...props}
72
+ >
73
+ <iframe
74
+ ref={iframeRef}
75
+ src={src}
76
+ title={title}
77
+ sandbox={sandbox}
78
+ allow={allow}
79
+ onLoad={onLoad}
80
+ onError={onError}
81
+ className={cn("h-full w-full border-0", iframeClassName)}
82
+ />
83
+
84
+ {loading && (
85
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-background/80">
86
+ <div
87
+ className="h-8 w-8 animate-spin rounded-full border-2 border-primary/30 border-t-primary"
88
+ role="status"
89
+ aria-label={loadingLabel}
90
+ />
91
+ <span className="text-sm text-muted-foreground">{loadingLabel}</span>
92
+ </div>
93
+ )}
94
+
95
+ {!loading && isEmpty && (
96
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-1 bg-background/80">
97
+ <span className="text-sm font-medium text-foreground">{emptyLabel}</span>
98
+ </div>
99
+ )}
100
+
101
+ {!loading && !isEmpty && error && (
102
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-1 bg-background/80">
103
+ <span className="text-sm font-medium text-destructive">{errorLabel}</span>
104
+ <span className="max-w-md text-center text-sm text-muted-foreground">{error}</span>
105
+ </div>
106
+ )}
107
+ </div>
108
+ )
109
+ },
110
+ )
111
+
112
+ ExpoAppPreview.displayName = "ExpoAppPreview"
@@ -0,0 +1,2 @@
1
+ export { ExpoAppPreview } from "./ExpoAppPreview"
2
+ export type { ExpoAppPreviewProps } from "./interfaces"
@@ -0,0 +1,58 @@
1
+ import type * as React from "react"
2
+
3
+ /**
4
+ * Props for the ExpoAppPreview Block.
5
+ *
6
+ * A generic, dependency-free container that renders a full-bleed sandboxed
7
+ * iframe pointing at an Expo/React Native app compiled to web (dev server or
8
+ * static export). It owns no knowledge of how the `src` is produced or served —
9
+ * consumers (e.g. waypoint) compute the URL and pass it in.
10
+ */
11
+ export interface ExpoAppPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ /** Absolute or same-origin URL of the app to render in the iframe. */
13
+ src: string
14
+ /** Accessible name for the iframe (default: "App preview"). */
15
+ title?: string
16
+ /**
17
+ * iframe `sandbox` attribute. Defaults to ADR-033's contract:
18
+ * `allow-scripts allow-same-origin allow-forms allow-popups`.
19
+ *
20
+ * Security note: `allow-scripts` combined with `allow-same-origin` lets the
21
+ * embedded app remove its own sandbox restrictions, so this is an isolation
22
+ * boundary for reliable rendering, not a security boundary against the
23
+ * embedded app. Omit `allow-same-origin` when the embedded app is untrusted.
24
+ */
25
+ sandbox?: string
26
+ /** iframe `allow` attribute (e.g. feature policy overrides). Optional. */
27
+ allow?: string
28
+ /** Show a loading overlay (spinner + label) while true. */
29
+ loading?: boolean
30
+ /** Label shown with the spinner while loading. */
31
+ loadingLabel?: string
32
+ /** Show an empty-state overlay when true (e.g. no build deployed yet). */
33
+ isEmpty?: boolean
34
+ /** Label shown in the empty state. */
35
+ emptyLabel?: string
36
+ /** When truthy, show an error overlay with this message. */
37
+ error?: string | null
38
+ /** Label shown above the error message. */
39
+ errorLabel?: string
40
+ /** Fired when the iframe finishes loading. */
41
+ onLoad?: () => void
42
+ /** Fired when the iframe errors. */
43
+ onError?: () => void
44
+ /**
45
+ * Receives the iframe element once mounted (and `null` on unmount).
46
+ * Lets consumers capture a screenshot (e.g. html-to-image) without the
47
+ * block owning capture tooling.
48
+ */
49
+ registerIframe?: (el: HTMLIFrameElement | null) => void
50
+ /**
51
+ * Receives host-side `message` events originating from the iframe window
52
+ * (browser postMessage primitive; optional passthrough — not required for
53
+ * standalone rendering).
54
+ */
55
+ onMessage?: (event: MessageEvent) => void
56
+ /** Extra classes for the iframe element. */
57
+ iframeClassName?: string
58
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Separator } from "@/components/ui/separator";
5
+ import { cn } from "@/lib/utils";
6
+ import { BookmarkIcon } from "lucide-react";
7
+ import type { ComponentProps, HTMLAttributes } from "react";
8
+
9
+ // ── Checkpoint ────────────────────────────────────────────────────────────────
10
+ // Root container: renders [icon] [trigger] [Separator] in a flex row.
11
+ // The Separator stretches to fill remaining width automatically.
12
+
13
+ export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
14
+
15
+ export const Checkpoint = ({
16
+ className,
17
+ children,
18
+ ...props
19
+ }: CheckpointProps) => (
20
+ <div
21
+ className={cn(
22
+ "flex items-center gap-0.5 overflow-hidden text-muted-foreground",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ <Separator className="flex-1" />
29
+ </div>
30
+ );
31
+
32
+ // ── CheckpointIcon ────────────────────────────────────────────────────────────
33
+ // Defaults to BookmarkIcon. Pass children to use a custom icon.
34
+
35
+ export type CheckpointIconProps = ComponentProps<typeof BookmarkIcon>;
36
+
37
+ export const CheckpointIcon = ({
38
+ className,
39
+ children,
40
+ ...props
41
+ }: CheckpointIconProps) =>
42
+ children ? (
43
+ <span className={cn("size-4 shrink-0", className)}>{children}</span>
44
+ ) : (
45
+ <BookmarkIcon
46
+ aria-hidden="true"
47
+ className={cn("size-4 shrink-0", className)}
48
+ {...props}
49
+ />
50
+ );
51
+
52
+ // ── CheckpointTrigger ─────────────────────────────────────────────────────────
53
+ // The clickable restore button. Extends shadcn/ui Button.
54
+
55
+ export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
56
+ /** Optional tooltip shown on hover and used as aria-label when no children. */
57
+ tooltip?: string;
58
+ };
59
+
60
+ export const CheckpointTrigger = ({
61
+ children,
62
+ tooltip,
63
+ variant = "ghost",
64
+ size = "sm",
65
+ className,
66
+ ...props
67
+ }: CheckpointTriggerProps) => (
68
+ <Button
69
+ aria-label={tooltip ?? "Restore checkpoint"}
70
+ className={cn(
71
+ "h-8 gap-1.5 px-3 text-muted-foreground hover:text-foreground",
72
+ className
73
+ )}
74
+ size={size}
75
+ title={tooltip}
76
+ type="button"
77
+ variant={variant}
78
+ {...props}
79
+ >
80
+ {children}
81
+ </Button>
82
+ );
83
+
84
+ Checkpoint.displayName = "Checkpoint";
85
+ CheckpointIcon.displayName = "CheckpointIcon";
86
+ CheckpointTrigger.displayName = "CheckpointTrigger";
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  BaseEdge,
3
+ EdgeLabelRenderer,
3
4
  type EdgeProps,
4
5
  getSimpleBezierPath,
5
6
  getSmoothStepPath,
@@ -45,18 +46,37 @@ const Strict = ({
45
46
  targetPosition,
46
47
  markerEnd,
47
48
  style,
49
+ label,
48
50
  }: EdgeProps) => {
49
- const [edgePath] = getSmoothStepPath({
51
+ const [edgePath, labelX, labelY] = getSmoothStepPath({
50
52
  sourceX,
51
53
  sourceY,
52
54
  sourcePosition,
53
55
  targetX,
54
56
  targetY,
55
57
  targetPosition,
56
- borderRadius: 0,
58
+ borderRadius: 8,
57
59
  });
58
60
 
59
- return <BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />;
61
+ return (
62
+ <>
63
+ <BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
64
+ {label && (
65
+ <EdgeLabelRenderer>
66
+ <div
67
+ style={{
68
+ position: "absolute",
69
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
70
+ pointerEvents: "all",
71
+ }}
72
+ className="nodrag nopan rounded-md bg-secondary/90 px-2 py-0.5 text-[11px] font-medium text-secondary-foreground border border-border/80 shadow-xs backdrop-blur-xs"
73
+ >
74
+ {label}
75
+ </div>
76
+ </EdgeLabelRenderer>
77
+ )}
78
+ </>
79
+ );
60
80
  };
61
81
 
62
82
  const Animated = ({
@@ -69,15 +89,16 @@ const Animated = ({
69
89
  targetPosition,
70
90
  markerEnd,
71
91
  style,
92
+ label,
72
93
  }: EdgeProps) => {
73
- const [edgePath] = getSmoothStepPath({
94
+ const [edgePath, labelX, labelY] = getSmoothStepPath({
74
95
  sourceX,
75
96
  sourceY,
76
97
  sourcePosition,
77
98
  targetX,
78
99
  targetY,
79
100
  targetPosition,
80
- borderRadius: 0,
101
+ borderRadius: 8,
81
102
  });
82
103
 
83
104
  return (
@@ -86,6 +107,20 @@ const Animated = ({
86
107
  <circle fill="var(--primary)" r="4">
87
108
  <animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
88
109
  </circle>
110
+ {label && (
111
+ <EdgeLabelRenderer>
112
+ <div
113
+ style={{
114
+ position: "absolute",
115
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
116
+ pointerEvents: "all",
117
+ }}
118
+ className="nodrag nopan rounded-md bg-secondary/90 px-2 py-0.5 text-[11px] font-medium text-secondary-foreground border border-border/80 shadow-xs backdrop-blur-xs"
119
+ >
120
+ {label}
121
+ </div>
122
+ </EdgeLabelRenderer>
123
+ )}
89
124
  </>
90
125
  );
91
126
  };
@@ -95,3 +130,4 @@ export const Edge = {
95
130
  Temporary,
96
131
  Animated,
97
132
  };
133
+
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react'
2
+ import { fn } from '@storybook/test'
2
3
  import { AIConversation } from './AIConversation'
3
4
 
4
5
  const meta: Meta<typeof AIConversation> = {
@@ -161,4 +162,46 @@ export const UserOnly: Story = {
161
162
  },
162
163
  ],
163
164
  },
165
+ }
166
+
167
+ export const WithCheckpointRestore: Story = {
168
+ args: {
169
+ messages: [
170
+ {
171
+ id: '1',
172
+ type: 'human',
173
+ role: 'user',
174
+ content: 'What is React?',
175
+ avatarSrc: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
176
+ avatarName: 'User',
177
+ },
178
+ {
179
+ id: '2',
180
+ type: 'ai',
181
+ role: 'orchestrator',
182
+ content: 'React is a JavaScript library for building user interfaces. It was developed by Facebook and is now maintained by Meta and a community of developers.',
183
+ avatarSrc: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
184
+ avatarName: 'Coordinator',
185
+ checkpointId: 'cp-step-1',
186
+ },
187
+ {
188
+ id: '3',
189
+ type: 'human',
190
+ role: 'user',
191
+ content: 'How does component state work?',
192
+ avatarSrc: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
193
+ avatarName: 'User',
194
+ },
195
+ {
196
+ id: '4',
197
+ type: 'ai',
198
+ role: 'orchestrator',
199
+ content: 'State in React represents values that can change over time and trigger component re-renders when updated.',
200
+ avatarSrc: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
201
+ avatarName: 'Coordinator',
202
+ checkpointId: 'cp-step-2',
203
+ },
204
+ ],
205
+ onRestoreCheckpoint: fn(),
206
+ },
164
207
  }
@@ -6,6 +6,11 @@ import {
6
6
  ConversationScrollButton,
7
7
  type ConversationProps,
8
8
  } from "@/components/ai-elements/conversation"
9
+ import {
10
+ Checkpoint,
11
+ CheckpointIcon,
12
+ CheckpointTrigger,
13
+ } from "@/components/ai-elements/checkpoint"
9
14
  import type { ToolCall, SubAgent } from "@/components/composites"
10
15
  import { UserMessage } from "@/components/composites/UserMessage"
11
16
  import { SpecialistMessage } from "@/components/composites/SpecialistMessage"
@@ -40,6 +45,7 @@ interface AIMessage {
40
45
  subAgents?: SubAgent[];
41
46
  isLoading?: boolean;
42
47
  blocks?: AIMessageBlock[];
48
+ checkpointId?: string;
43
49
  }
44
50
 
45
51
  export interface AIConversationProps
@@ -77,6 +83,10 @@ export interface AIConversationProps
77
83
  * Receives the raw content string and returns a ReactNode.
78
84
  */
79
85
  renderSystemMessage?: (content: string) => React.ReactNode
86
+ /**
87
+ * Callback fired when user restores a conversation checkpoint
88
+ */
89
+ onRestoreCheckpoint?: (messageId: string, checkpointId: string) => void
80
90
  }
81
91
 
82
92
  /**
@@ -91,6 +101,7 @@ export const AIConversation = React.memo<AIConversationProps>(
91
101
  emptyState,
92
102
  onToolAction,
93
103
  renderSystemMessage,
104
+ onRestoreCheckpoint,
94
105
  ...conversationProps
95
106
  }) => {
96
107
  const isEmpty = React.useMemo(
@@ -209,6 +220,9 @@ export const AIConversation = React.memo<AIConversationProps>(
209
220
  }
210
221
  currentGroup.blocks = updatedBlocks;
211
222
  }
223
+ if (msg.checkpointId) {
224
+ currentGroup.checkpointId = msg.checkpointId;
225
+ }
212
226
  currentGroup.isLoading = msg.isLoading;
213
227
  } else {
214
228
  currentGroup = {
@@ -216,6 +230,7 @@ export const AIConversation = React.memo<AIConversationProps>(
216
230
  toolCalls: msg.toolCalls ? [...msg.toolCalls] : undefined,
217
231
  subAgents: msg.subAgents ? [...msg.subAgents] : undefined,
218
232
  blocks: msg.blocks ? [...msg.blocks] : undefined,
233
+ checkpointId: msg.checkpointId,
219
234
  };
220
235
  result.push(currentGroup);
221
236
  }
@@ -269,7 +284,7 @@ export const AIConversation = React.memo<AIConversationProps>(
269
284
  )
270
285
  }
271
286
 
272
- if (message.role === "orchestrator") {
287
+ if (message.role === "orchestrator" || message.role === "assistant" || (!message.role && message.type === "ai")) {
273
288
  // Extract sub-agents from the message
274
289
  const subAgents = message.subAgents || []
275
290
 
@@ -296,39 +311,65 @@ export const AIConversation = React.memo<AIConversationProps>(
296
311
  const isStreaming = message.isLoading || reasoningCalls.some((tc) => tc.status === "pending") || subAgents.some(sa => sa.status === "active") || directToolCalls.some(tc => tc.status === "pending")
297
312
 
298
313
  return (
299
- <OrchestratorMessage
300
- key={message.id}
301
- message={{
302
- id: message.id,
303
- content: displayContentStr,
304
- avatarSrc: message.avatarSrc,
305
- avatarName: message.avatarName,
306
- isLoading: message.isLoading && !hasReasoning,
307
- }}
308
- showAvatar={showAvatars && hasDisplayContent}
309
- >
310
- {/* Render reasoning-section for hidden tool results and subagents */}
311
- {hasReasoning && (
312
- <ReasoningDisplay
313
- content={reasoningText}
314
- items={message.blocks && message.blocks.length > 0 ? [] : reasoningCalls}
315
- isStreaming={isStreaming}
316
- onToolAction={onToolAction}
317
- defaultOpen={isStreaming || index === groupedMessages.length - 1}
318
- >
319
- {message.blocks && message.blocks.length > 0 ? (
320
- message.blocks.map((block, i) => {
321
- if (block.type === 'text') {
322
- return block.text && block.text.trim() ? (
323
- <div key={`text-${i}`} className="mb-4 text-muted-foreground whitespace-pre-wrap">
324
- {block.text}
325
- </div>
326
- ) : null;
327
- } else if (block.type === 'toolCall' && block.toolCall && block.toolCall.name !== 'ask_user' && block.toolCall.name !== 'ask_question') {
328
- return <ToolCallDisplay key={block.id} toolCall={block.toolCall} onToolAction={onToolAction} />;
329
- } else if (block.type === 'subAgent' && block.subAgent) {
330
- const subAgent = block.subAgent;
331
- return (
314
+ <React.Fragment key={message.id}>
315
+ <OrchestratorMessage
316
+ message={{
317
+ id: message.id,
318
+ content: displayContentStr,
319
+ avatarSrc: message.avatarSrc,
320
+ avatarName: message.avatarName,
321
+ isLoading: message.isLoading && !hasReasoning,
322
+ }}
323
+ showAvatar={showAvatars && hasDisplayContent}
324
+ >
325
+ {/* Render reasoning-section for hidden tool results and subagents */}
326
+ {hasReasoning && (
327
+ <ReasoningDisplay
328
+ content={reasoningText}
329
+ items={message.blocks && message.blocks.length > 0 ? [] : reasoningCalls}
330
+ isStreaming={isStreaming}
331
+ onToolAction={onToolAction}
332
+ defaultOpen={isStreaming || index === groupedMessages.length - 1}
333
+ >
334
+ {message.blocks && message.blocks.length > 0 ? (
335
+ message.blocks.map((block, i) => {
336
+ if (block.type === 'text') {
337
+ return block.text && block.text.trim() ? (
338
+ <div key={`text-${i}`} className="mb-4 text-muted-foreground whitespace-pre-wrap">
339
+ {block.text}
340
+ </div>
341
+ ) : null;
342
+ } else if (block.type === 'toolCall' && block.toolCall && block.toolCall.name !== 'ask_user' && block.toolCall.name !== 'ask_question') {
343
+ return <ToolCallDisplay key={block.id} toolCall={block.toolCall} onToolAction={onToolAction} />;
344
+ } else if (block.type === 'subAgent' && block.subAgent) {
345
+ const subAgent = block.subAgent;
346
+ return (
347
+ <SpecialistMessage
348
+ key={subAgent.id}
349
+ message={{
350
+ id: subAgent.id,
351
+ name: subAgent.subAgentName,
352
+ description: undefined,
353
+ input: subAgent.input,
354
+ content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
355
+ status: subAgent.status,
356
+ toolCalls: [],
357
+ }}
358
+ isNested={true}
359
+ />
360
+ );
361
+ }
362
+ return null;
363
+ })
364
+ ) : (
365
+ <>
366
+ {/* Render direct tool calls fallback */}
367
+ {directToolCalls.map((tc) => (
368
+ <ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
369
+ ))}
370
+
371
+ {/* Render specialist sub-agents fallback */}
372
+ {subAgents.map((subAgent) => (
332
373
  <SpecialistMessage
333
374
  key={subAgent.id}
334
375
  message={{
@@ -342,57 +383,57 @@ export const AIConversation = React.memo<AIConversationProps>(
342
383
  }}
343
384
  isNested={true}
344
385
  />
345
- );
346
- }
347
- return null;
348
- })
349
- ) : (
350
- <>
351
- {/* Render direct tool calls fallback */}
352
- {directToolCalls.map((tc) => (
353
- <ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
354
- ))}
355
-
356
- {/* Render specialist sub-agents fallback */}
357
- {subAgents.map((subAgent) => (
358
- <SpecialistMessage
359
- key={subAgent.id}
360
- message={{
361
- id: subAgent.id,
362
- name: subAgent.subAgentName,
363
- description: undefined,
364
- input: subAgent.input,
365
- content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
366
- status: subAgent.status,
367
- toolCalls: [],
368
- }}
369
- isNested={true}
370
- />
371
- ))}
372
- </>
373
- )}
374
- </ReasoningDisplay>
386
+ ))}
387
+ </>
388
+ )}
389
+ </ReasoningDisplay>
390
+ )}
391
+ </OrchestratorMessage>
392
+ {message.checkpointId && onRestoreCheckpoint && !message.isLoading && !hasReasoning && (
393
+ <Checkpoint>
394
+ <CheckpointIcon />
395
+ <CheckpointTrigger
396
+ onClick={() =>
397
+ onRestoreCheckpoint(message.id, message.checkpointId!)
398
+ }
399
+ >
400
+ Restore checkpoint
401
+ </CheckpointTrigger>
402
+ </Checkpoint>
375
403
  )}
376
- </OrchestratorMessage>
404
+ </React.Fragment>
377
405
  )
378
406
  }
379
407
 
380
408
  // Default to specialist message for any other AI messages
381
409
  return (
382
- <SpecialistMessage
383
- key={message.id}
384
- message={{
385
- id: message.id,
386
- name: message.avatarName || "Agent",
387
- content: contentStr,
388
- toolCalls: message.toolCalls?.filter((tc) => tc.name !== "task"),
389
- status: "completed",
390
- }}
391
- isNested={false}
392
- />
410
+ <React.Fragment key={message.id}>
411
+ <SpecialistMessage
412
+ message={{
413
+ id: message.id,
414
+ name: message.avatarName || "Agent",
415
+ content: contentStr,
416
+ toolCalls: message.toolCalls?.filter((tc) => tc.name !== "task"),
417
+ status: "completed",
418
+ }}
419
+ isNested={false}
420
+ />
421
+ {message.checkpointId && onRestoreCheckpoint && !message.isLoading && (
422
+ <Checkpoint>
423
+ <CheckpointIcon />
424
+ <CheckpointTrigger
425
+ onClick={() =>
426
+ onRestoreCheckpoint(message.id, message.checkpointId!)
427
+ }
428
+ >
429
+ Restore checkpoint
430
+ </CheckpointTrigger>
431
+ </Checkpoint>
432
+ )}
433
+ </React.Fragment>
393
434
  )
394
435
  }),
395
- [groupedMessages, showAvatars, onToolAction]
436
+ [groupedMessages, showAvatars, onToolAction, onRestoreCheckpoint]
396
437
  )
397
438
 
398
439
  return (
@@ -0,0 +1,2 @@
1
+ export * from './interfaces';
2
+ export * from './preview-layout';