saeeol 1.2.1 → 1.2.2
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 +11 -11
- package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
- package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
- package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
- package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
- package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
- package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
- package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
- package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
- package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
- package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
- package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
- package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
- package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
- package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
- package/src/tool/apply_patch.ts +1 -334
- package/src/tool/bash.ts +1 -656
- package/src/tool/core/external-directory.ts +55 -0
- package/src/tool/core/invalid.ts +21 -0
- package/src/tool/core/recall.ts +164 -0
- package/src/tool/core/recall.txt +12 -0
- package/src/tool/core/schema.ts +16 -0
- package/src/tool/core/tool.ts +162 -0
- package/src/tool/core/truncate.ts +160 -0
- package/src/tool/core/truncation-dir.ts +4 -0
- package/src/tool/diagnostics.ts +1 -20
- package/src/tool/edit-replacers.ts +1 -288
- package/src/tool/edit-utils.ts +1 -86
- package/src/tool/edit.ts +1 -262
- package/src/tool/external-directory.ts +1 -55
- package/src/tool/file/apply_patch.ts +334 -0
- package/src/tool/file/apply_patch.txt +33 -0
- package/src/tool/file/bash.ts +656 -0
- package/src/tool/file/bash.txt +119 -0
- package/src/tool/file/edit-replacers.ts +288 -0
- package/src/tool/file/edit-utils.ts +86 -0
- package/src/tool/file/edit.ts +262 -0
- package/src/tool/file/edit.txt +10 -0
- package/src/tool/file/read.ts +389 -0
- package/src/tool/file/read.txt +14 -0
- package/src/tool/file/write.ts +114 -0
- package/src/tool/file/write.txt +8 -0
- package/src/tool/glob.ts +1 -115
- package/src/tool/grep.ts +1 -151
- package/src/tool/integration/diagnostics.ts +20 -0
- package/src/tool/integration/lsp.ts +113 -0
- package/src/tool/integration/lsp.txt +24 -0
- package/src/tool/integration/mcp-exa.ts +73 -0
- package/src/tool/integration/package.ts +168 -0
- package/src/tool/integration/registry.ts +375 -0
- package/src/tool/invalid.ts +1 -21
- package/src/tool/lsp.ts +1 -113
- package/src/tool/mcp-exa.ts +1 -73
- package/src/tool/package.ts +1 -168
- package/src/tool/plan.ts +1 -30
- package/src/tool/question.ts +1 -52
- package/src/tool/read.ts +1 -389
- package/src/tool/recall.ts +1 -164
- package/src/tool/registry.ts +1 -375
- package/src/tool/schema.ts +1 -16
- package/src/tool/search/glob.ts +115 -0
- package/src/tool/search/glob.txt +6 -0
- package/src/tool/search/grep.ts +151 -0
- package/src/tool/search/grep.txt +8 -0
- package/src/tool/search/warpgrep.ts +107 -0
- package/src/tool/search/warpgrep.txt +10 -0
- package/src/tool/search/webfetch.ts +202 -0
- package/src/tool/search/webfetch.txt +13 -0
- package/src/tool/search/websearch.ts +71 -0
- package/src/tool/search/websearch.txt +14 -0
- package/src/tool/skill.ts +1 -91
- package/src/tool/task.ts +1 -197
- package/src/tool/todo.ts +1 -62
- package/src/tool/tool.ts +1 -162
- package/src/tool/truncate.ts +1 -160
- package/src/tool/truncation-dir.ts +1 -4
- package/src/tool/warpgrep.ts +1 -107
- package/src/tool/webfetch.ts +1 -202
- package/src/tool/websearch.ts +1 -71
- package/src/tool/workflow/plan-enter.txt +14 -0
- package/src/tool/workflow/plan-exit.txt +13 -0
- package/src/tool/workflow/plan.ts +30 -0
- package/src/tool/workflow/question.ts +52 -0
- package/src/tool/workflow/question.txt +11 -0
- package/src/tool/workflow/skill.ts +91 -0
- package/src/tool/workflow/skill.txt +5 -0
- package/src/tool/workflow/task.ts +197 -0
- package/src/tool/workflow/task.txt +57 -0
- package/src/tool/workflow/todo.ts +62 -0
- package/src/tool/workflow/todowrite.txt +167 -0
- package/src/tool/write.ts +1 -114
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
2
|
+
import { createEffect, createMemo, createSignal, Show } from "solid-js"
|
|
3
|
+
import { useLocal } from "@tui/context/local"
|
|
4
|
+
import { useSync } from "@tui/context/sync"
|
|
5
|
+
import { map, pipe, flatMap, entries, filter, sortBy, take, groupBy } from "remeda"
|
|
6
|
+
import { DialogSelect } from "@tui/ui/dialog-select"
|
|
7
|
+
import { useDialog } from "@tui/ui/dialog"
|
|
8
|
+
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
|
9
|
+
import { DialogVariant } from "./dialog-variant"
|
|
10
|
+
import { useKeybind } from "../context/keybind"
|
|
11
|
+
import type { Model } from "@saeeol/sdk/v2"
|
|
12
|
+
import * as fuzzysort from "fuzzysort"
|
|
13
|
+
import { useConnected } from "../use-connected"
|
|
14
|
+
import { ModelInfoPanel } from "@/saeeol/components/model-info-panel"
|
|
15
|
+
import { t } from "@/util/i18n"
|
|
16
|
+
|
|
17
|
+
export function DialogModel(props: { providerID?: string }) {
|
|
18
|
+
const local = useLocal()
|
|
19
|
+
const sync = useSync()
|
|
20
|
+
const dialog = useDialog()
|
|
21
|
+
const keybind = useKeybind()
|
|
22
|
+
const [query, setQuery] = createSignal("")
|
|
23
|
+
const dimensions = useTerminalDimensions()
|
|
24
|
+
|
|
25
|
+
const connected = useConnected()
|
|
26
|
+
const providers = createDialogProviderOptions()
|
|
27
|
+
// Memoize anything that iterates all Saeeol models to avoid calculating it for
|
|
28
|
+
// each Saeeol model and tanking the UI at a couple hundred models
|
|
29
|
+
const saeeolRank = createMemo(() => {
|
|
30
|
+
const provider = sync.data.provider.find((provider) => provider.id === "saeeol")
|
|
31
|
+
const models = provider?.models ?? {}
|
|
32
|
+
return new Map(Object.entries(models).map(([id, info]) => [id, info.recommendedIndex ?? Infinity] as const))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const showExtra = createMemo(() => connected() && !props.providerID)
|
|
36
|
+
const wide = createMemo(() => dimensions().width >= 108)
|
|
37
|
+
const [preview, setPreview] = createSignal<{
|
|
38
|
+
model: Model
|
|
39
|
+
provider: string
|
|
40
|
+
}>()
|
|
41
|
+
|
|
42
|
+
const lookup = (providerID: string, modelID: string) => {
|
|
43
|
+
const provider = sync.data.provider.find((x) => x.id === providerID)
|
|
44
|
+
const model = provider?.models?.[modelID]
|
|
45
|
+
if (!provider || !model) return
|
|
46
|
+
return {
|
|
47
|
+
model,
|
|
48
|
+
provider: provider.name,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
createEffect(() => {
|
|
53
|
+
dialog.setSize(wide() ? "xlarge" : "large")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
createEffect(() => {
|
|
57
|
+
const current = local.model.current()
|
|
58
|
+
if (!current) return
|
|
59
|
+
const next = lookup(current.providerID, current.modelID)
|
|
60
|
+
if (!next) return
|
|
61
|
+
setPreview(next)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const options = createMemo(() => {
|
|
65
|
+
const needle = query().trim()
|
|
66
|
+
const favorites = connected() ? local.model.favorite() : []
|
|
67
|
+
const recents = local.model.recent()
|
|
68
|
+
|
|
69
|
+
function toOptions(items: typeof favorites, category: string) {
|
|
70
|
+
if (!showExtra()) return []
|
|
71
|
+
return items.flatMap((item) => {
|
|
72
|
+
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
|
73
|
+
if (!provider) return []
|
|
74
|
+
const model = provider.models?.[item.modelID]
|
|
75
|
+
if (!model) return []
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
key: item,
|
|
79
|
+
value: { providerID: provider.id, modelID: model.id },
|
|
80
|
+
title: model.name ?? item.modelID,
|
|
81
|
+
description: provider.name,
|
|
82
|
+
category,
|
|
83
|
+
disabled: provider.id === "saeeol" && model.id.includes("-nano"),
|
|
84
|
+
footer: model.cost?.input === 0 && provider.id === "saeeol" ? "Free" : undefined,
|
|
85
|
+
onSelect: () => {
|
|
86
|
+
onSelect(provider.id, model.id)
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const favoriteOptions = toOptions(favorites, t("cmd.model.favorites"))
|
|
94
|
+
const recentOptions = toOptions(
|
|
95
|
+
recents.filter(
|
|
96
|
+
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
|
97
|
+
),
|
|
98
|
+
t("cmd.model.recent"),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const providerOptions = pipe(
|
|
102
|
+
sync.data.provider,
|
|
103
|
+
sortBy(
|
|
104
|
+
(provider) => provider.id !== "saeeol",
|
|
105
|
+
(provider) => provider.name,
|
|
106
|
+
),
|
|
107
|
+
flatMap((provider) =>
|
|
108
|
+
pipe(
|
|
109
|
+
provider.models,
|
|
110
|
+
entries(),
|
|
111
|
+
filter(([_, info]) => info.status !== "deprecated"),
|
|
112
|
+
filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
|
|
113
|
+
map(([model, info]) => ({
|
|
114
|
+
value: { providerID: provider.id, modelID: model },
|
|
115
|
+
title: info.name ?? model,
|
|
116
|
+
description: favorites.some((item) => item.providerID === provider.id && item.modelID === model)
|
|
117
|
+
? "(Favorite)"
|
|
118
|
+
: undefined,
|
|
119
|
+
category: connected()
|
|
120
|
+
? provider.id === "saeeol" && info.recommendedIndex !== undefined
|
|
121
|
+
? t("cmd.model.recommended")
|
|
122
|
+
: provider.name
|
|
123
|
+
: undefined,
|
|
124
|
+
disabled: provider.id === "saeeol" && model.includes("-nano"),
|
|
125
|
+
footer: info.cost?.input === 0 && provider.id === "saeeol" ? "Free" : undefined,
|
|
126
|
+
onSelect() {
|
|
127
|
+
onSelect(provider.id, model)
|
|
128
|
+
},
|
|
129
|
+
})),
|
|
130
|
+
filter((x) => {
|
|
131
|
+
if (favorites.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
|
|
132
|
+
return false
|
|
133
|
+
if (recents.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
|
|
134
|
+
return false
|
|
135
|
+
return true
|
|
136
|
+
}),
|
|
137
|
+
sortBy(
|
|
138
|
+
(x) => (x.value.providerID === "saeeol" ? (saeeolRank().get(x.value.modelID) ?? Infinity) : 0),
|
|
139
|
+
(x) => x.footer !== "Free",
|
|
140
|
+
(x) => x.title,
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const popularProviders = !connected()
|
|
147
|
+
? pipe(
|
|
148
|
+
providers(),
|
|
149
|
+
map((option) => ({
|
|
150
|
+
...option,
|
|
151
|
+
category: t("cmd.model.popular_providers"),
|
|
152
|
+
})),
|
|
153
|
+
take(6),
|
|
154
|
+
)
|
|
155
|
+
: []
|
|
156
|
+
if (needle) {
|
|
157
|
+
const rank = <U extends { title: string; category?: string }>(items: U[]) =>
|
|
158
|
+
fuzzysort.go(needle, items, { keys: ["title", "category"] }).map((x) => x.obj)
|
|
159
|
+
// rank within each provider category to preserve category order
|
|
160
|
+
const rankedProviders = pipe(
|
|
161
|
+
providerOptions,
|
|
162
|
+
groupBy((x) => x.category ?? ""),
|
|
163
|
+
entries(),
|
|
164
|
+
flatMap(([_, items]) => rank(items)),
|
|
165
|
+
)
|
|
166
|
+
return [...rank(favoriteOptions), ...rank(recentOptions), ...rankedProviders, ...rank(popularProviders)]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const provider = createMemo(() =>
|
|
173
|
+
props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const title = createMemo(() => {
|
|
177
|
+
const value = provider()
|
|
178
|
+
if (!value) return t("cmd.model.title")
|
|
179
|
+
return value.name
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
function onSelect(providerID: string, modelID: string) {
|
|
183
|
+
local.model.set({ providerID, modelID }, { recent: true })
|
|
184
|
+
const list = local.model.variant.list()
|
|
185
|
+
const cur = local.model.variant.selected()
|
|
186
|
+
if (cur === "default" || (cur && list.includes(cur))) {
|
|
187
|
+
dialog.clear()
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (list.length > 0) {
|
|
191
|
+
dialog.replace(() => <DialogVariant />)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
dialog.clear()
|
|
195
|
+
}
|
|
196
|
+
return (
|
|
197
|
+
<box flexDirection="row">
|
|
198
|
+
<box flexGrow={1} flexShrink={1}>
|
|
199
|
+
<DialogSelect<ReturnType<typeof options>[number]["value"]>
|
|
200
|
+
options={options()}
|
|
201
|
+
keybind={[
|
|
202
|
+
{
|
|
203
|
+
keybind: keybind.all.model_provider_list?.[0],
|
|
204
|
+
title: connected() ? t("cmd.model.connect_provider") : t("cmd.model.view_all_providers"),
|
|
205
|
+
onTrigger() {
|
|
206
|
+
dialog.replace(() => <DialogProvider />)
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
keybind: keybind.all.model_favorite_toggle?.[0],
|
|
211
|
+
title: t("cmd.model.favorite"),
|
|
212
|
+
disabled: !connected(),
|
|
213
|
+
onTrigger: (option) => {
|
|
214
|
+
local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]}
|
|
218
|
+
onFilter={setQuery}
|
|
219
|
+
onMove={(option) => {
|
|
220
|
+
if (typeof option.value === "string") {
|
|
221
|
+
setPreview(undefined)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
const next = lookup(option.value.providerID, option.value.modelID)
|
|
225
|
+
if (!next) return
|
|
226
|
+
setPreview(next)
|
|
227
|
+
}}
|
|
228
|
+
skipFilter={true}
|
|
229
|
+
title={title()}
|
|
230
|
+
current={local.model.current()}
|
|
231
|
+
/>
|
|
232
|
+
</box>
|
|
233
|
+
<Show when={wide() && preview()}>
|
|
234
|
+
{(item) => <ModelInfoPanel model={item().model} provider={item().provider} />}
|
|
235
|
+
</Show>
|
|
236
|
+
</box>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
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 "@saeeol/sdk/v2"
|
|
12
|
+
import { DialogModel } from "./dialog-model"
|
|
13
|
+
import { useKeyboard } from "@opentui/solid"
|
|
14
|
+
import * as Clipboard from "@tui/util/clipboard"
|
|
15
|
+
import { useToast } from "../ui/toast"
|
|
16
|
+
import { isConsoleManagedProvider } from "@tui/util/provider-origin"
|
|
17
|
+
import * as Provider from "@/saeeol/cli/cmd/tui/component/dialog-provider"
|
|
18
|
+
import { useConnected } from "../use-connected"
|
|
19
|
+
|
|
20
|
+
const PROVIDER_PRIORITY: Record<string, number> = Provider.PROVIDER_PRIORITY
|
|
21
|
+
|
|
22
|
+
export function createDialogProviderOptions() {
|
|
23
|
+
const sync = useSync()
|
|
24
|
+
const dialog = useDialog()
|
|
25
|
+
const sdk = useSDK()
|
|
26
|
+
const toast = useToast()
|
|
27
|
+
const { theme } = useTheme()
|
|
28
|
+
const onboarded = useConnected()
|
|
29
|
+
const options = createMemo(() => {
|
|
30
|
+
return pipe(
|
|
31
|
+
sync.data.provider_next.all,
|
|
32
|
+
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
|
|
33
|
+
map((provider) => {
|
|
34
|
+
const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, provider.id)
|
|
35
|
+
const connected = sync.data.provider_next.connected.includes(provider.id)
|
|
36
|
+
const failed = sync.data.provider_next.failed ?? []
|
|
37
|
+
const failedGutter = Provider.renderGutter(provider.id, failed, theme)
|
|
38
|
+
const failedDesc = Provider.failedDescription(provider.id, failed)
|
|
39
|
+
const baseDesc = Provider.PROVIDER_DESCRIPTIONS[provider.id]
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
title: Provider.PROVIDER_TITLES[provider.id] ?? provider.name,
|
|
43
|
+
value: provider.id,
|
|
44
|
+
description: failedDesc ?? baseDesc,
|
|
45
|
+
footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,
|
|
46
|
+
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
|
47
|
+
gutter: failedGutter ?? (connected && onboarded() ? () => <text fg={theme.success}>✓</text> : undefined),
|
|
48
|
+
async onSelect() {
|
|
49
|
+
if (consoleManaged) return
|
|
50
|
+
|
|
51
|
+
const methods = sync.data.provider_auth[provider.id] ?? [
|
|
52
|
+
{
|
|
53
|
+
type: "api",
|
|
54
|
+
label: "API key",
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
let index: number | null = 0
|
|
58
|
+
if (methods.length > 1) {
|
|
59
|
+
index = await new Promise<number | null>((resolve) => {
|
|
60
|
+
dialog.replace(
|
|
61
|
+
() => (
|
|
62
|
+
<DialogSelect
|
|
63
|
+
title="Select auth method"
|
|
64
|
+
options={methods.map((x, index) => ({
|
|
65
|
+
title: x.label,
|
|
66
|
+
value: index,
|
|
67
|
+
}))}
|
|
68
|
+
onSelect={(option) => resolve(option.value)}
|
|
69
|
+
/>
|
|
70
|
+
),
|
|
71
|
+
() => resolve(null),
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
if (index == null) return
|
|
76
|
+
const method = methods[index]
|
|
77
|
+
if (method.type === "oauth") {
|
|
78
|
+
let inputs: Record<string, string> | undefined
|
|
79
|
+
if (method.prompts?.length) {
|
|
80
|
+
const value = await PromptsMethod({
|
|
81
|
+
dialog,
|
|
82
|
+
prompts: method.prompts,
|
|
83
|
+
})
|
|
84
|
+
if (!value) return
|
|
85
|
+
inputs = value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = await sdk.client.provider.oauth.authorize({
|
|
89
|
+
providerID: provider.id,
|
|
90
|
+
method: index,
|
|
91
|
+
inputs,
|
|
92
|
+
})
|
|
93
|
+
if (result.error) {
|
|
94
|
+
toast.show({
|
|
95
|
+
variant: "error",
|
|
96
|
+
message: JSON.stringify(result.error),
|
|
97
|
+
})
|
|
98
|
+
dialog.clear()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
if (result.data?.method === "code") {
|
|
102
|
+
dialog.replace(() => (
|
|
103
|
+
<CodeMethod
|
|
104
|
+
providerID={provider.id}
|
|
105
|
+
title={method.label}
|
|
106
|
+
index={index}
|
|
107
|
+
authorization={result.data!}
|
|
108
|
+
/>
|
|
109
|
+
))
|
|
110
|
+
}
|
|
111
|
+
if (result.data?.method === "auto") {
|
|
112
|
+
const saeeol = Provider.renderAutoMethod({
|
|
113
|
+
providerID: provider.id,
|
|
114
|
+
title: method.label,
|
|
115
|
+
index,
|
|
116
|
+
authorization: result.data!,
|
|
117
|
+
useSDK,
|
|
118
|
+
useTheme,
|
|
119
|
+
DialogModel,
|
|
120
|
+
})
|
|
121
|
+
if (saeeol) {
|
|
122
|
+
dialog.replace(saeeol)
|
|
123
|
+
} else {
|
|
124
|
+
dialog.replace(() => (
|
|
125
|
+
<AutoMethod
|
|
126
|
+
providerID={provider.id}
|
|
127
|
+
title={method.label}
|
|
128
|
+
index={index}
|
|
129
|
+
authorization={result.data!}
|
|
130
|
+
/>
|
|
131
|
+
))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (method.type === "api") {
|
|
136
|
+
let metadata: Record<string, string> | undefined
|
|
137
|
+
if (method.prompts?.length) {
|
|
138
|
+
const value = await PromptsMethod({ dialog, prompts: method.prompts })
|
|
139
|
+
if (!value) return
|
|
140
|
+
metadata = value
|
|
141
|
+
}
|
|
142
|
+
return dialog.replace(() => (
|
|
143
|
+
<ApiMethod providerID={provider.id} title={method.label} metadata={metadata} />
|
|
144
|
+
))
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
return options
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function DialogProvider() {
|
|
155
|
+
const options = createDialogProviderOptions()
|
|
156
|
+
return <DialogSelect title="Connect a provider" options={options()} />
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface AutoMethodProps {
|
|
160
|
+
index: number
|
|
161
|
+
providerID: string
|
|
162
|
+
title: string
|
|
163
|
+
authorization: ProviderAuthAuthorization
|
|
164
|
+
}
|
|
165
|
+
function AutoMethod(props: AutoMethodProps) {
|
|
166
|
+
const { theme } = useTheme()
|
|
167
|
+
const sdk = useSDK()
|
|
168
|
+
const dialog = useDialog()
|
|
169
|
+
const sync = useSync()
|
|
170
|
+
const toast = useToast()
|
|
171
|
+
|
|
172
|
+
useKeyboard((evt) => {
|
|
173
|
+
if (evt.name === "c" && !evt.ctrl && !evt.meta) {
|
|
174
|
+
const code = props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url
|
|
175
|
+
Clipboard.copy(code)
|
|
176
|
+
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
|
177
|
+
.catch(toast.error)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
onMount(async () => {
|
|
182
|
+
const result = await sdk.client.provider.oauth.callback({
|
|
183
|
+
providerID: props.providerID,
|
|
184
|
+
method: props.index,
|
|
185
|
+
})
|
|
186
|
+
if (result.error) {
|
|
187
|
+
dialog.clear()
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
await sdk.client.instance.dispose()
|
|
191
|
+
await sync.bootstrap()
|
|
192
|
+
dialog.replace(() => <DialogModel providerID={props.providerID} />)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
|
197
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
198
|
+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
|
199
|
+
{props.title}
|
|
200
|
+
</text>
|
|
201
|
+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
|
202
|
+
esc
|
|
203
|
+
</text>
|
|
204
|
+
</box>
|
|
205
|
+
<box gap={1}>
|
|
206
|
+
<Link href={props.authorization.url} fg={theme.primary} />
|
|
207
|
+
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
|
208
|
+
</box>
|
|
209
|
+
<text fg={theme.textMuted}>Waiting for authorization...</text>
|
|
210
|
+
<text fg={theme.text}>
|
|
211
|
+
c <span style={{ fg: theme.textMuted }}>copy</span>
|
|
212
|
+
</text>
|
|
213
|
+
</box>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface CodeMethodProps {
|
|
218
|
+
index: number
|
|
219
|
+
title: string
|
|
220
|
+
providerID: string
|
|
221
|
+
authorization: ProviderAuthAuthorization
|
|
222
|
+
}
|
|
223
|
+
function CodeMethod(props: CodeMethodProps) {
|
|
224
|
+
const { theme } = useTheme()
|
|
225
|
+
const sdk = useSDK()
|
|
226
|
+
const sync = useSync()
|
|
227
|
+
const dialog = useDialog()
|
|
228
|
+
const [error, setError] = createSignal(false)
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<DialogPrompt
|
|
232
|
+
title={props.title}
|
|
233
|
+
placeholder="Authorization code"
|
|
234
|
+
onConfirm={async (value) => {
|
|
235
|
+
const { error } = await sdk.client.provider.oauth.callback({
|
|
236
|
+
providerID: props.providerID,
|
|
237
|
+
method: props.index,
|
|
238
|
+
code: value,
|
|
239
|
+
})
|
|
240
|
+
if (!error) {
|
|
241
|
+
await sdk.client.instance.dispose()
|
|
242
|
+
await sync.bootstrap()
|
|
243
|
+
dialog.replace(() => <DialogModel providerID={props.providerID} />)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
setError(true)
|
|
247
|
+
}}
|
|
248
|
+
description={() => (
|
|
249
|
+
<box gap={1}>
|
|
250
|
+
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
|
251
|
+
<Link href={props.authorization.url} fg={theme.primary} />
|
|
252
|
+
<Show when={error()}>
|
|
253
|
+
<text fg={theme.error}>Invalid code</text>
|
|
254
|
+
</Show>
|
|
255
|
+
</box>
|
|
256
|
+
)}
|
|
257
|
+
/>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface ApiMethodProps {
|
|
262
|
+
providerID: string
|
|
263
|
+
title: string
|
|
264
|
+
metadata?: Record<string, string>
|
|
265
|
+
}
|
|
266
|
+
function ApiMethod(props: ApiMethodProps) {
|
|
267
|
+
const dialog = useDialog()
|
|
268
|
+
const sdk = useSDK()
|
|
269
|
+
const sync = useSync()
|
|
270
|
+
const { theme } = useTheme()
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<DialogPrompt
|
|
274
|
+
title={props.title}
|
|
275
|
+
placeholder="API key"
|
|
276
|
+
description={Provider.renderApiDescription(props.providerID, theme)}
|
|
277
|
+
onConfirm={async (value) => {
|
|
278
|
+
if (!value) return
|
|
279
|
+
await sdk.client.auth.set({
|
|
280
|
+
providerID: props.providerID,
|
|
281
|
+
auth: {
|
|
282
|
+
type: "api",
|
|
283
|
+
key: value,
|
|
284
|
+
...(props.metadata ? { metadata: props.metadata } : {}),
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
await sdk.client.instance.dispose()
|
|
288
|
+
await sync.bootstrap()
|
|
289
|
+
dialog.replace(() => <DialogModel providerID={props.providerID} />)
|
|
290
|
+
}}
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface PromptsMethodProps {
|
|
296
|
+
dialog: ReturnType<typeof useDialog>
|
|
297
|
+
prompts: NonNullable<ProviderAuthMethod["prompts"]>[number][]
|
|
298
|
+
}
|
|
299
|
+
async function PromptsMethod(props: PromptsMethodProps) {
|
|
300
|
+
const inputs: Record<string, string> = {}
|
|
301
|
+
for (const prompt of props.prompts) {
|
|
302
|
+
if (prompt.when) {
|
|
303
|
+
const value = inputs[prompt.when.key]
|
|
304
|
+
if (value === undefined) continue
|
|
305
|
+
const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value
|
|
306
|
+
if (!matches) continue
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (prompt.type === "select") {
|
|
310
|
+
const value = await new Promise<string | null>((resolve) => {
|
|
311
|
+
props.dialog.replace(
|
|
312
|
+
() => (
|
|
313
|
+
<DialogSelect
|
|
314
|
+
title={prompt.message}
|
|
315
|
+
options={prompt.options.map((x) => ({
|
|
316
|
+
title: x.label,
|
|
317
|
+
value: x.value,
|
|
318
|
+
description: x.hint,
|
|
319
|
+
}))}
|
|
320
|
+
onSelect={(option) => resolve(option.value)}
|
|
321
|
+
/>
|
|
322
|
+
),
|
|
323
|
+
() => resolve(null),
|
|
324
|
+
)
|
|
325
|
+
})
|
|
326
|
+
if (value === null) return null
|
|
327
|
+
inputs[prompt.key] = value
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const value = await new Promise<string | null>((resolve) => {
|
|
332
|
+
props.dialog.replace(
|
|
333
|
+
() => (
|
|
334
|
+
<DialogPrompt title={prompt.message} placeholder={prompt.placeholder} onConfirm={(value) => resolve(value)} />
|
|
335
|
+
),
|
|
336
|
+
() => resolve(null),
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
if (value === null) return null
|
|
340
|
+
inputs[prompt.key] = value
|
|
341
|
+
}
|
|
342
|
+
return inputs
|
|
343
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core"
|
|
2
|
+
import { useTheme } from "../context/theme"
|
|
3
|
+
import { useDialog } from "../ui/dialog"
|
|
4
|
+
import { createStore } from "solid-js/store"
|
|
5
|
+
import { For } from "solid-js"
|
|
6
|
+
import { useKeyboard } from "@opentui/solid"
|
|
7
|
+
|
|
8
|
+
export function DialogSessionDeleteFailed(props: {
|
|
9
|
+
session: string
|
|
10
|
+
workspace: string
|
|
11
|
+
onDelete?: () => boolean | void | Promise<boolean | void>
|
|
12
|
+
onRestore?: () => boolean | void | Promise<boolean | void>
|
|
13
|
+
onDone?: () => void
|
|
14
|
+
}) {
|
|
15
|
+
const dialog = useDialog()
|
|
16
|
+
const { theme } = useTheme()
|
|
17
|
+
const [store, setStore] = createStore({
|
|
18
|
+
active: "delete" as "delete" | "restore",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const options = [
|
|
22
|
+
{
|
|
23
|
+
id: "delete" as const,
|
|
24
|
+
title: "Delete workspace",
|
|
25
|
+
description: "Delete the workspace and all sessions attached to it.",
|
|
26
|
+
run: props.onDelete,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "restore" as const,
|
|
30
|
+
title: "Restore to new workspace",
|
|
31
|
+
description: "Try to restore this session into a new workspace.",
|
|
32
|
+
run: props.onRestore,
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
async function confirm() {
|
|
37
|
+
const result = await options.find((item) => item.id === store.active)?.run?.()
|
|
38
|
+
if (result === false) return
|
|
39
|
+
props.onDone?.()
|
|
40
|
+
if (!props.onDone) dialog.clear()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
useKeyboard((evt) => {
|
|
44
|
+
if (evt.name === "return") {
|
|
45
|
+
evt.preventDefault()
|
|
46
|
+
evt.stopPropagation()
|
|
47
|
+
void confirm()
|
|
48
|
+
}
|
|
49
|
+
if (evt.name === "left" || evt.name === "up") {
|
|
50
|
+
setStore("active", "delete")
|
|
51
|
+
}
|
|
52
|
+
if (evt.name === "right" || evt.name === "down") {
|
|
53
|
+
setStore("active", "restore")
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<box paddingLeft={2} paddingRight={2} gap={1}>
|
|
59
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
60
|
+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
|
61
|
+
Failed to Delete Session
|
|
62
|
+
</text>
|
|
63
|
+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
|
64
|
+
esc
|
|
65
|
+
</text>
|
|
66
|
+
</box>
|
|
67
|
+
<text fg={theme.textMuted} wrapMode="word">
|
|
68
|
+
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
|
|
69
|
+
</text>
|
|
70
|
+
<text fg={theme.textMuted} wrapMode="word">
|
|
71
|
+
Choose how you want to recover this broken workspace session.
|
|
72
|
+
</text>
|
|
73
|
+
<box flexDirection="column" paddingBottom={1} gap={1}>
|
|
74
|
+
<For each={options}>
|
|
75
|
+
{(item) => (
|
|
76
|
+
<box
|
|
77
|
+
flexDirection="column"
|
|
78
|
+
paddingLeft={1}
|
|
79
|
+
paddingRight={1}
|
|
80
|
+
paddingTop={1}
|
|
81
|
+
paddingBottom={1}
|
|
82
|
+
backgroundColor={item.id === store.active ? theme.primary : undefined}
|
|
83
|
+
onMouseUp={() => {
|
|
84
|
+
setStore("active", item.id)
|
|
85
|
+
void confirm()
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<text
|
|
89
|
+
attributes={TextAttributes.BOLD}
|
|
90
|
+
fg={item.id === store.active ? theme.selectedListItemText : theme.text}
|
|
91
|
+
>
|
|
92
|
+
{item.title}
|
|
93
|
+
</text>
|
|
94
|
+
<text fg={item.id === store.active ? theme.selectedListItemText : theme.textMuted} wrapMode="word">
|
|
95
|
+
{item.description}
|
|
96
|
+
</text>
|
|
97
|
+
</box>
|
|
98
|
+
)}
|
|
99
|
+
</For>
|
|
100
|
+
</box>
|
|
101
|
+
</box>
|
|
102
|
+
)
|
|
103
|
+
}
|