@jonsoc/app 1.1.34

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 (139) hide show
  1. package/AGENTS.md +30 -0
  2. package/README.md +51 -0
  3. package/bunfig.toml +2 -0
  4. package/e2e/context.spec.ts +45 -0
  5. package/e2e/file-open.spec.ts +23 -0
  6. package/e2e/file-viewer.spec.ts +35 -0
  7. package/e2e/fixtures.ts +40 -0
  8. package/e2e/home.spec.ts +21 -0
  9. package/e2e/model-picker.spec.ts +43 -0
  10. package/e2e/navigation.spec.ts +9 -0
  11. package/e2e/palette.spec.ts +15 -0
  12. package/e2e/prompt-mention.spec.ts +26 -0
  13. package/e2e/prompt-slash-open.spec.ts +22 -0
  14. package/e2e/prompt.spec.ts +62 -0
  15. package/e2e/session.spec.ts +21 -0
  16. package/e2e/settings.spec.ts +44 -0
  17. package/e2e/sidebar.spec.ts +21 -0
  18. package/e2e/terminal-init.spec.ts +25 -0
  19. package/e2e/terminal.spec.ts +16 -0
  20. package/e2e/tsconfig.json +8 -0
  21. package/e2e/utils.ts +38 -0
  22. package/happydom.ts +75 -0
  23. package/index.html +23 -0
  24. package/package.json +72 -0
  25. package/playwright.config.ts +43 -0
  26. package/public/_headers +17 -0
  27. package/public/apple-touch-icon-v3.png +1 -0
  28. package/public/apple-touch-icon.png +1 -0
  29. package/public/favicon-96x96-v3.png +1 -0
  30. package/public/favicon-96x96.png +1 -0
  31. package/public/favicon-v3.ico +1 -0
  32. package/public/favicon-v3.svg +1 -0
  33. package/public/favicon.ico +1 -0
  34. package/public/favicon.svg +1 -0
  35. package/public/oc-theme-preload.js +28 -0
  36. package/public/site.webmanifest +1 -0
  37. package/public/social-share-zen.png +1 -0
  38. package/public/social-share.png +1 -0
  39. package/public/web-app-manifest-192x192.png +1 -0
  40. package/public/web-app-manifest-512x512.png +1 -0
  41. package/script/e2e-local.ts +143 -0
  42. package/src/addons/serialize.test.ts +319 -0
  43. package/src/addons/serialize.ts +591 -0
  44. package/src/app.tsx +150 -0
  45. package/src/components/dialog-connect-provider.tsx +428 -0
  46. package/src/components/dialog-edit-project.tsx +259 -0
  47. package/src/components/dialog-fork.tsx +104 -0
  48. package/src/components/dialog-manage-models.tsx +59 -0
  49. package/src/components/dialog-select-directory.tsx +208 -0
  50. package/src/components/dialog-select-file.tsx +196 -0
  51. package/src/components/dialog-select-mcp.tsx +96 -0
  52. package/src/components/dialog-select-model-unpaid.tsx +130 -0
  53. package/src/components/dialog-select-model.tsx +162 -0
  54. package/src/components/dialog-select-provider.tsx +70 -0
  55. package/src/components/dialog-select-server.tsx +249 -0
  56. package/src/components/dialog-settings.tsx +112 -0
  57. package/src/components/file-tree.tsx +112 -0
  58. package/src/components/link.tsx +17 -0
  59. package/src/components/model-tooltip.tsx +91 -0
  60. package/src/components/prompt-input.tsx +2076 -0
  61. package/src/components/session/index.ts +5 -0
  62. package/src/components/session/session-context-tab.tsx +428 -0
  63. package/src/components/session/session-header.tsx +343 -0
  64. package/src/components/session/session-new-view.tsx +93 -0
  65. package/src/components/session/session-sortable-tab.tsx +56 -0
  66. package/src/components/session/session-sortable-terminal-tab.tsx +187 -0
  67. package/src/components/session-context-usage.tsx +113 -0
  68. package/src/components/session-lsp-indicator.tsx +42 -0
  69. package/src/components/session-mcp-indicator.tsx +34 -0
  70. package/src/components/settings-agents.tsx +15 -0
  71. package/src/components/settings-commands.tsx +15 -0
  72. package/src/components/settings-general.tsx +306 -0
  73. package/src/components/settings-keybinds.tsx +437 -0
  74. package/src/components/settings-mcp.tsx +15 -0
  75. package/src/components/settings-models.tsx +15 -0
  76. package/src/components/settings-permissions.tsx +234 -0
  77. package/src/components/settings-providers.tsx +15 -0
  78. package/src/components/terminal.tsx +315 -0
  79. package/src/components/titlebar.tsx +156 -0
  80. package/src/context/command.tsx +308 -0
  81. package/src/context/comments.tsx +140 -0
  82. package/src/context/file.tsx +409 -0
  83. package/src/context/global-sdk.tsx +106 -0
  84. package/src/context/global-sync.tsx +898 -0
  85. package/src/context/language.tsx +161 -0
  86. package/src/context/layout-scroll.test.ts +73 -0
  87. package/src/context/layout-scroll.ts +118 -0
  88. package/src/context/layout.tsx +648 -0
  89. package/src/context/local.tsx +578 -0
  90. package/src/context/notification.tsx +173 -0
  91. package/src/context/permission.tsx +167 -0
  92. package/src/context/platform.tsx +59 -0
  93. package/src/context/prompt.tsx +245 -0
  94. package/src/context/sdk.tsx +48 -0
  95. package/src/context/server.tsx +214 -0
  96. package/src/context/settings.tsx +166 -0
  97. package/src/context/sync.tsx +320 -0
  98. package/src/context/terminal.tsx +267 -0
  99. package/src/custom-elements.d.ts +17 -0
  100. package/src/entry.tsx +76 -0
  101. package/src/env.d.ts +8 -0
  102. package/src/hooks/use-providers.ts +31 -0
  103. package/src/i18n/ar.ts +656 -0
  104. package/src/i18n/br.ts +667 -0
  105. package/src/i18n/da.ts +582 -0
  106. package/src/i18n/de.ts +591 -0
  107. package/src/i18n/en.ts +665 -0
  108. package/src/i18n/es.ts +585 -0
  109. package/src/i18n/fr.ts +592 -0
  110. package/src/i18n/ja.ts +579 -0
  111. package/src/i18n/ko.ts +580 -0
  112. package/src/i18n/no.ts +602 -0
  113. package/src/i18n/pl.ts +661 -0
  114. package/src/i18n/ru.ts +664 -0
  115. package/src/i18n/zh.ts +574 -0
  116. package/src/i18n/zht.ts +570 -0
  117. package/src/index.css +57 -0
  118. package/src/index.ts +2 -0
  119. package/src/pages/directory-layout.tsx +57 -0
  120. package/src/pages/error.tsx +290 -0
  121. package/src/pages/home.tsx +125 -0
  122. package/src/pages/layout.tsx +2599 -0
  123. package/src/pages/session.tsx +2505 -0
  124. package/src/sst-env.d.ts +10 -0
  125. package/src/utils/dom.ts +51 -0
  126. package/src/utils/id.ts +99 -0
  127. package/src/utils/index.ts +1 -0
  128. package/src/utils/perf.ts +135 -0
  129. package/src/utils/persist.ts +377 -0
  130. package/src/utils/prompt.ts +203 -0
  131. package/src/utils/same.ts +6 -0
  132. package/src/utils/solid-dnd.tsx +55 -0
  133. package/src/utils/sound.ts +110 -0
  134. package/src/utils/speech.ts +302 -0
  135. package/src/utils/worktree.ts +58 -0
  136. package/sst-env.d.ts +9 -0
  137. package/tsconfig.json +26 -0
  138. package/vite.config.ts +15 -0
  139. package/vite.js +26 -0
