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.
Files changed (113) hide show
  1. package/package.json +11 -11
  2. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  3. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  4. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  20. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  21. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  22. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  23. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  24. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  25. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  26. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  27. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  28. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  29. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  30. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  31. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  32. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  33. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  34. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  35. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  36. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  37. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  38. package/src/tool/apply_patch.ts +1 -334
  39. package/src/tool/bash.ts +1 -656
  40. package/src/tool/core/external-directory.ts +55 -0
  41. package/src/tool/core/invalid.ts +21 -0
  42. package/src/tool/core/recall.ts +164 -0
  43. package/src/tool/core/recall.txt +12 -0
  44. package/src/tool/core/schema.ts +16 -0
  45. package/src/tool/core/tool.ts +162 -0
  46. package/src/tool/core/truncate.ts +160 -0
  47. package/src/tool/core/truncation-dir.ts +4 -0
  48. package/src/tool/diagnostics.ts +1 -20
  49. package/src/tool/edit-replacers.ts +1 -288
  50. package/src/tool/edit-utils.ts +1 -86
  51. package/src/tool/edit.ts +1 -262
  52. package/src/tool/external-directory.ts +1 -55
  53. package/src/tool/file/apply_patch.ts +334 -0
  54. package/src/tool/file/apply_patch.txt +33 -0
  55. package/src/tool/file/bash.ts +656 -0
  56. package/src/tool/file/bash.txt +119 -0
  57. package/src/tool/file/edit-replacers.ts +288 -0
  58. package/src/tool/file/edit-utils.ts +86 -0
  59. package/src/tool/file/edit.ts +262 -0
  60. package/src/tool/file/edit.txt +10 -0
  61. package/src/tool/file/read.ts +389 -0
  62. package/src/tool/file/read.txt +14 -0
  63. package/src/tool/file/write.ts +114 -0
  64. package/src/tool/file/write.txt +8 -0
  65. package/src/tool/glob.ts +1 -115
  66. package/src/tool/grep.ts +1 -151
  67. package/src/tool/integration/diagnostics.ts +20 -0
  68. package/src/tool/integration/lsp.ts +113 -0
  69. package/src/tool/integration/lsp.txt +24 -0
  70. package/src/tool/integration/mcp-exa.ts +73 -0
  71. package/src/tool/integration/package.ts +168 -0
  72. package/src/tool/integration/registry.ts +375 -0
  73. package/src/tool/invalid.ts +1 -21
  74. package/src/tool/lsp.ts +1 -113
  75. package/src/tool/mcp-exa.ts +1 -73
  76. package/src/tool/package.ts +1 -168
  77. package/src/tool/plan.ts +1 -30
  78. package/src/tool/question.ts +1 -52
  79. package/src/tool/read.ts +1 -389
  80. package/src/tool/recall.ts +1 -164
  81. package/src/tool/registry.ts +1 -375
  82. package/src/tool/schema.ts +1 -16
  83. package/src/tool/search/glob.ts +115 -0
  84. package/src/tool/search/glob.txt +6 -0
  85. package/src/tool/search/grep.ts +151 -0
  86. package/src/tool/search/grep.txt +8 -0
  87. package/src/tool/search/warpgrep.ts +107 -0
  88. package/src/tool/search/warpgrep.txt +10 -0
  89. package/src/tool/search/webfetch.ts +202 -0
  90. package/src/tool/search/webfetch.txt +13 -0
  91. package/src/tool/search/websearch.ts +71 -0
  92. package/src/tool/search/websearch.txt +14 -0
  93. package/src/tool/skill.ts +1 -91
  94. package/src/tool/task.ts +1 -197
  95. package/src/tool/todo.ts +1 -62
  96. package/src/tool/tool.ts +1 -162
  97. package/src/tool/truncate.ts +1 -160
  98. package/src/tool/truncation-dir.ts +1 -4
  99. package/src/tool/warpgrep.ts +1 -107
  100. package/src/tool/webfetch.ts +1 -202
  101. package/src/tool/websearch.ts +1 -71
  102. package/src/tool/workflow/plan-enter.txt +14 -0
  103. package/src/tool/workflow/plan-exit.txt +13 -0
  104. package/src/tool/workflow/plan.ts +30 -0
  105. package/src/tool/workflow/question.ts +52 -0
  106. package/src/tool/workflow/question.txt +11 -0
  107. package/src/tool/workflow/skill.ts +91 -0
  108. package/src/tool/workflow/skill.txt +5 -0
  109. package/src/tool/workflow/task.ts +197 -0
  110. package/src/tool/workflow/task.txt +57 -0
  111. package/src/tool/workflow/todo.ts +62 -0
  112. package/src/tool/workflow/todowrite.txt +167 -0
  113. 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
+ }