@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.
- package/AGENTS.md +30 -0
- package/README.md +51 -0
- package/bunfig.toml +2 -0
- package/e2e/context.spec.ts +45 -0
- package/e2e/file-open.spec.ts +23 -0
- package/e2e/file-viewer.spec.ts +35 -0
- package/e2e/fixtures.ts +40 -0
- package/e2e/home.spec.ts +21 -0
- package/e2e/model-picker.spec.ts +43 -0
- package/e2e/navigation.spec.ts +9 -0
- package/e2e/palette.spec.ts +15 -0
- package/e2e/prompt-mention.spec.ts +26 -0
- package/e2e/prompt-slash-open.spec.ts +22 -0
- package/e2e/prompt.spec.ts +62 -0
- package/e2e/session.spec.ts +21 -0
- package/e2e/settings.spec.ts +44 -0
- package/e2e/sidebar.spec.ts +21 -0
- package/e2e/terminal-init.spec.ts +25 -0
- package/e2e/terminal.spec.ts +16 -0
- package/e2e/tsconfig.json +8 -0
- package/e2e/utils.ts +38 -0
- package/happydom.ts +75 -0
- package/index.html +23 -0
- package/package.json +72 -0
- package/playwright.config.ts +43 -0
- package/public/_headers +17 -0
- package/public/apple-touch-icon-v3.png +1 -0
- package/public/apple-touch-icon.png +1 -0
- package/public/favicon-96x96-v3.png +1 -0
- package/public/favicon-96x96.png +1 -0
- package/public/favicon-v3.ico +1 -0
- package/public/favicon-v3.svg +1 -0
- package/public/favicon.ico +1 -0
- package/public/favicon.svg +1 -0
- package/public/oc-theme-preload.js +28 -0
- package/public/site.webmanifest +1 -0
- package/public/social-share-zen.png +1 -0
- package/public/social-share.png +1 -0
- package/public/web-app-manifest-192x192.png +1 -0
- package/public/web-app-manifest-512x512.png +1 -0
- package/script/e2e-local.ts +143 -0
- package/src/addons/serialize.test.ts +319 -0
- package/src/addons/serialize.ts +591 -0
- package/src/app.tsx +150 -0
- package/src/components/dialog-connect-provider.tsx +428 -0
- package/src/components/dialog-edit-project.tsx +259 -0
- package/src/components/dialog-fork.tsx +104 -0
- package/src/components/dialog-manage-models.tsx +59 -0
- package/src/components/dialog-select-directory.tsx +208 -0
- package/src/components/dialog-select-file.tsx +196 -0
- package/src/components/dialog-select-mcp.tsx +96 -0
- package/src/components/dialog-select-model-unpaid.tsx +130 -0
- package/src/components/dialog-select-model.tsx +162 -0
- package/src/components/dialog-select-provider.tsx +70 -0
- package/src/components/dialog-select-server.tsx +249 -0
- package/src/components/dialog-settings.tsx +112 -0
- package/src/components/file-tree.tsx +112 -0
- package/src/components/link.tsx +17 -0
- package/src/components/model-tooltip.tsx +91 -0
- package/src/components/prompt-input.tsx +2076 -0
- package/src/components/session/index.ts +5 -0
- package/src/components/session/session-context-tab.tsx +428 -0
- package/src/components/session/session-header.tsx +343 -0
- package/src/components/session/session-new-view.tsx +93 -0
- package/src/components/session/session-sortable-tab.tsx +56 -0
- package/src/components/session/session-sortable-terminal-tab.tsx +187 -0
- package/src/components/session-context-usage.tsx +113 -0
- package/src/components/session-lsp-indicator.tsx +42 -0
- package/src/components/session-mcp-indicator.tsx +34 -0
- package/src/components/settings-agents.tsx +15 -0
- package/src/components/settings-commands.tsx +15 -0
- package/src/components/settings-general.tsx +306 -0
- package/src/components/settings-keybinds.tsx +437 -0
- package/src/components/settings-mcp.tsx +15 -0
- package/src/components/settings-models.tsx +15 -0
- package/src/components/settings-permissions.tsx +234 -0
- package/src/components/settings-providers.tsx +15 -0
- package/src/components/terminal.tsx +315 -0
- package/src/components/titlebar.tsx +156 -0
- package/src/context/command.tsx +308 -0
- package/src/context/comments.tsx +140 -0
- package/src/context/file.tsx +409 -0
- package/src/context/global-sdk.tsx +106 -0
- package/src/context/global-sync.tsx +898 -0
- package/src/context/language.tsx +161 -0
- package/src/context/layout-scroll.test.ts +73 -0
- package/src/context/layout-scroll.ts +118 -0
- package/src/context/layout.tsx +648 -0
- package/src/context/local.tsx +578 -0
- package/src/context/notification.tsx +173 -0
- package/src/context/permission.tsx +167 -0
- package/src/context/platform.tsx +59 -0
- package/src/context/prompt.tsx +245 -0
- package/src/context/sdk.tsx +48 -0
- package/src/context/server.tsx +214 -0
- package/src/context/settings.tsx +166 -0
- package/src/context/sync.tsx +320 -0
- package/src/context/terminal.tsx +267 -0
- package/src/custom-elements.d.ts +17 -0
- package/src/entry.tsx +76 -0
- package/src/env.d.ts +8 -0
- package/src/hooks/use-providers.ts +31 -0
- package/src/i18n/ar.ts +656 -0
- package/src/i18n/br.ts +667 -0
- package/src/i18n/da.ts +582 -0
- package/src/i18n/de.ts +591 -0
- package/src/i18n/en.ts +665 -0
- package/src/i18n/es.ts +585 -0
- package/src/i18n/fr.ts +592 -0
- package/src/i18n/ja.ts +579 -0
- package/src/i18n/ko.ts +580 -0
- package/src/i18n/no.ts +602 -0
- package/src/i18n/pl.ts +661 -0
- package/src/i18n/ru.ts +664 -0
- package/src/i18n/zh.ts +574 -0
- package/src/i18n/zht.ts +570 -0
- package/src/index.css +57 -0
- package/src/index.ts +2 -0
- package/src/pages/directory-layout.tsx +57 -0
- package/src/pages/error.tsx +290 -0
- package/src/pages/home.tsx +125 -0
- package/src/pages/layout.tsx +2599 -0
- package/src/pages/session.tsx +2505 -0
- package/src/sst-env.d.ts +10 -0
- package/src/utils/dom.ts +51 -0
- package/src/utils/id.ts +99 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/perf.ts +135 -0
- package/src/utils/persist.ts +377 -0
- package/src/utils/prompt.ts +203 -0
- package/src/utils/same.ts +6 -0
- package/src/utils/solid-dnd.tsx +55 -0
- package/src/utils/sound.ts +110 -0
- package/src/utils/speech.ts +302 -0
- package/src/utils/worktree.ts +58 -0
- package/sst-env.d.ts +9 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +15 -0
- 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
|
+
}
|