@agentprojectcontext/apx 1.15.6 → 1.17.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 (222) hide show
  1. package/package.json +46 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent-langchain.js +296 -0
  28. package/src/daemon/super-agent.js +115 -19
  29. package/src/daemon/transcription.js +262 -59
  30. package/src/daemon/whisper-server.py +57 -6
  31. package/src/overlay/index.html +44 -0
  32. package/src/overlay/main.js +480 -0
  33. package/src/overlay/package.json +3 -0
  34. package/src/overlay/preload.js +34 -0
  35. package/src/overlay/renderer.js +371 -0
  36. package/src/overlay/style.css +250 -0
  37. package/src/tui/_shims/cli-error.ts +6 -0
  38. package/src/tui/_shims/cli-logo.ts +18 -0
  39. package/src/tui/_shims/cli-ui.ts +1 -0
  40. package/src/tui/_shims/config-console-state.ts +7 -0
  41. package/src/tui/_shims/core-any.ts +30 -0
  42. package/src/tui/_shims/core-binary.ts +13 -0
  43. package/src/tui/_shims/core-flag.ts +3 -0
  44. package/src/tui/_shims/core-log.ts +14 -0
  45. package/src/tui/_shims/lsp-language.ts +1 -0
  46. package/src/tui/_shims/opencode-any.ts +135 -0
  47. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  48. package/src/tui/_shims/plugin-tui.ts +13 -0
  49. package/src/tui/_shims/provider-provider.ts +10 -0
  50. package/src/tui/_shims/session-retry.ts +1 -0
  51. package/src/tui/_shims/session-schema.ts +15 -0
  52. package/src/tui/_shims/session-session.ts +3 -0
  53. package/src/tui/_shims/snapshot.ts +4 -0
  54. package/src/tui/_shims/tool-any.ts +18 -0
  55. package/src/tui/_shims/util-error.ts +7 -0
  56. package/src/tui/_shims/util-filesystem.ts +79 -0
  57. package/src/tui/_shims/util-format.ts +7 -0
  58. package/src/tui/_shims/util-iife.ts +3 -0
  59. package/src/tui/_shims/util-locale.ts +10 -0
  60. package/src/tui/_shims/util-process.ts +38 -0
  61. package/src/tui/app.tsx +783 -0
  62. package/src/tui/asset/charge.wav +0 -0
  63. package/src/tui/asset/pulse-a.wav +0 -0
  64. package/src/tui/asset/pulse-b.wav +0 -0
  65. package/src/tui/asset/pulse-c.wav +0 -0
  66. package/src/tui/attach.ts +100 -0
  67. package/src/tui/component/bg-pulse-render.ts +436 -0
  68. package/src/tui/component/bg-pulse.tsx +99 -0
  69. package/src/tui/component/border.tsx +21 -0
  70. package/src/tui/component/dialog-agent.tsx +31 -0
  71. package/src/tui/component/dialog-console-org.tsx +103 -0
  72. package/src/tui/component/dialog-mcp.tsx +85 -0
  73. package/src/tui/component/dialog-model.tsx +175 -0
  74. package/src/tui/component/dialog-provider.tsx +456 -0
  75. package/src/tui/component/dialog-retry-action.tsx +160 -0
  76. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  77. package/src/tui/component/dialog-session-list.tsx +323 -0
  78. package/src/tui/component/dialog-session-rename.tsx +31 -0
  79. package/src/tui/component/dialog-skill.tsx +36 -0
  80. package/src/tui/component/dialog-stash.tsx +87 -0
  81. package/src/tui/component/dialog-status.tsx +168 -0
  82. package/src/tui/component/dialog-tag.tsx +44 -0
  83. package/src/tui/component/dialog-theme-list.tsx +50 -0
  84. package/src/tui/component/dialog-variant.tsx +39 -0
  85. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  86. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  87. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  88. package/src/tui/component/error-component.tsx +92 -0
  89. package/src/tui/component/logo.tsx +896 -0
  90. package/src/tui/component/plugin-route-missing.tsx +14 -0
  91. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  92. package/src/tui/component/prompt/cwd.ts +0 -0
  93. package/src/tui/component/prompt/frecency.tsx +90 -0
  94. package/src/tui/component/prompt/history.tsx +108 -0
  95. package/src/tui/component/prompt/index.tsx +1809 -0
  96. package/src/tui/component/prompt/part.ts +16 -0
  97. package/src/tui/component/prompt/stash.tsx +101 -0
  98. package/src/tui/component/prompt/traits.ts +35 -0
  99. package/src/tui/component/spinner.tsx +24 -0
  100. package/src/tui/component/startup-loading.tsx +63 -0
  101. package/src/tui/component/todo-item.tsx +32 -0
  102. package/src/tui/component/use-connected.tsx +9 -0
  103. package/src/tui/component/workspace-label.tsx +19 -0
  104. package/src/tui/config/cwd.ts +5 -0
  105. package/src/tui/config/keybind.ts +432 -0
  106. package/src/tui/config/tui-migrate.ts +154 -0
  107. package/src/tui/config/tui-schema.ts +34 -0
  108. package/src/tui/config/tui.ts +46 -0
  109. package/src/tui/context/aggregate-failures.ts +34 -0
  110. package/src/tui/context/args.tsx +15 -0
  111. package/src/tui/context/command-palette.tsx +163 -0
  112. package/src/tui/context/directory.ts +15 -0
  113. package/src/tui/context/editor-zed.ts +283 -0
  114. package/src/tui/context/editor.ts +468 -0
  115. package/src/tui/context/event-apx.ts +22 -0
  116. package/src/tui/context/event.ts +6 -0
  117. package/src/tui/context/exit.tsx +60 -0
  118. package/src/tui/context/helper.tsx +25 -0
  119. package/src/tui/context/kv.tsx +81 -0
  120. package/src/tui/context/local.tsx +608 -0
  121. package/src/tui/context/path-format.tsx +39 -0
  122. package/src/tui/context/project-apx.tsx +48 -0
  123. package/src/tui/context/project.tsx +7 -0
  124. package/src/tui/context/prompt.tsx +18 -0
  125. package/src/tui/context/route.tsx +52 -0
  126. package/src/tui/context/sdk-apx.tsx +185 -0
  127. package/src/tui/context/sdk.tsx +6 -0
  128. package/src/tui/context/sync-apx.tsx +178 -0
  129. package/src/tui/context/sync-v2.tsx +16 -0
  130. package/src/tui/context/sync.tsx +118 -0
  131. package/src/tui/context/theme/aura.json +69 -0
  132. package/src/tui/context/theme/ayu.json +80 -0
  133. package/src/tui/context/theme/carbonfox.json +248 -0
  134. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  135. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  136. package/src/tui/context/theme/catppuccin.json +112 -0
  137. package/src/tui/context/theme/cobalt2.json +225 -0
  138. package/src/tui/context/theme/cursor.json +249 -0
  139. package/src/tui/context/theme/dracula.json +219 -0
  140. package/src/tui/context/theme/everforest.json +241 -0
  141. package/src/tui/context/theme/flexoki.json +237 -0
  142. package/src/tui/context/theme/github.json +233 -0
  143. package/src/tui/context/theme/gruvbox.json +242 -0
  144. package/src/tui/context/theme/kanagawa.json +77 -0
  145. package/src/tui/context/theme/lucent-orng.json +234 -0
  146. package/src/tui/context/theme/material.json +235 -0
  147. package/src/tui/context/theme/matrix.json +77 -0
  148. package/src/tui/context/theme/mercury.json +252 -0
  149. package/src/tui/context/theme/monokai.json +221 -0
  150. package/src/tui/context/theme/nightowl.json +221 -0
  151. package/src/tui/context/theme/nord.json +223 -0
  152. package/src/tui/context/theme/one-dark.json +84 -0
  153. package/src/tui/context/theme/opencode.json +245 -0
  154. package/src/tui/context/theme/orng.json +249 -0
  155. package/src/tui/context/theme/osaka-jade.json +93 -0
  156. package/src/tui/context/theme/palenight.json +222 -0
  157. package/src/tui/context/theme/rosepine.json +234 -0
  158. package/src/tui/context/theme/solarized.json +223 -0
  159. package/src/tui/context/theme/synthwave84.json +226 -0
  160. package/src/tui/context/theme/tokyonight.json +243 -0
  161. package/src/tui/context/theme/vercel.json +245 -0
  162. package/src/tui/context/theme/vesper.json +218 -0
  163. package/src/tui/context/theme/zenburn.json +223 -0
  164. package/src/tui/context/theme.tsx +1247 -0
  165. package/src/tui/context/tui-config.tsx +9 -0
  166. package/src/tui/event.ts +16 -0
  167. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  168. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  169. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  170. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  171. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  172. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  173. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  174. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  175. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  176. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  177. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  178. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  179. package/src/tui/keymap.tsx +166 -0
  180. package/src/tui/layer.ts +6 -0
  181. package/src/tui/plugin/api.tsx +381 -0
  182. package/src/tui/plugin/command-shim.ts +109 -0
  183. package/src/tui/plugin/internal.ts +33 -0
  184. package/src/tui/plugin/runtime.ts +1069 -0
  185. package/src/tui/plugin/slots.tsx +60 -0
  186. package/src/tui/routes/home.tsx +96 -0
  187. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  188. package/src/tui/routes/session/dialog-message.tsx +108 -0
  189. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  190. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  191. package/src/tui/routes/session/footer.tsx +91 -0
  192. package/src/tui/routes/session/index.tsx +188 -0
  193. package/src/tui/routes/session/permission.tsx +722 -0
  194. package/src/tui/routes/session/question.tsx +490 -0
  195. package/src/tui/routes/session/sidebar.tsx +102 -0
  196. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  197. package/src/tui/run.ts +84 -0
  198. package/src/tui/thread.ts +261 -0
  199. package/src/tui/tsconfig.json +40 -0
  200. package/src/tui/ui/dialog-alert.tsx +66 -0
  201. package/src/tui/ui/dialog-confirm.tsx +108 -0
  202. package/src/tui/ui/dialog-export-options.tsx +217 -0
  203. package/src/tui/ui/dialog-help.tsx +40 -0
  204. package/src/tui/ui/dialog-prompt.tsx +101 -0
  205. package/src/tui/ui/dialog-select.tsx +553 -0
  206. package/src/tui/ui/dialog.tsx +211 -0
  207. package/src/tui/ui/link.tsx +34 -0
  208. package/src/tui/ui/spinner.ts +368 -0
  209. package/src/tui/ui/toast.tsx +111 -0
  210. package/src/tui/util/clipboard.ts +217 -0
  211. package/src/tui/util/editor.ts +37 -0
  212. package/src/tui/util/model.ts +23 -0
  213. package/src/tui/util/provider-origin.ts +7 -0
  214. package/src/tui/util/revert-diff.ts +18 -0
  215. package/src/tui/util/scroll.ts +25 -0
  216. package/src/tui/util/selection.ts +65 -0
  217. package/src/tui/util/signal.ts +41 -0
  218. package/src/tui/util/sound.ts +156 -0
  219. package/src/tui/util/transcript.ts +112 -0
  220. package/src/tui/validate-session.ts +29 -0
  221. package/src/tui/win32.ts +130 -0
  222. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,468 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs"
