@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.
- package/AGENTS.md +1090 -0
- package/README.md +607 -0
- package/build.mjs +92 -0
- package/dist/di.js +8 -0
- package/dist/di.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandFooter.js +80 -0
- package/dist/frontend/components/CommandPalette/CommandFooter.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandHeader.js +53 -0
- package/dist/frontend/components/CommandPalette/CommandHeader.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandInput.js +29 -0
- package/dist/frontend/components/CommandPalette/CommandInput.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandItem.js +92 -0
- package/dist/frontend/components/CommandPalette/CommandItem.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPalette.js +244 -0
- package/dist/frontend/components/CommandPalette/CommandPalette.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js +42 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteProvider.js.map +7 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js +18 -0
- package/dist/frontend/components/CommandPalette/CommandPaletteWrapper.js.map +7 -0
- package/dist/frontend/components/CommandPalette/DebugPanel.js +215 -0
- package/dist/frontend/components/CommandPalette/DebugPanel.js.map +7 -0
- package/dist/frontend/components/CommandPalette/MessageBubble.js +64 -0
- package/dist/frontend/components/CommandPalette/MessageBubble.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js +91 -0
- package/dist/frontend/components/CommandPalette/ToolCallConfirmation.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolCallDisplay.js +47 -0
- package/dist/frontend/components/CommandPalette/ToolCallDisplay.js.map +7 -0
- package/dist/frontend/components/CommandPalette/ToolChatPage.js +74 -0
- package/dist/frontend/components/CommandPalette/ToolChatPage.js.map +7 -0
- package/dist/frontend/components/CommandPalette/index.js +28 -0
- package/dist/frontend/components/CommandPalette/index.js.map +7 -0
- package/dist/frontend/constants.js +41 -0
- package/dist/frontend/constants.js.map +7 -0
- package/dist/frontend/hooks/index.js +13 -0
- package/dist/frontend/hooks/index.js.map +7 -0
- package/dist/frontend/hooks/useCommandPalette.js +1094 -0
- package/dist/frontend/hooks/useCommandPalette.js.map +7 -0
- package/dist/frontend/hooks/useMcpTools.js +66 -0
- package/dist/frontend/hooks/useMcpTools.js.map +7 -0
- package/dist/frontend/hooks/usePageContext.js +48 -0
- package/dist/frontend/hooks/usePageContext.js.map +7 -0
- package/dist/frontend/hooks/useRecentActions.js +56 -0
- package/dist/frontend/hooks/useRecentActions.js.map +7 -0
- package/dist/frontend/hooks/useRecentTools.js +55 -0
- package/dist/frontend/hooks/useRecentTools.js.map +7 -0
- package/dist/frontend/index.js +35 -0
- package/dist/frontend/index.js.map +7 -0
- package/dist/frontend/types.js +1 -0
- package/dist/frontend/types.js.map +7 -0
- package/dist/frontend/utils/index.js +7 -0
- package/dist/frontend/utils/index.js.map +7 -0
- package/dist/frontend/utils/toolMatcher.js +95 -0
- package/dist/frontend/utils/toolMatcher.js.map +7 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/ai_assistant/acl.js +14 -0
- package/dist/modules/ai_assistant/acl.js.map +7 -0
- package/dist/modules/ai_assistant/api/chat/route.js +152 -0
- package/dist/modules/ai_assistant/api/chat/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/health/route.js +27 -0
- package/dist/modules/ai_assistant/api/health/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/route/route.js +123 -0
- package/dist/modules/ai_assistant/api/route/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +60 -0
- package/dist/modules/ai_assistant/api/settings/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/tools/execute/route.js +58 -0
- package/dist/modules/ai_assistant/api/tools/execute/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/tools/route.js +48 -0
- package/dist/modules/ai_assistant/api/tools/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +192 -0
- package/dist/modules/ai_assistant/cli.js.map +7 -0
- package/dist/modules/ai_assistant/di.js +11 -0
- package/dist/modules/ai_assistant/di.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +257 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/index.js +13 -0
- package/dist/modules/ai_assistant/index.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js +13 -0
- package/dist/modules/ai_assistant/lib/ai-sdk.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js +249 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +177 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js +210 -0
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +7 -0
- package/dist/modules/ai_assistant/lib/auth.js +87 -0
- package/dist/modules/ai_assistant/lib/auth.js.map +7 -0
- package/dist/modules/ai_assistant/lib/chat-config.js +117 -0
- package/dist/modules/ai_assistant/lib/chat-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/client-factory.js +60 -0
- package/dist/modules/ai_assistant/lib/client-factory.js.map +7 -0
- package/dist/modules/ai_assistant/lib/http-server.js +367 -0
- package/dist/modules/ai_assistant/lib/http-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/in-process-client.js +126 -0
- package/dist/modules/ai_assistant/lib/in-process-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-client.js +146 -0
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +283 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-server-config.js +160 -0
- package/dist/modules/ai_assistant/lib/mcp-server-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-server.js +156 -0
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +44 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +7 -0
- package/dist/modules/ai_assistant/lib/opencode-client.js +247 -0
- package/dist/modules/ai_assistant/lib/opencode-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/opencode-handlers.js +398 -0
- package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +7 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js +94 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-executor.js +55 -0
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-index-config.js +125 -0
- package/dist/modules/ai_assistant/lib/tool-index-config.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js +88 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-registry.js +65 -0
- package/dist/modules/ai_assistant/lib/tool-registry.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-search.js +192 -0
- package/dist/modules/ai_assistant/lib/tool-search.js.map +7 -0
- package/dist/modules/ai_assistant/lib/types.js +1 -0
- package/dist/modules/ai_assistant/lib/types.js.map +7 -0
- package/package.json +108 -0
- package/src/di.ts +11 -0
- package/src/frontend/components/CommandPalette/CommandFooter.tsx +113 -0
- package/src/frontend/components/CommandPalette/CommandHeader.tsx +76 -0
- package/src/frontend/components/CommandPalette/CommandInput.tsx +50 -0
- package/src/frontend/components/CommandPalette/CommandItem.tsx +111 -0
- package/src/frontend/components/CommandPalette/CommandPalette.tsx +276 -0
- package/src/frontend/components/CommandPalette/CommandPaletteProvider.tsx +60 -0
- package/src/frontend/components/CommandPalette/CommandPaletteWrapper.tsx +21 -0
- package/src/frontend/components/CommandPalette/DebugPanel.tsx +257 -0
- package/src/frontend/components/CommandPalette/MessageBubble.tsx +73 -0
- package/src/frontend/components/CommandPalette/ToolCallConfirmation.tsx +130 -0
- package/src/frontend/components/CommandPalette/ToolCallDisplay.tsx +57 -0
- package/src/frontend/components/CommandPalette/ToolChatPage.tsx +125 -0
- package/src/frontend/components/CommandPalette/index.ts +14 -0
- package/src/frontend/constants.ts +35 -0
- package/src/frontend/hooks/index.ts +5 -0
- package/src/frontend/hooks/useCommandPalette.ts +1389 -0
- package/src/frontend/hooks/useMcpTools.ts +73 -0
- package/src/frontend/hooks/usePageContext.ts +61 -0
- package/src/frontend/hooks/useRecentActions.ts +64 -0
- package/src/frontend/hooks/useRecentTools.ts +69 -0
- package/src/frontend/index.ts +39 -0
- package/src/frontend/types.ts +260 -0
- package/src/frontend/utils/index.ts +1 -0
- package/src/frontend/utils/toolMatcher.ts +127 -0
- package/src/index.ts +92 -0
- package/src/modules/ai_assistant/acl.ts +10 -0
- package/src/modules/ai_assistant/api/chat/route.ts +213 -0
- package/src/modules/ai_assistant/api/health/route.ts +30 -0
- package/src/modules/ai_assistant/api/route/route.ts +149 -0
- package/src/modules/ai_assistant/api/settings/route.ts +73 -0
- package/src/modules/ai_assistant/api/tools/execute/route.ts +71 -0
- package/src/modules/ai_assistant/api/tools/route.ts +57 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +233 -0
- package/src/modules/ai_assistant/di.ts +9 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +418 -0
- package/src/modules/ai_assistant/index.ts +11 -0
- package/src/modules/ai_assistant/lib/ai-sdk.ts +5 -0
- package/src/modules/ai_assistant/lib/api-discovery-tools.ts +334 -0
- package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +243 -0
- package/src/modules/ai_assistant/lib/api-endpoint-index.ts +381 -0
- package/src/modules/ai_assistant/lib/auth.ts +185 -0
- package/src/modules/ai_assistant/lib/chat-config.ts +152 -0
- package/src/modules/ai_assistant/lib/client-factory.ts +130 -0
- package/src/modules/ai_assistant/lib/http-server.ts +498 -0
- package/src/modules/ai_assistant/lib/in-process-client.ts +205 -0
- package/src/modules/ai_assistant/lib/mcp-client.ts +221 -0
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +373 -0
- package/src/modules/ai_assistant/lib/mcp-server-config.ts +287 -0
- package/src/modules/ai_assistant/lib/mcp-server.ts +214 -0
- package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +76 -0
- package/src/modules/ai_assistant/lib/opencode-client.ts +426 -0
- package/src/modules/ai_assistant/lib/opencode-handlers.ts +676 -0
- package/src/modules/ai_assistant/lib/schema-utils.ts +142 -0
- package/src/modules/ai_assistant/lib/tool-executor.ts +71 -0
- package/src/modules/ai_assistant/lib/tool-index-config.ts +178 -0
- package/src/modules/ai_assistant/lib/tool-loader.ts +149 -0
- package/src/modules/ai_assistant/lib/tool-registry.ts +114 -0
- package/src/modules/ai_assistant/lib/tool-search.ts +308 -0
- package/src/modules/ai_assistant/lib/types.ts +147 -0
- package/test-schema.ts +37 -0
- package/tsconfig.json +10 -0
- 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
|
+
}
|