@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,111 @@
1
+ import { createContext, useContext, type ParentProps, Show } from "solid-js"
2
+ import { createStore } from "solid-js/store"
3
+ import { useTheme } from "@tui/context/theme"
4
+ import { useTerminalDimensions } from "@opentui/solid"
5
+ import { SplitBorder } from "../component/border"
6
+ import { TextAttributes } from "@opentui/core"
7
+
8
+ export type ToastOptions = {
9
+ title?: string
10
+ message: string
11
+ variant: "info" | "success" | "warning" | "error"
12
+ duration?: number
13
+ }
14
+
15
+ const DEFAULT_TOAST_DURATION = 5000
16
+
17
+ export type ToastInput = Omit<ToastOptions, "duration"> & { duration?: number }
18
+
19
+ export function Toast() {
20
+ const toast = useToast()
21
+ const { theme } = useTheme()
22
+ const dimensions = useTerminalDimensions()
23
+
24
+ return (
25
+ <Show when={toast.currentToast}>
26
+ {(current) => (
27
+ <box
28
+ position="absolute"
29
+ justifyContent="center"
30
+ alignItems="flex-start"
31
+ top={2}
32
+ right={2}
33
+ maxWidth={Math.min(60, dimensions().width - 6)}
34
+ paddingLeft={2}
35
+ paddingRight={2}
36
+ paddingTop={1}
37
+ paddingBottom={1}
38
+ backgroundColor={theme.backgroundPanel}
39
+ borderColor={(theme as any)[current().variant]}
40
+ border={["left", "right"]}
41
+ customBorderChars={SplitBorder.customBorderChars}
42
+ >
43
+ <Show when={current().title}>
44
+ <text attributes={TextAttributes.BOLD} marginBottom={1} fg={theme.text}>
45
+ {current().title}
46
+ </text>
47
+ </Show>
48
+ <text fg={theme.text} wrapMode="word" width="100%">
49
+ {current().message}
50
+ </text>
51
+ </box>
52
+ )}
53
+ </Show>
54
+ )
55
+ }
56
+
57
+ function init() {
58
+ const [store, setStore] = createStore({
59
+ currentToast: null as ToastOptions | null,
60
+ })
61
+
62
+ let timeoutHandle: NodeJS.Timeout | null = null
63
+
64
+ const toast = {
65
+ show(options: ToastInput) {
66
+ const toastOptions: ToastOptions = {
67
+ title: options.title,
68
+ message: options.message,
69
+ variant: options.variant,
70
+ duration: options.duration ?? DEFAULT_TOAST_DURATION,
71
+ }
72
+ setStore("currentToast", toastOptions)
73
+ if (timeoutHandle) clearTimeout(timeoutHandle)
74
+ timeoutHandle = setTimeout(() => {
75
+ setStore("currentToast", null)
76
+ }, toastOptions.duration).unref()
77
+ },
78
+ error: (err: any) => {
79
+ if (err instanceof Error)
80
+ return toast.show({
81
+ variant: "error",
82
+ message: err.message,
83
+ })
84
+ toast.show({
85
+ variant: "error",
86
+ message: "An unknown error has occurred",
87
+ })
88
+ },
89
+ get currentToast(): ToastOptions | null {
90
+ return store.currentToast
91
+ },
92
+ }
93
+ return toast
94
+ }
95
+
96
+ export type ToastContext = ReturnType<typeof init>
97
+
98
+ const ctx = createContext<ToastContext>()
99
+
100
+ export function ToastProvider(props: ParentProps) {
101
+ const value = init()
102
+ return <ctx.Provider value={value}>{props.children}</ctx.Provider>
103
+ }
104
+
105
+ export function useToast() {
106
+ const value = useContext(ctx)
107
+ if (!value) {
108
+ throw new Error("useToast must be used within a ToastProvider")
109
+ }
110
+ return value
111
+ }
@@ -0,0 +1,217 @@
1
+ import { platform, release } from "os"
2
+ import { tmpdir } from "os"
3
+ import path from "path"
4
+ import fs from "fs/promises"
5
+ import { Filesystem } from "@/util/filesystem"
6
+ import { Process } from "@/util/process"
7
+
8
+ // Simple lazy helper (inlined to avoid path issues)
9
+ function lazy<T>(fn: () => Promise<T>): () => Promise<T> {
10
+ let result: T | undefined
11
+ let called = false
12
+ return async () => {
13
+ if (!called) {
14
+ called = true
15
+ result = await fn()
16
+ }
17
+ return result as T
18
+ }
19
+ }
20
+
21
+ // Lazy load which and clipboardy to avoid expensive execa/which/isexe chain at startup
22
+ const getWhich = lazy(async () => {
23
+ const { which } = await import("../../../../util/which")
24
+ return which
25
+ })
26
+
27
+ const getClipboardy = lazy(async () => {
28
+ const { default: clipboardy } = await import("clipboardy")
29
+ return clipboardy
30
+ })
31
+
32
+ /**
33
+ * Writes text to clipboard via OSC 52 escape sequence.
34
+ * This allows clipboard operations to work over SSH by having
35
+ * the terminal emulator handle the clipboard locally.
36
+ */
37
+ function writeOsc52(text: string): void {
38
+ if (!process.stdout.isTTY) return
39
+ const base64 = Buffer.from(text).toString("base64")
40
+ const osc52 = `\x1b]52;c;${base64}\x07`
41
+ const passthrough = process.env["TMUX"] || process.env["STY"]
42
+ const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52
43
+ process.stdout.write(sequence)
44
+ }
45
+
46
+ export interface Content {
47
+ data: string
48
+ mime: string
49
+ }
50
+
51
+ // Checks clipboard for images first, then falls back to text.
52
+ //
53
+ // On Windows prompt/ can call this from multiple paste signals because
54
+ // terminals surface image paste differently:
55
+ // 1. A forwarded Ctrl+V keypress
56
+ // 2. An empty bracketed-paste hint for image-only clipboard in Windows
57
+ // Terminal <1.25
58
+ // 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
59
+ export async function read(): Promise<Content | undefined> {
60
+ const os = platform()
61
+
62
+ if (os === "darwin") {
63
+ const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
64
+ try {
65
+ await Process.run(
66
+ [
67
+ "osascript",
68
+ "-e",
69
+ 'set imageData to the clipboard as "PNGf"',
70
+ "-e",
71
+ `set fileRef to open for access POSIX file "${tmpfile}" with write permission`,
72
+ "-e",
73
+ "set eof fileRef to 0",
74
+ "-e",
75
+ "write imageData to fileRef",
76
+ "-e",
77
+ "close access fileRef",
78
+ ],
79
+ { nothrow: true },
80
+ )
81
+ const buffer = await Filesystem.readBytes(tmpfile)
82
+ return { data: buffer.toString("base64"), mime: "image/png" }
83
+ } catch {
84
+ } finally {
85
+ await fs.rm(tmpfile, { force: true }).catch(() => {})
86
+ }
87
+ }
88
+
89
+ // Windows/WSL: probe clipboard for images via PowerShell.
90
+ // Bracketed paste can't carry image data so we read it directly.
91
+ if (os === "win32" || release().includes("WSL")) {
92
+ const script =
93
+ "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
94
+ const base64 = await Process.text(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], {
95
+ nothrow: true,
96
+ })
97
+ if (base64.text) {
98
+ const imageBuffer = Buffer.from(base64.text.trim(), "base64")
99
+ if (imageBuffer.length > 0) {
100
+ return { data: imageBuffer.toString("base64"), mime: "image/png" }
101
+ }
102
+ }
103
+ }
104
+
105
+ if (os === "linux") {
106
+ const wayland = await Process.run(["wl-paste", "-t", "image/png"], { nothrow: true })
107
+ if (wayland.stdout.byteLength > 0) {
108
+ return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" }
109
+ }
110
+ const x11 = await Process.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], {
111
+ nothrow: true,
112
+ })
113
+ if (x11.stdout.byteLength > 0) {
114
+ return { data: Buffer.from(x11.stdout).toString("base64"), mime: "image/png" }
115
+ }
116
+ }
117
+
118
+ const clipboardy = await getClipboardy()
119
+ const text = await clipboardy.read().catch(() => {})
120
+ if (text) {
121
+ return { data: text, mime: "text/plain" }
122
+ }
123
+ }
124
+
125
+ const getCopyMethod = lazy(async () => {
126
+ const os = platform()
127
+ const which = await getWhich()
128
+
129
+ if (os === "darwin" && which("osascript")) {
130
+ console.log("clipboard: using osascript")
131
+ return async (text: string) => {
132
+ const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
133
+ await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true })
134
+ }
135
+ }
136
+
137
+ if (os === "linux") {
138
+ if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) {
139
+ console.log("clipboard: using wl-copy")
140
+ return async (text: string) => {
141
+ const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
142
+ if (!proc.stdin) return
143
+ proc.stdin.write(text)
144
+ proc.stdin.end()
145
+ await proc.exited.catch(() => {})
146
+ }
147
+ }
148
+ if (which("xclip")) {
149
+ console.log("clipboard: using xclip")
150
+ return async (text: string) => {
151
+ const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
152
+ stdin: "pipe",
153
+ stdout: "ignore",
154
+ stderr: "ignore",
155
+ })
156
+ if (!proc.stdin) return
157
+ proc.stdin.write(text)
158
+ proc.stdin.end()
159
+ await proc.exited.catch(() => {})
160
+ }
161
+ }
162
+ if (which("xsel")) {
163
+ console.log("clipboard: using xsel")
164
+ return async (text: string) => {
165
+ const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
166
+ stdin: "pipe",
167
+ stdout: "ignore",
168
+ stderr: "ignore",
169
+ })
170
+ if (!proc.stdin) return
171
+ proc.stdin.write(text)
172
+ proc.stdin.end()
173
+ await proc.exited.catch(() => {})
174
+ }
175
+ }
176
+ }
177
+
178
+ if (os === "win32") {
179
+ console.log("clipboard: using powershell")
180
+ return async (text: string) => {
181
+ // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
182
+ const proc = Process.spawn(
183
+ [
184
+ "powershell.exe",
185
+ "-NonInteractive",
186
+ "-NoProfile",
187
+ "-Command",
188
+ "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
189
+ ],
190
+ {
191
+ stdin: "pipe",
192
+ stdout: "ignore",
193
+ stderr: "ignore",
194
+ },
195
+ )
196
+
197
+ if (!proc.stdin) return
198
+ proc.stdin.write(text)
199
+ proc.stdin.end()
200
+ await proc.exited.catch(() => {})
201
+ }
202
+ }
203
+
204
+ console.log("clipboard: no native support")
205
+ return async (text: string) => {
206
+ const clipboardy = await getClipboardy()
207
+ await clipboardy.write(text).catch(() => {})
208
+ }
209
+ })
210
+
211
+ export async function copy(text: string): Promise<void> {
212
+ writeOsc52(text)
213
+ const method = await getCopyMethod()
214
+ await method(text)
215
+ }
216
+
217
+ export * as Clipboard from "./clipboard"
@@ -0,0 +1,37 @@
1
+ import { defer } from "@/util/defer"
2
+ import { rm } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+ import { CliRenderer } from "@opentui/core"
6
+ import { Filesystem } from "@/util/filesystem"
7
+ import { Process } from "@/util/process"
8
+
9
+ export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
10
+ const editor = process.env["VISUAL"] || process.env["EDITOR"]
11
+ if (!editor) return
12
+
13
+ const filepath = join(tmpdir(), `${Date.now()}.md`)
14
+ await using _ = defer(async () => rm(filepath, { force: true }))
15
+
16
+ await Filesystem.write(filepath, opts.value)
17
+ opts.renderer.suspend()
18
+ opts.renderer.currentRenderBuffer.clear()
19
+ try {
20
+ const parts = editor.split(" ")
21
+ const proc = Process.spawn([...parts, filepath], {
22
+ stdin: "inherit",
23
+ stdout: "inherit",
24
+ stderr: "inherit",
25
+ shell: process.platform === "win32",
26
+ })
27
+ await proc.exited
28
+ const content = await Filesystem.readText(filepath)
29
+ return content || undefined
30
+ } finally {
31
+ opts.renderer.currentRenderBuffer.clear()
32
+ opts.renderer.resume()
33
+ opts.renderer.requestRender()
34
+ }
35
+ }
36
+
37
+ export * as Editor from "./editor"
@@ -0,0 +1,23 @@
1
+ import type { Provider } from "@opencode-ai/sdk/v2"
2
+
3
+ export function index(list: Provider[] | undefined) {
4
+ return new Map((list ?? []).map((item) => [item.id, item] as const))
5
+ }
6
+
7
+ export function get(list: Provider[] | ReadonlyMap<string, Provider> | undefined, providerID: string, modelID: string) {
8
+ const provider =
9
+ list instanceof Map
10
+ ? list.get(providerID)
11
+ : Array.isArray(list)
12
+ ? list.find((item) => item.id === providerID)
13
+ : undefined
14
+ return provider?.models[modelID]
15
+ }
16
+
17
+ export function name(
18
+ list: Provider[] | ReadonlyMap<string, Provider> | undefined,
19
+ providerID: string,
20
+ modelID: string,
21
+ ) {
22
+ return get(list, providerID, modelID)?.name ?? modelID
23
+ }
@@ -0,0 +1,7 @@
1
+ const contains = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
2
+ Array.isArray(consoleManagedProviders)
3
+ ? consoleManagedProviders.includes(providerID)
4
+ : consoleManagedProviders.has(providerID)
5
+
6
+ export const isConsoleManagedProvider = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
7
+ contains(consoleManagedProviders, providerID)
@@ -0,0 +1,18 @@
1
+ import { parsePatch } from "diff"
2
+
3
+ export function getRevertDiffFiles(diffText: string) {
4
+ if (!diffText) return []
5
+
6
+ try {
7
+ return parsePatch(diffText).map((patch) => {
8
+ const filename = [patch.newFileName, patch.oldFileName].find((item) => item && item !== "/dev/null") ?? "unknown"
9
+ return {
10
+ filename: filename.replace(/^[ab]\//, ""),
11
+ additions: patch.hunks.reduce((sum, hunk) => sum + hunk.lines.filter((line) => line.startsWith("+")).length, 0),
12
+ deletions: patch.hunks.reduce((sum, hunk) => sum + hunk.lines.filter((line) => line.startsWith("-")).length, 0),
13
+ }
14
+ })
15
+ } catch {
16
+ return []
17
+ }
18
+ }
@@ -0,0 +1,25 @@
1
+ import { MacOSScrollAccel, type ScrollAcceleration } from "@opentui/core"
2
+ import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
3
+
4
+ export class CustomSpeedScroll implements ScrollAcceleration {
5
+ constructor(private speed: number) {}
6
+
7
+ tick(_now?: number): number {
8
+ return this.speed
9
+ }
10
+
11
+ reset(): void {}
12
+ }
13
+
14
+ export function getScrollAcceleration(
15
+ tuiConfig?: Pick<TuiConfig.Info, "scroll_acceleration" | "scroll_speed">,
16
+ ): ScrollAcceleration {
17
+ if (tuiConfig?.scroll_acceleration?.enabled) {
18
+ return new MacOSScrollAccel()
19
+ }
20
+ if (tuiConfig?.scroll_speed !== undefined) {
21
+ return new CustomSpeedScroll(tuiConfig.scroll_speed)
22
+ }
23
+
24
+ return new CustomSpeedScroll(3)
25
+ }
@@ -0,0 +1,65 @@
1
+ import * as Clipboard from "./clipboard"
2
+
3
+ type Toast = {
4
+ show: (input: { message: string; variant: "info" | "success" | "warning" | "error" }) => void
5
+ error: (err: unknown) => void
6
+ }
7
+
8
+ type FocusableSelectionTarget = {
9
+ hasSelection: () => boolean
10
+ }
11
+
12
+ type Renderer = {
13
+ getSelection: () => { getSelectedText: () => string; selectedRenderables: FocusableSelectionTarget[] } | null
14
+ clearSelection: () => void
15
+ currentFocusedRenderable?: FocusableSelectionTarget | null
16
+ }
17
+
18
+ type SelectionKeyEvent = {
19
+ ctrl?: boolean
20
+ name: string
21
+ preventDefault: () => void
22
+ stopPropagation: () => void
23
+ }
24
+
25
+ export function copy(renderer: Renderer, toast: Toast): boolean {
26
+ const text = renderer.getSelection()?.getSelectedText()
27
+ if (!text) return false
28
+
29
+ Clipboard.copy(text)
30
+ .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
31
+ .catch(toast.error)
32
+
33
+ renderer.clearSelection()
34
+ return true
35
+ }
36
+
37
+ export function handleSelectionKey(renderer: Renderer, toast: Toast, event: SelectionKeyEvent) {
38
+ const selection = renderer.getSelection()
39
+ if (!selection) return
40
+
41
+ if (event.ctrl && event.name === "c") {
42
+ if (!copy(renderer, toast)) {
43
+ renderer.clearSelection()
44
+ return
45
+ }
46
+
47
+ event.preventDefault()
48
+ event.stopPropagation()
49
+ return
50
+ }
51
+
52
+ if (event.name === "escape") {
53
+ renderer.clearSelection()
54
+ event.preventDefault()
55
+ event.stopPropagation()
56
+ return
57
+ }
58
+
59
+ const focus = renderer.currentFocusedRenderable
60
+ if (focus?.hasSelection() && selection.selectedRenderables.includes(focus)) return
61
+
62
+ renderer.clearSelection()
63
+ }
64
+
65
+ export * as Selection from "./selection"
@@ -0,0 +1,41 @@
1
+ import { createEffect, createSignal, on, onCleanup, type Accessor } from "solid-js"
2
+ import { debounce, type Scheduled } from "@solid-primitives/scheduled"
3
+
4
+ export function createDebouncedSignal<T>(value: T, ms: number): [Accessor<T>, Scheduled<[value: T]>] {
5
+ const [get, set] = createSignal(value)
6
+ return [get, debounce((v: T) => set(() => v), ms)]
7
+ }
8
+
9
+ export function createFadeIn(show: Accessor<boolean>, enabled: Accessor<boolean>) {
10
+ const [alpha, setAlpha] = createSignal(show() ? 1 : 0)
11
+ let revealed = show()
12
+
13
+ createEffect(
14
+ on([show, enabled], ([visible, animate]) => {
15
+ if (!visible) {
16
+ setAlpha(0)
17
+ return
18
+ }
19
+
20
+ if (!animate || revealed) {
21
+ revealed = true
22
+ setAlpha(1)
23
+ return
24
+ }
25
+
26
+ const start = performance.now()
27
+ revealed = true
28
+ setAlpha(0)
29
+
30
+ const timer = setInterval(() => {
31
+ const progress = Math.min((performance.now() - start) / 160, 1)
32
+ setAlpha(progress * progress * (3 - 2 * progress))
33
+ if (progress >= 1) clearInterval(timer)
34
+ }, 16)
35
+
36
+ onCleanup(() => clearInterval(timer))
37
+ }),
38
+ )
39
+
40
+ return alpha
41
+ }