@bytespell/shella 0.2.1 → 0.2.3

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 (85) hide show
  1. package/dist/bin/shella-init.js +28 -93
  2. package/dist/bin/shella-init.js.map +1 -1
  3. package/dist/src/api.d.ts.map +1 -1
  4. package/dist/src/api.js +36 -2
  5. package/dist/src/api.js.map +1 -1
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/index.js +60 -5
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/logger.d.ts.map +1 -1
  10. package/dist/src/logger.js +46 -1
  11. package/dist/src/logger.js.map +1 -1
  12. package/dist/src/plugin-manager.d.ts +22 -1
  13. package/dist/src/plugin-manager.d.ts.map +1 -1
  14. package/dist/src/plugin-manager.js +93 -34
  15. package/dist/src/plugin-manager.js.map +1 -1
  16. package/dist/src/port-allocator.js +1 -1
  17. package/dist/src/port-allocator.js.map +1 -1
  18. package/dist/src/registry.d.ts +15 -1
  19. package/dist/src/registry.d.ts.map +1 -1
  20. package/dist/src/registry.js +29 -0
  21. package/dist/src/registry.js.map +1 -1
  22. package/dist/src/types.d.ts +20 -2
  23. package/dist/src/types.d.ts.map +1 -1
  24. package/dist/src/types.js.map +1 -1
  25. package/dist/src/watcher.d.ts +4 -3
  26. package/dist/src/watcher.d.ts.map +1 -1
  27. package/dist/src/watcher.js +91 -74
  28. package/dist/src/watcher.js.map +1 -1
  29. package/package.json +6 -2
  30. package/bundled-plugins/agent/README.md +0 -3
  31. package/bundled-plugins/agent/components.json +0 -24
  32. package/bundled-plugins/agent/dist/assets/index-BGeDYr6P.css +0 -1
  33. package/bundled-plugins/agent/dist/assets/index-J14lI3Er.js +0 -49
  34. package/bundled-plugins/agent/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  35. package/bundled-plugins/agent/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  36. package/bundled-plugins/agent/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  37. package/bundled-plugins/agent/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  38. package/bundled-plugins/agent/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  39. package/bundled-plugins/agent/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  40. package/bundled-plugins/agent/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  41. package/bundled-plugins/agent/dist/index.html +0 -14
  42. package/bundled-plugins/agent/dist/vite.svg +0 -1
  43. package/bundled-plugins/agent/eslint.config.js +0 -23
  44. package/bundled-plugins/agent/index.html +0 -13
  45. package/bundled-plugins/agent/package-lock.json +0 -11034
  46. package/bundled-plugins/agent/package.json +0 -52
  47. package/bundled-plugins/agent/public/vite.svg +0 -1
  48. package/bundled-plugins/agent/server.js +0 -240
  49. package/bundled-plugins/agent/src/App.tsx +0 -586
  50. package/bundled-plugins/agent/src/assets/react.svg +0 -1
  51. package/bundled-plugins/agent/src/components/ui/alert-dialog.tsx +0 -182
  52. package/bundled-plugins/agent/src/components/ui/badge.tsx +0 -45
  53. package/bundled-plugins/agent/src/components/ui/button.tsx +0 -60
  54. package/bundled-plugins/agent/src/components/ui/card.tsx +0 -94
  55. package/bundled-plugins/agent/src/components/ui/combobox.tsx +0 -294
  56. package/bundled-plugins/agent/src/components/ui/dropdown-menu.tsx +0 -253
  57. package/bundled-plugins/agent/src/components/ui/field.tsx +0 -225
  58. package/bundled-plugins/agent/src/components/ui/input-group.tsx +0 -147
  59. package/bundled-plugins/agent/src/components/ui/input.tsx +0 -19
  60. package/bundled-plugins/agent/src/components/ui/label.tsx +0 -24
  61. package/bundled-plugins/agent/src/components/ui/select.tsx +0 -185
  62. package/bundled-plugins/agent/src/components/ui/separator.tsx +0 -26
  63. package/bundled-plugins/agent/src/components/ui/switch.tsx +0 -31
  64. package/bundled-plugins/agent/src/components/ui/textarea.tsx +0 -18
  65. package/bundled-plugins/agent/src/index.css +0 -131
  66. package/bundled-plugins/agent/src/lib/utils.ts +0 -6
  67. package/bundled-plugins/agent/src/main.tsx +0 -11
  68. package/bundled-plugins/agent/tsconfig.app.json +0 -32
  69. package/bundled-plugins/agent/tsconfig.json +0 -13
  70. package/bundled-plugins/agent/tsconfig.node.json +0 -26
  71. package/bundled-plugins/agent/vite.config.ts +0 -14
  72. package/bundled-plugins/terminal/index.html +0 -24
  73. package/bundled-plugins/terminal/package-lock.json +0 -3316
  74. package/bundled-plugins/terminal/package.json +0 -37
  75. package/bundled-plugins/terminal/server.ts +0 -169
  76. package/bundled-plugins/terminal/src/App.tsx +0 -168
  77. package/bundled-plugins/terminal/src/main.tsx +0 -9
  78. package/bundled-plugins/terminal/tsconfig.json +0 -22
  79. package/bundled-plugins/terminal/vite.config.ts +0 -10
  80. package/templates/express/package.json +0 -11
  81. package/templates/express/public/index.html +0 -64
  82. package/templates/express/server.js +0 -37
  83. package/templates/vanilla/package.json +0 -8
  84. package/templates/vanilla/public/index.html +0 -64
  85. package/templates/vanilla/server.js +0 -54
