@open-mercato/ai-assistant 0.4.2-canary-c02407ff85

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 (193) hide show
  1. package/AGENTS.md +1090 -0
  2. package/README.md +607 -0
  3. package/build.mjs +92 -0
  4. package/dist/di.js +8 -0
  5. package/dist/di.js.map +7 -0
  6. package/dist/frontend/components/CommandPalette/CommandFooter.js +80 -0
  7. package/dist/frontend/components/CommandPalette/CommandFooter.js.map +7 -0
  8. package/dist/frontend/components/CommandPalette/CommandHeader.js +53 -0
  9. package/dist/frontend/components/CommandPalette/CommandHeader.js.map +7 -0
  10. package/dist/frontend/components/CommandPalette/CommandInput.js +29 -0
  11. package/dist/frontend/components/CommandPalette/CommandInput.js.map +7 -0
  12. package/dist/frontend/components/CommandPalette/CommandItem.js +92 -0
  13. package/dist/frontend/components/CommandPalette/CommandItem.js.map +7 -0
  14. package/dist/frontend/components/CommandPalette/CommandPalette.js +244 -0
  15. package/dist/frontend/components/CommandPalette/CommandPalette.js.map +7 -0
  16. package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js +42 -0
  17. package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js.map +7 -0
  18. package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js +18 -0
  19. package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js.map +7 -0
  20. package/dist/frontend/components/CommandPalette/DebugPanel.js +215 -0
  21. package/dist/frontend/components/CommandPalette/DebugPanel.js.map +7 -0
  22. package/dist/frontend/components/CommandPalette/MessageBubble.js +64 -0
  23. package/dist/frontend/components/CommandPalette/MessageBubble.js.map +7 -0
  24. package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js +91 -0
  25. package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js.map +7 -0
  26. package/dist/frontend/components/CommandPalette/ToolCallDisplay.js +47 -0
  27. package/dist/frontend/components/CommandPalette/ToolCallDisplay.js.map +7 -0
  28. package/dist/frontend/components/CommandPalette/ToolChatPage.js +74 -0
  29. package/dist/frontend/components/CommandPalette/ToolChatPage.js.map +7 -0
  30. package/dist/frontend/components/CommandPalette/index.js +28 -0
  31. package/dist/frontend/components/CommandPalette/index.js.map +7 -0
  32. package/dist/frontend/constants.js +41 -0
  33. package/dist/frontend/constants.js.map +7 -0
  34. package/dist/frontend/hooks/index.js +13 -0
  35. package/dist/frontend/hooks/index.js.map +7 -0
  36. package/dist/frontend/hooks/useCommandPalette.js +1094 -0
  37. package/dist/frontend/hooks/useCommandPalette.js.map +7 -0
  38. package/dist/frontend/hooks/useMcpTools.js +66 -0
  39. package/dist/frontend/hooks/useMcpTools.js.map +7 -0
  40. package/dist/frontend/hooks/usePageContext.js +48 -0
  41. package/dist/frontend/hooks/usePageContext.js.map +7 -0
  42. package/dist/frontend/hooks/useRecentActions.js +56 -0
  43. package/dist/frontend/hooks/useRecentActions.js.map +7 -0
  44. package/dist/frontend/hooks/useRecentTools.js +55 -0
  45. package/dist/frontend/hooks/useRecentTools.js.map +7 -0
  46. package/dist/frontend/index.js +35 -0
  47. package/dist/frontend/index.js.map +7 -0
  48. package/dist/frontend/types.js +1 -0
  49. package/dist/frontend/types.js.map +7 -0
  50. package/dist/frontend/utils/index.js +7 -0
  51. package/dist/frontend/utils/index.js.map +7 -0
  52. package/dist/frontend/utils/toolMatcher.js +95 -0
  53. package/dist/frontend/utils/toolMatcher.js.map +7 -0
  54. package/dist/index.js +57 -0
  55. package/dist/index.js.map +7 -0
  56. package/dist/modules/ai_assistant/acl.js +14 -0
  57. package/dist/modules/ai_assistant/acl.js.map +7 -0
  58. package/dist/modules/ai_assistant/api/chat/route.js +152 -0
  59. package/dist/modules/ai_assistant/api/chat/route.js.map +7 -0
  60. package/dist/modules/ai_assistant/api/health/route.js +27 -0
  61. package/dist/modules/ai_assistant/api/health/route.js.map +7 -0
  62. package/dist/modules/ai_assistant/api/route/route.js +123 -0
  63. package/dist/modules/ai_assistant/api/route/route.js.map +7 -0
  64. package/dist/modules/ai_assistant/api/settings/route.js +60 -0
  65. package/dist/modules/ai_assistant/api/settings/route.js.map +7 -0
  66. package/dist/modules/ai_assistant/api/tools/execute/route.js +58 -0
  67. package/dist/modules/ai_assistant/api/tools/execute/route.js.map +7 -0
  68. package/dist/modules/ai_assistant/api/tools/route.js +48 -0
  69. package/dist/modules/ai_assistant/api/tools/route.js.map +7 -0
  70. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +10 -0
  71. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +7 -0
  72. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +28 -0
  73. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +7 -0
  74. package/dist/modules/ai_assistant/cli.js +192 -0
  75. package/dist/modules/ai_assistant/cli.js.map +7 -0
  76. package/dist/modules/ai_assistant/di.js +11 -0
  77. package/dist/modules/ai_assistant/di.js.map +7 -0
  78. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +257 -0
  79. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +7 -0
  80. package/dist/modules/ai_assistant/index.js +13 -0
  81. package/dist/modules/ai_assistant/index.js.map +7 -0
  82. package/dist/modules/ai_assistant/lib/ai-sdk.js +13 -0
  83. package/dist/modules/ai_assistant/lib/ai-sdk.js.map +7 -0
  84. package/dist/modules/ai_assistant/lib/api-discovery-tools.js +249 -0
  85. package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +7 -0
  86. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +177 -0
  87. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +7 -0
  88. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +210 -0
  89. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +7 -0
  90. package/dist/modules/ai_assistant/lib/auth.js +87 -0
  91. package/dist/modules/ai_assistant/lib/auth.js.map +7 -0
  92. package/dist/modules/ai_assistant/lib/chat-config.js +117 -0
  93. package/dist/modules/ai_assistant/lib/chat-config.js.map +7 -0
  94. package/dist/modules/ai_assistant/lib/client-factory.js +60 -0
  95. package/dist/modules/ai_assistant/lib/client-factory.js.map +7 -0
  96. package/dist/modules/ai_assistant/lib/http-server.js +367 -0
  97. package/dist/modules/ai_assistant/lib/http-server.js.map +7 -0
  98. package/dist/modules/ai_assistant/lib/in-process-client.js +126 -0
  99. package/dist/modules/ai_assistant/lib/in-process-client.js.map +7 -0
  100. package/dist/modules/ai_assistant/lib/mcp-client.js +146 -0
  101. package/dist/modules/ai_assistant/lib/mcp-client.js.map +7 -0
  102. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +283 -0
  103. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +7 -0
  104. package/dist/modules/ai_assistant/lib/mcp-server-config.js +160 -0
  105. package/dist/modules/ai_assistant/lib/mcp-server-config.js.map +7 -0
  106. package/dist/modules/ai_assistant/lib/mcp-server.js +156 -0
  107. package/dist/modules/ai_assistant/lib/mcp-server.js.map +7 -0
  108. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +44 -0
  109. package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +7 -0
  110. package/dist/modules/ai_assistant/lib/opencode-client.js +247 -0
  111. package/dist/modules/ai_assistant/lib/opencode-client.js.map +7 -0
  112. package/dist/modules/ai_assistant/lib/opencode-handlers.js +398 -0
  113. package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +7 -0
  114. package/dist/modules/ai_assistant/lib/schema-utils.js +94 -0
  115. package/dist/modules/ai_assistant/lib/schema-utils.js.map +7 -0
  116. package/dist/modules/ai_assistant/lib/tool-executor.js +55 -0
  117. package/dist/modules/ai_assistant/lib/tool-executor.js.map +7 -0
  118. package/dist/modules/ai_assistant/lib/tool-index-config.js +125 -0
  119. package/dist/modules/ai_assistant/lib/tool-index-config.js.map +7 -0
  120. package/dist/modules/ai_assistant/lib/tool-loader.js +88 -0
  121. package/dist/modules/ai_assistant/lib/tool-loader.js.map +7 -0
  122. package/dist/modules/ai_assistant/lib/tool-registry.js +65 -0
  123. package/dist/modules/ai_assistant/lib/tool-registry.js.map +7 -0
  124. package/dist/modules/ai_assistant/lib/tool-search.js +192 -0
  125. package/dist/modules/ai_assistant/lib/tool-search.js.map +7 -0
  126. package/dist/modules/ai_assistant/lib/types.js +1 -0
  127. package/dist/modules/ai_assistant/lib/types.js.map +7 -0
  128. package/package.json +108 -0
  129. package/src/di.ts +11 -0
  130. package/src/frontend/components/CommandPalette/CommandFooter.tsx +113 -0
  131. package/src/frontend/components/CommandPalette/CommandHeader.tsx +76 -0
  132. package/src/frontend/components/CommandPalette/CommandInput.tsx +50 -0
  133. package/src/frontend/components/CommandPalette/CommandItem.tsx +111 -0
  134. package/src/frontend/components/CommandPalette/CommandPalette.tsx +276 -0
  135. package/src/frontend/components/CommandPalette/CommandPaletteProvider.tsx +60 -0
  136. package/src/frontend/components/CommandPalette/CommandPaletteWrapper.tsx +21 -0
  137. package/src/frontend/components/CommandPalette/DebugPanel.tsx +257 -0
  138. package/src/frontend/components/CommandPalette/MessageBubble.tsx +73 -0
  139. package/src/frontend/components/CommandPalette/ToolCallConfirmation.tsx +130 -0
  140. package/src/frontend/components/CommandPalette/ToolCallDisplay.tsx +57 -0
  141. package/src/frontend/components/CommandPalette/ToolChatPage.tsx +125 -0
  142. package/src/frontend/components/CommandPalette/index.ts +14 -0
  143. package/src/frontend/constants.ts +35 -0
  144. package/src/frontend/hooks/index.ts +5 -0
  145. package/src/frontend/hooks/useCommandPalette.ts +1389 -0
  146. package/src/frontend/hooks/useMcpTools.ts +73 -0
  147. package/src/frontend/hooks/usePageContext.ts +61 -0
  148. package/src/frontend/hooks/useRecentActions.ts +64 -0
  149. package/src/frontend/hooks/useRecentTools.ts +69 -0
  150. package/src/frontend/index.ts +39 -0
  151. package/src/frontend/types.ts +260 -0
  152. package/src/frontend/utils/index.ts +1 -0
  153. package/src/frontend/utils/toolMatcher.ts +127 -0
  154. package/src/index.ts +92 -0
  155. package/src/modules/ai_assistant/acl.ts +10 -0
  156. package/src/modules/ai_assistant/api/chat/route.ts +213 -0
  157. package/src/modules/ai_assistant/api/health/route.ts +30 -0
  158. package/src/modules/ai_assistant/api/route/route.ts +149 -0
  159. package/src/modules/ai_assistant/api/settings/route.ts +73 -0
  160. package/src/modules/ai_assistant/api/tools/execute/route.ts +71 -0
  161. package/src/modules/ai_assistant/api/tools/route.ts +57 -0
  162. package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +26 -0
  163. package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +12 -0
  164. package/src/modules/ai_assistant/cli.ts +233 -0
  165. package/src/modules/ai_assistant/di.ts +9 -0
  166. package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +418 -0
  167. package/src/modules/ai_assistant/index.ts +11 -0
  168. package/src/modules/ai_assistant/lib/ai-sdk.ts +5 -0
  169. package/src/modules/ai_assistant/lib/api-discovery-tools.ts +334 -0
  170. package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +243 -0
  171. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +381 -0
  172. package/src/modules/ai_assistant/lib/auth.ts +185 -0
  173. package/src/modules/ai_assistant/lib/chat-config.ts +152 -0
  174. package/src/modules/ai_assistant/lib/client-factory.ts +130 -0
  175. package/src/modules/ai_assistant/lib/http-server.ts +498 -0
  176. package/src/modules/ai_assistant/lib/in-process-client.ts +205 -0
  177. package/src/modules/ai_assistant/lib/mcp-client.ts +221 -0
  178. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +373 -0
  179. package/src/modules/ai_assistant/lib/mcp-server-config.ts +287 -0
  180. package/src/modules/ai_assistant/lib/mcp-server.ts +214 -0
  181. package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +76 -0
  182. package/src/modules/ai_assistant/lib/opencode-client.ts +426 -0
  183. package/src/modules/ai_assistant/lib/opencode-handlers.ts +676 -0
  184. package/src/modules/ai_assistant/lib/schema-utils.ts +142 -0
  185. package/src/modules/ai_assistant/lib/tool-executor.ts +71 -0
  186. package/src/modules/ai_assistant/lib/tool-index-config.ts +178 -0
  187. package/src/modules/ai_assistant/lib/tool-loader.ts +149 -0
  188. package/src/modules/ai_assistant/lib/tool-registry.ts +114 -0
  189. package/src/modules/ai_assistant/lib/tool-search.ts +308 -0
  190. package/src/modules/ai_assistant/lib/types.ts +147 -0
  191. package/test-schema.ts +37 -0
  192. package/tsconfig.json +10 -0
  193. package/watch.mjs +6 -0
