@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,34 @@
1
+ /**
2
+ * Aggregate Promise.allSettled results into a single Error that names every
3
+ * failed endpoint, or return null when all fulfilled. Used at TUI bootstrap
4
+ * boundaries so a single 4xx doesn't drown its parallel siblings as
5
+ * unhandled rejections — every failure surfaces in one labeled message.
6
+ */
7
+ export type LabeledSettled = {
8
+ name: string
9
+ result: PromiseSettledResult<unknown>
10
+ }
11
+
12
+ export function aggregateFailures(labeled: LabeledSettled[]): Error | null {
13
+ const failed = labeled.filter(
14
+ (x): x is { name: string; result: PromiseRejectedResult } => x.result.status === "rejected",
15
+ )
16
+ if (failed.length === 0) return null
17
+
18
+ const reasons = failed.map((f) => `${f.name}: ${reasonMessage(f.result.reason)}`).join("; ")
19
+ const summary = `${failed.length} of ${labeled.length} requests failed: ${reasons}`
20
+ const err = new Error(summary)
21
+ err.cause = { failures: failed.map((f) => ({ name: f.name, reason: f.result.reason })) }
22
+ return err
23
+ }
24
+
25
+ function reasonMessage(reason: unknown): string {
26
+ if (reason instanceof Error) return reason.message
27
+ if (typeof reason === "string") return reason
28
+ if (reason && typeof reason === "object") {
29
+ const obj = reason as { message?: unknown; name?: unknown }
30
+ if (typeof obj.message === "string") return obj.message
31
+ if (typeof obj.name === "string") return obj.name
32
+ }
33
+ return String(reason)
34
+ }
@@ -0,0 +1,15 @@
1
+ import { createSimpleContext } from "./helper"
2
+
3
+ export interface Args {
4
+ model?: string
5
+ agent?: string
6
+ prompt?: string
7
+ continue?: boolean
8
+ sessionID?: string
9
+ fork?: boolean
10
+ }
11
+
12
+ export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({
13
+ name: "Args",
14
+ init: (props: Args) => props,
15
+ })
@@ -0,0 +1,163 @@
1
+ import { createContext, createMemo, createSignal, useContext, type Accessor, type ParentProps } from "solid-js"
2
+ import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
3
+ import { useDialog, type DialogContext } from "@tui/ui/dialog"
4
+ import {
5
+ formatKeyBindings,
6
+ reactiveMatcherFromSignal,
7
+ type OpenTuiKeymap,
8
+ useKeymapSelector,
9
+ useOpencodeKeymap,
10
+ } from "../keymap"
11
+ import { useTuiConfig } from "./tui-config"
12
+
13
+ type SlashEntry = {
14
+ display: string
15
+ description?: string
16
+ aliases?: string[]
17
+ onSelect: () => void
18
+ }
19
+
20
+ type CommandPaletteContext = {
21
+ run(command: string): void
22
+ show(): void
23
+ slashes: Accessor<readonly SlashEntry[]>
24
+ suspend(enabled: boolean): void
25
+ readonly suspended: boolean
26
+ matcher: ReturnType<typeof reactiveMatcherFromSignal>
27
+ }
28
+
29
+ const COMMAND_PALETTE_DIALOG = "command.palette.show"
30
+ const ctx = createContext<CommandPaletteContext>()
31
+ type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]
32
+
33
+ function isVisiblePaletteCommand(entry: PaletteCommandEntry) {
34
+ return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_DIALOG
35
+ }
36
+
37
+ function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
38
+ const suggested = entry.command.suggested
39
+ if (typeof suggested === "boolean") return suggested
40
+ if (typeof suggested === "function") return suggested() === true
41
+ return false
42
+ }
43
+
44
+ export function CommandPaletteProvider(props: ParentProps) {
45
+ const dialog = useDialog()
46
+ const keymap = useOpencodeKeymap()
47
+ const [suspendCount, setSuspendCount] = createSignal(0)
48
+ const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
49
+ keymap
50
+ .getCommandEntries({
51
+ visibility: "reachable",
52
+ namespace: "palette",
53
+ })
54
+ .filter(isVisiblePaletteCommand),
55
+ )
56
+
57
+ const run = (command: string) => {
58
+ keymap.dispatchCommand(command)
59
+ }
60
+
61
+ const slashes = createMemo<SlashEntry[]>(() =>
62
+ entries().flatMap((entry) => {
63
+ const slashName = entry.command.slashName
64
+ if (typeof slashName !== "string" || !slashName) return []
65
+ const slashAliases = entry.command.slashAliases
66
+ return {
67
+ display: `/${slashName}`,
68
+ description:
69
+ typeof entry.command.desc === "string"
70
+ ? entry.command.desc
71
+ : typeof entry.command.title === "string"
72
+ ? entry.command.title
73
+ : undefined,
74
+ aliases: Array.isArray(slashAliases)
75
+ ? slashAliases.filter((alias): alias is string => typeof alias === "string").map((alias) => `/${alias}`)
76
+ : undefined,
77
+ onSelect: () => run(entry.command.name),
78
+ }
79
+ }),
80
+ )
81
+
82
+ const value: CommandPaletteContext = {
83
+ run,
84
+ show() {
85
+ dialog.replace(() => <CommandPaletteDialog run={run} />)
86
+ },
87
+ slashes,
88
+ suspend(enabled: boolean) {
89
+ setSuspendCount((count) => Math.max(0, count + (enabled ? 1 : -1)))
90
+ },
91
+ get suspended() {
92
+ return suspendCount() > 0 || dialog.stack.length > 0
93
+ },
94
+ matcher: reactiveMatcherFromSignal(() => suspendCount() === 0 && dialog.stack.length === 0),
95
+ }
96
+
97
+ return <ctx.Provider value={value}>{props.children}</ctx.Provider>
98
+ }
99
+
100
+ export function useCommandPalette() {
101
+ const value = useContext(ctx)
102
+ if (!value) throw new Error("CommandPalette context must be used within a CommandPaletteProvider")
103
+ return value
104
+ }
105
+
106
+ function CommandPaletteDialog(props: { run(command: string): void }) {
107
+ const config = useTuiConfig()
108
+ const entries = useKeymapSelector((keymap: OpenTuiKeymap) => {
109
+ const query = {
110
+ namespace: "palette",
111
+ }
112
+ const reachable = keymap
113
+ .getCommandEntries({
114
+ ...query,
115
+ visibility: "reachable",
116
+ })
117
+ .filter(isVisiblePaletteCommand)
118
+ const registeredBindings = keymap.getCommandBindings({
119
+ visibility: "registered",
120
+ commands: reachable.map((entry) => entry.command.name),
121
+ })
122
+
123
+ return reachable.map((entry) => ({
124
+ ...entry,
125
+ bindings: registeredBindings.get(entry.command.name) ?? entry.bindings,
126
+ }))
127
+ })
128
+ const options = createMemo(() =>
129
+ entries().map((entry) => ({
130
+ title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name,
131
+ description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
132
+ category: typeof entry.command.category === "string" ? entry.command.category : undefined,
133
+ footer: formatKeyBindings(entry.bindings, config),
134
+ value: entry.command.name,
135
+ suggested: isSuggestedPaletteCommand(entry),
136
+ onSelect: (dialog: DialogContext) => {
137
+ dialog.clear()
138
+ props.run(entry.command.name)
139
+ },
140
+ })),
141
+ )
142
+
143
+ let ref: DialogSelectRef<string>
144
+ const list = () => {
145
+ if (ref?.filter) return options()
146
+ return [
147
+ ...options()
148
+ .filter((option) => option.suggested)
149
+ .map((option) => ({
150
+ ...option,
151
+ value: `suggested:${option.value}`,
152
+ category: "Suggested",
153
+ })),
154
+ ...options(),
155
+ ]
156
+ }
157
+
158
+ return <DialogSelect ref={(value) => (ref = value)} title="Commands" options={list()} />
159
+ }
160
+
161
+ export function useCommandSlashes(): Accessor<readonly SlashEntry[]> {
162
+ return useCommandPalette().slashes
163
+ }
@@ -0,0 +1,15 @@
1
+ import { createMemo } from "solid-js"
2
+ import { useProject } from "./project"
3
+ import { useSync } from "./sync"
4
+ import { Global } from "@opencode-ai/core/global"
5
+
6
+ export function useDirectory() {
7
+ const project = useProject()
8
+ const sync = useSync()
9
+ return createMemo(() => {
10
+ const directory = project.instance.path().directory || process.cwd()
11
+ const result = directory.replace(Global.Path.home, "~")
12
+ if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
13
+ return result
14
+ })
15
+ }
@@ -0,0 +1,283 @@
1
+ import { Database } from "bun:sqlite"
2
+ import os from "node:os"
3
+ import path from "node:path"
4
+ import { Option, Schema } from "effect"
5
+ import { Filesystem } from "@/util/filesystem"
6
+ import type { EditorSelection } from "./editor"
7
+
8
+ const ZedEditorRowSchema = Schema.Struct({
9
+ item_kind: Schema.String,
10
+ editor_id: Schema.NullOr(Schema.Number),
11
+ workspace_id: Schema.Number,
12
+ workspace_paths: Schema.NullOr(Schema.String),
13
+ timestamp: Schema.String,
14
+ buffer_path: Schema.NullOr(Schema.String),
15
+ })
16
+
17
+ const ZedSelectionRowSchema = Schema.Struct({
18
+ selection_start: Schema.NullOr(Schema.Number),
19
+ selection_end: Schema.NullOr(Schema.Number),
20
+ })
21
+
22
+ const ZedEditorContentsSchema = Schema.Struct({
23
+ contents: Schema.NullOr(Schema.String),
24
+ })
25
+
26
+ const decodeZedEditorRow = Schema.decodeUnknownOption(ZedEditorRowSchema)
27
+ const decodeZedSelectionRow = Schema.decodeUnknownOption(ZedSelectionRowSchema)
28
+ const decodeZedEditorContents = Schema.decodeUnknownOption(ZedEditorContentsSchema)
29
+
30
+ const utf8 = new TextEncoder()
31
+
32
+ type ZedEditorRow = Schema.Schema.Type<typeof ZedEditorRowSchema>
33
+ type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
34
+
35
+ export type ZedSelectionResult =
36
+ | { type: "selection"; selection: EditorSelection }
37
+ | { type: "empty" }
38
+ | { type: "unavailable" }
39
+
40
+ export async function resolveZedSelection(dbPath: string, cwd = process.cwd()): Promise<ZedSelectionResult> {
41
+ const active = queryZedActiveEditor(dbPath, cwd)
42
+ if (active.type !== "row") return active
43
+
44
+ const row = active.row
45
+ if (!row.buffer_path) return { type: "empty" }
46
+
47
+ const selections = queryZedEditorSelections(dbPath, row)
48
+ if (selections.type !== "selections") return selections
49
+ const byteRanges = selections.selections
50
+ .flatMap((selection) => {
51
+ if (selection.selection_start == null || selection.selection_end == null) return []
52
+ return [
53
+ {
54
+ start: Math.min(selection.selection_start, selection.selection_end),
55
+ end: Math.max(selection.selection_start, selection.selection_end),
56
+ },
57
+ ]
58
+ })
59
+ .sort((left, right) => left.start - right.start || left.end - right.end)
60
+ if (byteRanges.length === 0) return { type: "unavailable" }
61
+
62
+ const contents = queryZedEditorContents(dbPath, row)
63
+ const text =
64
+ contents.type === "contents" && contents.contents != null
65
+ ? contents.contents
66
+ : await Bun.file(row.buffer_path)
67
+ .text()
68
+ .catch(() => undefined)
69
+ if (text == null) return { type: "unavailable" }
70
+
71
+ const ranges = byteRanges.map((range) => {
72
+ const startOffset = utf8ByteOffsetToStringIndex(text, range.start)
73
+ const endOffset = utf8ByteOffsetToStringIndex(text, range.end)
74
+ return {
75
+ text: text.slice(startOffset, endOffset),
76
+ selection: offsetsToSelection(text, startOffset, endOffset),
77
+ }
78
+ })
79
+
80
+ return {
81
+ type: "selection",
82
+ selection: {
83
+ filePath: row.buffer_path,
84
+ source: "zed",
85
+ ranges,
86
+ },
87
+ }
88
+ }
89
+
90
+ function queryZedActiveEditor(dbPath: string, cwd: string) {
91
+ let db: Database | undefined
92
+ try {
93
+ db = new Database(dbPath, { readonly: true })
94
+ const raw = db
95
+ .query(
96
+ `select
97
+ i.kind as item_kind,
98
+ e.item_id as editor_id,
99
+ i.workspace_id as workspace_id,
100
+ w.paths as workspace_paths,
101
+ w.timestamp as timestamp,
102
+ e.buffer_path as buffer_path
103
+ from items i
104
+ join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id
105
+ join workspaces w on w.workspace_id = i.workspace_id
106
+ left join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id
107
+ where i.active = 1 and p.active = 1
108
+ order by w.timestamp desc`,
109
+ )
110
+ .all()
111
+
112
+ const rows = raw.flatMap((row) => {
113
+ const parsed = decodeZedEditorRow(row)
114
+ return Option.isSome(parsed) ? [parsed.value] : []
115
+ })
116
+
117
+ if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
118
+
119
+ const row = rows
120
+ .map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) }))
121
+ .filter((entry) => entry.score > 0)
122
+ .sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row
123
+ if (!row) return { type: "empty" as const }
124
+ if (row.item_kind !== "Editor") return { type: "unavailable" as const }
125
+ if (!isZedActiveEditorRow(row)) return { type: "empty" as const }
126
+ return { type: "row" as const, row }
127
+ } catch {
128
+ return { type: "unavailable" as const }
129
+ } finally {
130
+ db?.close()
131
+ }
132
+ }
133
+
134
+ function queryZedEditorSelections(dbPath: string, row: ZedActiveEditorRow) {
135
+ let db: Database | undefined
136
+ try {
137
+ db = new Database(dbPath, { readonly: true })
138
+ const raw = db
139
+ .query(
140
+ `select
141
+ start as selection_start,
142
+ end as selection_end
143
+ from editor_selections
144
+ where editor_id = $editorID and workspace_id = $workspaceID`,
145
+ )
146
+ .all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
147
+
148
+ const selections = raw.flatMap((selection) => {
149
+ const parsed = decodeZedSelectionRow(selection)
150
+ return Option.isSome(parsed) ? [parsed.value] : []
151
+ })
152
+
153
+ if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const }
154
+ return { type: "selections" as const, selections }
155
+ } catch {
156
+ return { type: "unavailable" as const }
157
+ } finally {
158
+ db?.close()
159
+ }
160
+ }
161
+
162
+ function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
163
+ let db: Database | undefined
164
+ try {
165
+ db = new Database(dbPath, { readonly: true })
166
+ const parsed = decodeZedEditorContents(
167
+ db
168
+ .query(
169
+ `select contents
170
+ from editors
171
+ where item_id = $editorID and workspace_id = $workspaceID`,
172
+ )
173
+ .get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }),
174
+ )
175
+ if (Option.isNone(parsed)) return { type: "unavailable" as const }
176
+ return { type: "contents" as const, contents: parsed.value.contents }
177
+ } catch {
178
+ return { type: "unavailable" as const }
179
+ } finally {
180
+ db?.close()
181
+ }
182
+ }
183
+
184
+ function isZedActiveEditorRow(row: ZedEditorRow): row is ZedActiveEditorRow {
185
+ return row.item_kind === "Editor" && row.editor_id != null
186
+ }
187
+
188
+ export function resolveZedDbPath() {
189
+ const candidates = [
190
+ process.env.OPENCODE_ZED_DB,
191
+ path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"),
192
+ path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"),
193
+ ].filter((item): item is string => Boolean(item))
194
+
195
+ return candidates.find((item) => isFile(item))
196
+ }
197
+
198
+ function isFile(item: string) {
199
+ try {
200
+ return Filesystem.stat(item)?.isFile() === true
201
+ } catch {
202
+ return false
203
+ }
204
+ }
205
+
206
+ function scoreZedWorkspace(workspacePaths: string | null, cwd: string) {
207
+ return zedWorkspacePaths(workspacePaths).reduce((score, item) => {
208
+ if (pathContains(item, cwd)) return Math.max(score, path.resolve(item).length)
209
+ return score
210
+ }, 0)
211
+ }
212
+
213
+ function zedWorkspacePaths(value: string | null) {
214
+ if (!value) return []
215
+ const parsed = parseJson(value)
216
+ if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string")
217
+ return value.split(/\r?\n/).filter(Boolean)
218
+ }
219
+
220
+ export function offsetToPosition(text: string, offset: number) {
221
+ const stringOffset = utf8ByteOffsetToStringIndex(text, offset)
222
+ return offsetsToSelection(text, stringOffset, stringOffset).start
223
+ }
224
+
225
+ function utf8ByteOffsetToStringIndex(text: string, byteOffset: number) {
226
+ if (byteOffset <= 0) return 0
227
+
228
+ let bytes = 0
229
+ for (let index = 0; index < text.length; ) {
230
+ const codePoint = text.codePointAt(index)
231
+ if (codePoint === undefined) return text.length
232
+
233
+ const nextIndex = index + (codePoint > 0xffff ? 2 : 1)
234
+ bytes += utf8.encode(text.slice(index, nextIndex)).length
235
+ if (bytes >= byteOffset) return nextIndex
236
+ index = nextIndex
237
+ }
238
+
239
+ return text.length
240
+ }
241
+
242
+ function offsetsToSelection(text: string, startOffset: number, endOffset: number) {
243
+ const start = Math.max(0, Math.min(startOffset, text.length))
244
+ const end = Math.max(0, Math.min(endOffset, text.length))
245
+ let line = 1
246
+ let lineStart = 0
247
+ let startPosition = position(line, lineStart, start)
248
+ let endPosition = position(line, lineStart, end)
249
+
250
+ for (let index = 0; index <= end; index++) {
251
+ if (index === start) startPosition = position(line, lineStart, index)
252
+ if (index === end) {
253
+ endPosition = position(line, lineStart, index)
254
+ break
255
+ }
256
+ if (text[index] === "\n") {
257
+ line += 1
258
+ lineStart = index + 1
259
+ }
260
+ }
261
+
262
+ return { start: startPosition, end: endPosition }
263
+ }
264
+
265
+ function position(line: number, lineStart: number, offset: number) {
266
+ return {
267
+ line,
268
+ character: offset - lineStart + 1,
269
+ }
270
+ }
271
+
272
+ function pathContains(parent: string, child: string) {
273
+ const relative = path.relative(path.resolve(parent), path.resolve(child))
274
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
275
+ }
276
+
277
+ function parseJson(value: string) {
278
+ try {
279
+ return JSON.parse(value) as unknown
280
+ } catch {
281
+ return
282
+ }
283
+ }