saeeol 1.2.1 → 1.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 (151) hide show
  1. package/bin/saeeol.cjs +187 -0
  2. package/npm/bin/saeeol +0 -0
  3. package/package.json +12 -12
  4. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  20. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  21. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  22. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  23. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  24. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  25. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  26. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  27. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  28. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  29. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  30. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  31. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  32. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  33. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  34. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  35. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  36. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  37. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  38. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  39. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  40. package/src/cli/cmd/tui/context/app/args.tsx +15 -0
  41. package/src/cli/cmd/tui/context/app/directory.ts +15 -0
  42. package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
  43. package/src/cli/cmd/tui/context/app/editor.ts +425 -0
  44. package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
  45. package/src/cli/cmd/tui/context/app/project.tsx +109 -0
  46. package/src/cli/cmd/tui/context/app/route.tsx +67 -0
  47. package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
  48. package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
  49. package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
  50. package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
  51. package/src/cli/cmd/tui/context/args.tsx +1 -15
  52. package/src/cli/cmd/tui/context/directory.ts +1 -15
  53. package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
  54. package/src/cli/cmd/tui/context/editor.ts +1 -425
  55. package/src/cli/cmd/tui/context/event.ts +1 -45
  56. package/src/cli/cmd/tui/context/exit.tsx +1 -67
  57. package/src/cli/cmd/tui/context/helper.tsx +1 -25
  58. package/src/cli/cmd/tui/context/keybind.tsx +1 -105
  59. package/src/cli/cmd/tui/context/kv.tsx +1 -76
  60. package/src/cli/cmd/tui/context/local.tsx +1 -478
  61. package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
  62. package/src/cli/cmd/tui/context/project.tsx +1 -109
  63. package/src/cli/cmd/tui/context/prompt.tsx +1 -18
  64. package/src/cli/cmd/tui/context/route.tsx +1 -67
  65. package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
  66. package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
  67. package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
  68. package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
  69. package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
  70. package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
  71. package/src/cli/cmd/tui/context/sdk.tsx +1 -142
  72. package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
  73. package/src/cli/cmd/tui/context/sync.tsx +1 -713
  74. package/src/cli/cmd/tui/context/theme.tsx +1 -307
  75. package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
  76. package/src/tool/apply_patch.ts +1 -334
  77. package/src/tool/bash.ts +1 -656
  78. package/src/tool/core/external-directory.ts +55 -0
  79. package/src/tool/core/invalid.ts +21 -0
  80. package/src/tool/core/recall.ts +164 -0
  81. package/src/tool/core/recall.txt +12 -0
  82. package/src/tool/core/schema.ts +16 -0
  83. package/src/tool/core/tool.ts +162 -0
  84. package/src/tool/core/truncate.ts +160 -0
  85. package/src/tool/core/truncation-dir.ts +4 -0
  86. package/src/tool/diagnostics.ts +1 -20
  87. package/src/tool/edit-replacers.ts +1 -288
  88. package/src/tool/edit-utils.ts +1 -86
  89. package/src/tool/edit.ts +1 -262
  90. package/src/tool/external-directory.ts +1 -55
  91. package/src/tool/file/apply_patch.ts +334 -0
  92. package/src/tool/file/apply_patch.txt +33 -0
  93. package/src/tool/file/bash.ts +656 -0
  94. package/src/tool/file/bash.txt +119 -0
  95. package/src/tool/file/edit-replacers.ts +288 -0
  96. package/src/tool/file/edit-utils.ts +86 -0
  97. package/src/tool/file/edit.ts +262 -0
  98. package/src/tool/file/edit.txt +10 -0
  99. package/src/tool/file/read.ts +389 -0
  100. package/src/tool/file/read.txt +14 -0
  101. package/src/tool/file/write.ts +114 -0
  102. package/src/tool/file/write.txt +8 -0
  103. package/src/tool/glob.ts +1 -115
  104. package/src/tool/grep.ts +1 -151
  105. package/src/tool/integration/diagnostics.ts +20 -0
  106. package/src/tool/integration/lsp.ts +113 -0
  107. package/src/tool/integration/lsp.txt +24 -0
  108. package/src/tool/integration/mcp-exa.ts +73 -0
  109. package/src/tool/integration/package.ts +168 -0
  110. package/src/tool/integration/registry.ts +375 -0
  111. package/src/tool/invalid.ts +1 -21
  112. package/src/tool/lsp.ts +1 -113
  113. package/src/tool/mcp-exa.ts +1 -73
  114. package/src/tool/package.ts +1 -168
  115. package/src/tool/plan.ts +1 -30
  116. package/src/tool/question.ts +1 -52
  117. package/src/tool/read.ts +1 -389
  118. package/src/tool/recall.ts +1 -164
  119. package/src/tool/registry.ts +1 -375
  120. package/src/tool/schema.ts +1 -16
  121. package/src/tool/search/glob.ts +115 -0
  122. package/src/tool/search/glob.txt +6 -0
  123. package/src/tool/search/grep.ts +151 -0
  124. package/src/tool/search/grep.txt +8 -0
  125. package/src/tool/search/warpgrep.ts +107 -0
  126. package/src/tool/search/warpgrep.txt +10 -0
  127. package/src/tool/search/webfetch.ts +202 -0
  128. package/src/tool/search/webfetch.txt +13 -0
  129. package/src/tool/search/websearch.ts +71 -0
  130. package/src/tool/search/websearch.txt +14 -0
  131. package/src/tool/skill.ts +1 -91
  132. package/src/tool/task.ts +1 -197
  133. package/src/tool/todo.ts +1 -62
  134. package/src/tool/tool.ts +1 -162
  135. package/src/tool/truncate.ts +1 -160
  136. package/src/tool/truncation-dir.ts +1 -4
  137. package/src/tool/warpgrep.ts +1 -107
  138. package/src/tool/webfetch.ts +1 -202
  139. package/src/tool/websearch.ts +1 -71
  140. package/src/tool/workflow/plan-enter.txt +14 -0
  141. package/src/tool/workflow/plan-exit.txt +13 -0
  142. package/src/tool/workflow/plan.ts +30 -0
  143. package/src/tool/workflow/question.ts +52 -0
  144. package/src/tool/workflow/question.txt +11 -0
  145. package/src/tool/workflow/skill.ts +91 -0
  146. package/src/tool/workflow/skill.txt +5 -0
  147. package/src/tool/workflow/task.ts +197 -0
  148. package/src/tool/workflow/task.txt +57 -0
  149. package/src/tool/workflow/todo.ts +62 -0
  150. package/src/tool/workflow/todowrite.txt +167 -0
  151. package/src/tool/write.ts +1 -114
