@neurocode-ai/tui 1.18.8

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 (241) hide show
  1. package/bunfig.toml +4 -0
  2. package/package.json +72 -0
  3. package/src/app.tsx +1134 -0
  4. package/src/attention.ts +260 -0
  5. package/src/audio.d.ts +9 -0
  6. package/src/audio.ts +53 -0
  7. package/src/clipboard.ts +124 -0
  8. package/src/component/bg-pulse-render.ts +436 -0
  9. package/src/component/bg-pulse.tsx +99 -0
  10. package/src/component/command-palette.tsx +79 -0
  11. package/src/component/dialog-agent.tsx +31 -0
  12. package/src/component/dialog-console-org.tsx +135 -0
  13. package/src/component/dialog-debug.tsx +90 -0
  14. package/src/component/dialog-mcp.tsx +85 -0
  15. package/src/component/dialog-model.tsx +197 -0
  16. package/src/component/dialog-move-session.tsx +353 -0
  17. package/src/component/dialog-provider.tsx +469 -0
  18. package/src/component/dialog-retry-action.tsx +160 -0
  19. package/src/component/dialog-session-delete-failed.tsx +99 -0
  20. package/src/component/dialog-session-list.tsx +364 -0
  21. package/src/component/dialog-session-rename.tsx +31 -0
  22. package/src/component/dialog-skill.tsx +70 -0
  23. package/src/component/dialog-stash.tsx +87 -0
  24. package/src/component/dialog-status.tsx +168 -0
  25. package/src/component/dialog-tag.tsx +47 -0
  26. package/src/component/dialog-theme-list.tsx +50 -0
  27. package/src/component/dialog-variant.tsx +39 -0
  28. package/src/component/dialog-workspace-create.tsx +308 -0
  29. package/src/component/dialog-workspace-file-changes.tsx +144 -0
  30. package/src/component/dialog-workspace-list.tsx +112 -0
  31. package/src/component/dialog-workspace-unavailable.tsx +69 -0
  32. package/src/component/error-component.tsx +240 -0
  33. package/src/component/logo.tsx +61 -0
  34. package/src/component/plugin-route-missing.tsx +14 -0
  35. package/src/component/prompt/autocomplete.tsx +781 -0
  36. package/src/component/prompt/cwd.ts +0 -0
  37. package/src/component/prompt/frecency.tsx +1 -0
  38. package/src/component/prompt/history.tsx +1 -0
  39. package/src/component/prompt/index.tsx +1713 -0
  40. package/src/component/prompt/local-attachment.ts +48 -0
  41. package/src/component/prompt/move.tsx +205 -0
  42. package/src/component/prompt/stash.tsx +1 -0
  43. package/src/component/prompt/workspace.tsx +137 -0
  44. package/src/component/register-spinner.ts +6 -0
  45. package/src/component/spinner.tsx +26 -0
  46. package/src/component/startup-loading.tsx +63 -0
  47. package/src/component/todo-item.tsx +32 -0
  48. package/src/component/use-connected.tsx +12 -0
  49. package/src/component/workspace-label.tsx +19 -0
  50. package/src/config/index.tsx +129 -0
  51. package/src/config/keybind.ts +471 -0
  52. package/src/context/args.tsx +16 -0
  53. package/src/context/clipboard.tsx +18 -0
  54. package/src/context/data.tsx +569 -0
  55. package/src/context/directory.ts +17 -0
  56. package/src/context/editor.ts +408 -0
  57. package/src/context/epilogue.tsx +6 -0
  58. package/src/context/event.ts +36 -0
  59. package/src/context/exit.tsx +8 -0
  60. package/src/context/helper.tsx +26 -0
  61. package/src/context/kv.tsx +66 -0
  62. package/src/context/local.tsx +542 -0
  63. package/src/context/location.tsx +14 -0
  64. package/src/context/path-format.tsx +24 -0
  65. package/src/context/permission.tsx +26 -0
  66. package/src/context/project.tsx +115 -0
  67. package/src/context/prompt.tsx +18 -0
  68. package/src/context/route.tsx +60 -0
  69. package/src/context/runtime.tsx +62 -0
  70. package/src/context/sdk.tsx +151 -0
  71. package/src/context/sync.tsx +666 -0
  72. package/src/context/theme.tsx +332 -0
  73. package/src/context/thinking.ts +67 -0
  74. package/src/editor-zed.ts +286 -0
  75. package/src/editor.ts +101 -0
  76. package/src/feature-plugins/builtins.ts +36 -0
  77. package/src/feature-plugins/home/footer.tsx +100 -0
  78. package/src/feature-plugins/home/tips-view.tsx +287 -0
  79. package/src/feature-plugins/home/tips.tsx +59 -0
  80. package/src/feature-plugins/sidebar/context.tsx +65 -0
  81. package/src/feature-plugins/sidebar/files.tsx +70 -0
  82. package/src/feature-plugins/sidebar/footer.tsx +98 -0
  83. package/src/feature-plugins/sidebar/lsp.tsx +65 -0
  84. package/src/feature-plugins/sidebar/mcp.tsx +97 -0
  85. package/src/feature-plugins/sidebar/todo.tsx +49 -0
  86. package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
  87. package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
  88. package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
  89. package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
  90. package/src/feature-plugins/system/notifications.ts +94 -0
  91. package/src/feature-plugins/system/plugins.tsx +269 -0
  92. package/src/feature-plugins/system/which-key.tsx +608 -0
  93. package/src/index.tsx +1 -0
  94. package/src/keymap.tsx +290 -0
  95. package/src/logo.ts +11 -0
  96. package/src/parsers-config.ts +386 -0
  97. package/src/plugin/adapters.tsx +355 -0
  98. package/src/plugin/api.ts +52 -0
  99. package/src/plugin/command-shim.ts +109 -0
  100. package/src/plugin/runtime.tsx +81 -0
  101. package/src/plugin/slots.tsx +65 -0
  102. package/src/prompt/display.ts +48 -0
  103. package/src/prompt/frecency.tsx +80 -0
  104. package/src/prompt/history.tsx +111 -0
  105. package/src/prompt/part.ts +29 -0
  106. package/src/prompt/stash.tsx +89 -0
  107. package/src/prompt/traits.ts +29 -0
  108. package/src/routes/home/session-destination.tsx +41 -0
  109. package/src/routes/home.tsx +95 -0
  110. package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
  111. package/src/routes/session/dialog-message.tsx +109 -0
  112. package/src/routes/session/dialog-subagent.tsx +26 -0
  113. package/src/routes/session/dialog-timeline.tsx +47 -0
  114. package/src/routes/session/footer.tsx +91 -0
  115. package/src/routes/session/index.tsx +2710 -0
  116. package/src/routes/session/permission.tsx +718 -0
  117. package/src/routes/session/question.tsx +514 -0
  118. package/src/routes/session/sidebar.tsx +103 -0
  119. package/src/routes/session/subagent-footer.tsx +132 -0
  120. package/src/runtime.tsx +9 -0
  121. package/src/terminal-win32.ts +130 -0
  122. package/src/theme/assets/aura.json +69 -0
  123. package/src/theme/assets/ayu.json +80 -0
  124. package/src/theme/assets/carbonfox.json +248 -0
  125. package/src/theme/assets/catppuccin-frappe.json +230 -0
  126. package/src/theme/assets/catppuccin-macchiato.json +230 -0
  127. package/src/theme/assets/catppuccin.json +112 -0
  128. package/src/theme/assets/cobalt2.json +225 -0
  129. package/src/theme/assets/cursor.json +249 -0
  130. package/src/theme/assets/dracula.json +219 -0
  131. package/src/theme/assets/everforest.json +241 -0
  132. package/src/theme/assets/flexoki.json +237 -0
  133. package/src/theme/assets/github.json +233 -0
  134. package/src/theme/assets/gruvbox.json +242 -0
  135. package/src/theme/assets/kanagawa.json +77 -0
  136. package/src/theme/assets/lucent-orng.json +234 -0
  137. package/src/theme/assets/material.json +235 -0
  138. package/src/theme/assets/matrix.json +77 -0
  139. package/src/theme/assets/mercury.json +252 -0
  140. package/src/theme/assets/monokai.json +221 -0
  141. package/src/theme/assets/nightowl.json +221 -0
  142. package/src/theme/assets/nord.json +223 -0
  143. package/src/theme/assets/one-dark.json +84 -0
  144. package/src/theme/assets/opencode.json +245 -0
  145. package/src/theme/assets/orng.json +249 -0
  146. package/src/theme/assets/osaka-jade.json +93 -0
  147. package/src/theme/assets/palenight.json +222 -0
  148. package/src/theme/assets/rosepine.json +234 -0
  149. package/src/theme/assets/solarized.json +223 -0
  150. package/src/theme/assets/synthwave84.json +226 -0
  151. package/src/theme/assets/tokyonight.json +243 -0
  152. package/src/theme/assets/vercel.json +245 -0
  153. package/src/theme/assets/vesper.json +218 -0
  154. package/src/theme/assets/zenburn.json +223 -0
  155. package/src/theme/index.ts +1089 -0
  156. package/src/ui/border.ts +21 -0
  157. package/src/ui/dialog-alert.tsx +66 -0
  158. package/src/ui/dialog-confirm.tsx +108 -0
  159. package/src/ui/dialog-export-options.tsx +217 -0
  160. package/src/ui/dialog-help.tsx +40 -0
  161. package/src/ui/dialog-prompt.tsx +126 -0
  162. package/src/ui/dialog-select.tsx +790 -0
  163. package/src/ui/dialog.tsx +231 -0
  164. package/src/ui/link.tsx +34 -0
  165. package/src/ui/spinner.ts +368 -0
  166. package/src/ui/toast.tsx +102 -0
  167. package/src/util/collapse-tool-output.ts +19 -0
  168. package/src/util/error.ts +182 -0
  169. package/src/util/filetype.ts +130 -0
  170. package/src/util/format.ts +20 -0
  171. package/src/util/layout.ts +25 -0
  172. package/src/util/locale.ts +86 -0
  173. package/src/util/model.ts +28 -0
  174. package/src/util/path.ts +12 -0
  175. package/src/util/persistence.ts +33 -0
  176. package/src/util/presentation.ts +38 -0
  177. package/src/util/provider-origin.ts +7 -0
  178. package/src/util/record.ts +3 -0
  179. package/src/util/renderer.ts +7 -0
  180. package/src/util/revert-diff.ts +18 -0
  181. package/src/util/scroll.ts +27 -0
  182. package/src/util/selection.ts +79 -0
  183. package/src/util/session.ts +3 -0
  184. package/src/util/signal.ts +51 -0
  185. package/src/util/system.ts +20 -0
  186. package/src/util/tool-display.ts +13 -0
  187. package/src/util/transcript.ts +112 -0
  188. package/sst-env.d.ts +10 -0
  189. package/test/app-lifecycle.test.tsx +128 -0
  190. package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
  191. package/test/cli/cmd/tui/model-options.test.ts +32 -0
  192. package/test/cli/cmd/tui/notifications.test.ts +267 -0
  193. package/test/cli/cmd/tui/provider-options.test.ts +41 -0
  194. package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
  195. package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
  196. package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
  197. package/test/cli/cmd/tui/sync.test.tsx +65 -0
  198. package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
  199. package/test/cli/tui/data.test.tsx +486 -0
  200. package/test/cli/tui/dialog-prompt.test.tsx +147 -0
  201. package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
  202. package/test/cli/tui/diff-viewer.test.tsx +268 -0
  203. package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
  204. package/test/cli/tui/prompt-submit-race.test.ts +98 -0
  205. package/test/cli/tui/thinking.test.ts +36 -0
  206. package/test/cli/tui/use-event.test.tsx +148 -0
  207. package/test/clipboard.test.ts +19 -0
  208. package/test/component/dialog-session-list.test.ts +46 -0
  209. package/test/config.test.tsx +127 -0
  210. package/test/context/local.test.ts +22 -0
  211. package/test/editor.test.ts +32 -0
  212. package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
  213. package/test/fixture/fixture.ts +13 -0
  214. package/test/fixture/tui-environment.tsx +32 -0
  215. package/test/fixture/tui-plugin.ts +36 -0
  216. package/test/fixture/tui-runtime.ts +12 -0
  217. package/test/fixture/tui-sdk.ts +109 -0
  218. package/test/index.test.tsx +6 -0
  219. package/test/keymap.test.tsx +141 -0
  220. package/test/plugin/runtime.test.ts +50 -0
  221. package/test/plugin/slots.test.tsx +38 -0
  222. package/test/prompt/display.test.ts +33 -0
  223. package/test/prompt/history.test.ts +39 -0
  224. package/test/prompt/jsonl.test.ts +24 -0
  225. package/test/prompt/local-attachment.test.ts +43 -0
  226. package/test/prompt/part.test.ts +53 -0
  227. package/test/prompt/persistence.test.ts +23 -0
  228. package/test/prompt/traits.test.ts +25 -0
  229. package/test/runtime.test.tsx +37 -0
  230. package/test/theme.test.ts +81 -0
  231. package/test/util/error.test.ts +49 -0
  232. package/test/util/filetype.test.ts +16 -0
  233. package/test/util/format.test.ts +59 -0
  234. package/test/util/model.test.ts +9 -0
  235. package/test/util/presentation.test.ts +8 -0
  236. package/test/util/renderer.test.ts +30 -0
  237. package/test/util/revert-diff.test.ts +35 -0
  238. package/test/util/session.test.ts +10 -0
  239. package/test/util/tool-display.test.ts +40 -0
  240. package/test/util/transcript.test.ts +421 -0
  241. package/tsconfig.json +10 -0
