Merge branch 'dev' into desktop-poilsh-styles-ui-ux
This commit is contained in:
commit
6535556203
96 changed files with 3555 additions and 2689 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.23",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,26 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
|||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useFile } from "@/context/file"
|
||||
|
||||
type EntryType = "command" | "file"
|
||||
|
||||
type Entry = {
|
||||
id: string
|
||||
type: EntryType
|
||||
title: string
|
||||
description?: string
|
||||
keybind?: string
|
||||
category: "Commands" | "Files"
|
||||
option?: CommandOption
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function DialogSelectFile() {
|
||||
const command = useCommand()
|
||||
const layout = useLayout()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
|
|
@ -16,35 +31,148 @@ export function DialogSelectFile() {
|
|||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const common = ["session.new", "session.previous", "session.next", "terminal.toggle", "review.toggle"]
|
||||
const limit = 5
|
||||
|
||||
const allowed = createMemo(() =>
|
||||
command.options.filter(
|
||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
),
|
||||
)
|
||||
|
||||
const commandItem = (option: CommandOption): Entry => ({
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
keybind: option.keybind,
|
||||
category: "Commands",
|
||||
option,
|
||||
})
|
||||
|
||||
const fileItem = (path: string): Entry => ({
|
||||
id: "file:" + path,
|
||||
type: "file",
|
||||
title: path,
|
||||
category: "Files",
|
||||
path,
|
||||
})
|
||||
|
||||
const list = createMemo(() => allowed().map(commandItem))
|
||||
|
||||
const picks = createMemo(() => {
|
||||
const all = allowed()
|
||||
const order = new Map(common.map((id, index) => [id, index]))
|
||||
const picked = all.filter((option) => order.has(option.id))
|
||||
const base = picked.length ? picked : all.slice(0, limit)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
return sorted.map(commandItem)
|
||||
})
|
||||
|
||||
const recent = createMemo(() => {
|
||||
const all = tabs().all()
|
||||
const active = tabs().active()
|
||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const items: Entry[] = []
|
||||
|
||||
for (const item of order) {
|
||||
const path = file.pathFromTab(item)
|
||||
if (!path) continue
|
||||
if (seen.has(path)) continue
|
||||
seen.add(path)
|
||||
items.push(fileItem(path))
|
||||
}
|
||||
|
||||
return items.slice(0, limit)
|
||||
})
|
||||
|
||||
const items = async (filter: string) => {
|
||||
const query = filter.trim()
|
||||
setGrouped(query.length > 0)
|
||||
if (!query) return [...picks(), ...recent()]
|
||||
const files = await file.searchFiles(query)
|
||||
const entries = files.map(fileItem)
|
||||
return [...list(), ...entries]
|
||||
}
|
||||
|
||||
const handleMove = (item: Entry | undefined) => {
|
||||
state.cleanup?.()
|
||||
if (!item) return
|
||||
if (item.type !== "command") return
|
||||
state.cleanup = item.option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const open = (path: string) => {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
|
||||
const handleSelect = (item: Entry | undefined) => {
|
||||
if (!item) return
|
||||
state.committed = true
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
|
||||
if (!item.path) return
|
||||
open(item.path)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.committed) return
|
||||
state.cleanup?.()
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Select file">
|
||||
<Dialog title="Search">
|
||||
<List
|
||||
search={{ placeholder: "Search files", autofocus: true }}
|
||||
emptyMessage="No files found"
|
||||
items={file.searchFiles}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (path) {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
dialog.close()
|
||||
}}
|
||||
search={{ placeholder: "Search files and commands", autofocus: true }}
|
||||
emptyMessage="No results found"
|
||||
items={items}
|
||||
key={(item) => item.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(item) => (grouped() ? item.category : "")}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(i)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
||||
{(item) => (
|
||||
<Show
|
||||
when={item.type === "command"}
|
||||
fallback={
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(item.path ?? "")}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||
<Show when={item.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.keybind}>
|
||||
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(item.keybind ?? "")}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -54,17 +54,22 @@ export function SessionHeader() {
|
|||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] h-8 p-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="magnifying-glass" size="normal" class="icon-base" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate">Search {name()}</span>
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate" style={{ "line-height": 1 }}>
|
||||
Search {name()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={hotkey()}>
|
||||
{(keybind) => (
|
||||
<span class="shrink-0 flex items-center justify-center h-5 px-2 rounded-[2px] border border-border-weak-base bg-surface-base text-12-medium text-text-weak">
|
||||
<span
|
||||
class="shrink-0 flex items-center justify-center h-5 px-2 rounded-[2px] bg-surface-base text-12-medium text-text-weak"
|
||||
style={{ "box-shadow": "var(--shadow-xxs-border)" }}
|
||||
>
|
||||
{keybind()}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export function Titlebar() {
|
|||
const reserve = createMemo(
|
||||
() => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
|
||||
)
|
||||
const web = createMemo(() => platform.platform === "web")
|
||||
|
||||
const getWin = () => {
|
||||
if (platform.platform !== "desktop") return
|
||||
|
|
@ -88,7 +89,7 @@ export function Titlebar() {
|
|||
onClick={layout.mobileSidebar.toggle}
|
||||
/>
|
||||
<TooltipKeybind
|
||||
class="hidden xl:flex shrink-0 ml-14"
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
|
||||
placement="bottom"
|
||||
title="Toggle sidebar"
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
|
||||
import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
|
||||
|
|
@ -114,67 +111,11 @@ export function formatKeybind(config: string): string {
|
|||
return IS_MAC ? parts.join("") : parts.join("+")
|
||||
}
|
||||
|
||||
function DialogCommand(props: { options: CommandOption[] }) {
|
||||
const dialog = useDialog()
|
||||
let cleanup: (() => void) | void
|
||||
let committed = false
|
||||
|
||||
const handleMove = (option: CommandOption | undefined) => {
|
||||
cleanup?.()
|
||||
cleanup = option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const handleSelect = (option: CommandOption | undefined) => {
|
||||
if (option) {
|
||||
committed = true
|
||||
cleanup = undefined
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (!committed) {
|
||||
cleanup?.()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Commands">
|
||||
<List
|
||||
search={{ placeholder: "Search commands", autofocus: true }}
|
||||
emptyMessage="No commands found"
|
||||
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
|
||||
key={(x) => x?.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(x) => x.category ?? ""}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(option) => (
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
|
||||
<Show when={option.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{option.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={option.keybind}>
|
||||
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
|
||||
name: "Command",
|
||||
init: () => {
|
||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
||||
const dialog = useDialog()
|
||||
|
||||
const options = createMemo(() => {
|
||||
const seen = new Set<string>()
|
||||
|
|
@ -202,12 +143,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||
|
||||
const suspended = () => suspendCount() > 0
|
||||
|
||||
const showPalette = () => {
|
||||
if (!dialog.active) {
|
||||
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
|
||||
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showPalette = () => {
|
||||
run("file.open", "palette")
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (suspended()) return
|
||||
|
||||
|
|
@ -248,12 +196,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||
})
|
||||
},
|
||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
}
|
||||
run(id, source)
|
||||
},
|
||||
keybind(id: string) {
|
||||
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
|
||||
|
|
|
|||
|
|
@ -19,15 +19,29 @@ import {
|
|||
type QuestionRequest,
|
||||
createOpencodeClient,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { ErrorPage, type InitError } from "../pages/error"
|
||||
import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
getOwner,
|
||||
runWithOwner,
|
||||
useContext,
|
||||
onCleanup,
|
||||
onMount,
|
||||
type Accessor,
|
||||
type ParentProps,
|
||||
Switch,
|
||||
Match,
|
||||
} from "solid-js"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { usePlatform } from "./platform"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
type State = {
|
||||
status: "loading" | "partial" | "complete"
|
||||
|
|
@ -68,9 +82,18 @@ type State = {
|
|||
}
|
||||
}
|
||||
|
||||
type VcsCache = {
|
||||
store: Store<{ value: VcsInfo | undefined }>
|
||||
setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
|
||||
ready: Accessor<boolean>
|
||||
}
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
const vcsCache = new Map<string, VcsCache>()
|
||||
const [globalStore, setGlobalStore] = createStore<{
|
||||
ready: boolean
|
||||
error?: InitError
|
||||
|
|
@ -86,10 +109,19 @@ function createGlobalSync() {
|
|||
provider_auth: {},
|
||||
})
|
||||
|
||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||
function child(directory: string) {
|
||||
if (!directory) console.error("No directory provided")
|
||||
if (!children[directory]) {
|
||||
const cache = runWithOwner(owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||
createStore({ value: undefined as VcsInfo | undefined }),
|
||||
),
|
||||
)
|
||||
if (!cache) throw new Error("Failed to create persisted cache")
|
||||
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
|
||||
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
|
|
@ -107,14 +139,16 @@ function createGlobalSync() {
|
|||
question: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
vcs: undefined,
|
||||
vcs: cache[0].value,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
bootstrapInstance(directory)
|
||||
}
|
||||
return children[directory]
|
||||
const childStore = children[directory]
|
||||
if (!childStore) throw new Error("Failed to create store")
|
||||
return childStore
|
||||
}
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
|
|
@ -157,6 +191,8 @@ function createGlobalSync() {
|
|||
async function bootstrapInstance(directory: string) {
|
||||
if (!directory) return
|
||||
const [store, setStore] = child(directory)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (!cache) return
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
fetch: platform.fetch,
|
||||
|
|
@ -164,6 +200,13 @@ function createGlobalSync() {
|
|||
throwOnError: true,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!cache.ready()) return
|
||||
const cached = cache.store.value
|
||||
if (!cached?.branch) return
|
||||
setStore("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
const blockingRequests = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () =>
|
||||
|
|
@ -193,7 +236,11 @@ function createGlobalSync() {
|
|||
loadSessions(directory),
|
||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||
sdk.vcs.get().then((x) => {
|
||||
const next = x.data ?? store.vcs
|
||||
setStore("vcs", next)
|
||||
if (next?.branch) cache.setStore("value", next)
|
||||
}),
|
||||
sdk.permission.list().then((x) => {
|
||||
const grouped: Record<string, PermissionRequest[]> = {}
|
||||
for (const perm of x.data ?? []) {
|
||||
|
|
@ -406,7 +453,10 @@ function createGlobalSync() {
|
|||
break
|
||||
}
|
||||
case "vcs.branch.updated": {
|
||||
setStore("vcs", { branch: event.properties.branch })
|
||||
const next = { branch: event.properties.branch }
|
||||
setStore("vcs", next)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (cache) cache.setStore("value", next)
|
||||
break
|
||||
}
|
||||
case "permission.asked": {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
createStore({
|
||||
sidebar: {
|
||||
opened: false,
|
||||
width: 280,
|
||||
width: 344,
|
||||
workspaces: {} as Record<string, boolean>,
|
||||
workspacesDefault: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
|
|
@ -62,13 +63,18 @@ import { Titlebar } from "@/components/titlebar"
|
|||
import { useServer } from "@/context/server"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore] = createStore({
|
||||
lastSession: {} as { [directory: string]: string },
|
||||
activeProject: undefined as string | undefined,
|
||||
activeWorkspace: undefined as string | undefined,
|
||||
workspaceOrder: {} as Record<string, string[]>,
|
||||
workspaceExpanded: {} as Record<string, boolean>,
|
||||
})
|
||||
const [store, setStore, , ready] = persisted(
|
||||
Persist.global("layout.page", ["layout.page.v1"]),
|
||||
createStore({
|
||||
lastSession: {} as { [directory: string]: string },
|
||||
activeProject: undefined as string | undefined,
|
||||
activeWorkspace: undefined as string | undefined,
|
||||
workspaceOrder: {} as Record<string, string[]>,
|
||||
workspaceExpanded: {} as Record<string, boolean>,
|
||||
}),
|
||||
)
|
||||
|
||||
const pageReady = createMemo(() => ready())
|
||||
|
||||
let scrollContainerRef: HTMLDivElement | undefined
|
||||
const xlQuery = window.matchMedia("(min-width: 1280px)")
|
||||
|
|
@ -81,6 +87,7 @@ export default function Layout(props: ParentProps) {
|
|||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const layout = useLayout()
|
||||
const layoutReady = createMemo(() => layout.ready())
|
||||
const platform = usePlatform()
|
||||
const server = useServer()
|
||||
const notification = useNotification()
|
||||
|
|
@ -289,6 +296,8 @@ export default function Layout(props: ParentProps) {
|
|||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!pageReady()) return
|
||||
if (!layoutReady()) return
|
||||
const project = currentProject()
|
||||
if (!project) return
|
||||
|
||||
|
|
@ -313,6 +322,16 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!pageReady()) return
|
||||
if (!layoutReady()) return
|
||||
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
||||
if (layout.sidebar.workspaces(directory)()) continue
|
||||
if (!expanded) continue
|
||||
setStore("workspaceExpanded", directory, false)
|
||||
}
|
||||
})
|
||||
|
||||
const currentSessions = createMemo(() => {
|
||||
const project = currentProject()
|
||||
if (!project) return [] as Session[]
|
||||
|
|
@ -703,12 +722,13 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!pageReady()) return
|
||||
if (!params.dir || !params.id) return
|
||||
const directory = base64Decode(params.dir)
|
||||
const id = params.id
|
||||
setStore("lastSession", directory, id)
|
||||
notification.session.markViewed(id)
|
||||
untrack(() => setStore("workspaceExpanded", directory, true))
|
||||
untrack(() => setStore("workspaceExpanded", directory, (value) => value ?? true))
|
||||
requestAnimationFrame(() => scrollToSession(id))
|
||||
})
|
||||
|
||||
|
|
@ -823,7 +843,7 @@ export default function Layout(props: ParentProps) {
|
|||
iconUrl={props.project.icon?.url}
|
||||
iconColor={props.project.icon?.color}
|
||||
size="small"
|
||||
class={`size-full ${props.class ?? ""}`}
|
||||
class="size-full rounded"
|
||||
style={
|
||||
notifications().length > 0 && props.notify
|
||||
? { "-webkit-mask-image": mask, "mask-image": mask }
|
||||
|
|
@ -881,7 +901,7 @@ export default function Layout(props: ParentProps) {
|
|||
return (
|
||||
<div
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors px-3
|
||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||
>
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={16} openDelay={1000}>
|
||||
|
|
@ -896,9 +916,9 @@ export default function Layout(props: ParentProps) {
|
|||
class="shrink-0 size-6 flex items-center justify-center"
|
||||
style={{ color: tint() ?? "var(--icon-interactive-base)" }}
|
||||
>
|
||||
<Switch>
|
||||
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
||||
<Match when={isWorking()}>
|
||||
<Spinner class="size-[15px] opacity-50" />
|
||||
<Spinner class="size-[15px]" />
|
||||
</Match>
|
||||
<Match when={hasPermissions()}>
|
||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
||||
|
|
@ -940,6 +960,17 @@ export default function Layout(props: ParentProps) {
|
|||
)
|
||||
}
|
||||
|
||||
const SessionSkeleton = (props: { count?: number }): JSX.Element => {
|
||||
const items = Array.from({ length: props.count ?? 4 }, (_, index) => index)
|
||||
return (
|
||||
<div class="flex flex-col gap-1">
|
||||
<For each={items}>
|
||||
{() => <div class="h-8 w-full rounded-md bg-surface-raised-base opacity-60 animate-pulse" />}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
||||
const sortable = createSortable(props.project.worktree)
|
||||
const selected = createMemo(() => {
|
||||
|
|
@ -978,9 +1009,10 @@ export default function Layout(props: ParentProps) {
|
|||
<button
|
||||
type="button"
|
||||
classList={{
|
||||
"flex items-center justify-center size-10 p-1 rounded-md border transition-colors cursor-default": true,
|
||||
"bg-transparent border-icon-strong-base hover:bg-surface-base-hover": selected(),
|
||||
"bg-transparent border-transparent hover:bg-surface-base-hover hover:border-border-weak-base": !selected(),
|
||||
"flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": selected(),
|
||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||
!selected(),
|
||||
}}
|
||||
onClick={() => navigateToProject(props.project.worktree)}
|
||||
>
|
||||
|
|
@ -1033,7 +1065,7 @@ export default function Layout(props: ParentProps) {
|
|||
<div class="px-2 py-2 border-t border-border-weak-base">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-text-base px-2"
|
||||
class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
|
||||
onClick={() => {
|
||||
layout.sidebar.open()
|
||||
navigateToProject(props.project.worktree)
|
||||
|
|
@ -1101,6 +1133,7 @@ export default function Layout(props: ParentProps) {
|
|||
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
|
||||
|
|
@ -1162,6 +1195,9 @@ export default function Layout(props: ParentProps) {
|
|||
>
|
||||
New session
|
||||
</Button>
|
||||
<Show when={loading()}>
|
||||
<SessionSkeleton />
|
||||
</Show>
|
||||
<For each={sessions()}>
|
||||
{(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
|
||||
</For>
|
||||
|
|
@ -1196,6 +1232,7 @@ export default function Layout(props: ParentProps) {
|
|||
.filter((session) => !session.parentID)
|
||||
.toSorted(sortSessions),
|
||||
)
|
||||
const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0)
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > workspaceStore.session.length)
|
||||
const loadMore = async () => {
|
||||
setWorkspaceStore("limit", (limit) => limit + 5)
|
||||
|
|
@ -1210,6 +1247,9 @@ export default function Layout(props: ParentProps) {
|
|||
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar"
|
||||
>
|
||||
<nav class="flex flex-col gap-1 px-2">
|
||||
<Show when={loading()}>
|
||||
<SessionSkeleton />
|
||||
</Show>
|
||||
<For each={sessions()}>
|
||||
{(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
|
||||
</For>
|
||||
|
|
@ -1493,7 +1533,9 @@ export default function Layout(props: ParentProps) {
|
|||
<div class="xl:hidden">
|
||||
<div
|
||||
classList={{
|
||||
"fixed inset-0 z-40 transition-opacity duration-200": true,
|
||||
"fixed inset-x-0 bottom-0 z-40 transition-opacity duration-200": true,
|
||||
"top-10": platform.platform === "desktop",
|
||||
"top-0": platform.platform !== "desktop",
|
||||
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
|
||||
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
|
|
@ -1503,7 +1545,9 @@ export default function Layout(props: ParentProps) {
|
|||
/>
|
||||
<div
|
||||
classList={{
|
||||
"@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"@container fixed bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"top-10": platform.platform === "desktop",
|
||||
"top-0": platform.platform !== "desktop",
|
||||
"translate-x-0": layout.mobileSidebar.opened(),
|
||||
"-translate-x-full": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
|
|
@ -1516,7 +1560,7 @@ export default function Layout(props: ParentProps) {
|
|||
<main
|
||||
classList={{
|
||||
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base": true,
|
||||
"border-l rounded-tl-sm": !layout.sidebar.opened(),
|
||||
"xl:border-l xl:rounded-tl-sm": !layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ export default function Page() {
|
|||
{
|
||||
id: "file.open",
|
||||
title: "Open file",
|
||||
description: "Search and open a file",
|
||||
description: "Search files and commands",
|
||||
category: "File",
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue