@agentprojectcontext/apx 1.15.6 → 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/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,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
|
+
}
|