2
+ import os from "node:os"
3
+ import path from "node:path"
4
+ import { onCleanup, onMount } from "solid-js"
5
+ import { createStore } from "solid-js/store"
6
+ import { Option, Schema } from "effect"
7
+ import { isRecord } from "@/util/record"
8
+ import { createSimpleContext } from "./helper"
9
+ import { resolveZedDbPath, resolveZedSelection } from "./editor-zed"
10
+
11
+ const MCP_PROTOCOL_VERSION = "2025-11-25"
12
+
13
+ const JsonRpcMessageSchema = Schema.Struct({
14
+ id: Schema.optional(Schema.Union(Schema.Number, Schema.String, Schema.Null)),
15
+ method: Schema.optional(Schema.String),
16
+ params: Schema.optional(Schema.Unknown),
17
+ result: Schema.optional(Schema.Unknown),
18
+ error: Schema.optional(
19
+ Schema.Struct({
20
+ code: Schema.optional(Schema.Number),
21
+ message: Schema.optional(Schema.String),
22
+ }),
23
+ ),
24
+ })
25
+
26
+ const PositionSchema = Schema.Struct({
27
+ line: Schema.Number,
28
+ character: Schema.Number,
29
+ })
30
+
31
+ const EditorSelectionRangeSchema = Schema.Struct({
32
+ text: Schema.String,
33
+ selection: Schema.Struct({
34
+ start: PositionSchema,
35
+ end: PositionSchema,
36
+ }),
37
+ })
38
+
39
+ const EditorSelectionRangesSchema = Schema.Struct({
40
+ filePath: Schema.String,
41
+ source: Schema.optional(Schema.Union(Schema.Literal("websocket"), Schema.Literal("zed"))),
42
+ ranges: Schema.mutable(Schema.NonEmptyArray(EditorSelectionRangeSchema)),
43
+ })
44
+
45
+ const EditorSelectionRawSchema = Schema.Union(
46
+ EditorSelectionRangesSchema,
47
+ Schema.Struct({
48
+ text: Schema.String,
49
+ filePath: Schema.String,
50
+ source: Schema.optional(Schema.Union(Schema.Literal("websocket"), Schema.Literal("zed"))),
51
+ selection: Schema.Struct({
52
+ start: PositionSchema,
53
+ end: PositionSchema,
54
+ }),
55
+ }),
56
+ )
57
+
58
+ // Transform the raw union into EditorSelectionRangesSchema
59
+ const EditorSelectionSchema = Schema.transform(
60
+ EditorSelectionRawSchema,
61
+ EditorSelectionRangesSchema,
62
+ {
63
+ strict: false,
64
+ decode: (value) =>
65
+ "ranges" in value
66
+ ? value
67
+ : {
68
+ filePath: value.filePath,
69
+ source: value.source,
70
+ ranges: [
71
+ {
72
+ text: value.text,
73
+ selection: value.selection,
74
+ },
75
+ ],
76
+ },
77
+ encode: (value) => value,
78
+ },
79
+ )
80
+
81
+ const EditorMentionSchema = Schema.Struct({
82
+ filePath: Schema.String,
83
+ lineStart: Schema.Number,
84
+ lineEnd: Schema.Number,
85
+ })
86
+
87
+ const EditorServerInfoSchema = Schema.Struct({
88
+ protocolVersion: Schema.optional(Schema.String),
89
+ serverInfo: Schema.optional(
90
+ Schema.Struct({
91
+ name: Schema.optional(Schema.String),
92
+ version: Schema.optional(Schema.String),
93
+ }),
94
+ ),
95
+ })
96
+
97
+ const decodeJsonRpcMessage = Schema.decodeUnknownOption(JsonRpcMessageSchema)
98
+ const decodeEditorSelection = Schema.decodeUnknownOption(EditorSelectionSchema)
99
+ const decodeEditorMention = Schema.decodeUnknownOption(EditorMentionSchema)
100
+ const decodeEditorServerInfo = Schema.decodeUnknownOption(EditorServerInfoSchema)
101
+
102
+ type JsonRpcMessage = Schema.Schema.Type<typeof JsonRpcMessageSchema>
103
+ export type EditorSelection = Schema.Schema.Type<typeof EditorSelectionSchema>
104
+ export type EditorMention = Schema.Schema.Type<typeof EditorMentionSchema>
105
+ export type EditorLabelState = "pending" | "sent" | "none"
106
+ type EditorServerInfo = Schema.Schema.Type<typeof EditorServerInfoSchema>
107
+
108
+ type EditorConnection = {
109
+ url: string
110
+ authToken?: string
111
+ source: string
112
+ }
113
+
114
+ type EditorLockFile = {
115
+ port: number
116
+ authToken?: string
117
+ transport?: string
118
+ workspaceFolders: string[]
119
+ mtimeMs: number
120
+ }
121
+
122
+ export const { use: useEditorContext, provider: EditorContextProvider } = createSimpleContext({
123
+ name: "EditorContext",
124
+ init: (props: { WebSocketImpl?: typeof WebSocket }) => {
125
+ const mentionListeners = new Set<(mention: EditorMention) => void>()
126
+ const WebSocketImpl = props.WebSocketImpl ?? WebSocket
127
+ const [store, setStore] = createStore<{
128
+ status: "disabled" | "connecting" | "connected"
129
+ selection: EditorSelection | undefined
130
+ selectionSent: boolean
131
+ server: EditorServerInfo | undefined
132
+ }>({
133
+ status: "disabled",
134
+ selection: undefined,
135
+ selectionSent: false,
136
+ server: undefined,
137
+ })
138
+
139
+ let socket: WebSocket | undefined
140
+ let closed = false
141
+ let reconnect: ReturnType<typeof setTimeout> | undefined
142
+ let attempt = 0
143
+ let requestID = 0
144
+ let zedSelection: Promise<void> | undefined
145
+ let lastZedSelectionKey: string | undefined
146
+ let directory = process.cwd()
147
+ let preserveSelectionOnReconnect = false
148
+ const pending = new Map<number, string>()
149
+
150
+ const setSelection = (selection: EditorSelection | undefined) => {
151
+ const changed = editorSelectionKey(selection) !== editorSelectionKey(store.selection)
152
+ setStore("selection", selection)
153
+ if (changed) setStore("selectionSent", false)
154
+ }
155
+
156
+ const clearSelectionForReconnect = (options?: { resetZedSelectionKey?: boolean }) => {
157
+ if (preserveSelectionOnReconnect) {
158
+ preserveSelectionOnReconnect = false
159
+ return
160
+ }
161
+ if (options?.resetZedSelectionKey) lastZedSelectionKey = undefined
162
+ setSelection(undefined)
163
+ }
164
+
165
+ const send = (payload: JsonRpcMessage) => {
166
+ if (!socket || socket.readyState !== 1) return
167
+ socket.send(JSON.stringify({ jsonrpc: "2.0", ...payload }))
168
+ }
169
+
170
+ const request = (method: string, params?: unknown) => {
171
+ requestID += 1
172
+ pending.set(requestID, method)
173
+ send({ id: requestID, method, params })
174
+ }
175
+
176
+ const connect = () => {
177
+ if (closed) return
178
+
179
+ const connection = resolveEditorConnection(directory)
180
+ if (!connection) {
181
+ const dbPath = resolveZedDbPath()
182
+ if (!dbPath) {
183
+ setStore("status", "disabled")
184
+ scheduleReconnect()
185
+ return
186
+ }
187
+ zedSelection ??= resolveZedSelection(dbPath, directory)
188
+ .then((result) => {
189
+ if (closed || socket) return
190
+ if (result.type === "unavailable") return
191
+ const selection = result.type === "selection" ? result.selection : undefined
192
+ const key = editorSelectionKey(selection)
193
+ if (key !== lastZedSelectionKey) {
194
+ lastZedSelectionKey = key
195
+ setSelection(selection)
196
+ setStore("status", selection ? "connected" : "disabled")
197
+ }
198
+ })
199
+ .catch(() => {
200
+ // Keep the last known Zed selection for transient polling failures.
201
+ })
202
+ .finally(() => {
203
+ zedSelection = undefined
204
+ })
205
+ scheduleZedPoll()
206
+ return
207
+ }
208
+
209
+ setStore("status", "connecting")
210
+ const current = openEditorSocket(connection, WebSocketImpl)
211
+ socket = current
212
+
213
+ current.addEventListener("open", () => {
214
+ if (socket !== current) {
215
+ current.close()
216
+ return
217
+ }
218
+
219
+ attempt = 0
220
+ setStore("status", "connected")
221
+ request("initialize", {
222
+ protocolVersion: MCP_PROTOCOL_VERSION,
223
+ capabilities: {},
224
+ clientInfo: { name: "opencode", version: "0.0.0" },
225
+ })
226
+ })
227
+
228
+ current.addEventListener("message", (event) => {
229
+ const message = parseMessage(event.data)
230
+ if (!message) return
231
+
232
+ const selection = message.method === "selection_changed" ? decodeEditorSelection(message.params) : Option.none()
233
+ if (Option.isSome(selection)) {
234
+ setSelection({ ...selection.value, source: "websocket" })
235
+ return
236
+ }
237
+
238
+ const mention = message.method === "at_mentioned" ? decodeEditorMention(message.params) : Option.none()
239
+ if (Option.isSome(mention)) {
240
+ mentionListeners.forEach((listener) => listener(mention.value))
241
+ return
242
+ }
243
+
244
+ if (typeof message.id !== "number") return
245
+
246
+ const method = pending.get(message.id)
247
+ if (!method) return
248
+
249
+ pending.delete(message.id)
250
+ if (message.error) return
251
+
252
+ const initialize = method === "initialize" ? decodeEditorServerInfo(message.result) : Option.none()
253
+ if (Option.isSome(initialize)) {
254
+ setStore("server", initialize.value)
255
+ send({ method: "notifications/initialized" })
256
+ return
257
+ }
258
+ })
259
+
260
+ current.addEventListener("close", () => {
261
+ if (socket !== current) return
262
+
263
+ socket = undefined
264
+ pending.clear()
265
+ if (closed) return
266
+
267
+ setStore("status", "connecting")
268
+ scheduleReconnect()
269
+ })
270
+ }
271
+
272
+ const scheduleReconnect = () => {
273
+ if (closed) return
274
+ if (reconnect) clearTimeout(reconnect)
275
+ attempt += 1
276
+ const delay = Math.min(1000 * 2 ** (attempt - 1), 10_000)
277
+ reconnect = setTimeout(connect, delay)
278
+ }
279
+
280
+ const scheduleZedPoll = () => {
281
+ if (closed) return
282
+ if (reconnect) clearTimeout(reconnect)
283
+ reconnect = setTimeout(connect, 1000)
284
+ }
285
+
286
+ const reconnectWithDirectory = (nextDirectory?: string) => {
287
+ const resolved = nextDirectory || process.cwd()
288
+ const sameDirectory = directory === resolved
289
+ clearSelectionForReconnect({ resetZedSelectionKey: !sameDirectory })
290
+ if (sameDirectory) return
291
+
292
+ directory = resolved
293
+ attempt = 0
294
+ pending.clear()
295
+ if (reconnect) clearTimeout(reconnect)
296
+ reconnect = undefined
297
+ if (socket) {
298
+ const current = socket
299
+ socket = undefined
300
+ current.close()
301
+ }
302
+ setStore("status", "disabled")
303
+ setStore("server", undefined)
304
+ connect()
305
+ }
306
+
307
+ onMount(() => {
308
+ connect()
309
+
310
+ onCleanup(() => {
311
+ closed = true
312
+ if (reconnect) clearTimeout(reconnect)
313
+ socket?.close()
314
+ })
315
+ })
316
+
317
+ return {
318
+ enabled() {
319
+ return Boolean(resolveEditorConnection(directory) || resolveZedDbPath())
320
+ },
321
+ connected() {
322
+ return store.status === "connected"
323
+ },
324
+ selection() {
325
+ return store.selection
326
+ },
327
+ clearSelection() {
328
+ lastZedSelectionKey = undefined
329
+ zedSelection = undefined
330
+ setSelection(undefined)
331
+ },
332
+ preserveSelectionFromNewSession() {
333
+ preserveSelectionOnReconnect = true
334
+ },
335
+ markSelectionSent() {
336
+ if (!store.selection) return
337
+ setStore("selectionSent", true)
338
+ },
339
+ labelState(): EditorLabelState {
340
+ if (!store.selection) return "none"
341
+ return store.selectionSent ? "sent" : "pending"
342
+ },
343
+ onMention(listener: (mention: EditorMention) => void) {
344
+ mentionListeners.add(listener)
345
+ return () => mentionListeners.delete(listener)
346
+ },
347
+ server() {
348
+ return store.server
349
+ },
350
+ reconnect(directory?: string) {
351
+ reconnectWithDirectory(directory)
352
+ },
353
+ }
354
+ },
355
+ })
356
+
357
+ function parsePort(value: string | undefined) {
358
+ if (!value) return
359
+
360
+ const parsed = Number.parseInt(value, 10)
361
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) return
362
+ return parsed
363
+ }
364
+
365
+ function resolveEditorConnection(directory: string): EditorConnection | undefined {
366
+ const port = parsePort(process.env.CLAUDE_CODE_SSE_PORT || process.env.OPENCODE_EDITOR_SSE_PORT)
367
+ if (port) {
368
+ return {
369
+ url: `ws://127.0.0.1:${port}`,
370
+ source: `env:${port}`,
371
+ }
372
+ }
373
+
374
+ const lock = resolveEditorLockFile(directory)
375
+ if (lock) {
376
+ return {
377
+ url: `ws://127.0.0.1:${lock.port}`,
378
+ authToken: lock.authToken,
379
+ source: `lock:${lock.port}`,
380
+ }
381
+ }
382
+ }
383
+
384
+ function resolveEditorLockFile(activeDirectory: string) {
385
+ const directory = path.join(os.homedir(), ".claude", "ide")
386
+ let entries: string[]
387
+
388
+ try {
389
+ entries = readdirSync(directory)
390
+ } catch {
391
+ return
392
+ }
393
+
394
+ // longest workspace folder that contains the active session directory; 0 if none match
395
+ const bestMatchLength = (lock: EditorLockFile) =>
396
+ Math.max(0, ...lock.workspaceFolders.map((folder) => pathContainsLength(folder, activeDirectory)))
397
+ const locks = entries
398
+ .filter((entry) => entry.endsWith(".lock"))
399
+ .map((entry) => readEditorLockFile(path.join(directory, entry)))
400
+ .filter((entry): entry is EditorLockFile => Boolean(entry))
401
+ .filter((entry) => bestMatchLength(entry) > 0)
402
+ // prefer locks with longer matching workspace folders, then more recent ones
403
+ .sort((left, right) => bestMatchLength(right) - bestMatchLength(left) || right.mtimeMs - left.mtimeMs)
404
+ return locks[0]
405
+ }
406
+
407
+ function readEditorLockFile(filePath: string): EditorLockFile | undefined {
408
+ const port = parsePort(path.basename(filePath, ".lock"))
409
+ if (!port) return
410
+
411
+ try {
412
+ const parsed = JSON.parse(readFileSync(filePath, "utf-8")) as unknown
413
+ if (!isRecord(parsed)) return
414
+ if (parsed.transport !== undefined && parsed.transport !== "ws") return
415
+
416
+ return {
417
+ port,
418
+ authToken: typeof parsed.authToken === "string" ? parsed.authToken : undefined,
419
+ transport: typeof parsed.transport === "string" ? parsed.transport : undefined,
420
+ workspaceFolders: Array.isArray(parsed.workspaceFolders)
421
+ ? parsed.workspaceFolders.filter((value): value is string => typeof value === "string")
422
+ : [],
423
+ mtimeMs: statSync(filePath).mtimeMs,
424
+ }
425
+ } catch {
426
+ return
427
+ }
428
+ }
429
+
430
+ export function editorSelectionKey(selection: EditorSelection | undefined) {
431
+ if (!selection) return ""
432
+ return [
433
+ selection.filePath,
434
+ ...selection.ranges.flatMap((range) => [
435
+ range.selection.start.line,
436
+ range.selection.start.character,
437
+ range.selection.end.line,
438
+ range.selection.end.character,
439
+ range.text,
440
+ ]),
441
+ ].join("\0")
442
+ }
443
+
444
+ function pathContainsLength(parent: string, child: string) {
445
+ const resolved = path.resolve(parent)
446
+ const relative = path.relative(resolved, path.resolve(child))
447
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved.length : 0
448
+ }
449
+
450
+ function openEditorSocket(connection: EditorConnection, WebSocketImpl: typeof WebSocket) {
451
+ if (!connection.authToken) return new WebSocketImpl(connection.url)
452
+
453
+ return new WebSocketImpl(connection.url, {
454
+ headers: {
455
+ "x-claude-code-ide-authorization": connection.authToken,
456
+ },
457
+ } as any)
458
+ }
459
+
460
+ function parseMessage(value: unknown) {
461
+ if (typeof value !== "string") return
462
+
463
+ try {
464
+ return Option.getOrUndefined(decodeJsonRpcMessage(JSON.parse(value)))
465
+ } catch {
466
+ return
467
+ }
468
+ }
@@ -0,0 +1,22 @@
1
+ import type { ApxEvent } from "./sdk-apx"
2
+ import { useSDK } from "./sdk-apx"
3
+
4
+ export function useEvent() {
5
+ const sdk = useSDK()
6
+
7
+ function subscribe(handler: (event: ApxEvent) => void) {
8
+ return sdk.event.on("event", (ev) => handler(ev))
9
+ }
10
+
11
+ function on<T extends ApxEvent["type"]>(
12
+ type: T,
13
+ handler: (event: Extract<ApxEvent, { type: T }>, _metadata?: unknown) => void,
14
+ ) {
15
+ return subscribe((event) => {
16
+ if (event.type !== type) return
17
+ handler(event as Extract<ApxEvent, { type: T }>)
18
+ })
19
+ }
20
+
21
+ return { subscribe, on }
22
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * APX compatibility shim for the opencode Event context.
3
+ * Re-exports from event-apx.ts so all existing `import { useEvent } from "@tui/context/event"`
4
+ * imports continue to work.
5
+ */
6
+ export { useEvent } from "./event-apx"
@@ -0,0 +1,60 @@
1
+ import { useRenderer } from "@opentui/solid"
2
+ import { createSimpleContext } from "./helper"
3
+ import { FormatError, FormatUnknownError } from "@/cli/error"
4
+ import { win32FlushInputBuffer } from "../win32"
5
+ type Exit = ((reason?: unknown) => Promise<void>) & {
6
+ message: {
7
+ set: (value?: string) => () => void
8
+ clear: () => void
9
+ get: () => string | undefined
10
+ }
11
+ }
12
+
13
+ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
14
+ name: "Exit",
15
+ init: (input: { onBeforeExit?: () => Promise<void>; onExit?: () => Promise<void> }) => {
16
+ const renderer = useRenderer()
17
+ let message: string | undefined
18
+ let task: Promise<void> | undefined
19
+ const store = {
20
+ set: (value?: string) => {
21
+ const prev = message
22
+ message = value
23
+ return () => {
24
+ message = prev
25
+ }
26
+ },
27
+ clear: () => {
28
+ message = undefined
29
+ },
30
+ get: () => message,
31
+ }
32
+ const exit: Exit = Object.assign(
33
+ (reason?: unknown) => {
34
+ if (task) return task
35
+ task = (async () => {
36
+ await input.onBeforeExit?.()
37
+ // Reset window title before destroying renderer
38
+ renderer.setTerminalTitle("")
39
+ renderer.destroy()
40
+ win32FlushInputBuffer()
41
+ if (reason) {
42
+ const formatted = FormatError(reason) ?? FormatUnknownError(reason)
43
+ if (formatted) {
44
+ process.stderr.write(formatted + "\n")
45
+ }
46
+ }
47
+ const text = store.get()
48
+ if (text) process.stdout.write(text + "\n")
49
+ await input.onExit?.()
50
+ })()
51
+ return task
52
+ },
53
+ {
54
+ message: store,
55
+ },
56
+ )
57
+ process.on("SIGHUP", () => exit())
58
+ return exit
59
+ },
60
+ })
@@ -0,0 +1,25 @@
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
+ provider: (props: ParentProps<Props>) => {
11
+ const init = input.init(props)
12
+ return (
13
+ // @ts-expect-error
14
+ <Show when={init.ready === undefined || init.ready === true}>
15
+ <ctx.Provider value={init}>{props.children}</ctx.Provider>
16
+ </Show>
17
+ )
18
+ },
19
+ use() {
20
+ const value = useContext(ctx)
21
+ if (!value) throw new Error(`${input.name} context must be used within a context provider`)
22
+ return value
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,81 @@
1
+ import { rename, readFile, writeFile, rm, mkdir } from "fs/promises"
2
+ import { createSignal, type Setter } from "solid-js"
3
+ import { createStore, unwrap } from "solid-js/store"
4
+ import { createSimpleContext } from "./helper"
5
+ import path from "path"
6
+ import os from "os"
7
+
8
+ const APX_STATE_DIR = path.join(os.homedir(), ".apx", "tui-state")
9
+
10
+ async function readJson<T>(filePath: string): Promise<T | undefined> {
11
+ try {
12
+ const text = await readFile(filePath, "utf-8")
13
+ return JSON.parse(text) as T
14
+ } catch {
15
+ return undefined
16
+ }
17
+ }
18
+
19
+ async function writeJson(filePath: string, data: unknown): Promise<void> {
20
+ await mkdir(path.dirname(filePath), { recursive: true })
21
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`
22
+ await writeFile(tempPath, JSON.stringify(data, null, 2), "utf-8")
23
+ try {
24
+ await rename(tempPath, filePath)
25
+ } catch (error) {
26
+ await rm(tempPath, { force: true }).catch(() => undefined)
27
+ throw error
28
+ }
29
+ }
30
+
31
+ export const { use: useKV, provider: KVProvider } = createSimpleContext({
32
+ name: "KV",
33
+ init: () => {
34
+ const [ready, setReady] = createSignal(false)
35
+ const [store, setStore] = createStore<Record<string, any>>()
36
+ const filePath = path.join(APX_STATE_DIR, "kv.json")
37
+ let write = Promise.resolve()
38
+
39
+ readJson<Record<string, any>>(filePath)
40
+ .then((x) => {
41
+ if (x) setStore(x)
42
+ })
43
+ .catch((error) => {
44
+ console.error("Failed to read KV state", { filePath, error })
45
+ })
46
+ .finally(() => {
47
+ setReady(true)
48
+ })
49
+
50
+ const result = {
51
+ get ready() {
52
+ return ready()
53
+ },
54
+ get store() {
55
+ return store
56
+ },
57
+ signal<T>(name: string, defaultValue: T) {
58
+ if (store[name] === undefined) setStore(name, defaultValue)
59
+ return [
60
+ function () {
61
+ return result.get(name)
62
+ },
63
+ function setter(next: Setter<T>) {
64
+ result.set(name, next)
65
+ },
66
+ ] as const
67
+ },
68
+ get(key: string, defaultValue?: any) {
69
+ return store[key] ?? defaultValue
70
+ },
71
+ set(key: string, value: any) {
72
+ setStore(key, value)
73
+ const snapshot = structuredClone(unwrap(store))
74
+ write = write.then(() => writeJson(filePath, snapshot)).catch((error) => {
75
+ console.error("Failed to write KV state", { filePath, error })
76
+ })
77
+ },
78
+ }
79
+ return result
80
+ },
81
+ })