@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,408 @@
1
+ import { onCleanup, onMount } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { Option, Schema, SchemaGetter } from "effect"
4
+ import { isRecord } from "../util/record"
5
+ import { useTuiPaths } from "./runtime"
6
+ import { createSimpleContext } from "./helper"
7
+ import { editorIntegration } from "../editor"
8
+
9
+ const MCP_PROTOCOL_VERSION = "2025-11-25"
10
+
11
+ const JsonRpcMessageSchema = Schema.Struct({
12
+ id: Schema.optional(Schema.Union([Schema.Number, Schema.String, Schema.Null])),
13
+ method: Schema.optional(Schema.String),
14
+ params: Schema.optional(Schema.Unknown),
15
+ result: Schema.optional(Schema.Unknown),
16
+ error: Schema.optional(
17
+ Schema.Struct({
18
+ code: Schema.optional(Schema.Number),
19
+ message: Schema.optional(Schema.String),
20
+ }),
21
+ ),
22
+ })
23
+
24
+ const PositionSchema = Schema.Struct({
25
+ line: Schema.Number,
26
+ character: Schema.Number,
27
+ })
28
+
29
+ const EditorSelectionRangeSchema = Schema.Struct({
30
+ text: Schema.String,
31
+ selection: Schema.Struct({
32
+ start: PositionSchema,
33
+ end: PositionSchema,
34
+ }),
35
+ })
36
+
37
+ const EditorSelectionRangesSchema = Schema.Struct({
38
+ filePath: Schema.String,
39
+ source: Schema.optional(Schema.Literals(["websocket", "zed"])),
40
+ ranges: Schema.mutable(Schema.Array(EditorSelectionRangeSchema).check(Schema.isMinLength(1))),
41
+ })
42
+
43
+ const EditorSelectionSchema = Schema.Union([
44
+ EditorSelectionRangesSchema,
45
+ Schema.Struct({
46
+ text: Schema.String,
47
+ filePath: Schema.String,
48
+ source: Schema.optional(Schema.Literals(["websocket", "zed"])),
49
+ selection: Schema.Struct({
50
+ start: PositionSchema,
51
+ end: PositionSchema,
52
+ }),
53
+ }),
54
+ ]).pipe(
55
+ Schema.decodeTo(EditorSelectionRangesSchema, {
56
+ decode: SchemaGetter.transform((value) =>
57
+ "ranges" in value
58
+ ? value
59
+ : {
60
+ filePath: value.filePath,
61
+ source: value.source,
62
+ ranges: [
63
+ {
64
+ text: value.text,
65
+ selection: value.selection,
66
+ },
67
+ ],
68
+ },
69
+ ),
70
+ encode: SchemaGetter.passthrough({ strict: false }),
71
+ }),
72
+ )
73
+
74
+ const EditorMentionSchema = Schema.Struct({
75
+ filePath: Schema.String,
76
+ lineStart: Schema.Number,
77
+ lineEnd: Schema.Number,
78
+ })
79
+
80
+ const EditorServerInfoSchema = Schema.Struct({
81
+ protocolVersion: Schema.optional(Schema.String),
82
+ serverInfo: Schema.optional(
83
+ Schema.Struct({
84
+ name: Schema.optional(Schema.String),
85
+ version: Schema.optional(Schema.String),
86
+ }),
87
+ ),
88
+ })
89
+
90
+ const decodeJsonRpcMessage = Schema.decodeUnknownOption(JsonRpcMessageSchema)
91
+ const decodeEditorSelection = Schema.decodeUnknownOption(EditorSelectionSchema)
92
+ const decodeEditorMention = Schema.decodeUnknownOption(EditorMentionSchema)
93
+ const decodeEditorServerInfo = Schema.decodeUnknownOption(EditorServerInfoSchema)
94
+
95
+ type JsonRpcMessage = Schema.Schema.Type<typeof JsonRpcMessageSchema>
96
+ export type EditorSelection = Schema.Schema.Type<typeof EditorSelectionSchema>
97
+ export type EditorMention = Schema.Schema.Type<typeof EditorMentionSchema>
98
+ export type EditorLabelState = "pending" | "sent" | "none"
99
+ type EditorServerInfo = Schema.Schema.Type<typeof EditorServerInfoSchema>
100
+
101
+ type EditorConnection = {
102
+ url: string
103
+ authToken?: string
104
+ source: string
105
+ }
106
+
107
+ export type EditorIntegration = Readonly<{
108
+ connection?(directory: string): EditorConnection | undefined
109
+ selection?(directory: string): Promise<unknown>
110
+ }>
111
+
112
+ export const { use: useEditorContext, provider: EditorContextProvider } = createSimpleContext({
113
+ name: "EditorContext",
114
+ init: (props: { integration?: EditorIntegration; WebSocketImpl?: typeof WebSocket }) => {
115
+ const paths = useTuiPaths()
116
+ const editor = props.integration ?? editorIntegration
117
+ const value = process.env.CLAUDE_CODE_SSE_PORT || process.env.NEUROCODE_EDITOR_SSE_PORT
118
+ const parsedPort = value ? Number.parseInt(value, 10) : undefined
119
+ const port =
120
+ parsedPort && Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : undefined
121
+ const zedTerminal = process.env.ZED_TERM === "true" || process.env.TERM_PROGRAM?.toLowerCase() === "zed"
122
+ const mentionListeners = new Set<(mention: EditorMention) => void>()
123
+ const WebSocketImpl = props.WebSocketImpl ?? WebSocket
124
+ const [store, setStore] = createStore<{
125
+ status: "disabled" | "connecting" | "connected"
126
+ selection: EditorSelection | undefined
127
+ selectionSent: boolean
128
+ server: EditorServerInfo | undefined
129
+ }>({
130
+ status: "disabled",
131
+ selection: undefined,
132
+ selectionSent: false,
133
+ server: undefined,
134
+ })
135
+
136
+ let socket: WebSocket | undefined
137
+ let closed = false
138
+ let reconnect: ReturnType<typeof setTimeout> | undefined
139
+ let attempt = 0
140
+ let requestID = 0
141
+ let zedSelection: Promise<void> | undefined
142
+ let lastZedSelectionKey: string | undefined
143
+ let directory = paths.cwd
144
+ let preserveSelectionOnReconnect = false
145
+ const pending = new Map<number, string>()
146
+
147
+ const setSelection = (selection: EditorSelection | undefined) => {
148
+ const changed = editorSelectionKey(selection) !== editorSelectionKey(store.selection)
149
+ setStore("selection", selection)
150
+ if (changed) setStore("selectionSent", false)
151
+ }
152
+
153
+ const clearSelectionForReconnect = (options?: { resetZedSelectionKey?: boolean }) => {
154
+ if (preserveSelectionOnReconnect) {
155
+ preserveSelectionOnReconnect = false
156
+ return
157
+ }
158
+ if (options?.resetZedSelectionKey) lastZedSelectionKey = undefined
159
+ setSelection(undefined)
160
+ }
161
+
162
+ const send = (payload: JsonRpcMessage) => {
163
+ if (!socket || socket.readyState !== 1) return
164
+ socket.send(JSON.stringify({ jsonrpc: "2.0", ...payload }))
165
+ }
166
+
167
+ const request = (method: string, params?: unknown) => {
168
+ requestID += 1
169
+ pending.set(requestID, method)
170
+ send({ id: requestID, method, params })
171
+ }
172
+
173
+ const connect = () => {
174
+ if (closed) return
175
+
176
+ const connection = resolveEditorConnection(directory, port, editor.connection)
177
+ if (!connection) {
178
+ if (!zedTerminal) {
179
+ setStore("status", "disabled")
180
+ scheduleReconnect()
181
+ return
182
+ }
183
+ if (!editor.selection) {
184
+ setStore("status", "disabled")
185
+ scheduleReconnect()
186
+ return
187
+ }
188
+
189
+ zedSelection ??= editor
190
+ .selection(directory)
191
+ .then((result) => {
192
+ if (closed || socket) return
193
+ if (!isRecord(result) || result.type === "unavailable") return
194
+ const decoded = result.type === "selection" ? decodeEditorSelection(result.selection) : Option.none()
195
+ const selection = Option.getOrUndefined(decoded)
196
+ const key = editorSelectionKey(selection)
197
+ if (key !== lastZedSelectionKey) {
198
+ lastZedSelectionKey = key
199
+ setSelection(selection)
200
+ setStore("status", selection ? "connected" : "disabled")
201
+ }
202
+ })
203
+ .catch(() => {
204
+ // Keep the last known Zed selection for transient polling failures.
205
+ })
206
+ .finally(() => {
207
+ zedSelection = undefined
208
+ })
209
+ scheduleZedPoll()
210
+ return
211
+ }
212
+
213
+ setStore("status", "connecting")
214
+ const current = openEditorSocket(connection, WebSocketImpl)
215
+ socket = current
216
+
217
+ current.addEventListener("open", () => {
218
+ if (socket !== current) {
219
+ current.close()
220
+ return
221
+ }
222
+
223
+ attempt = 0
224
+ setStore("status", "connected")
225
+ request("initialize", {
226
+ protocolVersion: MCP_PROTOCOL_VERSION,
227
+ capabilities: {},
228
+ clientInfo: { name: "neurocode", version: "0.0.0" },
229
+ })
230
+ })
231
+
232
+ current.addEventListener("message", (event) => {
233
+ const message = parseMessage(event.data)
234
+ if (!message) return
235
+
236
+ const selection = message.method === "selection_changed" ? decodeEditorSelection(message.params) : Option.none()
237
+ if (Option.isSome(selection)) {
238
+ setSelection({ ...selection.value, source: "websocket" })
239
+ return
240
+ }
241
+
242
+ const mention = message.method === "at_mentioned" ? decodeEditorMention(message.params) : Option.none()
243
+ if (Option.isSome(mention)) {
244
+ mentionListeners.forEach((listener) => listener(mention.value))
245
+ return
246
+ }
247
+
248
+ if (typeof message.id !== "number") return
249
+
250
+ const method = pending.get(message.id)
251
+ if (!method) return
252
+
253
+ pending.delete(message.id)
254
+ if (message.error) return
255
+
256
+ const initialize = method === "initialize" ? decodeEditorServerInfo(message.result) : Option.none()
257
+ if (Option.isSome(initialize)) {
258
+ setStore("server", initialize.value)
259
+ send({ method: "notifications/initialized" })
260
+ return
261
+ }
262
+ })
263
+
264
+ current.addEventListener("close", () => {
265
+ if (socket !== current) return
266
+
267
+ socket = undefined
268
+ pending.clear()
269
+ if (closed) return
270
+
271
+ setStore("status", "connecting")
272
+ scheduleReconnect()
273
+ })
274
+ }
275
+
276
+ const scheduleReconnect = () => {
277
+ if (closed) return
278
+ if (reconnect) clearTimeout(reconnect)
279
+ attempt += 1
280
+ const delay = Math.min(1000 * 2 ** (attempt - 1), 10_000)
281
+ reconnect = setTimeout(connect, delay)
282
+ }
283
+
284
+ const scheduleZedPoll = () => {
285
+ if (closed) return
286
+ if (reconnect) clearTimeout(reconnect)
287
+ reconnect = setTimeout(connect, 1000)
288
+ }
289
+
290
+ const reconnectWithDirectory = (nextDirectory?: string) => {
291
+ const resolved = nextDirectory || paths.cwd
292
+ const sameDirectory = directory === resolved
293
+ clearSelectionForReconnect({ resetZedSelectionKey: !sameDirectory })
294
+ if (sameDirectory) return
295
+
296
+ directory = resolved
297
+ attempt = 0
298
+ pending.clear()
299
+ if (reconnect) clearTimeout(reconnect)
300
+ reconnect = undefined
301
+ if (socket) {
302
+ const current = socket
303
+ socket = undefined
304
+ current.close()
305
+ }
306
+ setStore("status", "disabled")
307
+ setStore("server", undefined)
308
+ connect()
309
+ }
310
+
311
+ onMount(() => {
312
+ connect()
313
+
314
+ onCleanup(() => {
315
+ closed = true
316
+ if (reconnect) clearTimeout(reconnect)
317
+ socket?.close()
318
+ })
319
+ })
320
+
321
+ return {
322
+ enabled() {
323
+ return Boolean(resolveEditorConnection(directory, port, editor.connection) || (zedTerminal && editor.selection))
324
+ },
325
+ connected() {
326
+ return store.status === "connected"
327
+ },
328
+ selection() {
329
+ return store.selection
330
+ },
331
+ clearSelection() {
332
+ lastZedSelectionKey = undefined
333
+ zedSelection = undefined
334
+ setSelection(undefined)
335
+ },
336
+ preserveSelectionFromNewSession() {
337
+ preserveSelectionOnReconnect = true
338
+ },
339
+ markSelectionSent() {
340
+ if (!store.selection) return
341
+ setStore("selectionSent", true)
342
+ },
343
+ labelState(): EditorLabelState {
344
+ if (!store.selection) return "none"
345
+ return store.selectionSent ? "sent" : "pending"
346
+ },
347
+ onMention(listener: (mention: EditorMention) => void) {
348
+ mentionListeners.add(listener)
349
+ return () => mentionListeners.delete(listener)
350
+ },
351
+ server() {
352
+ return store.server
353
+ },
354
+ reconnect(directory?: string) {
355
+ reconnectWithDirectory(directory)
356
+ },
357
+ }
358
+ },
359
+ })
360
+
361
+ function resolveEditorConnection(
362
+ directory: string,
363
+ port: number | undefined,
364
+ discover: ((directory: string) => EditorConnection | undefined) | undefined,
365
+ ): EditorConnection | undefined {
366
+ if (port) {
367
+ return {
368
+ url: `ws://127.0.0.1:${port}`,
369
+ source: `env:${port}`,
370
+ }
371
+ }
372
+
373
+ return discover?.(directory)
374
+ }
375
+
376
+ export function editorSelectionKey(selection: EditorSelection | undefined) {
377
+ if (!selection) return ""
378
+ return [
379
+ selection.filePath,
380
+ ...selection.ranges.flatMap((range) => [
381
+ range.selection.start.line,
382
+ range.selection.start.character,
383
+ range.selection.end.line,
384
+ range.selection.end.character,
385
+ range.text,
386
+ ]),
387
+ ].join("\0")
388
+ }
389
+
390
+ function openEditorSocket(connection: EditorConnection, WebSocketImpl: typeof WebSocket) {
391
+ if (!connection.authToken) return new WebSocketImpl(connection.url)
392
+
393
+ return new WebSocketImpl(connection.url, {
394
+ headers: {
395
+ "x-claude-code-ide-authorization": connection.authToken,
396
+ },
397
+ } as any)
398
+ }
399
+
400
+ function parseMessage(value: unknown) {
401
+ if (typeof value !== "string") return
402
+
403
+ try {
404
+ return Option.getOrUndefined(decodeJsonRpcMessage(JSON.parse(value)))
405
+ } catch {
406
+ return
407
+ }
408
+ }
@@ -0,0 +1,6 @@
1
+ import { createSimpleContext } from "./helper"
2
+
3
+ export const { use: useEpilogue, provider: EpilogueProvider } = createSimpleContext({
4
+ name: "Epilogue",
5
+ init: (props: { set(value?: string): void }) => props.set,
6
+ })
@@ -0,0 +1,36 @@
1
+ import type { Event } from "@neurocode-ai/sdk/v2"
2
+ import { useSDK } from "./sdk"
3
+
4
+ type EventMetadata = {
5
+ directory: string
6
+ workspace: string | undefined
7
+ }
8
+
9
+ export function useEvent() {
10
+ const sdk = useSDK()
11
+
12
+ function subscribe(handler: (event: Event, metadata: EventMetadata) => void) {
13
+ return sdk.event.on("event", (event) => {
14
+ if (event.payload.type === "sync") {
15
+ return
16
+ }
17
+
18
+ handler(event.payload, { directory: event.directory, workspace: event.workspace })
19
+ })
20
+ }
21
+
22
+ function on<T extends Event["type"]>(
23
+ type: T,
24
+ handler: (event: Extract<Event, { type: T }>, metadata: EventMetadata) => void,
25
+ ) {
26
+ return subscribe((event: Event, metadata: EventMetadata) => {
27
+ if (event.type !== type) return
28
+ handler(event as Extract<Event, { type: T }>, metadata)
29
+ })
30
+ }
31
+
32
+ return {
33
+ subscribe,
34
+ on,
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ import { createSimpleContext } from "./helper"
2
+
3
+ export type Exit = (reason?: unknown) => void
4
+
5
+ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
6
+ name: "Exit",
7
+ init: (input: { exit: Exit }) => input.exit,
8
+ })
@@ -0,0 +1,26 @@
1
+ import { createContext, Show, useContext, type ParentProps } from "solid-js"
2
+
3
+ export function createSimpleContext<T, Props extends Record<string, any>>(input: {
4
+ name: string
5
+ init: ((input: Props) => T) | (() => T)
6
+ }) {
7
+ const ctx = createContext<T>()
8
+
9
+ return {
10
+ context: ctx,
11
+ provider: (props: ParentProps<Props>) => {
12
+ const init = input.init(props)
13
+ return (
14
+ // @ts-expect-error
15
+ <Show when={init.ready === undefined || init.ready === true}>
16
+ <ctx.Provider value={init}>{props.children}</ctx.Provider>
17
+ </Show>
18
+ )
19
+ },
20
+ use() {
21
+ const value = useContext(ctx)
22
+ if (!value) throw new Error(`${input.name} context must be used within a context provider`)
23
+ return value
24
+ },
25
+ }
26
+ }
@@ -0,0 +1,66 @@
1
+ import { createSignal, type Setter } from "solid-js"
2
+ import { createStore, unwrap } from "solid-js/store"
3
+ import { createSimpleContext } from "./helper"
4
+ import { Flock } from "@neurocode-ai/core/util/flock"
5
+ import { Global } from "@neurocode-ai/core/global"
6
+ import { readJson, writeJsonAtomic } from "../util/persistence"
7
+ import { useTuiPaths } from "./runtime"
8
+ import path from "path"
9
+
10
+ export const { use: useKV, provider: KVProvider } = createSimpleContext({
11
+ name: "KV",
12
+ init: () => {
13
+ const paths = useTuiPaths()
14
+ void Global.Path.state
15
+ const file = path.join(paths.state, "kv.json")
16
+ const lock = `tui-kv:${file}`
17
+ const [ready, setReady] = createSignal(false)
18
+ const [store, setStore] = createStore<Record<string, any>>()
19
+ // Queue same-process writes so rapid updates persist in order.
20
+ let write = Promise.resolve()
21
+
22
+ Flock.withLock(lock, () => readJson<Record<string, unknown>>(file))
23
+ .then((x) => {
24
+ setStore(x)
25
+ })
26
+ .catch((error) => {
27
+ console.error("Failed to read KV state", { error })
28
+ })
29
+ .finally(() => {
30
+ setReady(true)
31
+ })
32
+
33
+ const result = {
34
+ get ready() {
35
+ return ready()
36
+ },
37
+ get store() {
38
+ return store
39
+ },
40
+ signal<T>(name: string, defaultValue: T) {
41
+ if (store[name] === undefined) setStore(name, defaultValue)
42
+ return [
43
+ function () {
44
+ return result.get(name)
45
+ },
46
+ function setter(next: Setter<T>) {
47
+ result.set(name, next)
48
+ },
49
+ ] as const
50
+ },
51
+ get(key: string, defaultValue?: any) {
52
+ return store[key] ?? defaultValue
53
+ },
54
+ set(key: string, value: any) {
55
+ setStore(key, value)
56
+ const snapshot = structuredClone(unwrap(store))
57
+ write = write
58
+ .then(() => Flock.withLock(lock, () => writeJsonAtomic(file, snapshot)))
59
+ .catch((error) => {
60
+ console.error("Failed to write KV state", { error })
61
+ })
62
+ },
63
+ }
64
+ return result
65
+ },
66
+ })