@bytespell/shella 0.2.4 → 0.2.6

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 (53) hide show
  1. package/bundled-plugins/agent/AGENT_SPEC.md +611 -0
  2. package/bundled-plugins/agent/README.md +7 -0
  3. package/bundled-plugins/agent/components.json +24 -0
  4. package/bundled-plugins/agent/eslint.config.js +23 -0
  5. package/bundled-plugins/agent/index.html +13 -0
  6. package/bundled-plugins/agent/package-lock.json +12140 -0
  7. package/bundled-plugins/agent/package.json +62 -0
  8. package/bundled-plugins/agent/public/vite.svg +1 -0
  9. package/bundled-plugins/agent/server.js +631 -0
  10. package/bundled-plugins/agent/src/App.tsx +755 -0
  11. package/bundled-plugins/agent/src/assets/react.svg +1 -0
  12. package/bundled-plugins/agent/src/components/ui/alert-dialog.tsx +182 -0
  13. package/bundled-plugins/agent/src/components/ui/badge.tsx +45 -0
  14. package/bundled-plugins/agent/src/components/ui/button.tsx +60 -0
  15. package/bundled-plugins/agent/src/components/ui/card.tsx +94 -0
  16. package/bundled-plugins/agent/src/components/ui/combobox.tsx +294 -0
  17. package/bundled-plugins/agent/src/components/ui/dropdown-menu.tsx +253 -0
  18. package/bundled-plugins/agent/src/components/ui/field.tsx +225 -0
  19. package/bundled-plugins/agent/src/components/ui/input-group.tsx +147 -0
  20. package/bundled-plugins/agent/src/components/ui/input.tsx +19 -0
  21. package/bundled-plugins/agent/src/components/ui/label.tsx +24 -0
  22. package/bundled-plugins/agent/src/components/ui/select.tsx +185 -0
  23. package/bundled-plugins/agent/src/components/ui/separator.tsx +26 -0
  24. package/bundled-plugins/agent/src/components/ui/switch.tsx +31 -0
  25. package/bundled-plugins/agent/src/components/ui/textarea.tsx +18 -0
  26. package/bundled-plugins/agent/src/index.css +131 -0
  27. package/bundled-plugins/agent/src/lib/utils.ts +6 -0
  28. package/bundled-plugins/agent/src/main.tsx +11 -0
  29. package/bundled-plugins/agent/src/reducer.test.ts +359 -0
  30. package/bundled-plugins/agent/src/reducer.ts +255 -0
  31. package/bundled-plugins/agent/src/store.ts +379 -0
  32. package/bundled-plugins/agent/src/types.ts +98 -0
  33. package/bundled-plugins/agent/src/utils.test.ts +393 -0
  34. package/bundled-plugins/agent/src/utils.ts +158 -0
  35. package/bundled-plugins/agent/tsconfig.app.json +32 -0
  36. package/bundled-plugins/agent/tsconfig.json +13 -0
  37. package/bundled-plugins/agent/tsconfig.node.json +26 -0
  38. package/bundled-plugins/agent/vite.config.ts +14 -0
  39. package/bundled-plugins/agent/vitest.config.ts +17 -0
  40. package/bundled-plugins/terminal/README.md +7 -0
  41. package/bundled-plugins/terminal/index.html +24 -0
  42. package/bundled-plugins/terminal/package-lock.json +3346 -0
  43. package/bundled-plugins/terminal/package.json +38 -0
  44. package/bundled-plugins/terminal/server.ts +265 -0
  45. package/bundled-plugins/terminal/src/App.tsx +153 -0
  46. package/bundled-plugins/terminal/src/TERMINAL_SPEC.md +404 -0
  47. package/bundled-plugins/terminal/src/main.tsx +9 -0
  48. package/bundled-plugins/terminal/src/store.ts +114 -0
  49. package/bundled-plugins/terminal/tsconfig.json +22 -0
  50. package/bundled-plugins/terminal/vite.config.ts +10 -0
  51. package/dist/src/plugin-manager.js +1 -1
  52. package/dist/src/plugin-manager.js.map +1 -1
  53. package/package.json +1 -1
