@agentprojectcontext/apx 1.15.5 → 1.16.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.
- package/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/wakeup.js +14 -19
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- 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,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
|
+
})
|