waypoi 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,442 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import {
3
+ Wrench,
4
+ Server,
5
+ Plug,
6
+ PlugZap,
7
+ RefreshCw,
8
+ ChevronDown,
9
+ ChevronRight,
10
+ Check,
11
+ X,
12
+ AlertCircle,
13
+ Loader2,
14
+ Plus,
15
+ } from 'lucide-react'
16
+ import { Button } from '@/components/ui/button'
17
+ import { Input } from '@/components/ui/input'
18
+ import { cn } from '@/lib/utils'
19
+ import {
20
+ listMcpServers,
21
+ listMcpTools,
22
+ connectMcpServer,
23
+ disconnectMcpServer,
24
+ discoverMcpTools,
25
+ addMcpServer,
26
+ deleteMcpServer,
27
+ BUILTIN_SERVER_ID,
28
+ type McpServer,
29
+ type McpTool,
30
+ } from '@/api/client'
31
+
32
+ interface ToolPickerProps {
33
+ selectedTools: Set<string>
34
+ onToolsChange: (tools: Set<string>) => void
35
+ className?: string
36
+ }
37
+
38
+ interface ServerWithTools extends McpServer {
39
+ tools: McpTool[]
40
+ expanded: boolean
41
+ }
42
+
43
+ export function ToolPicker({ selectedTools, onToolsChange, className }: ToolPickerProps) {
44
+ const [servers, setServers] = useState<ServerWithTools[]>([])
45
+ const [allTools, setAllTools] = useState<McpTool[]>([])
46
+ const [isLoading, setIsLoading] = useState(false)
47
+ const [isConnecting, setIsConnecting] = useState<string | null>(null)
48
+ const [showAddServer, setShowAddServer] = useState(false)
49
+ const [newServerName, setNewServerName] = useState('')
50
+ const [newServerUrl, setNewServerUrl] = useState('')
51
+ const [addError, setAddError] = useState<string | null>(null)
52
+
53
+ // Load servers and tools
54
+ const loadData = useCallback(async () => {
55
+ setIsLoading(true)
56
+ try {
57
+ const [serversRes, toolsRes] = await Promise.all([
58
+ listMcpServers(),
59
+ listMcpTools(),
60
+ ])
61
+
62
+ let tools = toolsRes.data
63
+
64
+ // Auto-connect the built-in server if it's present but not yet connected
65
+ const builtinServer = serversRes.data.find((s) => s.id === BUILTIN_SERVER_ID)
66
+ if (builtinServer && !builtinServer.connected) {
67
+ try {
68
+ await connectMcpServer(BUILTIN_SERVER_ID)
69
+ // Re-fetch tools now that built-in is connected
70
+ const refreshed = await listMcpTools()
71
+ tools = refreshed.data
72
+ } catch {
73
+ // Non-fatal — built-in may not be ready yet
74
+ }
75
+ }
76
+
77
+ setAllTools(tools)
78
+
79
+ // Group tools by server
80
+ const serverMap = new Map<string, McpTool[]>()
81
+ for (const tool of tools) {
82
+ const existing = serverMap.get(tool.serverId) || []
83
+ serverMap.set(tool.serverId, [...existing, tool])
84
+ }
85
+
86
+ setServers(
87
+ serversRes.data.map((s) => ({
88
+ ...s,
89
+ tools: serverMap.get(s.id) || [],
90
+ expanded: true,
91
+ }))
92
+ )
93
+ } catch (error) {
94
+ console.error('Failed to load MCP data:', error)
95
+ } finally {
96
+ setIsLoading(false)
97
+ }
98
+ }, [])
99
+
100
+ useEffect(() => {
101
+ loadData()
102
+ }, [loadData])
103
+
104
+ // Connect to a server
105
+ const handleConnect = async (serverId: string) => {
106
+ setIsConnecting(serverId)
107
+ try {
108
+ await connectMcpServer(serverId)
109
+ await loadData() // Reload to get new tools
110
+ } catch (error) {
111
+ console.error('Failed to connect:', error)
112
+ } finally {
113
+ setIsConnecting(null)
114
+ }
115
+ }
116
+
117
+ // Disconnect from a server
118
+ const handleDisconnect = async (serverId: string) => {
119
+ setIsConnecting(serverId)
120
+ try {
121
+ await disconnectMcpServer(serverId)
122
+ // Remove tools from selection
123
+ const serverTools = allTools.filter((t) => t.serverId === serverId)
124
+ const newSelected = new Set(selectedTools)
125
+ serverTools.forEach((t) => newSelected.delete(t.name))
126
+ onToolsChange(newSelected)
127
+ await loadData()
128
+ } catch (error) {
129
+ console.error('Failed to disconnect:', error)
130
+ } finally {
131
+ setIsConnecting(null)
132
+ }
133
+ }
134
+
135
+ // Discover all tools
136
+ const handleDiscoverAll = async () => {
137
+ setIsLoading(true)
138
+ try {
139
+ await discoverMcpTools()
140
+ await loadData()
141
+ } catch (error) {
142
+ console.error('Failed to discover tools:', error)
143
+ } finally {
144
+ setIsLoading(false)
145
+ }
146
+ }
147
+
148
+ // Add new server
149
+ const handleAddServer = async () => {
150
+ if (!newServerName.trim() || !newServerUrl.trim()) {
151
+ setAddError('Name and URL are required')
152
+ return
153
+ }
154
+
155
+ setAddError(null)
156
+ try {
157
+ await addMcpServer(newServerName.trim(), newServerUrl.trim(), true)
158
+ setNewServerName('')
159
+ setNewServerUrl('')
160
+ setShowAddServer(false)
161
+ await loadData()
162
+ } catch (error) {
163
+ setAddError((error as Error).message || 'Failed to add server')
164
+ }
165
+ }
166
+
167
+ // Delete server
168
+ const handleDeleteServer = async (serverId: string) => {
169
+ try {
170
+ await deleteMcpServer(serverId)
171
+ await loadData()
172
+ } catch (error) {
173
+ console.error('Failed to delete server:', error)
174
+ }
175
+ }
176
+
177
+ // Toggle tool selection
178
+ const toggleTool = (toolName: string) => {
179
+ const newSelected = new Set(selectedTools)
180
+ if (newSelected.has(toolName)) {
181
+ newSelected.delete(toolName)
182
+ } else {
183
+ newSelected.add(toolName)
184
+ }
185
+ onToolsChange(newSelected)
186
+ }
187
+
188
+ // Toggle all tools from a server
189
+ const toggleServerTools = (server: ServerWithTools) => {
190
+ const serverToolNames = server.tools.map((t) => t.name)
191
+ const allSelected = serverToolNames.every((name) => selectedTools.has(name))
192
+
193
+ const newSelected = new Set(selectedTools)
194
+ if (allSelected) {
195
+ serverToolNames.forEach((name) => newSelected.delete(name))
196
+ } else {
197
+ serverToolNames.forEach((name) => newSelected.add(name))
198
+ }
199
+ onToolsChange(newSelected)
200
+ }
201
+
202
+ // Toggle server expanded state
203
+ const toggleServerExpanded = (serverId: string) => {
204
+ setServers((prev) =>
205
+ prev.map((s) =>
206
+ s.id === serverId ? { ...s, expanded: !s.expanded } : s
207
+ )
208
+ )
209
+ }
210
+
211
+ return (
212
+ <div className={cn('flex flex-col', className)}>
213
+ {/* Header */}
214
+ <div className="flex items-center justify-between px-3 py-2 border-b border-border">
215
+ <div className="flex items-center gap-2">
216
+ <Wrench className="w-4 h-4 text-primary" />
217
+ <span className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
218
+ Tools
219
+ </span>
220
+ {selectedTools.size > 0 && (
221
+ <span className="px-1.5 py-0.5 bg-primary/20 text-primary text-2xs rounded font-mono">
222
+ {selectedTools.size}
223
+ </span>
224
+ )}
225
+ </div>
226
+ <div className="flex items-center gap-1">
227
+ <Button
228
+ variant="ghost"
229
+ size="icon"
230
+ className="h-6 w-6"
231
+ onClick={() => setShowAddServer(!showAddServer)}
232
+ title="Add server"
233
+ >
234
+ <Plus className="w-3.5 h-3.5" />
235
+ </Button>
236
+ <Button
237
+ variant="ghost"
238
+ size="icon"
239
+ className="h-6 w-6"
240
+ onClick={handleDiscoverAll}
241
+ disabled={isLoading}
242
+ title="Discover all tools"
243
+ >
244
+ <RefreshCw className={cn('w-3.5 h-3.5', isLoading && 'animate-spin')} />
245
+ </Button>
246
+ </div>
247
+ </div>
248
+
249
+ {/* Add Server Form */}
250
+ {showAddServer && (
251
+ <div className="p-3 border-b border-border bg-secondary/30 space-y-2 animate-slide-in-bottom">
252
+ <Input
253
+ placeholder="Server name"
254
+ value={newServerName}
255
+ onChange={(e) => setNewServerName(e.target.value)}
256
+ className="h-8 text-sm"
257
+ />
258
+ <Input
259
+ placeholder="http://localhost:3000/mcp"
260
+ value={newServerUrl}
261
+ onChange={(e) => setNewServerUrl(e.target.value)}
262
+ className="h-8 text-sm font-mono"
263
+ />
264
+ {addError && (
265
+ <p className="text-destructive text-2xs flex items-center gap-1">
266
+ <AlertCircle className="w-3 h-3" />
267
+ {addError}
268
+ </p>
269
+ )}
270
+ <div className="flex gap-2">
271
+ <Button size="sm" className="flex-1 h-7 text-xs" onClick={handleAddServer}>
272
+ Add
273
+ </Button>
274
+ <Button
275
+ variant="outline"
276
+ size="sm"
277
+ className="h-7 text-xs"
278
+ onClick={() => {
279
+ setShowAddServer(false)
280
+ setAddError(null)
281
+ }}
282
+ >
283
+ Cancel
284
+ </Button>
285
+ </div>
286
+ </div>
287
+ )}
288
+
289
+ {/* Server and Tools List */}
290
+ <div className="flex-1 overflow-y-auto">
291
+ {servers.length === 0 && !isLoading && (
292
+ <div className="p-4 text-center">
293
+ <Server className="w-8 h-8 text-muted-foreground/50 mx-auto mb-2" />
294
+ <p className="text-sm text-muted-foreground">No MCP servers</p>
295
+ <p className="text-2xs text-muted-foreground/70 mt-1">
296
+ Add a server to discover tools
297
+ </p>
298
+ </div>
299
+ )}
300
+
301
+ {servers.map((server) => (
302
+ <div key={server.id} className="border-b border-border/50 last:border-0">
303
+ {/* Server Header */}
304
+ <div
305
+ className={cn(
306
+ 'flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-secondary/50 transition-colors',
307
+ server.connected && 'bg-emerald-500/5'
308
+ )}
309
+ onClick={() => toggleServerExpanded(server.id)}
310
+ >
311
+ {server.expanded ? (
312
+ <ChevronDown className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
313
+ ) : (
314
+ <ChevronRight className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
315
+ )}
316
+
317
+ <div
318
+ className={cn(
319
+ 'w-2 h-2 rounded-full shrink-0',
320
+ server.connected
321
+ ? 'bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.5)]'
322
+ : server.status === 'error'
323
+ ? 'bg-red-500'
324
+ : 'bg-muted-foreground/30'
325
+ )}
326
+ />
327
+
328
+ <span className="text-sm font-medium truncate flex-1">{server.name}</span>
329
+
330
+ {server.id === BUILTIN_SERVER_ID && (
331
+ <span className="text-2xs font-mono text-muted-foreground/60 border border-border/50 rounded px-1 py-0.5 shrink-0">
332
+ built-in
333
+ </span>
334
+ )}
335
+
336
+ {server.tools.length > 0 && (
337
+ <button
338
+ onClick={(e) => {
339
+ e.stopPropagation()
340
+ toggleServerTools(server)
341
+ }}
342
+ className={cn(
343
+ 'w-5 h-5 rounded flex items-center justify-center transition-colors',
344
+ server.tools.every((t) => selectedTools.has(t.name))
345
+ ? 'bg-primary text-primary-foreground'
346
+ : server.tools.some((t) => selectedTools.has(t.name))
347
+ ? 'bg-primary/50 text-primary-foreground'
348
+ : 'bg-secondary hover:bg-secondary/80'
349
+ )}
350
+ >
351
+ <Check className="w-3 h-3" />
352
+ </button>
353
+ )}
354
+
355
+ {isConnecting === server.id ? (
356
+ <Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
357
+ ) : server.id === BUILTIN_SERVER_ID ? null : server.connected ? (
358
+ <button
359
+ onClick={(e) => {
360
+ e.stopPropagation()
361
+ handleDisconnect(server.id)
362
+ }}
363
+ className="p-1 hover:bg-destructive/20 rounded transition-colors"
364
+ title="Disconnect"
365
+ >
366
+ <PlugZap className="w-3.5 h-3.5 text-emerald-500" />
367
+ </button>
368
+ ) : (
369
+ <button
370
+ onClick={(e) => {
371
+ e.stopPropagation()
372
+ handleConnect(server.id)
373
+ }}
374
+ className="p-1 hover:bg-primary/20 rounded transition-colors"
375
+ title="Connect"
376
+ >
377
+ <Plug className="w-3.5 h-3.5 text-muted-foreground" />
378
+ </button>
379
+ )}
380
+
381
+ {server.id !== BUILTIN_SERVER_ID && (
382
+ <button
383
+ onClick={(e) => {
384
+ e.stopPropagation()
385
+ handleDeleteServer(server.id)
386
+ }}
387
+ className="p-1 hover:bg-destructive/20 rounded transition-colors opacity-50 hover:opacity-100"
388
+ title="Delete server"
389
+ >
390
+ <X className="w-3 h-3 text-destructive" />
391
+ </button>
392
+ )}
393
+ </div>
394
+
395
+ {/* Tools List */}
396
+ {server.expanded && (
397
+ <div className="pb-1">
398
+ {server.tools.length === 0 ? (
399
+ <div className="px-8 py-2 text-2xs text-muted-foreground/70">
400
+ {server.connected ? 'No tools available' : 'Connect to discover tools'}
401
+ </div>
402
+ ) : (
403
+ server.tools.map((tool) => (
404
+ <button
405
+ key={tool.name}
406
+ onClick={() => toggleTool(tool.name)}
407
+ className={cn(
408
+ 'w-full flex items-start gap-2 px-8 py-1.5 text-left hover:bg-secondary/50 transition-colors',
409
+ selectedTools.has(tool.name) && 'bg-primary/5'
410
+ )}
411
+ >
412
+ <div
413
+ className={cn(
414
+ 'w-4 h-4 rounded border flex items-center justify-center shrink-0 mt-0.5 transition-colors',
415
+ selectedTools.has(tool.name)
416
+ ? 'bg-primary border-primary'
417
+ : 'border-border'
418
+ )}
419
+ >
420
+ {selectedTools.has(tool.name) && (
421
+ <Check className="w-2.5 h-2.5 text-primary-foreground" />
422
+ )}
423
+ </div>
424
+ <div className="min-w-0">
425
+ <p className="text-sm font-mono truncate">{tool.name}</p>
426
+ {tool.description && (
427
+ <p className="text-2xs text-muted-foreground/70 line-clamp-2">
428
+ {tool.description}
429
+ </p>
430
+ )}
431
+ </div>
432
+ </button>
433
+ ))
434
+ )}
435
+ </div>
436
+ )}
437
+ </div>
438
+ ))}
439
+ </div>
440
+ </div>
441
+ )
442
+ }
@@ -0,0 +1,41 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { parseMessageContent } from './messageContentParser'
4
+
5
+ test('parses standard think block', () => {
6
+ const parts = parseMessageContent('<think>analyze</think>\n\nanswer')
7
+ assert.deepEqual(parts, [
8
+ { type: 'thinking', content: 'analyze' },
9
+ { type: 'text', content: 'answer' },
10
+ ])
11
+ })
12
+
13
+ test('parses missing opening tag before closing think tag', () => {
14
+ const parts = parseMessageContent('analysis text</think>\n\nanswer')
15
+ assert.deepEqual(parts, [
16
+ { type: 'thinking', content: 'analysis text' },
17
+ { type: 'text', content: 'answer' },
18
+ ])
19
+ })
20
+
21
+ test('parses unclosed think tag during streaming', () => {
22
+ const parts = parseMessageContent('<think>still thinking')
23
+ assert.deepEqual(parts, [
24
+ { type: 'thinking', content: 'still thinking' },
25
+ ])
26
+ })
27
+
28
+ test('parses whitespace-tolerant think tags', () => {
29
+ const parts = parseMessageContent('<think > spaced </ think >\n\nok')
30
+ assert.deepEqual(parts, [
31
+ { type: 'thinking', content: 'spaced' },
32
+ { type: 'text', content: 'ok' },
33
+ ])
34
+ })
35
+
36
+ test('plain text remains text segment', () => {
37
+ const parts = parseMessageContent('just answer')
38
+ assert.deepEqual(parts, [
39
+ { type: 'text', content: 'just answer' },
40
+ ])
41
+ })
@@ -0,0 +1,73 @@
1
+ export interface ParsedMessagePart {
2
+ type: 'text' | 'thinking'
3
+ content: string
4
+ }
5
+
6
+ // Parse content to extract thinking blocks.
7
+ // Handles:
8
+ // 1) <think>...</think>
9
+ // 2) content...</think> (missing opening tag)
10
+ // 3) <think>... (unclosed tag during streaming)
11
+ // 4) tags with extra whitespace, e.g. <think >, </ think >
12
+ export function parseMessageContent(content: string): ParsedMessagePart[] {
13
+ const parts: ParsedMessagePart[] = []
14
+ const openTag = /<\s*think\s*>/i
15
+ const closeTag = /<\s*\/\s*think\s*>/i
16
+
17
+ const startsWithOpeningTag = openTag.test(content.trimStart())
18
+ const hasClosingWithoutOpening = !startsWithOpeningTag && closeTag.test(content)
19
+
20
+ let processedContent = content
21
+ if (hasClosingWithoutOpening) {
22
+ const closeMatch = closeTag.exec(content)
23
+ if (closeMatch) {
24
+ const closeIndex = closeMatch.index
25
+ const thinkingContent = content.slice(0, closeIndex).trim()
26
+ if (thinkingContent) {
27
+ parts.push({ type: 'thinking', content: thinkingContent })
28
+ }
29
+ processedContent = content.slice(closeIndex + closeMatch[0].length)
30
+ }
31
+ }
32
+
33
+ const closedThinkRegex = /<\s*think\s*>([\s\S]*?)<\s*\/\s*think\s*>/gi
34
+ let lastIndex = 0
35
+ let match: RegExpExecArray | null
36
+
37
+ while ((match = closedThinkRegex.exec(processedContent)) !== null) {
38
+ if (match.index > lastIndex) {
39
+ const text = processedContent.slice(lastIndex, match.index).trim()
40
+ if (text) {
41
+ parts.push({ type: 'text', content: text })
42
+ }
43
+ }
44
+ parts.push({ type: 'thinking', content: (match[1] ?? '').trim() })
45
+ lastIndex = match.index + match[0].length
46
+ }
47
+
48
+ if (lastIndex < processedContent.length) {
49
+ const tail = processedContent.slice(lastIndex)
50
+ const unclosedMatch = tail.match(/([\s\S]*?)<\s*think\s*>([\s\S]*)$/i)
51
+ if (unclosedMatch) {
52
+ const textBefore = (unclosedMatch[1] ?? '').trim()
53
+ const thinkingTail = (unclosedMatch[2] ?? '').trim()
54
+ if (textBefore) {
55
+ parts.push({ type: 'text', content: textBefore })
56
+ }
57
+ if (thinkingTail) {
58
+ parts.push({ type: 'thinking', content: thinkingTail })
59
+ }
60
+ } else {
61
+ const text = tail.trim()
62
+ if (text) {
63
+ parts.push({ type: 'text', content: text })
64
+ }
65
+ }
66
+ }
67
+
68
+ if (parts.length === 0 && content.trim()) {
69
+ parts.push({ type: 'text', content: content.trim() })
70
+ }
71
+
72
+ return parts
73
+ }
@@ -0,0 +1,27 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { getLatestThinkingLines, hasUnclosedThinkingTag } from './thinkingPreview'
4
+
5
+ test('latest lines append chars within current trailing line', () => {
6
+ const before = getLatestThinkingLines('one\ntwo\npar')
7
+ const after = getLatestThinkingLines('one\ntwo\npart')
8
+
9
+ assert.deepEqual(before, ['one', 'two', 'par'])
10
+ assert.deepEqual(after, ['one', 'two', 'part'])
11
+ })
12
+
13
+ test('latest lines create a new line once newline arrives', () => {
14
+ const lines = getLatestThinkingLines('one\ntwo\npart\nnext')
15
+ assert.deepEqual(lines, ['one', 'two', 'part', 'next'])
16
+ })
17
+
18
+ test('latest lines drop oldest full line when window exceeds five lines', () => {
19
+ const lines = getLatestThinkingLines('1\n2\n3\n4\n5\n6')
20
+ assert.deepEqual(lines, ['2', '3', '4', '5', '6'])
21
+ })
22
+
23
+ test('unclosed think tags are considered live, closed are not', () => {
24
+ assert.equal(hasUnclosedThinkingTag('<think>still thinking'), true)
25
+ assert.equal(hasUnclosedThinkingTag('<think>done</think>final'), false)
26
+ })
27
+
@@ -0,0 +1,15 @@
1
+ export const LIVE_PREVIEW_MAX_LINES = 5
2
+
3
+ export function hasUnclosedThinkingTag(content: string): boolean {
4
+ const openTagMatches = content.match(/<\s*think\s*>/gi)?.length ?? 0
5
+ const closeTagMatches = content.match(/<\s*\/\s*think\s*>/gi)?.length ?? 0
6
+ return openTagMatches > closeTagMatches
7
+ }
8
+
9
+ export function getLatestThinkingLines(content: string, maxLines = LIVE_PREVIEW_MAX_LINES): string[] {
10
+ if (!content) return []
11
+ const normalized = content.replace(/\r\n?/g, '\n')
12
+ const lines = normalized.split('\n')
13
+ return lines.slice(-maxLines)
14
+ }
15
+
@@ -0,0 +1,78 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { toMermaidSankey } from './toMermaidSankey'
4
+
5
+ test('toMermaidSankey emits valid sankey-beta csv rows', () => {
6
+ const code = toMermaidSankey({
7
+ eligible: true,
8
+ method: 'exact_totals_estimated_categories',
9
+ totals: {
10
+ inputTokens: 1000,
11
+ outputTokens: 400,
12
+ totalTokens: 1400,
13
+ },
14
+ input: [
15
+ { key: 'system', label: 'System Prompt', tokens: 300 },
16
+ { key: 'user', label: 'User Prompt', tokens: 700 },
17
+ ],
18
+ output: [
19
+ { key: 'completion', label: 'Final Answer', tokens: 400 },
20
+ ],
21
+ notes: [],
22
+ })
23
+
24
+ const lines = code.split('\n')
25
+ assert.equal(lines[0], 'sankey-beta')
26
+ assert.ok(lines.includes('Context Window,Input Tokens,1000'))
27
+ assert.ok(lines.includes('Context Window,Output Tokens,400'))
28
+ assert.ok(lines.includes('Input Tokens,System Prompt [input:system],300'))
29
+ assert.ok(lines.includes('Output Tokens,Final Answer [output:completion],400'))
30
+ assert.ok(!code.includes('-->'))
31
+ assert.ok(!code.includes('[') || code.includes('[input:') || code.includes('[output:'))
32
+ })
33
+
34
+ test('toMermaidSankey compacts known category labels while retaining keys', () => {
35
+ const code = toMermaidSankey({
36
+ eligible: true,
37
+ method: 'exact_totals_estimated_categories',
38
+ totals: {
39
+ inputTokens: 100,
40
+ outputTokens: 50,
41
+ totalTokens: 150,
42
+ },
43
+ input: [
44
+ { key: 'assistant_history', label: 'Assistant History', tokens: 60 },
45
+ { key: 'input_media', label: 'Input Media', tokens: 40 },
46
+ ],
47
+ output: [
48
+ { key: 'assistant_text', label: 'Assistant Text', tokens: 50 },
49
+ ],
50
+ notes: [],
51
+ })
52
+
53
+ assert.ok(code.includes('Input Tokens,Asst Hist [input:assistant_history],60'))
54
+ assert.ok(code.includes('Input Tokens,Media In [input:input_media],40'))
55
+ assert.ok(code.includes('Output Tokens,Asst Text [output:assistant_text],50'))
56
+ })
57
+
58
+ test('toMermaidSankey escapes csv cells for commas and quotes', () => {
59
+ const code = toMermaidSankey({
60
+ eligible: true,
61
+ method: 'estimated_only',
62
+ totals: {
63
+ inputTokens: 10,
64
+ outputTokens: 5,
65
+ totalTokens: 15,
66
+ },
67
+ input: [
68
+ { key: 'quoted', label: 'System "Rules", Prompt', tokens: 10 },
69
+ ],
70
+ output: [
71
+ { key: 'final', label: 'Final, "Answer"', tokens: 5 },
72
+ ],
73
+ notes: [],
74
+ })
75
+
76
+ assert.match(code, /"System ""Rules"", Prompt \[input:quoted\]"/)
77
+ assert.match(code, /"Final, ""Answer"" \[output:final\]"/)
78
+ })