feat(tui): improve experimental session switcher (#30738)
This commit is contained in:
@@ -8,7 +8,8 @@ import { useSDK } from "@tui/context/sdk"
|
|||||||
import { useLocal } from "@tui/context/local"
|
import { useLocal } from "@tui/context/local"
|
||||||
import { useToast } from "@tui/ui/toast"
|
import { useToast } from "@tui/ui/toast"
|
||||||
import { useCommandShortcut } from "@tui/keymap"
|
import { useCommandShortcut } from "@tui/keymap"
|
||||||
import { createEffect, createMemo, createResource, createSignal, on, onMount, untrack } from "solid-js"
|
import { createEffect, createMemo, createResource, createSignal, on, Show, untrack } from "solid-js"
|
||||||
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { Spinner } from "@tui/component/spinner"
|
import { Spinner } from "@tui/component/spinner"
|
||||||
import { DialogSessionRename } from "@tui/component/dialog-session-rename"
|
import { DialogSessionRename } from "@tui/component/dialog-session-rename"
|
||||||
import { DialogSessionDeleteFailed } from "@tui/component/dialog-session-delete-failed"
|
import { DialogSessionDeleteFailed } from "@tui/component/dialog-session-delete-failed"
|
||||||
@@ -31,6 +32,7 @@ export function SessionSwitcherDialog() {
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const dimensions = useTerminalDimensions()
|
||||||
const [toDelete, setToDelete] = createSignal<string>()
|
const [toDelete, setToDelete] = createSignal<string>()
|
||||||
const [search, setSearch] = createDebouncedSignal("", 150)
|
const [search, setSearch] = createDebouncedSignal("", 150)
|
||||||
const deleteHint = useCommandShortcut("session.delete")
|
const deleteHint = useCommandShortcut("session.delete")
|
||||||
@@ -151,11 +153,6 @@ export function SessionSwitcherDialog() {
|
|||||||
if (!first || !last) return undefined
|
if (!first || !last) return undefined
|
||||||
return quickSwitchRange(first, last)
|
return quickSwitchRange(first, last)
|
||||||
})
|
})
|
||||||
const quickSwitchFooterHints = createMemo(() => {
|
|
||||||
const hint = quickSwitchHint()
|
|
||||||
return hint && local.session.slots().length > 0 ? [{ title: "switch", label: hint }] : []
|
|
||||||
})
|
|
||||||
|
|
||||||
const options = createMemo<DialogSelectOption<string>[]>(() => {
|
const options = createMemo<DialogSelectOption<string>[]>(() => {
|
||||||
const today = new Date().toDateString()
|
const today = new Date().toDateString()
|
||||||
const sessionMap = new Map(
|
const sessionMap = new Map(
|
||||||
@@ -183,10 +180,18 @@ export function SessionSwitcherDialog() {
|
|||||||
const status = sync.data.session_status?.[x.id]
|
const status = sync.data.session_status?.[x.id]
|
||||||
const isWorking = status?.type === "busy" || status?.type === "retry"
|
const isWorking = status?.type === "busy" || status?.type === "retry"
|
||||||
const slot = slotByID.get(x.id)
|
const slot = slotByID.get(x.id)
|
||||||
const gutter = isWorking
|
const gutter =
|
||||||
? () => <Spinner />
|
slot !== undefined || isWorking
|
||||||
: slot !== undefined
|
? () => (
|
||||||
? () => <text fg={theme.accent}>{slot}</text>
|
<box flexDirection="row" gap={1}>
|
||||||
|
<Show when={slot !== undefined}>
|
||||||
|
<text fg={theme.accent}>{slot}</text>
|
||||||
|
</Show>
|
||||||
|
<Show when={isWorking}>
|
||||||
|
<Spinner />
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
: undefined
|
: undefined
|
||||||
const titleText = isDeleting ? `Press ${deleteHint()} again to confirm` : isWorktree ? `⎇ ${x.title}` : x.title
|
const titleText = isDeleting ? `Press ${deleteHint()} again to confirm` : isWorktree ? `⎇ ${x.title}` : x.title
|
||||||
return {
|
return {
|
||||||
@@ -194,6 +199,17 @@ export function SessionSwitcherDialog() {
|
|||||||
bg: isDeleting ? theme.error : undefined,
|
bg: isDeleting ? theme.error : undefined,
|
||||||
value: x.id,
|
value: x.id,
|
||||||
category,
|
category,
|
||||||
|
categoryView:
|
||||||
|
category === "Pinned" ? (
|
||||||
|
<text>
|
||||||
|
<span style={{ fg: theme.accent }}>
|
||||||
|
<b>Pinned</b>
|
||||||
|
</span>
|
||||||
|
<Show when={quickSwitchHint()}>
|
||||||
|
{(hint) => <span style={{ fg: theme.textMuted }}> · switch {hint()}</span>}
|
||||||
|
</Show>
|
||||||
|
</text>
|
||||||
|
) : undefined,
|
||||||
footer,
|
footer,
|
||||||
gutter,
|
gutter,
|
||||||
}
|
}
|
||||||
@@ -224,8 +240,11 @@ export function SessionSwitcherDialog() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
onMount(() => {
|
const showPreview = createMemo(() => dimensions().width >= 100)
|
||||||
dialog.setSize("xlarge")
|
const height = createMemo(() => Math.max(8, Math.floor(dimensions().height / 2) - 4))
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
dialog.setSize(showPreview() ? "xlarge" : "large")
|
||||||
})
|
})
|
||||||
|
|
||||||
const list = (
|
const list = (
|
||||||
@@ -253,6 +272,7 @@ export function SessionSwitcherDialog() {
|
|||||||
title: "pin/unpin",
|
title: "pin/unpin",
|
||||||
onTrigger: (option: { value: string }) => {
|
onTrigger: (option: { value: string }) => {
|
||||||
local.session.togglePin(option.value)
|
local.session.togglePin(option.value)
|
||||||
|
queueMicrotask(() => select?.moveTo(option.value))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -311,19 +331,20 @@ export function SessionSwitcherDialog() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
footerHints={quickSwitchFooterHints()}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box flexDirection="row" width="100%">
|
<box flexDirection="row" width="100%" height={height()}>
|
||||||
<box flexBasis={68} flexShrink={0}>
|
<box flexBasis={showPreview() ? 68 : undefined} flexGrow={showPreview() ? 0 : 1} flexShrink={0}>
|
||||||
{list}
|
{list}
|
||||||
</box>
|
</box>
|
||||||
<box width={1} flexShrink={0} border={["left"]} borderColor={theme.borderSubtle} />
|
<Show when={showPreview()}>
|
||||||
<box flexGrow={1} flexShrink={1} flexDirection="column">
|
<box width={1} height={height() - 1} flexShrink={0} border={["left"]} borderColor={theme.borderSubtle} />
|
||||||
<SessionPreviewPane sessionID={focusedSession} session={focusedSessionInfo} />
|
<box flexGrow={1} flexShrink={1} flexDirection="column">
|
||||||
</box>
|
<SessionPreviewPane sessionID={focusedSession} session={focusedSessionInfo} />
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { createResource, Show, createMemo, createSignal, onMount, type Accessor, type JSX } from "solid-js"
|
import { createResource, Show, createMemo, createSignal, onMount, type Accessor, type JSX } from "solid-js"
|
||||||
import { TextAttributes, type RGBA } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { debounce, leadingAndTrailing } from "@solid-primitives/scheduled"
|
import { debounce, leadingAndTrailing } from "@solid-primitives/scheduled"
|
||||||
import type { Message, Part, Session as SdkSession, SnapshotFileDiff } from "@opencode-ai/sdk/v2"
|
import type { Message, Part, Session as SdkSession } from "@opencode-ai/sdk/v2"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
import { Spinner } from "@tui/component/spinner"
|
import { Spinner } from "@tui/component/spinner"
|
||||||
import { extractMessageMarkdown, extractMessageText, formatDiffSummary, relativeTime, shortModelLabel } from "./util"
|
import { extractMessageMarkdown, extractMessageText, relativeTime } from "./util"
|
||||||
|
|
||||||
type WithParts = { info: Message; parts: Part[] }
|
type WithParts = { info: Message; parts: Part[] }
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@ type Sdk = ReturnType<typeof useSDK>
|
|||||||
type Sync = ReturnType<typeof useSync>
|
type Sync = ReturnType<typeof useSync>
|
||||||
|
|
||||||
const messageCache = new Map<string, Promise<WithParts[]>>()
|
const messageCache = new Map<string, Promise<WithParts[]>>()
|
||||||
const diffCache = new Map<string, Promise<SnapshotFileDiff[]>>()
|
|
||||||
|
|
||||||
function cacheKey(sessionID: string, version: number) {
|
function cacheKey(sessionID: string, version: number) {
|
||||||
return `${sessionID}:${version}`
|
return `${sessionID}:${version}`
|
||||||
@@ -36,41 +35,21 @@ function loadMessages(sdk: Sdk, sessionID: string, version: number): Promise<Wit
|
|||||||
const promise = sdk.client.session
|
const promise = sdk.client.session
|
||||||
.messages({ sessionID, limit: 50 })
|
.messages({ sessionID, limit: 50 })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.error) messageCache.delete(key)
|
if (res.error) throw res.error
|
||||||
return (res.data as WithParts[] | undefined) ?? []
|
return (res.data as WithParts[] | undefined) ?? []
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
messageCache.delete(key)
|
messageCache.delete(key)
|
||||||
return [] as WithParts[]
|
throw error
|
||||||
})
|
})
|
||||||
messageCache.set(key, promise)
|
messageCache.set(key, promise)
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDiff(sdk: Sdk, sessionID: string, version: number): Promise<SnapshotFileDiff[]> {
|
|
||||||
const key = cacheKey(sessionID, version)
|
|
||||||
const cached = diffCache.get(key)
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const promise = sdk.client.session
|
|
||||||
.diff({ sessionID })
|
|
||||||
.then((res) => {
|
|
||||||
if (res.error) diffCache.delete(key)
|
|
||||||
return (res.data as SnapshotFileDiff[] | undefined) ?? []
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
diffCache.delete(key)
|
|
||||||
return [] as SnapshotFileDiff[]
|
|
||||||
})
|
|
||||||
diffCache.set(key, promise)
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prefetchPreviews(sdk: Sdk, sync: Sync, sessionIDs: readonly string[]) {
|
export function prefetchPreviews(sdk: Sdk, sync: Sync, sessionIDs: readonly string[]) {
|
||||||
for (const id of sessionIDs) {
|
for (const id of sessionIDs) {
|
||||||
const version = sync.data.session.find((session) => session.id === id)?.time.updated ?? 0
|
const version = sync.data.session.find((session) => session.id === id)?.time.updated ?? 0
|
||||||
if (!hydrateFromSync(sync, id)) loadMessages(sdk, id, version).catch(() => {})
|
if (!hydrateFromSync(sync, id)) loadMessages(sdk, id, version).catch(() => {})
|
||||||
if (!sync.data.session_diff[id]?.length) loadDiff(sdk, id, version).catch(() => {})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,13 +100,6 @@ export function SessionPreviewPane(props: {
|
|||||||
return hydrateFromSync(sync, id)
|
return hydrateFromSync(sync, id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const syncedDiff = createMemo(() => {
|
|
||||||
const id = props.sessionID()
|
|
||||||
if (!id) return undefined
|
|
||||||
const diff = sync.data.session_diff[id]
|
|
||||||
return diff && diff.length > 0 ? (diff as SnapshotFileDiff[]) : undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const [fetchedMessages] = createResource(
|
const [fetchedMessages] = createResource(
|
||||||
() => {
|
() => {
|
||||||
const id = props.sessionID()
|
const id = props.sessionID()
|
||||||
@@ -137,31 +109,7 @@ export function SessionPreviewPane(props: {
|
|||||||
async (input) => loadMessages(sdk, input.sessionID, input.version),
|
async (input) => loadMessages(sdk, input.sessionID, input.version),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [fetchedDiff] = createResource(
|
|
||||||
() => {
|
|
||||||
const id = props.sessionID()
|
|
||||||
if (!id || syncedDiff()) return undefined
|
|
||||||
return { sessionID: id, version: session()?.time.updated ?? 0 }
|
|
||||||
},
|
|
||||||
async (input) => loadDiff(sdk, input.sessionID, input.version),
|
|
||||||
)
|
|
||||||
|
|
||||||
const messages = createMemo(() => syncedMessages() ?? fetchedMessages() ?? [])
|
const messages = createMemo(() => syncedMessages() ?? fetchedMessages() ?? [])
|
||||||
const diff = createMemo(() => syncedDiff() ?? fetchedDiff() ?? [])
|
|
||||||
|
|
||||||
const diffSummary = createMemo(() => {
|
|
||||||
const live = diff()
|
|
||||||
if (live && live.length > 0) {
|
|
||||||
let additions = 0
|
|
||||||
let deletions = 0
|
|
||||||
for (const file of live) {
|
|
||||||
additions += file.additions ?? 0
|
|
||||||
deletions += file.deletions ?? 0
|
|
||||||
}
|
|
||||||
return formatDiffSummary({ additions, deletions, files: live.length })
|
|
||||||
}
|
|
||||||
return formatDiffSummary(session()?.summary)
|
|
||||||
})
|
|
||||||
|
|
||||||
const exchange = createMemo(() => {
|
const exchange = createMemo(() => {
|
||||||
const items = messages()
|
const items = messages()
|
||||||
@@ -174,13 +122,13 @@ export function SessionPreviewPane(props: {
|
|||||||
return { user, assistant }
|
return { user, assistant }
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = createMemo(() => (fetchedMessages.loading || fetchedDiff.loading) && !exchange())
|
const loading = createMemo(() => fetchedMessages.loading && !exchange())
|
||||||
|
|
||||||
const statusLabel = createMemo(() => {
|
const statusLabel = createMemo(() => {
|
||||||
const s = status()
|
const s = status()
|
||||||
if (s === "busy") return { text: "working", color: theme.warning }
|
if (s === "busy") return "working"
|
||||||
if (s === "retry") return { text: "retrying", color: theme.warning }
|
if (s === "retry") return "retrying"
|
||||||
return { text: "idle", color: theme.textMuted }
|
return "idle"
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -191,7 +139,7 @@ export function SessionPreviewPane(props: {
|
|||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
paddingBottom={1}
|
paddingBottom={1}
|
||||||
gap={1}
|
gap={1}
|
||||||
maxHeight={maxHeight()}
|
height={maxHeight()}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
@@ -204,7 +152,7 @@ export function SessionPreviewPane(props: {
|
|||||||
>
|
>
|
||||||
{(s) => (
|
{(s) => (
|
||||||
<>
|
<>
|
||||||
<Header session={s()} statusLabel={statusLabel()} diff={diffSummary()} />
|
<Header session={s()} statusLabel={statusLabel()} />
|
||||||
<Show when={loading()}>
|
<Show when={loading()}>
|
||||||
<Spinner>loading preview...</Spinner>
|
<Spinner>loading preview...</Spinner>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -213,7 +161,7 @@ export function SessionPreviewPane(props: {
|
|||||||
fallback={
|
fallback={
|
||||||
<Show when={!loading()}>
|
<Show when={!loading()}>
|
||||||
<text fg={theme.textMuted} wrapMode="word">
|
<text fg={theme.textMuted} wrapMode="word">
|
||||||
No messages yet
|
{fetchedMessages.error ? "Preview unavailable" : "No messages yet"}
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
@@ -241,24 +189,12 @@ function messageParentID(item: WithParts) {
|
|||||||
|
|
||||||
const ROW_WIDTH = 40
|
const ROW_WIDTH = 40
|
||||||
|
|
||||||
function Header(props: {
|
function Header(props: { session: SdkSession; statusLabel: string }) {
|
||||||
session: SdkSession
|
|
||||||
statusLabel: { text: string; color: RGBA }
|
|
||||||
diff: { additions: number; deletions: number; files: number } | undefined
|
|
||||||
}) {
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const title = createMemo(() => Locale.truncate(props.session.title, ROW_WIDTH))
|
const title = createMemo(() => Locale.truncate(props.session.title, ROW_WIDTH))
|
||||||
const modelAgent = createMemo(() => {
|
|
||||||
const m = shortModelLabel(props.session.model)
|
|
||||||
const a = props.session.agent ?? ""
|
|
||||||
if (m && a) return Locale.truncate(`${m} · ${a}`, ROW_WIDTH)
|
|
||||||
if (m) return Locale.truncate(m, ROW_WIDTH)
|
|
||||||
if (a) return Locale.truncate(a, ROW_WIDTH)
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
const statusRest = createMemo(() => {
|
const statusRest = createMemo(() => {
|
||||||
const joined = ` · ${relativeTime(props.session.time.updated)}`
|
const joined = ` · ${relativeTime(props.session.time.updated)}`
|
||||||
return Locale.truncate(joined, Math.max(0, ROW_WIDTH - props.statusLabel.text.length))
|
return Locale.truncate(joined, Math.max(0, ROW_WIDTH - props.statusLabel.length))
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -268,20 +204,12 @@ function Header(props: {
|
|||||||
{title()}
|
{title()}
|
||||||
</text>
|
</text>
|
||||||
</Row>
|
</Row>
|
||||||
<Show when={modelAgent()}>
|
|
||||||
<Row height={1}>
|
|
||||||
<text fg={theme.text} wrapMode="none" overflow="hidden">
|
|
||||||
{modelAgent()}
|
|
||||||
</text>
|
|
||||||
</Row>
|
|
||||||
</Show>
|
|
||||||
<Row height={1}>
|
<Row height={1}>
|
||||||
<text fg={theme.textMuted} wrapMode="none" overflow="hidden">
|
<text fg={theme.textMuted} wrapMode="none" overflow="hidden">
|
||||||
<span style={{ fg: props.statusLabel.color }}>{props.statusLabel.text}</span>
|
<span>{props.statusLabel}</span>
|
||||||
<span>{statusRest()}</span>
|
<span>{statusRest()}</span>
|
||||||
</text>
|
</text>
|
||||||
</Row>
|
</Row>
|
||||||
<Show when={props.diff}>{(d) => <DiffRow diff={d()} />}</Show>
|
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -294,28 +222,6 @@ function Row(props: { height: number; children: JSX.Element }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiffRow(props: { diff: { additions: number; deletions: number; files: number } }) {
|
|
||||||
const { theme } = useTheme()
|
|
||||||
const showAdds = () => props.diff.additions > 0
|
|
||||||
const showDels = () => props.diff.deletions > 0
|
|
||||||
if (!showAdds() && !showDels()) return null
|
|
||||||
return (
|
|
||||||
<Row height={1}>
|
|
||||||
<text wrapMode="none" overflow="hidden">
|
|
||||||
<Show when={showAdds()}>
|
|
||||||
<span style={{ fg: theme.diffAdded }}>+{props.diff.additions}</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={showAdds() && showDels()}>
|
|
||||||
<span> </span>
|
|
||||||
</Show>
|
|
||||||
<Show when={showDels()}>
|
|
||||||
<span style={{ fg: theme.diffRemoved }}>−{props.diff.deletions}</span>
|
|
||||||
</Show>
|
|
||||||
</text>
|
|
||||||
</Row>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const PROMPT_MAX_CHARS = 240
|
const PROMPT_MAX_CHARS = 240
|
||||||
const REPLY_MAX_LINES = 12
|
const REPLY_MAX_LINES = 12
|
||||||
const REPLY_MAX_CHARS = 800
|
const REPLY_MAX_CHARS = 800
|
||||||
|
|||||||
@@ -52,19 +52,3 @@ function collectTextParts(parts: readonly Part[]): string[] {
|
|||||||
}
|
}
|
||||||
return chunks
|
return chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDiffSummary(
|
|
||||||
summary: { additions: number; deletions: number; files: number } | undefined,
|
|
||||||
): { additions: number; deletions: number; files: number } | undefined {
|
|
||||||
if (!summary) return undefined
|
|
||||||
if (!summary.additions && !summary.deletions && !summary.files) return undefined
|
|
||||||
return summary
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shortModelLabel(model: { id: string; providerID?: string; variant?: string } | undefined): string {
|
|
||||||
if (!model) return ""
|
|
||||||
const id = model.id ?? ""
|
|
||||||
const stripped =
|
|
||||||
model.providerID && id.startsWith(`${model.providerID}/`) ? id.slice(model.providerID.length + 1) : id
|
|
||||||
return model.variant ? `${stripped} (${model.variant})` : stripped
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export type DialogSelectRef<T> = {
|
|||||||
filter: string
|
filter: string
|
||||||
filtered: DialogSelectOption<T>[]
|
filtered: DialogSelectOption<T>[]
|
||||||
selected: DialogSelectOption<T> | undefined
|
selected: DialogSelectOption<T> | undefined
|
||||||
|
moveTo(value: T): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||||
@@ -341,6 +342,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
get selected() {
|
get selected() {
|
||||||
return selected()
|
return selected()
|
||||||
},
|
},
|
||||||
|
moveTo(value) {
|
||||||
|
const index = flat().findIndex((option) => isDeepEqual(option.value, value))
|
||||||
|
if (index >= 0) moveTo(index, true)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
props.ref?.(ref)
|
props.ref?.(ref)
|
||||||
|
|
||||||
@@ -551,7 +556,7 @@ function Option(props: {
|
|||||||
●
|
●
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!props.current && props.gutter}>
|
<Show when={props.gutter}>
|
||||||
<box flexShrink={0} marginRight={0}>
|
<box flexShrink={0} marginRight={0}>
|
||||||
{props.gutter?.()}
|
{props.gutter?.()}
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
Reference in New Issue
Block a user