@@ -0,0 +1,713 @@
1
+ import type {
2
+ Message,
3
+ Agent,
4
+ Provider,
5
+ Session,
6
+ Part,
7
+ Config,
8
+ Todo,
9
+ Command,
10
+ PermissionRequest,
11
+ QuestionRequest,
12
+ SuggestionRequest,
13
+ SessionNetworkWait,
14
+ LspStatus,
15
+ McpStatus,
16
+ McpResource,
17
+ FormatterStatus,
18
+ SessionStatus,
19
+ ProviderListResponse,
20
+ ProviderAuthMethod,
21
+ VcsInfo,
22
+ } from "@saeeol/sdk/v2"
23
+ import { createStore, produce, reconcile } from "solid-js/store"
24
+ import { useProject } from "@tui/context/project"
25
+ import { useEvent } from "@tui/context/event"
26
+ import { useSDK } from "@tui/context/sdk"
27
+ import { Binary } from "@saeeol/core/util/binary"
28
+ import { createSimpleContext } from "./helper"
29
+ import type { Snapshot } from "@/snapshot"
30
+ import { useExit } from "../runtime/exit"
31
+ import { useArgs } from "./args"
32
+ import { batch, createEffect, on, onMount } from "solid-js"
33
+ import { handleSuggestionEvent } from "@/saeeol/suggestion/tui/sync"
34
+ import { useToast } from "@tui/ui/toast"
35
+ import * as Log from "@saeeol/core/util/log"
36
+ import { emptyConsoleState, type ConsoleState } from "@/config/console-state"
37
+ import type { IndexingStatus } from "@saeeol/indexing/status"
38
+ import { SaeeolIndexing } from "@/saeeol/indexing"
39
+ import path from "path"
40
+ import { useKV } from "../runtime/kv"
41
+
42
+ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
43
+ name: "Sync",
44
+ init: () => {
45
+ const [store, setStore] = createStore<{
46
+ status: "loading" | "partial" | "complete"
47
+ provider: Provider[]
48
+ provider_default: Record<string, string>
49
+ provider_next: ProviderListResponse
50
+ console_state: ConsoleState
51
+ provider_auth: Record<string, ProviderAuthMethod[]>
52
+ agent: Agent[]
53
+ command: Command[]
54
+ permission: {
55
+ [sessionID: string]: PermissionRequest[]
56
+ }
57
+ question: {
58
+ [sessionID: string]: QuestionRequest[]
59
+ }
60
+ suggestion: {
61
+ [sessionID: string]: SuggestionRequest[]
62
+ }
63
+ network: {
64
+ [sessionID: string]: SessionNetworkWait[]
65
+ }
66
+ config: Config
67
+ globalConfig: Config
68
+ session: Session[]
69
+ session_status: {
70
+ [sessionID: string]: SessionStatus
71
+ }
72
+ session_diff: {
73
+ [sessionID: string]: Omit<Snapshot.FileDiff, "before" | "after">[]
74
+ }
75
+ todo: {
76
+ [sessionID: string]: Todo[]
77
+ }
78
+ message: {
79
+ [sessionID: string]: Message[]
80
+ }
81
+ part: {
82
+ [messageID: string]: Part[]
83
+ }
84
+ lsp: LspStatus[]
85
+ mcp: {
86
+ [key: string]: McpStatus
87
+ }
88
+ mcp_resource: {
89
+ [key: string]: McpResource
90
+ }
91
+ formatter: FormatterStatus[]
92
+ vcs: VcsInfo | undefined
93
+ indexing: IndexingStatus
94
+ }>({
95
+ provider_next: {
96
+ all: [],
97
+ default: {},
98
+ connected: [],
99
+ failed: [],
100
+ },
101
+ console_state: emptyConsoleState,
102
+ provider_auth: {},
103
+ config: {},
104
+ globalConfig: {},
105
+ status: "loading",
106
+ agent: [],
107
+ permission: {},
108
+ question: {},
109
+ suggestion: {},
110
+ network: {},
111
+ command: [],
112
+ provider: [],
113
+ provider_default: {},
114
+ session: [],
115
+ session_status: {},
116
+ session_diff: {},
117
+ todo: {},
118
+ message: {},
119
+ part: {},
120
+ lsp: [],
121
+ mcp: {},
122
+ mcp_resource: {},
123
+ formatter: [],
124
+ vcs: undefined,
125
+ indexing: { state: "Disabled", message: "Indexing disabled.", processedFiles: 0, totalFiles: 0, percent: 0 },
126
+ })
127
+
128
+ const event = useEvent()
129
+ const project = useProject()
130
+ const sdk = useSDK()
131
+ const toast = useToast()
132
+ const kv = useKV()
133
+ function evict(sessionID: string) {
134
+ // Collect child session IDs so we can evict them too.
135
+ const children = store.session.filter((s) => s.parentID === sessionID).map((s) => s.id)
136
+ setStore(
137
+ produce((draft) => {
138
+ const messages = draft.message[sessionID]
139
+ if (messages) {
140
+ for (const msg of messages) delete draft.part[msg.id]
141
+ }
142
+ delete draft.message[sessionID]
143
+ delete draft.session_diff[sessionID]
144
+ delete draft.session_status[sessionID]
145
+ delete draft.todo[sessionID]
146
+ delete draft.permission[sessionID]
147
+ delete draft.question[sessionID]
148
+ delete draft.suggestion[sessionID]
149
+ delete draft.network[sessionID]
150
+ }),
151
+ )
152
+ fullSyncedSessions.delete(sessionID)
153
+ for (const child of children) evict(child)
154
+ }
155
+
156
+ // Strip summary.diffs from user messages — the TUI never reads them
157
+ // and they can carry multi-MB before/after file content strings.
158
+ function strip(msg: Message): Message {
159
+ if (msg.role !== "user" || !msg.summary?.diffs) return msg
160
+ return { ...msg, summary: { ...msg.summary, diffs: [] } } as Message
161
+ }
162
+
163
+ const fullSyncedSessions = new Set<string>()
164
+ let syncedWorkspace = project.workspace.current()
165
+
166
+ function sessionListQuery(): { scope?: "project"; path?: string } {
167
+ if (!kv.get("session_directory_filter_enabled", true)) return { scope: "project" }
168
+ if (!project.data.instance.path.worktree || !project.data.instance.path.directory) return { scope: "project" }
169
+ return {
170
+ path: path
171
+ .relative(path.resolve(project.data.instance.path.worktree), project.data.instance.path.directory)
172
+ .replaceAll("\\", "/"),
173
+ }
174
+ }
175
+
176
+ function listSessions() {
177
+ return sdk.client.session
178
+ .list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, ...sessionListQuery() })
179
+ .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
180
+ }
181
+
182
+ event.subscribe((event) => {
183
+ switch (event.type) {
184
+ case "server.instance.disposed":
185
+ void bootstrap()
186
+ break
187
+ case "permission.replied": {
188
+ const requests = store.permission[event.properties.sessionID]
189
+ if (!requests) break
190
+ const match = Binary.search(requests, event.properties.requestID, (r) => r.id)
191
+ if (!match.found) break
192
+ setStore(
193
+ "permission",
194
+ event.properties.sessionID,
195
+ produce((draft) => {
196
+ draft.splice(match.index, 1)
197
+ }),
198
+ )
199
+ break
200
+ }
201
+
202
+ case "permission.asked": {
203
+ const request = event.properties
204
+ const requests = store.permission[request.sessionID]
205
+ if (!requests) {
206
+ setStore("permission", request.sessionID, [request])
207
+ break
208
+ }
209
+ const match = Binary.search(requests, request.id, (r) => r.id)
210
+ if (match.found) {
211
+ setStore("permission", request.sessionID, match.index, reconcile(request))
212
+ break
213
+ }
214
+ setStore(
215
+ "permission",
216
+ request.sessionID,
217
+ produce((draft) => {
218
+ draft.splice(match.index, 0, request)
219
+ }),
220
+ )
221
+ break
222
+ }
223
+
224
+ case "question.replied":
225
+ case "question.rejected": {
226
+ const requests = store.question[event.properties.sessionID]
227
+ if (!requests) break
228
+ const match = Binary.search(requests, event.properties.requestID, (r) => r.id)
229
+ if (!match.found) break
230
+ setStore(
231
+ "question",
232
+ event.properties.sessionID,
233
+ produce((draft) => {
234
+ draft.splice(match.index, 1)
235
+ }),
236
+ )
237
+ break
238
+ }
239
+
240
+ case "question.asked": {
241
+ const request = event.properties
242
+ const requests = store.question[request.sessionID]
243
+ if (!requests) {
244
+ setStore("question", request.sessionID, [request])
245
+ break
246
+ }
247
+ const match = Binary.search(requests, request.id, (r) => r.id)
248
+ if (match.found) {
249
+ setStore("question", request.sessionID, match.index, reconcile(request))
250
+ break
251
+ }
252
+ setStore(
253
+ "question",
254
+ request.sessionID,
255
+ produce((draft) => {
256
+ draft.splice(match.index, 0, request)
257
+ }),
258
+ )
259
+ break
260
+ }
261
+ case "session.network.replied":
262
+ case "session.network.rejected": {
263
+ const requests = store.network[event.properties.sessionID]
264
+ if (!requests) break
265
+ const match = Binary.search(requests, event.properties.requestID, (r) => r.id)
266
+ if (!match.found) break
267
+ setStore(
268
+ "network",
269
+ event.properties.sessionID,
270
+ produce((draft) => {
271
+ draft.splice(match.index, 1)
272
+ }),
273
+ )
274
+ break
275
+ }
276
+ case "suggestion.accepted":
277
+ case "suggestion.dismissed":
278
+ case "suggestion.shown": {
279
+ handleSuggestionEvent(event, store, setStore)
280
+ break
281
+ }
282
+
283
+ case "session.network.restored": {
284
+ const requests = store.network[event.properties.sessionID]
285
+ if (!requests) break
286
+ const match = Binary.search(requests, event.properties.requestID, (r) => r.id)
287
+ if (match.found) {
288
+ setStore("network", event.properties.sessionID, match.index, "restored", true)
289
+ setStore("network", event.properties.sessionID, match.index, "time", "restored", event.properties.time)
290
+ }
291
+ break
292
+ }
293
+
294
+ case "session.network.asked": {
295
+ const request = event.properties
296
+ const requests = store.network[request.sessionID]
297
+ if (!requests) {
298
+ setStore("network", request.sessionID, [request])
299
+ break
300
+ }
301
+ const match = Binary.search(requests, request.id, (r) => r.id)
302
+ if (match.found) {
303
+ setStore("network", request.sessionID, match.index, reconcile(request))
304
+ break
305
+ }
306
+ setStore(
307
+ "network",
308
+ request.sessionID,
309
+ produce((draft) => {
310
+ draft.splice(match.index, 0, request)
311
+ }),
312
+ )
313
+ break
314
+ }
315
+ case "todo.updated":
316
+ setStore("todo", event.properties.sessionID, event.properties.todos)
317
+ break
318
+
319
+ case "session.diff":
320
+ setStore("session_diff", event.properties.sessionID, event.properties.diff)
321
+ break
322
+ case "session.deleted": {
323
+ const sid = event.properties.info.id
324
+ const match = Binary.search(store.session, sid, (s) => s.id)
325
+ if (match.found) {
326
+ setStore(
327
+ "session",
328
+ produce((draft) => {
329
+ draft.splice(match.index, 1)
330
+ }),
331
+ )
332
+ }
333
+ evict(sid)
334
+ break
335
+ }
336
+ case "session.updated": {
337
+ const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
338
+ if (result.found) {
339
+ setStore("session", result.index, reconcile(event.properties.info))
340
+ break
341
+ }
342
+ setStore(
343
+ "session",
344
+ produce((draft) => {
345
+ draft.splice(result.index, 0, event.properties.info)
346
+ }),
347
+ )
348
+ break
349
+ }
350
+
351
+ case "session.status": {
352
+ setStore("session_status", event.properties.sessionID, event.properties.status)
353
+ break
354
+ }
355
+ case "message.updated": {
356
+ const info = strip(event.properties.info)
357
+ const messages = store.message[info.sessionID]
358
+ if (!messages) {
359
+ setStore("message", info.sessionID, [info])
360
+ break
361
+ }
362
+ const result = Binary.search(messages, info.id, (m) => m.id)
363
+ if (result.found) {
364
+ setStore("message", info.sessionID, result.index, reconcile(info))
365
+ break
366
+ }
367
+ setStore(
368
+ "message",
369
+ info.sessionID,
370
+ produce((draft) => {
371
+ draft.splice(result.index, 0, info)
372
+ }),
373
+ )
374
+ const updated = store.message[info.sessionID]
375
+ if (updated.length > 100) {
376
+ const oldest = updated[0]
377
+ batch(() => {
378
+ setStore(
379
+ "message",
380
+ info.sessionID,
381
+ produce((draft) => {
382
+ draft.shift()
383
+ }),
384
+ )
385
+ setStore(
386
+ "part",
387
+ produce((draft) => {
388
+ delete draft[oldest.id]
389
+ }),
390
+ )
391
+ })
392
+ }
393
+ break
394
+ }
395
+ case "message.removed": {
396
+ const messages = store.message[event.properties.sessionID]
397
+ const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
398
+ if (result.found) {
399
+ setStore(
400
+ "message",
401
+ event.properties.sessionID,
402
+ produce((draft) => {
403
+ draft.splice(result.index, 1)
404
+ }),
405
+ )
406
+ }
407
+ break
408
+ }
409
+ case "message.part.updated": {
410
+ const parts = store.part[event.properties.part.messageID]
411
+ if (!parts) {
412
+ setStore("part", event.properties.part.messageID, [event.properties.part])
413
+ break
414
+ }
415
+ const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
416
+ if (result.found) {
417
+ setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
418
+ break
419
+ }
420
+ setStore(
421
+ "part",
422
+ event.properties.part.messageID,
423
+ produce((draft) => {
424
+ draft.splice(result.index, 0, event.properties.part)
425
+ }),
426
+ )
427
+ break
428
+ }
429
+
430
+ case "message.part.delta": {
431
+ const parts = store.part[event.properties.messageID]
432
+ if (!parts) break
433
+ const result = Binary.search(parts, event.properties.partID, (p) => p.id)
434
+ if (!result.found) break
435
+ setStore(
436
+ "part",
437
+ event.properties.messageID,
438
+ produce((draft) => {
439
+ const part = draft[result.index]
440
+ const field = event.properties.field as keyof typeof part
441
+ const existing = part[field] as string | undefined
442
+ ;(part[field] as string) = (existing ?? "") + event.properties.delta
443
+ }),
444
+ )
445
+ break
446
+ }
447
+
448
+ case "message.part.removed": {
449
+ const parts = store.part[event.properties.messageID]
450
+ const result = Binary.search(parts, event.properties.partID, (p) => p.id)
451
+ if (result.found)
452
+ setStore(
453
+ "part",
454
+ event.properties.messageID,
455
+ produce((draft) => {
456
+ draft.splice(result.index, 1)
457
+ }),
458
+ )
459
+ break
460
+ }
461
+
462
+ case "lsp.updated": {
463
+ const workspace = project.workspace.current()
464
+ void sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data ?? []))
465
+ break
466
+ }
467
+
468
+ case "mcp.tools.changed": {
469
+ const workspace = project.workspace.current()
470
+ void sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data ?? {})))
471
+ break
472
+ }
473
+
474
+ case "vcs.branch.updated": {
475
+ setStore("vcs", { branch: event.properties.branch })
476
+ break
477
+ }
478
+ case "global.config.updated": {
479
+ sdk.client.global.config.get().then((x) => {
480
+ if (x.data) setStore("globalConfig", reconcile(x.data))
481
+ })
482
+ sdk.client.config.get().then((x) => {
483
+ if (x.data) setStore("config", reconcile(x.data))
484
+ })
485
+ break
486
+ }
487
+ case "indexing.status": {
488
+ setStore("indexing", reconcile(event.properties.status))
489
+ break
490
+ }
491
+ }
492
+ })
493
+
494
+ const exit = useExit()
495
+ const args = useArgs()
496
+
497
+ async function bootstrap(input: { fatal?: boolean } = {}) {
498
+ const fatal = input.fatal ?? true
499
+ const workspace = project.workspace.current()
500
+ if (workspace !== syncedWorkspace) {
501
+ fullSyncedSessions.clear()
502
+ syncedWorkspace = workspace
503
+ }
504
+ const projectPromise = project.sync()
505
+ const sessionListPromise = projectPromise.then(() => listSessions())
506
+
507
+ // blocking - include session.list when continuing a session
508
+ const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true })
509
+ const providerListPromise = sdk.client.provider.list({ workspace }, { throwOnError: true })
510
+ const consoleStatePromise = sdk.client.experimental.console
511
+ .get({ workspace }, { throwOnError: true })
512
+ .then((x) => x.data)
513
+ .catch(() => emptyConsoleState)
514
+ const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
515
+ const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
516
+ const globalConfigPromise = sdk.client.global.config.get({ throwOnError: true })
517
+ const blockingRequests: Promise<unknown>[] = [
518
+ providersPromise,
519
+ providerListPromise,
520
+ agentsPromise,
521
+ configPromise,
522
+ globalConfigPromise,
523
+ projectPromise,
524
+ ...(args.continue ? [sessionListPromise] : []),
525
+ ]
526
+
527
+ await Promise.all(blockingRequests)
528
+ .then(async () => {
529
+ const providersResponse = providersPromise.then((x) => x.data!)
530
+ const providerListResponse = providerListPromise.then((x) => x.data!)
531
+ const consoleStateResponse = consoleStatePromise
532
+ const agentsResponse = agentsPromise.then((x) => x.data ?? [])
533
+ const configResponse = configPromise.then((x) => x.data!)
534
+ const globalConfigResponse = globalConfigPromise.then((x) => x.data!)
535
+ const sessionListResponse = args.continue ? sessionListPromise : undefined
536
+
537
+ return Promise.all([
538
+ providersResponse,
539
+ providerListResponse,
540
+ consoleStateResponse,
541
+ agentsResponse,
542
+ configResponse,
543
+ globalConfigResponse,
544
+ ...(sessionListResponse ? [sessionListResponse] : []),
545
+ ]).then((responses) => {
546
+ const providers = responses[0]
547
+ const providerList = responses[1]
548
+ const consoleState = responses[2]
549
+ const agents = responses[3]
550
+ const config = responses[4]
551
+ const globalConfig = responses[5]
552
+ const sessions = responses[6]
553
+
554
+ batch(() => {
555
+ setStore("provider", reconcile(providers.providers))
556
+ setStore("provider_default", reconcile(providers.default))
557
+ setStore("provider_next", reconcile(providerList))
558
+ setStore("console_state", reconcile(consoleState))
559
+ setStore("agent", reconcile(agents))
560
+ setStore("config", reconcile(config))
561
+ setStore("globalConfig", reconcile(globalConfig))
562
+ if (sessions !== undefined) setStore("session", reconcile(sessions))
563
+ })
564
+ })
565
+ })
566
+ .then(() => {
567
+ if (store.status !== "complete") setStore("status", "partial")
568
+ // non-blocking
569
+ void Promise.all([
570
+ ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
571
+ consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
572
+ sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))),
573
+ sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", reconcile(x.data ?? []))),
574
+ sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data ?? {}))),
575
+ sdk.client.experimental.resource
576
+ .list({ workspace })
577
+ .then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
578
+ sdk.client.formatter.status({ workspace }).then((x) => setStore("formatter", reconcile(x.data!))),
579
+ sdk.client.network.list().then((x) => {
580
+ const next: Record<string, SessionNetworkWait[]> = {}
581
+ for (const item of x.data ?? []) {
582
+ if (!next[item.sessionID]) next[item.sessionID] = []
583
+ next[item.sessionID].push(item)
584
+ }
585
+ setStore("network", reconcile(next))
586
+ }),
587
+ sdk.client.session.status({ workspace }).then((x) => {
588
+ setStore("session_status", reconcile(x.data ?? {}))
589
+ }),
590
+ sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
591
+ sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))),
592
+ project.workspace.sync(),
593
+ sdk.client.config
594
+ .warnings()
595
+ .then((x) => {
596
+ const list = x.data ?? []
597
+ if (list.length === 0) return
598
+ const first = list[0]
599
+ const suffix = list.length > 1 ? ` (and ${list.length - 1} more)` : ""
600
+ toast.show({
601
+ title: "Config Warning",
602
+ message: first.message + suffix,
603
+ variant: "warning",
604
+ duration: 0,
605
+ })
606
+ })
607
+ .catch(() => {}),
608
+ SaeeolIndexing.current().then((x) => setStore("indexing", reconcile(x))),
609
+ ]).then(() => {
610
+ setStore("status", "complete")
611
+ // Re-fetch MCP status after bootstrap completes to catch
612
+ // servers that finished connecting during the initial load.
613
+ setTimeout(() => {
614
+ const ws = project.workspace.current()
615
+ void sdk.client.mcp.status({ workspace: ws }).then((x) => setStore("mcp", reconcile(x.data ?? {})))
616
+ }, 3000)
617
+ })
618
+ })
619
+ .catch(async (e) => {
620
+ Log.Default.error("tui bootstrap failed", {
621
+ error: e instanceof Error ? e.message : String(e),
622
+ name: e instanceof Error ? e.name : undefined,
623
+ stack: e instanceof Error ? e.stack : undefined,
624
+ })
625
+ if (fatal) {
626
+ await exit(e)
627
+ } else {
628
+ throw e
629
+ }
630
+ })
631
+ }
632
+
633
+ onMount(() => {
634
+ void bootstrap()
635
+ })
636
+ createEffect(
637
+ on(
638
+ () => project.workspace.current(),
639
+ () => {
640
+ fullSyncedSessions.clear()
641
+ void bootstrap()
642
+ },
643
+ { defer: true },
644
+ ),
645
+ )
646
+
647
+ const result = {
648
+ data: store,
649
+ set: setStore,
650
+ get status() {
651
+ return store.status
652
+ },
653
+ get ready() {
654
+ // return true
655
+ if (process.env.SAEEOL_FAST_BOOT) return true
656
+ return store.status !== "loading"
657
+ },
658
+ get path() {
659
+ return project.instance.path()
660
+ },
661
+ session: {
662
+ get(sessionID: string) {
663
+ const match = Binary.search(store.session, sessionID, (s) => s.id)
664
+ if (match.found) return store.session[match.index]
665
+ return undefined
666
+ },
667
+ query() {
668
+ return sessionListQuery()
669
+ },
670
+ async refresh() {
671
+ const list = await listSessions()
672
+ setStore("session", reconcile(list))
673
+ },
674
+ status(sessionID: string) {
675
+ const session = result.session.get(sessionID)
676
+ if (!session) return "idle"
677
+ if (session.time.compacting) return "compacting"
678
+ const messages = store.message[sessionID] ?? []
679
+ const last = messages.at(-1)
680
+ if (!last) return "idle"
681
+ if (last.role === "user") return "working"
682
+ return last.time.completed ? "idle" : "working"
683
+ },
684
+ async sync(sessionID: string) {
685
+ if (fullSyncedSessions.has(sessionID)) return
686
+ const [session, messages, todo, diff] = await Promise.all([
687
+ sdk.client.session.get({ sessionID }, { throwOnError: true }),
688
+ sdk.client.session.messages({ sessionID, limit: 100 }),
689
+ sdk.client.session.todo({ sessionID }),
690
+ sdk.client.session.diff({ sessionID }),
691
+ ])
692
+ setStore(
693
+ produce((draft) => {
694
+ const match = Binary.search(draft.session, sessionID, (s) => s.id)
695
+ if (match.found) draft.session[match.index] = session.data!
696
+ if (!match.found) draft.session.splice(match.index, 0, session.data!)
697
+ draft.todo[sessionID] = todo.data ?? []
698
+ draft.message[sessionID] = messages.data!.map((x) => strip(x.info))
699
+ for (const message of messages.data!) {
700
+ draft.part[message.info.id] = message.parts
701
+ }
702
+ draft.session_diff[sessionID] = diff.data ?? []
703
+ }),
704
+ )
705
+ fullSyncedSessions.add(sessionID)
706
+ },
707
+ evict,
708
+ },
709
+ bootstrap,
710
+ }
711
+ return result
712
+ },
713
+ })