kaizenai 0.2.2 → 0.4.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 (73) hide show
  1. package/bin/kaizen +16 -5
  2. package/dist/client/app-icon-180.png +0 -0
  3. package/dist/client/app-icon-192.png +0 -0
  4. package/dist/client/app-icon-512.png +0 -0
  5. package/dist/client/app-icon-96.png +0 -0
  6. package/dist/client/apple-touch-icon.png +0 -0
  7. package/dist/client/assets/index-BR0jGwm_.css +32 -0
  8. package/dist/client/assets/index-CYqy6l6r.js +628 -0
  9. package/dist/client/favicon.png +0 -0
  10. package/dist/client/index.html +17 -10
  11. package/dist/client/manifest-dark.webmanifest +3 -3
  12. package/dist/client/manifest.webmanifest +3 -3
  13. package/dist/client/pwa-192.png +0 -0
  14. package/dist/client/pwa-512.png +0 -0
  15. package/dist/server/cli-supervisor.js +307 -0
  16. package/dist/server/cli.js +12371 -0
  17. package/package.json +7 -9
  18. package/dist/client/assets/index-Cojjg8ln.js +0 -598
  19. package/dist/client/assets/index-lSe049cB.css +0 -32
  20. package/src/server/acp-shared.ts +0 -315
  21. package/src/server/agent.ts +0 -1121
  22. package/src/server/attachments.ts +0 -133
  23. package/src/server/backgrounds.ts +0 -74
  24. package/src/server/cli-runtime.ts +0 -375
  25. package/src/server/cli-supervisor.ts +0 -97
  26. package/src/server/cli.ts +0 -68
  27. package/src/server/codex-app-server-protocol.ts +0 -453
  28. package/src/server/codex-app-server.ts +0 -1350
  29. package/src/server/cursor-acp.ts +0 -819
  30. package/src/server/discovery.ts +0 -322
  31. package/src/server/event-store.ts +0 -1369
  32. package/src/server/events.ts +0 -244
  33. package/src/server/external-open.ts +0 -272
  34. package/src/server/gemini-acp.ts +0 -844
  35. package/src/server/gemini-cli.ts +0 -525
  36. package/src/server/generate-title.ts +0 -36
  37. package/src/server/git-manager.ts +0 -79
  38. package/src/server/git-repository.ts +0 -101
  39. package/src/server/harness-types.ts +0 -20
  40. package/src/server/keybindings.ts +0 -177
  41. package/src/server/machine-name.ts +0 -22
  42. package/src/server/paths.ts +0 -112
  43. package/src/server/process-utils.ts +0 -22
  44. package/src/server/project-icon.ts +0 -352
  45. package/src/server/project-metadata.ts +0 -10
  46. package/src/server/provider-catalog.ts +0 -85
  47. package/src/server/provider-settings.ts +0 -155
  48. package/src/server/quick-response.ts +0 -153
  49. package/src/server/read-models.ts +0 -275
  50. package/src/server/recovery.ts +0 -507
  51. package/src/server/restart.ts +0 -56
  52. package/src/server/server.ts +0 -244
  53. package/src/server/terminal-manager.ts +0 -350
  54. package/src/server/theme-settings.ts +0 -179
  55. package/src/server/update-manager.ts +0 -230
  56. package/src/server/usage/base-provider-usage.ts +0 -57
  57. package/src/server/usage/claude-usage.ts +0 -558
  58. package/src/server/usage/codex-usage.ts +0 -144
  59. package/src/server/usage/cursor-browser.ts +0 -120
  60. package/src/server/usage/cursor-cookies.ts +0 -390
  61. package/src/server/usage/cursor-usage.ts +0 -490
  62. package/src/server/usage/gemini-usage.ts +0 -24
  63. package/src/server/usage/provider-usage.ts +0 -61
  64. package/src/server/usage/test-helpers.ts +0 -9
  65. package/src/server/usage/types.ts +0 -54
  66. package/src/server/usage/utils.ts +0 -325
  67. package/src/server/ws-router.ts +0 -709
  68. package/src/shared/branding.ts +0 -83
  69. package/src/shared/dev-ports.ts +0 -43
  70. package/src/shared/ports.ts +0 -2
  71. package/src/shared/protocol.ts +0 -152
  72. package/src/shared/tools.ts +0 -251
  73. package/src/shared/types.ts +0 -1028
