@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,2599 @@
|
|
|
1
|
+
import {
|
|
2
|
+
batch,
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
createSignal,
|
|
6
|
+
For,
|
|
7
|
+
Match,
|
|
8
|
+
on,
|
|
9
|
+
onCleanup,
|
|
10
|
+
onMount,
|
|
11
|
+
ParentProps,
|
|
12
|
+
Show,
|
|
13
|
+
Switch,
|
|
14
|
+
untrack,
|
|
15
|
+
type Accessor,
|
|
16
|
+
type JSX,
|
|
17
|
+
} from "solid-js"
|
|
18
|
+
import { A, useNavigate, useParams } from "@solidjs/router"
|
|
19
|
+
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
|
|
20
|
+
import { useGlobalSync } from "@/context/global-sync"
|
|
21
|
+
import { Persist, persisted } from "@/utils/persist"
|
|
22
|
+
import { base64Decode, base64Encode } from "@jonsoc/util/encode"
|
|
23
|
+
import { Avatar } from "@jonsoc/ui/avatar"
|
|
24
|
+
import { ResizeHandle } from "@jonsoc/ui/resize-handle"
|
|
25
|
+
import { Button } from "@jonsoc/ui/button"
|
|
26
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
27
|
+
import { IconButton } from "@jonsoc/ui/icon-button"
|
|
28
|
+
import { InlineInput } from "@jonsoc/ui/inline-input"
|
|
29
|
+
import { Tooltip, TooltipKeybind } from "@jonsoc/ui/tooltip"
|
|
30
|
+
import { HoverCard } from "@jonsoc/ui/hover-card"
|
|
31
|
+
import { MessageNav } from "@jonsoc/ui/message-nav"
|
|
32
|
+
import { DropdownMenu } from "@jonsoc/ui/dropdown-menu"
|
|
33
|
+
import { Collapsible } from "@jonsoc/ui/collapsible"
|
|
34
|
+
import { DiffChanges } from "@jonsoc/ui/diff-changes"
|
|
35
|
+
import { Spinner } from "@jonsoc/ui/spinner"
|
|
36
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
37
|
+
import { getFilename } from "@jonsoc/util/path"
|
|
38
|
+
import { Session, type Message, type TextPart } from "@jonsoc/sdk/v2/client"
|
|
39
|
+
import { usePlatform } from "@/context/platform"
|
|
40
|
+
import { useSettings } from "@/context/settings"
|
|
41
|
+
import { createStore, produce, reconcile } from "solid-js/store"
|
|
42
|
+
import {
|
|
43
|
+
DragDropProvider,
|
|
44
|
+
DragDropSensors,
|
|
45
|
+
DragOverlay,
|
|
46
|
+
SortableProvider,
|
|
47
|
+
closestCenter,
|
|
48
|
+
createSortable,
|
|
49
|
+
} from "@thisbeyond/solid-dnd"
|
|
50
|
+
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
|
51
|
+
import { useProviders } from "@/hooks/use-providers"
|
|
52
|
+
import { showToast, Toast, toaster } from "@jonsoc/ui/toast"
|
|
53
|
+
import { useGlobalSDK } from "@/context/global-sdk"
|
|
54
|
+
import { useNotification } from "@/context/notification"
|
|
55
|
+
import { usePermission } from "@/context/permission"
|
|
56
|
+
import { Binary } from "@jonsoc/util/binary"
|
|
57
|
+
import { retry } from "@jonsoc/util/retry"
|
|
58
|
+
import { playSound, soundSrc } from "@/utils/sound"
|
|
59
|
+
import { Worktree as WorktreeState } from "@/utils/worktree"
|
|
60
|
+
|
|
61
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
62
|
+
import { useTheme, type ColorScheme } from "@jonsoc/ui/theme"
|
|
63
|
+
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
|
64
|
+
import { DialogSelectServer } from "@/components/dialog-select-server"
|
|
65
|
+
import { DialogSettings } from "@/components/dialog-settings"
|
|
66
|
+
import { useCommand, type CommandOption } from "@/context/command"
|
|
67
|
+
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
|
68
|
+
import { navStart } from "@/utils/perf"
|
|
69
|
+
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
|
70
|
+
import { DialogEditProject } from "@/components/dialog-edit-project"
|
|
71
|
+
import { Titlebar } from "@/components/titlebar"
|
|
72
|
+
import { useServer } from "@/context/server"
|
|
73
|
+
import { useLanguage, type Locale } from "@/context/language"
|
|
74
|
+
|
|
75
|
+
export default function Layout(props: ParentProps) {
|
|
76
|
+
const [store, setStore, , ready] = persisted(
|
|
77
|
+
Persist.global("layout.page", ["layout.page.v1"]),
|
|
78
|
+
createStore({
|
|
79
|
+
lastSession: {} as { [directory: string]: string },
|
|
80
|
+
activeProject: undefined as string | undefined,
|
|
81
|
+
activeWorkspace: undefined as string | undefined,
|
|
82
|
+
workspaceOrder: {} as Record<string, string[]>,
|
|
83
|
+
workspaceName: {} as Record<string, string>,
|
|
84
|
+
workspaceBranchName: {} as Record<string, Record<string, string>>,
|
|
85
|
+
workspaceExpanded: {} as Record<string, boolean>,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const pageReady = createMemo(() => ready())
|
|
90
|
+
|
|
91
|
+
let scrollContainerRef: HTMLDivElement | undefined
|
|
92
|
+
const xlQuery = window.matchMedia("(min-width: 1280px)")
|
|
93
|
+
const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches)
|
|
94
|
+
const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches)
|
|
95
|
+
xlQuery.addEventListener("change", handleViewportChange)
|
|
96
|
+
onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange))
|
|
97
|
+
|
|
98
|
+
const params = useParams()
|
|
99
|
+
const [autoselect, setAutoselect] = createSignal(!params.dir)
|
|
100
|
+
const globalSDK = useGlobalSDK()
|
|
101
|
+
const globalSync = useGlobalSync()
|
|
102
|
+
const layout = useLayout()
|
|
103
|
+
const layoutReady = createMemo(() => layout.ready())
|
|
104
|
+
const platform = usePlatform()
|
|
105
|
+
const settings = useSettings()
|
|
106
|
+
const server = useServer()
|
|
107
|
+
const notification = useNotification()
|
|
108
|
+
const permission = usePermission()
|
|
109
|
+
const navigate = useNavigate()
|
|
110
|
+
const providers = useProviders()
|
|
111
|
+
const dialog = useDialog()
|
|
112
|
+
const command = useCommand()
|
|
113
|
+
const theme = useTheme()
|
|
114
|
+
const language = useLanguage()
|
|
115
|
+
const initialDir = params.dir
|
|
116
|
+
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
|
117
|
+
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
|
118
|
+
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
|
|
119
|
+
system: "theme.scheme.system",
|
|
120
|
+
light: "theme.scheme.light",
|
|
121
|
+
dark: "theme.scheme.dark",
|
|
122
|
+
}
|
|
123
|
+
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
|
124
|
+
|
|
125
|
+
const [editor, setEditor] = createStore({
|
|
126
|
+
active: "" as string,
|
|
127
|
+
value: "",
|
|
128
|
+
})
|
|
129
|
+
const [busyWorkspaces, setBusyWorkspaces] = createSignal<Set<string>>(new Set())
|
|
130
|
+
const setBusy = (directory: string, value: boolean) => {
|
|
131
|
+
const key = workspaceKey(directory)
|
|
132
|
+
setBusyWorkspaces((prev) => {
|
|
133
|
+
const next = new Set(prev)
|
|
134
|
+
if (value) next.add(key)
|
|
135
|
+
else next.delete(key)
|
|
136
|
+
return next
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
const isBusy = (directory: string) => busyWorkspaces().has(workspaceKey(directory))
|
|
140
|
+
const editorRef = { current: undefined as HTMLInputElement | undefined }
|
|
141
|
+
|
|
142
|
+
const [hoverSession, setHoverSession] = createSignal<string | undefined>()
|
|
143
|
+
|
|
144
|
+
const autoselecting = createMemo(() => {
|
|
145
|
+
if (params.dir) return false
|
|
146
|
+
if (initialDir) return false
|
|
147
|
+
if (!autoselect()) return false
|
|
148
|
+
if (!pageReady()) return true
|
|
149
|
+
if (!layoutReady()) return true
|
|
150
|
+
const list = layout.projects.list()
|
|
151
|
+
if (list.length === 0) return false
|
|
152
|
+
return true
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const editorOpen = (id: string) => editor.active === id
|
|
156
|
+
const editorValue = () => editor.value
|
|
157
|
+
|
|
158
|
+
const openEditor = (id: string, value: string) => {
|
|
159
|
+
if (!id) return
|
|
160
|
+
setEditor({ active: id, value })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const closeEditor = () => setEditor({ active: "", value: "" })
|
|
164
|
+
|
|
165
|
+
const saveEditor = (callback: (next: string) => void) => {
|
|
166
|
+
const next = editor.value.trim()
|
|
167
|
+
if (!next) {
|
|
168
|
+
closeEditor()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
closeEditor()
|
|
172
|
+
callback(next)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const editorKeyDown = (event: KeyboardEvent, callback: (next: string) => void) => {
|
|
176
|
+
if (event.key === "Enter") {
|
|
177
|
+
event.preventDefault()
|
|
178
|
+
saveEditor(callback)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (event.key === "Escape") {
|
|
182
|
+
event.preventDefault()
|
|
183
|
+
closeEditor()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const InlineEditor = (props: {
|
|
188
|
+
id: string
|
|
189
|
+
value: Accessor<string>
|
|
190
|
+
onSave: (next: string) => void
|
|
191
|
+
class?: string
|
|
192
|
+
displayClass?: string
|
|
193
|
+
editing?: boolean
|
|
194
|
+
stopPropagation?: boolean
|
|
195
|
+
openOnDblClick?: boolean
|
|
196
|
+
}) => {
|
|
197
|
+
const isEditing = () => props.editing ?? editorOpen(props.id)
|
|
198
|
+
const stopEvents = () => props.stopPropagation ?? false
|
|
199
|
+
const allowDblClick = () => props.openOnDblClick ?? true
|
|
200
|
+
const stopPropagation = (event: Event) => {
|
|
201
|
+
if (!stopEvents()) return
|
|
202
|
+
event.stopPropagation()
|
|
203
|
+
}
|
|
204
|
+
const handleDblClick = (event: MouseEvent) => {
|
|
205
|
+
if (!allowDblClick()) return
|
|
206
|
+
stopPropagation(event)
|
|
207
|
+
openEditor(props.id, props.value())
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Show
|
|
212
|
+
when={isEditing()}
|
|
213
|
+
fallback={
|
|
214
|
+
<span
|
|
215
|
+
class={props.displayClass ?? props.class}
|
|
216
|
+
onDblClick={handleDblClick}
|
|
217
|
+
onPointerDown={stopPropagation}
|
|
218
|
+
onMouseDown={stopPropagation}
|
|
219
|
+
onClick={stopPropagation}
|
|
220
|
+
onTouchStart={stopPropagation}
|
|
221
|
+
>
|
|
222
|
+
{props.value()}
|
|
223
|
+
</span>
|
|
224
|
+
}
|
|
225
|
+
>
|
|
226
|
+
<InlineInput
|
|
227
|
+
ref={(el) => {
|
|
228
|
+
editorRef.current = el
|
|
229
|
+
requestAnimationFrame(() => el.focus())
|
|
230
|
+
}}
|
|
231
|
+
value={editorValue()}
|
|
232
|
+
class={props.class}
|
|
233
|
+
onInput={(event) => setEditor("value", event.currentTarget.value)}
|
|
234
|
+
onKeyDown={(event) => {
|
|
235
|
+
event.stopPropagation()
|
|
236
|
+
editorKeyDown(event, props.onSave)
|
|
237
|
+
}}
|
|
238
|
+
onBlur={() => closeEditor()}
|
|
239
|
+
onPointerDown={stopPropagation}
|
|
240
|
+
onClick={stopPropagation}
|
|
241
|
+
onDblClick={stopPropagation}
|
|
242
|
+
onMouseDown={stopPropagation}
|
|
243
|
+
onMouseUp={stopPropagation}
|
|
244
|
+
onTouchStart={stopPropagation}
|
|
245
|
+
/>
|
|
246
|
+
</Show>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function cycleTheme(direction = 1) {
|
|
251
|
+
const ids = availableThemeEntries().map(([id]) => id)
|
|
252
|
+
if (ids.length === 0) return
|
|
253
|
+
const currentIndex = ids.indexOf(theme.themeId())
|
|
254
|
+
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
|
|
255
|
+
const nextThemeId = ids[nextIndex]
|
|
256
|
+
theme.setTheme(nextThemeId)
|
|
257
|
+
const nextTheme = theme.themes()[nextThemeId]
|
|
258
|
+
showToast({
|
|
259
|
+
title: language.t("toast.theme.title"),
|
|
260
|
+
description: nextTheme?.name ?? nextThemeId,
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function cycleColorScheme(direction = 1) {
|
|
265
|
+
const current = theme.colorScheme()
|
|
266
|
+
const currentIndex = colorSchemeOrder.indexOf(current)
|
|
267
|
+
const nextIndex =
|
|
268
|
+
currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length
|
|
269
|
+
const next = colorSchemeOrder[nextIndex]
|
|
270
|
+
theme.setColorScheme(next)
|
|
271
|
+
showToast({
|
|
272
|
+
title: language.t("toast.scheme.title"),
|
|
273
|
+
description: colorSchemeLabel(next),
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function setLocale(next: Locale) {
|
|
278
|
+
if (next === language.locale()) return
|
|
279
|
+
language.setLocale(next)
|
|
280
|
+
showToast({
|
|
281
|
+
title: language.t("toast.language.title"),
|
|
282
|
+
description: language.t("toast.language.description", { language: language.label(next) }),
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function cycleLanguage(direction = 1) {
|
|
287
|
+
const locales = language.locales
|
|
288
|
+
const currentIndex = locales.indexOf(language.locale())
|
|
289
|
+
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + locales.length) % locales.length
|
|
290
|
+
const next = locales[nextIndex]
|
|
291
|
+
if (!next) return
|
|
292
|
+
setLocale(next)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
onMount(() => {
|
|
296
|
+
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
|
297
|
+
|
|
298
|
+
let toastId: number | undefined
|
|
299
|
+
|
|
300
|
+
async function pollUpdate() {
|
|
301
|
+
const { updateAvailable, version } = await platform.checkUpdate!()
|
|
302
|
+
if (updateAvailable && toastId === undefined) {
|
|
303
|
+
toastId = showToast({
|
|
304
|
+
persistent: true,
|
|
305
|
+
icon: "download",
|
|
306
|
+
title: language.t("toast.update.title"),
|
|
307
|
+
description: language.t("toast.update.description", { version: version ?? "" }),
|
|
308
|
+
actions: [
|
|
309
|
+
{
|
|
310
|
+
label: language.t("toast.update.action.installRestart"),
|
|
311
|
+
onClick: async () => {
|
|
312
|
+
await platform.update!()
|
|
313
|
+
await platform.restart!()
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
label: language.t("toast.update.action.notYet"),
|
|
318
|
+
onClick: "dismiss",
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
pollUpdate()
|
|
326
|
+
const interval = setInterval(pollUpdate, 10 * 60 * 1000)
|
|
327
|
+
onCleanup(() => clearInterval(interval))
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
onMount(() => {
|
|
331
|
+
const toastBySession = new Map<string, number>()
|
|
332
|
+
const alertedAtBySession = new Map<string, number>()
|
|
333
|
+
const cooldownMs = 5000
|
|
334
|
+
|
|
335
|
+
const unsub = globalSDK.event.listen((e) => {
|
|
336
|
+
if (e.details?.type === "worktree.ready") {
|
|
337
|
+
setBusy(e.name, false)
|
|
338
|
+
WorktreeState.ready(e.name)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (e.details?.type === "worktree.failed") {
|
|
343
|
+
setBusy(e.name, false)
|
|
344
|
+
WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed"))
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
|
|
349
|
+
const title =
|
|
350
|
+
e.details.type === "permission.asked"
|
|
351
|
+
? language.t("notification.permission.title")
|
|
352
|
+
: language.t("notification.question.title")
|
|
353
|
+
const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const)
|
|
354
|
+
const directory = e.name
|
|
355
|
+
const props = e.details.properties
|
|
356
|
+
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
|
|
357
|
+
|
|
358
|
+
const [store] = globalSync.child(directory)
|
|
359
|
+
const session = store.session.find((s) => s.id === props.sessionID)
|
|
360
|
+
const sessionKey = `${directory}:${props.sessionID}`
|
|
361
|
+
|
|
362
|
+
const sessionTitle = session?.title ?? language.t("command.session.new")
|
|
363
|
+
const projectName = getFilename(directory)
|
|
364
|
+
const description =
|
|
365
|
+
e.details.type === "permission.asked"
|
|
366
|
+
? language.t("notification.permission.description", { sessionTitle, projectName })
|
|
367
|
+
: language.t("notification.question.description", { sessionTitle, projectName })
|
|
368
|
+
const href = `/${base64Encode(directory)}/session/${props.sessionID}`
|
|
369
|
+
|
|
370
|
+
const now = Date.now()
|
|
371
|
+
const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0
|
|
372
|
+
if (now - lastAlerted < cooldownMs) return
|
|
373
|
+
alertedAtBySession.set(sessionKey, now)
|
|
374
|
+
|
|
375
|
+
if (e.details.type === "permission.asked") {
|
|
376
|
+
playSound(soundSrc(settings.sounds.permissions()))
|
|
377
|
+
if (settings.notifications.permissions()) {
|
|
378
|
+
void platform.notify(title, description, href)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (e.details.type === "question.asked") {
|
|
383
|
+
if (settings.notifications.agent()) {
|
|
384
|
+
void platform.notify(title, description, href)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
|
389
|
+
const currentSession = params.id
|
|
390
|
+
if (directory === currentDir && props.sessionID === currentSession) return
|
|
391
|
+
if (directory === currentDir && session?.parentID === currentSession) return
|
|
392
|
+
|
|
393
|
+
const existingToastId = toastBySession.get(sessionKey)
|
|
394
|
+
if (existingToastId !== undefined) toaster.dismiss(existingToastId)
|
|
395
|
+
|
|
396
|
+
const toastId = showToast({
|
|
397
|
+
persistent: true,
|
|
398
|
+
icon,
|
|
399
|
+
title,
|
|
400
|
+
description,
|
|
401
|
+
actions: [
|
|
402
|
+
{
|
|
403
|
+
label: language.t("notification.action.goToSession"),
|
|
404
|
+
onClick: () => navigate(href),
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
label: language.t("common.dismiss"),
|
|
408
|
+
onClick: "dismiss",
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
})
|
|
412
|
+
toastBySession.set(sessionKey, toastId)
|
|
413
|
+
})
|
|
414
|
+
onCleanup(unsub)
|
|
415
|
+
|
|
416
|
+
createEffect(() => {
|
|
417
|
+
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
|
418
|
+
const currentSession = params.id
|
|
419
|
+
if (!currentDir || !currentSession) return
|
|
420
|
+
const sessionKey = `${currentDir}:${currentSession}`
|
|
421
|
+
const toastId = toastBySession.get(sessionKey)
|
|
422
|
+
if (toastId !== undefined) {
|
|
423
|
+
toaster.dismiss(toastId)
|
|
424
|
+
toastBySession.delete(sessionKey)
|
|
425
|
+
alertedAtBySession.delete(sessionKey)
|
|
426
|
+
}
|
|
427
|
+
const [store] = globalSync.child(currentDir)
|
|
428
|
+
const childSessions = store.session.filter((s) => s.parentID === currentSession)
|
|
429
|
+
for (const child of childSessions) {
|
|
430
|
+
const childKey = `${currentDir}:${child.id}`
|
|
431
|
+
const childToastId = toastBySession.get(childKey)
|
|
432
|
+
if (childToastId !== undefined) {
|
|
433
|
+
toaster.dismiss(childToastId)
|
|
434
|
+
toastBySession.delete(childKey)
|
|
435
|
+
alertedAtBySession.delete(childKey)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
function sortSessions(a: Session, b: Session) {
|
|
442
|
+
const now = Date.now()
|
|
443
|
+
const oneMinuteAgo = now - 60 * 1000
|
|
444
|
+
const aUpdated = a.time.updated ?? a.time.created
|
|
445
|
+
const bUpdated = b.time.updated ?? b.time.created
|
|
446
|
+
const aRecent = aUpdated > oneMinuteAgo
|
|
447
|
+
const bRecent = bUpdated > oneMinuteAgo
|
|
448
|
+
if (aRecent && bRecent) return a.id.localeCompare(b.id)
|
|
449
|
+
if (aRecent && !bRecent) return -1
|
|
450
|
+
if (!aRecent && bRecent) return 1
|
|
451
|
+
return bUpdated - aUpdated
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const [scrollSessionKey, setScrollSessionKey] = createSignal<string | undefined>(undefined)
|
|
455
|
+
|
|
456
|
+
function scrollToSession(sessionId: string, sessionKey: string) {
|
|
457
|
+
if (!scrollContainerRef) return
|
|
458
|
+
if (scrollSessionKey() === sessionKey) return
|
|
459
|
+
const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`)
|
|
460
|
+
if (!element) return
|
|
461
|
+
const containerRect = scrollContainerRef.getBoundingClientRect()
|
|
462
|
+
const elementRect = element.getBoundingClientRect()
|
|
463
|
+
if (elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom) {
|
|
464
|
+
setScrollSessionKey(sessionKey)
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
setScrollSessionKey(sessionKey)
|
|
468
|
+
element.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const currentProject = createMemo(() => {
|
|
472
|
+
const directory = params.dir ? base64Decode(params.dir) : undefined
|
|
473
|
+
if (!directory) return
|
|
474
|
+
|
|
475
|
+
const projects = layout.projects.list()
|
|
476
|
+
|
|
477
|
+
const sandbox = projects.find((p) => p.sandboxes?.includes(directory))
|
|
478
|
+
if (sandbox) return sandbox
|
|
479
|
+
|
|
480
|
+
const direct = projects.find((p) => p.worktree === directory)
|
|
481
|
+
if (direct) return direct
|
|
482
|
+
|
|
483
|
+
const [child] = globalSync.child(directory)
|
|
484
|
+
const id = child.project
|
|
485
|
+
if (!id) return
|
|
486
|
+
|
|
487
|
+
const meta = globalSync.data.project.find((p) => p.id === id)
|
|
488
|
+
const root = meta?.worktree
|
|
489
|
+
if (!root) return
|
|
490
|
+
|
|
491
|
+
return projects.find((p) => p.worktree === root)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
createEffect(
|
|
495
|
+
on(
|
|
496
|
+
() => ({ ready: pageReady(), project: currentProject() }),
|
|
497
|
+
(value) => {
|
|
498
|
+
if (!value.ready) return
|
|
499
|
+
const project = value.project
|
|
500
|
+
if (!project) return
|
|
501
|
+
const last = server.projects.last()
|
|
502
|
+
if (last === project.worktree) return
|
|
503
|
+
server.projects.touch(project.worktree)
|
|
504
|
+
},
|
|
505
|
+
{ defer: true },
|
|
506
|
+
),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
createEffect(
|
|
510
|
+
on(
|
|
511
|
+
() => ({ ready: pageReady(), layoutReady: layoutReady(), dir: params.dir, list: layout.projects.list() }),
|
|
512
|
+
(value) => {
|
|
513
|
+
if (!value.ready) return
|
|
514
|
+
if (!value.layoutReady) return
|
|
515
|
+
if (!autoselect()) return
|
|
516
|
+
if (initialDir) return
|
|
517
|
+
if (value.dir) return
|
|
518
|
+
if (value.list.length === 0) return
|
|
519
|
+
|
|
520
|
+
const last = server.projects.last()
|
|
521
|
+
const next = value.list.find((project) => project.worktree === last) ?? value.list[0]
|
|
522
|
+
if (!next) return
|
|
523
|
+
setAutoselect(false)
|
|
524
|
+
openProject(next.worktree, false)
|
|
525
|
+
navigateToProject(next.worktree)
|
|
526
|
+
},
|
|
527
|
+
{ defer: true },
|
|
528
|
+
),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
const workspaceKey = (directory: string) => directory.replace(/[\\/]+$/, "")
|
|
532
|
+
|
|
533
|
+
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
|
|
534
|
+
const key = workspaceKey(directory)
|
|
535
|
+
const direct = store.workspaceName[key] ?? store.workspaceName[directory]
|
|
536
|
+
if (direct) return direct
|
|
537
|
+
if (!projectId) return
|
|
538
|
+
if (!branch) return
|
|
539
|
+
return store.workspaceBranchName[projectId]?.[branch]
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
|
|
543
|
+
const key = workspaceKey(directory)
|
|
544
|
+
setStore("workspaceName", (prev) => ({ ...(prev ?? {}), [key]: next }))
|
|
545
|
+
if (!projectId) return
|
|
546
|
+
if (!branch) return
|
|
547
|
+
setStore("workspaceBranchName", projectId, (prev) => ({ ...(prev ?? {}), [branch]: next }))
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const workspaceLabel = (directory: string, branch?: string, projectId?: string) =>
|
|
551
|
+
workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
|
|
552
|
+
|
|
553
|
+
const isWorkspaceEditing = () => editor.active.startsWith("workspace:")
|
|
554
|
+
|
|
555
|
+
const workspaceSetting = createMemo(() => {
|
|
556
|
+
const project = currentProject()
|
|
557
|
+
if (!project) return false
|
|
558
|
+
return layout.sidebar.workspaces(project.worktree)()
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
createEffect(() => {
|
|
562
|
+
if (!pageReady()) return
|
|
563
|
+
if (!layoutReady()) return
|
|
564
|
+
const project = currentProject()
|
|
565
|
+
if (!project) return
|
|
566
|
+
|
|
567
|
+
const local = project.worktree
|
|
568
|
+
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
|
569
|
+
const existing = store.workspaceOrder[project.worktree]
|
|
570
|
+
if (!existing) {
|
|
571
|
+
setStore("workspaceOrder", project.worktree, dirs)
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const keep = existing.filter((d) => d !== local && dirs.includes(d))
|
|
576
|
+
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
|
|
577
|
+
const merged = [local, ...missing, ...keep]
|
|
578
|
+
|
|
579
|
+
if (merged.length !== existing.length) {
|
|
580
|
+
setStore("workspaceOrder", project.worktree, merged)
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (merged.some((d, i) => d !== existing[i])) {
|
|
585
|
+
setStore("workspaceOrder", project.worktree, merged)
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
createEffect(() => {
|
|
590
|
+
if (!pageReady()) return
|
|
591
|
+
if (!layoutReady()) return
|
|
592
|
+
const projects = layout.projects.list()
|
|
593
|
+
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
|
594
|
+
if (!expanded) continue
|
|
595
|
+
const project = projects.find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
|
596
|
+
if (!project) continue
|
|
597
|
+
if (layout.sidebar.workspaces(project.worktree)()) continue
|
|
598
|
+
setStore("workspaceExpanded", directory, false)
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
const currentSessions = createMemo(() => {
|
|
603
|
+
const project = currentProject()
|
|
604
|
+
if (!project) return [] as Session[]
|
|
605
|
+
if (workspaceSetting()) {
|
|
606
|
+
const dirs = workspaceIds(project)
|
|
607
|
+
const activeDir = params.dir ? base64Decode(params.dir) : ""
|
|
608
|
+
const result: Session[] = []
|
|
609
|
+
for (const dir of dirs) {
|
|
610
|
+
const expanded = store.workspaceExpanded[dir] ?? dir === project.worktree
|
|
611
|
+
const active = dir === activeDir
|
|
612
|
+
if (!expanded && !active) continue
|
|
613
|
+
const [dirStore] = globalSync.child(dir, { bootstrap: true })
|
|
614
|
+
const dirSessions = dirStore.session
|
|
615
|
+
.filter((session) => session.directory === dirStore.path.directory)
|
|
616
|
+
.filter((session) => !session.parentID && !session.time?.archived)
|
|
617
|
+
.toSorted(sortSessions)
|
|
618
|
+
result.push(...dirSessions)
|
|
619
|
+
}
|
|
620
|
+
return result
|
|
621
|
+
}
|
|
622
|
+
const [projectStore] = globalSync.child(project.worktree)
|
|
623
|
+
return projectStore.session
|
|
624
|
+
.filter((session) => session.directory === projectStore.path.directory)
|
|
625
|
+
.filter((session) => !session.parentID && !session.time?.archived)
|
|
626
|
+
.toSorted(sortSessions)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
type PrefetchQueue = {
|
|
630
|
+
inflight: Set<string>
|
|
631
|
+
pending: string[]
|
|
632
|
+
pendingSet: Set<string>
|
|
633
|
+
running: number
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const prefetchChunk = 600
|
|
637
|
+
const prefetchConcurrency = 1
|
|
638
|
+
const prefetchPendingLimit = 6
|
|
639
|
+
const prefetchToken = { value: 0 }
|
|
640
|
+
const prefetchQueues = new Map<string, PrefetchQueue>()
|
|
641
|
+
|
|
642
|
+
createEffect(() => {
|
|
643
|
+
params.dir
|
|
644
|
+
globalSDK.url
|
|
645
|
+
|
|
646
|
+
prefetchToken.value += 1
|
|
647
|
+
for (const q of prefetchQueues.values()) {
|
|
648
|
+
q.pending.length = 0
|
|
649
|
+
q.pendingSet.clear()
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
const queueFor = (directory: string) => {
|
|
654
|
+
const existing = prefetchQueues.get(directory)
|
|
655
|
+
if (existing) return existing
|
|
656
|
+
|
|
657
|
+
const created: PrefetchQueue = {
|
|
658
|
+
inflight: new Set(),
|
|
659
|
+
pending: [],
|
|
660
|
+
pendingSet: new Set(),
|
|
661
|
+
running: 0,
|
|
662
|
+
}
|
|
663
|
+
prefetchQueues.set(directory, created)
|
|
664
|
+
return created
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
|
668
|
+
const [, setStore] = globalSync.child(directory)
|
|
669
|
+
|
|
670
|
+
return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
|
671
|
+
.then((messages) => {
|
|
672
|
+
if (prefetchToken.value !== token) return
|
|
673
|
+
|
|
674
|
+
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
|
675
|
+
const next = items
|
|
676
|
+
.map((x) => x.info)
|
|
677
|
+
.filter((m) => !!m?.id)
|
|
678
|
+
.slice()
|
|
679
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
680
|
+
|
|
681
|
+
batch(() => {
|
|
682
|
+
setStore("message", sessionID, reconcile(next, { key: "id" }))
|
|
683
|
+
|
|
684
|
+
for (const message of items) {
|
|
685
|
+
setStore(
|
|
686
|
+
"part",
|
|
687
|
+
message.info.id,
|
|
688
|
+
reconcile(
|
|
689
|
+
message.parts
|
|
690
|
+
.filter((p) => !!p?.id)
|
|
691
|
+
.slice()
|
|
692
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
693
|
+
{ key: "id" },
|
|
694
|
+
),
|
|
695
|
+
)
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
.catch(() => undefined)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const pumpPrefetch = (directory: string) => {
|
|
703
|
+
const q = queueFor(directory)
|
|
704
|
+
if (q.running >= prefetchConcurrency) return
|
|
705
|
+
|
|
706
|
+
const sessionID = q.pending.shift()
|
|
707
|
+
if (!sessionID) return
|
|
708
|
+
|
|
709
|
+
q.pendingSet.delete(sessionID)
|
|
710
|
+
q.inflight.add(sessionID)
|
|
711
|
+
q.running += 1
|
|
712
|
+
|
|
713
|
+
const token = prefetchToken.value
|
|
714
|
+
|
|
715
|
+
void prefetchMessages(directory, sessionID, token).finally(() => {
|
|
716
|
+
q.running -= 1
|
|
717
|
+
q.inflight.delete(sessionID)
|
|
718
|
+
pumpPrefetch(directory)
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const prefetchSession = (session: Session, priority: "high" | "low" = "low") => {
|
|
723
|
+
const directory = session.directory
|
|
724
|
+
if (!directory) return
|
|
725
|
+
|
|
726
|
+
const [store] = globalSync.child(directory)
|
|
727
|
+
if (store.message[session.id] !== undefined) return
|
|
728
|
+
|
|
729
|
+
const q = queueFor(directory)
|
|
730
|
+
if (q.inflight.has(session.id)) return
|
|
731
|
+
if (q.pendingSet.has(session.id)) return
|
|
732
|
+
|
|
733
|
+
if (priority === "high") q.pending.unshift(session.id)
|
|
734
|
+
if (priority !== "high") q.pending.push(session.id)
|
|
735
|
+
q.pendingSet.add(session.id)
|
|
736
|
+
|
|
737
|
+
while (q.pending.length > prefetchPendingLimit) {
|
|
738
|
+
const dropped = q.pending.pop()
|
|
739
|
+
if (!dropped) continue
|
|
740
|
+
q.pendingSet.delete(dropped)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
pumpPrefetch(directory)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
createEffect(() => {
|
|
747
|
+
const sessions = currentSessions()
|
|
748
|
+
const id = params.id
|
|
749
|
+
|
|
750
|
+
if (!id) {
|
|
751
|
+
const first = sessions[0]
|
|
752
|
+
if (first) prefetchSession(first)
|
|
753
|
+
|
|
754
|
+
const second = sessions[1]
|
|
755
|
+
if (second) prefetchSession(second)
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const index = sessions.findIndex((s) => s.id === id)
|
|
760
|
+
if (index === -1) return
|
|
761
|
+
|
|
762
|
+
const next = sessions[index + 1]
|
|
763
|
+
if (next) prefetchSession(next)
|
|
764
|
+
|
|
765
|
+
const prev = sessions[index - 1]
|
|
766
|
+
if (prev) prefetchSession(prev)
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
function navigateSessionByOffset(offset: number) {
|
|
770
|
+
const sessions = currentSessions()
|
|
771
|
+
if (sessions.length === 0) return
|
|
772
|
+
|
|
773
|
+
const sessionIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1
|
|
774
|
+
|
|
775
|
+
let targetIndex: number
|
|
776
|
+
if (sessionIndex === -1) {
|
|
777
|
+
targetIndex = offset > 0 ? 0 : sessions.length - 1
|
|
778
|
+
} else {
|
|
779
|
+
targetIndex = (sessionIndex + offset + sessions.length) % sessions.length
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const session = sessions[targetIndex]
|
|
783
|
+
if (!session) return
|
|
784
|
+
|
|
785
|
+
const next = sessions[(targetIndex + 1) % sessions.length]
|
|
786
|
+
const prev = sessions[(targetIndex - 1 + sessions.length) % sessions.length]
|
|
787
|
+
|
|
788
|
+
if (offset > 0) {
|
|
789
|
+
if (next) prefetchSession(next, "high")
|
|
790
|
+
if (prev) prefetchSession(prev)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (offset < 0) {
|
|
794
|
+
if (prev) prefetchSession(prev, "high")
|
|
795
|
+
if (next) prefetchSession(next)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (import.meta.env.DEV) {
|
|
799
|
+
navStart({
|
|
800
|
+
dir: base64Encode(session.directory),
|
|
801
|
+
from: params.id,
|
|
802
|
+
to: session.id,
|
|
803
|
+
trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup",
|
|
804
|
+
})
|
|
805
|
+
}
|
|
806
|
+
navigateToSession(session)
|
|
807
|
+
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function archiveSession(session: Session) {
|
|
811
|
+
const [store, setStore] = globalSync.child(session.directory)
|
|
812
|
+
const sessions = store.session ?? []
|
|
813
|
+
const index = sessions.findIndex((s) => s.id === session.id)
|
|
814
|
+
const nextSession = sessions[index + 1] ?? sessions[index - 1]
|
|
815
|
+
|
|
816
|
+
await globalSDK.client.session.update({
|
|
817
|
+
directory: session.directory,
|
|
818
|
+
sessionID: session.id,
|
|
819
|
+
time: { archived: Date.now() },
|
|
820
|
+
})
|
|
821
|
+
setStore(
|
|
822
|
+
produce((draft) => {
|
|
823
|
+
const match = Binary.search(draft.session, session.id, (s) => s.id)
|
|
824
|
+
if (match.found) draft.session.splice(match.index, 1)
|
|
825
|
+
}),
|
|
826
|
+
)
|
|
827
|
+
if (session.id === params.id) {
|
|
828
|
+
if (nextSession) {
|
|
829
|
+
navigate(`/${params.dir}/session/${nextSession.id}`)
|
|
830
|
+
} else {
|
|
831
|
+
navigate(`/${params.dir}/session`)
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function deleteSession(session: Session) {
|
|
837
|
+
const [store, setStore] = globalSync.child(session.directory)
|
|
838
|
+
const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
|
839
|
+
const index = sessions.findIndex((s) => s.id === session.id)
|
|
840
|
+
const nextSession = sessions[index + 1] ?? sessions[index - 1]
|
|
841
|
+
|
|
842
|
+
const result = await globalSDK.client.session
|
|
843
|
+
.delete({ directory: session.directory, sessionID: session.id })
|
|
844
|
+
.then((x) => x.data)
|
|
845
|
+
.catch((err) => {
|
|
846
|
+
showToast({
|
|
847
|
+
title: language.t("session.delete.failed.title"),
|
|
848
|
+
description: errorMessage(err),
|
|
849
|
+
})
|
|
850
|
+
return false
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
if (!result) return
|
|
854
|
+
|
|
855
|
+
setStore(
|
|
856
|
+
produce((draft) => {
|
|
857
|
+
const removed = new Set<string>([session.id])
|
|
858
|
+
const collect = (parentID: string) => {
|
|
859
|
+
for (const item of draft.session) {
|
|
860
|
+
if (item.parentID !== parentID) continue
|
|
861
|
+
removed.add(item.id)
|
|
862
|
+
collect(item.id)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
collect(session.id)
|
|
866
|
+
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
|
867
|
+
}),
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
if (session.id === params.id) {
|
|
871
|
+
if (nextSession) {
|
|
872
|
+
navigate(`/${params.dir}/session/${nextSession.id}`)
|
|
873
|
+
} else {
|
|
874
|
+
navigate(`/${params.dir}/session`)
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
command.register(() => {
|
|
880
|
+
const commands: CommandOption[] = [
|
|
881
|
+
{
|
|
882
|
+
id: "sidebar.toggle",
|
|
883
|
+
title: language.t("command.sidebar.toggle"),
|
|
884
|
+
category: language.t("command.category.view"),
|
|
885
|
+
keybind: "mod+b",
|
|
886
|
+
onSelect: () => layout.sidebar.toggle(),
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
id: "project.open",
|
|
890
|
+
title: language.t("command.project.open"),
|
|
891
|
+
category: language.t("command.category.project"),
|
|
892
|
+
keybind: "mod+o",
|
|
893
|
+
onSelect: () => chooseProject(),
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
id: "provider.connect",
|
|
897
|
+
title: language.t("command.provider.connect"),
|
|
898
|
+
category: language.t("command.category.provider"),
|
|
899
|
+
onSelect: () => connectProvider(),
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
id: "server.switch",
|
|
903
|
+
title: language.t("command.server.switch"),
|
|
904
|
+
category: language.t("command.category.server"),
|
|
905
|
+
onSelect: () => openServer(),
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
id: "settings.open",
|
|
909
|
+
title: language.t("command.settings.open"),
|
|
910
|
+
category: language.t("command.category.settings"),
|
|
911
|
+
keybind: "mod+comma",
|
|
912
|
+
onSelect: () => openSettings(),
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
id: "session.previous",
|
|
916
|
+
title: language.t("command.session.previous"),
|
|
917
|
+
category: language.t("command.category.session"),
|
|
918
|
+
keybind: "alt+arrowup",
|
|
919
|
+
onSelect: () => navigateSessionByOffset(-1),
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
id: "session.next",
|
|
923
|
+
title: language.t("command.session.next"),
|
|
924
|
+
category: language.t("command.category.session"),
|
|
925
|
+
keybind: "alt+arrowdown",
|
|
926
|
+
onSelect: () => navigateSessionByOffset(1),
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
id: "session.archive",
|
|
930
|
+
title: language.t("command.session.archive"),
|
|
931
|
+
category: language.t("command.category.session"),
|
|
932
|
+
keybind: "mod+shift+backspace",
|
|
933
|
+
disabled: !params.dir || !params.id,
|
|
934
|
+
onSelect: () => {
|
|
935
|
+
const session = currentSessions().find((s) => s.id === params.id)
|
|
936
|
+
if (session) archiveSession(session)
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
id: "theme.cycle",
|
|
941
|
+
title: language.t("command.theme.cycle"),
|
|
942
|
+
category: language.t("command.category.theme"),
|
|
943
|
+
keybind: "mod+shift+t",
|
|
944
|
+
onSelect: () => cycleTheme(1),
|
|
945
|
+
},
|
|
946
|
+
]
|
|
947
|
+
|
|
948
|
+
for (const [id, definition] of availableThemeEntries()) {
|
|
949
|
+
commands.push({
|
|
950
|
+
id: `theme.set.${id}`,
|
|
951
|
+
title: language.t("command.theme.set", { theme: definition.name ?? id }),
|
|
952
|
+
category: language.t("command.category.theme"),
|
|
953
|
+
onSelect: () => theme.commitPreview(),
|
|
954
|
+
onHighlight: () => {
|
|
955
|
+
theme.previewTheme(id)
|
|
956
|
+
return () => theme.cancelPreview()
|
|
957
|
+
},
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
commands.push({
|
|
962
|
+
id: "theme.scheme.cycle",
|
|
963
|
+
title: language.t("command.theme.scheme.cycle"),
|
|
964
|
+
category: language.t("command.category.theme"),
|
|
965
|
+
keybind: "mod+shift+s",
|
|
966
|
+
onSelect: () => cycleColorScheme(1),
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
for (const scheme of colorSchemeOrder) {
|
|
970
|
+
commands.push({
|
|
971
|
+
id: `theme.scheme.${scheme}`,
|
|
972
|
+
title: language.t("command.theme.scheme.set", { scheme: colorSchemeLabel(scheme) }),
|
|
973
|
+
category: language.t("command.category.theme"),
|
|
974
|
+
onSelect: () => theme.commitPreview(),
|
|
975
|
+
onHighlight: () => {
|
|
976
|
+
theme.previewColorScheme(scheme)
|
|
977
|
+
return () => theme.cancelPreview()
|
|
978
|
+
},
|
|
979
|
+
})
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
commands.push({
|
|
983
|
+
id: "language.cycle",
|
|
984
|
+
title: language.t("command.language.cycle"),
|
|
985
|
+
category: language.t("command.category.language"),
|
|
986
|
+
onSelect: () => cycleLanguage(1),
|
|
987
|
+
})
|
|
988
|
+
|
|
989
|
+
for (const locale of language.locales) {
|
|
990
|
+
commands.push({
|
|
991
|
+
id: `language.set.${locale}`,
|
|
992
|
+
title: language.t("command.language.set", { language: language.label(locale) }),
|
|
993
|
+
category: language.t("command.category.language"),
|
|
994
|
+
onSelect: () => setLocale(locale),
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return commands
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
function connectProvider() {
|
|
1002
|
+
dialog.show(() => <DialogSelectProvider />)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function openServer() {
|
|
1006
|
+
dialog.show(() => <DialogSelectServer />)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function openSettings() {
|
|
1010
|
+
dialog.show(() => <DialogSettings />)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function navigateToProject(directory: string | undefined) {
|
|
1014
|
+
if (!directory) return
|
|
1015
|
+
server.projects.touch(directory)
|
|
1016
|
+
const lastSession = store.lastSession[directory]
|
|
1017
|
+
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
|
|
1018
|
+
layout.mobileSidebar.hide()
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function navigateToSession(session: Session | undefined) {
|
|
1022
|
+
if (!session) return
|
|
1023
|
+
navigate(`/${base64Encode(session.directory)}/session/${session.id}`)
|
|
1024
|
+
layout.mobileSidebar.hide()
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function openProject(directory: string, navigate = true) {
|
|
1028
|
+
layout.projects.open(directory)
|
|
1029
|
+
if (navigate) navigateToProject(directory)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
|
|
1033
|
+
|
|
1034
|
+
async function renameProject(project: LocalProject, next: string) {
|
|
1035
|
+
const current = displayName(project)
|
|
1036
|
+
if (next === current) return
|
|
1037
|
+
const name = next === getFilename(project.worktree) ? "" : next
|
|
1038
|
+
|
|
1039
|
+
if (project.id && project.id !== "global") {
|
|
1040
|
+
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
|
|
1041
|
+
return
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
globalSync.project.meta(project.worktree, { name })
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function renameSession(session: Session, next: string) {
|
|
1048
|
+
if (next === session.title) return
|
|
1049
|
+
await globalSDK.client.session.update({
|
|
1050
|
+
directory: session.directory,
|
|
1051
|
+
sessionID: session.id,
|
|
1052
|
+
title: next,
|
|
1053
|
+
})
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
|
|
1057
|
+
const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
|
|
1058
|
+
if (current === next) return
|
|
1059
|
+
setWorkspaceName(directory, next, projectId, branch)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function closeProject(directory: string) {
|
|
1063
|
+
const index = layout.projects.list().findIndex((x) => x.worktree === directory)
|
|
1064
|
+
const next = layout.projects.list()[index + 1]
|
|
1065
|
+
layout.projects.close(directory)
|
|
1066
|
+
if (next) navigateToProject(next.worktree)
|
|
1067
|
+
else navigate("/")
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function chooseProject() {
|
|
1071
|
+
function resolve(result: string | string[] | null) {
|
|
1072
|
+
if (Array.isArray(result)) {
|
|
1073
|
+
for (const directory of result) {
|
|
1074
|
+
openProject(directory, false)
|
|
1075
|
+
}
|
|
1076
|
+
navigateToProject(result[0])
|
|
1077
|
+
} else if (result) {
|
|
1078
|
+
openProject(result)
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
|
1083
|
+
const result = await platform.openDirectoryPickerDialog?.({
|
|
1084
|
+
title: language.t("command.project.open"),
|
|
1085
|
+
multiple: true,
|
|
1086
|
+
})
|
|
1087
|
+
resolve(result)
|
|
1088
|
+
} else {
|
|
1089
|
+
dialog.show(
|
|
1090
|
+
() => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
|
|
1091
|
+
() => resolve(null),
|
|
1092
|
+
)
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const errorMessage = (err: unknown) => {
|
|
1097
|
+
if (err && typeof err === "object" && "data" in err) {
|
|
1098
|
+
const data = (err as { data?: { message?: string } }).data
|
|
1099
|
+
if (data?.message) return data.message
|
|
1100
|
+
}
|
|
1101
|
+
if (err instanceof Error) return err.message
|
|
1102
|
+
return language.t("common.requestFailed")
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const deleteWorkspace = async (directory: string) => {
|
|
1106
|
+
const current = currentProject()
|
|
1107
|
+
if (!current) return
|
|
1108
|
+
if (directory === current.worktree) return
|
|
1109
|
+
|
|
1110
|
+
setBusy(directory, true)
|
|
1111
|
+
|
|
1112
|
+
const result = await globalSDK.client.worktree
|
|
1113
|
+
.remove({ directory: current.worktree, worktreeRemoveInput: { directory } })
|
|
1114
|
+
.then((x) => x.data)
|
|
1115
|
+
.catch((err) => {
|
|
1116
|
+
showToast({
|
|
1117
|
+
title: language.t("workspace.delete.failed.title"),
|
|
1118
|
+
description: errorMessage(err),
|
|
1119
|
+
})
|
|
1120
|
+
return false
|
|
1121
|
+
})
|
|
1122
|
+
|
|
1123
|
+
setBusy(directory, false)
|
|
1124
|
+
|
|
1125
|
+
if (!result) return
|
|
1126
|
+
|
|
1127
|
+
layout.projects.close(directory)
|
|
1128
|
+
layout.projects.open(current.worktree)
|
|
1129
|
+
|
|
1130
|
+
if (params.dir && base64Decode(params.dir) === directory) {
|
|
1131
|
+
navigateToProject(current.worktree)
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const resetWorkspace = async (directory: string) => {
|
|
1136
|
+
const current = currentProject()
|
|
1137
|
+
if (!current) return
|
|
1138
|
+
if (directory === current.worktree) return
|
|
1139
|
+
setBusy(directory, true)
|
|
1140
|
+
|
|
1141
|
+
const progress = showToast({
|
|
1142
|
+
persistent: true,
|
|
1143
|
+
title: language.t("workspace.resetting.title"),
|
|
1144
|
+
description: language.t("workspace.resetting.description"),
|
|
1145
|
+
})
|
|
1146
|
+
const dismiss = () => toaster.dismiss(progress)
|
|
1147
|
+
|
|
1148
|
+
const sessions = await globalSDK.client.session
|
|
1149
|
+
.list({ directory })
|
|
1150
|
+
.then((x) => x.data ?? [])
|
|
1151
|
+
.catch(() => [])
|
|
1152
|
+
|
|
1153
|
+
const result = await globalSDK.client.worktree
|
|
1154
|
+
.reset({ directory: current.worktree, worktreeResetInput: { directory } })
|
|
1155
|
+
.then((x) => x.data)
|
|
1156
|
+
.catch((err) => {
|
|
1157
|
+
showToast({
|
|
1158
|
+
title: language.t("workspace.reset.failed.title"),
|
|
1159
|
+
description: errorMessage(err),
|
|
1160
|
+
})
|
|
1161
|
+
return false
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
if (!result) {
|
|
1165
|
+
setBusy(directory, false)
|
|
1166
|
+
dismiss()
|
|
1167
|
+
return
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const archivedAt = Date.now()
|
|
1171
|
+
await Promise.all(
|
|
1172
|
+
sessions
|
|
1173
|
+
.filter((session) => session.time.archived === undefined)
|
|
1174
|
+
.map((session) =>
|
|
1175
|
+
globalSDK.client.session
|
|
1176
|
+
.update({
|
|
1177
|
+
sessionID: session.id,
|
|
1178
|
+
directory: session.directory,
|
|
1179
|
+
time: { archived: archivedAt },
|
|
1180
|
+
})
|
|
1181
|
+
.catch(() => undefined),
|
|
1182
|
+
),
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
|
1186
|
+
|
|
1187
|
+
setBusy(directory, false)
|
|
1188
|
+
dismiss()
|
|
1189
|
+
|
|
1190
|
+
showToast({
|
|
1191
|
+
title: language.t("workspace.reset.success.title"),
|
|
1192
|
+
description: language.t("workspace.reset.success.description"),
|
|
1193
|
+
actions: [
|
|
1194
|
+
{
|
|
1195
|
+
label: language.t("command.session.new"),
|
|
1196
|
+
onClick: () => {
|
|
1197
|
+
const href = `/${base64Encode(directory)}/session`
|
|
1198
|
+
navigate(href)
|
|
1199
|
+
layout.mobileSidebar.hide()
|
|
1200
|
+
},
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
label: language.t("common.dismiss"),
|
|
1204
|
+
onClick: "dismiss",
|
|
1205
|
+
},
|
|
1206
|
+
],
|
|
1207
|
+
})
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function DialogDeleteSession(props: { session: Session }) {
|
|
1211
|
+
const handleDelete = async () => {
|
|
1212
|
+
await deleteSession(props.session)
|
|
1213
|
+
dialog.close()
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return (
|
|
1217
|
+
<Dialog title={language.t("session.delete.title")} fit>
|
|
1218
|
+
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
|
1219
|
+
<div class="flex flex-col gap-1">
|
|
1220
|
+
<span class="text-14-regular text-text-strong">
|
|
1221
|
+
{language.t("session.delete.confirm", { name: props.session.title })}
|
|
1222
|
+
</span>
|
|
1223
|
+
</div>
|
|
1224
|
+
<div class="flex justify-end gap-2">
|
|
1225
|
+
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
|
1226
|
+
{language.t("common.cancel")}
|
|
1227
|
+
</Button>
|
|
1228
|
+
<Button variant="primary" size="large" onClick={handleDelete}>
|
|
1229
|
+
{language.t("session.delete.button")}
|
|
1230
|
+
</Button>
|
|
1231
|
+
</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
</Dialog>
|
|
1234
|
+
)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function DialogDeleteWorkspace(props: { directory: string }) {
|
|
1238
|
+
const name = createMemo(() => getFilename(props.directory))
|
|
1239
|
+
const [data, setData] = createStore({
|
|
1240
|
+
status: "loading" as "loading" | "ready" | "error",
|
|
1241
|
+
dirty: false,
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1244
|
+
onMount(() => {
|
|
1245
|
+
const current = currentProject()
|
|
1246
|
+
if (!current) {
|
|
1247
|
+
setData({ status: "error", dirty: false })
|
|
1248
|
+
return
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
globalSDK.client.file
|
|
1252
|
+
.status({ directory: props.directory })
|
|
1253
|
+
.then((x) => {
|
|
1254
|
+
const files = x.data ?? []
|
|
1255
|
+
const dirty = files.length > 0
|
|
1256
|
+
setData({ status: "ready", dirty })
|
|
1257
|
+
})
|
|
1258
|
+
.catch(() => {
|
|
1259
|
+
setData({ status: "error", dirty: false })
|
|
1260
|
+
})
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
const handleDelete = () => {
|
|
1264
|
+
dialog.close()
|
|
1265
|
+
void deleteWorkspace(props.directory)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const description = () => {
|
|
1269
|
+
if (data.status === "loading") return language.t("workspace.status.checking")
|
|
1270
|
+
if (data.status === "error") return language.t("workspace.status.error")
|
|
1271
|
+
if (!data.dirty) return language.t("workspace.status.clean")
|
|
1272
|
+
return language.t("workspace.status.dirty")
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return (
|
|
1276
|
+
<Dialog title={language.t("workspace.delete.title")} fit>
|
|
1277
|
+
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
|
1278
|
+
<div class="flex flex-col gap-1">
|
|
1279
|
+
<span class="text-14-regular text-text-strong">
|
|
1280
|
+
{language.t("workspace.delete.confirm", { name: name() })}
|
|
1281
|
+
</span>
|
|
1282
|
+
<span class="text-12-regular text-text-weak">{description()}</span>
|
|
1283
|
+
</div>
|
|
1284
|
+
<div class="flex justify-end gap-2">
|
|
1285
|
+
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
|
1286
|
+
{language.t("common.cancel")}
|
|
1287
|
+
</Button>
|
|
1288
|
+
<Button variant="primary" size="large" disabled={data.status === "loading"} onClick={handleDelete}>
|
|
1289
|
+
{language.t("workspace.delete.button")}
|
|
1290
|
+
</Button>
|
|
1291
|
+
</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
</Dialog>
|
|
1294
|
+
)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function DialogResetWorkspace(props: { directory: string }) {
|
|
1298
|
+
const name = createMemo(() => getFilename(props.directory))
|
|
1299
|
+
const [state, setState] = createStore({
|
|
1300
|
+
status: "loading" as "loading" | "ready" | "error",
|
|
1301
|
+
dirty: false,
|
|
1302
|
+
sessions: [] as Session[],
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1305
|
+
const refresh = async () => {
|
|
1306
|
+
const sessions = await globalSDK.client.session
|
|
1307
|
+
.list({ directory: props.directory })
|
|
1308
|
+
.then((x) => x.data ?? [])
|
|
1309
|
+
.catch(() => [])
|
|
1310
|
+
const active = sessions.filter((session) => session.time.archived === undefined)
|
|
1311
|
+
setState({ sessions: active })
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
onMount(() => {
|
|
1315
|
+
const current = currentProject()
|
|
1316
|
+
if (!current) {
|
|
1317
|
+
setState({ status: "error", dirty: false })
|
|
1318
|
+
return
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
globalSDK.client.file
|
|
1322
|
+
.status({ directory: props.directory })
|
|
1323
|
+
.then((x) => {
|
|
1324
|
+
const files = x.data ?? []
|
|
1325
|
+
const dirty = files.length > 0
|
|
1326
|
+
setState({ status: "ready", dirty })
|
|
1327
|
+
void refresh()
|
|
1328
|
+
})
|
|
1329
|
+
.catch(() => {
|
|
1330
|
+
setState({ status: "error", dirty: false })
|
|
1331
|
+
})
|
|
1332
|
+
})
|
|
1333
|
+
|
|
1334
|
+
const handleReset = () => {
|
|
1335
|
+
dialog.close()
|
|
1336
|
+
void resetWorkspace(props.directory)
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const archivedCount = () => state.sessions.length
|
|
1340
|
+
|
|
1341
|
+
const description = () => {
|
|
1342
|
+
if (state.status === "loading") return language.t("workspace.status.checking")
|
|
1343
|
+
if (state.status === "error") return language.t("workspace.status.error")
|
|
1344
|
+
if (!state.dirty) return language.t("workspace.status.clean")
|
|
1345
|
+
return language.t("workspace.status.dirty")
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const archivedLabel = () => {
|
|
1349
|
+
const count = archivedCount()
|
|
1350
|
+
if (count === 0) return language.t("workspace.reset.archived.none")
|
|
1351
|
+
if (count === 1) return language.t("workspace.reset.archived.one")
|
|
1352
|
+
return language.t("workspace.reset.archived.many", { count })
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
return (
|
|
1356
|
+
<Dialog title={language.t("workspace.reset.title")} fit>
|
|
1357
|
+
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
|
1358
|
+
<div class="flex flex-col gap-1">
|
|
1359
|
+
<span class="text-14-regular text-text-strong">
|
|
1360
|
+
{language.t("workspace.reset.confirm", { name: name() })}
|
|
1361
|
+
</span>
|
|
1362
|
+
<span class="text-12-regular text-text-weak">
|
|
1363
|
+
{description()} {archivedLabel()} {language.t("workspace.reset.note")}
|
|
1364
|
+
</span>
|
|
1365
|
+
</div>
|
|
1366
|
+
<div class="flex justify-end gap-2">
|
|
1367
|
+
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
|
1368
|
+
{language.t("common.cancel")}
|
|
1369
|
+
</Button>
|
|
1370
|
+
<Button variant="primary" size="large" disabled={state.status === "loading"} onClick={handleReset}>
|
|
1371
|
+
{language.t("workspace.reset.button")}
|
|
1372
|
+
</Button>
|
|
1373
|
+
</div>
|
|
1374
|
+
</div>
|
|
1375
|
+
</Dialog>
|
|
1376
|
+
)
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
createEffect(
|
|
1380
|
+
on(
|
|
1381
|
+
() => ({ ready: pageReady(), dir: params.dir, id: params.id }),
|
|
1382
|
+
(value) => {
|
|
1383
|
+
if (!value.ready) return
|
|
1384
|
+
const dir = value.dir
|
|
1385
|
+
const id = value.id
|
|
1386
|
+
if (!dir || !id) return
|
|
1387
|
+
const directory = base64Decode(dir)
|
|
1388
|
+
setStore("lastSession", directory, id)
|
|
1389
|
+
notification.session.markViewed(id)
|
|
1390
|
+
const expanded = untrack(() => store.workspaceExpanded[directory])
|
|
1391
|
+
if (expanded === false) {
|
|
1392
|
+
setStore("workspaceExpanded", directory, true)
|
|
1393
|
+
}
|
|
1394
|
+
requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`))
|
|
1395
|
+
},
|
|
1396
|
+
{ defer: true },
|
|
1397
|
+
),
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
createEffect(() => {
|
|
1401
|
+
const project = currentProject()
|
|
1402
|
+
if (!project) return
|
|
1403
|
+
|
|
1404
|
+
if (workspaceSetting()) {
|
|
1405
|
+
const activeDir = params.dir ? base64Decode(params.dir) : ""
|
|
1406
|
+
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
|
1407
|
+
for (const directory of dirs) {
|
|
1408
|
+
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
|
1409
|
+
const active = directory === activeDir
|
|
1410
|
+
if (!expanded && !active) continue
|
|
1411
|
+
globalSync.project.loadSessions(directory)
|
|
1412
|
+
}
|
|
1413
|
+
return
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
globalSync.project.loadSessions(project.worktree)
|
|
1417
|
+
})
|
|
1418
|
+
|
|
1419
|
+
function getDraggableId(event: unknown): string | undefined {
|
|
1420
|
+
if (typeof event !== "object" || event === null) return undefined
|
|
1421
|
+
if (!("draggable" in event)) return undefined
|
|
1422
|
+
const draggable = (event as { draggable?: { id?: unknown } }).draggable
|
|
1423
|
+
if (!draggable) return undefined
|
|
1424
|
+
return typeof draggable.id === "string" ? draggable.id : undefined
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function handleDragStart(event: unknown) {
|
|
1428
|
+
const id = getDraggableId(event)
|
|
1429
|
+
if (!id) return
|
|
1430
|
+
setStore("activeProject", id)
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function handleDragOver(event: DragEvent) {
|
|
1434
|
+
const { draggable, droppable } = event
|
|
1435
|
+
if (draggable && droppable) {
|
|
1436
|
+
const projects = layout.projects.list()
|
|
1437
|
+
const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
|
|
1438
|
+
const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
|
|
1439
|
+
if (fromIndex !== toIndex && toIndex !== -1) {
|
|
1440
|
+
layout.projects.move(draggable.id.toString(), toIndex)
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function handleDragEnd() {
|
|
1446
|
+
setStore("activeProject", undefined)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function workspaceIds(project: LocalProject | undefined) {
|
|
1450
|
+
if (!project) return []
|
|
1451
|
+
const local = project.worktree
|
|
1452
|
+
const dirs = [local, ...(project.sandboxes ?? [])]
|
|
1453
|
+
const active = currentProject()
|
|
1454
|
+
const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined
|
|
1455
|
+
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
|
|
1456
|
+
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
|
1457
|
+
|
|
1458
|
+
const existing = store.workspaceOrder[project.worktree]
|
|
1459
|
+
if (!existing) return extra ? [...dirs, extra] : dirs
|
|
1460
|
+
|
|
1461
|
+
const keep = existing.filter((d) => d !== local && dirs.includes(d))
|
|
1462
|
+
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
|
|
1463
|
+
const merged = [local, ...(pending && extra ? [extra] : []), ...missing, ...keep]
|
|
1464
|
+
if (!extra) return merged
|
|
1465
|
+
if (pending) return merged
|
|
1466
|
+
return [...merged, extra]
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function handleWorkspaceDragStart(event: unknown) {
|
|
1470
|
+
const id = getDraggableId(event)
|
|
1471
|
+
if (!id) return
|
|
1472
|
+
setStore("activeWorkspace", id)
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function handleWorkspaceDragOver(event: DragEvent) {
|
|
1476
|
+
const { draggable, droppable } = event
|
|
1477
|
+
if (!draggable || !droppable) return
|
|
1478
|
+
|
|
1479
|
+
const project = currentProject()
|
|
1480
|
+
if (!project) return
|
|
1481
|
+
|
|
1482
|
+
const ids = workspaceIds(project)
|
|
1483
|
+
const fromIndex = ids.findIndex((dir) => dir === draggable.id.toString())
|
|
1484
|
+
const toIndex = ids.findIndex((dir) => dir === droppable.id.toString())
|
|
1485
|
+
if (fromIndex === -1 || toIndex === -1) return
|
|
1486
|
+
if (fromIndex === toIndex) return
|
|
1487
|
+
|
|
1488
|
+
const result = ids.slice()
|
|
1489
|
+
const [item] = result.splice(fromIndex, 1)
|
|
1490
|
+
if (!item) return
|
|
1491
|
+
result.splice(toIndex, 0, item)
|
|
1492
|
+
setStore("workspaceOrder", project.worktree, result)
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function handleWorkspaceDragEnd() {
|
|
1496
|
+
setStore("activeWorkspace", undefined)
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
|
1500
|
+
const notification = useNotification()
|
|
1501
|
+
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
|
|
1502
|
+
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
|
1503
|
+
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
|
1504
|
+
const mask = "radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px)"
|
|
1505
|
+
const jonsoc = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
|
1506
|
+
|
|
1507
|
+
return (
|
|
1508
|
+
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
|
|
1509
|
+
<div class="size-full rounded overflow-clip">
|
|
1510
|
+
<Avatar
|
|
1511
|
+
fallback={name()}
|
|
1512
|
+
src={props.project.id === jonsoc ? "https://jonsoc.com/favicon.svg" : props.project.icon?.override}
|
|
1513
|
+
{...getAvatarColors(props.project.icon?.color)}
|
|
1514
|
+
class="size-full rounded"
|
|
1515
|
+
style={
|
|
1516
|
+
notifications().length > 0 && props.notify
|
|
1517
|
+
? { "-webkit-mask-image": mask, "mask-image": mask }
|
|
1518
|
+
: undefined
|
|
1519
|
+
}
|
|
1520
|
+
/>
|
|
1521
|
+
</div>
|
|
1522
|
+
<Show when={notifications().length > 0 && props.notify}>
|
|
1523
|
+
<div
|
|
1524
|
+
classList={{
|
|
1525
|
+
"absolute top-px right-px size-1.5 rounded-full z-10": true,
|
|
1526
|
+
"bg-icon-critical-base": hasError(),
|
|
1527
|
+
"bg-text-interactive-base": !hasError(),
|
|
1528
|
+
}}
|
|
1529
|
+
/>
|
|
1530
|
+
</Show>
|
|
1531
|
+
</div>
|
|
1532
|
+
)
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const SessionItem = (props: {
|
|
1536
|
+
session: Session
|
|
1537
|
+
slug: string
|
|
1538
|
+
mobile?: boolean
|
|
1539
|
+
dense?: boolean
|
|
1540
|
+
popover?: boolean
|
|
1541
|
+
}): JSX.Element => {
|
|
1542
|
+
const notification = useNotification()
|
|
1543
|
+
const notifications = createMemo(() => notification.session.unseen(props.session.id))
|
|
1544
|
+
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
|
1545
|
+
const [sessionStore] = globalSync.child(props.session.directory)
|
|
1546
|
+
const hasPermissions = createMemo(() => {
|
|
1547
|
+
const permissions = sessionStore.permission?.[props.session.id] ?? []
|
|
1548
|
+
if (permissions.length > 0) return true
|
|
1549
|
+
const childSessions = sessionStore.session.filter((s) => s.parentID === props.session.id)
|
|
1550
|
+
for (const child of childSessions) {
|
|
1551
|
+
const childPermissions = sessionStore.permission?.[child.id] ?? []
|
|
1552
|
+
if (childPermissions.length > 0) return true
|
|
1553
|
+
}
|
|
1554
|
+
return false
|
|
1555
|
+
})
|
|
1556
|
+
const isWorking = createMemo(() => {
|
|
1557
|
+
if (hasPermissions()) return false
|
|
1558
|
+
const status = sessionStore.session_status[props.session.id]
|
|
1559
|
+
return status?.type === "busy" || status?.type === "retry"
|
|
1560
|
+
})
|
|
1561
|
+
|
|
1562
|
+
const tint = createMemo(() => {
|
|
1563
|
+
const messages = sessionStore.message[props.session.id]
|
|
1564
|
+
if (!messages) return undefined
|
|
1565
|
+
const user = messages
|
|
1566
|
+
.slice()
|
|
1567
|
+
.reverse()
|
|
1568
|
+
.find((m) => m.role === "user")
|
|
1569
|
+
if (!user?.agent) return undefined
|
|
1570
|
+
|
|
1571
|
+
const agent = sessionStore.agent.find((a) => a.name === user.agent)
|
|
1572
|
+
return agent?.color
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
const hoverMessages = createMemo(() =>
|
|
1576
|
+
sessionStore.message[props.session.id]?.filter((message) => message.role === "user"),
|
|
1577
|
+
)
|
|
1578
|
+
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
|
|
1579
|
+
const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
|
|
1580
|
+
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
|
1581
|
+
const isActive = createMemo(() => props.session.id === params.id)
|
|
1582
|
+
const [menuOpen, setMenuOpen] = createSignal(false)
|
|
1583
|
+
const [pendingRename, setPendingRename] = createSignal(false)
|
|
1584
|
+
|
|
1585
|
+
const messageLabel = (message: Message) => {
|
|
1586
|
+
const parts = sessionStore.part[message.id] ?? []
|
|
1587
|
+
const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored)
|
|
1588
|
+
return text?.text
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const item = (
|
|
1592
|
+
<A
|
|
1593
|
+
href={`${props.slug}/session/${props.session.id}`}
|
|
1594
|
+
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menuOpen() ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
|
1595
|
+
onMouseEnter={() => prefetchSession(props.session, "high")}
|
|
1596
|
+
onFocus={() => prefetchSession(props.session, "high")}
|
|
1597
|
+
onClick={() => setHoverSession(undefined)}
|
|
1598
|
+
>
|
|
1599
|
+
<div class="flex items-center gap-1 w-full">
|
|
1600
|
+
<div
|
|
1601
|
+
class="shrink-0 size-6 flex items-center justify-center"
|
|
1602
|
+
style={{ color: tint() ?? "var(--icon-interactive-base)" }}
|
|
1603
|
+
>
|
|
1604
|
+
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
|
1605
|
+
<Match when={isWorking()}>
|
|
1606
|
+
<Spinner class="size-[15px]" />
|
|
1607
|
+
</Match>
|
|
1608
|
+
<Match when={hasPermissions()}>
|
|
1609
|
+
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
|
1610
|
+
</Match>
|
|
1611
|
+
<Match when={hasError()}>
|
|
1612
|
+
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
|
1613
|
+
</Match>
|
|
1614
|
+
<Match when={notifications().length > 0}>
|
|
1615
|
+
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
|
1616
|
+
</Match>
|
|
1617
|
+
</Switch>
|
|
1618
|
+
</div>
|
|
1619
|
+
<InlineEditor
|
|
1620
|
+
id={`session:${props.session.id}`}
|
|
1621
|
+
value={() => props.session.title}
|
|
1622
|
+
onSave={(next) => renameSession(props.session, next)}
|
|
1623
|
+
class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
|
1624
|
+
displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
|
1625
|
+
stopPropagation
|
|
1626
|
+
/>
|
|
1627
|
+
<Show when={props.session.summary}>
|
|
1628
|
+
{(summary) => (
|
|
1629
|
+
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
|
1630
|
+
<DiffChanges changes={summary()} />
|
|
1631
|
+
</div>
|
|
1632
|
+
)}
|
|
1633
|
+
</Show>
|
|
1634
|
+
</div>
|
|
1635
|
+
</A>
|
|
1636
|
+
)
|
|
1637
|
+
|
|
1638
|
+
return (
|
|
1639
|
+
<div
|
|
1640
|
+
data-session-id={props.session.id}
|
|
1641
|
+
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
|
|
1642
|
+
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
|
1643
|
+
>
|
|
1644
|
+
<Show
|
|
1645
|
+
when={hoverEnabled()}
|
|
1646
|
+
fallback={
|
|
1647
|
+
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
|
|
1648
|
+
{item}
|
|
1649
|
+
</Tooltip>
|
|
1650
|
+
}
|
|
1651
|
+
>
|
|
1652
|
+
<HoverCard
|
|
1653
|
+
openDelay={1000}
|
|
1654
|
+
closeDelay={0}
|
|
1655
|
+
placement="right"
|
|
1656
|
+
gutter={28}
|
|
1657
|
+
trigger={item}
|
|
1658
|
+
open={hoverSession() === props.session.id}
|
|
1659
|
+
onOpenChange={(open) => setHoverSession(open ? props.session.id : undefined)}
|
|
1660
|
+
>
|
|
1661
|
+
<Show
|
|
1662
|
+
when={hoverReady()}
|
|
1663
|
+
fallback={<div class="text-12-regular text-text-weak">{language.t("session.messages.loading")}</div>}
|
|
1664
|
+
>
|
|
1665
|
+
<div class="overflow-y-auto max-h-72 h-full">
|
|
1666
|
+
<MessageNav
|
|
1667
|
+
messages={hoverMessages() ?? []}
|
|
1668
|
+
current={undefined}
|
|
1669
|
+
getLabel={messageLabel}
|
|
1670
|
+
onMessageSelect={(message) => {
|
|
1671
|
+
if (!isActive()) {
|
|
1672
|
+
sessionStorage.setItem("jonsoc.pendingMessage", `${props.session.id}|${message.id}`)
|
|
1673
|
+
navigate(`${props.slug}/session/${props.session.id}`)
|
|
1674
|
+
return
|
|
1675
|
+
}
|
|
1676
|
+
window.history.replaceState(null, "", `#message-${message.id}`)
|
|
1677
|
+
window.dispatchEvent(new HashChangeEvent("hashchange"))
|
|
1678
|
+
}}
|
|
1679
|
+
size="normal"
|
|
1680
|
+
class="w-60"
|
|
1681
|
+
/>
|
|
1682
|
+
</div>
|
|
1683
|
+
</Show>
|
|
1684
|
+
</HoverCard>
|
|
1685
|
+
</Show>
|
|
1686
|
+
<div
|
|
1687
|
+
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
|
1688
|
+
classList={{
|
|
1689
|
+
"opacity-100 pointer-events-auto": menuOpen(),
|
|
1690
|
+
"opacity-0 pointer-events-none": !menuOpen(),
|
|
1691
|
+
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
|
1692
|
+
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
|
1693
|
+
}}
|
|
1694
|
+
>
|
|
1695
|
+
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
|
|
1696
|
+
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
|
1697
|
+
<DropdownMenu.Trigger
|
|
1698
|
+
as={IconButton}
|
|
1699
|
+
icon="dot-grid"
|
|
1700
|
+
variant="ghost"
|
|
1701
|
+
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
|
1702
|
+
aria-label={language.t("common.moreOptions")}
|
|
1703
|
+
/>
|
|
1704
|
+
</Tooltip>
|
|
1705
|
+
<DropdownMenu.Portal>
|
|
1706
|
+
<DropdownMenu.Content
|
|
1707
|
+
onCloseAutoFocus={(event) => {
|
|
1708
|
+
if (!pendingRename()) return
|
|
1709
|
+
event.preventDefault()
|
|
1710
|
+
setPendingRename(false)
|
|
1711
|
+
openEditor(`session:${props.session.id}`, props.session.title)
|
|
1712
|
+
}}
|
|
1713
|
+
>
|
|
1714
|
+
<DropdownMenu.Item
|
|
1715
|
+
onSelect={() => {
|
|
1716
|
+
setPendingRename(true)
|
|
1717
|
+
setMenuOpen(false)
|
|
1718
|
+
}}
|
|
1719
|
+
>
|
|
1720
|
+
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
|
1721
|
+
</DropdownMenu.Item>
|
|
1722
|
+
<DropdownMenu.Item onSelect={() => archiveSession(props.session)}>
|
|
1723
|
+
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
|
1724
|
+
</DropdownMenu.Item>
|
|
1725
|
+
<DropdownMenu.Separator />
|
|
1726
|
+
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogDeleteSession session={props.session} />)}>
|
|
1727
|
+
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
|
1728
|
+
</DropdownMenu.Item>
|
|
1729
|
+
</DropdownMenu.Content>
|
|
1730
|
+
</DropdownMenu.Portal>
|
|
1731
|
+
</DropdownMenu>
|
|
1732
|
+
</div>
|
|
1733
|
+
</div>
|
|
1734
|
+
)
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
const NewSessionItem = (props: { slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => {
|
|
1738
|
+
const label = language.t("command.session.new")
|
|
1739
|
+
const tooltip = () => props.mobile || !layout.sidebar.opened()
|
|
1740
|
+
const item = (
|
|
1741
|
+
<A
|
|
1742
|
+
href={`${props.slug}/session`}
|
|
1743
|
+
end
|
|
1744
|
+
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
|
1745
|
+
onClick={() => setHoverSession(undefined)}
|
|
1746
|
+
>
|
|
1747
|
+
<div class="flex items-center gap-1 w-full">
|
|
1748
|
+
<div class="shrink-0 size-6 flex items-center justify-center">
|
|
1749
|
+
<Icon name="plus-small" size="small" class="text-icon-weak" />
|
|
1750
|
+
</div>
|
|
1751
|
+
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
|
1752
|
+
{label}
|
|
1753
|
+
</span>
|
|
1754
|
+
</div>
|
|
1755
|
+
</A>
|
|
1756
|
+
)
|
|
1757
|
+
|
|
1758
|
+
return (
|
|
1759
|
+
<div class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3 hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active">
|
|
1760
|
+
<Show
|
|
1761
|
+
when={!tooltip()}
|
|
1762
|
+
fallback={
|
|
1763
|
+
<Tooltip placement={props.mobile ? "bottom" : "right"} value={label} gutter={10}>
|
|
1764
|
+
{item}
|
|
1765
|
+
</Tooltip>
|
|
1766
|
+
}
|
|
1767
|
+
>
|
|
1768
|
+
{item}
|
|
1769
|
+
</Show>
|
|
1770
|
+
</div>
|
|
1771
|
+
)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const SessionSkeleton = (props: { count?: number }): JSX.Element => {
|
|
1775
|
+
const items = Array.from({ length: props.count ?? 4 }, (_, index) => index)
|
|
1776
|
+
return (
|
|
1777
|
+
<div class="flex flex-col gap-1">
|
|
1778
|
+
<For each={items}>
|
|
1779
|
+
{() => <div class="h-8 w-full rounded-md bg-surface-raised-base opacity-60 animate-pulse" />}
|
|
1780
|
+
</For>
|
|
1781
|
+
</div>
|
|
1782
|
+
)
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const ProjectDragOverlay = (): JSX.Element => {
|
|
1786
|
+
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeProject))
|
|
1787
|
+
return (
|
|
1788
|
+
<Show when={project()}>
|
|
1789
|
+
{(p) => (
|
|
1790
|
+
<div class="bg-background-base rounded-xl p-1">
|
|
1791
|
+
<ProjectIcon project={p()} />
|
|
1792
|
+
</div>
|
|
1793
|
+
)}
|
|
1794
|
+
</Show>
|
|
1795
|
+
)
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const WorkspaceDragOverlay = (): JSX.Element => {
|
|
1799
|
+
const label = createMemo(() => {
|
|
1800
|
+
const project = currentProject()
|
|
1801
|
+
if (!project) return
|
|
1802
|
+
const directory = store.activeWorkspace
|
|
1803
|
+
if (!directory) return
|
|
1804
|
+
|
|
1805
|
+
const [workspaceStore] = globalSync.child(directory)
|
|
1806
|
+
const kind =
|
|
1807
|
+
directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
|
1808
|
+
const name = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
|
|
1809
|
+
return `${kind} : ${name}`
|
|
1810
|
+
})
|
|
1811
|
+
|
|
1812
|
+
return (
|
|
1813
|
+
<Show when={label()}>
|
|
1814
|
+
{(value) => (
|
|
1815
|
+
<div class="bg-background-base rounded-md px-2 py-1 text-14-medium text-text-strong">{value()}</div>
|
|
1816
|
+
)}
|
|
1817
|
+
</Show>
|
|
1818
|
+
)
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => {
|
|
1822
|
+
const sortable = createSortable(props.directory)
|
|
1823
|
+
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
|
|
1824
|
+
const [menuOpen, setMenuOpen] = createSignal(false)
|
|
1825
|
+
const [pendingRename, setPendingRename] = createSignal(false)
|
|
1826
|
+
const slug = createMemo(() => base64Encode(props.directory))
|
|
1827
|
+
const sessions = createMemo(() =>
|
|
1828
|
+
workspaceStore.session
|
|
1829
|
+
.filter((session) => session.directory === workspaceStore.path.directory)
|
|
1830
|
+
.filter((session) => !session.parentID && !session.time?.archived)
|
|
1831
|
+
.toSorted(sortSessions),
|
|
1832
|
+
)
|
|
1833
|
+
const local = createMemo(() => props.directory === props.project.worktree)
|
|
1834
|
+
const active = createMemo(() => {
|
|
1835
|
+
const current = params.dir ? base64Decode(params.dir) : ""
|
|
1836
|
+
return current === props.directory
|
|
1837
|
+
})
|
|
1838
|
+
const workspaceValue = createMemo(() => {
|
|
1839
|
+
const branch = workspaceStore.vcs?.branch
|
|
1840
|
+
const name = branch ?? getFilename(props.directory)
|
|
1841
|
+
return workspaceName(props.directory, props.project.id, branch) ?? name
|
|
1842
|
+
})
|
|
1843
|
+
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local())
|
|
1844
|
+
const boot = createMemo(() => open() || active())
|
|
1845
|
+
const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
|
|
1846
|
+
const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length)
|
|
1847
|
+
const busy = createMemo(() => isBusy(props.directory))
|
|
1848
|
+
const loadMore = async () => {
|
|
1849
|
+
if (!local()) return
|
|
1850
|
+
setWorkspaceStore("limit", (limit) => limit + 5)
|
|
1851
|
+
await globalSync.project.loadSessions(props.directory)
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
const workspaceEditActive = createMemo(() => editorOpen(`workspace:${props.directory}`))
|
|
1855
|
+
|
|
1856
|
+
const openWrapper = (value: boolean) => {
|
|
1857
|
+
setStore("workspaceExpanded", props.directory, value)
|
|
1858
|
+
if (value) return
|
|
1859
|
+
if (editorOpen(`workspace:${props.directory}`)) closeEditor()
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
createEffect(() => {
|
|
1863
|
+
if (!boot()) return
|
|
1864
|
+
globalSync.child(props.directory, { bootstrap: true })
|
|
1865
|
+
})
|
|
1866
|
+
|
|
1867
|
+
const header = () => (
|
|
1868
|
+
<div class="flex items-center gap-1 min-w-0 flex-1">
|
|
1869
|
+
<div class="flex items-center justify-center shrink-0 size-6">
|
|
1870
|
+
<Show when={busy()} fallback={<Icon name="branch" size="small" />}>
|
|
1871
|
+
<Spinner class="size-[15px]" />
|
|
1872
|
+
</Show>
|
|
1873
|
+
</div>
|
|
1874
|
+
<span class="text-14-medium text-text-base shrink-0">
|
|
1875
|
+
{local() ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")} :
|
|
1876
|
+
</span>
|
|
1877
|
+
<Show
|
|
1878
|
+
when={!local()}
|
|
1879
|
+
fallback={
|
|
1880
|
+
<span class="text-14-medium text-text-base min-w-0 truncate">
|
|
1881
|
+
{workspaceStore.vcs?.branch ?? getFilename(props.directory)}
|
|
1882
|
+
</span>
|
|
1883
|
+
}
|
|
1884
|
+
>
|
|
1885
|
+
<InlineEditor
|
|
1886
|
+
id={`workspace:${props.directory}`}
|
|
1887
|
+
value={workspaceValue}
|
|
1888
|
+
onSave={(next) => {
|
|
1889
|
+
const trimmed = next.trim()
|
|
1890
|
+
if (!trimmed) return
|
|
1891
|
+
renameWorkspace(props.directory, trimmed, props.project.id, workspaceStore.vcs?.branch)
|
|
1892
|
+
setEditor("value", workspaceValue())
|
|
1893
|
+
}}
|
|
1894
|
+
class="text-14-medium text-text-base min-w-0 truncate"
|
|
1895
|
+
displayClass="text-14-medium text-text-base min-w-0 truncate"
|
|
1896
|
+
editing={workspaceEditActive()}
|
|
1897
|
+
stopPropagation={false}
|
|
1898
|
+
openOnDblClick={false}
|
|
1899
|
+
/>
|
|
1900
|
+
</Show>
|
|
1901
|
+
<Icon
|
|
1902
|
+
name={open() ? "chevron-down" : "chevron-right"}
|
|
1903
|
+
size="small"
|
|
1904
|
+
class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/workspace:opacity-100 group-focus-within/workspace:opacity-100"
|
|
1905
|
+
/>
|
|
1906
|
+
</div>
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
return (
|
|
1910
|
+
<div
|
|
1911
|
+
// @ts-ignore
|
|
1912
|
+
use:sortable
|
|
1913
|
+
classList={{
|
|
1914
|
+
"opacity-30": sortable.isActiveDraggable,
|
|
1915
|
+
"opacity-50 pointer-events-none": busy(),
|
|
1916
|
+
}}
|
|
1917
|
+
>
|
|
1918
|
+
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
|
|
1919
|
+
<div class="px-2 py-1">
|
|
1920
|
+
<div class="group/workspace relative">
|
|
1921
|
+
<div class="flex items-center gap-1">
|
|
1922
|
+
<Show
|
|
1923
|
+
when={workspaceEditActive()}
|
|
1924
|
+
fallback={
|
|
1925
|
+
<Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover">
|
|
1926
|
+
{header()}
|
|
1927
|
+
</Collapsible.Trigger>
|
|
1928
|
+
}
|
|
1929
|
+
>
|
|
1930
|
+
<div class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md">{header()}</div>
|
|
1931
|
+
</Show>
|
|
1932
|
+
<div
|
|
1933
|
+
class="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5 transition-opacity"
|
|
1934
|
+
classList={{
|
|
1935
|
+
"opacity-100 pointer-events-auto": menuOpen(),
|
|
1936
|
+
"opacity-0 pointer-events-none": !menuOpen(),
|
|
1937
|
+
"group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto": true,
|
|
1938
|
+
"group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto": true,
|
|
1939
|
+
}}
|
|
1940
|
+
>
|
|
1941
|
+
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
|
|
1942
|
+
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
|
1943
|
+
<DropdownMenu.Trigger
|
|
1944
|
+
as={IconButton}
|
|
1945
|
+
icon="dot-grid"
|
|
1946
|
+
variant="ghost"
|
|
1947
|
+
class="size-6 rounded-md"
|
|
1948
|
+
aria-label={language.t("common.moreOptions")}
|
|
1949
|
+
/>
|
|
1950
|
+
</Tooltip>
|
|
1951
|
+
<DropdownMenu.Portal>
|
|
1952
|
+
<DropdownMenu.Content
|
|
1953
|
+
onCloseAutoFocus={(event) => {
|
|
1954
|
+
if (!pendingRename()) return
|
|
1955
|
+
event.preventDefault()
|
|
1956
|
+
setPendingRename(false)
|
|
1957
|
+
openEditor(`workspace:${props.directory}`, workspaceValue())
|
|
1958
|
+
}}
|
|
1959
|
+
>
|
|
1960
|
+
<DropdownMenu.Item
|
|
1961
|
+
disabled={local()}
|
|
1962
|
+
onSelect={() => {
|
|
1963
|
+
setPendingRename(true)
|
|
1964
|
+
setMenuOpen(false)
|
|
1965
|
+
}}
|
|
1966
|
+
>
|
|
1967
|
+
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
|
1968
|
+
</DropdownMenu.Item>
|
|
1969
|
+
<DropdownMenu.Item
|
|
1970
|
+
disabled={local() || busy()}
|
|
1971
|
+
onSelect={() => dialog.show(() => <DialogResetWorkspace directory={props.directory} />)}
|
|
1972
|
+
>
|
|
1973
|
+
<DropdownMenu.ItemLabel>{language.t("common.reset")}</DropdownMenu.ItemLabel>
|
|
1974
|
+
</DropdownMenu.Item>
|
|
1975
|
+
<DropdownMenu.Item
|
|
1976
|
+
disabled={local() || busy()}
|
|
1977
|
+
onSelect={() => dialog.show(() => <DialogDeleteWorkspace directory={props.directory} />)}
|
|
1978
|
+
>
|
|
1979
|
+
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
|
1980
|
+
</DropdownMenu.Item>
|
|
1981
|
+
</DropdownMenu.Content>
|
|
1982
|
+
</DropdownMenu.Portal>
|
|
1983
|
+
</DropdownMenu>
|
|
1984
|
+
</div>
|
|
1985
|
+
</div>
|
|
1986
|
+
</div>
|
|
1987
|
+
</div>
|
|
1988
|
+
|
|
1989
|
+
<Collapsible.Content>
|
|
1990
|
+
<nav class="flex flex-col gap-1 px-2">
|
|
1991
|
+
<Show when={workspaceSetting()}>
|
|
1992
|
+
<NewSessionItem slug={slug()} mobile={props.mobile} />
|
|
1993
|
+
</Show>
|
|
1994
|
+
<Show when={loading()}>
|
|
1995
|
+
<SessionSkeleton />
|
|
1996
|
+
</Show>
|
|
1997
|
+
<For each={sessions()}>
|
|
1998
|
+
{(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
|
|
1999
|
+
</For>
|
|
2000
|
+
<Show when={hasMore()}>
|
|
2001
|
+
<div class="relative w-full py-1">
|
|
2002
|
+
<Button
|
|
2003
|
+
variant="ghost"
|
|
2004
|
+
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
|
2005
|
+
size="large"
|
|
2006
|
+
onClick={(e: MouseEvent) => {
|
|
2007
|
+
loadMore()
|
|
2008
|
+
;(e.currentTarget as HTMLButtonElement).blur()
|
|
2009
|
+
}}
|
|
2010
|
+
>
|
|
2011
|
+
{language.t("common.loadMore")}
|
|
2012
|
+
</Button>
|
|
2013
|
+
</div>
|
|
2014
|
+
</Show>
|
|
2015
|
+
</nav>
|
|
2016
|
+
</Collapsible.Content>
|
|
2017
|
+
</Collapsible>
|
|
2018
|
+
</div>
|
|
2019
|
+
)
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
|
2023
|
+
const sortable = createSortable(props.project.worktree)
|
|
2024
|
+
const selected = createMemo(() => {
|
|
2025
|
+
const current = params.dir ? base64Decode(params.dir) : ""
|
|
2026
|
+
return props.project.worktree === current || props.project.sandboxes?.includes(current)
|
|
2027
|
+
})
|
|
2028
|
+
|
|
2029
|
+
const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2))
|
|
2030
|
+
const workspaceEnabled = createMemo(() => layout.sidebar.workspaces(props.project.worktree)())
|
|
2031
|
+
const [open, setOpen] = createSignal(false)
|
|
2032
|
+
|
|
2033
|
+
const label = (directory: string) => {
|
|
2034
|
+
const [data] = globalSync.child(directory)
|
|
2035
|
+
const kind =
|
|
2036
|
+
directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
|
2037
|
+
const name = workspaceLabel(directory, data.vcs?.branch, props.project.id)
|
|
2038
|
+
return `${kind} : ${name}`
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const sessions = (directory: string) => {
|
|
2042
|
+
const [data] = globalSync.child(directory)
|
|
2043
|
+
return data.session
|
|
2044
|
+
.filter((session) => session.directory === data.path.directory)
|
|
2045
|
+
.filter((session) => !session.parentID && !session.time?.archived)
|
|
2046
|
+
.toSorted(sortSessions)
|
|
2047
|
+
.slice(0, 2)
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
const projectSessions = () => {
|
|
2051
|
+
const [data] = globalSync.child(props.project.worktree)
|
|
2052
|
+
return data.session
|
|
2053
|
+
.filter((session) => session.directory === data.path.directory)
|
|
2054
|
+
.filter((session) => !session.parentID && !session.time?.archived)
|
|
2055
|
+
.toSorted(sortSessions)
|
|
2056
|
+
.slice(0, 2)
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
const projectName = () => props.project.name || getFilename(props.project.worktree)
|
|
2060
|
+
const trigger = (
|
|
2061
|
+
<button
|
|
2062
|
+
type="button"
|
|
2063
|
+
aria-label={projectName()}
|
|
2064
|
+
classList={{
|
|
2065
|
+
"flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
|
2066
|
+
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": selected(),
|
|
2067
|
+
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
|
2068
|
+
!selected() && !open(),
|
|
2069
|
+
"bg-surface-base-hover border border-border-weak-base": !selected() && open(),
|
|
2070
|
+
}}
|
|
2071
|
+
onClick={() => navigateToProject(props.project.worktree)}
|
|
2072
|
+
onBlur={() => setOpen(false)}
|
|
2073
|
+
>
|
|
2074
|
+
<ProjectIcon project={props.project} notify />
|
|
2075
|
+
</button>
|
|
2076
|
+
)
|
|
2077
|
+
|
|
2078
|
+
return (
|
|
2079
|
+
// @ts-ignore
|
|
2080
|
+
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
|
2081
|
+
<HoverCard
|
|
2082
|
+
open={open()}
|
|
2083
|
+
openDelay={0}
|
|
2084
|
+
closeDelay={0}
|
|
2085
|
+
placement="right-start"
|
|
2086
|
+
gutter={6}
|
|
2087
|
+
trigger={trigger}
|
|
2088
|
+
onOpenChange={(value) => {
|
|
2089
|
+
setOpen(value)
|
|
2090
|
+
if (value) setHoverSession(undefined)
|
|
2091
|
+
}}
|
|
2092
|
+
>
|
|
2093
|
+
<div class="-m-3 p-2 flex flex-col w-72">
|
|
2094
|
+
<div class="px-4 pt-2 pb-1 flex items-center gap-2">
|
|
2095
|
+
<div class="text-14-medium text-text-strong truncate grow">{displayName(props.project)}</div>
|
|
2096
|
+
<Tooltip value={language.t("common.close")} placement="top" gutter={6}>
|
|
2097
|
+
<IconButton
|
|
2098
|
+
icon="circle-x"
|
|
2099
|
+
variant="ghost"
|
|
2100
|
+
class="shrink-0"
|
|
2101
|
+
aria-label={language.t("common.close")}
|
|
2102
|
+
onClick={(event) => {
|
|
2103
|
+
event.stopPropagation()
|
|
2104
|
+
setOpen(false)
|
|
2105
|
+
closeProject(props.project.worktree)
|
|
2106
|
+
}}
|
|
2107
|
+
/>
|
|
2108
|
+
</Tooltip>
|
|
2109
|
+
</div>
|
|
2110
|
+
<div class="px-4 pb-2 text-12-medium text-text-weak">{language.t("sidebar.project.recentSessions")}</div>
|
|
2111
|
+
<div class="px-2 pb-2 flex flex-col gap-2">
|
|
2112
|
+
<Show
|
|
2113
|
+
when={workspaceEnabled()}
|
|
2114
|
+
fallback={
|
|
2115
|
+
<For each={projectSessions()}>
|
|
2116
|
+
{(session) => (
|
|
2117
|
+
<SessionItem
|
|
2118
|
+
session={session}
|
|
2119
|
+
slug={base64Encode(props.project.worktree)}
|
|
2120
|
+
dense
|
|
2121
|
+
mobile={props.mobile}
|
|
2122
|
+
popover={false}
|
|
2123
|
+
/>
|
|
2124
|
+
)}
|
|
2125
|
+
</For>
|
|
2126
|
+
}
|
|
2127
|
+
>
|
|
2128
|
+
<For each={workspaces()}>
|
|
2129
|
+
{(directory) => (
|
|
2130
|
+
<div class="flex flex-col gap-1">
|
|
2131
|
+
<div class="px-2 py-0.5 flex items-center gap-1 min-w-0">
|
|
2132
|
+
<div class="shrink-0 size-6 flex items-center justify-center">
|
|
2133
|
+
<Icon name="branch" size="small" class="text-icon-base" />
|
|
2134
|
+
</div>
|
|
2135
|
+
<span class="truncate text-14-medium text-text-base">{label(directory)}</span>
|
|
2136
|
+
</div>
|
|
2137
|
+
<For each={sessions(directory)}>
|
|
2138
|
+
{(session) => (
|
|
2139
|
+
<SessionItem
|
|
2140
|
+
session={session}
|
|
2141
|
+
slug={base64Encode(directory)}
|
|
2142
|
+
dense
|
|
2143
|
+
mobile={props.mobile}
|
|
2144
|
+
popover={false}
|
|
2145
|
+
/>
|
|
2146
|
+
)}
|
|
2147
|
+
</For>
|
|
2148
|
+
</div>
|
|
2149
|
+
)}
|
|
2150
|
+
</For>
|
|
2151
|
+
</Show>
|
|
2152
|
+
</div>
|
|
2153
|
+
<div class="px-2 py-2 border-t border-border-weak-base">
|
|
2154
|
+
<Button
|
|
2155
|
+
variant="ghost"
|
|
2156
|
+
class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
|
|
2157
|
+
onClick={() => {
|
|
2158
|
+
layout.sidebar.open()
|
|
2159
|
+
if (selected()) {
|
|
2160
|
+
setOpen(false)
|
|
2161
|
+
return
|
|
2162
|
+
}
|
|
2163
|
+
navigateToProject(props.project.worktree)
|
|
2164
|
+
}}
|
|
2165
|
+
>
|
|
2166
|
+
{language.t("sidebar.project.viewAllSessions")}
|
|
2167
|
+
</Button>
|
|
2168
|
+
</div>
|
|
2169
|
+
</div>
|
|
2170
|
+
</HoverCard>
|
|
2171
|
+
</div>
|
|
2172
|
+
)
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
const LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
|
2176
|
+
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.project.worktree)
|
|
2177
|
+
const slug = createMemo(() => base64Encode(props.project.worktree))
|
|
2178
|
+
const sessions = createMemo(() =>
|
|
2179
|
+
workspaceStore.session
|
|
2180
|
+
.filter((session) => session.directory === workspaceStore.path.directory)
|
|
2181
|
+
.filter((session) => !session.parentID && !session.time?.archived)
|
|
2182
|
+
.toSorted(sortSessions),
|
|
2183
|
+
)
|
|
2184
|
+
const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0)
|
|
2185
|
+
const hasMore = createMemo(() => workspaceStore.sessionTotal > workspaceStore.session.length)
|
|
2186
|
+
const loadMore = async () => {
|
|
2187
|
+
setWorkspaceStore("limit", (limit) => limit + 5)
|
|
2188
|
+
await globalSync.project.loadSessions(props.project.worktree)
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
return (
|
|
2192
|
+
<div
|
|
2193
|
+
ref={(el) => {
|
|
2194
|
+
if (!props.mobile) scrollContainerRef = el
|
|
2195
|
+
}}
|
|
2196
|
+
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar"
|
|
2197
|
+
style={{ "overflow-anchor": "none" }}
|
|
2198
|
+
>
|
|
2199
|
+
<nav class="flex flex-col gap-1 px-2">
|
|
2200
|
+
<Show when={workspaceSetting()}>
|
|
2201
|
+
<NewSessionItem slug={slug()} mobile={props.mobile} />
|
|
2202
|
+
</Show>
|
|
2203
|
+
<Show when={loading()}>
|
|
2204
|
+
<SessionSkeleton />
|
|
2205
|
+
</Show>
|
|
2206
|
+
<For each={sessions()}>
|
|
2207
|
+
{(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
|
|
2208
|
+
</For>
|
|
2209
|
+
<Show when={hasMore()}>
|
|
2210
|
+
<div class="relative w-full py-1">
|
|
2211
|
+
<Button
|
|
2212
|
+
variant="ghost"
|
|
2213
|
+
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
|
2214
|
+
size="large"
|
|
2215
|
+
onClick={(e: MouseEvent) => {
|
|
2216
|
+
loadMore()
|
|
2217
|
+
;(e.currentTarget as HTMLButtonElement).blur()
|
|
2218
|
+
}}
|
|
2219
|
+
>
|
|
2220
|
+
{language.t("common.loadMore")}
|
|
2221
|
+
</Button>
|
|
2222
|
+
</div>
|
|
2223
|
+
</Show>
|
|
2224
|
+
</nav>
|
|
2225
|
+
</div>
|
|
2226
|
+
)
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
|
|
2230
|
+
const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
|
|
2231
|
+
|
|
2232
|
+
const sync = useGlobalSync()
|
|
2233
|
+
const project = createMemo(() => currentProject())
|
|
2234
|
+
const projectName = createMemo(() => {
|
|
2235
|
+
const current = project()
|
|
2236
|
+
if (!current) return ""
|
|
2237
|
+
return current.name || getFilename(current.worktree)
|
|
2238
|
+
})
|
|
2239
|
+
const projectId = createMemo(() => project()?.id ?? "")
|
|
2240
|
+
const workspaces = createMemo(() => workspaceIds(project()))
|
|
2241
|
+
|
|
2242
|
+
const createWorkspace = async () => {
|
|
2243
|
+
const current = project()
|
|
2244
|
+
if (!current) return
|
|
2245
|
+
|
|
2246
|
+
const created = await globalSDK.client.worktree
|
|
2247
|
+
.create({ directory: current.worktree })
|
|
2248
|
+
.then((x) => x.data)
|
|
2249
|
+
.catch((err) => {
|
|
2250
|
+
showToast({
|
|
2251
|
+
title: language.t("workspace.create.failed.title"),
|
|
2252
|
+
description: errorMessage(err),
|
|
2253
|
+
})
|
|
2254
|
+
return undefined
|
|
2255
|
+
})
|
|
2256
|
+
|
|
2257
|
+
if (!created?.directory) return
|
|
2258
|
+
|
|
2259
|
+
setBusy(created.directory, true)
|
|
2260
|
+
WorktreeState.pending(created.directory)
|
|
2261
|
+
setStore("workspaceExpanded", created.directory, true)
|
|
2262
|
+
setStore("workspaceOrder", current.worktree, (prev) => {
|
|
2263
|
+
const existing = prev ?? []
|
|
2264
|
+
const local = current.worktree
|
|
2265
|
+
const next = existing.filter((d) => d !== local && d !== created.directory)
|
|
2266
|
+
return [local, created.directory, ...next]
|
|
2267
|
+
})
|
|
2268
|
+
|
|
2269
|
+
globalSync.child(created.directory)
|
|
2270
|
+
navigate(`/${base64Encode(created.directory)}/session`)
|
|
2271
|
+
layout.mobileSidebar.hide()
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
command.register(() => [
|
|
2275
|
+
{
|
|
2276
|
+
id: "workspace.new",
|
|
2277
|
+
title: language.t("workspace.new"),
|
|
2278
|
+
category: language.t("command.category.workspace"),
|
|
2279
|
+
keybind: "mod+shift+w",
|
|
2280
|
+
disabled: !layout.sidebar.workspaces(project()?.worktree ?? "")(),
|
|
2281
|
+
onSelect: createWorkspace,
|
|
2282
|
+
},
|
|
2283
|
+
])
|
|
2284
|
+
|
|
2285
|
+
const homedir = createMemo(() => sync.data.path.home)
|
|
2286
|
+
|
|
2287
|
+
return (
|
|
2288
|
+
<div class="flex h-full w-full overflow-hidden">
|
|
2289
|
+
<div class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden">
|
|
2290
|
+
<div class="flex-1 min-h-0 w-full">
|
|
2291
|
+
<DragDropProvider
|
|
2292
|
+
onDragStart={handleDragStart}
|
|
2293
|
+
onDragEnd={handleDragEnd}
|
|
2294
|
+
onDragOver={handleDragOver}
|
|
2295
|
+
collisionDetector={closestCenter}
|
|
2296
|
+
>
|
|
2297
|
+
<DragDropSensors />
|
|
2298
|
+
<ConstrainDragXAxis />
|
|
2299
|
+
<div class="h-full w-full flex flex-col items-center gap-3 px-3 py-2 overflow-y-auto no-scrollbar">
|
|
2300
|
+
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
|
|
2301
|
+
<For each={layout.projects.list()}>
|
|
2302
|
+
{(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
|
|
2303
|
+
</For>
|
|
2304
|
+
</SortableProvider>
|
|
2305
|
+
<Tooltip
|
|
2306
|
+
placement={sidebarProps.mobile ? "bottom" : "right"}
|
|
2307
|
+
value={
|
|
2308
|
+
<div class="flex items-center gap-2">
|
|
2309
|
+
<span>{language.t("command.project.open")}</span>
|
|
2310
|
+
<Show when={!sidebarProps.mobile}>
|
|
2311
|
+
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
|
|
2312
|
+
</Show>
|
|
2313
|
+
</div>
|
|
2314
|
+
}
|
|
2315
|
+
>
|
|
2316
|
+
<IconButton
|
|
2317
|
+
icon="plus"
|
|
2318
|
+
variant="ghost"
|
|
2319
|
+
size="large"
|
|
2320
|
+
onClick={chooseProject}
|
|
2321
|
+
aria-label={language.t("command.project.open")}
|
|
2322
|
+
/>
|
|
2323
|
+
</Tooltip>
|
|
2324
|
+
</div>
|
|
2325
|
+
<DragOverlay>
|
|
2326
|
+
<ProjectDragOverlay />
|
|
2327
|
+
</DragOverlay>
|
|
2328
|
+
</DragDropProvider>
|
|
2329
|
+
</div>
|
|
2330
|
+
<div class="shrink-0 w-full pt-3 pb-3 flex flex-col items-center gap-2">
|
|
2331
|
+
<TooltipKeybind
|
|
2332
|
+
placement={sidebarProps.mobile ? "bottom" : "right"}
|
|
2333
|
+
title={language.t("sidebar.settings")}
|
|
2334
|
+
keybind={command.keybind("settings.open")}
|
|
2335
|
+
>
|
|
2336
|
+
<IconButton
|
|
2337
|
+
icon="settings-gear"
|
|
2338
|
+
variant="ghost"
|
|
2339
|
+
size="large"
|
|
2340
|
+
onClick={openSettings}
|
|
2341
|
+
aria-label={language.t("sidebar.settings")}
|
|
2342
|
+
/>
|
|
2343
|
+
</TooltipKeybind>
|
|
2344
|
+
<Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value={language.t("sidebar.help")}>
|
|
2345
|
+
<IconButton
|
|
2346
|
+
icon="help"
|
|
2347
|
+
variant="ghost"
|
|
2348
|
+
size="large"
|
|
2349
|
+
onClick={() => platform.openLink("https://jonsoc.com/desktop-feedback")}
|
|
2350
|
+
aria-label={language.t("sidebar.help")}
|
|
2351
|
+
/>
|
|
2352
|
+
</Tooltip>
|
|
2353
|
+
</div>
|
|
2354
|
+
</div>
|
|
2355
|
+
|
|
2356
|
+
<Show when={expanded()}>
|
|
2357
|
+
<div
|
|
2358
|
+
classList={{
|
|
2359
|
+
"flex flex-col min-h-0 bg-background-stronger border border-b-0 border-border-weak-base rounded-tl-sm": true,
|
|
2360
|
+
"flex-1 min-w-0": sidebarProps.mobile,
|
|
2361
|
+
}}
|
|
2362
|
+
style={{ width: sidebarProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
|
|
2363
|
+
>
|
|
2364
|
+
<Show when={project()}>
|
|
2365
|
+
{(projectAccessor) => {
|
|
2366
|
+
const p = () => projectAccessor()
|
|
2367
|
+
return (
|
|
2368
|
+
<>
|
|
2369
|
+
<div class="shrink-0 px-2 py-1">
|
|
2370
|
+
<div class="group/project flex items-start justify-between gap-2 p-2 pr-1">
|
|
2371
|
+
<div class="flex flex-col min-w-0">
|
|
2372
|
+
<InlineEditor
|
|
2373
|
+
id={`project:${projectId()}`}
|
|
2374
|
+
value={projectName}
|
|
2375
|
+
onSave={(next) => project() && renameProject(project()!, next)}
|
|
2376
|
+
class="text-16-medium text-text-strong truncate"
|
|
2377
|
+
displayClass="text-16-medium text-text-strong truncate"
|
|
2378
|
+
stopPropagation
|
|
2379
|
+
/>
|
|
2380
|
+
|
|
2381
|
+
<Tooltip
|
|
2382
|
+
placement="bottom"
|
|
2383
|
+
gutter={2}
|
|
2384
|
+
value={project()?.worktree}
|
|
2385
|
+
class="shrink-0"
|
|
2386
|
+
contentStyle={{
|
|
2387
|
+
"max-width": "640px",
|
|
2388
|
+
transform: "translate3d(52px, 0, 0)",
|
|
2389
|
+
}}
|
|
2390
|
+
>
|
|
2391
|
+
<span class="text-12-regular text-text-base truncate select-text">
|
|
2392
|
+
{project()?.worktree.replace(homedir(), "~")}
|
|
2393
|
+
</span>
|
|
2394
|
+
</Tooltip>
|
|
2395
|
+
</div>
|
|
2396
|
+
|
|
2397
|
+
<DropdownMenu>
|
|
2398
|
+
<DropdownMenu.Trigger
|
|
2399
|
+
as={IconButton}
|
|
2400
|
+
icon="dot-grid"
|
|
2401
|
+
variant="ghost"
|
|
2402
|
+
class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
|
|
2403
|
+
aria-label={language.t("common.moreOptions")}
|
|
2404
|
+
/>
|
|
2405
|
+
<DropdownMenu.Portal>
|
|
2406
|
+
<DropdownMenu.Content class="mt-1">
|
|
2407
|
+
<DropdownMenu.Item
|
|
2408
|
+
onSelect={() => dialog.show(() => <DialogEditProject project={p()} />)}
|
|
2409
|
+
>
|
|
2410
|
+
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
|
2411
|
+
</DropdownMenu.Item>
|
|
2412
|
+
<DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces(p().worktree)}>
|
|
2413
|
+
<DropdownMenu.ItemLabel>
|
|
2414
|
+
{layout.sidebar.workspaces(p().worktree)()
|
|
2415
|
+
? language.t("sidebar.workspaces.disable")
|
|
2416
|
+
: language.t("sidebar.workspaces.enable")}
|
|
2417
|
+
</DropdownMenu.ItemLabel>
|
|
2418
|
+
</DropdownMenu.Item>
|
|
2419
|
+
<DropdownMenu.Separator />
|
|
2420
|
+
<DropdownMenu.Item onSelect={() => closeProject(p().worktree)}>
|
|
2421
|
+
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
|
2422
|
+
</DropdownMenu.Item>
|
|
2423
|
+
</DropdownMenu.Content>
|
|
2424
|
+
</DropdownMenu.Portal>
|
|
2425
|
+
</DropdownMenu>
|
|
2426
|
+
</div>
|
|
2427
|
+
</div>
|
|
2428
|
+
|
|
2429
|
+
<Show
|
|
2430
|
+
when={layout.sidebar.workspaces(p().worktree)()}
|
|
2431
|
+
fallback={
|
|
2432
|
+
<>
|
|
2433
|
+
<div class="py-4 px-3">
|
|
2434
|
+
<TooltipKeybind
|
|
2435
|
+
title={language.t("command.session.new")}
|
|
2436
|
+
keybind={command.keybind("session.new")}
|
|
2437
|
+
placement="top"
|
|
2438
|
+
>
|
|
2439
|
+
<Button
|
|
2440
|
+
size="large"
|
|
2441
|
+
icon="plus-small"
|
|
2442
|
+
class="w-full"
|
|
2443
|
+
onClick={() => {
|
|
2444
|
+
navigate(`/${base64Encode(p().worktree)}/session`)
|
|
2445
|
+
layout.mobileSidebar.hide()
|
|
2446
|
+
}}
|
|
2447
|
+
>
|
|
2448
|
+
{language.t("command.session.new")}
|
|
2449
|
+
</Button>
|
|
2450
|
+
</TooltipKeybind>
|
|
2451
|
+
</div>
|
|
2452
|
+
<div class="flex-1 min-h-0">
|
|
2453
|
+
<LocalWorkspace project={p()} mobile={sidebarProps.mobile} />
|
|
2454
|
+
</div>
|
|
2455
|
+
</>
|
|
2456
|
+
}
|
|
2457
|
+
>
|
|
2458
|
+
<>
|
|
2459
|
+
<div class="py-4 px-3">
|
|
2460
|
+
<TooltipKeybind
|
|
2461
|
+
title={language.t("workspace.new")}
|
|
2462
|
+
keybind={command.keybind("workspace.new")}
|
|
2463
|
+
placement="top"
|
|
2464
|
+
>
|
|
2465
|
+
<Button size="large" icon="plus-small" class="w-full" onClick={createWorkspace}>
|
|
2466
|
+
{language.t("workspace.new")}
|
|
2467
|
+
</Button>
|
|
2468
|
+
</TooltipKeybind>
|
|
2469
|
+
</div>
|
|
2470
|
+
<div class="relative flex-1 min-h-0">
|
|
2471
|
+
<DragDropProvider
|
|
2472
|
+
onDragStart={handleWorkspaceDragStart}
|
|
2473
|
+
onDragEnd={handleWorkspaceDragEnd}
|
|
2474
|
+
onDragOver={handleWorkspaceDragOver}
|
|
2475
|
+
collisionDetector={closestCenter}
|
|
2476
|
+
>
|
|
2477
|
+
<DragDropSensors />
|
|
2478
|
+
<ConstrainDragXAxis />
|
|
2479
|
+
<div
|
|
2480
|
+
ref={(el) => {
|
|
2481
|
+
if (!sidebarProps.mobile) scrollContainerRef = el
|
|
2482
|
+
}}
|
|
2483
|
+
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar"
|
|
2484
|
+
style={{ "overflow-anchor": "none" }}
|
|
2485
|
+
>
|
|
2486
|
+
<SortableProvider ids={workspaces()}>
|
|
2487
|
+
<For each={workspaces()}>
|
|
2488
|
+
{(directory) => (
|
|
2489
|
+
<SortableWorkspace
|
|
2490
|
+
directory={directory}
|
|
2491
|
+
project={p()}
|
|
2492
|
+
mobile={sidebarProps.mobile}
|
|
2493
|
+
/>
|
|
2494
|
+
)}
|
|
2495
|
+
</For>
|
|
2496
|
+
</SortableProvider>
|
|
2497
|
+
</div>
|
|
2498
|
+
<DragOverlay>
|
|
2499
|
+
<WorkspaceDragOverlay />
|
|
2500
|
+
</DragOverlay>
|
|
2501
|
+
</DragDropProvider>
|
|
2502
|
+
</div>
|
|
2503
|
+
</>
|
|
2504
|
+
</Show>
|
|
2505
|
+
</>
|
|
2506
|
+
)
|
|
2507
|
+
}}
|
|
2508
|
+
</Show>
|
|
2509
|
+
<Show when={providers.all().length > 0 && providers.paid().length === 0}>
|
|
2510
|
+
<div class="shrink-0 px-2 py-3 border-t border-border-weak-base">
|
|
2511
|
+
<div class="rounded-md bg-background-base shadow-xs-border-base">
|
|
2512
|
+
<div class="p-3 flex flex-col gap-2">
|
|
2513
|
+
<div class="text-12-medium text-text-strong">{language.t("sidebar.gettingStarted.title")}</div>
|
|
2514
|
+
<div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div>
|
|
2515
|
+
<div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</div>
|
|
2516
|
+
</div>
|
|
2517
|
+
<Button
|
|
2518
|
+
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3"
|
|
2519
|
+
size="large"
|
|
2520
|
+
icon="plus"
|
|
2521
|
+
onClick={connectProvider}
|
|
2522
|
+
>
|
|
2523
|
+
{language.t("command.provider.connect")}
|
|
2524
|
+
</Button>
|
|
2525
|
+
</div>
|
|
2526
|
+
</div>
|
|
2527
|
+
</Show>
|
|
2528
|
+
</div>
|
|
2529
|
+
</Show>
|
|
2530
|
+
</div>
|
|
2531
|
+
)
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
return (
|
|
2535
|
+
<div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
|
2536
|
+
<Titlebar />
|
|
2537
|
+
<div class="flex-1 min-h-0 flex">
|
|
2538
|
+
<nav
|
|
2539
|
+
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
|
2540
|
+
classList={{
|
|
2541
|
+
"hidden xl:block": true,
|
|
2542
|
+
"relative shrink-0": true,
|
|
2543
|
+
}}
|
|
2544
|
+
style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }}
|
|
2545
|
+
>
|
|
2546
|
+
<div class="@container w-full h-full contain-strict">
|
|
2547
|
+
<SidebarContent />
|
|
2548
|
+
</div>
|
|
2549
|
+
<Show when={layout.sidebar.opened()}>
|
|
2550
|
+
<ResizeHandle
|
|
2551
|
+
direction="horizontal"
|
|
2552
|
+
size={layout.sidebar.width()}
|
|
2553
|
+
min={244}
|
|
2554
|
+
max={window.innerWidth * 0.3 + 64}
|
|
2555
|
+
collapseThreshold={244}
|
|
2556
|
+
onResize={layout.sidebar.resize}
|
|
2557
|
+
onCollapse={layout.sidebar.close}
|
|
2558
|
+
/>
|
|
2559
|
+
</Show>
|
|
2560
|
+
</nav>
|
|
2561
|
+
<div class="xl:hidden">
|
|
2562
|
+
<div
|
|
2563
|
+
classList={{
|
|
2564
|
+
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
|
|
2565
|
+
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
|
|
2566
|
+
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
|
|
2567
|
+
}}
|
|
2568
|
+
onClick={(e) => {
|
|
2569
|
+
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
|
|
2570
|
+
}}
|
|
2571
|
+
/>
|
|
2572
|
+
<nav
|
|
2573
|
+
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
|
2574
|
+
classList={{
|
|
2575
|
+
"@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
|
|
2576
|
+
"translate-x-0": layout.mobileSidebar.opened(),
|
|
2577
|
+
"-translate-x-full": !layout.mobileSidebar.opened(),
|
|
2578
|
+
}}
|
|
2579
|
+
onClick={(e) => e.stopPropagation()}
|
|
2580
|
+
>
|
|
2581
|
+
<SidebarContent mobile />
|
|
2582
|
+
</nav>
|
|
2583
|
+
</div>
|
|
2584
|
+
|
|
2585
|
+
<main
|
|
2586
|
+
classList={{
|
|
2587
|
+
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base": true,
|
|
2588
|
+
"xl:border-l xl:rounded-tl-sm": !layout.sidebar.opened(),
|
|
2589
|
+
}}
|
|
2590
|
+
>
|
|
2591
|
+
<Show when={!autoselecting()} fallback={<div class="size-full" />}>
|
|
2592
|
+
{props.children}
|
|
2593
|
+
</Show>
|
|
2594
|
+
</main>
|
|
2595
|
+
</div>
|
|
2596
|
+
<Toast.Region />
|
|
2597
|
+
</div>
|
|
2598
|
+
)
|
|
2599
|
+
}
|