|
|
|
|
@ -12,6 +12,7 @@ import {
|
|
|
|
|
Show,
|
|
|
|
|
Switch,
|
|
|
|
|
untrack,
|
|
|
|
|
type Accessor,
|
|
|
|
|
type JSX,
|
|
|
|
|
} from "solid-js"
|
|
|
|
|
import { A, useNavigate, useParams } from "@solidjs/router"
|
|
|
|
|
@ -24,6 +25,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
|
|
|
|
import { Button } from "@opencode-ai/ui/button"
|
|
|
|
|
import { Icon } from "@opencode-ai/ui/icon"
|
|
|
|
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
|
|
|
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
|
|
|
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
|
|
|
|
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
|
|
|
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
|
|
|
@ -71,6 +73,7 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
activeProject: undefined as string | undefined,
|
|
|
|
|
activeWorkspace: undefined as string | undefined,
|
|
|
|
|
workspaceOrder: {} as Record<string, string[]>,
|
|
|
|
|
workspaceName: {} as Record<string, string>,
|
|
|
|
|
workspaceExpanded: {} as Record<string, boolean>,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
@ -106,6 +109,104 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
dark: "Dark",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [editor, setEditor] = createStore({
|
|
|
|
|
active: "" as string,
|
|
|
|
|
value: "",
|
|
|
|
|
})
|
|
|
|
|
const editorRef = { current: undefined as HTMLInputElement | undefined }
|
|
|
|
|
|
|
|
|
|
const editorOpen = (id: string) => editor.active === id
|
|
|
|
|
const editorValue = () => editor.value
|
|
|
|
|
|
|
|
|
|
const openEditor = (id: string, value: string) => {
|
|
|
|
|
if (!id) return
|
|
|
|
|
setEditor({ active: id, value })
|
|
|
|
|
queueMicrotask(() => editorRef.current?.focus())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const closeEditor = () => setEditor({ active: "", value: "" })
|
|
|
|
|
|
|
|
|
|
const saveEditor = (callback: (next: string) => void) => {
|
|
|
|
|
const next = editor.value.trim()
|
|
|
|
|
if (!next) {
|
|
|
|
|
closeEditor()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
closeEditor()
|
|
|
|
|
callback(next)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const editorKeyDown = (event: KeyboardEvent, callback: (next: string) => void) => {
|
|
|
|
|
if (event.key === "Enter") {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
saveEditor(callback)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (event.key === "Escape") {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
closeEditor()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const InlineEditor = (props: {
|
|
|
|
|
id: string
|
|
|
|
|
value: Accessor<string>
|
|
|
|
|
onSave: (next: string) => void
|
|
|
|
|
class?: string
|
|
|
|
|
displayClass?: string
|
|
|
|
|
editing?: boolean
|
|
|
|
|
stopPropagation?: boolean
|
|
|
|
|
openOnDblClick?: boolean
|
|
|
|
|
}) => {
|
|
|
|
|
const isEditing = () => props.editing ?? editorOpen(props.id)
|
|
|
|
|
const stopEvents = () => props.stopPropagation ?? false
|
|
|
|
|
const allowDblClick = () => props.openOnDblClick ?? true
|
|
|
|
|
const stopPropagation = (event: Event) => {
|
|
|
|
|
if (!stopEvents()) return
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
}
|
|
|
|
|
const handleDblClick = (event: MouseEvent) => {
|
|
|
|
|
if (!allowDblClick()) return
|
|
|
|
|
stopPropagation(event)
|
|
|
|
|
openEditor(props.id, props.value())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Show
|
|
|
|
|
when={isEditing()}
|
|
|
|
|
fallback={
|
|
|
|
|
<span
|
|
|
|
|
class={props.displayClass ?? props.class}
|
|
|
|
|
onDblClick={handleDblClick}
|
|
|
|
|
onPointerDown={stopPropagation}
|
|
|
|
|
onMouseDown={stopPropagation}
|
|
|
|
|
onClick={stopPropagation}
|
|
|
|
|
onTouchStart={stopPropagation}
|
|
|
|
|
>
|
|
|
|
|
{props.value()}
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<InlineInput
|
|
|
|
|
ref={(el) => {
|
|
|
|
|
editorRef.current = el
|
|
|
|
|
}}
|
|
|
|
|
value={editorValue()}
|
|
|
|
|
class={props.class}
|
|
|
|
|
onInput={(event) => setEditor("value", event.currentTarget.value)}
|
|
|
|
|
onKeyDown={(event) => editorKeyDown(event, props.onSave)}
|
|
|
|
|
onBlur={() => closeEditor()}
|
|
|
|
|
onPointerDown={stopPropagation}
|
|
|
|
|
onClick={stopPropagation}
|
|
|
|
|
onDblClick={stopPropagation}
|
|
|
|
|
onMouseDown={stopPropagation}
|
|
|
|
|
onMouseUp={stopPropagation}
|
|
|
|
|
onTouchStart={stopPropagation}
|
|
|
|
|
/>
|
|
|
|
|
</Show>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cycleTheme(direction = 1) {
|
|
|
|
|
const ids = availableThemeEntries().map(([id]) => id)
|
|
|
|
|
if (ids.length === 0) return
|
|
|
|
|
@ -299,6 +400,12 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const workspaceName = (directory: string) => store.workspaceName[directory]
|
|
|
|
|
const workspaceLabel = (directory: string, branch?: string) =>
|
|
|
|
|
workspaceName(directory) ?? branch ?? getFilename(directory)
|
|
|
|
|
|
|
|
|
|
const isWorkspaceEditing = () => editor.active.startsWith("workspace:")
|
|
|
|
|
|
|
|
|
|
const workspaceSetting = createMemo(() => {
|
|
|
|
|
const project = currentProject()
|
|
|
|
|
if (!project) return false
|
|
|
|
|
@ -700,6 +807,31 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
if (navigate) navigateToProject(directory)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
|
|
|
|
|
|
|
|
|
|
async function renameProject(project: LocalProject, next: string) {
|
|
|
|
|
if (!project.id) return
|
|
|
|
|
const current = displayName(project)
|
|
|
|
|
if (next === current) return
|
|
|
|
|
const name = next === getFilename(project.worktree) ? "" : next
|
|
|
|
|
await globalSDK.client.project.update({ projectID: project.id, name })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function renameSession(session: Session, next: string) {
|
|
|
|
|
if (next === session.title) return
|
|
|
|
|
await globalSDK.client.session.update({
|
|
|
|
|
directory: session.directory,
|
|
|
|
|
sessionID: session.id,
|
|
|
|
|
title: next,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renameWorkspace = (directory: string, next: string) => {
|
|
|
|
|
const current = workspaceName(directory) ?? getFilename(directory)
|
|
|
|
|
if (current === next) return
|
|
|
|
|
setStore("workspaceName", directory, next)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeProject(directory: string) {
|
|
|
|
|
const index = layout.projects.list().findIndex((x) => x.worktree === directory)
|
|
|
|
|
const next = layout.projects.list()[index + 1]
|
|
|
|
|
@ -953,9 +1085,14 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
</Match>
|
|
|
|
|
</Switch>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
|
|
|
|
{props.session.title}
|
|
|
|
|
</span>
|
|
|
|
|
<InlineEditor
|
|
|
|
|
id={`session:${props.session.id}`}
|
|
|
|
|
value={() => props.session.title}
|
|
|
|
|
onSave={(next) => renameSession(props.session, next)}
|
|
|
|
|
class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
|
|
|
|
displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
|
|
|
|
stopPropagation
|
|
|
|
|
/>
|
|
|
|
|
<Show when={props.session.summary}>
|
|
|
|
|
{(summary) => (
|
|
|
|
|
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
|
|
|
|
@ -993,6 +1130,174 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ProjectDragOverlay = (): JSX.Element => {
|
|
|
|
|
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeProject))
|
|
|
|
|
return (
|
|
|
|
|
<Show when={project()}>
|
|
|
|
|
{(p) => (
|
|
|
|
|
<div class="bg-background-base rounded-xl p-1">
|
|
|
|
|
<ProjectIcon project={p()} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Show>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const WorkspaceDragOverlay = (): JSX.Element => {
|
|
|
|
|
const label = createMemo(() => {
|
|
|
|
|
const project = currentProject()
|
|
|
|
|
if (!project) return
|
|
|
|
|
const directory = store.activeWorkspace
|
|
|
|
|
if (!directory) return
|
|
|
|
|
|
|
|
|
|
const [workspaceStore] = globalSync.child(directory)
|
|
|
|
|
const kind = directory === project.worktree ? "local" : "sandbox"
|
|
|
|
|
const name = workspaceLabel(directory, workspaceStore.vcs?.branch)
|
|
|
|
|
return `${kind} : ${name}`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Show when={label()}>
|
|
|
|
|
{(value) => (
|
|
|
|
|
<div class="bg-background-base rounded-md px-2 py-1 text-14-medium text-text-strong">{value()}</div>
|
|
|
|
|
)}
|
|
|
|
|
</Show>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => {
|
|
|
|
|
const sortable = createSortable(props.directory)
|
|
|
|
|
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory)
|
|
|
|
|
const slug = createMemo(() => base64Encode(props.directory))
|
|
|
|
|
const sessions = createMemo(() =>
|
|
|
|
|
workspaceStore.session
|
|
|
|
|
.filter((session) => session.directory === workspaceStore.path.directory)
|
|
|
|
|
.filter((session) => !session.parentID)
|
|
|
|
|
.toSorted(sortSessions),
|
|
|
|
|
)
|
|
|
|
|
const local = createMemo(() => props.directory === props.project.worktree)
|
|
|
|
|
const workspaceValue = createMemo(() => {
|
|
|
|
|
const name = workspaceStore.vcs?.branch ?? getFilename(props.directory)
|
|
|
|
|
return workspaceName(props.directory) ?? name
|
|
|
|
|
})
|
|
|
|
|
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? true)
|
|
|
|
|
const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
|
|
|
|
|
const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length)
|
|
|
|
|
const loadMore = async () => {
|
|
|
|
|
if (!local()) return
|
|
|
|
|
setWorkspaceStore("limit", (limit) => limit + 5)
|
|
|
|
|
await globalSync.project.loadSessions(props.directory)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const workspaceEditActive = createMemo(() => editorOpen(`workspace:${props.directory}`))
|
|
|
|
|
|
|
|
|
|
const openWrapper = (value: boolean) => {
|
|
|
|
|
setStore("workspaceExpanded", props.directory, value)
|
|
|
|
|
if (value) return
|
|
|
|
|
if (editorOpen(`workspace:${props.directory}`)) closeEditor()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
|
|
|
|
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
|
|
|
|
|
<div class="px-2 py-1">
|
|
|
|
|
<div class="group/trigger relative">
|
|
|
|
|
<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">
|
|
|
|
|
<div class="flex items-center gap-1 min-w-0">
|
|
|
|
|
<div class="flex items-center justify-center shrink-0 size-6">
|
|
|
|
|
<Icon name="branch" size="small" />
|
|
|
|
|
</div>
|
|
|
|
|
<span class="text-14-medium text-text-base shrink-0">{local() ? "local" : "sandbox"} :</span>
|
|
|
|
|
<Show
|
|
|
|
|
when={!local()}
|
|
|
|
|
fallback={
|
|
|
|
|
<span class="text-14-medium text-text-base">
|
|
|
|
|
{workspaceStore.vcs?.branch ?? getFilename(props.directory)}
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<InlineEditor
|
|
|
|
|
id={`workspace:${props.directory}`}
|
|
|
|
|
value={workspaceValue}
|
|
|
|
|
onSave={(next) => {
|
|
|
|
|
const trimmed = next.trim()
|
|
|
|
|
if (!trimmed) return
|
|
|
|
|
renameWorkspace(props.directory, trimmed)
|
|
|
|
|
setEditor("value", workspaceValue())
|
|
|
|
|
}}
|
|
|
|
|
class="text-14-medium text-text-base"
|
|
|
|
|
displayClass="text-14-medium text-text-base"
|
|
|
|
|
editing={workspaceEditActive()}
|
|
|
|
|
stopPropagation={false}
|
|
|
|
|
openOnDblClick={false}
|
|
|
|
|
/>
|
|
|
|
|
</Show>
|
|
|
|
|
<Icon
|
|
|
|
|
name={open() ? "chevron-down" : "chevron-right"}
|
|
|
|
|
size="small"
|
|
|
|
|
class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/trigger:opacity-100 group-focus-within/trigger:opacity-100"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</Collapsible.Trigger>
|
|
|
|
|
<div class="absolute right-1 top-1/2 -translate-y-1/2 hidden items-center gap-0.5 pointer-events-none group-hover/trigger:flex group-focus-within/trigger:flex">
|
|
|
|
|
<IconButton icon="dot-grid" variant="ghost" class="size-6 rounded-md pointer-events-auto" />
|
|
|
|
|
<TooltipKeybind
|
|
|
|
|
class="pointer-events-auto"
|
|
|
|
|
placement="right"
|
|
|
|
|
title="New session"
|
|
|
|
|
keybind={command.keybind("session.new")}
|
|
|
|
|
>
|
|
|
|
|
<IconButton
|
|
|
|
|
icon="plus-small"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
class="size-6 rounded-md"
|
|
|
|
|
onClick={() => navigate(`/${slug()}/session`)}
|
|
|
|
|
/>
|
|
|
|
|
</TooltipKeybind>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Collapsible.Content>
|
|
|
|
|
<nav class="flex flex-col gap-1 px-2">
|
|
|
|
|
<Button
|
|
|
|
|
as={A}
|
|
|
|
|
href={`${slug()}/session`}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="large"
|
|
|
|
|
icon="edit"
|
|
|
|
|
class="hidden _flex w-full text-left justify-start text-text-base rounded-md px-3"
|
|
|
|
|
>
|
|
|
|
|
New session
|
|
|
|
|
</Button>
|
|
|
|
|
<Show when={loading()}>
|
|
|
|
|
<SessionSkeleton />
|
|
|
|
|
</Show>
|
|
|
|
|
<For each={sessions()}>
|
|
|
|
|
{(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
|
|
|
|
|
</For>
|
|
|
|
|
<Show when={hasMore()}>
|
|
|
|
|
<div class="relative w-full py-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
|
|
|
|
size="large"
|
|
|
|
|
onClick={(e: MouseEvent) => {
|
|
|
|
|
loadMore()
|
|
|
|
|
;(e.currentTarget as HTMLButtonElement).blur()
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Load more
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</nav>
|
|
|
|
|
</Collapsible.Content>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
|
|
|
|
const sortable = createSortable(props.project.worktree)
|
|
|
|
|
const selected = createMemo(() => {
|
|
|
|
|
@ -1005,7 +1310,7 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
const label = (directory: string) => {
|
|
|
|
|
const [data] = globalSync.child(directory)
|
|
|
|
|
const kind = directory === props.project.worktree ? "local" : "sandbox"
|
|
|
|
|
const name = data.vcs?.branch ?? getFilename(directory)
|
|
|
|
|
const name = workspaceLabel(directory, data.vcs?.branch)
|
|
|
|
|
return `${kind} : ${name}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1103,148 +1408,6 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ProjectDragOverlay = (): JSX.Element => {
|
|
|
|
|
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeProject))
|
|
|
|
|
return (
|
|
|
|
|
<Show when={project()}>
|
|
|
|
|
{(p) => (
|
|
|
|
|
<div class="bg-background-base rounded-xl p-1">
|
|
|
|
|
<ProjectIcon project={p()} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Show>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const WorkspaceDragOverlay = (): JSX.Element => {
|
|
|
|
|
const label = createMemo(() => {
|
|
|
|
|
const project = currentProject()
|
|
|
|
|
if (!project) return
|
|
|
|
|
const directory = store.activeWorkspace
|
|
|
|
|
if (!directory) return
|
|
|
|
|
|
|
|
|
|
const [workspaceStore] = globalSync.child(directory)
|
|
|
|
|
const kind = directory === project.worktree ? "local" : "sandbox"
|
|
|
|
|
const name = workspaceStore.vcs?.branch ?? getFilename(directory)
|
|
|
|
|
return `${kind} : ${name}`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Show when={label()}>
|
|
|
|
|
{(value) => (
|
|
|
|
|
<div class="bg-background-base rounded-md px-2 py-1 text-14-medium text-text-strong">{value()}</div>
|
|
|
|
|
)}
|
|
|
|
|
</Show>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => {
|
|
|
|
|
const sortable = createSortable(props.directory)
|
|
|
|
|
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory)
|
|
|
|
|
const slug = createMemo(() => base64Encode(props.directory))
|
|
|
|
|
const sessions = createMemo(() =>
|
|
|
|
|
workspaceStore.session
|
|
|
|
|
.filter((session) => session.directory === workspaceStore.path.directory)
|
|
|
|
|
.filter((session) => !session.parentID)
|
|
|
|
|
.toSorted(sortSessions),
|
|
|
|
|
)
|
|
|
|
|
const local = createMemo(() => props.directory === props.project.worktree)
|
|
|
|
|
const title = createMemo(() => {
|
|
|
|
|
const kind = local() ? "local" : "sandbox"
|
|
|
|
|
const name = workspaceStore.vcs?.branch ?? getFilename(props.directory)
|
|
|
|
|
return `${kind} : ${name}`
|
|
|
|
|
})
|
|
|
|
|
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? true)
|
|
|
|
|
const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
|
|
|
|
|
const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length)
|
|
|
|
|
const loadMore = async () => {
|
|
|
|
|
if (!local()) return
|
|
|
|
|
setWorkspaceStore("limit", (limit) => limit + 5)
|
|
|
|
|
await globalSync.project.loadSessions(props.directory)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
|
|
|
|
<Collapsible
|
|
|
|
|
variant="ghost"
|
|
|
|
|
open={open()}
|
|
|
|
|
class="shrink-0"
|
|
|
|
|
onOpenChange={(value) => setStore("workspaceExpanded", props.directory, value)}
|
|
|
|
|
>
|
|
|
|
|
<div class="px-2 py-1">
|
|
|
|
|
<div class="group/trigger relative">
|
|
|
|
|
<Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-2 py-1.5 rounded-md hover:bg-surface-raised-base-hover transition-all group-hover/trigger:pr-16 group-focus-within/trigger:pr-16">
|
|
|
|
|
<div class="flex items-center gap-1 min-w-0">
|
|
|
|
|
<div class="flex items-center justify-center shrink-0 size-6">
|
|
|
|
|
<Icon name="branch" size="small" />
|
|
|
|
|
</div>
|
|
|
|
|
<span class="truncate text-14-medium text-text-base">{title()}</span>
|
|
|
|
|
<Icon
|
|
|
|
|
name={open() ? "chevron-down" : "chevron-right"}
|
|
|
|
|
size="small"
|
|
|
|
|
class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/trigger:opacity-100 group-focus-within/trigger:opacity-100"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</Collapsible.Trigger>
|
|
|
|
|
<div class="absolute right-1 top-1/2 -translate-y-1/2 hidden items-center gap-0.5 pointer-events-none group-hover/trigger:flex group-focus-within/trigger:flex">
|
|
|
|
|
<IconButton icon="dot-grid" variant="ghost" class="size-6 rounded-md pointer-events-auto" />
|
|
|
|
|
<TooltipKeybind
|
|
|
|
|
class="pointer-events-auto"
|
|
|
|
|
placement="right"
|
|
|
|
|
title="New session"
|
|
|
|
|
keybind={command.keybind("session.new")}
|
|
|
|
|
>
|
|
|
|
|
<IconButton
|
|
|
|
|
icon="plus-small"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
class="size-6 rounded-md"
|
|
|
|
|
onClick={() => navigate(`/${slug()}/session`)}
|
|
|
|
|
/>
|
|
|
|
|
</TooltipKeybind>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Collapsible.Content>
|
|
|
|
|
<nav class="flex flex-col gap-1 px-2">
|
|
|
|
|
<Button
|
|
|
|
|
as={A}
|
|
|
|
|
href={`${slug()}/session`}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="large"
|
|
|
|
|
icon="edit"
|
|
|
|
|
class="hidden _flex w-full text-left justify-start text-text-base rounded-md px-3"
|
|
|
|
|
>
|
|
|
|
|
New session
|
|
|
|
|
</Button>
|
|
|
|
|
<Show when={loading()}>
|
|
|
|
|
<SessionSkeleton />
|
|
|
|
|
</Show>
|
|
|
|
|
<For each={sessions()}>
|
|
|
|
|
{(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
|
|
|
|
|
</For>
|
|
|
|
|
<Show when={hasMore()}>
|
|
|
|
|
<div class="relative w-full py-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
|
|
|
|
size="large"
|
|
|
|
|
onClick={(e: MouseEvent) => {
|
|
|
|
|
loadMore()
|
|
|
|
|
;(e.currentTarget as HTMLButtonElement).blur()
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Load more
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</nav>
|
|
|
|
|
</Collapsible.Content>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
|
|
|
|
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.project.worktree)
|
|
|
|
|
const slug = createMemo(() => base64Encode(props.project.worktree))
|
|
|
|
|
@ -1306,6 +1469,7 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
if (!current) return ""
|
|
|
|
|
return current.name || getFilename(current.worktree)
|
|
|
|
|
})
|
|
|
|
|
const projectId = createMemo(() => project()?.id ?? "")
|
|
|
|
|
const workspaces = createMemo(() => workspaceIds(project()))
|
|
|
|
|
|
|
|
|
|
const errorMessage = (err: unknown) => {
|
|
|
|
|
@ -1406,13 +1570,22 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
<div class="shrink-0 px-2 py-1">
|
|
|
|
|
<div class="group/project flex items-start justify-between gap-2 p-2 pr-1">
|
|
|
|
|
<div class="flex flex-col min-w-0">
|
|
|
|
|
<span class="text-16-medium text-text-strong truncate">{projectName()}</span>
|
|
|
|
|
<InlineEditor
|
|
|
|
|
id={`project:${projectId()}`}
|
|
|
|
|
value={projectName}
|
|
|
|
|
onSave={(next) => project() && renameProject(project()!, next)}
|
|
|
|
|
class="text-16-medium text-text-strong truncate"
|
|
|
|
|
displayClass="text-16-medium text-text-strong truncate"
|
|
|
|
|
stopPropagation
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Tooltip placement="right" value={project()?.worktree} class="shrink-0">
|
|
|
|
|
<span class="text-12-regular text-text-base truncate">
|
|
|
|
|
{project()?.worktree.replace(homedir(), "~")}
|
|
|
|
|
</span>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenu.Trigger
|
|
|
|
|
as={IconButton}
|
|
|
|
|
@ -1469,7 +1642,7 @@ export default function Layout(props: ParentProps) {
|
|
|
|
|
New workspace
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 min-h-0">
|
|
|
|
|
<div class="relative flex-1 min-h-0">
|
|
|
|
|
<DragDropProvider
|
|
|
|
|
onDragStart={handleWorkspaceDragStart}
|
|
|
|
|
onDragEnd={handleWorkspaceDragEnd}
|
|
|
|
|
|