@@ -1,709 +0,0 @@
1
- import type { ServerWebSocket } from "bun"
2
- import { DEFAULT_PROVIDER_SETTINGS, PROTOCOL_VERSION, getSelectableProviders, isProviderSelectable, type AgentProvider } from "../shared/types"
3
- import type { ClientEnvelope, ServerEnvelope, SubscriptionTopic } from "../shared/protocol"
4
- import { isClientEnvelope } from "../shared/protocol"
5
- import type { AgentCoordinator } from "./agent"
6
- import type { DiscoveredProject } from "./discovery"
7
- import { EventStore } from "./event-store"
8
- import { openExternal, openUrl } from "./external-open"
9
- import { GitManager } from "./git-manager"
10
- import { KeybindingsManager } from "./keybindings"
11
- import { ProviderSettingsManager } from "./provider-settings"
12
- import { ThemeSettingsManager } from "./theme-settings"
13
- import { listProjectDirectories, requireProjectDirectory, ensureProjectDirectory } from "./paths"
14
- import { importProjectHistory } from "./recovery"
15
- import { TerminalManager } from "./terminal-manager"
16
- import type { UpdateManager } from "./update-manager"
17
- import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
18
- import { refreshClaudeRateLimitFromCli } from "./usage/claude-usage"
19
- import { importCursorUsageFromCurl, refreshCursorUsage, signInToCursorWithBrowser } from "./usage/cursor-usage"
20
-
21
- export interface ClientState {
22
- subscriptions: Map<string, SubscriptionTopic>
23
- }
24
-
25
- const PROVIDER_USAGE_POLL_INTERVAL_MS = 30 * 60 * 1000
26
- const PROVIDER_USAGE_POLL_MAX_INTERVAL_MS = 31 * 60 * 1000
27
-
28
- interface CreateWsRouterArgs {
29
- store: EventStore
30
- agent: AgentCoordinator
31
- terminals: TerminalManager
32
- git: GitManager
33
- keybindings: KeybindingsManager
34
- providerSettings?: ProviderSettingsManager
35
- themeSettings: ThemeSettingsManager
36
- refreshDiscovery: () => Promise<DiscoveredProject[]>
37
- getDiscoveredProjects: () => DiscoveredProject[]
38
- machineDisplayName: string
39
- updateManager: UpdateManager | null
40
- providerUsagePollIntervalMs?: number
41
- refreshProviderUsage?: (provider?: AgentProvider, force?: boolean) => Promise<void>
42
- openUrlCommand?: typeof openUrl
43
- }
44
-
45
- function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
46
- ws.send(JSON.stringify(message))
47
- }
48
-
49
- export function createWsRouter({
50
- store,
51
- agent,
52
- terminals,
53
- git,
54
- keybindings,
55
- providerSettings,
56
- themeSettings,
57
- refreshDiscovery,
58
- getDiscoveredProjects,
59
- machineDisplayName,
60
- updateManager,
61
- providerUsagePollIntervalMs,
62
- refreshProviderUsage = async (provider, force) => {
63
- const selectableProviders = getSelectableProviders(providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS).map((entry) => entry.id)
64
- if (((!provider && selectableProviders.includes("claude")) || provider === "claude") && selectableProviders.includes("claude")) {
65
- await refreshClaudeRateLimitFromCli(store.dataDir, undefined, force).then(() => {})
66
- }
67
- if (((!provider && selectableProviders.includes("cursor")) || provider === "cursor") && selectableProviders.includes("cursor")) {
68
- await refreshCursorUsage(store.dataDir, undefined, force).then(() => {})
69
- }
70
- },
71
- openUrlCommand = openUrl,
72
- }: CreateWsRouterArgs) {
73
- const sockets = new Set<ServerWebSocket<ClientState>>()
74
- let providerUsageRefreshInFlight: Promise<void> | null = null
75
- let providerUsagePollTimer: Timer | null = null
76
-
77
- function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
78
- if (topic.type === "sidebar") {
79
- return {
80
- v: PROTOCOL_VERSION,
81
- type: "snapshot",
82
- id,
83
- snapshot: {
84
- type: "sidebar",
85
- data: deriveSidebarData(store.state, agent.getActiveStatuses(), agent.getProviderUsage()),
86
- },
87
- }
88
- }
89
-
90
- if (topic.type === "local-projects") {
91
- const discoveredProjects = getDiscoveredProjects()
92
- const data = deriveLocalProjectsSnapshot(store.state, discoveredProjects, machineDisplayName)
93
-
94
- return {
95
- v: PROTOCOL_VERSION,
96
- type: "snapshot",
97
- id,
98
- snapshot: {
99
- type: "local-projects",
100
- data,
101
- },
102
- }
103
- }
104
-
105
- if (topic.type === "keybindings") {
106
- return {
107
- v: PROTOCOL_VERSION,
108
- type: "snapshot",
109
- id,
110
- snapshot: {
111
- type: "keybindings",
112
- data: keybindings.getSnapshot(),
113
- },
114
- }
115
- }
116
-
117
- if (topic.type === "theme-settings") {
118
- return {
119
- v: PROTOCOL_VERSION,
120
- type: "snapshot",
121
- id,
122
- snapshot: {
123
- type: "theme-settings",
124
- data: themeSettings.getSnapshot(),
125
- },
126
- }
127
- }
128
-
129
- if (topic.type === "provider-settings") {
130
- return {
131
- v: PROTOCOL_VERSION,
132
- type: "snapshot",
133
- id,
134
- snapshot: {
135
- type: "provider-settings",
136
- data: providerSettings?.getSnapshot() ?? {
137
- settings: DEFAULT_PROVIDER_SETTINGS,
138
- warning: null,
139
- filePathDisplay: "",
140
- },
141
- },
142
- }
143
- }
144
-
145
- if (topic.type === "update") {
146
- return {
147
- v: PROTOCOL_VERSION,
148
- type: "snapshot",
149
- id,
150
- snapshot: {
151
- type: "update",
152
- data: updateManager?.getSnapshot() ?? {
153
- currentVersion: "unknown",
154
- latestVersion: null,
155
- status: "idle",
156
- updateAvailable: false,
157
- lastCheckedAt: null,
158
- error: null,
159
- installAction: "restart",
160
- },
161
- },
162
- }
163
- }
164
-
165
- if (topic.type === "terminal") {
166
- return {
167
- v: PROTOCOL_VERSION,
168
- type: "snapshot",
169
- id,
170
- snapshot: {
171
- type: "terminal",
172
- data: terminals.getSnapshot(topic.terminalId),
173
- },
174
- }
175
- }
176
-
177
- return {
178
- v: PROTOCOL_VERSION,
179
- type: "snapshot",
180
- id,
181
- snapshot: {
182
- type: "chat",
183
- data: (() => {
184
- return deriveChatSnapshot(
185
- store.state,
186
- agent.getActiveStatuses(),
187
- topic.chatId,
188
- (chatId) => store.getMessages(chatId),
189
- agent.getChatPendingTool(topic.chatId),
190
- agent.getLiveUsage(topic.chatId),
191
- providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS
192
- )
193
- })(),
194
- },
195
- }
196
- }
197
-
198
- function pushSnapshots(ws: ServerWebSocket<ClientState>) {
199
- for (const [id, topic] of ws.data.subscriptions.entries()) {
200
- send(ws, createEnvelope(id, topic))
201
- }
202
- }
203
-
204
- function broadcastSnapshots() {
205
- for (const ws of sockets) {
206
- pushSnapshots(ws)
207
- }
208
- }
209
-
210
- function broadcastSidebarSnapshots() {
211
- for (const ws of sockets) {
212
- for (const [id, topic] of ws.data.subscriptions.entries()) {
213
- if (topic.type !== "sidebar") continue
214
- send(ws, createEnvelope(id, topic))
215
- }
216
- }
217
- }
218
-
219
- function hasSidebarSubscribers() {
220
- for (const ws of sockets) {
221
- for (const topic of ws.data.subscriptions.values()) {
222
- if (topic.type === "sidebar") return true
223
- }
224
- }
225
- return false
226
- }
227
-
228
- function pushTerminalSnapshot(terminalId: string) {
229
- for (const ws of sockets) {
230
- for (const [id, topic] of ws.data.subscriptions.entries()) {
231
- if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
232
- send(ws, createEnvelope(id, topic))
233
- }
234
- }
235
- }
236
-
237
- function pushTerminalEvent(terminalId: string, event: Extract<ServerEnvelope, { type: "event" }>["event"]) {
238
- for (const ws of sockets) {
239
- for (const [id, topic] of ws.data.subscriptions.entries()) {
240
- if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
241
- send(ws, {
242
- v: PROTOCOL_VERSION,
243
- type: "event",
244
- id,
245
- event,
246
- })
247
- }
248
- }
249
- }
250
-
251
- const disposeTerminalEvents = terminals.onEvent((event) => {
252
- pushTerminalEvent(event.terminalId, event)
253
- })
254
-
255
- const disposeKeybindingEvents = keybindings.onChange(() => {
256
- for (const ws of sockets) {
257
- for (const [id, topic] of ws.data.subscriptions.entries()) {
258
- if (topic.type !== "keybindings") continue
259
- send(ws, createEnvelope(id, topic))
260
- }
261
- }
262
- })
263
-
264
- const disposeThemeSettingsEvents = themeSettings.onChange(() => {
265
- for (const ws of sockets) {
266
- for (const [id, topic] of ws.data.subscriptions.entries()) {
267
- if (topic.type !== "theme-settings") continue
268
- send(ws, createEnvelope(id, topic))
269
- }
270
- }
271
- })
272
-
273
- const disposeProviderSettingsEvents = providerSettings?.onChange(() => {
274
- for (const ws of sockets) {
275
- for (const [id, topic] of ws.data.subscriptions.entries()) {
276
- if (topic.type !== "provider-settings") continue
277
- send(ws, createEnvelope(id, topic))
278
- }
279
- }
280
- broadcastSnapshots()
281
- }) ?? (() => {})
282
-
283
- const disposeUpdateEvents = updateManager?.onChange(() => {
284
- for (const ws of sockets) {
285
- for (const [id, topic] of ws.data.subscriptions.entries()) {
286
- if (topic.type !== "update") continue
287
- send(ws, createEnvelope(id, topic))
288
- }
289
- }
290
- }) ?? (() => {})
291
-
292
- async function runProviderUsagePoll() {
293
- if (!hasSidebarSubscribers()) return
294
- if (!providerUsageRefreshInFlight) {
295
- providerUsageRefreshInFlight = refreshProviderUsage().finally(() => {
296
- providerUsageRefreshInFlight = null
297
- })
298
- }
299
- await providerUsageRefreshInFlight
300
- broadcastSidebarSnapshots()
301
- }
302
-
303
- function nextProviderUsagePollDelayMs() {
304
- if (typeof providerUsagePollIntervalMs === "number") {
305
- return providerUsagePollIntervalMs
306
- }
307
-
308
- return Math.floor(Math.random() * (PROVIDER_USAGE_POLL_MAX_INTERVAL_MS - PROVIDER_USAGE_POLL_INTERVAL_MS + 1))
309
- + PROVIDER_USAGE_POLL_INTERVAL_MS
310
- }
311
-
312
- function scheduleProviderUsagePoll() {
313
- if (providerUsagePollTimer) {
314
- clearTimeout(providerUsagePollTimer)
315
- }
316
-
317
- providerUsagePollTimer = setTimeout(() => {
318
- void runProviderUsagePoll().finally(() => {
319
- scheduleProviderUsagePoll()
320
- })
321
- }, nextProviderUsagePollDelayMs())
322
- }
323
-
324
- scheduleProviderUsagePoll()
325
-
326
- async function handleCommand(ws: ServerWebSocket<ClientState>, message: Extract<ClientEnvelope, { type: "command" }>) {
327
- const { command, id } = message
328
- try {
329
- switch (command.type) {
330
- case "system.ping": {
331
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
332
- return
333
- }
334
- case "system.listDirectory": {
335
- const directory = await listProjectDirectories(command.localPath)
336
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: directory })
337
- return
338
- }
339
- case "update.check": {
340
- const snapshot = updateManager
341
- ? await updateManager.checkForUpdates({ force: command.force })
342
- : {
343
- currentVersion: "unknown",
344
- latestVersion: null,
345
- status: "error",
346
- updateAvailable: false,
347
- lastCheckedAt: Date.now(),
348
- error: "Update manager unavailable.",
349
- installAction: "restart",
350
- }
351
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
352
- return
353
- }
354
- case "update.install": {
355
- if (!updateManager) {
356
- throw new Error("Update manager unavailable.")
357
- }
358
- const result = await updateManager.installUpdate()
359
- send(ws, {
360
- v: PROTOCOL_VERSION,
361
- type: "ack",
362
- id,
363
- result,
364
- })
365
- return
366
- }
367
- case "settings.readKeybindings": {
368
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: keybindings.getSnapshot() })
369
- return
370
- }
371
- case "settings.writeKeybindings": {
372
- const snapshot = await keybindings.write(command.bindings)
373
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
374
- return
375
- }
376
- case "settings.writeThemeSettings": {
377
- const snapshot = await themeSettings.write(command.settings)
378
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
379
- return
380
- }
381
- case "settings.writeProviderSettings": {
382
- if (!providerSettings) throw new Error("Provider settings unavailable.")
383
- const snapshot = await providerSettings.write(command.settings)
384
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
385
- return
386
- }
387
- case "project.open": {
388
- await requireProjectDirectory(command.localPath)
389
- await store.unhideProject(command.localPath)
390
- const project = await store.openProject(command.localPath)
391
- const imported = await importProjectHistory({
392
- store,
393
- projectId: project.id,
394
- repoKey: project.repoKey,
395
- localPath: project.localPath,
396
- worktreePaths: project.worktreePaths,
397
- })
398
- await store.reconcileProjectFeatureState(project.id)
399
- await refreshDiscovery()
400
- send(ws, {
401
- v: PROTOCOL_VERSION,
402
- type: "ack",
403
- id,
404
- result: {
405
- projectId: project.id,
406
- chatId: imported.newestChatId,
407
- importedChats: imported.importedChats,
408
- },
409
- })
410
- break
411
- }
412
- case "project.create": {
413
- await ensureProjectDirectory(command.localPath)
414
- await store.unhideProject(command.localPath)
415
- const project = await store.openProject(command.localPath, command.title)
416
- const imported = await importProjectHistory({
417
- store,
418
- projectId: project.id,
419
- repoKey: project.repoKey,
420
- localPath: project.localPath,
421
- worktreePaths: project.worktreePaths,
422
- })
423
- await store.reconcileProjectFeatureState(project.id)
424
- await refreshDiscovery()
425
- send(ws, {
426
- v: PROTOCOL_VERSION,
427
- type: "ack",
428
- id,
429
- result: {
430
- projectId: project.id,
431
- chatId: imported.newestChatId,
432
- importedChats: imported.importedChats,
433
- },
434
- })
435
- break
436
- }
437
- case "project.remove": {
438
- const project = store.getProject(command.projectId)
439
- for (const chat of store.listChatsByProject(command.projectId)) {
440
- await agent.cancel(chat.id)
441
- }
442
- if (project) {
443
- for (const worktreePath of project.worktreePaths) {
444
- terminals.closeByCwd(worktreePath)
445
- }
446
- await store.hideProject(project.localPath)
447
- }
448
- await refreshDiscovery()
449
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
450
- break
451
- }
452
- case "project.setBrowserState": {
453
- await store.setProjectBrowserState(command.projectId, command.browserState)
454
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
455
- break
456
- }
457
- case "project.setGeneralChatsBrowserState": {
458
- await store.setProjectGeneralChatsBrowserState(command.projectId, command.browserState)
459
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
460
- break
461
- }
462
- case "project.hide": {
463
- const existingProject = store.listProjects().find((project) => project.localPath === command.localPath)
464
- if (existingProject) {
465
- for (const chat of store.listChatsByProject(existingProject.id)) {
466
- await agent.cancel(chat.id)
467
- }
468
- for (const worktreePath of existingProject.worktreePaths) {
469
- terminals.closeByCwd(worktreePath)
470
- }
471
- }
472
- await store.hideProject(command.localPath)
473
- await refreshDiscovery()
474
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
475
- break
476
- }
477
- case "project.setProjectMetadataDirectoryCommitMode": {
478
- const localPath = command.projectId
479
- ? store.getProject(command.projectId)?.localPath
480
- : command.localPath
481
- if (!localPath) {
482
- throw new Error("Project not found")
483
- }
484
- await git.setProjectMetadataDirectoryCommitMode(localPath, command.commitProjectMetadata)
485
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
486
- break
487
- }
488
- case "system.openExternal": {
489
- await openExternal(command)
490
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
491
- break
492
- }
493
- case "system.openUrl": {
494
- await openUrlCommand(command)
495
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
496
- break
497
- }
498
- case "provider.refreshUsage": {
499
- if (command.provider && !isProviderSelectable(command.provider, providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS)) {
500
- throw new Error(`${command.provider} is inactive.`)
501
- }
502
- await refreshProviderUsage(command.provider, true)
503
- broadcastSidebarSnapshots()
504
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
505
- break
506
- }
507
- case "provider.browserLogin": {
508
- if (command.provider !== "cursor") {
509
- throw new Error(`Browser login is not supported for provider: ${command.provider}`)
510
- }
511
- const result = await signInToCursorWithBrowser(store.dataDir)
512
- broadcastSidebarSnapshots()
513
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
514
- break
515
- }
516
- case "provider.importUsageCurl": {
517
- if (command.provider !== "cursor") {
518
- throw new Error(`cURL import is not supported for provider: ${command.provider}`)
519
- }
520
- const result = await importCursorUsageFromCurl(store.dataDir, command.curlCommand)
521
- broadcastSidebarSnapshots()
522
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
523
- break
524
- }
525
- case "feature.create": {
526
- const feature = await store.createFeature(command.projectId, command.title, command.description ?? "")
527
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { featureId: feature.id } })
528
- break
529
- }
530
- case "feature.rename": {
531
- await store.renameFeature(command.featureId, command.title)
532
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
533
- break
534
- }
535
- case "feature.setBrowserState": {
536
- await store.setFeatureBrowserState(command.featureId, command.browserState)
537
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
538
- break
539
- }
540
- case "feature.setStage": {
541
- await store.setFeatureStage(command.featureId, command.stage)
542
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
543
- break
544
- }
545
- case "feature.reorder": {
546
- await store.reorderFeatures(command.projectId, command.orderedFeatureIds)
547
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
548
- break
549
- }
550
- case "feature.delete": {
551
- await store.deleteFeature(command.featureId)
552
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
553
- break
554
- }
555
- case "chat.create": {
556
- const chat = await store.createChat(command.projectId, command.featureId)
557
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
558
- break
559
- }
560
- case "chat.setFeature": {
561
- await store.setChatFeature(command.chatId, command.featureId)
562
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
563
- break
564
- }
565
- case "chat.rename": {
566
- await store.renameChat(command.chatId, command.title)
567
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
568
- break
569
- }
570
- case "chat.delete": {
571
- await agent.cancel(command.chatId)
572
- await store.deleteChat(command.chatId)
573
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
574
- break
575
- }
576
- case "chat.send": {
577
- if (command.provider && !isProviderSelectable(command.provider, providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS)) {
578
- throw new Error(`${command.provider} is inactive.`)
579
- }
580
- const result = await agent.send(command)
581
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
582
- break
583
- }
584
- case "chat.cancel": {
585
- await agent.cancel(command.chatId)
586
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
587
- break
588
- }
589
- case "chat.respondTool": {
590
- await agent.respondTool(command)
591
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
592
- break
593
- }
594
- case "terminal.create": {
595
- const project = store.getProject(command.projectId)
596
- if (!project) {
597
- throw new Error("Project not found")
598
- }
599
- const snapshot = terminals.createTerminal({
600
- projectPath: project.localPath,
601
- terminalId: command.terminalId,
602
- cols: command.cols,
603
- rows: command.rows,
604
- scrollback: command.scrollback,
605
- })
606
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
607
- return
608
- }
609
- case "terminal.input": {
610
- terminals.write(command.terminalId, command.data)
611
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
612
- return
613
- }
614
- case "terminal.resize": {
615
- terminals.resize(command.terminalId, command.cols, command.rows)
616
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
617
- return
618
- }
619
- case "terminal.close": {
620
- terminals.close(command.terminalId)
621
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
622
- pushTerminalSnapshot(command.terminalId)
623
- return
624
- }
625
- case "git.getBranches": {
626
- const project = store.getProject(command.projectId)
627
- if (!project) throw new Error("Project not found")
628
- const result = await git.getBranches(project.localPath)
629
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
630
- return
631
- }
632
- case "git.switchBranch": {
633
- const project = store.getProject(command.projectId)
634
- if (!project) throw new Error("Project not found")
635
- const result = await git.switchBranch(project.localPath, command.branchName)
636
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
637
- return
638
- }
639
- case "git.createBranch": {
640
- const project = store.getProject(command.projectId)
641
- if (!project) throw new Error("Project not found")
642
- const result = await git.createBranch(project.localPath, command.branchName, command.checkout)
643
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
644
- return
645
- }
646
- }
647
-
648
- broadcastSnapshots()
649
- } catch (error) {
650
- const messageText = error instanceof Error ? error.message : String(error)
651
- send(ws, { v: PROTOCOL_VERSION, type: "error", id, message: messageText })
652
- }
653
- }
654
-
655
- return {
656
- handleOpen(ws: ServerWebSocket<ClientState>) {
657
- sockets.add(ws)
658
- },
659
- handleClose(ws: ServerWebSocket<ClientState>) {
660
- sockets.delete(ws)
661
- },
662
- broadcastSnapshots,
663
- handleMessage(ws: ServerWebSocket<ClientState>, raw: string | Buffer | ArrayBuffer | Uint8Array) {
664
- let parsed: unknown
665
- try {
666
- parsed = JSON.parse(String(raw))
667
- } catch {
668
- send(ws, { v: PROTOCOL_VERSION, type: "error", message: "Invalid JSON" })
669
- return
670
- }
671
-
672
- if (!isClientEnvelope(parsed)) {
673
- send(ws, { v: PROTOCOL_VERSION, type: "error", message: "Invalid envelope" })
674
- return
675
- }
676
-
677
- if (parsed.type === "subscribe") {
678
- ws.data.subscriptions.set(parsed.id, parsed.topic)
679
- if (parsed.topic.type === "local-projects") {
680
- void refreshDiscovery().then(() => {
681
- if (ws.data.subscriptions.has(parsed.id)) {
682
- send(ws, createEnvelope(parsed.id, parsed.topic))
683
- }
684
- })
685
- }
686
- send(ws, createEnvelope(parsed.id, parsed.topic))
687
- return
688
- }
689
-
690
- if (parsed.type === "unsubscribe") {
691
- ws.data.subscriptions.delete(parsed.id)
692
- send(ws, { v: PROTOCOL_VERSION, type: "ack", id: parsed.id })
693
- return
694
- }
695
-
696
- void handleCommand(ws, parsed)
697
- },
698
- dispose() {
699
- if (providerUsagePollTimer) {
700
- clearTimeout(providerUsagePollTimer)
701
- }
702
- disposeTerminalEvents()
703
- disposeKeybindingEvents()
704
- disposeProviderSettingsEvents()
705
- disposeThemeSettingsEvents()
706
- disposeUpdateEvents()
707
- },
708
- }
709
- }