@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,343 @@
|
|
|
1
|
+
import { createEffect, createMemo, onCleanup, Show } from "solid-js"
|
|
2
|
+
import { createStore } from "solid-js/store"
|
|
3
|
+
import { Portal } from "solid-js/web"
|
|
4
|
+
import { useParams } from "@solidjs/router"
|
|
5
|
+
import { useLayout } from "@/context/layout"
|
|
6
|
+
import { useCommand } from "@/context/command"
|
|
7
|
+
import { useLanguage } from "@/context/language"
|
|
8
|
+
// import { useServer } from "@/context/server"
|
|
9
|
+
// import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
10
|
+
import { usePlatform } from "@/context/platform"
|
|
11
|
+
import { useSync } from "@/context/sync"
|
|
12
|
+
import { useGlobalSDK } from "@/context/global-sdk"
|
|
13
|
+
import { getFilename } from "@jonsoc/util/path"
|
|
14
|
+
import { base64Decode } from "@jonsoc/util/encode"
|
|
15
|
+
|
|
16
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
17
|
+
import { IconButton } from "@jonsoc/ui/icon-button"
|
|
18
|
+
import { Button } from "@jonsoc/ui/button"
|
|
19
|
+
import { Tooltip, TooltipKeybind } from "@jonsoc/ui/tooltip"
|
|
20
|
+
import { Popover } from "@jonsoc/ui/popover"
|
|
21
|
+
import { TextField } from "@jonsoc/ui/text-field"
|
|
22
|
+
import { Keybind } from "@jonsoc/ui/keybind"
|
|
23
|
+
|
|
24
|
+
export function SessionHeader() {
|
|
25
|
+
const globalSDK = useGlobalSDK()
|
|
26
|
+
const layout = useLayout()
|
|
27
|
+
const params = useParams()
|
|
28
|
+
const command = useCommand()
|
|
29
|
+
// const server = useServer()
|
|
30
|
+
// const dialog = useDialog()
|
|
31
|
+
const sync = useSync()
|
|
32
|
+
const platform = usePlatform()
|
|
33
|
+
const language = useLanguage()
|
|
34
|
+
|
|
35
|
+
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
|
36
|
+
const project = createMemo(() => {
|
|
37
|
+
const directory = projectDirectory()
|
|
38
|
+
if (!directory) return
|
|
39
|
+
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
|
40
|
+
})
|
|
41
|
+
const name = createMemo(() => {
|
|
42
|
+
const current = project()
|
|
43
|
+
if (current) return current.name || getFilename(current.worktree)
|
|
44
|
+
return getFilename(projectDirectory())
|
|
45
|
+
})
|
|
46
|
+
const hotkey = createMemo(() => command.keybind("file.open"))
|
|
47
|
+
|
|
48
|
+
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
|
49
|
+
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
|
50
|
+
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
|
51
|
+
const showReview = createMemo(() => !!currentSession())
|
|
52
|
+
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
|
53
|
+
const view = createMemo(() => layout.view(sessionKey))
|
|
54
|
+
|
|
55
|
+
const [state, setState] = createStore({
|
|
56
|
+
share: false,
|
|
57
|
+
unshare: false,
|
|
58
|
+
copied: false,
|
|
59
|
+
timer: undefined as number | undefined,
|
|
60
|
+
})
|
|
61
|
+
const shareUrl = createMemo(() => currentSession()?.share?.url)
|
|
62
|
+
|
|
63
|
+
createEffect(() => {
|
|
64
|
+
const url = shareUrl()
|
|
65
|
+
if (url) return
|
|
66
|
+
if (state.timer) window.clearTimeout(state.timer)
|
|
67
|
+
setState({ copied: false, timer: undefined })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
onCleanup(() => {
|
|
71
|
+
if (state.timer) window.clearTimeout(state.timer)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
function shareSession() {
|
|
75
|
+
const session = currentSession()
|
|
76
|
+
if (!session || state.share) return
|
|
77
|
+
setState("share", true)
|
|
78
|
+
globalSDK.client.session
|
|
79
|
+
.share({ sessionID: session.id, directory: projectDirectory() })
|
|
80
|
+
.catch((error) => {
|
|
81
|
+
console.error("Failed to share session", error)
|
|
82
|
+
})
|
|
83
|
+
.finally(() => {
|
|
84
|
+
setState("share", false)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function unshareSession() {
|
|
89
|
+
const session = currentSession()
|
|
90
|
+
if (!session || state.unshare) return
|
|
91
|
+
setState("unshare", true)
|
|
92
|
+
globalSDK.client.session
|
|
93
|
+
.unshare({ sessionID: session.id, directory: projectDirectory() })
|
|
94
|
+
.catch((error) => {
|
|
95
|
+
console.error("Failed to unshare session", error)
|
|
96
|
+
})
|
|
97
|
+
.finally(() => {
|
|
98
|
+
setState("unshare", false)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function copyLink() {
|
|
103
|
+
const url = shareUrl()
|
|
104
|
+
if (!url) return
|
|
105
|
+
navigator.clipboard
|
|
106
|
+
.writeText(url)
|
|
107
|
+
.then(() => {
|
|
108
|
+
if (state.timer) window.clearTimeout(state.timer)
|
|
109
|
+
setState("copied", true)
|
|
110
|
+
const timer = window.setTimeout(() => {
|
|
111
|
+
setState("copied", false)
|
|
112
|
+
setState("timer", undefined)
|
|
113
|
+
}, 3000)
|
|
114
|
+
setState("timer", timer)
|
|
115
|
+
})
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
console.error("Failed to copy share link", error)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function viewShare() {
|
|
122
|
+
const url = shareUrl()
|
|
123
|
+
if (!url) return
|
|
124
|
+
platform.openLink(url)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const centerMount = createMemo(() => document.getElementById("jonsoc-titlebar-center"))
|
|
128
|
+
const rightMount = createMemo(() => document.getElementById("jonsoc-titlebar-right"))
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
<Show when={centerMount()}>
|
|
133
|
+
{(mount) => (
|
|
134
|
+
<Portal mount={mount()}>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
|
138
|
+
onClick={() => command.trigger("file.open")}
|
|
139
|
+
aria-label={language.t("session.header.searchFiles")}
|
|
140
|
+
>
|
|
141
|
+
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
|
|
142
|
+
<Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
|
|
143
|
+
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate h-4.5 flex items-center">
|
|
144
|
+
{language.t("session.header.search.placeholder", { project: name() })}
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<Show when={hotkey()}>{(keybind) => <Keybind class="shrink-0">{keybind()}</Keybind>}</Show>
|
|
149
|
+
</button>
|
|
150
|
+
</Portal>
|
|
151
|
+
)}
|
|
152
|
+
</Show>
|
|
153
|
+
<Show when={rightMount()}>
|
|
154
|
+
{(mount) => (
|
|
155
|
+
<Portal mount={mount()}>
|
|
156
|
+
<div class="flex items-center gap-3">
|
|
157
|
+
{/* <div class="hidden md:flex items-center gap-1"> */}
|
|
158
|
+
{/* <Button */}
|
|
159
|
+
{/* size="small" */}
|
|
160
|
+
{/* variant="ghost" */}
|
|
161
|
+
{/* onClick={() => { */}
|
|
162
|
+
{/* dialog.show(() => <DialogSelectServer />) */}
|
|
163
|
+
{/* }} */}
|
|
164
|
+
{/* > */}
|
|
165
|
+
{/* <div */}
|
|
166
|
+
{/* classList={{ */}
|
|
167
|
+
{/* "size-1.5 rounded-full": true, */}
|
|
168
|
+
{/* "bg-icon-success-base": server.healthy() === true, */}
|
|
169
|
+
{/* "bg-icon-critical-base": server.healthy() === false, */}
|
|
170
|
+
{/* "bg-border-weak-base": server.healthy() === undefined, */}
|
|
171
|
+
{/* }} */}
|
|
172
|
+
{/* /> */}
|
|
173
|
+
{/* <Icon name="server" size="small" class="text-icon-weak" /> */}
|
|
174
|
+
{/* <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span> */}
|
|
175
|
+
{/* </Button> */}
|
|
176
|
+
{/* <SessionLspIndicator /> */}
|
|
177
|
+
{/* <SessionMcpIndicator /> */}
|
|
178
|
+
{/* </div> */}
|
|
179
|
+
<div class="flex items-center gap-1">
|
|
180
|
+
<div class="hidden md:block shrink-0">
|
|
181
|
+
<TooltipKeybind
|
|
182
|
+
title={language.t("command.review.toggle")}
|
|
183
|
+
keybind={command.keybind("review.toggle")}
|
|
184
|
+
>
|
|
185
|
+
<Button
|
|
186
|
+
variant="ghost"
|
|
187
|
+
class="group/review-toggle size-6 p-0"
|
|
188
|
+
onClick={() => view().reviewPanel.toggle()}
|
|
189
|
+
aria-label={language.t("command.review.toggle")}
|
|
190
|
+
aria-expanded={view().reviewPanel.opened()}
|
|
191
|
+
aria-controls="review-panel"
|
|
192
|
+
tabIndex={showReview() ? 0 : -1}
|
|
193
|
+
>
|
|
194
|
+
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
|
195
|
+
<Icon
|
|
196
|
+
size="small"
|
|
197
|
+
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
|
|
198
|
+
class="group-hover/review-toggle:hidden"
|
|
199
|
+
/>
|
|
200
|
+
<Icon
|
|
201
|
+
size="small"
|
|
202
|
+
name="layout-right-partial"
|
|
203
|
+
class="hidden group-hover/review-toggle:inline-block"
|
|
204
|
+
/>
|
|
205
|
+
<Icon
|
|
206
|
+
size="small"
|
|
207
|
+
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
|
|
208
|
+
class="hidden group-active/review-toggle:inline-block"
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
</Button>
|
|
212
|
+
</TooltipKeybind>
|
|
213
|
+
</div>
|
|
214
|
+
<TooltipKeybind
|
|
215
|
+
class="hidden md:block shrink-0"
|
|
216
|
+
title={language.t("command.terminal.toggle")}
|
|
217
|
+
keybind={command.keybind("terminal.toggle")}
|
|
218
|
+
>
|
|
219
|
+
<Button
|
|
220
|
+
variant="ghost"
|
|
221
|
+
class="group/terminal-toggle size-6 p-0"
|
|
222
|
+
onClick={() => view().terminal.toggle()}
|
|
223
|
+
aria-label={language.t("command.terminal.toggle")}
|
|
224
|
+
aria-expanded={view().terminal.opened()}
|
|
225
|
+
aria-controls="terminal-panel"
|
|
226
|
+
>
|
|
227
|
+
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
|
228
|
+
<Icon
|
|
229
|
+
size="small"
|
|
230
|
+
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
|
231
|
+
class="group-hover/terminal-toggle:hidden"
|
|
232
|
+
/>
|
|
233
|
+
<Icon
|
|
234
|
+
size="small"
|
|
235
|
+
name="layout-bottom-partial"
|
|
236
|
+
class="hidden group-hover/terminal-toggle:inline-block"
|
|
237
|
+
/>
|
|
238
|
+
<Icon
|
|
239
|
+
size="small"
|
|
240
|
+
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
|
241
|
+
class="hidden group-active/terminal-toggle:inline-block"
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
</Button>
|
|
245
|
+
</TooltipKeybind>
|
|
246
|
+
</div>
|
|
247
|
+
<Show when={showShare()}>
|
|
248
|
+
<div class="flex items-center">
|
|
249
|
+
<Popover
|
|
250
|
+
title={language.t("session.share.popover.title")}
|
|
251
|
+
description={
|
|
252
|
+
shareUrl()
|
|
253
|
+
? language.t("session.share.popover.description.shared")
|
|
254
|
+
: language.t("session.share.popover.description.unshared")
|
|
255
|
+
}
|
|
256
|
+
triggerAs={Button}
|
|
257
|
+
triggerProps={{
|
|
258
|
+
variant: "secondary",
|
|
259
|
+
classList: { "rounded-r-none": shareUrl() !== undefined },
|
|
260
|
+
style: { scale: 1 },
|
|
261
|
+
}}
|
|
262
|
+
trigger={language.t("session.share.action.share")}
|
|
263
|
+
>
|
|
264
|
+
<div class="flex flex-col gap-2">
|
|
265
|
+
<Show
|
|
266
|
+
when={shareUrl()}
|
|
267
|
+
fallback={
|
|
268
|
+
<div class="flex">
|
|
269
|
+
<Button
|
|
270
|
+
size="large"
|
|
271
|
+
variant="primary"
|
|
272
|
+
class="w-1/2"
|
|
273
|
+
onClick={shareSession}
|
|
274
|
+
disabled={state.share}
|
|
275
|
+
>
|
|
276
|
+
{state.share
|
|
277
|
+
? language.t("session.share.action.publishing")
|
|
278
|
+
: language.t("session.share.action.publish")}
|
|
279
|
+
</Button>
|
|
280
|
+
</div>
|
|
281
|
+
}
|
|
282
|
+
>
|
|
283
|
+
<div class="flex flex-col gap-2 w-72">
|
|
284
|
+
<TextField value={shareUrl() ?? ""} readOnly copyable class="w-full" />
|
|
285
|
+
<div class="grid grid-cols-2 gap-2">
|
|
286
|
+
<Button
|
|
287
|
+
size="large"
|
|
288
|
+
variant="secondary"
|
|
289
|
+
class="w-full shadow-none border border-border-weak-base"
|
|
290
|
+
onClick={unshareSession}
|
|
291
|
+
disabled={state.unshare}
|
|
292
|
+
>
|
|
293
|
+
{state.unshare
|
|
294
|
+
? language.t("session.share.action.unpublishing")
|
|
295
|
+
: language.t("session.share.action.unpublish")}
|
|
296
|
+
</Button>
|
|
297
|
+
<Button
|
|
298
|
+
size="large"
|
|
299
|
+
variant="primary"
|
|
300
|
+
class="w-full"
|
|
301
|
+
onClick={viewShare}
|
|
302
|
+
disabled={state.unshare}
|
|
303
|
+
>
|
|
304
|
+
{language.t("session.share.action.view")}
|
|
305
|
+
</Button>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</Show>
|
|
309
|
+
</div>
|
|
310
|
+
</Popover>
|
|
311
|
+
<Show when={shareUrl()} fallback={<div class="size-6" aria-hidden="true" />}>
|
|
312
|
+
<Tooltip
|
|
313
|
+
value={
|
|
314
|
+
state.copied
|
|
315
|
+
? language.t("session.share.copy.copied")
|
|
316
|
+
: language.t("session.share.copy.copyLink")
|
|
317
|
+
}
|
|
318
|
+
placement="top"
|
|
319
|
+
gutter={8}
|
|
320
|
+
>
|
|
321
|
+
<IconButton
|
|
322
|
+
icon={state.copied ? "check" : "copy"}
|
|
323
|
+
variant="secondary"
|
|
324
|
+
class="rounded-l-none"
|
|
325
|
+
onClick={copyLink}
|
|
326
|
+
disabled={state.unshare}
|
|
327
|
+
aria-label={
|
|
328
|
+
state.copied
|
|
329
|
+
? language.t("session.share.copy.copied")
|
|
330
|
+
: language.t("session.share.copy.copyLink")
|
|
331
|
+
}
|
|
332
|
+
/>
|
|
333
|
+
</Tooltip>
|
|
334
|
+
</Show>
|
|
335
|
+
</div>
|
|
336
|
+
</Show>
|
|
337
|
+
</div>
|
|
338
|
+
</Portal>
|
|
339
|
+
)}
|
|
340
|
+
</Show>
|
|
341
|
+
</>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Show, createMemo } from "solid-js"
|
|
2
|
+
import { DateTime } from "luxon"
|
|
3
|
+
import { useSync } from "@/context/sync"
|
|
4
|
+
import { useLanguage } from "@/context/language"
|
|
5
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
6
|
+
import { getDirectory, getFilename } from "@jonsoc/util/path"
|
|
7
|
+
import { Select } from "@jonsoc/ui/select"
|
|
8
|
+
|
|
9
|
+
const MAIN_WORKTREE = "main"
|
|
10
|
+
const CREATE_WORKTREE = "create"
|
|
11
|
+
|
|
12
|
+
interface NewSessionViewProps {
|
|
13
|
+
worktree: string
|
|
14
|
+
onWorktreeChange: (value: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function NewSessionView(props: NewSessionViewProps) {
|
|
18
|
+
const sync = useSync()
|
|
19
|
+
const language = useLanguage()
|
|
20
|
+
|
|
21
|
+
const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
|
|
22
|
+
const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
|
|
23
|
+
const current = createMemo(() => {
|
|
24
|
+
const selection = props.worktree
|
|
25
|
+
if (options().includes(selection)) return selection
|
|
26
|
+
return MAIN_WORKTREE
|
|
27
|
+
})
|
|
28
|
+
const projectRoot = createMemo(() => sync.project?.worktree ?? sync.data.path.directory)
|
|
29
|
+
const isWorktree = createMemo(() => {
|
|
30
|
+
const project = sync.project
|
|
31
|
+
if (!project) return false
|
|
32
|
+
return sync.data.path.directory !== project.worktree
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const label = (value: string) => {
|
|
36
|
+
if (value === MAIN_WORKTREE) {
|
|
37
|
+
if (isWorktree()) return language.t("session.new.worktree.main")
|
|
38
|
+
const branch = sync.data.vcs?.branch
|
|
39
|
+
if (branch) return language.t("session.new.worktree.mainWithBranch", { branch })
|
|
40
|
+
return language.t("session.new.worktree.main")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create")
|
|
44
|
+
|
|
45
|
+
return getFilename(value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"
|
|
51
|
+
style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }}
|
|
52
|
+
>
|
|
53
|
+
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
|
54
|
+
<div class="flex justify-center items-center gap-3">
|
|
55
|
+
<Icon name="folder" size="small" />
|
|
56
|
+
<div class="text-12-medium text-text-weak select-text">
|
|
57
|
+
{getDirectory(projectRoot())}
|
|
58
|
+
<span class="text-text-strong">{getFilename(projectRoot())}</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="flex justify-center items-center gap-1">
|
|
62
|
+
<Icon name="branch" size="small" />
|
|
63
|
+
<Select
|
|
64
|
+
options={options()}
|
|
65
|
+
current={current()}
|
|
66
|
+
value={(x) => x}
|
|
67
|
+
label={label}
|
|
68
|
+
onSelect={(value) => {
|
|
69
|
+
props.onWorktreeChange(value ?? MAIN_WORKTREE)
|
|
70
|
+
}}
|
|
71
|
+
size="normal"
|
|
72
|
+
variant="ghost"
|
|
73
|
+
class="text-12-medium"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
<Show when={sync.project}>
|
|
77
|
+
{(project) => (
|
|
78
|
+
<div class="flex justify-center items-center gap-3">
|
|
79
|
+
<Icon name="pencil-line" size="small" />
|
|
80
|
+
<div class="text-12-medium text-text-weak">
|
|
81
|
+
{language.t("session.new.lastModified")}
|
|
82
|
+
<span class="text-text-strong">
|
|
83
|
+
{DateTime.fromMillis(project().time.updated ?? project().time.created)
|
|
84
|
+
.setLocale(language.locale())
|
|
85
|
+
.toRelative()}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</Show>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createMemo, Show } from "solid-js"
|
|
2
|
+
import type { JSX } from "solid-js"
|
|
3
|
+
import { createSortable } from "@thisbeyond/solid-dnd"
|
|
4
|
+
import { FileIcon } from "@jonsoc/ui/file-icon"
|
|
5
|
+
import { IconButton } from "@jonsoc/ui/icon-button"
|
|
6
|
+
import { Tooltip } from "@jonsoc/ui/tooltip"
|
|
7
|
+
import { Tabs } from "@jonsoc/ui/tabs"
|
|
8
|
+
import { getFilename } from "@jonsoc/util/path"
|
|
9
|
+
import { useFile } from "@/context/file"
|
|
10
|
+
import { useLanguage } from "@/context/language"
|
|
11
|
+
|
|
12
|
+
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
|
13
|
+
return (
|
|
14
|
+
<div class="flex items-center gap-x-1.5">
|
|
15
|
+
<FileIcon
|
|
16
|
+
node={{ path: props.path, type: "file" }}
|
|
17
|
+
classList={{
|
|
18
|
+
"grayscale-100 group-data-[selected]/tab:grayscale-0": !props.active,
|
|
19
|
+
"grayscale-0": props.active,
|
|
20
|
+
}}
|
|
21
|
+
/>
|
|
22
|
+
<span class="text-14-medium">{getFilename(props.path)}</span>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
|
|
28
|
+
const file = useFile()
|
|
29
|
+
const language = useLanguage()
|
|
30
|
+
const sortable = createSortable(props.tab)
|
|
31
|
+
const path = createMemo(() => file.pathFromTab(props.tab))
|
|
32
|
+
return (
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
|
|
35
|
+
<div class="relative h-full">
|
|
36
|
+
<Tabs.Trigger
|
|
37
|
+
value={props.tab}
|
|
38
|
+
closeButton={
|
|
39
|
+
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
|
40
|
+
<IconButton
|
|
41
|
+
icon="close"
|
|
42
|
+
variant="ghost"
|
|
43
|
+
onClick={() => props.onTabClose(props.tab)}
|
|
44
|
+
aria-label={language.t("common.closeTab")}
|
|
45
|
+
/>
|
|
46
|
+
</Tooltip>
|
|
47
|
+
}
|
|
48
|
+
hideCloseButton
|
|
49
|
+
onMiddleClick={() => props.onTabClose(props.tab)}
|
|
50
|
+
>
|
|
51
|
+
<Show when={path()}>{(p) => <FileVisual path={p()} />}</Show>
|
|
52
|
+
</Tabs.Trigger>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { JSX } from "solid-js"
|
|
2
|
+
import { createSignal, Show } from "solid-js"
|
|
3
|
+
import { createSortable } from "@thisbeyond/solid-dnd"
|
|
4
|
+
import { IconButton } from "@jonsoc/ui/icon-button"
|
|
5
|
+
import { Tabs } from "@jonsoc/ui/tabs"
|
|
6
|
+
import { DropdownMenu } from "@jonsoc/ui/dropdown-menu"
|
|
7
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
8
|
+
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
|
9
|
+
import { useLanguage } from "@/context/language"
|
|
10
|
+
|
|
11
|
+
export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => void }): JSX.Element {
|
|
12
|
+
const terminal = useTerminal()
|
|
13
|
+
const language = useLanguage()
|
|
14
|
+
const sortable = createSortable(props.terminal.id)
|
|
15
|
+
const [editing, setEditing] = createSignal(false)
|
|
16
|
+
const [title, setTitle] = createSignal(props.terminal.title)
|
|
17
|
+
const [menuOpen, setMenuOpen] = createSignal(false)
|
|
18
|
+
const [menuPosition, setMenuPosition] = createSignal({ x: 0, y: 0 })
|
|
19
|
+
const [blurEnabled, setBlurEnabled] = createSignal(false)
|
|
20
|
+
|
|
21
|
+
const isDefaultTitle = () => {
|
|
22
|
+
const number = props.terminal.titleNumber
|
|
23
|
+
if (!Number.isFinite(number) || number <= 0) return false
|
|
24
|
+
const match = props.terminal.title.match(/^Terminal (\d+)$/)
|
|
25
|
+
if (!match) return false
|
|
26
|
+
const parsed = Number(match[1])
|
|
27
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return false
|
|
28
|
+
return parsed === number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const label = () => {
|
|
32
|
+
language.locale()
|
|
33
|
+
if (props.terminal.title && !isDefaultTitle()) return props.terminal.title
|
|
34
|
+
|
|
35
|
+
const number = props.terminal.titleNumber
|
|
36
|
+
if (Number.isFinite(number) && number > 0) return language.t("terminal.title.numbered", { number })
|
|
37
|
+
if (props.terminal.title) return props.terminal.title
|
|
38
|
+
return language.t("terminal.title")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const close = () => {
|
|
42
|
+
const count = terminal.all().length
|
|
43
|
+
terminal.close(props.terminal.id)
|
|
44
|
+
if (count === 1) {
|
|
45
|
+
props.onClose?.()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const focus = () => {
|
|
50
|
+
if (editing()) return
|
|
51
|
+
|
|
52
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
53
|
+
document.activeElement.blur()
|
|
54
|
+
}
|
|
55
|
+
const wrapper = document.getElementById(`terminal-wrapper-${props.terminal.id}`)
|
|
56
|
+
const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement
|
|
57
|
+
if (!element) return
|
|
58
|
+
|
|
59
|
+
const textarea = element.querySelector("textarea") as HTMLTextAreaElement
|
|
60
|
+
if (textarea) {
|
|
61
|
+
textarea.focus()
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
element.focus()
|
|
65
|
+
element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const edit = (e?: Event) => {
|
|
69
|
+
if (e) {
|
|
70
|
+
e.stopPropagation()
|
|
71
|
+
e.preventDefault()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setBlurEnabled(false)
|
|
75
|
+
setTitle(props.terminal.title)
|
|
76
|
+
setEditing(true)
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement
|
|
79
|
+
if (!input) return
|
|
80
|
+
input.focus()
|
|
81
|
+
input.select()
|
|
82
|
+
setTimeout(() => setBlurEnabled(true), 100)
|
|
83
|
+
}, 10)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const save = () => {
|
|
87
|
+
if (!blurEnabled()) return
|
|
88
|
+
|
|
89
|
+
const value = title().trim()
|
|
90
|
+
if (value && value !== props.terminal.title) {
|
|
91
|
+
terminal.update({ id: props.terminal.id, title: value })
|
|
92
|
+
}
|
|
93
|
+
setEditing(false)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const keydown = (e: KeyboardEvent) => {
|
|
97
|
+
if (e.key === "Enter") {
|
|
98
|
+
e.preventDefault()
|
|
99
|
+
save()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (e.key === "Escape") {
|
|
103
|
+
e.preventDefault()
|
|
104
|
+
setEditing(false)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const menu = (e: MouseEvent) => {
|
|
109
|
+
e.preventDefault()
|
|
110
|
+
setMenuPosition({ x: e.clientX, y: e.clientY })
|
|
111
|
+
setMenuOpen(true)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
use:sortable
|
|
118
|
+
class="outline-none focus:outline-none focus-visible:outline-none"
|
|
119
|
+
classList={{
|
|
120
|
+
"h-full": true,
|
|
121
|
+
"opacity-0": sortable.isActiveDraggable,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<div class="relative h-full">
|
|
125
|
+
<Tabs.Trigger
|
|
126
|
+
value={props.terminal.id}
|
|
127
|
+
onClick={focus}
|
|
128
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
129
|
+
onContextMenu={menu}
|
|
130
|
+
class="!shadow-none"
|
|
131
|
+
classes={{
|
|
132
|
+
button: "border-0 outline-none focus:outline-none focus-visible:outline-none !shadow-none !ring-0",
|
|
133
|
+
}}
|
|
134
|
+
closeButton={
|
|
135
|
+
<IconButton
|
|
136
|
+
icon="close"
|
|
137
|
+
variant="ghost"
|
|
138
|
+
onClick={(e) => {
|
|
139
|
+
e.stopPropagation()
|
|
140
|
+
close()
|
|
141
|
+
}}
|
|
142
|
+
aria-label={language.t("terminal.close")}
|
|
143
|
+
/>
|
|
144
|
+
}
|
|
145
|
+
>
|
|
146
|
+
<span onDblClick={edit} style={{ visibility: editing() ? "hidden" : "visible" }}>
|
|
147
|
+
{label()}
|
|
148
|
+
</span>
|
|
149
|
+
</Tabs.Trigger>
|
|
150
|
+
<Show when={editing()}>
|
|
151
|
+
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
|
|
152
|
+
<input
|
|
153
|
+
id={`terminal-title-input-${props.terminal.id}`}
|
|
154
|
+
type="text"
|
|
155
|
+
value={title()}
|
|
156
|
+
onInput={(e) => setTitle(e.currentTarget.value)}
|
|
157
|
+
onBlur={save}
|
|
158
|
+
onKeyDown={keydown}
|
|
159
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
160
|
+
class="bg-transparent border-none outline-none text-sm min-w-0 flex-1"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
</Show>
|
|
164
|
+
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
|
|
165
|
+
<DropdownMenu.Portal>
|
|
166
|
+
<DropdownMenu.Content
|
|
167
|
+
style={{
|
|
168
|
+
position: "fixed",
|
|
169
|
+
left: `${menuPosition().x}px`,
|
|
170
|
+
top: `${menuPosition().y}px`,
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<DropdownMenu.Item onSelect={edit}>
|
|
174
|
+
<Icon name="edit" class="w-4 h-4 mr-2" />
|
|
175
|
+
{language.t("common.rename")}
|
|
176
|
+
</DropdownMenu.Item>
|
|
177
|
+
<DropdownMenu.Item onSelect={close}>
|
|
178
|
+
<Icon name="close" class="w-4 h-4 mr-2" />
|
|
179
|
+
{language.t("common.close")}
|
|
180
|
+
</DropdownMenu.Item>
|
|
181
|
+
</DropdownMenu.Content>
|
|
182
|
+
</DropdownMenu.Portal>
|
|
183
|
+
</DropdownMenu>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|