@swarmclawai/swarmclaw 1.9.20 → 1.9.22
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/README.md +22 -2
- package/package.json +3 -3
- package/src/app/api/setup/check-provider/route.test.ts +44 -0
- package/src/app/api/setup/check-provider/route.ts +235 -63
- package/src/components/agents/agent-sheet.tsx +18 -5
- package/src/components/auth/setup-wizard/step-connect.tsx +13 -3
- package/src/components/auth/setup-wizard/types.ts +2 -9
- package/src/components/chat/activity-moment.tsx +4 -0
- package/src/components/chat/tool-call-bubble.tsx +6 -0
- package/src/components/providers/provider-diagnostics-list.tsx +58 -0
- package/src/components/providers/provider-sheet.tsx +11 -1
- package/src/features/providers/queries.ts +2 -1
- package/src/lib/server/capability-router.test.ts +4 -4
- package/src/lib/server/capability-router.ts +1 -0
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +27 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +21 -0
- package/src/lib/server/chat-execution/iteration-event-handler.ts +1 -1
- package/src/lib/server/chat-execution/stream-continuation.ts +6 -2
- package/src/lib/server/plugins-advanced.test.ts +7 -3
- package/src/lib/server/provider-diagnostics.test.ts +39 -0
- package/src/lib/server/provider-diagnostics.ts +114 -0
- package/src/lib/server/session-tools/web-crawl.test.ts +106 -0
- package/src/lib/server/session-tools/web-inputs.test.ts +5 -0
- package/src/lib/server/session-tools/web-utils.ts +8 -2
- package/src/lib/server/session-tools/web.ts +256 -29
- package/src/lib/server/storage.ts +2 -0
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +3 -3
- package/src/lib/server/tool-capability-policy.ts +4 -1
- package/src/lib/server/tool-planning.test.ts +2 -1
- package/src/lib/server/tool-planning.ts +31 -0
- package/src/lib/server/untrusted-content.ts +2 -2
- package/src/types/provider.ts +21 -0
- package/src/types/session.ts +2 -0
|
@@ -10,7 +10,7 @@ import { sleep } from '@/lib/shared-utils'
|
|
|
10
10
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
11
11
|
import { toast } from 'sonner'
|
|
12
12
|
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
13
|
-
import type { ProviderType, ClaudeSkill, AgentPackManifest, AgentRoutingStrategy, AgentRoutingTarget } from '@/types'
|
|
13
|
+
import type { ProviderType, ProviderDiagnosticStep, ClaudeSkill, AgentPackManifest, AgentRoutingStrategy, AgentRoutingTarget } from '@/types'
|
|
14
14
|
import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
|
|
15
15
|
import { MCP_INJECTION_PROVIDER_IDS, NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS, WORKER_ONLY_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
16
16
|
import { isOrchestratorProviderEligible } from '@/lib/orchestrator-config'
|
|
@@ -33,6 +33,7 @@ import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredential
|
|
|
33
33
|
import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
|
|
34
34
|
import { AgentMarketplaceSettings } from '@/features/swarmdock/agent-marketplace-settings'
|
|
35
35
|
import type { ConfigVersion } from '@/types/config-version'
|
|
36
|
+
import { ProviderDiagnosticsList } from '@/components/providers/provider-diagnostics-list'
|
|
36
37
|
|
|
37
38
|
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
38
39
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
@@ -284,6 +285,7 @@ export function AgentSheet() {
|
|
|
284
285
|
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'pass' | 'fail'>('idle')
|
|
285
286
|
const [testMessage, setTestMessage] = useState('')
|
|
286
287
|
const [testErrorCode, setTestErrorCode] = useState<string | null>(null)
|
|
288
|
+
const [testDiagnostics, setTestDiagnostics] = useState<ProviderDiagnosticStep[]>([])
|
|
287
289
|
const [testDeviceId, setTestDeviceId] = useState<string | null>(null)
|
|
288
290
|
const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
|
|
289
291
|
const [configCopied, setConfigCopied] = useState(false)
|
|
@@ -426,6 +428,7 @@ export function AgentSheet() {
|
|
|
426
428
|
.catch(() => {})
|
|
427
429
|
setTestStatus('idle')
|
|
428
430
|
setTestMessage('')
|
|
431
|
+
setTestDiagnostics([])
|
|
429
432
|
setShowAdvancedSettings(false)
|
|
430
433
|
if (editing) {
|
|
431
434
|
setName(editing.name)
|
|
@@ -690,6 +693,7 @@ export function AgentSheet() {
|
|
|
690
693
|
useEffect(() => {
|
|
691
694
|
setTestStatus('idle')
|
|
692
695
|
setTestMessage('')
|
|
696
|
+
setTestDiagnostics([])
|
|
693
697
|
}, [provider, credentialId, apiEndpoint])
|
|
694
698
|
|
|
695
699
|
// Fetch MCP tools when selected servers change
|
|
@@ -758,6 +762,7 @@ export function AgentSheet() {
|
|
|
758
762
|
setTestStatus('idle')
|
|
759
763
|
setTestMessage('')
|
|
760
764
|
setTestErrorCode(null)
|
|
765
|
+
setTestDiagnostics([])
|
|
761
766
|
setAddingKey(false)
|
|
762
767
|
setNewKeyName('')
|
|
763
768
|
setNewKeyValue('')
|
|
@@ -1047,8 +1052,9 @@ export function AgentSheet() {
|
|
|
1047
1052
|
setTestStatus('testing')
|
|
1048
1053
|
setTestMessage('')
|
|
1049
1054
|
setTestErrorCode(null)
|
|
1055
|
+
setTestDiagnostics([])
|
|
1050
1056
|
try {
|
|
1051
|
-
const result = await api<{ ok: boolean; message: string; errorCode?: string; deviceId?: string }>('POST', '/setup/check-provider', {
|
|
1057
|
+
const result = await api<{ ok: boolean; message: string; errorCode?: string; deviceId?: string; diagnostics?: ProviderDiagnosticStep[] }>('POST', '/setup/check-provider', {
|
|
1052
1058
|
provider,
|
|
1053
1059
|
credentialId,
|
|
1054
1060
|
endpoint: apiEndpoint,
|
|
@@ -1057,6 +1063,7 @@ export function AgentSheet() {
|
|
|
1057
1063
|
}, {
|
|
1058
1064
|
timeoutMs: CONNECTION_TEST_TIMEOUT_MS,
|
|
1059
1065
|
})
|
|
1066
|
+
setTestDiagnostics(result.diagnostics ?? [])
|
|
1060
1067
|
if (result.deviceId) setTestDeviceId(result.deviceId)
|
|
1061
1068
|
if (result.ok) {
|
|
1062
1069
|
let syncedModels: string[] = []
|
|
@@ -1084,6 +1091,7 @@ export function AgentSheet() {
|
|
|
1084
1091
|
const msg = err instanceof Error ? err.message : 'Connection test failed'
|
|
1085
1092
|
setTestStatus('fail')
|
|
1086
1093
|
setTestMessage(msg)
|
|
1094
|
+
setTestDiagnostics([])
|
|
1087
1095
|
toast.error(msg)
|
|
1088
1096
|
return false
|
|
1089
1097
|
}
|
|
@@ -1317,6 +1325,10 @@ export function AgentSheet() {
|
|
|
1317
1325
|
<button
|
|
1318
1326
|
type="button"
|
|
1319
1327
|
onClick={() => {
|
|
1328
|
+
setTestStatus('idle')
|
|
1329
|
+
setTestMessage('')
|
|
1330
|
+
setTestErrorCode(null)
|
|
1331
|
+
setTestDiagnostics([])
|
|
1320
1332
|
if (!openclawEnabled) {
|
|
1321
1333
|
setOpenclawEnabled(true)
|
|
1322
1334
|
setProvider('openclaw')
|
|
@@ -1330,9 +1342,6 @@ export function AgentSheet() {
|
|
|
1330
1342
|
setApiEndpoint(null)
|
|
1331
1343
|
setCredentialId(null)
|
|
1332
1344
|
setGatewayProfileId(null)
|
|
1333
|
-
setTestStatus('idle')
|
|
1334
|
-
setTestMessage('')
|
|
1335
|
-
setTestErrorCode(null)
|
|
1336
1345
|
}
|
|
1337
1346
|
}}
|
|
1338
1347
|
className={`relative h-6 w-11 rounded-full border-none transition-colors duration-200 ${openclawEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
@@ -1473,6 +1482,7 @@ export function AgentSheet() {
|
|
|
1473
1482
|
<p className="text-[14px] text-emerald-400 font-600">Connected</p>
|
|
1474
1483
|
</div>
|
|
1475
1484
|
<p className="text-[13px] text-text-2/80 leading-[1.6]">Gateway is reachable and this device is paired. Tools and models are managed by the OpenClaw instance.</p>
|
|
1485
|
+
<ProviderDiagnosticsList diagnostics={testDiagnostics} />
|
|
1476
1486
|
</div>
|
|
1477
1487
|
)}
|
|
1478
1488
|
{testStatus === 'fail' && (
|
|
@@ -1548,6 +1558,7 @@ export function AgentSheet() {
|
|
|
1548
1558
|
</p>
|
|
1549
1559
|
</div>
|
|
1550
1560
|
)}
|
|
1561
|
+
<ProviderDiagnosticsList diagnostics={testDiagnostics} />
|
|
1551
1562
|
</div>
|
|
1552
1563
|
)}
|
|
1553
1564
|
</div>
|
|
@@ -2965,11 +2976,13 @@ export function AgentSheet() {
|
|
|
2965
2976
|
{!openclawEnabled && testStatus === 'fail' && (
|
|
2966
2977
|
<div className="mb-4 p-3 rounded-[12px] bg-red-500/[0.08] border border-red-500/20">
|
|
2967
2978
|
<p className="text-[13px] text-red-400">{testMessage || 'Connection test failed'}</p>
|
|
2979
|
+
<ProviderDiagnosticsList diagnostics={testDiagnostics} />
|
|
2968
2980
|
</div>
|
|
2969
2981
|
)}
|
|
2970
2982
|
{!openclawEnabled && testStatus === 'pass' && (
|
|
2971
2983
|
<div className="mb-4 p-3 rounded-[12px] bg-emerald-500/[0.08] border border-emerald-500/20">
|
|
2972
2984
|
<p className="text-[13px] text-emerald-400">{testMessage || 'Connected successfully'}</p>
|
|
2985
|
+
<ProviderDiagnosticsList diagnostics={testDiagnostics} />
|
|
2973
2986
|
</div>
|
|
2974
2987
|
)}
|
|
2975
2988
|
|
|
@@ -5,7 +5,8 @@ import { api } from '@/lib/app/api-client'
|
|
|
5
5
|
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
6
6
|
import { getDefaultModelForProvider } from '@/lib/setup-defaults'
|
|
7
7
|
import { OpenClawDeployPanel } from '@/components/openclaw/openclaw-deploy-panel'
|
|
8
|
-
import
|
|
8
|
+
import { ProviderDiagnosticsList } from '@/components/providers/provider-diagnostics-list'
|
|
9
|
+
import type { Credential, Credentials, GatewayProfile, ProviderDiagnosticStep, ProviderId, ProviderConfig } from '@/types'
|
|
9
10
|
import type { StepConnectProps, CheckState, ProviderCheckResponse, ConfiguredProvider } from './types'
|
|
10
11
|
import {
|
|
11
12
|
formatEndpointHost,
|
|
@@ -34,6 +35,7 @@ export function StepConnect({
|
|
|
34
35
|
const [checkState, setCheckState] = useState<CheckState>(editingProvider?.verified ? 'ok' : 'idle')
|
|
35
36
|
const [checkMessage, setCheckMessage] = useState('')
|
|
36
37
|
const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
|
|
38
|
+
const [checkDiagnostics, setCheckDiagnostics] = useState<ProviderDiagnosticStep[]>([])
|
|
37
39
|
const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
|
|
38
40
|
const [providerSuggestedModel, setProviderSuggestedModel] = useState(
|
|
39
41
|
editingProvider?.defaultModel || (provider === 'custom' ? '' : getDefaultModelForProvider(provider)),
|
|
@@ -111,6 +113,7 @@ export function StepConnect({
|
|
|
111
113
|
setCheckState('idle')
|
|
112
114
|
setCheckMessage('')
|
|
113
115
|
setCheckErrorCode(null)
|
|
116
|
+
setCheckDiagnostics([])
|
|
114
117
|
setError('')
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -118,12 +121,14 @@ export function StepConnect({
|
|
|
118
121
|
if (requiresKey && !hasKeyOrCredential) {
|
|
119
122
|
setCheckState('error')
|
|
120
123
|
setCheckMessage('Please paste your API key or select a saved key first.')
|
|
124
|
+
setCheckDiagnostics([])
|
|
121
125
|
return false
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
setCheckState('checking')
|
|
125
129
|
setCheckMessage('')
|
|
126
130
|
setCheckErrorCode(null)
|
|
131
|
+
setCheckDiagnostics([])
|
|
127
132
|
setError('')
|
|
128
133
|
try {
|
|
129
134
|
const result = await api<ProviderCheckResponse>('POST', '/setup/check-provider', {
|
|
@@ -140,6 +145,7 @@ export function StepConnect({
|
|
|
140
145
|
setProviderSuggestedModel(result.recommendedModel)
|
|
141
146
|
}
|
|
142
147
|
setCheckErrorCode(result.errorCode || null)
|
|
148
|
+
setCheckDiagnostics(result.diagnostics ?? [])
|
|
143
149
|
setOpenclawDeviceId(result.deviceId || null)
|
|
144
150
|
setCheckState(result.ok ? 'ok' : 'error')
|
|
145
151
|
setCheckMessage(result.message || (result.ok ? 'Connected successfully.' : 'Connection failed.'))
|
|
@@ -148,6 +154,7 @@ export function StepConnect({
|
|
|
148
154
|
setCheckState('error')
|
|
149
155
|
setCheckMessage(errorMessage(err))
|
|
150
156
|
setCheckErrorCode(null)
|
|
157
|
+
setCheckDiagnostics([])
|
|
151
158
|
return false
|
|
152
159
|
}
|
|
153
160
|
}
|
|
@@ -280,7 +287,7 @@ export function StepConnect({
|
|
|
280
287
|
<input
|
|
281
288
|
type="text"
|
|
282
289
|
value={endpoint}
|
|
283
|
-
onChange={(e) => { setEndpoint(e.target.value); setCheckState('idle'); setCheckMessage('') }}
|
|
290
|
+
onChange={(e) => { setEndpoint(e.target.value); setCheckState('idle'); setCheckMessage(''); setCheckDiagnostics([]) }}
|
|
284
291
|
placeholder={selectedProvider.defaultEndpoint || ''}
|
|
285
292
|
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface
|
|
286
293
|
text-text text-[14px] font-mono outline-none transition-all duration-200
|
|
@@ -295,6 +302,7 @@ export function StepConnect({
|
|
|
295
302
|
setEndpoint(isCloud ? (selectedProvider.defaultEndpoint || '') : selectedProvider.cloudEndpoint!)
|
|
296
303
|
setCheckState('idle')
|
|
297
304
|
setCheckMessage('')
|
|
305
|
+
setCheckDiagnostics([])
|
|
298
306
|
}}
|
|
299
307
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[10px] border text-[12px] font-500 cursor-pointer transition-all duration-200 bg-transparent
|
|
300
308
|
border-white/[0.08] text-text-2 hover:bg-white/[0.04]"
|
|
@@ -440,6 +448,7 @@ export function StepConnect({
|
|
|
440
448
|
setApiKey('')
|
|
441
449
|
setCheckState('idle')
|
|
442
450
|
setCheckMessage('')
|
|
451
|
+
setCheckDiagnostics([])
|
|
443
452
|
}
|
|
444
453
|
}}
|
|
445
454
|
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface
|
|
@@ -466,7 +475,7 @@ export function StepConnect({
|
|
|
466
475
|
<input
|
|
467
476
|
type="password"
|
|
468
477
|
value={apiKey}
|
|
469
|
-
onChange={(e) => { setApiKey(e.target.value); setCredentialId(null); setCheckState('idle'); setCheckMessage(''); setError('') }}
|
|
478
|
+
onChange={(e) => { setApiKey(e.target.value); setCredentialId(null); setCheckState('idle'); setCheckMessage(''); setCheckDiagnostics([]); setError('') }}
|
|
470
479
|
placeholder={selectedProvider.keyPlaceholder || (provider === 'openclaw' ? 'Paste OpenClaw bearer token' : 'sk-...')}
|
|
471
480
|
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface
|
|
472
481
|
text-text text-[14px] font-mono outline-none transition-all duration-200
|
|
@@ -530,6 +539,7 @@ export function StepConnect({
|
|
|
530
539
|
Device paired as <code className="text-text-2">{openclawDeviceId.slice(0, 12)}...</code>.
|
|
531
540
|
</p>
|
|
532
541
|
)}
|
|
542
|
+
<ProviderDiagnosticsList diagnostics={checkDiagnostics} />
|
|
533
543
|
</div>
|
|
534
544
|
)}
|
|
535
545
|
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import type { GatewayProfile, ProviderId } from '@/types'
|
|
1
|
+
import type { GatewayProfile, ProviderCheckResult, ProviderId } from '@/types'
|
|
2
2
|
import type { SetupProvider } from '@/lib/setup-defaults'
|
|
3
3
|
|
|
4
4
|
export type SetupStep = 'profile' | 'path' | 'providers' | 'connect' | 'agents' | 'next' | 'done'
|
|
5
5
|
export type CheckState = 'idle' | 'checking' | 'ok' | 'error'
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
ok: boolean
|
|
9
|
-
message: string
|
|
10
|
-
normalizedEndpoint?: string
|
|
11
|
-
recommendedModel?: string
|
|
12
|
-
errorCode?: string
|
|
13
|
-
deviceId?: string
|
|
14
|
-
}
|
|
7
|
+
export type ProviderCheckResponse = ProviderCheckResult
|
|
15
8
|
|
|
16
9
|
export interface SetupDoctorCheck {
|
|
17
10
|
id: string
|
|
@@ -19,6 +19,9 @@ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain
|
|
|
19
19
|
delegate_to_agent: { label: 'Delegating task', color: '#6366F1', icon: 'delegate' },
|
|
20
20
|
check_delegation_status: { label: 'Checking delegation', color: '#6366F1', icon: 'delegate' },
|
|
21
21
|
web_search: { label: 'Searched the web', color: '#22C55E', icon: 'search' },
|
|
22
|
+
web_fetch: { label: 'Read a web page', color: '#22C55E', icon: 'search' },
|
|
23
|
+
web_extract: { label: 'Extracted a web page', color: '#22C55E', icon: 'search' },
|
|
24
|
+
web_crawl: { label: 'Crawled a site', color: '#22C55E', icon: 'search' },
|
|
22
25
|
connector_message_tool: { label: 'Sent a message', color: '#F97316', icon: 'message' },
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -35,6 +38,7 @@ function extractSnippet(toolName: string, toolInput: string): string | null {
|
|
|
35
38
|
if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
|
|
36
39
|
if (toolName.startsWith('delegate_to_') && parsed.task) return parsed.task
|
|
37
40
|
if (toolName === 'web_search' && parsed.query) return parsed.query
|
|
41
|
+
if ((toolName === 'web_fetch' || toolName === 'web_extract' || toolName === 'web_crawl') && parsed.url) return parsed.url
|
|
38
42
|
if (toolName === 'connector_message_tool' && parsed.to) return parsed.to
|
|
39
43
|
} catch { /* ignore parse errors */ }
|
|
40
44
|
return null
|
|
@@ -20,6 +20,8 @@ const TOOL_COLORS: Record<string, string> = {
|
|
|
20
20
|
create_spreadsheet: '#10B981',
|
|
21
21
|
web_search: '#3B82F6',
|
|
22
22
|
web_fetch: '#3B82F6',
|
|
23
|
+
web_extract: '#3B82F6',
|
|
24
|
+
web_crawl: '#3B82F6',
|
|
23
25
|
spawn_subagent: '#8B5CF6',
|
|
24
26
|
delegate_to_agent: '#6366F1',
|
|
25
27
|
check_delegation_status: '#6366F1',
|
|
@@ -77,6 +79,8 @@ export const TOOL_LABELS: Record<string, string> = {
|
|
|
77
79
|
create_spreadsheet: 'Create Spreadsheet',
|
|
78
80
|
web_search: 'Web Search',
|
|
79
81
|
web_fetch: 'Web Fetch',
|
|
82
|
+
web_extract: 'Web Extract',
|
|
83
|
+
web_crawl: 'Web Crawl',
|
|
80
84
|
claude_code: 'Claude Code',
|
|
81
85
|
codex_cli: 'Codex CLI',
|
|
82
86
|
opencode_cli: 'OpenCode CLI',
|
|
@@ -127,6 +131,8 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
|
127
131
|
create_spreadsheet: 'Create Excel or CSV files from structured data',
|
|
128
132
|
web_search: 'Search the web for information',
|
|
129
133
|
web_fetch: 'Fetch and read web page content',
|
|
134
|
+
web_extract: 'Extract readable content from a source URL',
|
|
135
|
+
web_crawl: 'Crawl a bounded set of pages from one site',
|
|
130
136
|
claude_code: 'Enable delegation to Claude Code CLI',
|
|
131
137
|
codex_cli: 'Enable delegation to OpenAI Codex CLI',
|
|
132
138
|
opencode_cli: 'Enable delegation to OpenCode CLI',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ProviderDiagnosticStep } from '@/types'
|
|
4
|
+
|
|
5
|
+
const STATUS_CLASSES: Record<ProviderDiagnosticStep['status'], string> = {
|
|
6
|
+
pass: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-300',
|
|
7
|
+
warn: 'border-amber-500/25 bg-amber-500/10 text-amber-300',
|
|
8
|
+
fail: 'border-red-500/25 bg-red-500/10 text-red-300',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STATUS_LABELS: Record<ProviderDiagnosticStep['status'], string> = {
|
|
12
|
+
pass: 'Pass',
|
|
13
|
+
warn: 'Warn',
|
|
14
|
+
fail: 'Fail',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ProviderDiagnosticsList({
|
|
18
|
+
diagnostics,
|
|
19
|
+
className = '',
|
|
20
|
+
}: {
|
|
21
|
+
diagnostics?: ProviderDiagnosticStep[] | null
|
|
22
|
+
className?: string
|
|
23
|
+
}) {
|
|
24
|
+
if (!diagnostics?.length) return null
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={`mt-3 border-t border-white/[0.06] pt-3 ${className}`}>
|
|
28
|
+
<div className="mb-2 text-[11px] font-700 uppercase tracking-[0.1em] text-text-3/70">
|
|
29
|
+
Diagnostics
|
|
30
|
+
</div>
|
|
31
|
+
<ol className="space-y-2">
|
|
32
|
+
{diagnostics.map((step) => (
|
|
33
|
+
<li key={step.id} className="grid gap-1 text-left sm:grid-cols-[64px_minmax(0,1fr)] sm:gap-3">
|
|
34
|
+
<div>
|
|
35
|
+
<span className={`inline-flex min-w-[54px] items-center justify-center rounded-[999px] border px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] ${STATUS_CLASSES[step.status]}`}>
|
|
36
|
+
{STATUS_LABELS[step.status]}
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="min-w-0">
|
|
40
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
41
|
+
<span className="text-[12px] font-650 text-text-2">{step.label}</span>
|
|
42
|
+
{typeof step.durationMs === 'number' && (
|
|
43
|
+
<span className="text-[11px] text-text-3">{step.durationMs} ms</span>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
{step.target && (
|
|
47
|
+
<div className="mt-0.5 break-all font-mono text-[11px] text-text-3">{step.target}</div>
|
|
48
|
+
)}
|
|
49
|
+
{step.detail && (
|
|
50
|
+
<div className="mt-0.5 text-[11px] leading-relaxed text-text-3">{step.detail}</div>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ol>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
6
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
7
|
+
import { ProviderDiagnosticsList } from '@/components/providers/provider-diagnostics-list'
|
|
7
8
|
import { toast } from 'sonner'
|
|
8
9
|
import { errorMessage } from '@/lib/shared-utils'
|
|
9
10
|
import {
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
useSaveCustomProviderMutation,
|
|
18
19
|
} from '@/features/providers/queries'
|
|
19
20
|
import { useCreateCredentialMutation, useCredentialsQuery } from '@/features/credentials/queries'
|
|
21
|
+
import type { ProviderDiagnosticStep } from '@/types'
|
|
20
22
|
|
|
21
23
|
export function ProviderSheet() {
|
|
22
24
|
const open = useAppStore((s) => s.providerSheetOpen)
|
|
@@ -54,6 +56,7 @@ export function ProviderSheet() {
|
|
|
54
56
|
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'pass' | 'fail'>('idle')
|
|
55
57
|
const [testMessage, setTestMessage] = useState('')
|
|
56
58
|
const [testModel, setTestModel] = useState('')
|
|
59
|
+
const [testDiagnostics, setTestDiagnostics] = useState<ProviderDiagnosticStep[]>([])
|
|
57
60
|
|
|
58
61
|
const [liveModels, setLiveModels] = useState<string[]>([])
|
|
59
62
|
const [liveLoading, setLiveLoading] = useState(false)
|
|
@@ -77,6 +80,7 @@ export function ProviderSheet() {
|
|
|
77
80
|
setLiveCached(false)
|
|
78
81
|
setTestStatus('idle')
|
|
79
82
|
setTestMessage('')
|
|
83
|
+
setTestDiagnostics([])
|
|
80
84
|
if (editingCustom) {
|
|
81
85
|
setName(editingCustom.name)
|
|
82
86
|
setBaseUrl(editingCustom.baseUrl || '')
|
|
@@ -108,6 +112,7 @@ export function ProviderSheet() {
|
|
|
108
112
|
useEffect(() => {
|
|
109
113
|
setTestStatus('idle')
|
|
110
114
|
setTestMessage('')
|
|
115
|
+
setTestDiagnostics([])
|
|
111
116
|
}, [credentialId, baseUrl])
|
|
112
117
|
|
|
113
118
|
useEffect(() => {
|
|
@@ -121,6 +126,7 @@ export function ProviderSheet() {
|
|
|
121
126
|
if (!isBuiltin) return
|
|
122
127
|
setTestStatus('testing')
|
|
123
128
|
setTestMessage('')
|
|
129
|
+
setTestDiagnostics([])
|
|
124
130
|
try {
|
|
125
131
|
const result = await checkProviderConnectionMutation.mutateAsync({
|
|
126
132
|
provider: editingId || 'custom',
|
|
@@ -128,6 +134,7 @@ export function ProviderSheet() {
|
|
|
128
134
|
endpoint: baseUrl,
|
|
129
135
|
model: testModel || undefined,
|
|
130
136
|
})
|
|
137
|
+
setTestDiagnostics(result.diagnostics ?? [])
|
|
131
138
|
if (result.ok) {
|
|
132
139
|
setTestStatus('pass')
|
|
133
140
|
setTestMessage(result.message)
|
|
@@ -141,6 +148,7 @@ export function ProviderSheet() {
|
|
|
141
148
|
const msg = err instanceof Error ? err.message : 'Connection test failed'
|
|
142
149
|
setTestStatus('fail')
|
|
143
150
|
setTestMessage(msg)
|
|
151
|
+
setTestDiagnostics([])
|
|
144
152
|
toast.error(msg)
|
|
145
153
|
}
|
|
146
154
|
}
|
|
@@ -530,7 +538,7 @@ export function ProviderSheet() {
|
|
|
530
538
|
</label>
|
|
531
539
|
<select
|
|
532
540
|
value={testModel}
|
|
533
|
-
onChange={(e) => { setTestModel(e.target.value); setTestStatus('idle'); setTestMessage('') }}
|
|
541
|
+
onChange={(e) => { setTestModel(e.target.value); setTestStatus('idle'); setTestMessage(''); setTestDiagnostics([]) }}
|
|
534
542
|
className={`${inputClass} appearance-none cursor-pointer`}
|
|
535
543
|
style={{ fontFamily: 'inherit' }}
|
|
536
544
|
>
|
|
@@ -546,11 +554,13 @@ export function ProviderSheet() {
|
|
|
546
554
|
{isBuiltin && testStatus === 'fail' && (
|
|
547
555
|
<div className="mb-4 p-3 rounded-[12px] bg-red-500/[0.08] border border-red-500/20">
|
|
548
556
|
<p className="text-[13px] text-red-400">{testMessage || 'Connection test failed'}</p>
|
|
557
|
+
<ProviderDiagnosticsList diagnostics={testDiagnostics} />
|
|
549
558
|
</div>
|
|
550
559
|
)}
|
|
551
560
|
{isBuiltin && testStatus === 'pass' && (
|
|
552
561
|
<div className="mb-4 p-3 rounded-[12px] bg-emerald-500/[0.08] border border-emerald-500/20">
|
|
553
562
|
<p className="text-[13px] text-emerald-400">{testMessage || 'Connected successfully'}</p>
|
|
563
|
+
<ProviderDiagnosticsList diagnostics={testDiagnostics} />
|
|
554
564
|
</div>
|
|
555
565
|
)}
|
|
556
566
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { fetchProviders } from '@/lib/chat/chats'
|
|
14
14
|
import type {
|
|
15
15
|
ProviderConfig,
|
|
16
|
+
ProviderCheckResult,
|
|
16
17
|
ProviderInfo,
|
|
17
18
|
ProviderModelDiscoveryResult,
|
|
18
19
|
} from '@/types'
|
|
@@ -129,7 +130,7 @@ export function useResetProviderModelsMutation() {
|
|
|
129
130
|
export function useCheckProviderConnectionMutation() {
|
|
130
131
|
return useMutation({
|
|
131
132
|
mutationFn: ({ provider, credentialId, endpoint, model }: CheckProviderConnectionInput) =>
|
|
132
|
-
api<
|
|
133
|
+
api<ProviderCheckResult>('POST', '/setup/check-provider', {
|
|
133
134
|
provider,
|
|
134
135
|
credentialId,
|
|
135
136
|
endpoint,
|
|
@@ -26,7 +26,7 @@ test('routeTaskIntent keeps coding prompts prioritized over memory keywords', ()
|
|
|
26
26
|
test('routeTaskIntent keeps hybrid research-plus-media prompts in research intent', () => {
|
|
27
27
|
const decision = routeTaskIntent(
|
|
28
28
|
'Can you tell me more if there is any news related to the US-Iran war, and can you send me some screenshots and give me a summary and maybe send me a voice note about it?',
|
|
29
|
-
['web_search', 'web_fetch', 'browser', 'manage_connectors'],
|
|
29
|
+
['web_search', 'web_fetch', 'web_crawl', 'browser', 'manage_connectors'],
|
|
30
30
|
null,
|
|
31
31
|
makeClassification({
|
|
32
32
|
taskIntent: 'research',
|
|
@@ -39,7 +39,7 @@ test('routeTaskIntent keeps hybrid research-plus-media prompts in research inten
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
assert.equal(decision.intent, 'research')
|
|
42
|
-
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'browser', 'connector_message_tool'])
|
|
42
|
+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser', 'connector_message_tool'])
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
test('routeTaskIntent treats direct voice-note delivery as outreach', () => {
|
|
@@ -72,7 +72,7 @@ test('routeTaskIntent treats keep-watching update requests as research even with
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
assert.equal(decision.intent, 'research')
|
|
75
|
-
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch'])
|
|
75
|
+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'web_extract', 'web_crawl'])
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
test('routeTaskIntent uses structured classification when available', () => {
|
|
@@ -99,7 +99,7 @@ test('routeTaskIntent uses structured classification when available', () => {
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
assert.equal(decision.intent, 'browsing')
|
|
102
|
-
assert.deepEqual(decision.preferredTools, ['browser', 'web_fetch'])
|
|
102
|
+
assert.deepEqual(decision.preferredTools, ['browser', 'web_fetch', 'web_extract'])
|
|
103
103
|
})
|
|
104
104
|
|
|
105
105
|
function makeClassification(overrides: Partial<MessageClassification>): MessageClassification {
|
|
@@ -144,6 +144,7 @@ export function routeTaskIntent(
|
|
|
144
144
|
[
|
|
145
145
|
TOOL_CAPABILITY.researchSearch,
|
|
146
146
|
TOOL_CAPABILITY.researchFetch,
|
|
147
|
+
TOOL_CAPABILITY.researchCrawl,
|
|
147
148
|
...(wantsScreenshots ? [TOOL_CAPABILITY.browserCapture] : []),
|
|
148
149
|
...(wantsVoiceDelivery ? [TOOL_CAPABILITY.deliveryVoiceNote] : []),
|
|
149
150
|
...(wantsOutboundDelivery ? [TOOL_CAPABILITY.deliveryMedia, TOOL_CAPABILITY.deliveryMessage] : []),
|
|
@@ -407,6 +407,33 @@ describe('translateRequestedToolInvocation advanced', () => {
|
|
|
407
407
|
assert.equal(args.action, 'search')
|
|
408
408
|
assert.equal(args.query, 'test query')
|
|
409
409
|
})
|
|
410
|
+
|
|
411
|
+
it('maps web_extract to web with action=extract', () => {
|
|
412
|
+
const { toolName, args } = translateRequestedToolInvocation(
|
|
413
|
+
'web_extract',
|
|
414
|
+
{ url: 'https://example.com/source' },
|
|
415
|
+
'',
|
|
416
|
+
['web'],
|
|
417
|
+
)
|
|
418
|
+
assert.equal(toolName, 'web')
|
|
419
|
+
assert.equal(args.action, 'extract')
|
|
420
|
+
assert.equal(args.url, 'https://example.com/source')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('maps web_crawl to web with bounded crawl arguments', () => {
|
|
424
|
+
const { toolName, args } = translateRequestedToolInvocation(
|
|
425
|
+
'web_crawl',
|
|
426
|
+
{ url: 'https://example.com/', maxPages: 4, maxDepth: 1, includeExternal: false },
|
|
427
|
+
'',
|
|
428
|
+
['web'],
|
|
429
|
+
)
|
|
430
|
+
assert.equal(toolName, 'web')
|
|
431
|
+
assert.equal(args.action, 'crawl')
|
|
432
|
+
assert.equal(args.url, 'https://example.com/')
|
|
433
|
+
assert.equal(args.maxPages, 4)
|
|
434
|
+
assert.equal(args.maxDepth, 1)
|
|
435
|
+
assert.equal(args.includeExternal, false)
|
|
436
|
+
})
|
|
410
437
|
})
|
|
411
438
|
|
|
412
439
|
// ---------------------------------------------------------------------------
|
|
@@ -127,6 +127,27 @@ export function translateRequestedToolInvocation(
|
|
|
127
127
|
},
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
if (requestedName === 'web_extract') {
|
|
131
|
+
return {
|
|
132
|
+
toolName: 'web',
|
|
133
|
+
args: {
|
|
134
|
+
action: 'extract',
|
|
135
|
+
url: rawArgs.url,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (requestedName === 'web_crawl') {
|
|
140
|
+
return {
|
|
141
|
+
toolName: 'web',
|
|
142
|
+
args: {
|
|
143
|
+
action: 'crawl',
|
|
144
|
+
url: rawArgs.url || rawArgs.query,
|
|
145
|
+
maxPages: rawArgs.maxPages ?? rawArgs.maxResults,
|
|
146
|
+
maxDepth: rawArgs.maxDepth,
|
|
147
|
+
includeExternal: rawArgs.includeExternal,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
130
151
|
if (requestedName === 'delegate_to_claude_code') {
|
|
131
152
|
return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
|
|
132
153
|
}
|
|
@@ -349,7 +349,7 @@ export async function processIterationEvents(opts: ProcessIterationEventsOpts):
|
|
|
349
349
|
}
|
|
350
350
|
if (
|
|
351
351
|
boundedExternalExecutionTask
|
|
352
|
-
&& ['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(toolName)
|
|
352
|
+
&& ['http_request', 'web', 'web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser'].includes(toolName)
|
|
353
353
|
&& countExternalExecutionResearchSteps(state.streamedToolEvents) >= 5
|
|
354
354
|
&& countDistinctExternalResearchHosts(state.streamedToolEvents) >= 3
|
|
355
355
|
) {
|
|
@@ -196,7 +196,7 @@ function getRequestedArtifactStatus(params: {
|
|
|
196
196
|
|
|
197
197
|
export function countExternalExecutionResearchSteps(toolEvents: MessageToolEvent[]): number {
|
|
198
198
|
return toolEvents.filter((event) => {
|
|
199
|
-
return ['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(event.name)
|
|
199
|
+
return ['http_request', 'web', 'web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser'].includes(event.name)
|
|
200
200
|
}).length
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -300,6 +300,8 @@ const RECOVERABLE_TOOL_ERROR_NAMES = new Set([
|
|
|
300
300
|
'web',
|
|
301
301
|
'web_search',
|
|
302
302
|
'web_fetch',
|
|
303
|
+
'web_extract',
|
|
304
|
+
'web_crawl',
|
|
303
305
|
'http_request',
|
|
304
306
|
])
|
|
305
307
|
|
|
@@ -390,6 +392,8 @@ export function getToolFrequencyHint(toolName: string, sessionExtensions: string
|
|
|
390
392
|
case 'http_request':
|
|
391
393
|
case 'web_search':
|
|
392
394
|
case 'web_fetch':
|
|
395
|
+
case 'web_extract':
|
|
396
|
+
case 'web_crawl':
|
|
393
397
|
return 'Hint: You have done extensive research. Stop gathering more sources and use the information you already have to complete the task.'
|
|
394
398
|
|
|
395
399
|
case 'spawn_subagent':
|
|
@@ -490,7 +494,7 @@ function buildDeliverableFollowthroughPrompt(params: {
|
|
|
490
494
|
}
|
|
491
495
|
|
|
492
496
|
if (
|
|
493
|
-
params.toolEvents.some((event) => ['web', 'web_search', 'web_fetch', 'browser', 'http_request'].includes(event.name))
|
|
497
|
+
params.toolEvents.some((event) => ['web', 'web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser', 'http_request'].includes(event.name))
|
|
494
498
|
&& !params.toolEvents.some((event) => ['files', 'write_file', 'edit_file', 'shell', 'execute_command'].includes(event.name))
|
|
495
499
|
) {
|
|
496
500
|
lines.push(
|
|
@@ -135,11 +135,13 @@ describe('expandExtensionIds', () => {
|
|
|
135
135
|
}
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
-
it('web expands to include
|
|
138
|
+
it('web expands to include granular web tools', () => {
|
|
139
139
|
const result = expandExtensionIds(['web'])
|
|
140
140
|
assert.ok(result.includes('web'))
|
|
141
141
|
assert.ok(result.includes('web_search'))
|
|
142
142
|
assert.ok(result.includes('web_fetch'))
|
|
143
|
+
assert.ok(result.includes('web_extract'))
|
|
144
|
+
assert.ok(result.includes('web_crawl'))
|
|
143
145
|
})
|
|
144
146
|
|
|
145
147
|
it('removes duplicates after expansion', () => {
|
|
@@ -199,12 +201,14 @@ describe('expandExtensionIds', () => {
|
|
|
199
201
|
// getExtensionAliases
|
|
200
202
|
// ---------------------------------------------------------------------------
|
|
201
203
|
describe('getExtensionAliases', () => {
|
|
202
|
-
it('web returns
|
|
204
|
+
it('web returns the full web alias group', () => {
|
|
203
205
|
const result = getExtensionAliases('web')
|
|
204
206
|
assert.ok(result.includes('web'))
|
|
205
207
|
assert.ok(result.includes('web_search'))
|
|
206
208
|
assert.ok(result.includes('web_fetch'))
|
|
207
|
-
assert.
|
|
209
|
+
assert.ok(result.includes('web_extract'))
|
|
210
|
+
assert.ok(result.includes('web_crawl'))
|
|
211
|
+
assert.equal(result.length, 7) // web, web_search, web_fetch, web_extract, web_crawl, http_request, http
|
|
208
212
|
})
|
|
209
213
|
|
|
210
214
|
it('web_search returns the same group as web', () => {
|