package/src/app.tsx ADDED
@@ -0,0 +1,150 @@
1
+ import "@/index.css"
2
+ import { ErrorBoundary, Show, lazy, type ParentProps } from "solid-js"
3
+ import { Router, Route, Navigate } from "@solidjs/router"
4
+ import { MetaProvider } from "@solidjs/meta"
5
+ import { Font } from "@jonsoc/ui/font"
6
+ import { MarkedProvider } from "@jonsoc/ui/context/marked"
7
+ import { DiffComponentProvider } from "@jonsoc/ui/context/diff"
8
+ import { CodeComponentProvider } from "@jonsoc/ui/context/code"
9
+ import { I18nProvider } from "@jonsoc/ui/context"
10
+ import { Diff } from "@jonsoc/ui/diff"
11
+ import { Code } from "@jonsoc/ui/code"
12
+ import { ThemeProvider } from "@jonsoc/ui/theme"
13
+ import { GlobalSyncProvider } from "@/context/global-sync"
14
+ import { PermissionProvider } from "@/context/permission"
15
+ import { LayoutProvider } from "@/context/layout"
16
+ import { GlobalSDKProvider } from "@/context/global-sdk"
17
+ import { ServerProvider, useServer } from "@/context/server"
18
+ import { SettingsProvider } from "@/context/settings"
19
+ import { TerminalProvider } from "@/context/terminal"
20
+ import { PromptProvider } from "@/context/prompt"
21
+ import { FileProvider } from "@/context/file"
22
+ import { CommentsProvider } from "@/context/comments"
23
+ import { NotificationProvider } from "@/context/notification"
24
+ import { DialogProvider } from "@jonsoc/ui/context/dialog"
25
+ import { CommandProvider } from "@/context/command"
26
+ import { LanguageProvider, useLanguage } from "@/context/language"
27
+ import { usePlatform } from "@/context/platform"
28
+ import { Logo } from "@jonsoc/ui/logo"
29
+ import Layout from "@/pages/layout"
30
+ import DirectoryLayout from "@/pages/directory-layout"
31
+ import { ErrorPage } from "./pages/error"
32
+ import { iife } from "@jonsoc/util/iife"
33
+ import { Suspense } from "solid-js"
34
+
35
+ const Home = lazy(() => import("@/pages/home"))
36
+ const Session = lazy(() => import("@/pages/session"))
37
+ const Loading = () => <div class="size-full" />
38
+
39
+ function UiI18nBridge(props: ParentProps) {
40
+ const language = useLanguage()
41
+ return <I18nProvider value={{ locale: language.locale, t: language.t }}>{props.children}</I18nProvider>
42
+ }
43
+
44
+ declare global {
45
+ interface Window {
46
+ __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
47
+ }
48
+ }
49
+
50
+ function MarkedProviderWithNativeParser(props: ParentProps) {
51
+ const platform = usePlatform()
52
+ return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
53
+ }
54
+
55
+ export function AppBaseProviders(props: ParentProps) {
56
+ return (
57
+ <MetaProvider>
58
+ <Font />
59
+ <ThemeProvider>
60
+ <LanguageProvider>
61
+ <UiI18nBridge>
62
+ <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
63
+ <DialogProvider>
64
+ <MarkedProviderWithNativeParser>
65
+ <DiffComponentProvider component={Diff}>
66
+ <CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
67
+ </DiffComponentProvider>
68
+ </MarkedProviderWithNativeParser>
69
+ </DialogProvider>
70
+ </ErrorBoundary>
71
+ </UiI18nBridge>
72
+ </LanguageProvider>
73
+ </ThemeProvider>
74
+ </MetaProvider>
75
+ )
76
+ }
77
+
78
+ function ServerKey(props: ParentProps) {
79
+ const server = useServer()
80
+ return (
81
+ <Show when={server.url} keyed>
82
+ {props.children}
83
+ </Show>
84
+ )
85
+ }
86
+
87
+ export function AppInterface(props: { defaultUrl?: string }) {
88
+ const defaultServerUrl = () => {
89
+ if (props.defaultUrl) return props.defaultUrl
90
+ if (location.hostname.includes("jonsoc.com")) return "http://localhost:4096"
91
+ if (import.meta.env.DEV)
92
+ return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
93
+
94
+ return window.location.origin
95
+ }
96
+
97
+ return (
98
+ <ServerProvider defaultUrl={defaultServerUrl()}>
99
+ <ServerKey>
100
+ <GlobalSDKProvider>
101
+ <GlobalSyncProvider>
102
+ <Router
103
+ root={(props) => (
104
+ <SettingsProvider>
105
+ <PermissionProvider>
106
+ <LayoutProvider>
107
+ <NotificationProvider>
108
+ <CommandProvider>
109
+ <Layout>{props.children}</Layout>
110
+ </CommandProvider>
111
+ </NotificationProvider>
112
+ </LayoutProvider>
113
+ </PermissionProvider>
114
+ </SettingsProvider>
115
+ )}
116
+ >
117
+ <Route
118
+ path="/"
119
+ component={() => (
120
+ <Suspense fallback={<Loading />}>
121
+ <Home />
122
+ </Suspense>
123
+ )}
124
+ />
125
+ <Route path="/:dir" component={DirectoryLayout}>
126
+ <Route path="/" component={() => <Navigate href="session" />} />
127
+ <Route
128
+ path="/session/:id?"
129
+ component={() => (
130
+ <TerminalProvider>
131
+ <FileProvider>
132
+ <PromptProvider>
133
+ <CommentsProvider>
134
+ <Suspense fallback={<Loading />}>
135
+ <Session />
136
+ </Suspense>
137
+ </CommentsProvider>
138
+ </PromptProvider>
139
+ </FileProvider>
140
+ </TerminalProvider>
141
+ )}
142
+ />
143
+ </Route>
144
+ </Router>
145
+ </GlobalSyncProvider>
146
+ </GlobalSDKProvider>
147
+ </ServerKey>
148
+ </ServerProvider>
149
+ )
150
+ }
@@ -0,0 +1,428 @@
1
+ import type { ProviderAuthAuthorization } from "@jonsoc/sdk/v2/client"
2
+ import { Button } from "@jonsoc/ui/button"
3
+ import { useDialog } from "@jonsoc/ui/context/dialog"
4
+ import { Dialog } from "@jonsoc/ui/dialog"
5
+ import { Icon } from "@jonsoc/ui/icon"
6
+ import { IconButton } from "@jonsoc/ui/icon-button"
7
+ import type { IconName } from "@jonsoc/ui/icons/provider"
8
+ import { List, type ListRef } from "@jonsoc/ui/list"
9
+ import { ProviderIcon } from "@jonsoc/ui/provider-icon"
10
+ import { Spinner } from "@jonsoc/ui/spinner"
11
+ import { TextField } from "@jonsoc/ui/text-field"
12
+ import { showToast } from "@jonsoc/ui/toast"
13
+ import { iife } from "@jonsoc/util/iife"
14
+ import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
15
+ import { createStore, produce } from "solid-js/store"
16
+ import { Link } from "@/components/link"
17
+ import { useLanguage } from "@/context/language"
18
+ import { useGlobalSDK } from "@/context/global-sdk"
19
+ import { useGlobalSync } from "@/context/global-sync"
20
+ import { usePlatform } from "@/context/platform"
21
+ import { DialogSelectModel } from "./dialog-select-model"
22
+ import { DialogSelectProvider } from "./dialog-select-provider"
23
+
24
+ export function DialogConnectProvider(props: { provider: string }) {
25
+ const dialog = useDialog()
26
+ const globalSync = useGlobalSync()
27
+ const globalSDK = useGlobalSDK()
28
+ const platform = usePlatform()
29
+ const language = useLanguage()
30
+ const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
31
+ const methods = createMemo(
32
+ () =>
33
+ globalSync.data.provider_auth[props.provider] ?? [
34
+ {
35
+ type: "api",
36
+ label: language.t("provider.connect.method.apiKey"),
37
+ },
38
+ ],
39
+ )
40
+ const [store, setStore] = createStore({
41
+ methodIndex: undefined as undefined | number,
42
+ authorization: undefined as undefined | ProviderAuthAuthorization,
43
+ state: "pending" as undefined | "pending" | "complete" | "error",
44
+ error: undefined as string | undefined,
45
+ })
46
+
47
+ const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
48
+
49
+ const methodLabel = (value?: { type?: string; label?: string }) => {
50
+ if (!value) return ""
51
+ if (value.type === "api") return language.t("provider.connect.method.apiKey")
52
+ return value.label ?? ""
53
+ }
54
+
55
+ async function selectMethod(index: number) {
56
+ const method = methods()[index]
57
+ setStore(
58
+ produce((draft) => {
59
+ draft.methodIndex = index
60
+ draft.authorization = undefined
61
+ draft.state = undefined
62
+ draft.error = undefined
63
+ }),
64
+ )
65
+
66
+ if (method.type === "oauth") {
67
+ setStore("state", "pending")
68
+ const start = Date.now()
69
+ await globalSDK.client.provider.oauth
70
+ .authorize(
71
+ {
72
+ providerID: props.provider,
73
+ method: index,
74
+ },
75
+ { throwOnError: true },
76
+ )
77
+ .then((x) => {
78
+ const elapsed = Date.now() - start
79
+ const delay = 1000 - elapsed
80
+
81
+ if (delay > 0) {
82
+ setTimeout(() => {
83
+ setStore("state", "complete")
84
+ setStore("authorization", x.data!)
85
+ }, delay)
86
+ return
87
+ }
88
+ setStore("state", "complete")
89
+ setStore("authorization", x.data!)
90
+ })
91
+ .catch((e) => {
92
+ setStore("state", "error")
93
+ setStore("error", String(e))
94
+ })
95
+ }
96
+ }
97
+
98
+ let listRef: ListRef | undefined
99
+ function handleKey(e: KeyboardEvent) {
100
+ if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
101
+ return
102
+ }
103
+ if (e.key === "Escape") return
104
+ listRef?.onKeyDown(e)
105
+ }
106
+
107
+ onMount(() => {
108
+ if (methods().length === 1) {
109
+ selectMethod(0)
110
+ }
111
+ document.addEventListener("keydown", handleKey)
112
+ onCleanup(() => {
113
+ document.removeEventListener("keydown", handleKey)
114
+ })
115
+ })
116
+
117
+ async function complete() {
118
+ await globalSDK.client.global.dispose()
119
+ dialog.close()
120
+ showToast({
121
+ variant: "success",
122
+ icon: "circle-check",
123
+ title: language.t("provider.connect.toast.connected.title", { provider: provider().name }),
124
+ description: language.t("provider.connect.toast.connected.description", { provider: provider().name }),
125
+ })
126
+ }
127
+
128
+ function goBack() {
129
+ if (methods().length === 1) {
130
+ dialog.show(() => <DialogSelectProvider />)
131
+ return
132
+ }
133
+ if (store.authorization) {
134
+ setStore("authorization", undefined)
135
+ setStore("methodIndex", undefined)
136
+ return
137
+ }
138
+ if (store.methodIndex) {
139
+ setStore("methodIndex", undefined)
140
+ return
141
+ }
142
+ dialog.show(() => <DialogSelectProvider />)
143
+ }
144
+
145
+ return (
146
+ <Dialog
147
+ title={
148
+ <IconButton
149
+ tabIndex={-1}
150
+ icon="arrow-left"
151
+ variant="ghost"
152
+ onClick={goBack}
153
+ aria-label={language.t("common.goBack")}
154
+ />
155
+ }
156
+ >
157
+ <div class="flex flex-col gap-6 px-2.5 pb-3">
158
+ <div class="px-2.5 flex gap-4 items-center">
159
+ <ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
160
+ <div class="text-16-medium text-text-strong">
161
+ <Switch>
162
+ <Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
163
+ {language.t("provider.connect.title.anthropicProMax")}
164
+ </Match>
165
+ <Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
166
+ </Switch>
167
+ </div>
168
+ </div>
169
+ <div class="px-2.5 pb-10 flex flex-col gap-6">
170
+ <Switch>
171
+ <Match when={store.methodIndex === undefined}>
172
+ <div class="text-14-regular text-text-base">
173
+ {language.t("provider.connect.selectMethod", { provider: provider().name })}
174
+ </div>
175
+ <div class="">
176
+ <List
177
+ ref={(ref) => {
178
+ listRef = ref
179
+ }}
180
+ items={methods}
181
+ key={(m) => m?.label}
182
+ onSelect={async (method, index) => {
183
+ if (!method) return
184
+ selectMethod(index)
185
+ }}
186
+ >
187
+ {(i) => (
188
+ <div class="w-full flex items-center gap-x-2">
189
+ <div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
190
+ <div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
191
+ </div>
192
+ <span>{methodLabel(i)}</span>
193
+ </div>
194
+ )}
195
+ </List>
196
+ </div>
197
+ </Match>
198
+ <Match when={store.state === "pending"}>
199
+ <div class="text-14-regular text-text-base">
200
+ <div class="flex items-center gap-x-2">
201
+ <Spinner />
202
+ <span>{language.t("provider.connect.status.inProgress")}</span>
203
+ </div>
204
+ </div>
205
+ </Match>
206
+ <Match when={store.state === "error"}>
207
+ <div class="text-14-regular text-text-base">
208
+ <div class="flex items-center gap-x-2">
209
+ <Icon name="circle-ban-sign" class="text-icon-critical-base" />
210
+ <span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
211
+ </div>
212
+ </div>
213
+ </Match>
214
+ <Match when={method()?.type === "api"}>
215
+ {iife(() => {
216
+ const [formStore, setFormStore] = createStore({
217
+ value: "",
218
+ error: undefined as string | undefined,
219
+ })
220
+
221
+ async function handleSubmit(e: SubmitEvent) {
222
+ e.preventDefault()
223
+
224
+ const form = e.currentTarget as HTMLFormElement
225
+ const formData = new FormData(form)
226
+ const apiKey = formData.get("apiKey") as string
227
+
228
+ if (!apiKey?.trim()) {
229
+ setFormStore("error", language.t("provider.connect.apiKey.required"))
230
+ return
231
+ }
232
+
233
+ setFormStore("error", undefined)
234
+ await globalSDK.client.auth.set({
235
+ providerID: props.provider,
236
+ auth: {
237
+ type: "api",
238
+ key: apiKey,
239
+ },
240
+ })
241
+ await complete()
242
+ }
243
+
244
+ return (
245
+ <div class="flex flex-col gap-6">
246
+ <Switch>
247
+ <Match when={provider().id === "jonsoc"}>
248
+ <div class="flex flex-col gap-4">
249
+ <div class="text-14-regular text-text-base">
250
+ {language.t("provider.connect.jonsocZen.line1")}
251
+ </div>
252
+ <div class="text-14-regular text-text-base">
253
+ {language.t("provider.connect.jonsocZen.line2")}
254
+ </div>
255
+ <div class="text-14-regular text-text-base">
256
+ {language.t("provider.connect.jonsocZen.visit.prefix")}
257
+ <Link href="https://jonsoc.com/zen" tabIndex={-1}>
258
+ {language.t("provider.connect.jonsocZen.visit.link")}
259
+ </Link>
260
+ {language.t("provider.connect.jonsocZen.visit.suffix")}
261
+ </div>
262
+ </div>
263
+ </Match>
264
+ <Match when={true}>
265
+ <div class="text-14-regular text-text-base">
266
+ {language.t("provider.connect.apiKey.description", { provider: provider().name })}
267
+ </div>
268
+ </Match>
269
+ </Switch>
270
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
271
+ <TextField
272
+ autofocus
273
+ type="text"
274
+ label={language.t("provider.connect.apiKey.label", { provider: provider().name })}
275
+ placeholder={language.t("provider.connect.apiKey.placeholder")}
276
+ name="apiKey"
277
+ value={formStore.value}
278
+ onChange={setFormStore.bind(null, "value")}
279
+ validationState={formStore.error ? "invalid" : undefined}
280
+ error={formStore.error}
281
+ />
282
+ <Button class="w-auto" type="submit" size="large" variant="primary">
283
+ {language.t("common.submit")}
284
+ </Button>
285
+ </form>
286
+ </div>
287
+ )
288
+ })}
289
+ </Match>
290
+ <Match when={method()?.type === "oauth"}>
291
+ <Switch>
292
+ <Match when={store.authorization?.method === "code"}>
293
+ {iife(() => {
294
+ const [formStore, setFormStore] = createStore({
295
+ value: "",
296
+ error: undefined as string | undefined,
297
+ })
298
+
299
+ onMount(() => {
300
+ if (store.authorization?.method === "code" && store.authorization?.url) {
301
+ platform.openLink(store.authorization.url)
302
+ }
303
+ })
304
+
305
+ async function handleSubmit(e: SubmitEvent) {
306
+ e.preventDefault()
307
+
308
+ const form = e.currentTarget as HTMLFormElement
309
+ const formData = new FormData(form)
310
+ const code = formData.get("code") as string
311
+
312
+ if (!code?.trim()) {
313
+ setFormStore("error", language.t("provider.connect.oauth.code.required"))
314
+ return
315
+ }
316
+
317
+ setFormStore("error", undefined)
318
+ const result = await globalSDK.client.provider.oauth
319
+ .callback({
320
+ providerID: props.provider,
321
+ method: store.methodIndex,
322
+ code,
323
+ })
324
+ .then((value) =>
325
+ value.error ? { ok: false as const, error: value.error } : { ok: true as const },
326
+ )
327
+ .catch((error) => ({ ok: false as const, error }))
328
+ if (result.ok) {
329
+ await complete()
330
+ return
331
+ }
332
+ const message = result.error instanceof Error ? result.error.message : String(result.error)
333
+ setFormStore("error", message || language.t("provider.connect.oauth.code.invalid"))
334
+ }
335
+
336
+ return (
337
+ <div class="flex flex-col gap-6">
338
+ <div class="text-14-regular text-text-base">
339
+ {language.t("provider.connect.oauth.code.visit.prefix")}
340
+ <Link href={store.authorization!.url}>
341
+ {language.t("provider.connect.oauth.code.visit.link")}
342
+ </Link>
343
+ {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
344
+ </div>
345
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
346
+ <TextField
347
+ autofocus
348
+ type="text"
349
+ label={language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })}
350
+ placeholder={language.t("provider.connect.oauth.code.placeholder")}
351
+ name="code"
352
+ value={formStore.value}
353
+ onChange={setFormStore.bind(null, "value")}
354
+ validationState={formStore.error ? "invalid" : undefined}
355
+ error={formStore.error}
356
+ />
357
+ <Button class="w-auto" type="submit" size="large" variant="primary">
358
+ {language.t("common.submit")}
359
+ </Button>
360
+ </form>
361
+ </div>
362
+ )
363
+ })}
364
+ </Match>
365
+ <Match when={store.authorization?.method === "auto"}>
366
+ {iife(() => {
367
+ const code = createMemo(() => {
368
+ const instructions = store.authorization?.instructions
369
+ if (instructions?.includes(":")) {
370
+ return instructions?.split(":")[1]?.trim()
371
+ }
372
+ return instructions
373
+ })
374
+
375
+ onMount(async () => {
376
+ if (store.authorization?.url) {
377
+ platform.openLink(store.authorization.url)
378
+ }
379
+ const result = await globalSDK.client.provider.oauth
380
+ .callback({
381
+ providerID: props.provider,
382
+ method: store.methodIndex,
383
+ })
384
+ .then((value) =>
385
+ value.error ? { ok: false as const, error: value.error } : { ok: true as const },
386
+ )
387
+ .catch((error) => ({ ok: false as const, error }))
388
+ if (!result.ok) {
389
+ const message = result.error instanceof Error ? result.error.message : String(result.error)
390
+ setStore("state", "error")
391
+ setStore("error", message)
392
+ return
393
+ }
394
+ await complete()
395
+ })
396
+
397
+ return (
398
+ <div class="flex flex-col gap-6">
399
+ <div class="text-14-regular text-text-base">
400
+ {language.t("provider.connect.oauth.auto.visit.prefix")}
401
+ <Link href={store.authorization!.url}>
402
+ {language.t("provider.connect.oauth.auto.visit.link")}
403
+ </Link>
404
+ {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
405
+ </div>
406
+ <TextField
407
+ label={language.t("provider.connect.oauth.auto.confirmationCode")}
408
+ class="font-mono"
409
+ value={code()}
410
+ readOnly
411
+ copyable
412
+ />
413
+ <div class="text-14-regular text-text-base flex items-center gap-4">
414
+ <Spinner />
415
+ <span>{language.t("provider.connect.status.waiting")}</span>
416
+ </div>
417
+ </div>
418
+ )
419
+ })}
420
+ </Match>
421
+ </Switch>
422
+ </Match>
423
+ </Switch>
424
+ </div>
425
+ </div>
426
+ </Dialog>
427
+ )
428
+ }