@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,456 @@
|
|
|
1
|
+
import { createMemo, createSignal, onMount, Show } from "solid-js"
|
|
2
|
+
import { useSync } from "@tui/context/sync"
|
|
3
|
+
import { map, pipe, sortBy } from "remeda"
|
|
4
|
+
import { DialogSelect } from "@tui/ui/dialog-select"
|
|
5
|
+
import { useDialog } from "@tui/ui/dialog"
|
|
6
|
+
import { useSDK } from "../context/sdk"
|
|
7
|
+
import { DialogPrompt } from "../ui/dialog-prompt"
|
|
8
|
+
import { Link } from "../ui/link"
|
|
9
|
+
import { useTheme } from "../context/theme"
|
|
10
|
+
import { TextAttributes } from "@opentui/core"
|
|
11
|
+
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2"
|
|
12
|
+
import { DialogModel } from "./dialog-model"
|
|
13
|
+
import * as Clipboard from "@tui/util/clipboard"
|
|
14
|
+
import { useToast } from "../ui/toast"
|
|
15
|
+
import { isConsoleManagedProvider } from "@tui/util/provider-origin"
|
|
16
|
+
import { useConnected } from "./use-connected"
|
|
17
|
+
import { useBindings } from "../keymap"
|
|
18
|
+
|
|
19
|
+
const PROVIDER_PRIORITY: Record<string, number> = {
|
|
20
|
+
opencode: 0,
|
|
21
|
+
"opencode-go": 1,
|
|
22
|
+
openai: 2,
|
|
23
|
+
"github-copilot": 3,
|
|
24
|
+
anthropic: 4,
|
|
25
|
+
google: 5,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CUSTOM_PROVIDER_OPTION_VALUE = "__opencode_custom_provider__"
|
|
29
|
+
const CUSTOM_PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
|
|
30
|
+
|
|
31
|
+
type ProviderOptionBase = {
|
|
32
|
+
title: string
|
|
33
|
+
value: string
|
|
34
|
+
description?: string
|
|
35
|
+
category: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ProviderOption =
|
|
39
|
+
| (ProviderOptionBase & {
|
|
40
|
+
type: "provider"
|
|
41
|
+
providerID: string
|
|
42
|
+
})
|
|
43
|
+
| (ProviderOptionBase & {
|
|
44
|
+
type: "custom"
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export function providerOptions(list: { id: string; name: string }[]): ProviderOption[] {
|
|
48
|
+
return [
|
|
49
|
+
...pipe(
|
|
50
|
+
list,
|
|
51
|
+
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
|
|
52
|
+
map((provider) => ({
|
|
53
|
+
type: "provider" as const,
|
|
54
|
+
title: provider.name,
|
|
55
|
+
value: provider.id,
|
|
56
|
+
providerID: provider.id,
|
|
57
|
+
description: {
|
|
58
|
+
opencode: "(Recommended)",
|
|
59
|
+
anthropic: "(API key)",
|
|
60
|
+
openai: "(ChatGPT Plus/Pro or API key)",
|
|
61
|
+
"opencode-go": "Low cost subscription for everyone",
|
|
62
|
+
}[provider.id],
|
|
63
|
+
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Providers",
|
|
64
|
+
})),
|
|
65
|
+
),
|
|
66
|
+
{
|
|
67
|
+
type: "custom",
|
|
68
|
+
title: "Other",
|
|
69
|
+
value: CUSTOM_PROVIDER_OPTION_VALUE,
|
|
70
|
+
description: "Custom provider",
|
|
71
|
+
category: "Providers",
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function normalizeCustomProviderID(value: string) {
|
|
77
|
+
const providerID = value.trim().replace(/^@ai-sdk\//, "")
|
|
78
|
+
if (!CUSTOM_PROVIDER_ID.test(providerID)) return
|
|
79
|
+
return providerID
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createDialogProviderOptions() {
|
|
83
|
+
const sync = useSync()
|
|
84
|
+
const dialog = useDialog()
|
|
85
|
+
const sdk = useSDK()
|
|
86
|
+
const toast = useToast()
|
|
87
|
+
const { theme } = useTheme()
|
|
88
|
+
const onboarded = useConnected()
|
|
89
|
+
|
|
90
|
+
async function promptCustomProviderID(): Promise<string | undefined> {
|
|
91
|
+
const value = await DialogPrompt.show(dialog, "Other", {
|
|
92
|
+
placeholder: "Provider id",
|
|
93
|
+
description: () => (
|
|
94
|
+
<text fg={theme.textMuted}>
|
|
95
|
+
This only stores a credential. Configure the provider in opencode.json to use it.
|
|
96
|
+
</text>
|
|
97
|
+
),
|
|
98
|
+
})
|
|
99
|
+
if (value === null) return
|
|
100
|
+
|
|
101
|
+
const providerID = normalizeCustomProviderID(value)
|
|
102
|
+
if (providerID) return providerID
|
|
103
|
+
|
|
104
|
+
toast.show({
|
|
105
|
+
variant: "error",
|
|
106
|
+
message:
|
|
107
|
+
"Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores",
|
|
108
|
+
})
|
|
109
|
+
return promptCustomProviderID()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const options = createMemo(() => {
|
|
113
|
+
return pipe(
|
|
114
|
+
providerOptions(sync.data.provider_next.all),
|
|
115
|
+
map((provider) => {
|
|
116
|
+
if (provider.type === "custom") {
|
|
117
|
+
return {
|
|
118
|
+
title: provider.title,
|
|
119
|
+
value: provider.value,
|
|
120
|
+
description: provider.description,
|
|
121
|
+
category: provider.category,
|
|
122
|
+
async onSelect() {
|
|
123
|
+
const providerID = await promptCustomProviderID()
|
|
124
|
+
if (!providerID) return
|
|
125
|
+
return dialog.replace(() => <ApiMethod providerID={providerID} title="API key" custom />)
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const providerID = provider.providerID
|
|
131
|
+
const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, providerID)
|
|
132
|
+
const connected = sync.data.provider_next.connected.includes(providerID)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
title: provider.title,
|
|
136
|
+
value: provider.value,
|
|
137
|
+
description: provider.description,
|
|
138
|
+
footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,
|
|
139
|
+
category: provider.category,
|
|
140
|
+
gutter: connected && onboarded() ? () => <text fg={theme.success}>✓</text> : undefined,
|
|
141
|
+
async onSelect() {
|
|
142
|
+
if (consoleManaged) return
|
|
143
|
+
|
|
144
|
+
const methods = sync.data.provider_auth[providerID] ?? [
|
|
145
|
+
{
|
|
146
|
+
type: "api",
|
|
147
|
+
label: "API key",
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
let index: number | null = 0
|
|
151
|
+
if (methods.length > 1) {
|
|
152
|
+
index = await new Promise<number | null>((resolve) => {
|
|
153
|
+
dialog.replace(
|
|
154
|
+
() => (
|
|
155
|
+
<DialogSelect
|
|
156
|
+
title="Select auth method"
|
|
157
|
+
options={methods.map((x, index) => ({
|
|
158
|
+
title: x.label,
|
|
159
|
+
value: index,
|
|
160
|
+
}))}
|
|
161
|
+
onSelect={(option) => resolve(option.value)}
|
|
162
|
+
/>
|
|
163
|
+
),
|
|
164
|
+
() => resolve(null),
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
if (index == null) return
|
|
169
|
+
const method = methods[index]
|
|
170
|
+
if (method.type === "oauth") {
|
|
171
|
+
let inputs: Record<string, string> | undefined
|
|
172
|
+
if (method.prompts?.length) {
|
|
173
|
+
const value = await PromptsMethod({
|
|
174
|
+
dialog,
|
|
175
|
+
prompts: method.prompts,
|
|
176
|
+
})
|
|
177
|
+
if (!value) return
|
|
178
|
+
inputs = value
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = await sdk.client.provider.oauth.authorize({
|
|
182
|
+
providerID,
|
|
183
|
+
method: index,
|
|
184
|
+
inputs,
|
|
185
|
+
})
|
|
186
|
+
if (result.error) {
|
|
187
|
+
toast.show({
|
|
188
|
+
variant: "error",
|
|
189
|
+
message: JSON.stringify(result.error),
|
|
190
|
+
})
|
|
191
|
+
dialog.clear()
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if (result.data?.method === "code") {
|
|
195
|
+
dialog.replace(() => (
|
|
196
|
+
<CodeMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} />
|
|
197
|
+
))
|
|
198
|
+
}
|
|
199
|
+
if (result.data?.method === "auto") {
|
|
200
|
+
dialog.replace(() => (
|
|
201
|
+
<AutoMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} />
|
|
202
|
+
))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (method.type === "api") {
|
|
206
|
+
let metadata: Record<string, string> | undefined
|
|
207
|
+
if (method.prompts?.length) {
|
|
208
|
+
const value = await PromptsMethod({ dialog, prompts: method.prompts })
|
|
209
|
+
if (!value) return
|
|
210
|
+
metadata = value
|
|
211
|
+
}
|
|
212
|
+
return dialog.replace(() => (
|
|
213
|
+
<ApiMethod providerID={providerID} title={method.label} metadata={metadata} />
|
|
214
|
+
))
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
return options
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function DialogProvider() {
|
|
225
|
+
const options = createDialogProviderOptions()
|
|
226
|
+
return <DialogSelect title="Connect a provider" options={options()} />
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface AutoMethodProps {
|
|
230
|
+
index: number
|
|
231
|
+
providerID: string
|
|
232
|
+
title: string
|
|
233
|
+
authorization: ProviderAuthAuthorization
|
|
234
|
+
}
|
|
235
|
+
function AutoMethod(props: AutoMethodProps) {
|
|
236
|
+
const { theme } = useTheme()
|
|
237
|
+
const sdk = useSDK()
|
|
238
|
+
const dialog = useDialog()
|
|
239
|
+
const sync = useSync()
|
|
240
|
+
const toast = useToast()
|
|
241
|
+
|
|
242
|
+
useBindings(() => ({
|
|
243
|
+
bindings: [
|
|
244
|
+
{
|
|
245
|
+
key: "c",
|
|
246
|
+
desc: "Copy provider code",
|
|
247
|
+
group: "Dialog",
|
|
248
|
+
cmd: () => {
|
|
249
|
+
const code =
|
|
250
|
+
props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url
|
|
251
|
+
Clipboard.copy(code)
|
|
252
|
+
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
|
253
|
+
.catch(toast.error)
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
}))
|
|
258
|
+
|
|
259
|
+
onMount(async () => {
|
|
260
|
+
const result = await sdk.client.provider.oauth.callback({
|
|
261
|
+
providerID: props.providerID,
|
|
262
|
+
method: props.index,
|
|
263
|
+
})
|
|
264
|
+
if (result.error) {
|
|
265
|
+
dialog.clear()
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
await sdk.client.instance.dispose()
|
|
269
|
+
await sync.bootstrap()
|
|
270
|
+
dialog.replace(() => <DialogModel providerID={props.providerID} />)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
|
275
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
276
|
+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
|
277
|
+
{props.title}
|
|
278
|
+
</text>
|
|
279
|
+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
|
280
|
+
esc
|
|
281
|
+
</text>
|
|
282
|
+
</box>
|
|
283
|
+
<box gap={1}>
|
|
284
|
+
<Link href={props.authorization.url} fg={theme.primary} />
|
|
285
|
+
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
|
286
|
+
</box>
|
|
287
|
+
<text fg={theme.textMuted}>Waiting for authorization...</text>
|
|
288
|
+
<text fg={theme.text}>
|
|
289
|
+
c <span style={{ fg: theme.textMuted }}>copy</span>
|
|
290
|
+
</text>
|
|
291
|
+
</box>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface CodeMethodProps {
|
|
296
|
+
index: number
|
|
297
|
+
title: string
|
|
298
|
+
providerID: string
|
|
299
|
+
authorization: ProviderAuthAuthorization
|
|
300
|
+
}
|
|
301
|
+
function CodeMethod(props: CodeMethodProps) {
|
|
302
|
+
const { theme } = useTheme()
|
|
303
|
+
const sdk = useSDK()
|
|
304
|
+
const sync = useSync()
|
|
305
|
+
const dialog = useDialog()
|
|
306
|
+
const [error, setError] = createSignal(false)
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<DialogPrompt
|
|
310
|
+
title={props.title}
|
|
311
|
+
placeholder="Authorization code"
|
|
312
|
+
onConfirm={async (value) => {
|
|
313
|
+
const { error } = await sdk.client.provider.oauth.callback({
|
|
314
|
+
providerID: props.providerID,
|
|
315
|
+
method: props.index,
|
|
316
|
+
code: value,
|
|
317
|
+
})
|
|
318
|
+
if (!error) {
|
|
319
|
+
await sdk.client.instance.dispose()
|
|
320
|
+
await sync.bootstrap()
|
|
321
|
+
dialog.replace(() => <DialogModel providerID={props.providerID} />)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
setError(true)
|
|
325
|
+
}}
|
|
326
|
+
description={() => (
|
|
327
|
+
<box gap={1}>
|
|
328
|
+
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
|
329
|
+
<Link href={props.authorization.url} fg={theme.primary} />
|
|
330
|
+
<Show when={error()}>
|
|
331
|
+
<text fg={theme.error}>Invalid code</text>
|
|
332
|
+
</Show>
|
|
333
|
+
</box>
|
|
334
|
+
)}
|
|
335
|
+
/>
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
interface ApiMethodProps {
|
|
340
|
+
providerID: string
|
|
341
|
+
title: string
|
|
342
|
+
metadata?: Record<string, string>
|
|
343
|
+
custom?: boolean
|
|
344
|
+
}
|
|
345
|
+
function ApiMethod(props: ApiMethodProps) {
|
|
346
|
+
const dialog = useDialog()
|
|
347
|
+
const sdk = useSDK()
|
|
348
|
+
const sync = useSync()
|
|
349
|
+
const toast = useToast()
|
|
350
|
+
const { theme } = useTheme()
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<DialogPrompt
|
|
354
|
+
title={props.title}
|
|
355
|
+
placeholder="API key"
|
|
356
|
+
description={
|
|
357
|
+
{
|
|
358
|
+
opencode: (
|
|
359
|
+
<box gap={1}>
|
|
360
|
+
<text fg={theme.textMuted}>
|
|
361
|
+
OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API
|
|
362
|
+
key.
|
|
363
|
+
</text>
|
|
364
|
+
<text fg={theme.text}>
|
|
365
|
+
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
|
|
366
|
+
</text>
|
|
367
|
+
</box>
|
|
368
|
+
),
|
|
369
|
+
"opencode-go": (
|
|
370
|
+
<box gap={1}>
|
|
371
|
+
<text fg={theme.textMuted}>
|
|
372
|
+
OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models
|
|
373
|
+
with generous usage limits.
|
|
374
|
+
</text>
|
|
375
|
+
<text fg={theme.text}>
|
|
376
|
+
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> and enable OpenCode Go
|
|
377
|
+
</text>
|
|
378
|
+
</box>
|
|
379
|
+
),
|
|
380
|
+
}[props.providerID] ?? undefined
|
|
381
|
+
}
|
|
382
|
+
onConfirm={async (value) => {
|
|
383
|
+
if (!value) return
|
|
384
|
+
await sdk.client.auth.set({
|
|
385
|
+
providerID: props.providerID,
|
|
386
|
+
auth: {
|
|
387
|
+
type: "api",
|
|
388
|
+
key: value,
|
|
389
|
+
...(props.metadata ? { metadata: props.metadata } : {}),
|
|
390
|
+
},
|
|
391
|
+
})
|
|
392
|
+
await sdk.client.instance.dispose()
|
|
393
|
+
await sync.bootstrap()
|
|
394
|
+
if (props.custom && !sync.data.provider_next.all.some((provider) => provider.id === props.providerID)) {
|
|
395
|
+
toast.show({
|
|
396
|
+
variant: "info",
|
|
397
|
+
message: `Saved credential for ${props.providerID}. Configure it in opencode.json to use it.`,
|
|
398
|
+
})
|
|
399
|
+
dialog.clear()
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
dialog.replace(() => <DialogModel providerID={props.providerID} />)
|
|
403
|
+
}}
|
|
404
|
+
/>
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
interface PromptsMethodProps {
|
|
409
|
+
dialog: ReturnType<typeof useDialog>
|
|
410
|
+
prompts: NonNullable<ProviderAuthMethod["prompts"]>[number][]
|
|
411
|
+
}
|
|
412
|
+
async function PromptsMethod(props: PromptsMethodProps) {
|
|
413
|
+
const inputs: Record<string, string> = {}
|
|
414
|
+
for (const prompt of props.prompts) {
|
|
415
|
+
if (prompt.when) {
|
|
416
|
+
const value = inputs[prompt.when.key]
|
|
417
|
+
if (value === undefined) continue
|
|
418
|
+
const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value
|
|
419
|
+
if (!matches) continue
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (prompt.type === "select") {
|
|
423
|
+
const value = await new Promise<string | null>((resolve) => {
|
|
424
|
+
props.dialog.replace(
|
|
425
|
+
() => (
|
|
426
|
+
<DialogSelect
|
|
427
|
+
title={prompt.message}
|
|
428
|
+
options={prompt.options.map((x) => ({
|
|
429
|
+
title: x.label,
|
|
430
|
+
value: x.value,
|
|
431
|
+
description: x.hint,
|
|
432
|
+
}))}
|
|
433
|
+
onSelect={(option) => resolve(option.value)}
|
|
434
|
+
/>
|
|
435
|
+
),
|
|
436
|
+
() => resolve(null),
|
|
437
|
+
)
|
|
438
|
+
})
|
|
439
|
+
if (value === null) return null
|
|
440
|
+
inputs[prompt.key] = value
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const value = await new Promise<string | null>((resolve) => {
|
|
445
|
+
props.dialog.replace(
|
|
446
|
+
() => (
|
|
447
|
+
<DialogPrompt title={prompt.message} placeholder={prompt.placeholder} onConfirm={(value) => resolve(value)} />
|
|
448
|
+
),
|
|
449
|
+
() => resolve(null),
|
|
450
|
+
)
|
|
451
|
+
})
|
|
452
|
+
if (value === null) return null
|
|
453
|
+
inputs[prompt.key] = value
|
|
454
|
+
}
|
|
455
|
+
return inputs
|
|
456
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { RGBA, TextAttributes } from "@opentui/core"
|
|
2
|
+
import open from "open"
|
|
3
|
+
import { createSignal } from "solid-js"
|
|
4
|
+
import { selectedForeground, useTheme } from "@tui/context/theme"
|
|
5
|
+
import { useDialog, type DialogContext } from "@tui/ui/dialog"
|
|
6
|
+
import { Link } from "@tui/ui/link"
|
|
7
|
+
import { BgPulse } from "./bg-pulse"
|
|
8
|
+
import { useBindings } from "../keymap"
|
|
9
|
+
|
|
10
|
+
const GO_URL = "https://opencode.ai/go"
|
|
11
|
+
const PAD_X = 3
|
|
12
|
+
const PAD_TOP_OUTER = 1
|
|
13
|
+
const FOREGROUND_ALPHA = 186
|
|
14
|
+
|
|
15
|
+
export type DialogRetryActionProps = {
|
|
16
|
+
title: string
|
|
17
|
+
message: string
|
|
18
|
+
label: string
|
|
19
|
+
link?: string
|
|
20
|
+
onClose?: (dontShowAgain?: boolean) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runAction(props: DialogRetryActionProps, dialog: ReturnType<typeof useDialog>) {
|
|
24
|
+
if (props.link) open(props.link).catch(() => {})
|
|
25
|
+
props.onClose?.()
|
|
26
|
+
dialog.clear()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function dismiss(props: DialogRetryActionProps, dialog: ReturnType<typeof useDialog>) {
|
|
30
|
+
props.onClose?.(true)
|
|
31
|
+
dialog.clear()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function panelOverlay(color: RGBA) {
|
|
35
|
+
const [r, g, b] = color.toInts()
|
|
36
|
+
return RGBA.fromInts(r, g, b, FOREGROUND_ALPHA)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function DialogRetryAction(props: DialogRetryActionProps) {
|
|
40
|
+
const dialog = useDialog()
|
|
41
|
+
const { theme } = useTheme()
|
|
42
|
+
const fg = selectedForeground(theme)
|
|
43
|
+
const showGoTreatment = () => props.link === GO_URL
|
|
44
|
+
const textBg = () => (showGoTreatment() ? panelOverlay(theme.backgroundPanel) : undefined)
|
|
45
|
+
const [selected, setSelected] = createSignal<"dismiss" | "action">("action")
|
|
46
|
+
|
|
47
|
+
useBindings(() => ({
|
|
48
|
+
bindings: [
|
|
49
|
+
{
|
|
50
|
+
key: "left",
|
|
51
|
+
desc: "Previous retry option",
|
|
52
|
+
group: "Dialog",
|
|
53
|
+
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "right",
|
|
57
|
+
desc: "Next retry option",
|
|
58
|
+
group: "Dialog",
|
|
59
|
+
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "tab",
|
|
63
|
+
desc: "Next retry option",
|
|
64
|
+
group: "Dialog",
|
|
65
|
+
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "return",
|
|
69
|
+
desc: "Confirm retry option",
|
|
70
|
+
group: "Dialog",
|
|
71
|
+
cmd: () => {
|
|
72
|
+
if (selected() === "action") runAction(props, dialog)
|
|
73
|
+
else dismiss(props, dialog)
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<box>
|
|
81
|
+
{showGoTreatment() ? (
|
|
82
|
+
<box position="absolute" top={-PAD_TOP_OUTER} left={0} right={0} bottom={0} zIndex={0}>
|
|
83
|
+
<BgPulse />
|
|
84
|
+
</box>
|
|
85
|
+
) : null}
|
|
86
|
+
<box zIndex={1} paddingLeft={PAD_X} paddingRight={PAD_X} paddingBottom={1} gap={1}>
|
|
87
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
88
|
+
<text attributes={TextAttributes.BOLD} fg={theme.text} bg={textBg()}>
|
|
89
|
+
{props.title}
|
|
90
|
+
</text>
|
|
91
|
+
<text fg={theme.textMuted} bg={textBg()} onMouseUp={() => dialog.clear()}>
|
|
92
|
+
esc
|
|
93
|
+
</text>
|
|
94
|
+
</box>
|
|
95
|
+
<box gap={0}>
|
|
96
|
+
<text fg={theme.textMuted} bg={textBg()}>
|
|
97
|
+
{props.message}
|
|
98
|
+
</text>
|
|
99
|
+
</box>
|
|
100
|
+
{props.link ? (
|
|
101
|
+
showGoTreatment() ? (
|
|
102
|
+
<box alignItems="center" justifyContent="flex-end" height={7} paddingBottom={1}>
|
|
103
|
+
<Link href={props.link} fg={theme.primary} bg={textBg()} wrapMode="none" />
|
|
104
|
+
</box>
|
|
105
|
+
) : (
|
|
106
|
+
<box width="100%" flexDirection="row" justifyContent="center" paddingBottom={1}>
|
|
107
|
+
<Link href={props.link} fg={theme.primary} wrapMode="none" />
|
|
108
|
+
</box>
|
|
109
|
+
)
|
|
110
|
+
) : (
|
|
111
|
+
<box paddingBottom={1} />
|
|
112
|
+
)}
|
|
113
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
114
|
+
<box
|
|
115
|
+
paddingLeft={2}
|
|
116
|
+
paddingRight={2}
|
|
117
|
+
backgroundColor={selected() === "dismiss" ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
|
118
|
+
onMouseOver={() => setSelected("dismiss")}
|
|
119
|
+
onMouseUp={() => dismiss(props, dialog)}
|
|
120
|
+
>
|
|
121
|
+
<text
|
|
122
|
+
fg={selected() === "dismiss" ? fg : theme.textMuted}
|
|
123
|
+
bg={selected() === "dismiss" ? undefined : textBg()}
|
|
124
|
+
attributes={selected() === "dismiss" ? TextAttributes.BOLD : undefined}
|
|
125
|
+
>
|
|
126
|
+
don't show again
|
|
127
|
+
</text>
|
|
128
|
+
</box>
|
|
129
|
+
<box
|
|
130
|
+
paddingLeft={2}
|
|
131
|
+
paddingRight={2}
|
|
132
|
+
backgroundColor={selected() === "action" ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
|
133
|
+
onMouseOver={() => setSelected("action")}
|
|
134
|
+
onMouseUp={() => runAction(props, dialog)}
|
|
135
|
+
>
|
|
136
|
+
<text
|
|
137
|
+
fg={selected() === "action" ? fg : theme.text}
|
|
138
|
+
bg={selected() === "action" ? undefined : textBg()}
|
|
139
|
+
attributes={selected() === "action" ? TextAttributes.BOLD : undefined}
|
|
140
|
+
>
|
|
141
|
+
{props.label}
|
|
142
|
+
</text>
|
|
143
|
+
</box>
|
|
144
|
+
</box>
|
|
145
|
+
</box>
|
|
146
|
+
</box>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
DialogRetryAction.show = (
|
|
151
|
+
dialog: DialogContext,
|
|
152
|
+
props: Pick<DialogRetryActionProps, "title" | "message" | "label" | "link">,
|
|
153
|
+
) => {
|
|
154
|
+
return new Promise<boolean>((resolve) => {
|
|
155
|
+
dialog.replace(
|
|
156
|
+
() => <DialogRetryAction {...props} onClose={(dontShow) => resolve(dontShow ?? false)} />,
|
|
157
|
+
() => resolve(false),
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
}
|