@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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createMemo, Show, type ParentProps } from "solid-js"
|
|
2
|
+
import { useNavigate, useParams } from "@solidjs/router"
|
|
3
|
+
import { SDKProvider, useSDK } from "@/context/sdk"
|
|
4
|
+
import { SyncProvider, useSync } from "@/context/sync"
|
|
5
|
+
import { LocalProvider } from "@/context/local"
|
|
6
|
+
|
|
7
|
+
import { base64Decode } from "@jonsoc/util/encode"
|
|
8
|
+
import { DataProvider } from "@jonsoc/ui/context"
|
|
9
|
+
import { iife } from "@jonsoc/util/iife"
|
|
10
|
+
import type { QuestionAnswer } from "@jonsoc/sdk/v2"
|
|
11
|
+
|
|
12
|
+
export default function Layout(props: ParentProps) {
|
|
13
|
+
const params = useParams()
|
|
14
|
+
const navigate = useNavigate()
|
|
15
|
+
const directory = createMemo(() => {
|
|
16
|
+
return base64Decode(params.dir!)
|
|
17
|
+
})
|
|
18
|
+
return (
|
|
19
|
+
<Show when={params.dir}>
|
|
20
|
+
<SDKProvider directory={directory()}>
|
|
21
|
+
<SyncProvider>
|
|
22
|
+
{iife(() => {
|
|
23
|
+
const sync = useSync()
|
|
24
|
+
const sdk = useSDK()
|
|
25
|
+
const respond = (input: {
|
|
26
|
+
sessionID: string
|
|
27
|
+
permissionID: string
|
|
28
|
+
response: "once" | "always" | "reject"
|
|
29
|
+
}) => sdk.client.permission.respond(input)
|
|
30
|
+
|
|
31
|
+
const replyToQuestion = (input: { requestID: string; answers: QuestionAnswer[] }) =>
|
|
32
|
+
sdk.client.question.reply(input)
|
|
33
|
+
|
|
34
|
+
const rejectQuestion = (input: { requestID: string }) => sdk.client.question.reject(input)
|
|
35
|
+
|
|
36
|
+
const navigateToSession = (sessionID: string) => {
|
|
37
|
+
navigate(`/${params.dir}/session/${sessionID}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<DataProvider
|
|
42
|
+
data={sync.data}
|
|
43
|
+
directory={directory()}
|
|
44
|
+
onPermissionRespond={respond}
|
|
45
|
+
onQuestionReply={replyToQuestion}
|
|
46
|
+
onQuestionReject={rejectQuestion}
|
|
47
|
+
onNavigateToSession={navigateToSession}
|
|
48
|
+
>
|
|
49
|
+
<LocalProvider>{props.children}</LocalProvider>
|
|
50
|
+
</DataProvider>
|
|
51
|
+
)
|
|
52
|
+
})}
|
|
53
|
+
</SyncProvider>
|
|
54
|
+
</SDKProvider>
|
|
55
|
+
</Show>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { TextField } from "@jonsoc/ui/text-field"
|
|
2
|
+
import { Logo } from "@jonsoc/ui/logo"
|
|
3
|
+
import { Button } from "@jonsoc/ui/button"
|
|
4
|
+
import { Component, Show } from "solid-js"
|
|
5
|
+
import { createStore } from "solid-js/store"
|
|
6
|
+
import { usePlatform } from "@/context/platform"
|
|
7
|
+
import { useLanguage } from "@/context/language"
|
|
8
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
9
|
+
|
|
10
|
+
export type InitError = {
|
|
11
|
+
name: string
|
|
12
|
+
data: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Translator = ReturnType<typeof useLanguage>["t"]
|
|
16
|
+
|
|
17
|
+
function isInitError(error: unknown): error is InitError {
|
|
18
|
+
return (
|
|
19
|
+
typeof error === "object" &&
|
|
20
|
+
error !== null &&
|
|
21
|
+
"name" in error &&
|
|
22
|
+
"data" in error &&
|
|
23
|
+
typeof (error as InitError).data === "object"
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeJson(value: unknown): string {
|
|
28
|
+
const seen = new WeakSet<object>()
|
|
29
|
+
const json = JSON.stringify(
|
|
30
|
+
value,
|
|
31
|
+
(_key, val) => {
|
|
32
|
+
if (typeof val === "bigint") return val.toString()
|
|
33
|
+
if (typeof val === "object" && val) {
|
|
34
|
+
if (seen.has(val)) return "[Circular]"
|
|
35
|
+
seen.add(val)
|
|
36
|
+
}
|
|
37
|
+
return val
|
|
38
|
+
},
|
|
39
|
+
2,
|
|
40
|
+
)
|
|
41
|
+
return json ?? String(value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatInitError(error: InitError, t: Translator): string {
|
|
45
|
+
const data = error.data
|
|
46
|
+
switch (error.name) {
|
|
47
|
+
case "MCPFailed": {
|
|
48
|
+
const name = typeof data.name === "string" ? data.name : ""
|
|
49
|
+
return t("error.chain.mcpFailed", { name })
|
|
50
|
+
}
|
|
51
|
+
case "ProviderAuthError": {
|
|
52
|
+
const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
|
|
53
|
+
const message = typeof data.message === "string" ? data.message : safeJson(data.message)
|
|
54
|
+
return t("error.chain.providerAuthFailed", { provider: providerID, message })
|
|
55
|
+
}
|
|
56
|
+
case "APIError": {
|
|
57
|
+
const message = typeof data.message === "string" ? data.message : t("error.chain.apiError")
|
|
58
|
+
const lines: string[] = [message]
|
|
59
|
+
|
|
60
|
+
if (typeof data.statusCode === "number") {
|
|
61
|
+
lines.push(t("error.chain.status", { status: data.statusCode }))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof data.isRetryable === "boolean") {
|
|
65
|
+
lines.push(t("error.chain.retryable", { retryable: data.isRetryable }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof data.responseBody === "string" && data.responseBody) {
|
|
69
|
+
lines.push(t("error.chain.responseBody", { body: data.responseBody }))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return lines.join("\n")
|
|
73
|
+
}
|
|
74
|
+
case "ProviderModelNotFoundError": {
|
|
75
|
+
const { providerID, modelID, suggestions } = data as {
|
|
76
|
+
providerID: string
|
|
77
|
+
modelID: string
|
|
78
|
+
suggestions?: string[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const suggestionsLine =
|
|
82
|
+
Array.isArray(suggestions) && suggestions.length
|
|
83
|
+
? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })]
|
|
84
|
+
: []
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
t("error.chain.modelNotFound", { provider: providerID, model: modelID }),
|
|
88
|
+
...suggestionsLine,
|
|
89
|
+
t("error.chain.checkConfig"),
|
|
90
|
+
].join("\n")
|
|
91
|
+
}
|
|
92
|
+
case "ProviderInitError": {
|
|
93
|
+
const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
|
|
94
|
+
return t("error.chain.providerInitFailed", { provider: providerID })
|
|
95
|
+
}
|
|
96
|
+
case "ConfigJsonError": {
|
|
97
|
+
const path = typeof data.path === "string" ? data.path : safeJson(data.path)
|
|
98
|
+
const message = typeof data.message === "string" ? data.message : ""
|
|
99
|
+
if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message })
|
|
100
|
+
return t("error.chain.configJsonInvalid", { path })
|
|
101
|
+
}
|
|
102
|
+
case "ConfigDirectoryTypoError": {
|
|
103
|
+
const path = typeof data.path === "string" ? data.path : safeJson(data.path)
|
|
104
|
+
const dir = typeof data.dir === "string" ? data.dir : safeJson(data.dir)
|
|
105
|
+
const suggestion = typeof data.suggestion === "string" ? data.suggestion : safeJson(data.suggestion)
|
|
106
|
+
return t("error.chain.configDirectoryTypo", { dir, path, suggestion })
|
|
107
|
+
}
|
|
108
|
+
case "ConfigFrontmatterError": {
|
|
109
|
+
const path = typeof data.path === "string" ? data.path : safeJson(data.path)
|
|
110
|
+
const message = typeof data.message === "string" ? data.message : safeJson(data.message)
|
|
111
|
+
return t("error.chain.configFrontmatterError", { path, message })
|
|
112
|
+
}
|
|
113
|
+
case "ConfigInvalidError": {
|
|
114
|
+
const issues = Array.isArray(data.issues)
|
|
115
|
+
? data.issues.map(
|
|
116
|
+
(issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."),
|
|
117
|
+
)
|
|
118
|
+
: []
|
|
119
|
+
const message = typeof data.message === "string" ? data.message : ""
|
|
120
|
+
const path = typeof data.path === "string" ? data.path : safeJson(data.path)
|
|
121
|
+
|
|
122
|
+
const line = message
|
|
123
|
+
? t("error.chain.configInvalidWithMessage", { path, message })
|
|
124
|
+
: t("error.chain.configInvalid", { path })
|
|
125
|
+
|
|
126
|
+
return [line, ...issues].join("\n")
|
|
127
|
+
}
|
|
128
|
+
case "UnknownError":
|
|
129
|
+
return typeof data.message === "string" ? data.message : safeJson(data)
|
|
130
|
+
default:
|
|
131
|
+
if (typeof data.message === "string") return data.message
|
|
132
|
+
return safeJson(data)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string {
|
|
137
|
+
if (!error) return t("error.chain.unknown")
|
|
138
|
+
|
|
139
|
+
if (isInitError(error)) {
|
|
140
|
+
const message = formatInitError(error, t)
|
|
141
|
+
if (depth > 0 && parentMessage === message) return ""
|
|
142
|
+
const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : ""
|
|
143
|
+
return indent + `${error.name}\n${message}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (error instanceof Error) {
|
|
147
|
+
const isDuplicate = depth > 0 && parentMessage === error.message
|
|
148
|
+
const parts: string[] = []
|
|
149
|
+
const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : ""
|
|
150
|
+
|
|
151
|
+
const header = `${error.name}${error.message ? `: ${error.message}` : ""}`
|
|
152
|
+
const stack = error.stack?.trim()
|
|
153
|
+
|
|
154
|
+
if (stack) {
|
|
155
|
+
const startsWithHeader = stack.startsWith(header)
|
|
156
|
+
|
|
157
|
+
if (isDuplicate && startsWithHeader) {
|
|
158
|
+
const trace = stack.split("\n").slice(1).join("\n").trim()
|
|
159
|
+
if (trace) {
|
|
160
|
+
parts.push(indent + trace)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (isDuplicate && !startsWithHeader) {
|
|
165
|
+
parts.push(indent + stack)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!isDuplicate && startsWithHeader) {
|
|
169
|
+
parts.push(indent + stack)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!isDuplicate && !startsWithHeader) {
|
|
173
|
+
parts.push(indent + `${header}\n${stack}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!stack && !isDuplicate) {
|
|
178
|
+
parts.push(indent + header)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (error.cause) {
|
|
182
|
+
const causeResult = formatErrorChain(error.cause, t, depth + 1, error.message)
|
|
183
|
+
if (causeResult) {
|
|
184
|
+
parts.push(causeResult)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return parts.join("\n\n")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (typeof error === "string") {
|
|
192
|
+
if (depth > 0 && parentMessage === error) return ""
|
|
193
|
+
const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : ""
|
|
194
|
+
return indent + error
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : ""
|
|
198
|
+
return indent + safeJson(error)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatError(error: unknown, t: Translator): string {
|
|
202
|
+
return formatErrorChain(error, t, 0)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface ErrorPageProps {
|
|
206
|
+
error: unknown
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|
210
|
+
const platform = usePlatform()
|
|
211
|
+
const language = useLanguage()
|
|
212
|
+
const [store, setStore] = createStore({
|
|
213
|
+
checking: false,
|
|
214
|
+
version: undefined as string | undefined,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
async function checkForUpdates() {
|
|
218
|
+
if (!platform.checkUpdate) return
|
|
219
|
+
setStore("checking", true)
|
|
220
|
+
const result = await platform.checkUpdate()
|
|
221
|
+
setStore("checking", false)
|
|
222
|
+
if (result.updateAvailable && result.version) setStore("version", result.version)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function installUpdate() {
|
|
226
|
+
if (!platform.update || !platform.restart) return
|
|
227
|
+
await platform.update()
|
|
228
|
+
await platform.restart()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans">
|
|
233
|
+
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
|
|
234
|
+
<Logo class="w-58.5 opacity-12 shrink-0" />
|
|
235
|
+
<div class="flex flex-col items-center gap-2 text-center">
|
|
236
|
+
<h1 class="text-lg font-medium text-text-strong">{language.t("error.page.title")}</h1>
|
|
237
|
+
<p class="text-sm text-text-weak">{language.t("error.page.description")}</p>
|
|
238
|
+
</div>
|
|
239
|
+
<TextField
|
|
240
|
+
value={formatError(props.error, language.t)}
|
|
241
|
+
readOnly
|
|
242
|
+
copyable
|
|
243
|
+
multiline
|
|
244
|
+
class="max-h-96 w-full font-mono text-xs no-scrollbar"
|
|
245
|
+
label={language.t("error.page.details.label")}
|
|
246
|
+
hideLabel
|
|
247
|
+
/>
|
|
248
|
+
<div class="flex items-center gap-3">
|
|
249
|
+
<Button size="large" onClick={platform.restart}>
|
|
250
|
+
{language.t("error.page.action.restart")}
|
|
251
|
+
</Button>
|
|
252
|
+
<Show when={platform.checkUpdate}>
|
|
253
|
+
<Show
|
|
254
|
+
when={store.version}
|
|
255
|
+
fallback={
|
|
256
|
+
<Button size="large" variant="ghost" onClick={checkForUpdates} disabled={store.checking}>
|
|
257
|
+
{store.checking
|
|
258
|
+
? language.t("error.page.action.checking")
|
|
259
|
+
: language.t("error.page.action.checkUpdates")}
|
|
260
|
+
</Button>
|
|
261
|
+
}
|
|
262
|
+
>
|
|
263
|
+
<Button size="large" onClick={installUpdate}>
|
|
264
|
+
{language.t("error.page.action.updateTo", { version: store.version ?? "" })}
|
|
265
|
+
</Button>
|
|
266
|
+
</Show>
|
|
267
|
+
</Show>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="flex flex-col items-center gap-2">
|
|
270
|
+
<div class="flex items-center justify-center gap-1">
|
|
271
|
+
{language.t("error.page.report.prefix")}
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
class="flex items-center text-text-interactive-base gap-1"
|
|
275
|
+
onClick={() => platform.openLink("https://jonsoc.com/desktop-feedback")}
|
|
276
|
+
>
|
|
277
|
+
<div>{language.t("error.page.report.discord")}</div>
|
|
278
|
+
<Icon name="discord" class="text-text-interactive-base" />
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
<Show when={platform.version}>
|
|
282
|
+
{(version) => (
|
|
283
|
+
<p class="text-xs text-text-weak">{language.t("error.page.version", { version: version() })}</p>
|
|
284
|
+
)}
|
|
285
|
+
</Show>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
|
2
|
+
import { Button } from "@jonsoc/ui/button"
|
|
3
|
+
import { Logo } from "@jonsoc/ui/logo"
|
|
4
|
+
import { useLayout } from "@/context/layout"
|
|
5
|
+
import { useNavigate } from "@solidjs/router"
|
|
6
|
+
import { base64Encode } from "@jonsoc/util/encode"
|
|
7
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
8
|
+
import { usePlatform } from "@/context/platform"
|
|
9
|
+
import { DateTime } from "luxon"
|
|
10
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
11
|
+
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
|
12
|
+
import { DialogSelectServer } from "@/components/dialog-select-server"
|
|
13
|
+
import { useServer } from "@/context/server"
|
|
14
|
+
import { useGlobalSync } from "@/context/global-sync"
|
|
15
|
+
import { useLanguage } from "@/context/language"
|
|
16
|
+
|
|
17
|
+
export default function Home() {
|
|
18
|
+
const sync = useGlobalSync()
|
|
19
|
+
const layout = useLayout()
|
|
20
|
+
const platform = usePlatform()
|
|
21
|
+
const dialog = useDialog()
|
|
22
|
+
const navigate = useNavigate()
|
|
23
|
+
const server = useServer()
|
|
24
|
+
const language = useLanguage()
|
|
25
|
+
const homedir = createMemo(() => sync.data.path.home)
|
|
26
|
+
|
|
27
|
+
function openProject(directory: string) {
|
|
28
|
+
layout.projects.open(directory)
|
|
29
|
+
server.projects.touch(directory)
|
|
30
|
+
navigate(`/${base64Encode(directory)}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function chooseProject() {
|
|
34
|
+
function resolve(result: string | string[] | null) {
|
|
35
|
+
if (Array.isArray(result)) {
|
|
36
|
+
for (const directory of result) {
|
|
37
|
+
openProject(directory)
|
|
38
|
+
}
|
|
39
|
+
} else if (result) {
|
|
40
|
+
openProject(result)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
|
45
|
+
const result = await platform.openDirectoryPickerDialog?.({
|
|
46
|
+
title: language.t("command.project.open"),
|
|
47
|
+
multiple: true,
|
|
48
|
+
})
|
|
49
|
+
resolve(result)
|
|
50
|
+
} else {
|
|
51
|
+
dialog.show(
|
|
52
|
+
() => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
|
|
53
|
+
() => resolve(null),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div class="mx-auto mt-55 w-full md:w-auto px-4">
|
|
60
|
+
<Logo class="md:w-xl opacity-12" />
|
|
61
|
+
<Button
|
|
62
|
+
size="large"
|
|
63
|
+
variant="ghost"
|
|
64
|
+
class="mt-4 mx-auto text-14-regular text-text-weak"
|
|
65
|
+
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
classList={{
|
|
69
|
+
"size-2 rounded-full": true,
|
|
70
|
+
"bg-icon-success-base": server.healthy() === true,
|
|
71
|
+
"bg-icon-critical-base": server.healthy() === false,
|
|
72
|
+
"bg-border-weak-base": server.healthy() === undefined,
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
{server.name}
|
|
76
|
+
</Button>
|
|
77
|
+
<Switch>
|
|
78
|
+
<Match when={sync.data.project.length > 0}>
|
|
79
|
+
<div class="mt-20 w-full flex flex-col gap-4">
|
|
80
|
+
<div class="flex gap-2 items-center justify-between pl-3">
|
|
81
|
+
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
|
|
82
|
+
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
|
|
83
|
+
{language.t("command.project.open")}
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
<ul class="flex flex-col gap-2">
|
|
87
|
+
<For
|
|
88
|
+
each={sync.data.project
|
|
89
|
+
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
|
90
|
+
.slice(0, 5)}
|
|
91
|
+
>
|
|
92
|
+
{(project) => (
|
|
93
|
+
<Button
|
|
94
|
+
size="large"
|
|
95
|
+
variant="ghost"
|
|
96
|
+
class="text-14-mono text-left justify-between px-3"
|
|
97
|
+
onClick={() => openProject(project.worktree)}
|
|
98
|
+
>
|
|
99
|
+
{project.worktree.replace(homedir(), "~")}
|
|
100
|
+
<div class="text-14-regular text-text-weak">
|
|
101
|
+
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
|
102
|
+
</div>
|
|
103
|
+
</Button>
|
|
104
|
+
)}
|
|
105
|
+
</For>
|
|
106
|
+
</ul>
|
|
107
|
+
</div>
|
|
108
|
+
</Match>
|
|
109
|
+
<Match when={true}>
|
|
110
|
+
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
|
111
|
+
<Icon name="folder-add-left" size="large" />
|
|
112
|
+
<div class="flex flex-col gap-1 items-center justify-center">
|
|
113
|
+
<div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
|
|
114
|
+
<div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div />
|
|
117
|
+
<Button class="px-3" onClick={chooseProject}>
|
|
118
|
+
{language.t("command.project.open")}
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</Match>
|
|
122
|
+
</Switch>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|