@@ -0,0 +1,379 @@
1
+ import { create } from 'zustand'
2
+ import type {
3
+ ContentBlock,
4
+ AgentMessage,
5
+ ActiveToolCall,
6
+ SessionState,
7
+ SessionStats,
8
+ AvailableModel,
9
+ ConnectionStatus,
10
+ ToolResultMessage,
11
+ Session,
12
+ } from './types'
13
+
14
+ interface AgentStore {
15
+ // Connection state
16
+ status: ConnectionStatus
17
+ error: string | null
18
+ currentCwd: string | null
19
+
20
+ // Session state
21
+ sessionState: SessionState | null
22
+ sessionStats: SessionStats | null
23
+ availableModels: AvailableModel[]
24
+ sessions: Session[]
25
+
26
+ // Messages
27
+ messages: AgentMessage[]
28
+ isStreaming: boolean
29
+ streamingMessageIndex: number | null
30
+ activeToolCalls: Map<string, ActiveToolCall>
31
+
32
+ // Actions
33
+ connect: () => void
34
+ disconnect: () => void
35
+ send: (data: unknown) => void
36
+ sendPrompt: (message: string) => void
37
+ abort: () => void
38
+ changeCwd: (path: string) => void
39
+ setError: (error: string | null) => void
40
+
41
+ // Commands
42
+ getState: () => void
43
+ getMessages: () => void
44
+ getAvailableModels: () => void
45
+ getSessionStats: () => void
46
+ getSessions: () => void
47
+ setModel: (provider: string, modelId: string) => void
48
+ setThinkingLevel: (level: string) => void
49
+ setAutoCompaction: (enabled: boolean) => void
50
+ compact: () => void
51
+ newSession: () => void
52
+ switchSession: (sessionId: string) => void
53
+ }
54
+
55
+ // WebSocket singleton - lives outside React
56
+ let ws: WebSocket | null = null
57
+ let reconnectTimeout: number | null = null
58
+ let reconnectAttempts = 0
59
+
60
+ export const useAgentStore = create<AgentStore>((set, get) => ({
61
+ // Initial state
62
+ status: 'connecting',
63
+ error: null,
64
+ currentCwd: null,
65
+ sessionState: null,
66
+ sessionStats: null,
67
+ availableModels: [],
68
+ sessions: [],
69
+ messages: [],
70
+ isStreaming: false,
71
+ streamingMessageIndex: null,
72
+ activeToolCalls: new Map(),
73
+
74
+ // Connect to WebSocket
75
+ connect: () => {
76
+ // Don't reconnect if already open or connecting
77
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
78
+
79
+ if (ws) {
80
+ ws.onclose = null // Prevent reconnect on intentional close
81
+ ws.close()
82
+ }
83
+
84
+ set({ status: 'connecting' })
85
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
86
+ ws = new WebSocket(`${protocol}//${window.location.host}/ws`)
87
+
88
+ ws.onopen = () => {
89
+ set({ status: 'connected', error: null })
90
+ reconnectAttempts = 0
91
+ ws?.send(JSON.stringify({ type: 'start' }))
92
+ }
93
+
94
+ ws.onclose = () => {
95
+ set({ status: 'disconnected' })
96
+ ws = null
97
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000)
98
+ reconnectAttempts++
99
+ reconnectTimeout = window.setTimeout(() => {
100
+ get().connect()
101
+ }, delay)
102
+ }
103
+
104
+ ws.onerror = () => {
105
+ set({ status: 'error', error: 'WebSocket connection failed' })
106
+ }
107
+
108
+ ws.onmessage = (event) => {
109
+ try {
110
+ const data = JSON.parse(event.data)
111
+ handleMessage(data, set, get)
112
+ } catch (err) {
113
+ console.error('Parse error:', err)
114
+ }
115
+ }
116
+ },
117
+
118
+ disconnect: () => {
119
+ if (reconnectTimeout) {
120
+ clearTimeout(reconnectTimeout)
121
+ reconnectTimeout = null
122
+ }
123
+ if (ws) {
124
+ ws.onclose = null
125
+ ws.close()
126
+ ws = null
127
+ }
128
+ set({ status: 'disconnected' })
129
+ },
130
+
131
+ send: (data) => {
132
+ ws?.send(JSON.stringify(data))
133
+ },
134
+
135
+ sendPrompt: (message) => {
136
+ const state = get()
137
+ if (!message.trim() || !ws || state.sessionState?.isStreaming) return
138
+
139
+ // Optimistically add user message
140
+ set({
141
+ messages: [...state.messages, { role: 'user', content: [{ type: 'text', text: message }] }]
142
+ })
143
+ ws.send(JSON.stringify({ type: 'prompt', message }))
144
+ },
145
+
146
+ abort: () => {
147
+ ws?.send(JSON.stringify({ type: 'abort' }))
148
+ },
149
+
150
+ changeCwd: (path) => {
151
+ ws?.send(JSON.stringify({ type: 'change_cwd', path }))
152
+ },
153
+
154
+ setError: (error) => set({ error }),
155
+
156
+ // Commands
157
+ getState: () => {
158
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'get_state' } }))
159
+ },
160
+
161
+ getMessages: () => {
162
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'get_messages' } }))
163
+ },
164
+
165
+ getAvailableModels: () => {
166
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'get_available_models' } }))
167
+ },
168
+
169
+ getSessionStats: () => {
170
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'get_session_stats' } }))
171
+ },
172
+
173
+ getSessions: () => {
174
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'get_sessions' } }))
175
+ },
176
+
177
+ setModel: (provider, modelId) => {
178
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'set_model', provider, modelId } }))
179
+ },
180
+
181
+ setThinkingLevel: (level) => {
182
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'set_thinking_level', level } }))
183
+ },
184
+
185
+ setAutoCompaction: (enabled) => {
186
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'set_auto_compaction', enabled } }))
187
+ },
188
+
189
+ compact: () => {
190
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'compact' } }))
191
+ },
192
+
193
+ newSession: () => {
194
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'new_session' } }))
195
+ },
196
+
197
+ switchSession: (sessionPath) => {
198
+ ws?.send(JSON.stringify({ type: 'command', command: { type: 'switch_session', sessionPath } }))
199
+ },
200
+ }))
201
+
202
+ // Handle incoming WebSocket messages
203
+ function handleMessage(
204
+ data: { type: string; event?: Record<string, unknown>; message?: string; cwd?: string },
205
+ set: (partial: Partial<AgentStore>) => void,
206
+ get: () => AgentStore
207
+ ) {
208
+ if (data.type === 'ready') {
209
+ set({ status: 'connected' })
210
+ if (data.cwd) set({ currentCwd: data.cwd })
211
+ } else if (data.type === 'cwd_changed') {
212
+ if (data.cwd) set({ currentCwd: data.cwd })
213
+ set({ messages: [], sessionStats: null, activeToolCalls: new Map() })
214
+ } else if (data.type === 'error') {
215
+ set({ error: data.message || 'Unknown error' })
216
+ } else if (data.type === 'rpc_event' && data.event) {
217
+ handleRpcEvent(data.event, set, get)
218
+ }
219
+ }
220
+
221
+ // Handle RPC events from pi-agent
222
+ function handleRpcEvent(
223
+ event: Record<string, unknown>,
224
+ set: (partial: Partial<AgentStore>) => void,
225
+ get: () => AgentStore
226
+ ) {
227
+ const eventType = event.type as string
228
+
229
+ // Response events
230
+ if (eventType === 'response') {
231
+ const command = event.command as string
232
+ if (command === 'get_state' && event.success) {
233
+ set({ sessionState: event.data as SessionState })
234
+ } else if (command === 'get_messages' && event.success) {
235
+ const data = event.data as { messages: AgentMessage[] }
236
+ console.log('[store] get_messages response:', data.messages?.length, 'messages', data)
237
+ set({ messages: data.messages || [] })
238
+ } else if (command === 'get_available_models' && event.success) {
239
+ const data = event.data as { models: AvailableModel[] }
240
+ set({ availableModels: data.models || [] })
241
+ } else if (command === 'get_session_stats' && event.success) {
242
+ set({ sessionStats: event.data as SessionStats })
243
+ } else if (command === 'set_model' && event.success) {
244
+ get().getState()
245
+ } else if (command === 'set_thinking_level' && event.success) {
246
+ get().getState()
247
+ } else if (command === 'set_auto_compaction' && event.success) {
248
+ get().getState()
249
+ } else if (command === 'compact' && event.success) {
250
+ get().getState()
251
+ get().getSessionStats()
252
+ } else if (command === 'new_session' && event.success) {
253
+ set({ messages: [], sessionStats: null })
254
+ get().getState()
255
+ get().getSessions() // Refresh sessions list
256
+ } else if (command === 'get_sessions' && event.success) {
257
+ const data = event.data as { sessions: Session[] }
258
+ set({ sessions: data.sessions || [] })
259
+ } else if (command === 'switch_session' && event.success) {
260
+ // Session switched - clear current state and fetch new messages
261
+ set({ messages: [], sessionStats: null, activeToolCalls: new Map() })
262
+ get().getState()
263
+ get().getMessages()
264
+ get().getSessionStats()
265
+ get().getSessions() // Refresh to update current session indicator
266
+ }
267
+ }
268
+
269
+ // Agent lifecycle events
270
+ else if (eventType === 'agent_start') {
271
+ set({ isStreaming: true, activeToolCalls: new Map() })
272
+ }
273
+ else if (eventType === 'agent_end') {
274
+ set({ isStreaming: false, activeToolCalls: new Map(), streamingMessageIndex: null })
275
+ get().getState()
276
+ get().getSessionStats()
277
+ }
278
+
279
+ // Message events
280
+ else if (eventType === 'message_start') {
281
+ const msg = event.message as AgentMessage
282
+ console.log('[store] message_start:', msg?.role, msg)
283
+ if (msg) {
284
+ // Skip user messages - we add them optimistically in sendPrompt
285
+ if (msg.role === 'user') {
286
+ return
287
+ }
288
+
289
+ const messages = get().messages
290
+ const newMessages = [...messages, msg]
291
+
292
+ const updates: Partial<AgentStore> = { messages: newMessages }
293
+
294
+ if (msg.role === 'assistant') {
295
+ updates.streamingMessageIndex = newMessages.length - 1
296
+ }
297
+
298
+ if (msg.role === 'toolResult') {
299
+ const toolCallId = (msg as ToolResultMessage).toolCallId
300
+ const activeToolCalls = new Map(get().activeToolCalls)
301
+ activeToolCalls.delete(toolCallId)
302
+ updates.activeToolCalls = activeToolCalls
303
+ }
304
+
305
+ set(updates)
306
+ }
307
+ }
308
+ else if (eventType === 'message_update') {
309
+ const msg = event.message as AgentMessage
310
+ if (msg) {
311
+ const messages = get().messages
312
+ if (messages.length === 0) {
313
+ set({ messages: [msg] })
314
+ } else {
315
+ const updated = [...messages]
316
+ updated[updated.length - 1] = msg
317
+ set({ messages: updated })
318
+ }
319
+ }
320
+ }
321
+ else if (eventType === 'message_end') {
322
+ set({ streamingMessageIndex: null })
323
+ }
324
+
325
+ // Tool execution events
326
+ else if (eventType === 'tool_execution_start') {
327
+ const toolCallId = event.toolCallId as string
328
+ const activeToolCalls = new Map(get().activeToolCalls)
329
+ activeToolCalls.set(toolCallId, {
330
+ toolCallId,
331
+ toolName: event.toolName as string,
332
+ args: (event.args as Record<string, unknown>) || {},
333
+ status: 'running',
334
+ partialOutput: null,
335
+ })
336
+ set({ activeToolCalls })
337
+ }
338
+ else if (eventType === 'tool_execution_update') {
339
+ const toolCallId = event.toolCallId as string
340
+ const partialResult = event.partialResult as { content?: ContentBlock[] } | undefined
341
+ const activeToolCalls = new Map(get().activeToolCalls)
342
+ const existing = activeToolCalls.get(toolCallId)
343
+ if (existing) {
344
+ const text = partialResult?.content?.find((c): c is { type: 'text'; text: string } => c.type === 'text')?.text
345
+ activeToolCalls.set(toolCallId, { ...existing, partialOutput: text || null })
346
+ set({ activeToolCalls })
347
+ }
348
+ }
349
+ else if (eventType === 'tool_execution_end') {
350
+ const toolCallId = event.toolCallId as string
351
+ const result = event.result as { content?: ContentBlock[] } | undefined
352
+ const activeToolCalls = new Map(get().activeToolCalls)
353
+ const existing = activeToolCalls.get(toolCallId)
354
+ if (existing) {
355
+ const text = result?.content?.find((c): c is { type: 'text'; text: string } => c.type === 'text')?.text
356
+ activeToolCalls.set(toolCallId, {
357
+ ...existing,
358
+ status: event.isError ? 'error' : 'done',
359
+ partialOutput: text || null,
360
+ })
361
+ set({ activeToolCalls })
362
+ }
363
+ }
364
+ }
365
+
366
+ // Reconnect on visibility change
367
+ if (typeof document !== 'undefined') {
368
+ document.addEventListener('visibilitychange', () => {
369
+ const { status, connect } = useAgentStore.getState()
370
+ if (document.visibilityState === 'visible' && (status === 'disconnected' || status === 'error')) {
371
+ if (reconnectTimeout) {
372
+ clearTimeout(reconnectTimeout)
373
+ reconnectTimeout = null
374
+ }
375
+ reconnectAttempts = 0
376
+ connect()
377
+ }
378
+ })
379
+ }
@@ -0,0 +1,98 @@
1
+ // =============================================================================
2
+ // Types - Following AGENT_SPEC.md
3
+ // =============================================================================
4
+
5
+ export type ContentBlock =
6
+ | { type: 'text'; text: string }
7
+ | { type: 'thinking'; thinking: string }
8
+ | { type: 'toolCall'; id: string; name: string; arguments: Record<string, unknown> }
9
+ | { type: 'image'; data: string; mimeType: string }
10
+
11
+ export interface UserMessage {
12
+ role: 'user'
13
+ content: ContentBlock[]
14
+ timestamp?: number
15
+ }
16
+
17
+ export interface AssistantMessage {
18
+ role: 'assistant'
19
+ content: ContentBlock[]
20
+ model?: string
21
+ stopReason?: string
22
+ timestamp?: number
23
+ }
24
+
25
+ export interface ToolResultMessage {
26
+ role: 'toolResult'
27
+ toolCallId: string
28
+ toolName: string
29
+ content: ContentBlock[]
30
+ isError?: boolean
31
+ timestamp?: number
32
+ details?: ToolResultDetails
33
+ }
34
+
35
+ // Tool-specific details
36
+ export type ToolResultDetails = EditToolDetails | BashToolDetails | unknown
37
+
38
+ export interface EditToolDetails {
39
+ diff: string
40
+ firstChangedLine?: number
41
+ }
42
+
43
+ export interface BashToolDetails {
44
+ exitCode?: number
45
+ truncated?: boolean
46
+ }
47
+
48
+ export type AgentMessage = UserMessage | AssistantMessage | ToolResultMessage
49
+
50
+ export interface ActiveToolCall {
51
+ toolCallId: string
52
+ toolName: string
53
+ args: Record<string, unknown>
54
+ status: 'running' | 'done' | 'error'
55
+ partialOutput: string | null
56
+ }
57
+
58
+ export interface SessionState {
59
+ model?: { provider: string; id: string; name?: string; reasoning?: boolean }
60
+ thinkingLevel?: string
61
+ isStreaming: boolean
62
+ isCompacting?: boolean
63
+ autoCompactionEnabled?: boolean
64
+ }
65
+
66
+ export interface SessionStats {
67
+ tokens?: { input: number; output: number; cacheRead?: number; cacheWrite?: number; total: number }
68
+ cost?: number
69
+ }
70
+
71
+ export interface AvailableModel {
72
+ provider: string
73
+ id: string
74
+ name?: string
75
+ reasoning?: boolean
76
+ }
77
+
78
+ export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error'
79
+
80
+ export interface Session {
81
+ id: string
82
+ name?: string | null
83
+ file?: string
84
+ messageCount?: number
85
+ created?: number | null
86
+ lastModified?: number
87
+ isCurrent?: boolean
88
+ }
89
+
90
+ // =============================================================================
91
+ // Render Item Types - For splitting assistant messages
92
+ // =============================================================================
93
+
94
+ export type RenderItem =
95
+ | { type: 'user-message'; content: ContentBlock[] }
96
+ | { type: 'text-chunk'; blocks: ContentBlock[]; isStreaming: boolean }
97
+ | { type: 'tool-call'; id: string; name: string; args: Record<string, unknown> }
98
+ | { type: 'tool-result'; toolCallId: string; toolName: string; command?: string; output: string; isError: boolean; diff?: string; filePath?: string }