@@ -0,0 +1,276 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { createPortal } from 'react-dom'
5
+ import { Command } from 'cmdk'
6
+ import { Loader2, Send } from 'lucide-react'
7
+ import { cn } from '@open-mercato/shared/lib/utils'
8
+ import { Button } from '@open-mercato/ui/primitives/button'
9
+ import { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'
10
+ import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
11
+ import { useCommandPaletteContext } from './CommandPaletteProvider'
12
+ import { CommandInput } from './CommandInput'
13
+ import { CommandHeader } from './CommandHeader'
14
+ import { CommandFooter } from './CommandFooter'
15
+ import { ToolChatPage } from './ToolChatPage'
16
+ import { DebugPanel } from './DebugPanel'
17
+
18
+ // Idle state - shown when palette is open but no query submitted
19
+ function IdleState() {
20
+ return (
21
+ <div className="py-8 px-4 text-center text-muted-foreground">
22
+ <p className="mb-2">Ask me anything or describe what you want to do.</p>
23
+ <p className="text-sm">Examples:</p>
24
+ <ul className="text-sm mt-2 space-y-1">
25
+ <li>&quot;Search for customers in New York&quot;</li>
26
+ <li>&quot;Create a new product&quot;</li>
27
+ <li>&quot;Show me recent orders&quot;</li>
28
+ </ul>
29
+ </div>
30
+ )
31
+ }
32
+
33
+ // Routing indicator - shown while fast model analyzes intent
34
+ function RoutingIndicator() {
35
+ return (
36
+ <div className="py-8 flex items-center justify-center gap-2">
37
+ <Loader2 className="h-4 w-4 animate-spin" />
38
+ <span className="text-sm text-muted-foreground">Analyzing request...</span>
39
+ </div>
40
+ )
41
+ }
42
+
43
+ export function CommandPalette() {
44
+ const {
45
+ state,
46
+ isThinking,
47
+ isSessionAuthorized,
48
+ messages,
49
+ pendingToolCalls,
50
+ selectedTool,
51
+ close,
52
+ setInputValue,
53
+ handleSubmit,
54
+ reset,
55
+ sendAgenticMessage,
56
+ approveToolCall,
57
+ rejectToolCall,
58
+ debugEvents,
59
+ showDebug,
60
+ setShowDebug,
61
+ clearDebugEvents,
62
+ pendingQuestion,
63
+ answerQuestion,
64
+ } = useCommandPaletteContext()
65
+
66
+ const {
67
+ isOpen,
68
+ phase,
69
+ inputValue,
70
+ isLoading,
71
+ isStreaming,
72
+ connectionStatus,
73
+ } = state
74
+
75
+ const [localInput, setLocalInput] = React.useState('')
76
+ const [chatInput, setChatInput] = React.useState('')
77
+ const chatInputRef = React.useRef<HTMLInputElement>(null)
78
+
79
+ // Reset local input when phase changes to idle
80
+ React.useEffect(() => {
81
+ if (phase === 'idle') {
82
+ setLocalInput('')
83
+ setChatInput('')
84
+ }
85
+ }, [phase])
86
+
87
+ // Focus chat input when entering chatting phase
88
+ React.useEffect(() => {
89
+ if (phase === 'chatting' || phase === 'confirming' || phase === 'executing') {
90
+ // Small delay to ensure DOM is ready
91
+ setTimeout(() => chatInputRef.current?.focus(), 50)
92
+ }
93
+ }, [phase])
94
+
95
+ const handleOpenChange = (open: boolean) => {
96
+ if (!open) {
97
+ close()
98
+ }
99
+ }
100
+
101
+ const handleInputSubmit = async () => {
102
+ const query = localInput.trim()
103
+ if (!query) return
104
+ setLocalInput('')
105
+ await handleSubmit(query)
106
+ }
107
+
108
+ const handleKeyDown = (e: React.KeyboardEvent) => {
109
+ if (e.key === 'Enter' && phase === 'idle' && localInput.trim()) {
110
+ e.preventDefault()
111
+ handleInputSubmit()
112
+ }
113
+ }
114
+
115
+ const handleChatSubmit = async (e: React.FormEvent) => {
116
+ e.preventDefault()
117
+ if (!chatInput.trim() || isStreaming) return
118
+
119
+ const content = chatInput
120
+ setChatInput('')
121
+ await sendAgenticMessage(content)
122
+ }
123
+
124
+ const handleChatKeyDown = (e: React.KeyboardEvent) => {
125
+ // Prevent escape from bubbling to close the palette
126
+ if (e.key === 'Escape') {
127
+ e.stopPropagation()
128
+ }
129
+ // Submit on Enter (not Shift+Enter for multiline)
130
+ if (e.key === 'Enter' && !e.shiftKey) {
131
+ e.preventDefault()
132
+ if (chatInput.trim() && !isStreaming) {
133
+ const content = chatInput
134
+ setChatInput('')
135
+ sendAgenticMessage(content)
136
+ }
137
+ }
138
+ }
139
+
140
+ return (
141
+ <>
142
+ {/* Custom blur overlay when debug mode is on (since modal=false removes it) */}
143
+ {isOpen && showDebug && (
144
+ <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm pointer-events-none" />
145
+ )}
146
+ <Dialog open={isOpen} onOpenChange={handleOpenChange} modal={!showDebug}>
147
+ <DialogContent
148
+ className={cn(
149
+ 'fixed left-1/2 top-[10vh] z-50 -translate-x-1/2',
150
+ 'w-full max-w-2xl p-0',
151
+ 'rounded-xl border bg-background shadow-2xl',
152
+ 'flex flex-col'
153
+ )}
154
+ style={{ maxHeight: 500, overflow: 'hidden' }}
155
+ onKeyDown={handleKeyDown}
156
+ onPointerDownOutside={(e) => {
157
+ // Prevent closing on outside click when debug mode is on
158
+ if (showDebug) {
159
+ e.preventDefault()
160
+ }
161
+ }}
162
+ onInteractOutside={(e) => {
163
+ // Prevent closing on outside interaction when debug mode is on
164
+ if (showDebug) {
165
+ e.preventDefault()
166
+ }
167
+ }}
168
+ >
169
+ {/* Visually hidden title for accessibility */}
170
+ <VisuallyHidden>
171
+ <DialogTitle>AI Command Palette</DialogTitle>
172
+ </VisuallyHidden>
173
+ <Command className="flex flex-col flex-1 min-h-0" shouldFilter={false}>
174
+ {/* Header - shows phase/tool info */}
175
+ <CommandHeader
176
+ phase={phase}
177
+ selectedTool={selectedTool}
178
+ onBack={reset}
179
+ />
180
+
181
+ {/* Input - shown in idle phase */}
182
+ {phase === 'idle' && (
183
+ <CommandInput
184
+ value={localInput}
185
+ onValueChange={setLocalInput}
186
+ mode="commands"
187
+ isLoading={isLoading}
188
+ placeholder="Ask me anything or describe what you want to do..."
189
+ />
190
+ )}
191
+
192
+ {/* Content area */}
193
+ <div className="flex-1 min-h-0 overflow-y-auto">
194
+ {phase === 'idle' && !localInput && <IdleState />}
195
+
196
+ {phase === 'routing' && <RoutingIndicator />}
197
+
198
+ {(phase === 'chatting' || phase === 'confirming' || phase === 'executing') && (
199
+ <ToolChatPage
200
+ tool={selectedTool}
201
+ messages={messages}
202
+ pendingToolCalls={pendingToolCalls}
203
+ isStreaming={isStreaming}
204
+ isThinking={isThinking}
205
+ onApproveToolCall={approveToolCall}
206
+ onRejectToolCall={rejectToolCall}
207
+ pendingQuestion={pendingQuestion}
208
+ onAnswerQuestion={answerQuestion}
209
+ />
210
+ )}
211
+ </div>
212
+
213
+ {/* Chat input - shown in chatting phases */}
214
+ {(phase === 'chatting' || phase === 'confirming' || phase === 'executing') && (
215
+ <form onSubmit={handleChatSubmit} className="shrink-0 border-t p-3">
216
+ <div className="flex items-center gap-2">
217
+ <input
218
+ ref={chatInputRef}
219
+ type="text"
220
+ value={chatInput}
221
+ onChange={(e) => setChatInput(e.target.value)}
222
+ onKeyDown={handleChatKeyDown}
223
+ placeholder="Describe what you want to do..."
224
+ className={cn(
225
+ 'flex-1 bg-muted rounded-lg px-4 py-2 text-sm outline-none',
226
+ 'focus:ring-2 focus:ring-ring',
227
+ 'disabled:opacity-50'
228
+ )}
229
+ disabled={isStreaming}
230
+ />
231
+ <Button
232
+ type="submit"
233
+ size="icon"
234
+ disabled={!chatInput.trim() || isStreaming}
235
+ >
236
+ {isStreaming ? (
237
+ <Loader2 className="h-4 w-4 animate-spin" />
238
+ ) : (
239
+ <Send className="h-4 w-4" />
240
+ )}
241
+ </Button>
242
+ </div>
243
+ </form>
244
+ )}
245
+
246
+ {/* Footer with connection status and keyboard hints */}
247
+ <CommandFooter
248
+ phase={phase}
249
+ connectionStatus={connectionStatus}
250
+ isSessionAuthorized={isSessionAuthorized}
251
+ showDebug={showDebug}
252
+ onToggleDebug={() => setShowDebug(!showDebug)}
253
+ />
254
+ </Command>
255
+ </DialogContent>
256
+ </Dialog>
257
+
258
+ {/* Debug panel - rendered via portal outside the dialog DOM tree */}
259
+ {isOpen && showDebug && typeof document !== 'undefined' && createPortal(
260
+ <div
261
+ data-debug-panel
262
+ className="fixed z-[9999] bg-gray-900 rounded-xl border border-gray-700 shadow-2xl flex flex-col overflow-hidden"
263
+ style={{ top: '80px', right: '20px', width: '400px', minWidth: '400px', maxWidth: '400px', maxHeight: 'calc(100vh - 100px)' }}
264
+ >
265
+ <DebugPanel
266
+ events={debugEvents}
267
+ onClear={clearDebugEvents}
268
+ isOpen={true}
269
+ onToggle={() => setShowDebug(false)}
270
+ />
271
+ </div>,
272
+ document.body
273
+ )}
274
+ </>
275
+ )
276
+ }
@@ -0,0 +1,60 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { createContext, useContext, useState, useEffect } from 'react'
5
+ import type { PageContext, SelectedEntity } from '../../types'
6
+ import { usePageContext } from '../../hooks/usePageContext'
7
+ import { useCommandPalette } from '../../hooks/useCommandPalette'
8
+
9
+ interface CommandPaletteProviderProps {
10
+ children: React.ReactNode
11
+ tenantId: string
12
+ organizationId: string | null
13
+ disableKeyboardShortcut?: boolean
14
+ }
15
+
16
+ type CommandPaletteContextValue = ReturnType<typeof useCommandPalette>
17
+
18
+ const CommandPaletteContext = createContext<CommandPaletteContextValue | null>(null)
19
+
20
+ export function CommandPaletteProvider({
21
+ children,
22
+ tenantId,
23
+ organizationId,
24
+ disableKeyboardShortcut = true,
25
+ }: CommandPaletteProviderProps) {
26
+ const pageContext = usePageContext({ tenantId, organizationId })
27
+ const [selectedEntities, setSelectedEntities] = useState<SelectedEntity[]>([])
28
+
29
+ // Listen for DataTable selection events
30
+ useEffect(() => {
31
+ const handleSelectionChange = (event: CustomEvent<SelectedEntity[]>) => {
32
+ setSelectedEntities(event.detail || [])
33
+ }
34
+
35
+ window.addEventListener('om:selection-change', handleSelectionChange as EventListener)
36
+ return () => {
37
+ window.removeEventListener('om:selection-change', handleSelectionChange as EventListener)
38
+ }
39
+ }, [])
40
+
41
+ const commandPalette = useCommandPalette({
42
+ pageContext,
43
+ selectedEntities,
44
+ disableKeyboardShortcut,
45
+ })
46
+
47
+ return (
48
+ <CommandPaletteContext.Provider value={commandPalette}>
49
+ {children}
50
+ </CommandPaletteContext.Provider>
51
+ )
52
+ }
53
+
54
+ export function useCommandPaletteContext(): CommandPaletteContextValue {
55
+ const context = useContext(CommandPaletteContext)
56
+ if (!context) {
57
+ throw new Error('useCommandPaletteContext must be used within CommandPaletteProvider')
58
+ }
59
+ return context
60
+ }
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { CommandPaletteProvider } from './CommandPaletteProvider'
5
+ import { CommandPalette } from './CommandPalette'
6
+
7
+ interface CommandPaletteWrapperProps {
8
+ tenantId: string | null
9
+ organizationId: string | null
10
+ }
11
+
12
+ export function CommandPaletteWrapper({ tenantId, organizationId }: CommandPaletteWrapperProps) {
13
+ return (
14
+ <CommandPaletteProvider
15
+ tenantId={tenantId ?? ''}
16
+ organizationId={organizationId}
17
+ >
18
+ <CommandPalette />
19
+ </CommandPaletteProvider>
20
+ )
21
+ }
@@ -0,0 +1,257 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Code, X, Trash2 } from 'lucide-react'
5
+ import { JsonView } from 'react-json-view-lite'
6
+ import { cn } from '@open-mercato/shared/lib/utils'
7
+
8
+ export interface DebugEvent {
9
+ id: string
10
+ timestamp: Date
11
+ type: 'thinking' | 'tool-call' | 'tool-result' | 'text' | 'error' | 'done' | 'message' | 'connection' | 'metadata' | 'debug' | 'question' | 'session-authorized'
12
+ data: unknown
13
+ }
14
+
15
+ interface DebugPanelProps {
16
+ events: DebugEvent[]
17
+ onClear: () => void
18
+ isOpen: boolean
19
+ onToggle: () => void
20
+ }
21
+
22
+ export function DebugPanel({ events, onClear, isOpen, onToggle }: DebugPanelProps) {
23
+ const scrollRef = React.useRef<HTMLDivElement>(null)
24
+
25
+ // Filter out text events - only show tool calls, results, errors, etc.
26
+ const filteredEvents = React.useMemo(
27
+ () => events.filter((e) => e.type !== 'text'),
28
+ [events]
29
+ )
30
+
31
+ React.useEffect(() => {
32
+ if (scrollRef.current) {
33
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight
34
+ }
35
+ }, [filteredEvents])
36
+
37
+ if (!isOpen) {
38
+ return null
39
+ }
40
+
41
+ return (
42
+ <div className="flex flex-col h-full min-h-0">
43
+ <div className="flex-shrink-0 flex items-center justify-between px-3 py-2 border-b border-gray-700">
44
+ <span className="text-xs font-medium text-gray-300">Debug Events ({filteredEvents.length})</span>
45
+ <div className="flex gap-2">
46
+ <button
47
+ onClick={onClear}
48
+ className="text-gray-400 hover:text-gray-200"
49
+ title="Clear events"
50
+ >
51
+ <Trash2 className="w-3 h-3" />
52
+ </button>
53
+ <button
54
+ onClick={onToggle}
55
+ className="text-gray-400 hover:text-gray-200"
56
+ title="Close debug panel"
57
+ >
58
+ <X className="w-3 h-3" />
59
+ </button>
60
+ </div>
61
+ </div>
62
+ <div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto p-2 space-y-1">
63
+ {filteredEvents.map((event) => (
64
+ <DebugEventRow key={event.id} event={event} />
65
+ ))}
66
+ {filteredEvents.length === 0 && (
67
+ <p className="text-xs text-gray-500 text-center py-4">No events yet</p>
68
+ )}
69
+ </div>
70
+ </div>
71
+ )
72
+ }
73
+
74
+ // Recursively parse stringified JSON fields
75
+ function parseNestedJson(data: unknown): unknown {
76
+ if (typeof data === 'string') {
77
+ try {
78
+ const parsed = JSON.parse(data)
79
+ return parseNestedJson(parsed)
80
+ } catch {
81
+ return data
82
+ }
83
+ }
84
+ if (Array.isArray(data)) {
85
+ return data.map(parseNestedJson)
86
+ }
87
+ if (data && typeof data === 'object') {
88
+ const result: Record<string, unknown> = {}
89
+ for (const [key, value] of Object.entries(data)) {
90
+ result[key] = parseNestedJson(value)
91
+ }
92
+ return result
93
+ }
94
+ return data
95
+ }
96
+
97
+ // Custom dark styles for better readability (no external CSS needed)
98
+ const customDarkStyles: Record<string, string> = {
99
+ container: 'bg-transparent text-[11px] leading-relaxed font-mono',
100
+ basicChildStyle: 'pl-4 ml-0',
101
+ label: 'text-purple-400 mr-1',
102
+ nullValue: 'text-gray-500 italic',
103
+ undefinedValue: 'text-gray-500 italic',
104
+ stringValue: 'text-green-400',
105
+ booleanValue: 'text-yellow-400',
106
+ numberValue: 'text-blue-400',
107
+ otherValue: 'text-gray-300',
108
+ punctuation: 'text-gray-500',
109
+ collapseIcon: 'text-gray-400 cursor-pointer select-none mr-1',
110
+ expandIcon: 'text-gray-400 cursor-pointer select-none mr-1',
111
+ collapsedContent: 'text-gray-500',
112
+ }
113
+
114
+ function DebugEventRow({ event }: { event: DebugEvent }) {
115
+ const [expanded, setExpanded] = React.useState(false)
116
+ const typeColors: Record<string, string> = {
117
+ 'thinking': 'text-orange-400',
118
+ 'tool-call': 'text-blue-400',
119
+ 'tool-result': 'text-green-400',
120
+ 'text': 'text-gray-300',
121
+ 'error': 'text-red-400',
122
+ 'done': 'text-purple-400',
123
+ 'message': 'text-yellow-400',
124
+ 'connection': 'text-cyan-400',
125
+ 'metadata': 'text-teal-400',
126
+ 'debug': 'text-gray-500',
127
+ 'question': 'text-amber-400',
128
+ }
129
+
130
+ const formatTime = (date: Date) => {
131
+ return date.toLocaleTimeString('en-US', {
132
+ hour12: false,
133
+ hour: '2-digit',
134
+ minute: '2-digit',
135
+ second: '2-digit',
136
+ }) + '.' + String(date.getMilliseconds()).padStart(3, '0')
137
+ }
138
+
139
+ // Parse nested JSON strings for better display
140
+ const parsedData = React.useMemo(() => parseNestedJson(event.data), [event.data])
141
+
142
+ return (
143
+ <div className="text-xs font-mono">
144
+ <button
145
+ onClick={() => setExpanded(!expanded)}
146
+ className="w-full text-left flex items-center gap-2 hover:bg-gray-800 rounded px-1 py-0.5"
147
+ >
148
+ <span className="text-gray-500 flex-shrink-0">
149
+ {formatTime(event.timestamp)}
150
+ </span>
151
+ <span className={cn('font-medium flex-shrink-0', typeColors[event.type] || 'text-gray-400')}>
152
+ {event.type}
153
+ </span>
154
+ <span className="text-gray-400 truncate flex-1">
155
+ {getEventPreview(event)}
156
+ </span>
157
+ <span className="text-gray-600 flex-shrink-0">
158
+ {expanded ? '▼' : '▶'}
159
+ </span>
160
+ </button>
161
+ {expanded && (
162
+ <div className="bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-[300px] overflow-y-auto">
163
+ <JsonView
164
+ data={parsedData as object}
165
+ style={customDarkStyles}
166
+ shouldExpandNode={(level) => level < 2}
167
+ />
168
+ </div>
169
+ )}
170
+ </div>
171
+ )
172
+ }
173
+
174
+ function getEventPreview(event: DebugEvent): string {
175
+ const data = event.data as Record<string, unknown>
176
+
177
+ if (event.type === 'thinking') {
178
+ return 'Agent is processing...'
179
+ }
180
+ if (event.type === 'tool-call') {
181
+ const toolName = data?.toolName as string || 'unknown'
182
+ const args = data?.args
183
+ const argsPreview = args ? JSON.stringify(args).substring(0, 40) : ''
184
+ return `${toolName}(${argsPreview}${argsPreview.length >= 40 ? '...' : ''})`
185
+ }
186
+ if (event.type === 'tool-result') {
187
+ const result = data?.result
188
+ const resultPreview = typeof result === 'string'
189
+ ? result.substring(0, 50)
190
+ : JSON.stringify(result)?.substring(0, 50)
191
+ return `→ ${resultPreview}${resultPreview && resultPreview.length >= 50 ? '...' : ''}`
192
+ }
193
+ if (event.type === 'text') {
194
+ const content = data?.content as string || ''
195
+ return content.substring(0, 50) + (content.length > 50 ? '...' : '')
196
+ }
197
+ if (event.type === 'error') {
198
+ return data?.error as string || 'Unknown error'
199
+ }
200
+ if (event.type === 'message') {
201
+ const role = data?.role as string || ''
202
+ const content = data?.content as string || ''
203
+ return `[${role}] ${content.substring(0, 40)}...`
204
+ }
205
+ if (event.type === 'connection') {
206
+ return data?.status as string || ''
207
+ }
208
+ if (event.type === 'metadata') {
209
+ const model = data?.model as string || ''
210
+ const tokens = data?.tokens as { input?: number; output?: number } | undefined
211
+ const durationMs = data?.durationMs as number | undefined
212
+ const parts: string[] = []
213
+ if (model) parts.push(model)
214
+ if (tokens) parts.push(`${tokens.input || 0}→${tokens.output || 0} tokens`)
215
+ if (durationMs) parts.push(`${(durationMs / 1000).toFixed(1)}s`)
216
+ return parts.join(' | ') || 'No metadata'
217
+ }
218
+ if (event.type === 'debug') {
219
+ const partType = data?.partType as string || 'unknown'
220
+ return `Unknown part: ${partType}`
221
+ }
222
+ if (event.type === 'question') {
223
+ // Use the enriched questionText field if available (added by frontend)
224
+ const questionText = (data?.questionText as string)
225
+ || (data?.question as { questions?: Array<{ question: string }> })?.questions?.[0]?.question
226
+ || (data?.header as string)
227
+ || 'Confirmation required'
228
+ return questionText.substring(0, 50) + (questionText.length > 50 ? '...' : '')
229
+ }
230
+ return ''
231
+ }
232
+
233
+ // Export a toggle button component for use in footer
234
+ export function DebugToggleButton({
235
+ isOpen,
236
+ onToggle
237
+ }: {
238
+ isOpen: boolean
239
+ onToggle: () => void
240
+ }) {
241
+ return (
242
+ <button
243
+ type="button"
244
+ onClick={onToggle}
245
+ className={cn(
246
+ 'flex items-center gap-1 text-xs transition-colors',
247
+ isOpen
248
+ ? 'text-blue-500 hover:text-blue-400'
249
+ : 'text-muted-foreground hover:text-foreground'
250
+ )}
251
+ title="Toggle debug panel"
252
+ >
253
+ <Code className="w-3 h-3" />
254
+ <span>{isOpen ? 'Hide Debug' : 'Debug'}</span>
255
+ </button>
256
+ )
257
+ }
@@ -0,0 +1,73 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { User, Bot } from 'lucide-react'
5
+ import Markdown from 'react-markdown'
6
+ import remarkGfm from 'remark-gfm'
7
+ import { cn } from '@open-mercato/shared/lib/utils'
8
+ import type { ChatMessage } from '../../types'
9
+
10
+ interface MessageBubbleProps {
11
+ message: ChatMessage
12
+ }
13
+
14
+ export function MessageBubble({ message }: MessageBubbleProps) {
15
+ const isUser = message.role === 'user'
16
+
17
+ return (
18
+ <div
19
+ className={cn(
20
+ 'flex gap-3 py-3',
21
+ isUser ? 'flex-row-reverse' : 'flex-row'
22
+ )}
23
+ >
24
+ <div
25
+ className={cn(
26
+ 'flex items-center justify-center w-8 h-8 rounded-full shrink-0',
27
+ isUser ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'
28
+ )}
29
+ >
30
+ {isUser ? <User className="h-4 w-4" /> : <Bot className="h-4 w-4" />}
31
+ </div>
32
+
33
+ <div
34
+ className={cn(
35
+ 'flex-1 min-w-0 px-4 py-2 rounded-lg text-sm',
36
+ isUser
37
+ ? 'bg-primary text-primary-foreground ml-12'
38
+ : 'bg-muted text-foreground mr-12'
39
+ )}
40
+ >
41
+ {isUser ? (
42
+ <div className="whitespace-pre-wrap break-words">{message.content}</div>
43
+ ) : (
44
+ <div className={cn(
45
+ 'prose prose-sm dark:prose-invert max-w-none break-words',
46
+ // Reset margins for first/last children
47
+ '[&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
48
+ // Paragraph spacing - also handle plain text with whitespace-pre-line
49
+ '[&_p]:my-2 [&_p]:leading-relaxed [&_p]:whitespace-pre-line',
50
+ // List styling
51
+ '[&_ul]:my-2 [&_ul]:pl-4 [&_ol]:my-2 [&_ol]:pl-4',
52
+ '[&_li]:my-0.5 [&_li]:leading-relaxed',
53
+ // Headers
54
+ '[&_h1]:text-base [&_h1]:font-semibold [&_h1]:mt-3 [&_h1]:mb-2',
55
+ '[&_h2]:text-sm [&_h2]:font-semibold [&_h2]:mt-3 [&_h2]:mb-1',
56
+ '[&_h3]:text-sm [&_h3]:font-medium [&_h3]:mt-2 [&_h3]:mb-1',
57
+ // Code blocks
58
+ '[&_pre]:bg-background/50 [&_pre]:rounded [&_pre]:p-2 [&_pre]:my-2 [&_pre]:overflow-x-auto',
59
+ '[&_code]:bg-background/50 [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs',
60
+ // Strong/emphasis
61
+ '[&_strong]:font-semibold [&_em]:italic',
62
+ // Blockquotes
63
+ '[&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground/30 [&_blockquote]:pl-3 [&_blockquote]:italic'
64
+ )}>
65
+ <Markdown remarkPlugins={[remarkGfm]}>
66
+ {message.content}
67
+ </Markdown>
68
+ </div>
69
+ )}
70
+ </div>
71
+ </div>
72
+ )
73
+ }