@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,1389 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
4
+ import type {
5
+ CommandPaletteMode,
6
+ CommandPalettePage,
7
+ CommandPaletteState,
8
+ ConnectionStatus,
9
+ PalettePhase,
10
+ PageContext,
11
+ SelectedEntity,
12
+ ToolInfo,
13
+ ToolExecutionResult,
14
+ PendingToolCall,
15
+ ChatMessage,
16
+ RouteResult,
17
+ DebugEvent,
18
+ DebugEventType,
19
+ OpenCodeQuestion,
20
+ } from '../types'
21
+ import { COMMAND_PALETTE_SHORTCUT } from '../constants'
22
+ import { filterTools } from '../utils/toolMatcher'
23
+ import { useMcpTools } from './useMcpTools'
24
+ import { useRecentActions } from './useRecentActions'
25
+ import { useRecentTools } from './useRecentTools'
26
+
27
+ interface UseCommandPaletteOptions {
28
+ pageContext: PageContext | null
29
+ selectedEntities?: SelectedEntity[]
30
+ disableKeyboardShortcut?: boolean
31
+ }
32
+
33
+ function generateId(): string {
34
+ return Math.random().toString(36).substring(2, 15)
35
+ }
36
+
37
+ // Tools that are safe to auto-execute without user confirmation
38
+ const SAFE_TOOL_PATTERNS = [
39
+ /^search_/, // search_query, search_schema, search_get, search_aggregate, search_status
40
+ /^get_/, // get_ operations are read-only
41
+ /^list_/, // list_ operations are read-only
42
+ /^view_/, // view_ operations are read-only
43
+ /^context_/, // context_whoami etc.
44
+ /_get$/, // tools ending with _get
45
+ /_list$/, // tools ending with _list
46
+ /_status$/, // tools ending with _status
47
+ /_schema$/, // tools ending with _schema
48
+ ]
49
+
50
+ // Tools that should always require confirmation
51
+ const DANGEROUS_TOOL_PATTERNS = [
52
+ /^delete_/,
53
+ /^remove_/,
54
+ /_delete$/,
55
+ /_remove$/,
56
+ /^reindex_/,
57
+ /_reindex$/,
58
+ ]
59
+
60
+ function isToolSafeToAutoExecute(toolName: string): boolean {
61
+ // First check if it's a dangerous tool
62
+ if (DANGEROUS_TOOL_PATTERNS.some(p => p.test(toolName))) {
63
+ return false
64
+ }
65
+ // Then check if it matches safe patterns
66
+ return SAFE_TOOL_PATTERNS.some(p => p.test(toolName))
67
+ }
68
+
69
+ function getToolPrompt(tool: ToolInfo): string {
70
+ const schema = tool.inputSchema
71
+ if (!schema || typeof schema !== 'object') {
72
+ return 'What would you like to do?'
73
+ }
74
+
75
+ const properties = (schema as { properties?: Record<string, unknown> }).properties
76
+ if (!properties || Object.keys(properties).length === 0) {
77
+ return 'This tool has no parameters. Ready to execute?'
78
+ }
79
+
80
+ const paramNames = Object.keys(properties).slice(0, 3)
81
+ return `Please provide the following: ${paramNames.join(', ')}.`
82
+ }
83
+
84
+ export function useCommandPalette(options: UseCommandPaletteOptions) {
85
+ const { pageContext, selectedEntities = [], disableKeyboardShortcut = false } = options
86
+
87
+ // Core state with phase-based navigation for intelligent routing
88
+ const [state, setState] = useState<CommandPaletteState>({
89
+ isOpen: false,
90
+ phase: 'idle',
91
+ inputValue: '',
92
+ selectedIndex: 0,
93
+ isLoading: false,
94
+ isStreaming: false,
95
+ connectionStatus: 'disconnected',
96
+ // Legacy fields for backwards compatibility
97
+ page: 'home',
98
+ mode: 'commands',
99
+ })
100
+
101
+ // Tool-related hooks
102
+ const { tools, isLoading: toolsLoading, executeTool: executeToolApi } = useMcpTools()
103
+ const { recentActions, addRecentAction } = useRecentActions()
104
+ const { recentTools, saveRecentTool } = useRecentTools(tools)
105
+
106
+ // Selected tool for tool-chat page
107
+ const [selectedTool, setSelectedTool] = useState<ToolInfo | null>(null)
108
+
109
+ // Chat state for tool-chat page
110
+ const [messages, setMessages] = useState<ChatMessage[]>([])
111
+ const [pendingToolCalls, setPendingToolCalls] = useState<PendingToolCall[]>([])
112
+
113
+ // Initial context from context_whoami - fetched when palette opens
114
+ const [initialContext, setInitialContext] = useState<{
115
+ tenantId: string | null
116
+ organizationId: string | null
117
+ userId: string
118
+ isSuperAdmin: boolean
119
+ features: string[]
120
+ } | null>(null)
121
+
122
+ // Available entity types from search_schema - fetched when palette opens
123
+ const [availableEntities, setAvailableEntities] = useState<Array<{
124
+ entityId: string
125
+ enabled: boolean
126
+ }> | null>(null)
127
+
128
+ // Debug mode state
129
+ const [debugEvents, setDebugEvents] = useState<DebugEvent[]>([])
130
+ const [showDebug, setShowDebug] = useState(false)
131
+
132
+ // OpenCode session state for conversation persistence
133
+ // Use both state (for React reactivity) and ref (to avoid stale closures in callbacks)
134
+ const [opencodeSessionId, setOpencodeSessionId] = useState<string | null>(null)
135
+ const opencodeSessionIdRef = useRef<string | null>(null)
136
+ const [isThinking, setIsThinking] = useState(false)
137
+ const [isSessionAuthorized, setIsSessionAuthorized] = useState(false)
138
+
139
+ // Pending question from OpenCode requiring user confirmation
140
+ const [pendingQuestion, setPendingQuestion] = useState<OpenCodeQuestion | null>(null)
141
+
142
+ // Track answered question IDs to prevent re-showing them
143
+ const answeredQuestionIds = useRef<Set<string>>(new Set())
144
+
145
+ // Flag to indicate the next text event should start a new message (after answering question)
146
+ const shouldStartNewMessage = useRef<boolean>(false)
147
+
148
+ // AbortController for current streaming request - allows canceling when answering questions
149
+ const currentStreamController = useRef<AbortController | null>(null)
150
+
151
+ // Wrapper to update both state and ref for opencodeSessionId
152
+ // This ensures the ref is always in sync and callbacks get the latest value
153
+ const updateOpencodeSessionId = useCallback((id: string | null) => {
154
+ opencodeSessionIdRef.current = id
155
+ setOpencodeSessionId(id)
156
+ }, [])
157
+
158
+ // Helper to add debug events
159
+ const addDebugEvent = useCallback((type: DebugEventType, data: unknown) => {
160
+ // Deep clone the data to capture state at this moment (prevents mutation issues)
161
+ const clonedData = JSON.parse(JSON.stringify(data))
162
+ setDebugEvents((prev) => [
163
+ ...prev.slice(-999), // Keep last 1000 events
164
+ {
165
+ id: generateId(),
166
+ timestamp: new Date(),
167
+ type,
168
+ data: clonedData,
169
+ },
170
+ ])
171
+ }, [])
172
+
173
+ const clearDebugEvents = useCallback(() => {
174
+ setDebugEvents([])
175
+ }, [])
176
+
177
+ // Update connection status when tools load
178
+ useEffect(() => {
179
+ if (toolsLoading) {
180
+ setState((prev) => ({ ...prev, connectionStatus: 'connecting' }))
181
+ } else if (tools.length > 0) {
182
+ setState((prev) => ({ ...prev, connectionStatus: 'connected' }))
183
+ } else {
184
+ setState((prev) => ({ ...prev, connectionStatus: 'disconnected' }))
185
+ }
186
+ }, [tools, toolsLoading])
187
+
188
+ // Fetch initial context and entity schema when palette opens and tools are available
189
+ useEffect(() => {
190
+ if (state.isOpen && tools.length > 0) {
191
+ // Fetch auth context if not already loaded
192
+ if (!initialContext) {
193
+ console.log('[CommandPalette] Fetching initial context via context_whoami...')
194
+ executeToolApi('context_whoami', {})
195
+ .then((result) => {
196
+ if (result.success && result.result) {
197
+ console.log('[CommandPalette] Got initial context:', result.result)
198
+ const ctx = result.result as {
199
+ tenantId: string | null
200
+ organizationId: string | null
201
+ userId: string
202
+ isSuperAdmin: boolean
203
+ features: string[]
204
+ }
205
+ setInitialContext(ctx)
206
+ }
207
+ })
208
+ .catch((err) => {
209
+ console.error('[CommandPalette] Failed to fetch initial context:', err)
210
+ })
211
+ }
212
+
213
+ // Fetch available entity types if not already loaded
214
+ if (!availableEntities) {
215
+ console.log('[CommandPalette] Fetching available entities via search_schema...')
216
+ executeToolApi('search_schema', {})
217
+ .then((result) => {
218
+ if (result.success && result.result) {
219
+ const schemaResult = result.result as { entities?: Array<{ entityId: string; enabled: boolean }> }
220
+ console.log('[CommandPalette] Got entity schema:', schemaResult.entities?.length, 'entities')
221
+ if (schemaResult.entities) {
222
+ setAvailableEntities(schemaResult.entities.filter(e => e.enabled))
223
+ }
224
+ }
225
+ })
226
+ .catch((err) => {
227
+ console.error('[CommandPalette] Failed to fetch entity schema:', err)
228
+ })
229
+ }
230
+ }
231
+ }, [state.isOpen, tools.length, initialContext, availableEntities, executeToolApi])
232
+
233
+ // Filtered tools based on input
234
+ const filteredTools = useMemo(() => {
235
+ const query = state.inputValue.startsWith('/') ? state.inputValue.slice(1) : state.inputValue
236
+ return filterTools(tools, query)
237
+ }, [tools, state.inputValue])
238
+
239
+ // Keyboard shortcut handler
240
+ useEffect(() => {
241
+ if (disableKeyboardShortcut) return
242
+
243
+ const handleKeyDown = (event: KeyboardEvent) => {
244
+ // Open/close with Cmd+K or Ctrl+K
245
+ if (
246
+ (event.metaKey || event.ctrlKey) &&
247
+ event.key.toLowerCase() === COMMAND_PALETTE_SHORTCUT.key
248
+ ) {
249
+ event.preventDefault()
250
+ setState((prev) => {
251
+ if (prev.isOpen) {
252
+ // Closing - reset to idle
253
+ return {
254
+ ...prev,
255
+ isOpen: false,
256
+ phase: 'idle',
257
+ inputValue: '',
258
+ page: 'home',
259
+ selectedIndex: 0,
260
+ mode: 'commands',
261
+ }
262
+ } else {
263
+ // Opening
264
+ return {
265
+ ...prev,
266
+ isOpen: true,
267
+ }
268
+ }
269
+ })
270
+ // Reset selected tool when closing
271
+ if (state.isOpen) {
272
+ setSelectedTool(null)
273
+ setMessages([])
274
+ setPendingToolCalls([])
275
+ }
276
+ }
277
+
278
+ // Escape - reset or close
279
+ if (event.key === 'Escape' && state.isOpen) {
280
+ event.preventDefault()
281
+ if (state.phase !== 'idle') {
282
+ // Reset to idle
283
+ setState((prev) => ({
284
+ ...prev,
285
+ phase: 'idle',
286
+ inputValue: '',
287
+ page: 'home',
288
+ selectedIndex: 0,
289
+ mode: 'commands',
290
+ }))
291
+ setSelectedTool(null)
292
+ setMessages([])
293
+ setPendingToolCalls([])
294
+ } else {
295
+ // Close palette
296
+ setState((prev) => ({
297
+ ...prev,
298
+ isOpen: false,
299
+ phase: 'idle',
300
+ inputValue: '',
301
+ page: 'home',
302
+ selectedIndex: 0,
303
+ mode: 'commands',
304
+ }))
305
+ setSelectedTool(null)
306
+ setMessages([])
307
+ setPendingToolCalls([])
308
+ }
309
+ }
310
+ }
311
+
312
+ window.addEventListener('keydown', handleKeyDown)
313
+ return () => window.removeEventListener('keydown', handleKeyDown)
314
+ }, [state.isOpen, state.phase, state.inputValue, disableKeyboardShortcut])
315
+
316
+ // Actions
317
+ const open = useCallback(() => {
318
+ setState((prev) => ({ ...prev, isOpen: true }))
319
+ }, [])
320
+
321
+ const close = useCallback(() => {
322
+ setState((prev) => ({
323
+ ...prev,
324
+ isOpen: false,
325
+ phase: 'idle',
326
+ inputValue: '',
327
+ page: 'home',
328
+ selectedIndex: 0,
329
+ mode: 'commands',
330
+ }))
331
+ setSelectedTool(null)
332
+ setMessages([])
333
+ setPendingToolCalls([])
334
+ updateOpencodeSessionId(null)
335
+ setIsSessionAuthorized(false)
336
+ // Don't reset initialContext - it stays valid for the session
337
+ }, [updateOpencodeSessionId])
338
+
339
+ // Reset to idle state (without closing)
340
+ const reset = useCallback(() => {
341
+ setState((prev) => ({
342
+ ...prev,
343
+ phase: 'idle',
344
+ inputValue: '',
345
+ page: 'home',
346
+ selectedIndex: 0,
347
+ mode: 'commands',
348
+ }))
349
+ setSelectedTool(null)
350
+ setMessages([])
351
+ setPendingToolCalls([])
352
+ updateOpencodeSessionId(null)
353
+ setIsSessionAuthorized(false)
354
+ }, [updateOpencodeSessionId])
355
+
356
+ const setIsOpen = useCallback(
357
+ (isOpen: boolean) => {
358
+ if (isOpen) {
359
+ open()
360
+ } else {
361
+ close()
362
+ }
363
+ },
364
+ [open, close]
365
+ )
366
+
367
+ const setMode = useCallback((mode: CommandPaletteMode) => {
368
+ setState((prev) => ({ ...prev, mode }))
369
+ }, [])
370
+
371
+ const setInputValue = useCallback((value: string) => {
372
+ setState((prev) => ({ ...prev, inputValue: value, selectedIndex: 0 }))
373
+ }, [])
374
+
375
+ const setSelectedIndex = useCallback((index: number) => {
376
+ setState((prev) => ({ ...prev, selectedIndex: index }))
377
+ }, [])
378
+
379
+ // Page navigation - go to tool chat
380
+ const goToToolChat = useCallback(
381
+ (tool: ToolInfo) => {
382
+ setSelectedTool(tool)
383
+ saveRecentTool(tool.name)
384
+
385
+ // Create initial assistant message
386
+ const initialMessage: ChatMessage = {
387
+ id: generateId(),
388
+ role: 'assistant',
389
+ content: `I'll help you with "${tool.name}". ${getToolPrompt(tool)}`,
390
+ createdAt: new Date(),
391
+ }
392
+ setMessages([initialMessage])
393
+ setPendingToolCalls([])
394
+
395
+ setState((prev) => ({
396
+ ...prev,
397
+ page: 'tool-chat',
398
+ inputValue: '',
399
+ selectedIndex: 0,
400
+ mode: 'chat',
401
+ }))
402
+ },
403
+ [saveRecentTool]
404
+ )
405
+
406
+ // Page navigation - go back (legacy, kept for compatibility)
407
+ const goBack = useCallback(() => {
408
+ if (state.phase !== 'idle') {
409
+ setState((prev) => ({
410
+ ...prev,
411
+ phase: 'idle',
412
+ page: 'home',
413
+ inputValue: '',
414
+ selectedIndex: 0,
415
+ mode: 'commands',
416
+ }))
417
+ setSelectedTool(null)
418
+ setMessages([])
419
+ setPendingToolCalls([])
420
+ }
421
+ }, [state.phase])
422
+
423
+ // Route query using fast model
424
+ const routeQuery = useCallback(
425
+ async (query: string): Promise<RouteResult> => {
426
+ const response = await fetch('/api/ai_assistant/route', {
427
+ method: 'POST',
428
+ headers: { 'Content-Type': 'application/json' },
429
+ body: JSON.stringify({
430
+ query,
431
+ availableTools: tools.map((t) => ({
432
+ name: t.name,
433
+ description: t.description,
434
+ })),
435
+ }),
436
+ })
437
+
438
+ if (!response.ok) {
439
+ throw new Error(`Routing failed: ${response.status}`)
440
+ }
441
+
442
+ return response.json()
443
+ },
444
+ [tools]
445
+ )
446
+
447
+ // Start agentic chat - AI has access to all tools
448
+ const startAgenticChat = useCallback(
449
+ async (initialQuery: string) => {
450
+ console.log('[startAgenticChat] Starting with query:', initialQuery)
451
+
452
+ setState((prev) => ({
453
+ ...prev,
454
+ phase: 'chatting',
455
+ page: 'tool-chat',
456
+ inputValue: '',
457
+ mode: 'chat',
458
+ }))
459
+
460
+ // Send the initial query to the chat API
461
+ setState((prev) => ({ ...prev, isStreaming: true }))
462
+
463
+ try {
464
+ const userMessage: ChatMessage = {
465
+ id: generateId(),
466
+ role: 'user',
467
+ content: initialQuery,
468
+ createdAt: new Date(),
469
+ }
470
+ setMessages([userMessage])
471
+
472
+ // Create abort controller for this stream
473
+ currentStreamController.current?.abort()
474
+ const controller = new AbortController()
475
+ currentStreamController.current = controller
476
+
477
+ console.log('[startAgenticChat] Sending request to /api/ai_assistant/chat with mode: agentic')
478
+ const response = await fetch('/api/ai_assistant/chat', {
479
+ method: 'POST',
480
+ headers: { 'Content-Type': 'application/json' },
481
+ body: JSON.stringify({
482
+ messages: [{ role: 'user', content: initialQuery }],
483
+ context: pageContext,
484
+ authContext: initialContext,
485
+ availableEntities: availableEntities?.map(e => e.entityId),
486
+ mode: 'agentic',
487
+ }),
488
+ signal: controller.signal,
489
+ })
490
+
491
+ console.log('[startAgenticChat] Response status:', response.status, 'ok:', response.ok)
492
+
493
+ if (!response.ok) {
494
+ const errorText = await response.text()
495
+ console.error('[startAgenticChat] Error response body:', errorText)
496
+ throw new Error(`Chat request failed: ${response.status} - ${errorText}`)
497
+ }
498
+
499
+ // Process the SSE stream
500
+ const reader = response.body?.getReader()
501
+ if (!reader) {
502
+ throw new Error('No response body')
503
+ }
504
+
505
+ const decoder = new TextDecoder()
506
+ let assistantContent = ''
507
+ let buffer = ''
508
+ let chunkCount = 0
509
+
510
+ while (true) {
511
+ const { done, value } = await reader.read()
512
+ if (done) {
513
+ console.log('[startAgenticChat] Stream done after', chunkCount, 'chunks')
514
+ break
515
+ }
516
+
517
+ chunkCount++
518
+ const rawChunk = decoder.decode(value, { stream: true })
519
+ buffer += rawChunk
520
+ console.log('[startAgenticChat] Chunk', chunkCount, 'raw:', rawChunk.substring(0, 200))
521
+
522
+ const lines = buffer.split('\n')
523
+ buffer = lines.pop() || ''
524
+
525
+ for (const line of lines) {
526
+ console.log('[startAgenticChat] Processing line:', line.substring(0, 100))
527
+ if (line.startsWith('data: ')) {
528
+ const data = line.slice(6)
529
+ if (data === '[DONE]') {
530
+ console.log('[startAgenticChat] Received [DONE]')
531
+ continue
532
+ }
533
+
534
+ try {
535
+ const event = JSON.parse(data)
536
+ console.log('[startAgenticChat] Parsed event:', event.type, event)
537
+
538
+ // Track all events for debug panel (except question - handled separately with enriched data)
539
+ if (event.type !== 'question') {
540
+ addDebugEvent(event.type as DebugEventType, event)
541
+ }
542
+
543
+ if (event.type === 'text') {
544
+ // Text received - no longer thinking
545
+ setIsThinking(false)
546
+
547
+ // Check if we need to start a new message (e.g., after answering a question)
548
+ if (shouldStartNewMessage.current) {
549
+ shouldStartNewMessage.current = false
550
+ // Finalize the current streaming message and reset content
551
+ setMessages((prev) => prev.map((m) => (m.id === 'streaming' ? { ...m, id: generateId() } : m)))
552
+ assistantContent = '' // Reset for new message
553
+ }
554
+
555
+ assistantContent += event.content || ''
556
+ console.log('[startAgenticChat] Text content now:', assistantContent.substring(0, 100))
557
+ setMessages((prev) => {
558
+ const existingAssistant = prev.find((m) => m.id === 'streaming')
559
+ if (existingAssistant) {
560
+ return prev.map((m) =>
561
+ m.id === 'streaming' ? { ...m, content: assistantContent } : m
562
+ )
563
+ } else {
564
+ return [
565
+ ...prev,
566
+ {
567
+ id: 'streaming',
568
+ role: 'assistant' as const,
569
+ content: assistantContent,
570
+ createdAt: new Date(),
571
+ },
572
+ ]
573
+ }
574
+ })
575
+ } else if (event.type === 'tool-call') {
576
+ // Tool calls are executed SERVER-SIDE via the AI SDK's maxSteps feature
577
+ // The AI will interpret the results and generate a human-friendly response
578
+ // We only need to show a visual indicator for dangerous tools that need confirmation
579
+ console.log('[startAgenticChat] Tool call event (executed server-side):', event.toolName)
580
+ const toolName = event.toolName as string
581
+ const toolArgs = event.args ?? {}
582
+
583
+ // For dangerous tools, show a confirmation indicator (though server already executed)
584
+ // This is mainly for UX feedback - in future, dangerous tools should require confirmation
585
+ if (!isToolSafeToAutoExecute(toolName)) {
586
+ console.log('[startAgenticChat] Dangerous tool was executed:', toolName)
587
+ // Note: Server already executed this - just show visual feedback
588
+ setPendingToolCalls((prev) => [
589
+ ...prev,
590
+ {
591
+ id: event.id,
592
+ toolName: toolName,
593
+ args: toolArgs,
594
+ status: 'completed' as const, // Already executed server-side
595
+ },
596
+ ])
597
+ }
598
+ // Safe tools: no action needed, server handled execution and AI interprets results
599
+ } else if (event.type === 'error') {
600
+ console.error('[startAgenticChat] Error event:', event.error)
601
+ } else if (event.type === 'done') {
602
+ console.log('[startAgenticChat] Done event received, sessionId:', event.sessionId)
603
+ // DIAGNOSTIC: Log done event details
604
+ console.log('[startAgenticChat] DIAGNOSTIC - Done event:', {
605
+ eventSessionId: event.sessionId,
606
+ currentRefValue: opencodeSessionIdRef.current,
607
+ willUpdate: !!event.sessionId,
608
+ })
609
+ setIsThinking(false)
610
+ setState((prev) => ({ ...prev, isStreaming: false }))
611
+ // Save session ID for conversation continuity
612
+ if (event.sessionId) {
613
+ updateOpencodeSessionId(event.sessionId)
614
+ // DIAGNOSTIC: Verify ref was updated
615
+ console.log('[startAgenticChat] DIAGNOSTIC - After update, ref value:', opencodeSessionIdRef.current)
616
+ }
617
+ } else if (event.type === 'question') {
618
+ // OpenCode is asking for confirmation
619
+ const question = event.question as OpenCodeQuestion
620
+ // Skip if already answered
621
+ if (answeredQuestionIds.current.has(question.id)) {
622
+ console.log('[startAgenticChat] Skipping already-answered question:', question.id)
623
+ } else {
624
+ console.log('[startAgenticChat] Question event:', question.id, 'questions:', question.questions)
625
+ setIsThinking(false)
626
+ setState((prev) => ({ ...prev, isStreaming: false }))
627
+ setPendingQuestion(question)
628
+ // Save session ID for conversation continuity
629
+ if (question.sessionID) {
630
+ updateOpencodeSessionId(question.sessionID)
631
+ }
632
+ // Add enriched debug event with question details visible at top level
633
+ addDebugEvent('question', {
634
+ type: 'question',
635
+ questionId: question.id,
636
+ sessionID: question.sessionID,
637
+ questionText: question.questions?.[0]?.question || 'No question text',
638
+ header: question.questions?.[0]?.header || 'Confirmation',
639
+ options: question.questions?.[0]?.options?.map(o => o.label) || [],
640
+ fullQuestion: question,
641
+ })
642
+ }
643
+ } else if (event.type === 'session-authorized') {
644
+ // Session has been authorized with ephemeral API key
645
+ console.log('[startAgenticChat] Session authorized:', event.sessionToken)
646
+ setIsSessionAuthorized(true)
647
+ }
648
+ } catch (parseError) {
649
+ console.warn('[startAgenticChat] Failed to parse event:', data, parseError)
650
+ }
651
+ }
652
+ }
653
+ }
654
+
655
+ console.log('[startAgenticChat] Final assistant content:', assistantContent)
656
+ // Finalize the assistant message
657
+ setMessages((prev) => prev.map((m) => (m.id === 'streaming' ? { ...m, id: generateId() } : m)))
658
+ } catch (error) {
659
+ // Ignore AbortError - this happens when we intentionally cancel the stream (e.g., answering a question)
660
+ if (error instanceof Error && error.name === 'AbortError') {
661
+ console.log('[startAgenticChat] Stream aborted (intentional)')
662
+ return
663
+ }
664
+ console.error('[startAgenticChat] Error:', error)
665
+ setMessages((prev) => [
666
+ ...prev,
667
+ {
668
+ id: generateId(),
669
+ role: 'assistant',
670
+ content: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,
671
+ createdAt: new Date(),
672
+ },
673
+ ])
674
+ } finally {
675
+ setState((prev) => ({ ...prev, isStreaming: false }))
676
+ }
677
+ },
678
+ [pageContext, initialContext, availableEntities, executeToolApi, addDebugEvent, updateOpencodeSessionId]
679
+ )
680
+
681
+ // Start general chat (no specific tool)
682
+ const startGeneralChat = useCallback(
683
+ async (initialQuery: string) => {
684
+ setState((prev) => ({
685
+ ...prev,
686
+ phase: 'chatting',
687
+ page: 'tool-chat',
688
+ inputValue: '',
689
+ mode: 'chat',
690
+ }))
691
+
692
+ setState((prev) => ({ ...prev, isStreaming: true }))
693
+
694
+ try {
695
+ const userMessage: ChatMessage = {
696
+ id: generateId(),
697
+ role: 'user',
698
+ content: initialQuery,
699
+ createdAt: new Date(),
700
+ }
701
+ setMessages([userMessage])
702
+
703
+ const response = await fetch('/api/ai_assistant/chat', {
704
+ method: 'POST',
705
+ headers: { 'Content-Type': 'application/json' },
706
+ body: JSON.stringify({
707
+ messages: [{ role: 'user', content: initialQuery }],
708
+ context: pageContext,
709
+ authContext: initialContext,
710
+ availableEntities: availableEntities?.map(e => e.entityId),
711
+ mode: 'default',
712
+ }),
713
+ })
714
+
715
+ if (!response.ok) {
716
+ throw new Error(`Chat request failed: ${response.status}`)
717
+ }
718
+
719
+ const reader = response.body?.getReader()
720
+ if (!reader) {
721
+ throw new Error('No response body')
722
+ }
723
+
724
+ const decoder = new TextDecoder()
725
+ let assistantContent = ''
726
+
727
+ while (true) {
728
+ const { done, value } = await reader.read()
729
+ if (done) break
730
+
731
+ const chunk = decoder.decode(value, { stream: true })
732
+ assistantContent += chunk
733
+
734
+ setMessages((prev) => {
735
+ const existingAssistant = prev.find((m) => m.id === 'streaming')
736
+ if (existingAssistant) {
737
+ return prev.map((m) =>
738
+ m.id === 'streaming' ? { ...m, content: assistantContent } : m
739
+ )
740
+ } else {
741
+ return [
742
+ ...prev,
743
+ {
744
+ id: 'streaming',
745
+ role: 'assistant' as const,
746
+ content: assistantContent,
747
+ createdAt: new Date(),
748
+ },
749
+ ]
750
+ }
751
+ })
752
+ }
753
+
754
+ setMessages((prev) => prev.map((m) => (m.id === 'streaming' ? { ...m, id: generateId() } : m)))
755
+ } catch (error) {
756
+ console.error('General chat error:', error)
757
+ setMessages((prev) => [
758
+ ...prev,
759
+ {
760
+ id: generateId(),
761
+ role: 'assistant',
762
+ content: 'Sorry, I encountered an error. Please try again.',
763
+ createdAt: new Date(),
764
+ },
765
+ ])
766
+ } finally {
767
+ setState((prev) => ({ ...prev, isStreaming: false }))
768
+ }
769
+ },
770
+ [pageContext, initialContext, availableEntities]
771
+ )
772
+
773
+ // Execute tool directly
774
+ const executeTool = useCallback(
775
+ async (toolName: string, args: Record<string, unknown> = {}): Promise<ToolExecutionResult> => {
776
+ setState((prev) => ({ ...prev, isLoading: true }))
777
+
778
+ try {
779
+ const result = await executeToolApi(toolName, args)
780
+
781
+ if (result.success) {
782
+ // Add to recent actions
783
+ const tool = tools.find((t) => t.name === toolName)
784
+ addRecentAction({
785
+ toolName,
786
+ displayName: tool?.description || toolName,
787
+ args,
788
+ })
789
+ }
790
+
791
+ return result
792
+ } finally {
793
+ setState((prev) => ({ ...prev, isLoading: false }))
794
+ }
795
+ },
796
+ [executeToolApi, tools, addRecentAction]
797
+ )
798
+
799
+ // Approve a pending tool call
800
+ const approveToolCall = useCallback(
801
+ async (toolCallId: string) => {
802
+ const toolCall = pendingToolCalls.find((tc) => tc.id === toolCallId)
803
+ if (!toolCall) return
804
+
805
+ // Update status to executing
806
+ setPendingToolCalls((prev) =>
807
+ prev.map((tc) => (tc.id === toolCallId ? { ...tc, status: 'executing' as const } : tc))
808
+ )
809
+
810
+ try {
811
+ // Execute the tool
812
+ const result = await executeToolApi(toolCall.toolName, toolCall.args)
813
+
814
+ // Update tool call with result
815
+ setPendingToolCalls((prev) =>
816
+ prev.map((tc) =>
817
+ tc.id === toolCallId
818
+ ? {
819
+ ...tc,
820
+ status: result.success ? ('completed' as const) : ('error' as const),
821
+ result: result.result,
822
+ error: result.error,
823
+ }
824
+ : tc
825
+ )
826
+ )
827
+
828
+ // Add to recent actions on success, show error on failure
829
+ // Don't add raw JSON - the AI will provide human-friendly interpretation
830
+ if (result.success) {
831
+ const tool = tools.find((t) => t.name === toolCall.toolName)
832
+ addRecentAction({
833
+ toolName: toolCall.toolName,
834
+ displayName: tool?.description || toolCall.toolName,
835
+ args: toolCall.args,
836
+ })
837
+ // Add a brief success indicator (optional - AI can interpret the result)
838
+ setMessages((prev) => [
839
+ ...prev,
840
+ {
841
+ id: generateId(),
842
+ role: 'assistant' as const,
843
+ content: `Done! The ${toolCall.toolName.replace(/_/g, ' ')} operation completed successfully.`,
844
+ createdAt: new Date(),
845
+ },
846
+ ])
847
+ } else {
848
+ setMessages((prev) => [
849
+ ...prev,
850
+ {
851
+ id: generateId(),
852
+ role: 'assistant' as const,
853
+ content: `I encountered an issue: ${result.error || 'Unknown error'}`,
854
+ createdAt: new Date(),
855
+ },
856
+ ])
857
+ }
858
+ } catch (error) {
859
+ setPendingToolCalls((prev) =>
860
+ prev.map((tc) =>
861
+ tc.id === toolCallId
862
+ ? {
863
+ ...tc,
864
+ status: 'error' as const,
865
+ error: error instanceof Error ? error.message : 'Unknown error',
866
+ }
867
+ : tc
868
+ )
869
+ )
870
+ }
871
+ },
872
+ [pendingToolCalls, executeToolApi, tools, addRecentAction]
873
+ )
874
+
875
+ // Reject a pending tool call
876
+ const rejectToolCall = useCallback((toolCallId: string) => {
877
+ setPendingToolCalls((prev) =>
878
+ prev.map((tc) => (tc.id === toolCallId ? { ...tc, status: 'rejected' as const } : tc))
879
+ )
880
+
881
+ setMessages((prev) => [
882
+ ...prev,
883
+ {
884
+ id: generateId(),
885
+ role: 'assistant' as const,
886
+ content: 'Tool call cancelled. How else can I help?',
887
+ createdAt: new Date(),
888
+ },
889
+ ])
890
+ }, [])
891
+
892
+ // Send message in agentic chat (via OpenCode agent)
893
+ const sendAgenticMessage = useCallback(
894
+ async (content: string) => {
895
+ if (!content.trim()) return
896
+
897
+ // Add user message
898
+ const userMessage: ChatMessage = {
899
+ id: generateId(),
900
+ role: 'user',
901
+ content: content.trim(),
902
+ createdAt: new Date(),
903
+ }
904
+ setMessages((prev) => [...prev, userMessage])
905
+ setState((prev) => ({ ...prev, isStreaming: true }))
906
+ setIsThinking(true)
907
+
908
+ try {
909
+ // DIAGNOSTIC: Log what we're about to send
910
+ console.log('[sendAgenticMessage] DIAGNOSTIC - About to send request:', {
911
+ sessionId: opencodeSessionIdRef.current,
912
+ messagesLength: messages.length + 1,
913
+ lastMessageContent: userMessage.content.substring(0, 50) + '...',
914
+ })
915
+
916
+ // Send to chat API with OpenCode session for context persistence
917
+ const response = await fetch('/api/ai_assistant/chat', {
918
+ method: 'POST',
919
+ headers: { 'Content-Type': 'application/json' },
920
+ body: JSON.stringify({
921
+ messages: [...messages, userMessage].map((m) => ({
922
+ role: m.role,
923
+ content: m.content,
924
+ })),
925
+ sessionId: opencodeSessionIdRef.current,
926
+ }),
927
+ })
928
+
929
+ if (!response.ok) {
930
+ throw new Error(`Chat request failed: ${response.status}`)
931
+ }
932
+
933
+ // Read the streaming response
934
+ const reader = response.body?.getReader()
935
+ if (!reader) {
936
+ throw new Error('No response body')
937
+ }
938
+
939
+ const decoder = new TextDecoder()
940
+ let assistantContent = ''
941
+ let buffer = ''
942
+
943
+ while (true) {
944
+ const { done, value } = await reader.read()
945
+ if (done) break
946
+
947
+ buffer += decoder.decode(value, { stream: true })
948
+
949
+ // Process SSE events
950
+ const lines = buffer.split('\n')
951
+ buffer = lines.pop() || '' // Keep incomplete line in buffer
952
+
953
+ for (const line of lines) {
954
+ if (line.startsWith('data: ')) {
955
+ const data = line.slice(6)
956
+ if (data === '[DONE]') continue
957
+
958
+ try {
959
+ const event = JSON.parse(data)
960
+
961
+ // Track all events for debug panel (except question - handled separately with enriched data)
962
+ if (event.type !== 'question') {
963
+ addDebugEvent(event.type as DebugEventType, event)
964
+ }
965
+
966
+ if (event.type === 'thinking') {
967
+ // OpenCode is processing - keep thinking state active
968
+ setIsThinking(true)
969
+ } else if (event.type === 'session-authorized') {
970
+ // Session has been authorized with ephemeral API key
971
+ console.log('[sendAgenticMessage] Session authorized:', event.sessionToken)
972
+ setIsSessionAuthorized(true)
973
+ } else if (event.type === 'text') {
974
+ // Check if we need to start a new message (e.g., after answering a question)
975
+ if (shouldStartNewMessage.current) {
976
+ shouldStartNewMessage.current = false
977
+ // Finalize the current streaming message and reset content
978
+ setMessages((prev) => prev.map((m) => (m.id === 'streaming' ? { ...m, id: generateId() } : m)))
979
+ assistantContent = '' // Reset for new message
980
+ }
981
+
982
+ // Text received - no longer thinking
983
+ setIsThinking(false)
984
+ assistantContent += event.content || ''
985
+ // Update assistant message in real-time
986
+ setMessages((prev) => {
987
+ const existingAssistant = prev.find(
988
+ (m) => m.role === 'assistant' && m.id === 'streaming'
989
+ )
990
+ if (existingAssistant) {
991
+ return prev.map((m) =>
992
+ m.id === 'streaming' ? { ...m, content: assistantContent } : m
993
+ )
994
+ } else {
995
+ return [
996
+ ...prev,
997
+ {
998
+ id: 'streaming',
999
+ role: 'assistant' as const,
1000
+ content: assistantContent,
1001
+ createdAt: new Date(),
1002
+ },
1003
+ ]
1004
+ }
1005
+ })
1006
+ } else if (event.type === 'done') {
1007
+ // Stream complete - save session ID for conversation persistence
1008
+ setIsThinking(false)
1009
+ setState((prev) => ({ ...prev, isStreaming: false }))
1010
+ if (event.sessionId) {
1011
+ updateOpencodeSessionId(event.sessionId)
1012
+ }
1013
+ } else if (event.type === 'error') {
1014
+ // Handle error event
1015
+ setIsThinking(false)
1016
+ setMessages((prev) => [
1017
+ ...prev,
1018
+ {
1019
+ id: generateId(),
1020
+ role: 'assistant' as const,
1021
+ content: `Error: ${event.error || 'Unknown error occurred'}`,
1022
+ createdAt: new Date(),
1023
+ },
1024
+ ])
1025
+ } else if (event.type === 'tool-call') {
1026
+ // Tool calls are executed SERVER-SIDE via the AI SDK's maxSteps feature
1027
+ // The AI will interpret the results and generate a human-friendly response
1028
+ console.log('[sendAgenticMessage] Tool call event (executed server-side):', event.toolName)
1029
+ const toolName = event.toolName as string
1030
+ const toolArgs = event.args ?? {}
1031
+
1032
+ // For dangerous tools, show visual feedback (server already executed)
1033
+ if (!isToolSafeToAutoExecute(toolName)) {
1034
+ console.log('[sendAgenticMessage] Dangerous tool was executed:', toolName)
1035
+ setPendingToolCalls((prev) => [
1036
+ ...prev,
1037
+ { id: event.id, toolName, args: toolArgs, status: 'completed' as const },
1038
+ ])
1039
+ }
1040
+ // Safe tools: no action needed, server handled execution
1041
+ } else if (event.type === 'question') {
1042
+ // OpenCode is asking for confirmation
1043
+ const question = event.question as OpenCodeQuestion
1044
+ // Skip if already answered
1045
+ if (answeredQuestionIds.current.has(question.id)) {
1046
+ console.log('[sendAgenticMessage] Skipping already-answered question:', question.id)
1047
+ } else {
1048
+ console.log('[sendAgenticMessage] Question event:', question.id, 'questions:', question.questions)
1049
+ setIsThinking(false)
1050
+ setState((prev) => ({ ...prev, isStreaming: false }))
1051
+ setPendingQuestion(question)
1052
+ // Add enriched debug event with question details visible at top level
1053
+ addDebugEvent('question', {
1054
+ type: 'question',
1055
+ questionId: question.id,
1056
+ sessionID: question.sessionID,
1057
+ questionText: question.questions?.[0]?.question || 'No question text',
1058
+ header: question.questions?.[0]?.header || 'Confirmation',
1059
+ options: question.questions?.[0]?.options?.map(o => o.label) || [],
1060
+ fullQuestion: question,
1061
+ })
1062
+ }
1063
+ }
1064
+ } catch {
1065
+ // Plain text chunk (fallback for non-SSE responses)
1066
+ assistantContent += data
1067
+ setMessages((prev) => {
1068
+ const existingAssistant = prev.find(
1069
+ (m) => m.role === 'assistant' && m.id === 'streaming'
1070
+ )
1071
+ if (existingAssistant) {
1072
+ return prev.map((m) =>
1073
+ m.id === 'streaming' ? { ...m, content: assistantContent } : m
1074
+ )
1075
+ } else {
1076
+ return [
1077
+ ...prev,
1078
+ {
1079
+ id: 'streaming',
1080
+ role: 'assistant' as const,
1081
+ content: assistantContent,
1082
+ createdAt: new Date(),
1083
+ },
1084
+ ]
1085
+ }
1086
+ })
1087
+ }
1088
+ } else if (line.trim() && !line.startsWith(':')) {
1089
+ // Plain text (not SSE format)
1090
+ assistantContent += line
1091
+ setMessages((prev) => {
1092
+ const existingAssistant = prev.find(
1093
+ (m) => m.role === 'assistant' && m.id === 'streaming'
1094
+ )
1095
+ if (existingAssistant) {
1096
+ return prev.map((m) =>
1097
+ m.id === 'streaming' ? { ...m, content: assistantContent } : m
1098
+ )
1099
+ } else {
1100
+ return [
1101
+ ...prev,
1102
+ {
1103
+ id: 'streaming',
1104
+ role: 'assistant' as const,
1105
+ content: assistantContent,
1106
+ createdAt: new Date(),
1107
+ },
1108
+ ]
1109
+ }
1110
+ })
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ // Finalize the assistant message
1116
+ setMessages((prev) => prev.map((m) => (m.id === 'streaming' ? { ...m, id: generateId() } : m)))
1117
+ } catch (error) {
1118
+ console.error('Tool chat error:', error)
1119
+ setMessages((prev) => [
1120
+ ...prev,
1121
+ {
1122
+ id: generateId(),
1123
+ role: 'assistant',
1124
+ content: 'Sorry, I encountered an error. Please try again.',
1125
+ createdAt: new Date(),
1126
+ },
1127
+ ])
1128
+ } finally {
1129
+ setState((prev) => ({ ...prev, isStreaming: false }))
1130
+ setIsThinking(false)
1131
+ }
1132
+ },
1133
+ [messages, addDebugEvent, updateOpencodeSessionId]
1134
+ )
1135
+
1136
+ // Main submit handler - starts agentic chat or continues existing session
1137
+ const handleSubmit = useCallback(
1138
+ async (query: string) => {
1139
+ if (!query.trim()) return
1140
+
1141
+ // DIAGNOSTIC: Log session check
1142
+ console.log('[handleSubmit] DIAGNOSTIC - Session check:', {
1143
+ refValue: opencodeSessionIdRef.current,
1144
+ willContinue: !!opencodeSessionIdRef.current,
1145
+ query: query.substring(0, 50) + '...',
1146
+ })
1147
+
1148
+ // If we have an existing session, continue the conversation
1149
+ // Use ref to get latest value (avoids stale closure issues)
1150
+ if (opencodeSessionIdRef.current) {
1151
+ console.log('[handleSubmit] Continuing session with query:', query, 'sessionId:', opencodeSessionIdRef.current)
1152
+ await sendAgenticMessage(query)
1153
+ } else {
1154
+ console.log('[handleSubmit] Starting new agentic chat with query:', query)
1155
+ // Start new agentic session where AI has access to all tools
1156
+ await startAgenticChat(query)
1157
+ }
1158
+ },
1159
+ [startAgenticChat, sendAgenticMessage]
1160
+ )
1161
+
1162
+ // Answer a pending OpenCode question
1163
+ // The original SSE stream continues running and will receive the follow-up response
1164
+ const answerQuestion = useCallback(
1165
+ async (answer: number) => {
1166
+ if (!pendingQuestion) return
1167
+
1168
+ console.log('[answerQuestion] Answering question:', pendingQuestion.id, 'with:', answer)
1169
+
1170
+ // Mark question as answered BEFORE sending - prevents duplicate display
1171
+ const questionId = pendingQuestion.id
1172
+ answeredQuestionIds.current.add(questionId)
1173
+
1174
+ // Signal that the next text event should start a NEW message (not update the old one)
1175
+ shouldStartNewMessage.current = true
1176
+
1177
+ // Clear the pending question UI and show thinking state
1178
+ setPendingQuestion(null)
1179
+ setIsThinking(true)
1180
+
1181
+ // Add visual feedback of the answer
1182
+ const selectedOption = pendingQuestion.questions[0]?.options[answer]
1183
+ setMessages((prev) => [
1184
+ ...prev,
1185
+ {
1186
+ id: generateId(),
1187
+ role: 'user' as const,
1188
+ content: `[Confirmed: ${selectedOption?.label || 'Yes'}]`,
1189
+ createdAt: new Date(),
1190
+ },
1191
+ ])
1192
+
1193
+ try {
1194
+ // Send answer as simple POST - the original SSE stream will receive the follow-up
1195
+ const sessionId = pendingQuestion.sessionID
1196
+ const response = await fetch('/api/ai_assistant/chat', {
1197
+ method: 'POST',
1198
+ headers: { 'Content-Type': 'application/json' },
1199
+ body: JSON.stringify({
1200
+ answerQuestion: {
1201
+ questionId,
1202
+ answer,
1203
+ sessionId,
1204
+ },
1205
+ }),
1206
+ })
1207
+
1208
+ if (!response.ok) {
1209
+ const errorData = await response.json().catch(() => ({}))
1210
+ throw new Error(errorData.error || `Answer request failed: ${response.status}`)
1211
+ }
1212
+
1213
+ // Answer sent successfully - the original stream will handle the response
1214
+ console.log('[answerQuestion] Answer sent, original stream will receive response')
1215
+ // Note: isThinking stays true until the original stream sends 'done' or more 'text'
1216
+ } catch (error) {
1217
+ console.error('[answerQuestion] Error:', error)
1218
+ setIsThinking(false)
1219
+ setMessages((prev) => [
1220
+ ...prev,
1221
+ {
1222
+ id: generateId(),
1223
+ role: 'assistant' as const,
1224
+ content: `Error: ${error instanceof Error ? error.message : 'Failed to send answer'}`,
1225
+ createdAt: new Date(),
1226
+ },
1227
+ ])
1228
+ }
1229
+ },
1230
+ [pendingQuestion]
1231
+ )
1232
+
1233
+ // Legacy sendMessage function (for backwards compatibility)
1234
+ const sendMessage = useCallback(
1235
+ async (content: string) => {
1236
+ if (!content.trim()) return
1237
+
1238
+ // Add user message
1239
+ const userMessage: ChatMessage = {
1240
+ id: generateId(),
1241
+ role: 'user',
1242
+ content: content.trim(),
1243
+ createdAt: new Date(),
1244
+ }
1245
+ setMessages((prev) => [...prev, userMessage])
1246
+ setState((prev) => ({ ...prev, isStreaming: true }))
1247
+
1248
+ try {
1249
+ // Send to chat API
1250
+ const response = await fetch('/api/ai_assistant/chat', {
1251
+ method: 'POST',
1252
+ headers: { 'Content-Type': 'application/json' },
1253
+ body: JSON.stringify({
1254
+ messages: [...messages, userMessage].map((m) => ({
1255
+ role: m.role,
1256
+ content: m.content,
1257
+ })),
1258
+ context: pageContext,
1259
+ }),
1260
+ })
1261
+
1262
+ if (!response.ok) {
1263
+ throw new Error(`Chat request failed: ${response.status}`)
1264
+ }
1265
+
1266
+ // Read the streaming response
1267
+ const reader = response.body?.getReader()
1268
+ if (!reader) {
1269
+ throw new Error('No response body')
1270
+ }
1271
+
1272
+ const decoder = new TextDecoder()
1273
+ let assistantContent = ''
1274
+
1275
+ while (true) {
1276
+ const { done, value } = await reader.read()
1277
+ if (done) break
1278
+
1279
+ const chunk = decoder.decode(value, { stream: true })
1280
+ assistantContent += chunk
1281
+
1282
+ // Update assistant message in real-time
1283
+ setMessages((prev) => {
1284
+ const existingAssistant = prev.find(
1285
+ (m) => m.role === 'assistant' && m.id === 'streaming'
1286
+ )
1287
+ if (existingAssistant) {
1288
+ return prev.map((m) =>
1289
+ m.id === 'streaming' ? { ...m, content: assistantContent } : m
1290
+ )
1291
+ } else {
1292
+ return [
1293
+ ...prev,
1294
+ {
1295
+ id: 'streaming',
1296
+ role: 'assistant' as const,
1297
+ content: assistantContent,
1298
+ createdAt: new Date(),
1299
+ },
1300
+ ]
1301
+ }
1302
+ })
1303
+ }
1304
+
1305
+ // Finalize the assistant message
1306
+ setMessages((prev) => prev.map((m) => (m.id === 'streaming' ? { ...m, id: generateId() } : m)))
1307
+ } catch (error) {
1308
+ console.error('Chat error:', error)
1309
+ // Add error message
1310
+ setMessages((prev) => [
1311
+ ...prev,
1312
+ {
1313
+ id: generateId(),
1314
+ role: 'assistant',
1315
+ content: 'Sorry, I encountered an error. Please try again.',
1316
+ createdAt: new Date(),
1317
+ },
1318
+ ])
1319
+ } finally {
1320
+ setState((prev) => ({ ...prev, isStreaming: false }))
1321
+ }
1322
+ },
1323
+ [messages, pageContext]
1324
+ )
1325
+
1326
+ const clearMessages = useCallback(() => {
1327
+ setMessages([])
1328
+ setPendingToolCalls([])
1329
+ }, [])
1330
+
1331
+ return {
1332
+ // State
1333
+ state: {
1334
+ ...state,
1335
+ isLoading: state.isLoading || toolsLoading,
1336
+ },
1337
+ isThinking,
1338
+ isSessionAuthorized,
1339
+ pageContext,
1340
+ selectedEntities,
1341
+ tools,
1342
+ filteredTools,
1343
+ recentActions,
1344
+ recentTools,
1345
+ messages,
1346
+ pendingToolCalls,
1347
+ selectedTool,
1348
+ initialContext,
1349
+ availableEntities,
1350
+
1351
+ // Navigation actions
1352
+ open,
1353
+ close,
1354
+ setIsOpen,
1355
+ setInputValue,
1356
+ setSelectedIndex,
1357
+
1358
+ // Intelligent routing - submit natural language query
1359
+ handleSubmit,
1360
+ reset,
1361
+
1362
+ // Page navigation (legacy, kept for compatibility)
1363
+ goToToolChat,
1364
+ goBack,
1365
+
1366
+ // Tool execution
1367
+ executeTool,
1368
+ approveToolCall,
1369
+ rejectToolCall,
1370
+
1371
+ // Chat actions
1372
+ sendMessage,
1373
+ sendAgenticMessage,
1374
+ clearMessages,
1375
+
1376
+ // Legacy compatibility
1377
+ setMode,
1378
+
1379
+ // Debug mode
1380
+ debugEvents,
1381
+ showDebug,
1382
+ setShowDebug,
1383
+ clearDebugEvents,
1384
+
1385
+ // OpenCode question handling
1386
+ pendingQuestion,
1387
+ answerQuestion,
1388
+ }
1389
+ }