@@ -1,586 +0,0 @@
1
- import { useState, useEffect, useRef, useCallback } from 'react'
2
- import { Button } from "@/components/ui/button"
3
- import { Textarea } from "@/components/ui/textarea"
4
- import { Badge } from "@/components/ui/badge"
5
- import { Card, CardContent } from "@/components/ui/card"
6
- import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"
7
- import { Switch } from "@/components/ui/switch"
8
- import { Streamdown } from 'streamdown'
9
- import { code } from '@streamdown/code'
10
- import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom'
11
-
12
- interface Message {
13
- role: 'user' | 'assistant' | 'tool_result'
14
- content: ContentBlock[]
15
- }
16
-
17
- interface ContentBlock {
18
- type: 'text' | 'thinking' | 'tool_use' | 'tool_result'
19
- text?: string
20
- thinking?: string
21
- name?: string
22
- input?: Record<string, unknown>
23
- }
24
-
25
- interface ToolExecution {
26
- toolCallId: string
27
- toolName: string
28
- args: Record<string, unknown>
29
- status: 'running' | 'done' | 'error'
30
- partialResult?: string
31
- isError?: boolean
32
- }
33
-
34
- interface SessionState {
35
- model?: { provider: string; id: string; name?: string; reasoning?: boolean }
36
- thinkingLevel?: string
37
- isStreaming: boolean
38
- isCompacting?: boolean
39
- autoCompactionEnabled?: boolean
40
- }
41
-
42
- interface SessionStats {
43
- tokens?: {
44
- input: number
45
- output: number
46
- cacheRead?: number
47
- cacheWrite?: number
48
- total: number
49
- }
50
- cost?: number
51
- }
52
-
53
- interface AvailableModel {
54
- provider: string
55
- id: string
56
- name?: string
57
- reasoning?: boolean
58
- }
59
-
60
- const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'] as const
61
-
62
- type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error'
63
-
64
- function ScrollToBottom() {
65
- const { isAtBottom, scrollToBottom } = useStickToBottomContext()
66
-
67
- if (isAtBottom) return null
68
-
69
- return (
70
- <button
71
- className="absolute left-1/2 -translate-x-1/2 bottom-4 bg-muted hover:bg-muted/80 text-muted-foreground px-3 py-1 rounded-full text-xs shadow-lg"
72
- onClick={() => scrollToBottom()}
73
- >
74
- Scroll to bottom
75
- </button>
76
- )
77
- }
78
-
79
- export function App() {
80
- const [status, setStatus] = useState<ConnectionStatus>('connecting')
81
- const [authStatus, setAuthStatus] = useState<{ authenticated: boolean; providers: string[] } | null>(null)
82
- const [sessionState, setSessionState] = useState<SessionState | null>(null)
83
- const [messages, setMessages] = useState<Message[]>([])
84
- const [streamingMessage, setStreamingMessage] = useState<Message | null>(null)
85
- const [toolExecutions, setToolExecutions] = useState<Map<string, ToolExecution>>(new Map())
86
- const [input, setInput] = useState('')
87
- const [error, setError] = useState<string | null>(null)
88
- const [availableModels, setAvailableModels] = useState<AvailableModel[]>([])
89
- const [sessionStats, setSessionStats] = useState<SessionStats | null>(null)
90
-
91
- const wsRef = useRef<WebSocket | null>(null)
92
-
93
- useEffect(() => {
94
- fetch('/api/auth/status')
95
- .then(r => r.json())
96
- .then(setAuthStatus)
97
- .catch(() => setAuthStatus({ authenticated: false, providers: [] }))
98
- }, [])
99
-
100
- useEffect(() => {
101
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
102
- const ws = new WebSocket(`${protocol}//${window.location.host}/ws`)
103
- wsRef.current = ws
104
-
105
- ws.onopen = () => {
106
- setStatus('connected')
107
- setError(null)
108
- ws.send(JSON.stringify({ type: 'start' }))
109
- }
110
-
111
- ws.onclose = () => setStatus('disconnected')
112
- ws.onerror = () => {
113
- setStatus('error')
114
- setError('WebSocket connection failed')
115
- }
116
-
117
- ws.onmessage = (event) => {
118
- try {
119
- const data = JSON.parse(event.data)
120
- handleMessage(data)
121
- } catch (err) {
122
- console.error('Parse error:', err)
123
- }
124
- }
125
-
126
- return () => ws.close()
127
- }, [])
128
-
129
- const handleMessage = useCallback((data: { type: string; event?: Record<string, unknown>; message?: string }) => {
130
- if (data.type === 'ready') {
131
- setStatus('connected')
132
- } else if (data.type === 'error') {
133
- setError(data.message || 'Unknown error')
134
- } else if (data.type === 'rpc_event') {
135
- handleRpcEvent(data.event!)
136
- }
137
- }, [])
138
-
139
- const handleRpcEvent = useCallback((event: Record<string, unknown>) => {
140
- const eventType = event.type as string
141
-
142
- if (eventType === 'response') {
143
- const command = event.command as string
144
- if (command === 'get_state' && event.success) {
145
- setSessionState(event.data as SessionState)
146
- } else if (command === 'get_messages' && event.success) {
147
- const data = event.data as { messages: Message[] }
148
- console.log('[App] Got messages:', JSON.stringify(data.messages, null, 2))
149
- setMessages(data.messages || [])
150
- } else if (command === 'get_available_models' && event.success) {
151
- const data = event.data as { models: AvailableModel[] }
152
- setAvailableModels(data.models || [])
153
- } else if (command === 'set_model' && event.success) {
154
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_state' } }))
155
- } else if (command === 'get_session_stats' && event.success) {
156
- setSessionStats(event.data as SessionStats)
157
- } else if (command === 'set_thinking_level' && event.success) {
158
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_state' } }))
159
- } else if (command === 'set_auto_compaction' && event.success) {
160
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_state' } }))
161
- } else if (command === 'compact' && event.success) {
162
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_state' } }))
163
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_session_stats' } }))
164
- } else if (command === 'new_session' && event.success) {
165
- setMessages([])
166
- setSessionStats(null)
167
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_state' } }))
168
- }
169
- } else if (eventType === 'agent_start') {
170
- setStreamingMessage(null)
171
- setToolExecutions(new Map())
172
- } else if (eventType === 'agent_end') {
173
- setStreamingMessage(null)
174
- setToolExecutions(new Map()) // Clear tool executions to avoid duplication
175
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_messages' } }))
176
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_state' } }))
177
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_session_stats' } }))
178
- } else if (eventType === 'message_start' || eventType === 'message_update') {
179
- const msg = event.message as Message
180
- if (msg?.role === 'assistant') {
181
- setStreamingMessage(msg)
182
- }
183
- } else if (eventType === 'message_end') {
184
- setStreamingMessage(null)
185
- } else if (eventType === 'tool_execution_start') {
186
- const toolCallId = event.toolCallId as string
187
- setToolExecutions(prev => {
188
- const next = new Map(prev)
189
- next.set(toolCallId, {
190
- toolCallId,
191
- toolName: event.toolName as string,
192
- args: event.args as Record<string, unknown>,
193
- status: 'running',
194
- })
195
- return next
196
- })
197
- } else if (eventType === 'tool_execution_update') {
198
- const toolCallId = event.toolCallId as string
199
- const partialResult = event.partialResult as { content: ContentBlock[] }
200
- setToolExecutions(prev => {
201
- const next = new Map(prev)
202
- const existing = next.get(toolCallId)
203
- if (existing) {
204
- const text = partialResult?.content?.find(c => c.type === 'text')?.text
205
- next.set(toolCallId, { ...existing, partialResult: text })
206
- }
207
- return next
208
- })
209
- } else if (eventType === 'tool_execution_end') {
210
- const toolCallId = event.toolCallId as string
211
- setToolExecutions(prev => {
212
- const next = new Map(prev)
213
- const existing = next.get(toolCallId)
214
- if (existing) {
215
- const result = event.result as { content: ContentBlock[] }
216
- next.set(toolCallId, {
217
- ...existing,
218
- status: event.isError ? 'error' : 'done',
219
- partialResult: result?.content?.find(c => c.type === 'text')?.text,
220
- isError: event.isError as boolean,
221
- })
222
- }
223
- return next
224
- })
225
- }
226
- }, [])
227
-
228
- const sendMessage = useCallback(() => {
229
- if (!input.trim() || !wsRef.current || sessionState?.isStreaming) return
230
- const message = input.trim()
231
- setInput('')
232
- setMessages(prev => [...prev, { role: 'user', content: [{ type: 'text', text: message }] }])
233
- wsRef.current.send(JSON.stringify({ type: 'prompt', message }))
234
- }, [input, sessionState?.isStreaming])
235
-
236
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
237
- if (e.key === 'Enter' && !e.shiftKey) {
238
- e.preventDefault()
239
- sendMessage()
240
- }
241
- }, [sendMessage])
242
-
243
- const handleAbort = useCallback(() => {
244
- wsRef.current?.send(JSON.stringify({ type: 'abort' }))
245
- }, [])
246
-
247
- const refreshAuth = useCallback(async () => {
248
- const res = await fetch('/api/auth/status')
249
- setAuthStatus(await res.json())
250
- }, [])
251
-
252
- // Check if model supports reasoning - must be explicitly true
253
- const supportsReasoning = sessionState?.model?.reasoning === true
254
-
255
- // Get text content from a message, handling different content block structures
256
- const getTextContent = (msg: Message): string => {
257
- for (const block of msg.content) {
258
- // Handle standard text blocks
259
- if (block.type === 'text' && block.text) {
260
- return block.text
261
- }
262
- // Handle blocks that might just have text property without type
263
- if ('text' in block && typeof (block as { text?: string }).text === 'string') {
264
- return (block as { text: string }).text
265
- }
266
- }
267
- return ''
268
- }
269
-
270
- const hasDisplayableContent = (msg: Message): boolean => {
271
- // Don't show tool_result messages
272
- if (msg.role === 'tool_result') return false
273
-
274
- // Check if there's any text content
275
- const text = getTextContent(msg)
276
- if (text.trim()) return true
277
-
278
- // Check for thinking content
279
- for (const block of msg.content) {
280
- if (block.type === 'thinking' && block.thinking?.trim()) return true
281
- }
282
-
283
- return false
284
- }
285
-
286
- const renderContent = (content: ContentBlock[], isStreaming = false) => {
287
- const elements: React.ReactNode[] = []
288
-
289
- for (let i = 0; i < content.length; i++) {
290
- const block = content[i]!
291
-
292
- // Handle text blocks
293
- if (block.type === 'text' && block.text?.trim()) {
294
- elements.push(
295
- <div key={i} className="prose prose-sm dark:prose-invert max-w-none break-words overflow-hidden">
296
- <Streamdown mode={isStreaming ? undefined : "static"} plugins={{ code }}>
297
- {block.text}
298
- </Streamdown>
299
- </div>
300
- )
301
- }
302
- // Handle thinking blocks
303
- else if (block.type === 'thinking' && block.thinking?.trim()) {
304
- elements.push(
305
- <details key={i} className="text-muted-foreground text-sm">
306
- <summary className="cursor-pointer">Thinking...</summary>
307
- <div className="mt-2 pl-3 border-l-2 border-muted break-words overflow-hidden">{block.thinking}</div>
308
- </details>
309
- )
310
- }
311
- // Handle blocks that might just have text without proper type
312
- else if ('text' in block && typeof (block as { text?: string }).text === 'string' && (block as { text: string }).text.trim()) {
313
- const text = (block as { text: string }).text
314
- elements.push(
315
- <div key={i} className="prose prose-sm dark:prose-invert max-w-none break-words overflow-hidden">
316
- <Streamdown mode={isStreaming ? undefined : "static"} plugins={{ code }}>
317
- {text}
318
- </Streamdown>
319
- </div>
320
- )
321
- }
322
- }
323
-
324
- return elements
325
- }
326
-
327
- // Auth screen
328
- if (authStatus && !authStatus.authenticated && status === 'connected') {
329
- return (
330
- <div className="h-screen flex items-center justify-center bg-background p-4">
331
- <Card className="max-w-sm w-full">
332
- <CardContent className="pt-6 text-center space-y-4">
333
- <p className="text-muted-foreground text-sm">
334
- Run <code className="bg-muted px-1.5 py-0.5 rounded">pi</code> in your terminal to log in.
335
- </p>
336
- <div className="text-left text-sm text-muted-foreground">
337
- <p className="mb-2">Supported providers:</p>
338
- <ul className="list-disc list-inside space-y-1">
339
- <li>Anthropic (Claude Pro/Max)</li>
340
- <li>GitHub Copilot</li>
341
- <li>Google Gemini CLI</li>
342
- <li>OpenAI Codex</li>
343
- </ul>
344
- </div>
345
- <Button variant="secondary" onClick={refreshAuth}>Refresh</Button>
346
- </CardContent>
347
- </Card>
348
- </div>
349
- )
350
- }
351
-
352
- const filteredMessages = messages.filter(hasDisplayableContent)
353
-
354
- return (
355
- <div className="h-screen flex flex-col bg-background overflow-hidden fixed inset-0">
356
- {/* Header - minimal */}
357
- <div className="flex items-center justify-between px-4 py-2 border-b flex-shrink-0">
358
- <div className="flex items-center gap-2">
359
- {/* Session stats */}
360
- {sessionStats?.tokens && (
361
- <span className="text-xs text-muted-foreground">
362
- {sessionStats.tokens.total.toLocaleString()}tk
363
- {sessionStats.cost !== undefined && ` · $${sessionStats.cost.toFixed(3)}`}
364
- </span>
365
- )}
366
- </div>
367
-
368
- <div className="flex items-center gap-2">
369
- {/* Compact button */}
370
- <Button
371
- variant="outline"
372
- size="sm"
373
- className="h-7 px-3 text-xs"
374
- onClick={() => {
375
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'compact' } }))
376
- }}
377
- disabled={sessionState?.isCompacting || sessionState?.isStreaming}
378
- >
379
- {sessionState?.isCompacting ? '...' : 'Compact'}
380
- </Button>
381
-
382
- {/* New session button */}
383
- <Button
384
- variant="outline"
385
- size="sm"
386
- className="h-7 px-3 text-xs"
387
- onClick={() => {
388
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'new_session' } }))
389
- }}
390
- disabled={sessionState?.isStreaming}
391
- >
392
- New
393
- </Button>
394
-
395
- {/* Settings dropdown */}
396
- <DropdownMenu>
397
- <DropdownMenuTrigger asChild>
398
- <Button variant="ghost" size="sm" className="h-7 px-2 text-xs">
399
- Settings
400
- </Button>
401
- </DropdownMenuTrigger>
402
- <DropdownMenuContent align="end">
403
- <div className="flex items-center justify-between px-2 py-1.5 gap-4">
404
- <span className="text-xs">Auto-compact</span>
405
- <Switch
406
- checked={sessionState?.autoCompactionEnabled ?? true}
407
- onCheckedChange={(checked) => {
408
- wsRef.current?.send(JSON.stringify({
409
- type: 'command',
410
- command: { type: 'set_auto_compaction', enabled: checked }
411
- }))
412
- }}
413
- className="scale-75"
414
- />
415
- </div>
416
- </DropdownMenuContent>
417
- </DropdownMenu>
418
-
419
- {/* Connection status */}
420
- <div className={`w-2 h-2 rounded-full flex-shrink-0 ${
421
- status === 'connected' ? 'bg-green-500' :
422
- status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
423
- }`} />
424
- </div>
425
- </div>
426
-
427
- {/* Error */}
428
- {error && (
429
- <div className="px-4 py-2 bg-destructive/10 text-destructive text-sm flex justify-between flex-shrink-0">
430
- <span>{error}</span>
431
- <button onClick={() => setError(null)} className="text-xs hover:underline">Dismiss</button>
432
- </div>
433
- )}
434
-
435
- {/* Messages with stick-to-bottom */}
436
- <StickToBottom className="flex-1 relative" resize="smooth" initial="smooth">
437
- <StickToBottom.Content className="flex flex-col gap-4 p-4">
438
- {filteredMessages.length === 0 && !streamingMessage && (
439
- <div className="h-full flex items-center justify-center text-muted-foreground min-h-[200px]">
440
- Start a conversation
441
- </div>
442
- )}
443
-
444
- {filteredMessages.map((msg, i) => (
445
- <div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
446
- <div className={`max-w-[80%] rounded-lg px-4 py-2 overflow-hidden ${
447
- msg.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted'
448
- }`}>
449
- {renderContent(msg.content, false)}
450
- </div>
451
- </div>
452
- ))}
453
-
454
- {streamingMessage && (() => {
455
- const rendered = renderContent(streamingMessage.content, true)
456
- if (rendered.length === 0) return null
457
- return (
458
- <div className="flex justify-start">
459
- <div className="max-w-[80%] rounded-lg px-4 py-2 bg-muted overflow-hidden">
460
- {rendered}
461
- <span className="inline-block w-2 h-4 bg-foreground/50 animate-pulse ml-1" />
462
- </div>
463
- </div>
464
- )
465
- })()}
466
-
467
- {toolExecutions.size > 0 && (
468
- <div className="space-y-2">
469
- {Array.from(toolExecutions.values()).map(tool => (
470
- <Card key={tool.toolCallId} className="overflow-hidden">
471
- <div className="flex items-center gap-2 px-3 py-2 bg-muted/50">
472
- <span className={`w-2 h-2 rounded-full ${
473
- tool.status === 'running' ? 'bg-yellow-500 animate-pulse' :
474
- tool.status === 'done' ? 'bg-green-500' : 'bg-red-500'
475
- }`} />
476
- <code className="text-xs">{tool.toolName}</code>
477
- {tool.status === 'running' && (
478
- <span className="ml-auto text-xs text-muted-foreground">Running</span>
479
- )}
480
- </div>
481
- {tool.partialResult && (
482
- <pre className="p-3 text-xs overflow-auto max-h-32 bg-background break-all">
483
- {tool.partialResult}
484
- </pre>
485
- )}
486
- </Card>
487
- ))}
488
- </div>
489
- )}
490
- </StickToBottom.Content>
491
-
492
- <ScrollToBottom />
493
- </StickToBottom>
494
-
495
- {/* Input area with model/thinking controls */}
496
- <div className="border-t p-4 flex-shrink-0">
497
- {/* Model and thinking level controls */}
498
- <div className="flex items-center gap-2 mb-3">
499
- {/* Model selector */}
500
- <DropdownMenu onOpenChange={(open) => {
501
- if (open) {
502
- wsRef.current?.send(JSON.stringify({ type: 'command', command: { type: 'get_available_models' } }))
503
- }
504
- }}>
505
- <DropdownMenuTrigger asChild>
506
- <Badge variant="secondary" className="text-xs cursor-pointer">
507
- {sessionState?.model?.name || sessionState?.model?.id || 'Select model'}
508
- </Badge>
509
- </DropdownMenuTrigger>
510
- <DropdownMenuContent align="start" className="max-h-80 overflow-y-auto min-w-[200px]">
511
- {availableModels.length === 0 ? (
512
- <div className="px-2 py-1 text-xs text-muted-foreground">Loading models...</div>
513
- ) : (
514
- availableModels.map((model) => (
515
- <DropdownMenuItem
516
- key={`${model.provider}/${model.id}`}
517
- onClick={() => {
518
- wsRef.current?.send(JSON.stringify({
519
- type: 'command',
520
- command: { type: 'set_model', provider: model.provider, modelId: model.id }
521
- }))
522
- }}
523
- >
524
- <span className="text-xs">{model.name || model.id}</span>
525
- <span className="text-xs text-muted-foreground ml-2">{model.provider}</span>
526
- {model.reasoning && <span className="text-xs text-muted-foreground ml-1">(reasoning)</span>}
527
- </DropdownMenuItem>
528
- ))
529
- )}
530
- </DropdownMenuContent>
531
- </DropdownMenu>
532
-
533
- {/* Thinking level selector - only show if model explicitly supports reasoning */}
534
- {supportsReasoning && (
535
- <DropdownMenu>
536
- <DropdownMenuTrigger asChild>
537
- <Badge variant="outline" className="text-xs cursor-pointer">
538
- thinking: {sessionState?.thinkingLevel || 'medium'}
539
- </Badge>
540
- </DropdownMenuTrigger>
541
- <DropdownMenuContent align="start">
542
- {THINKING_LEVELS.map((level) => (
543
- <DropdownMenuItem
544
- key={level}
545
- onClick={() => {
546
- wsRef.current?.send(JSON.stringify({
547
- type: 'command',
548
- command: { type: 'set_thinking_level', level }
549
- }))
550
- }}
551
- >
552
- <span className={`text-xs ${sessionState?.thinkingLevel === level ? 'font-bold' : ''}`}>
553
- {level}
554
- </span>
555
- </DropdownMenuItem>
556
- ))}
557
- </DropdownMenuContent>
558
- </DropdownMenu>
559
- )}
560
- </div>
561
-
562
- {/* Text input and send button */}
563
- <div className="flex gap-2 items-end">
564
- <Textarea
565
- value={input}
566
- onChange={(e) => setInput(e.target.value)}
567
- onKeyDown={handleKeyDown}
568
- placeholder={sessionState?.isStreaming ? 'Agent is responding...' : 'Type a message...'}
569
- disabled={status !== 'connected'}
570
- className="min-h-[44px] max-h-[200px] resize-none flex-1"
571
- rows={1}
572
- />
573
- {sessionState?.isStreaming ? (
574
- <Button variant="destructive" onClick={handleAbort} className="h-[44px]">Stop</Button>
575
- ) : (
576
- <Button onClick={sendMessage} disabled={!input.trim() || status !== 'connected'} className="h-[44px]">
577
- Send
578
- </Button>
579
- )}
580
- </div>
581
- </div>
582
- </div>
583
- )
584
- }
585
-
586
- export default App
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>