ai-design-system 0.1.54 → 0.1.57

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.
@@ -22,7 +22,7 @@ type ReasoningContextValue = {
22
22
 
23
23
  const ReasoningContext = createContext<ReasoningContextValue | null>(null);
24
24
 
25
- const useReasoning = () => {
25
+ export const useReasoning = () => {
26
26
  const context = useContext(ReasoningContext);
27
27
  if (!context) {
28
28
  throw new Error("Reasoning components must be used within Reasoning");
@@ -124,14 +124,14 @@ export const Reasoning = memo(
124
124
 
125
125
  export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
126
126
 
127
- const getThinkingMessage = (isStreaming: boolean, duration?: number) => {
128
- if (isStreaming || duration === 0) {
127
+ export const getThinkingMessage = (isStreaming: boolean, duration?: number) => {
128
+ if (isStreaming) {
129
129
  return <Shimmer duration={1}>Thinking...</Shimmer>;
130
130
  }
131
- if (duration === undefined) {
132
- return <p>Thought for a few seconds</p>;
131
+ if (duration === undefined || duration === 0) {
132
+ return <span>Thought for a few seconds</span>;
133
133
  }
134
- return <p>Thought for {duration} seconds</p>;
134
+ return <span>Thought for {duration} seconds</span>;
135
135
  };
136
136
 
137
137
  export const ReasoningTrigger = memo(
@@ -11,6 +11,7 @@ import { UserMessage } from "@/components/composites/UserMessage"
11
11
  import { SpecialistMessage } from "@/components/composites/SpecialistMessage"
12
12
  import { OrchestratorMessage } from "@/components/composites/OrchestratorMessage"
13
13
  import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay"
14
+ import { ReasoningDisplay } from "@/components/composites/ReasoningDisplay"
14
15
  import { ApprovalCard } from "@/components/composites/ApprovalCard"
15
16
 
16
17
  /**
@@ -127,28 +128,49 @@ export const AIConversation = React.memo<AIConversationProps>(
127
128
 
128
129
  // Filter tool calls that aren't "task" type (those become sub-agents)
129
130
  // Also completely hide "ask_user" and "ask_question" tools so they are ONLY rendered in the prompt input area
130
- const directToolCalls =
131
+ const allToolCalls =
131
132
  message.toolCalls?.filter(
132
133
  (tc) => tc.name !== "task" && tc.name !== "ask_user"
133
134
  ) || []
134
135
 
136
+ // Split into reasoning tools (shown collapsed) and direct tools (shown normally)
137
+ const reasoningCalls = allToolCalls.filter((tc) => tc.visibility === "reasoning")
138
+ const directToolCalls = allToolCalls.filter((tc) => tc.visibility !== "reasoning")
139
+
135
140
  const hasContent = contentStr.trim() !== ""
136
- if (!hasContent && directToolCalls.length === 0 && subAgents.length === 0 && !message.isLoading) {
141
+ const hasReasoning = reasoningCalls.length > 0
142
+ const reasoningText = hasReasoning ? contentStr : undefined
143
+ const displayContentStr = hasReasoning ? "" : contentStr
144
+ const hasDisplayContent = displayContentStr.trim() !== ""
145
+
146
+ if (!hasDisplayContent && directToolCalls.length === 0 && reasoningCalls.length === 0 && subAgents.length === 0 && !message.isLoading) {
137
147
  return null;
138
148
  }
139
149
 
150
+ const isStreaming = reasoningCalls.some((tc) => tc.status === "pending")
151
+
140
152
  return (
141
153
  <OrchestratorMessage
142
154
  key={message.id}
143
155
  message={{
144
156
  id: message.id,
145
- content: contentStr,
157
+ content: displayContentStr,
146
158
  avatarSrc: message.avatarSrc,
147
159
  avatarName: message.avatarName,
148
- isLoading: message.isLoading,
160
+ isLoading: message.isLoading && !hasReasoning,
149
161
  }}
150
- showAvatar={showAvatars}
162
+ showAvatar={showAvatars && hasDisplayContent}
151
163
  >
164
+ {/* Render reasoning-section for hidden tool results */}
165
+ {hasReasoning && (
166
+ <ReasoningDisplay
167
+ content={reasoningText}
168
+ items={reasoningCalls}
169
+ isStreaming={isStreaming}
170
+ onToolAction={onToolAction}
171
+ />
172
+ )}
173
+
152
174
  {/* Render direct tool calls */}
153
175
  {directToolCalls.map((tc) => (
154
176
  <ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
@@ -2,6 +2,8 @@ import * as React from "react"
2
2
  import { SidebarTrigger } from "@/components/primitives/Sidebar"
3
3
  import { Separator } from "@/components/primitives/Separator"
4
4
  import { Tabs, TabsList, TabsTrigger } from "@/components/primitives/Tabs"
5
+ import { ChatToggleButton } from "@/components/composites/ChatToggleButton"
6
+ import { WorkflowSwitcher } from "@/components/composites/WorkflowSwitcher"
5
7
  import type { AppHeaderProps } from "./interfaces"
6
8
 
7
9
  export const AppHeader = React.memo<AppHeaderProps>(({
@@ -13,14 +15,16 @@ export const AppHeader = React.memo<AppHeaderProps>(({
13
15
  className,
14
16
  tabsPosition = 'center',
15
17
  showSidebarToggle = true,
16
- showTitle = true
18
+ showTitle = true,
19
+ workflowSwitcherProps,
20
+ chatToggleProps
17
21
  }) => {
18
22
  return (
19
23
  <header className={`flex h-14 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-14 ${className || ""}`}>
20
24
  <div className="grid w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center px-4 lg:px-6">
21
25
  <div className="min-w-0 flex items-center gap-1 lg:gap-2">
22
26
  {showSidebarToggle && <SidebarTrigger className="-ml-1" />}
23
- {showSidebarToggle && showTitle && title && (
27
+ {showSidebarToggle && (showTitle && title || workflowSwitcherProps || chatToggleProps) && (
24
28
  <Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
25
29
  )}
26
30
  {showTitle && title && (
@@ -28,6 +32,8 @@ export const AppHeader = React.memo<AppHeaderProps>(({
28
32
  ? <h1 className="max-w-[28rem] truncate text-base font-medium">{title}</h1>
29
33
  : title
30
34
  )}
35
+ {chatToggleProps && <ChatToggleButton {...chatToggleProps} />}
36
+ {workflowSwitcherProps && <WorkflowSwitcher {...workflowSwitcherProps} />}
31
37
  </div>
32
38
 
33
39
  <div className="justify-self-center">
@@ -1,4 +1,6 @@
1
1
  import type React from "react";
2
+ import type { WorkflowSwitcherProps } from "@/components/composites/WorkflowSwitcher/WorkflowSwitcher";
3
+ import type { ChatToggleButtonProps } from "@/components/composites/ChatToggleButton/ChatToggleButton";
2
4
 
3
5
  export interface TabItem {
4
6
  value: string;
@@ -15,4 +17,6 @@ export interface AppHeaderProps {
15
17
  className?: string;
16
18
  showSidebarToggle?: boolean;
17
19
  showTitle?: boolean;
20
+ workflowSwitcherProps?: WorkflowSwitcherProps;
21
+ chatToggleProps?: ChatToggleButtonProps;
18
22
  }
@@ -88,17 +88,36 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
88
88
  className,
89
89
  }) => {
90
90
  const [searchQuery, setSearchQuery] = React.useState("")
91
+ const [userExpanded, setUserExpanded] = React.useState<Set<string>>(defaultExpanded || new Set())
92
+
93
+ // Auto-expand parents when selectedPath changes
94
+ React.useEffect(() => {
95
+ if (selectedPath) {
96
+ const parts = selectedPath.split('/')
97
+ if (parts.length > 1) {
98
+ setUserExpanded(prev => {
99
+ const next = new Set(prev)
100
+ let currentPath = ''
101
+ for (let i = 0; i < parts.length - 1; i++) {
102
+ currentPath += (i === 0 ? '' : '/') + parts[i]
103
+ next.add(currentPath)
104
+ }
105
+ return next
106
+ })
107
+ }
108
+ }
109
+ }, [selectedPath])
91
110
 
92
111
  const filteredTree = React.useMemo(() => {
93
112
  return filterTree(tree, searchQuery)
94
113
  }, [tree, searchQuery])
95
114
 
96
- const expandedPaths = React.useMemo(() => {
115
+ const activeExpanded = React.useMemo(() => {
97
116
  if (searchQuery) {
98
117
  return getAllFolderPaths(filteredTree)
99
118
  }
100
- return defaultExpanded
101
- }, [searchQuery, filteredTree, defaultExpanded])
119
+ return userExpanded
120
+ }, [searchQuery, filteredTree, userExpanded])
102
121
 
103
122
  return (
104
123
  <div className={cn("flex flex-col rounded-lg border bg-background", className)}>
@@ -130,7 +149,8 @@ export const FileTreeExplorer = React.memo<FileTreeExplorerProps>(
130
149
  <div className="p-2">
131
150
  <FileTree
132
151
  className="border-none bg-transparent"
133
- defaultExpanded={expandedPaths}
152
+ expanded={activeExpanded}
153
+ onExpandedChange={setUserExpanded}
134
154
  selectedPath={selectedPath}
135
155
  onSelect={onSelect}
136
156
  >
@@ -0,0 +1,76 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { ReasoningDisplay } from './ReasoningDisplay'
3
+
4
+ const meta: Meta<typeof ReasoningDisplay> = {
5
+ title: 'Composites/ReasoningDisplay',
6
+ component: ReasoningDisplay,
7
+ parameters: {
8
+ layout: 'padded',
9
+ },
10
+ } satisfies Meta<typeof ReasoningDisplay>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ content: "Let me evaluate the user's request and read the necessary files to propose a solution.",
18
+ isStreaming: false,
19
+ items: [
20
+ {
21
+ id: 'tool_1',
22
+ name: 'read_file',
23
+ args: { file_path: '/workspace/src/main.tsx' },
24
+ result: 'import { App } from "./App"; ...',
25
+ status: 'completed',
26
+ visibility: 'reasoning',
27
+ uiVariant: 'link',
28
+ linkText: 'src/main.tsx',
29
+ linkAction: 'open-file',
30
+ },
31
+ {
32
+ id: 'tool_2',
33
+ name: 'read_file',
34
+ args: { file_path: '/workspace/src/App.tsx' },
35
+ result: 'export const App = () => <div>Hello</div>;',
36
+ status: 'completed',
37
+ visibility: 'reasoning',
38
+ uiVariant: 'link',
39
+ linkText: 'src/App.tsx',
40
+ linkAction: 'open-file',
41
+ }
42
+ ],
43
+ },
44
+ }
45
+
46
+ export const Streaming: Story = {
47
+ args: {
48
+ content: "I'll start by listing the directory contents...",
49
+ isStreaming: true,
50
+ items: [
51
+ {
52
+ id: 'tool_1',
53
+ name: 'list_dir',
54
+ args: { path: '/src' },
55
+ status: 'pending',
56
+ visibility: 'reasoning',
57
+ },
58
+ ],
59
+ },
60
+ }
61
+
62
+ export const OnlyTools: Story = {
63
+ args: {
64
+ isStreaming: false,
65
+ items: [
66
+ {
67
+ id: 'tool_1',
68
+ name: 'write_todos',
69
+ args: { todos: ['Fix layout', 'Update colors'] },
70
+ result: 'Todos written successfully.',
71
+ status: 'completed',
72
+ visibility: 'reasoning',
73
+ },
74
+ ],
75
+ },
76
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import {
4
+ Reasoning,
5
+ ReasoningTrigger,
6
+ } from "@/components/ai-elements/reasoning";
7
+ import { CollapsibleContent } from "@/components/primitives/Collapsible";
8
+ import { Response } from "@/components/ai-elements/response";
9
+ import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay";
10
+ import type { ToolCall } from "@/components/composites/ToolCallDisplay";
11
+
12
+ export interface ReasoningDisplayProps {
13
+ items: ToolCall[];
14
+ isStreaming?: boolean;
15
+ content?: string;
16
+ onToolAction?: (toolCall: ToolCall, action: string) => void;
17
+ }
18
+
19
+ export const ReasoningDisplay = ({
20
+ items,
21
+ isStreaming = false,
22
+ content,
23
+ onToolAction,
24
+ }: ReasoningDisplayProps) => {
25
+ if (items.length === 0 && !content) return null;
26
+
27
+ return (
28
+ <Reasoning isStreaming={isStreaming} defaultOpen={false}>
29
+ <ReasoningTrigger />
30
+ <CollapsibleContent
31
+ className="mt-4 flex flex-col gap-2 data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
32
+ >
33
+ {content && content.trim() && (
34
+ <div className="mb-4">
35
+ <Response className="grid gap-2">{content}</Response>
36
+ </div>
37
+ )}
38
+ <div className="flex flex-col gap-2">
39
+ {items.map((item) => (
40
+ <ToolCallDisplay key={item.id} toolCall={item} onToolAction={onToolAction} />
41
+ ))}
42
+ </div>
43
+ </CollapsibleContent>
44
+ </Reasoning>
45
+ );
46
+ };
@@ -0,0 +1,2 @@
1
+ export { ReasoningDisplay } from './ReasoningDisplay'
2
+ export type { ReasoningDisplayProps } from './ReasoningDisplay'
@@ -21,6 +21,7 @@ export interface ToolCall {
21
21
  args: Record<string, unknown>
22
22
  result?: string
23
23
  status: "pending" | "completed" | "error"
24
+ visibility?: "visible" | "reasoning"
24
25
  uiVariant?: "default" | "link"
25
26
  linkText?: string
26
27
  linkAction?: string
@@ -0,0 +1,32 @@
1
+ import * as React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { WorkflowSwitcher } from "./WorkflowSwitcher"
4
+
5
+ const meta: Meta<typeof WorkflowSwitcher> = {
6
+ title: "Composites/WorkflowSwitcher",
7
+ component: WorkflowSwitcher,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ args: {
12
+ workflows: [
13
+ { id: "1", name: "Alpha Workflow" },
14
+ { id: "2", name: "Beta Workflow" },
15
+ { id: "3", name: "Gamma Workflow" },
16
+ ],
17
+ currentWorkflowId: "1",
18
+ onSelectWorkflow: () => {},
19
+ },
20
+ }
21
+
22
+ export default meta
23
+ type Story = StoryObj<typeof WorkflowSwitcher>
24
+
25
+ export const Default: Story = {}
26
+
27
+ export const Empty: Story = {
28
+ args: {
29
+ workflows: [],
30
+ currentWorkflowId: undefined,
31
+ },
32
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from "react"
2
+ import { Button } from "@/components/primitives/Button"
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ } from "@/components/primitives/DropdownMenu"
9
+ import { Icon } from "@/components/primitives/Icon"
10
+
11
+ export interface WorkflowItem {
12
+ id: string
13
+ name: string
14
+ }
15
+
16
+ export interface WorkflowSwitcherProps {
17
+ workflows: WorkflowItem[]
18
+ currentWorkflowId?: string | null
19
+ onSelectWorkflow: (id: string) => void
20
+ className?: string
21
+ }
22
+
23
+ export const WorkflowSwitcher = React.memo<WorkflowSwitcherProps>(
24
+ ({ workflows, currentWorkflowId, onSelectWorkflow, className }) => {
25
+ const currentWorkflow = workflows.find((w) => w.id === currentWorkflowId)
26
+
27
+ return (
28
+ <div className={className}>
29
+ <DropdownMenu>
30
+ <DropdownMenuTrigger asChild>
31
+ <Button
32
+ className="h-9 border hover:bg-black/5 dark:hover:bg-white/5"
33
+ size="sm"
34
+ title="Select workflow"
35
+ variant="secondary"
36
+ >
37
+ <span className="truncate max-w-[150px]">
38
+ {currentWorkflow?.name ?? "Select Workflow"}
39
+ </span>
40
+ <Icon name="chevron-down" size="xs" className="ml-1 opacity-50 shrink-0" />
41
+ </Button>
42
+ </DropdownMenuTrigger>
43
+ <DropdownMenuContent align="start">
44
+ {workflows.map((w) => (
45
+ <DropdownMenuItem
46
+ className="flex items-center justify-between"
47
+ key={w.id}
48
+ onClick={() => onSelectWorkflow(w.id)}
49
+ >
50
+ <span className="truncate pr-4">{w.name}</span>
51
+ {w.id === currentWorkflowId && <Icon name="check" size="sm" className="ml-auto shrink-0" />}
52
+ </DropdownMenuItem>
53
+ ))}
54
+ {workflows.length === 0 && (
55
+ <div className="px-2 py-1.5 text-sm text-muted-foreground text-center">
56
+ No workflows found
57
+ </div>
58
+ )}
59
+ </DropdownMenuContent>
60
+ </DropdownMenu>
61
+ </div>
62
+ )
63
+ }
64
+ )
65
+
66
+ WorkflowSwitcher.displayName = "WorkflowSwitcher"
@@ -0,0 +1 @@
1
+ export * from "./WorkflowSwitcher"
@@ -115,11 +115,16 @@ export function WorkflowToolbar({
115
115
  {/* Left: workflow name text + version dropdown */}
116
116
  <div className="flex items-center gap-2">
117
117
  {/* Plain text title */}
118
- <div className="flex h-9 items-center rounded-md border bg-secondary px-3 text-secondary-foreground">
119
- <span className="truncate font-medium text-sm">
120
- {workflowName || "Untitled Workflow"}
121
- </span>
122
- </div>
118
+ {
119
+ workflowName && (
120
+ <div className="flex h-9 items-center rounded-md border bg-secondary px-3 text-secondary-foreground">
121
+ <span className="truncate font-medium text-sm">
122
+ {workflowName || ""}
123
+ </span>
124
+ </div>
125
+ )
126
+ }
127
+
123
128
 
124
129
  {/* Version selector — only shown when versions are provided */}
125
130
  {versions && versions.length > 0 && (
@@ -9,6 +9,10 @@
9
9
  export { ToolCallDisplay } from './ToolCallDisplay'
10
10
  export type { ToolCallDisplayProps, ToolCall } from './ToolCallDisplay'
11
11
 
12
+ // ReasoningDisplay Block
13
+ export { ReasoningDisplay } from './ReasoningDisplay'
14
+ export type { ReasoningDisplayProps } from './ReasoningDisplay'
15
+
12
16
  // AgentIndicator Block
13
17
  export { AgentIndicator } from './AgentIndicator'
14
18
  export type { AgentIndicatorProps, SubAgent } from './AgentIndicator'
@@ -186,3 +190,7 @@ export type { PromptInputBlockProps } from './PromptInput'
186
190
  // ChatToggleButton Composite
187
191
  export { ChatToggleButton } from './ChatToggleButton'
188
192
  export type { ChatToggleButtonProps } from './ChatToggleButton'
193
+
194
+ // WorkflowSwitcher Composite
195
+ export { WorkflowSwitcher } from './WorkflowSwitcher'
196
+ export type { WorkflowSwitcherProps, WorkflowItem } from './WorkflowSwitcher'
@@ -22,3 +22,5 @@ export type { ExternalToast, ToastT, ToasterProps } from 'sonner';
22
22
 
23
23
  // Utilities
24
24
  export { cn } from '@/lib/utils';
25
+ export { WorkflowSwitcher } from './composites';
26
+ export type { WorkflowSwitcherProps, WorkflowItem } from './composites';