@@ -0,0 +1,115 @@
1
+ import { batch } from "solid-js"
2
+ import type { Path, Workspace } from "@neurocode-ai/sdk/v2"
3
+ import { createStore, reconcile } from "solid-js/store"
4
+ import { createSimpleContext } from "./helper"
5
+ import { useSDK } from "./sdk"
6
+
7
+ type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
8
+
9
+ export const { use: useProject, provider: ProjectProvider } = createSimpleContext({
10
+ name: "Project",
11
+ init: () => {
12
+ const sdk = useSDK()
13
+
14
+ const defaultPath = {
15
+ home: "",
16
+ state: "",
17
+ config: "",
18
+ worktree: "",
19
+ directory: sdk.directory ?? "",
20
+ } satisfies Path
21
+
22
+ const [store, setStore] = createStore({
23
+ project: {
24
+ id: undefined as string | undefined,
25
+ worktree: undefined as string | undefined,
26
+ mainDir: undefined as string | undefined,
27
+ },
28
+ instance: {
29
+ path: defaultPath,
30
+ },
31
+ workspace: {
32
+ current: undefined as string | undefined,
33
+ list: [] as Workspace[],
34
+ status: {} as Record<string, WorkspaceStatus>,
35
+ },
36
+ })
37
+
38
+ async function sync() {
39
+ const workspace = store.workspace.current
40
+ const [instancePath, project] = await Promise.all([
41
+ sdk.client.path.get({ workspace }),
42
+ sdk.client.project.current({ workspace }),
43
+ ])
44
+ const directories = project.data?.id
45
+ ? await sdk.client.project.directories({ projectID: project.data.id, workspace })
46
+ : undefined
47
+ batch(() => {
48
+ setStore("instance", "path", reconcile(instancePath.data || defaultPath))
49
+ setStore("project", "id", project.data?.id)
50
+ setStore("project", "worktree", project.data?.worktree)
51
+ setStore("project", "mainDir", directories?.data?.findLast((item) => item.strategy === undefined)?.directory)
52
+ })
53
+ }
54
+
55
+ async function syncWorkspace() {
56
+ const listed = await sdk.client.experimental.workspace.list().catch(() => undefined)
57
+ if (!listed?.data) return
58
+ const status = await sdk.client.experimental.workspace.status().catch(() => undefined)
59
+ const next = Object.fromEntries((status?.data ?? []).map((item) => [item.workspaceID, item.status]))
60
+
61
+ batch(() => {
62
+ setStore("workspace", "list", reconcile(listed.data))
63
+ setStore("workspace", "status", reconcile(next))
64
+ if (!listed.data.some((item) => item.id === store.workspace.current)) {
65
+ setStore("workspace", "current", undefined)
66
+ }
67
+ })
68
+ }
69
+
70
+ sdk.event.on("event", (event) => {
71
+ if (event.payload.type === "workspace.status") {
72
+ setStore("workspace", "status", event.payload.properties.workspaceID, event.payload.properties.status)
73
+ }
74
+ })
75
+
76
+ return {
77
+ data: store,
78
+ project() {
79
+ return store.project.id
80
+ },
81
+ instance: {
82
+ path() {
83
+ return store.instance.path
84
+ },
85
+ directory() {
86
+ return store.instance.path.directory
87
+ },
88
+ },
89
+ workspace: {
90
+ current() {
91
+ return store.workspace.current
92
+ },
93
+ set(next?: string | null) {
94
+ const workspace = next ?? undefined
95
+ if (store.workspace.current === workspace) return
96
+ setStore("workspace", "current", workspace)
97
+ },
98
+ list() {
99
+ return store.workspace.list
100
+ },
101
+ get(workspaceID: string) {
102
+ return store.workspace.list.find((item) => item.id === workspaceID)
103
+ },
104
+ status(workspaceID: string) {
105
+ return store.workspace.status[workspaceID]
106
+ },
107
+ statuses() {
108
+ return store.workspace.status
109
+ },
110
+ sync: syncWorkspace,
111
+ },
112
+ sync,
113
+ }
114
+ },
115
+ })
@@ -0,0 +1,18 @@
1
+ import { createSimpleContext } from "./helper"
2
+ import type { PromptRef } from "../component/prompt"
3
+
4
+ export const { use: usePromptRef, provider: PromptRefProvider } = createSimpleContext({
5
+ name: "PromptRef",
6
+ init: () => {
7
+ let current: PromptRef | undefined
8
+
9
+ return {
10
+ get current() {
11
+ return current
12
+ },
13
+ set(ref: PromptRef | undefined) {
14
+ current = ref
15
+ },
16
+ }
17
+ },
18
+ })
@@ -0,0 +1,60 @@
1
+ import { createStore, reconcile } from "solid-js/store"
2
+ import { createSimpleContext } from "./helper"
3
+ import type { PromptInfo } from "../prompt/history"
4
+ import { useTuiStartup } from "./runtime"
5
+
6
+ export type HomeRoute = {
7
+ type: "home"
8
+ prompt?: PromptInfo
9
+ }
10
+
11
+ export type SessionRoute = {
12
+ type: "session"
13
+ sessionID: string
14
+ prompt?: PromptInfo
15
+ }
16
+
17
+ export type PluginRoute = {
18
+ type: "plugin"
19
+ id: string
20
+ data?: Record<string, unknown>
21
+ }
22
+
23
+ export type Route = HomeRoute | SessionRoute | PluginRoute
24
+
25
+ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
26
+ name: "Route",
27
+ init: (props: { initialRoute?: Route }) => {
28
+ const startup = useTuiStartup()
29
+ const [store, setStore] = createStore<Route>(
30
+ props.initialRoute ?? initialRoute(startup.initialRoute) ?? { type: "home" },
31
+ )
32
+
33
+ return {
34
+ get data() {
35
+ return store
36
+ },
37
+ navigate(route: Route) {
38
+ setStore(reconcile(route))
39
+ },
40
+ }
41
+ },
42
+ })
43
+
44
+ function initialRoute(value: unknown): Route | undefined {
45
+ if (!value || typeof value !== "object" || !("type" in value)) return
46
+ if (value.type === "home") return { type: "home" }
47
+ if (value.type === "session" && "sessionID" in value && typeof value.sessionID === "string") {
48
+ return { type: "session", sessionID: value.sessionID }
49
+ }
50
+ if (value.type === "plugin" && "id" in value && typeof value.id === "string") {
51
+ return { type: "plugin", id: value.id }
52
+ }
53
+ }
54
+
55
+ export type RouteContext = ReturnType<typeof useRoute>
56
+
57
+ export function useRouteData<T extends Route["type"]>(type: T) {
58
+ const route = useRoute()
59
+ return route.data as Extract<Route, { type: typeof type }>
60
+ }
@@ -0,0 +1,62 @@
1
+ import { createComponent, createContext, type JSX, useContext } from "solid-js"
2
+
3
+ export type TuiPaths = Readonly<{
4
+ cwd: string
5
+ home: string
6
+ state: string
7
+ worktree: string
8
+ }>
9
+
10
+ export type TuiTerminalEnvironment = Readonly<{
11
+ platform: string
12
+ multiplexer?: "tmux" | "screen"
13
+ displayServer?: "wayland" | "x11"
14
+ }>
15
+
16
+ export type TuiStartup = Readonly<{
17
+ initialRoute?: unknown
18
+ skipInitialLoading: boolean
19
+ }>
20
+
21
+ const PathsContext = createContext<TuiPaths>()
22
+ const TerminalEnvironmentContext = createContext<TuiTerminalEnvironment>()
23
+ const StartupContext = createContext<TuiStartup>()
24
+
25
+ function provider<T>(context: ReturnType<typeof createContext<T>>, value: T, children: () => JSX.Element) {
26
+ return createComponent(context.Provider, {
27
+ value: Object.freeze({ ...value }),
28
+ get children() {
29
+ return children()
30
+ },
31
+ })
32
+ }
33
+
34
+ export function TuiPathsProvider(props: { value: TuiPaths; children: JSX.Element }) {
35
+ return provider(PathsContext, props.value, () => props.children)
36
+ }
37
+
38
+ export function TuiTerminalEnvironmentProvider(props: { value: TuiTerminalEnvironment; children: JSX.Element }) {
39
+ return provider(TerminalEnvironmentContext, props.value, () => props.children)
40
+ }
41
+
42
+ export function TuiStartupProvider(props: { value: TuiStartup; children: JSX.Element }) {
43
+ return provider(StartupContext, props.value, () => props.children)
44
+ }
45
+
46
+ function required<T>(context: ReturnType<typeof createContext<T>>, name: string) {
47
+ const value = useContext(context)
48
+ if (!value) throw new Error(`${name} is missing`)
49
+ return value
50
+ }
51
+
52
+ export function useTuiPaths() {
53
+ return required(PathsContext, "TuiPathsProvider")
54
+ }
55
+
56
+ export function useTuiTerminalEnvironment() {
57
+ return required(TerminalEnvironmentContext, "TuiTerminalEnvironmentProvider")
58
+ }
59
+
60
+ export function useTuiStartup() {
61
+ return required(StartupContext, "TuiStartupProvider")
62
+ }
@@ -0,0 +1,151 @@
1
+ import { createNeurocodeClient } from "@neurocode-ai/sdk/v2"
2
+ import type { GlobalEvent } from "@neurocode-ai/sdk/v2"
3
+ import { Flag } from "@neurocode-ai/core/flag/flag"
4
+ import { createSimpleContext } from "./helper"
5
+ import { batch, onCleanup, onMount } from "solid-js"
6
+
7
+ export type EventSource = {
8
+ subscribe: (handler: (event: GlobalEvent) => void) => Promise<() => void>
9
+ }
10
+
11
+ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
12
+ name: "SDK",
13
+ init: (props: {
14
+ url: string
15
+ directory?: string
16
+ fetch?: typeof fetch
17
+ headers?: RequestInit["headers"]
18
+ events?: EventSource
19
+ }) => {
20
+ const abort = new AbortController()
21
+ let sse: AbortController | undefined
22
+
23
+ function createSDK() {
24
+ return createNeurocodeClient({
25
+ baseUrl: props.url,
26
+ signal: abort.signal,
27
+ directory: props.directory,
28
+ fetch: props.fetch,
29
+ headers: props.headers,
30
+ })
31
+ }
32
+
33
+ let sdk = createSDK()
34
+
35
+ const handlers = new Set<(event: GlobalEvent) => void>()
36
+ const emitter = {
37
+ emit(_type: "event", event: GlobalEvent) {
38
+ for (const handler of handlers) handler(event)
39
+ },
40
+ on(_type: "event", handler: (event: GlobalEvent) => void) {
41
+ handlers.add(handler)
42
+ return () => {
43
+ handlers.delete(handler)
44
+ }
45
+ },
46
+ }
47
+
48
+ let queue: GlobalEvent[] = []
49
+ let timer: Timer | undefined
50
+ let last = 0
51
+ const retryDelay = 1000
52
+ const maxRetryDelay = 30000
53
+
54
+ const flush = () => {
55
+ if (queue.length === 0) return
56
+ const events = queue
57
+ queue = []
58
+ timer = undefined
59
+ last = Date.now()
60
+ // Batch all event emissions so all store updates result in a single render
61
+ batch(() => {
62
+ for (const event of events) {
63
+ emitter.emit("event", event)
64
+ }
65
+ })
66
+ }
67
+
68
+ const handleEvent = (event: GlobalEvent) => {
69
+ queue.push(event)
70
+ const elapsed = Date.now() - last
71
+
72
+ if (timer) return
73
+ // If we just flushed recently (within 16ms), batch this with future events
74
+ // Otherwise, process immediately to avoid latency
75
+ if (elapsed < 16) {
76
+ timer = setTimeout(flush, 16)
77
+ return
78
+ }
79
+ flush()
80
+ }
81
+
82
+ function startSSE() {
83
+ sse?.abort()
84
+ const ctrl = new AbortController()
85
+ sse = ctrl
86
+ ;(async () => {
87
+ let attempt = 0
88
+ while (true) {
89
+ if (abort.signal.aborted || ctrl.signal.aborted) break
90
+
91
+ const events = await sdk.global.event({
92
+ signal: ctrl.signal,
93
+ sseMaxRetryAttempts: 0,
94
+ })
95
+
96
+ if (Flag.NEUROCODE_EXPERIMENTAL_WORKSPACES) {
97
+ // Start syncing workspaces, it's important to do this after
98
+ // we've started listening to events
99
+ await sdk.sync.start().catch(() => {})
100
+ }
101
+
102
+ for await (const event of events.stream) {
103
+ if (ctrl.signal.aborted) break
104
+ handleEvent(event)
105
+ }
106
+
107
+ if (timer) clearTimeout(timer)
108
+ if (queue.length > 0) flush()
109
+ attempt += 1
110
+ if (abort.signal.aborted || ctrl.signal.aborted) break
111
+
112
+ // Exponential backoff
113
+ const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)
114
+ await new Promise((resolve) => setTimeout(resolve, backoff))
115
+ }
116
+ })().catch(() => {})
117
+ }
118
+
119
+ onMount(async () => {
120
+ if (props.events) {
121
+ const unsub = await props.events.subscribe(handleEvent)
122
+ onCleanup(unsub)
123
+
124
+ if (Flag.NEUROCODE_EXPERIMENTAL_WORKSPACES) {
125
+ // Start syncing workspaces, it's important to do this after
126
+ // we've started listening to events
127
+ await sdk.sync.start().catch(() => {})
128
+ }
129
+ } else {
130
+ startSSE()
131
+ }
132
+ })
133
+
134
+ onCleanup(() => {
135
+ abort.abort()
136
+ sse?.abort()
137
+ if (timer) clearTimeout(timer)
138
+ handlers.clear()
139
+ })
140
+
141
+ return {
142
+ get client() {
143
+ return sdk
144
+ },
145
+ directory: props.directory,
146
+ event: emitter,
147
+ fetch: props.fetch ?? fetch,
148
+ url: props.url,
149
+ }
150
+ },
151
+ })