mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-05 00:16:22 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f1361a149 | ||
|
|
2516d8e976 |
@@ -34,8 +34,6 @@ export const ProviderTipSchema = Persistence.struct({
|
||||
dismissedAt: Schema.Finite,
|
||||
})
|
||||
|
||||
export const WorkspaceTipSchema = ProviderTipSchema
|
||||
|
||||
export function NewSessionView(props: {
|
||||
composer: ComposerModel
|
||||
project: PromptProjectController
|
||||
@@ -101,15 +99,7 @@ export function NewSessionView(props: {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NewSessionTips
|
||||
workspaceEligible={
|
||||
!!props.project.selected() &&
|
||||
props.workspace.bar.visible() &&
|
||||
props.workspace.selection.value() !== "create" &&
|
||||
props.workspace.project.managed() === 0
|
||||
}
|
||||
onWorkspace={() => select("create")}
|
||||
/>
|
||||
<ProviderTip />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -128,90 +118,56 @@ export function NewSessionStatus(props: { visible: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
function NewSessionTips(props: { workspaceEligible: boolean; onWorkspace: () => void }) {
|
||||
function ProviderTip() {
|
||||
const language = useLanguage()
|
||||
const dialog = useDialog()
|
||||
const sdk = useWorkspaceLocation()
|
||||
const providers = useProviders(() => sdk().directory)
|
||||
const [providerState, setProviderState, , providerReady] = persisted(
|
||||
const [persistedState, setPersistedState, , persistedReady] = persisted(
|
||||
Persist.global("new-session.provider-tip"),
|
||||
ProviderTipSchema,
|
||||
{ dismissedAt: 0 },
|
||||
)
|
||||
const [workspaceState, setWorkspaceState, , workspaceReady] = persisted(
|
||||
Persist.global("new-session.workspace-tip"),
|
||||
WorkspaceTipSchema,
|
||||
{ dismissedAt: 0 },
|
||||
)
|
||||
const workspaceVisible = createMemo(
|
||||
() =>
|
||||
props.workspaceEligible &&
|
||||
workspaceReady() &&
|
||||
Date.now() - workspaceState.dismissedAt >= providerTipDismissalDuration,
|
||||
)
|
||||
const providerVisible = createMemo(
|
||||
const visible = createMemo(
|
||||
() =>
|
||||
providers.ready() &&
|
||||
providerReady() &&
|
||||
persistedReady() &&
|
||||
providers.paid().length === 0 &&
|
||||
Date.now() - providerState.dismissedAt >= providerTipDismissalDuration,
|
||||
Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration,
|
||||
)
|
||||
const tip = createMemo<"workspace" | "provider" | undefined>(() => {
|
||||
if (providerVisible()) return "provider"
|
||||
if (workspaceVisible()) return "workspace"
|
||||
})
|
||||
const displayed = createMemo<"workspace" | "provider" | undefined>((previous) => tip() ?? previous)
|
||||
const [ref, setRef] = createSignal<HTMLDivElement>()
|
||||
const presence = createPresence({
|
||||
show: () => tip() !== undefined,
|
||||
show: visible,
|
||||
element: () => ref() ?? null,
|
||||
})
|
||||
const open = () => {
|
||||
const current = tip()
|
||||
if (!current) return
|
||||
if (current === "workspace") {
|
||||
setWorkspaceState("dismissedAt", Date.now())
|
||||
props.onWorkspace()
|
||||
return
|
||||
}
|
||||
const openProviders = () => {
|
||||
void import("@/providers/connect/dialog").then(({ DialogConnectProvider }) => {
|
||||
void dialog.show(() => <DialogConnectProvider directory={sdk().directory} />)
|
||||
})
|
||||
}
|
||||
const dismiss = () => {
|
||||
const current = tip()
|
||||
if (!current) return
|
||||
if (current === "workspace") {
|
||||
setWorkspaceState("dismissedAt", Date.now())
|
||||
return
|
||||
}
|
||||
setProviderState("dismissedAt", Date.now())
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={presence.present()}>
|
||||
<div class="pointer-events-none absolute inset-x-0 bottom-4 flex justify-center px-10">
|
||||
<div
|
||||
ref={setRef}
|
||||
data-component="new-session-tip"
|
||||
data-visible={tip() !== undefined}
|
||||
class="group/new-session-tip pointer-events-auto relative flex h-6 max-w-full items-center transition-[opacity,transform] duration-[250ms] ease-[cubic-bezier(0.215,0.61,0.355,1)] motion-reduce:transition-none"
|
||||
data-component="provider-tip"
|
||||
data-visible={visible()}
|
||||
class="group/provider-tip pointer-events-auto relative flex h-6 max-w-full items-center transition-[opacity,transform] duration-[250ms] ease-[cubic-bezier(0.215,0.61,0.355,1)] motion-reduce:transition-none"
|
||||
classList={{ "data-[visible=false]:animate-out fade-out slide-out-to-bottom-4": true }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-6 min-w-0 items-center rounded-[4px] pl-1.5 text-[13px] leading-text-compact tracking-[-0.04px] text-v2-text-text-faint transition-[background-color,color] duration-150 ease-in-out hover:bg-v2-overlay-simple-overlay-hover hover:text-v2-text-text-muted focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:text-v2-text-text-muted focus-visible:outline-none"
|
||||
onClick={open}
|
||||
onClick={openProviders}
|
||||
>
|
||||
<span class="truncate">
|
||||
{language.t(displayed() === "workspace" ? "home.workspaceTip" : "home.providerTip")}
|
||||
</span>
|
||||
<span class="truncate">{language.t("home.providerTip")}</span>
|
||||
<span class="flex size-6 shrink-0 items-center justify-center" aria-hidden="true">
|
||||
<Icon name="chevron-down" size="small" class="-rotate-90" />
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip
|
||||
class="hover-reveal absolute left-full top-0 flex h-6 w-7 items-center justify-end delay-0 duration-0 group-hover/new-session-tip:delay-[250ms] group-hover/new-session-tip:duration-150 group-hover/new-session-tip:opacity-100 focus-within:delay-0 focus-within:duration-0 focus-within:opacity-100"
|
||||
class="hover-reveal absolute left-full top-0 flex h-6 w-7 items-center justify-end delay-0 duration-0 group-hover/provider-tip:delay-[250ms] group-hover/provider-tip:duration-150 group-hover/provider-tip:opacity-100 focus-within:delay-0 focus-within:duration-0 focus-within:opacity-100"
|
||||
placement="top"
|
||||
openDelay={1000}
|
||||
value={language.t("common.dismiss")}
|
||||
@@ -220,7 +176,7 @@ function NewSessionTips(props: { workspaceEligible: boolean; onWorkspace: () =>
|
||||
type="button"
|
||||
class="flex size-6 items-center justify-center rounded-[4px] text-v2-icon-icon-muted transition-[background-color,color] duration-150 ease-in-out hover:bg-v2-overlay-simple-overlay-hover hover:text-v2-icon-icon-base focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:text-v2-icon-icon-base focus-visible:outline-none"
|
||||
aria-label={language.t("common.dismiss")}
|
||||
onClick={dismiss}
|
||||
onClick={() => setPersistedState("dismissedAt", Date.now())}
|
||||
>
|
||||
<Icon name="xmark-small" />
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { debounce } from "@solid-primitives/scheduled"
|
||||
import { createEffect, createMemo, createResource, onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, createResource } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useWorkspaceLocation } from "@/workspaces/location"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
@@ -11,8 +11,8 @@ import { normalizeProjectInfo } from "@/runtime/server/global-sync/utils"
|
||||
import {
|
||||
isWorkspaceDirectory,
|
||||
isWorkspaceSelection,
|
||||
sameDirectory,
|
||||
workspaceDefaultSelection,
|
||||
workspaceDirectories,
|
||||
workspaceSelectionDestination,
|
||||
} from "@/workspaces/paths"
|
||||
|
||||
@@ -56,45 +56,6 @@ export function createNewSessionWorkspaceController(input: {
|
||||
const current = projectID ? data.project.get(projectID) : undefined
|
||||
return current ? normalizeProjectInfo(current) : undefined
|
||||
})
|
||||
const [worktrees, worktreeActions] = createResource(
|
||||
() => currentProject()?.id,
|
||||
async (projectID) => ({
|
||||
projectID,
|
||||
items: await serverSDK.api.worktree
|
||||
.list({ projectID })
|
||||
.catch(() => (currentProject()?.id === projectID ? currentProject()?.worktrees : undefined) ?? []),
|
||||
}),
|
||||
)
|
||||
onCleanup(
|
||||
serverSDK.event.listen((event) => {
|
||||
if (event.type === "worktree.updated") void worktreeActions.refetch()
|
||||
}),
|
||||
)
|
||||
const worktreeItems = createMemo(() => {
|
||||
const project = currentProject()
|
||||
if (!project) return []
|
||||
const loaded = worktrees.latest
|
||||
return loaded?.projectID === project.id ? loaded.items : project.worktrees
|
||||
})
|
||||
const worktreeDirectories = createMemo(() => {
|
||||
const project = currentProject()
|
||||
if (!project) return []
|
||||
const directories = [
|
||||
...worktreeItems().map((item) => item.directory),
|
||||
...project.worktrees.map((item) => item.directory),
|
||||
...(project.sandboxes ?? []),
|
||||
]
|
||||
return directories
|
||||
.filter((directory) => !sameDirectory(project.worktree, directory))
|
||||
.filter((directory, index, items) => items.findIndex((item) => sameDirectory(item, directory)) === index)
|
||||
})
|
||||
const managedWorktrees = createMemo(() => {
|
||||
const project = currentProject()
|
||||
if (!project) return 0
|
||||
return worktreeItems().filter(
|
||||
(item) => item.strategy !== undefined && !sameDirectory(project.worktree, item.directory),
|
||||
).length
|
||||
})
|
||||
const visible = createMemo(() =>
|
||||
resolveNewSessionGit({
|
||||
projectVcs: currentProject()?.vcs,
|
||||
@@ -105,9 +66,7 @@ export function createNewSessionWorkspaceController(input: {
|
||||
const project = currentProject()
|
||||
const worktree = input.selectedWorktree()
|
||||
if (!project || !worktree) return
|
||||
return isWorkspaceSelection(project, worktree) || worktreeDirectories().some((item) => sameDirectory(item, worktree))
|
||||
? worktree
|
||||
: undefined
|
||||
return isWorkspaceSelection(project, worktree) ? worktree : undefined
|
||||
})
|
||||
const fallback = createMemo(() => {
|
||||
const project = currentProject()
|
||||
@@ -138,7 +97,7 @@ export function createNewSessionWorkspaceController(input: {
|
||||
() => undefined,
|
||||
)
|
||||
const project = currentProject()
|
||||
const directories = project ? [project.worktree, ...worktreeDirectories()] : [sdk().directory]
|
||||
const directories = project ? [project.worktree, ...workspaceDirectories(project)] : [sdk().directory]
|
||||
directories.forEach((directory) => void data.location.vcs.sync({ directory }).catch(() => undefined))
|
||||
})
|
||||
const branch = createMemo(() =>
|
||||
@@ -163,11 +122,7 @@ export function createNewSessionWorkspaceController(input: {
|
||||
workspace: createMemo(() => {
|
||||
const project = currentProject()
|
||||
const current = value()
|
||||
return (
|
||||
current === "create" ||
|
||||
(!!project &&
|
||||
(isWorkspaceDirectory(project, current) || worktreeDirectories().some((item) => sameDirectory(item, current))))
|
||||
)
|
||||
return current === "create" || (!!project && isWorkspaceDirectory(project, current))
|
||||
}),
|
||||
reset: () => {
|
||||
input.setSelectedWorktree(undefined)
|
||||
@@ -187,8 +142,10 @@ export function createNewSessionWorkspaceController(input: {
|
||||
},
|
||||
project: {
|
||||
root: projectRoot,
|
||||
workspaces: worktreeDirectories,
|
||||
managed: managedWorktrees,
|
||||
workspaces: () => {
|
||||
const project = currentProject()
|
||||
return project ? workspaceDirectories(project) : []
|
||||
},
|
||||
git: visible,
|
||||
branches: () => {
|
||||
const current = data.location.vcs.info({ directory: sdk().directory })?.branch.current
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { Menu } from "@opencode-ai/ui/menu"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
@@ -26,9 +25,6 @@ export function PromptWorkspaceSelector(props: {
|
||||
let searchInput: HTMLInputElement | undefined
|
||||
let branchSearchInput: HTMLInputElement | undefined
|
||||
let focusSearch = false
|
||||
const branchTruncation = createTruncatedText()
|
||||
const focusWorktreeSearch = () =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => searchInput?.focus({ preventScroll: true })))
|
||||
let pending: { type: "select"; value: string } | { type: "create"; branch: string } | { type: "viewAll" } | undefined
|
||||
const selected = () => (sameDirectory(props.value, props.projectRoot) ? "main" : props.value)
|
||||
const workspaces = createMemo(() => {
|
||||
@@ -38,8 +34,8 @@ export function PromptWorkspaceSelector(props: {
|
||||
})
|
||||
const icon = () => {
|
||||
if (selected() === "main") return "monitor"
|
||||
if (selected() === "create") return "plus"
|
||||
return "outline-worktree"
|
||||
if (selected() === "create") return "workspace-new"
|
||||
return "workspace-isolated"
|
||||
}
|
||||
const select = (value: string) => {
|
||||
pending = { type: "select", value }
|
||||
@@ -70,14 +66,13 @@ export function PromptWorkspaceSelector(props: {
|
||||
<>
|
||||
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
||||
<Tooltip
|
||||
appearance={props.onboarding ? "large" : undefined}
|
||||
placement="top"
|
||||
openDelay={800}
|
||||
value={
|
||||
props.onboarding ? (
|
||||
<div class="flex flex-col gap-1 text-start">
|
||||
<div class="flex items-center gap-1.5 font-[530] text-v2-text-text-base">
|
||||
<Icon name="outline-worktree" size="small" class="shrink-0 text-v2-text-text-accent" />
|
||||
<Icon name="workspace-isolated" size="small" class="shrink-0 text-v2-text-text-accent" />
|
||||
<span>{language.t("workspace.onboarding.title")}</span>
|
||||
</div>
|
||||
<span class="font-[440] text-v2-text-text-muted">{language.t("workspace.onboarding.description")}</span>
|
||||
@@ -94,10 +89,7 @@ export function PromptWorkspaceSelector(props: {
|
||||
aria-description={language.t("session.new.workspace.trigger.tooltip")}
|
||||
class="flex h-6 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed data-[expanded]:text-v2-text-text-muted"
|
||||
>
|
||||
<Icon
|
||||
name={icon()}
|
||||
class={`shrink-0 ${selected() === "main" || selected() === "create" ? "text-v2-icon-icon-muted" : "text-v2-icon-icon-accent"}`}
|
||||
/>
|
||||
<Icon name={icon()} class="shrink-0 text-v2-icon-icon-muted" />
|
||||
<span class="min-w-0 truncate">{label()}</span>
|
||||
<Show when={props.onboarding}>
|
||||
<span
|
||||
@@ -117,8 +109,14 @@ export function PromptWorkspaceSelector(props: {
|
||||
<Tooltip
|
||||
placement="right"
|
||||
openDelay={800}
|
||||
value={language.t("session.new.workspace.local.tooltip")}
|
||||
contentClass="max-w-[140px]"
|
||||
value={
|
||||
<span class="flex flex-col gap-0.5">
|
||||
<span>{language.t("session.new.workspace.local")}</span>
|
||||
<span class="font-[440] text-v2-text-text-muted">
|
||||
{language.t("session.new.workspace.local.tooltip")}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
class="min-w-0 flex-1"
|
||||
>
|
||||
<span class="min-w-0 truncate">{language.t("session.new.workspace.local")}</span>
|
||||
@@ -128,22 +126,24 @@ export function PromptWorkspaceSelector(props: {
|
||||
</Show>
|
||||
</Menu.Item>
|
||||
<Menu.Item onSelect={() => select("create")}>
|
||||
<Icon name="plus" />
|
||||
<Tooltip
|
||||
placement="right"
|
||||
openDelay={800}
|
||||
value={language.t("session.new.workspace.new.tooltip")}
|
||||
contentClass="max-w-[140px]"
|
||||
class="min-w-0 flex-1"
|
||||
>
|
||||
<span class="min-w-0 truncate">{language.t("workspace.new")}</span>
|
||||
</Tooltip>
|
||||
<Icon name="workspace-new" />
|
||||
<span class="min-w-0 flex-1 truncate">{language.t("workspace.new")}</span>
|
||||
<Show when={selected() === "create"}>
|
||||
<Icon name="check" size="small" class="shrink-0" />
|
||||
</Show>
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Show when={props.workspaces.length > 0}>
|
||||
<Show
|
||||
when={props.workspaces.length > 0}
|
||||
fallback={
|
||||
<>
|
||||
<Menu.Separator class="h-[0.5px]" />
|
||||
<Menu.Item onSelect={() => (pending = { type: "viewAll" })}>
|
||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
||||
</Menu.Item>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Menu.Separator class="h-[0.5px]" />
|
||||
<Menu.Sub
|
||||
gutter={0}
|
||||
@@ -156,11 +156,10 @@ export function PromptWorkspaceSelector(props: {
|
||||
}
|
||||
if (!focusSearch || props.workspaces.length < 10) return
|
||||
focusSearch = false
|
||||
focusWorktreeSearch()
|
||||
requestAnimationFrame(() => searchInput?.focus())
|
||||
}}
|
||||
>
|
||||
<Menu.SubTrigger
|
||||
onClick={focusWorktreeSearch}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "ArrowRight" ||
|
||||
@@ -171,7 +170,7 @@ export function PromptWorkspaceSelector(props: {
|
||||
focusSearch = true
|
||||
}}
|
||||
>
|
||||
<Icon name="outline-worktree" />
|
||||
<Icon name="workspace-isolated" />
|
||||
<span class="min-w-0 flex-1 truncate">
|
||||
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
|
||||
</span>
|
||||
@@ -206,7 +205,7 @@ export function PromptWorkspaceSelector(props: {
|
||||
<For each={workspaces()}>
|
||||
{(workspace) => (
|
||||
<Menu.Item onSelect={() => select(workspace)}>
|
||||
<Icon name="outline-worktree" />
|
||||
<Icon name="workspace-isolated" />
|
||||
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
||||
<Show when={selected() === workspace}>
|
||||
<Icon name="check" size="small" class="shrink-0" />
|
||||
@@ -214,11 +213,6 @@ export function PromptWorkspaceSelector(props: {
|
||||
</Menu.Item>
|
||||
)}
|
||||
</For>
|
||||
<Show when={search.workspaces.trim() && workspaces().length === 0}>
|
||||
<div class="px-3 py-4 text-center text-[13px] font-[440] leading-5 text-v2-text-text-muted">
|
||||
{language.t("session.new.workspace.search.empty")}
|
||||
</div>
|
||||
</Show>
|
||||
<Menu.Separator class="h-[0.5px]" />
|
||||
<Menu.Item onSelect={() => (pending = { type: "viewAll" })}>
|
||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
||||
@@ -238,7 +232,6 @@ export function PromptWorkspaceSelector(props: {
|
||||
<Tooltip
|
||||
placement="top"
|
||||
value={language.t("session.new.workspace.fromBranch", { branch: props.branch! })}
|
||||
disabled={!branchTruncation.truncated()}
|
||||
class="ms-1 min-w-0 max-w-[220px]"
|
||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||
>
|
||||
@@ -252,7 +245,7 @@ export function PromptWorkspaceSelector(props: {
|
||||
>
|
||||
<Menu.Trigger class="flex h-6 min-w-0 max-w-[220px] items-center gap-1.5 rounded-full bg-v2-background-bg-layer-02 px-2.5 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:bg-v2-background-bg-layer-03 hover:text-v2-text-text-muted focus-visible:bg-v2-background-bg-layer-03 focus-visible:text-v2-text-text-muted focus-visible:outline-none data-[expanded]:bg-v2-background-bg-layer-03 data-[expanded]:text-v2-text-text-muted">
|
||||
<Icon name="branch-out" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
<span ref={branchTruncation.observe} class="min-w-0 truncate">
|
||||
<span class="min-w-0 truncate">
|
||||
{language.t("session.new.workspace.fromBranch", { branch: props.branch! })}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
@@ -326,7 +319,6 @@ export function PromptWorkspaceSelector(props: {
|
||||
|
||||
export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?: boolean; class?: string }) {
|
||||
const language = useLanguage()
|
||||
const truncation = createTruncatedText()
|
||||
const label = () => {
|
||||
if (props.noGit) return language.t("session.new.git.none")
|
||||
if (!props.branch) return undefined
|
||||
@@ -346,27 +338,15 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?
|
||||
<Tooltip
|
||||
placement="top"
|
||||
value={value()}
|
||||
disabled={!truncation.truncated()}
|
||||
class={`min-w-0 max-w-[220px] ${props.class ?? ""}`}
|
||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||
>
|
||||
<div class="flex h-6 min-w-0 max-w-[220px] items-center gap-1.5 rounded-full bg-v2-background-bg-layer-02 px-2.5 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint">
|
||||
<Icon name={icon()} size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
<span ref={truncation.observe} class="min-w-0 truncate">
|
||||
{value()}
|
||||
</span>
|
||||
<span class="min-w-0 truncate">{value()}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function createTruncatedText() {
|
||||
const [truncated, setTruncated] = createSignal(false)
|
||||
return {
|
||||
truncated,
|
||||
observe: (element: HTMLSpanElement) =>
|
||||
createResizeObserver(element, () => setTruncated(element.scrollWidth > element.clientWidth)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,7 +659,6 @@ export const dict = {
|
||||
"home.sessions.group.yesterday": "Yesterday",
|
||||
"home.sessions.group.older": "Older",
|
||||
"home.providerTip": "Connect to 75+ providers to use other models, including Claude, GPT, Gemini, etc",
|
||||
"home.workspaceTip": "Start next session in a new workspace to keep changes isolated",
|
||||
|
||||
"session.tab.session": "Session",
|
||||
"session.tab.files": "Files",
|
||||
@@ -677,7 +676,7 @@ export const dict = {
|
||||
"session.error.incompatible.description":
|
||||
"{{server}} is running OpenCode {{version}}, which isn't compatible with this app. Upgrade the server to OpenCode V2 to continue.",
|
||||
"session.background.moveTasks": "Move {{tasks}} to background",
|
||||
"session.background.moveRunning": "Move running work to background",
|
||||
"session.background.moveRunning": "Move to background",
|
||||
"session.background.inBackground": "Running {{tasks}} in background",
|
||||
"session.background.moveInline": "Press {{keybind}} to move running work to the background",
|
||||
"session.background.running": "Running work in background",
|
||||
@@ -1239,43 +1238,30 @@ export const dict = {
|
||||
|
||||
"workspace.new": "New worktree",
|
||||
"common.viewAll": "View all",
|
||||
"session.new.workspace.local.tooltip": "Uses project’s current checkout",
|
||||
"session.new.workspace.new.tooltip": "Creates isolated copy from current checkout",
|
||||
"session.new.workspace.local.tooltip": "Use current checkout",
|
||||
"session.new.workspace.new.tooltip": "Create isolated checkout",
|
||||
"session.new.workspace.fromBranch": "from {{branch}}",
|
||||
"session.new.workspace.createFrom": "Create from branch",
|
||||
"session.new.workspace.branch.search.placeholder": "Search branches",
|
||||
"session.new.workspace.trigger.tooltip": "Select where to run session",
|
||||
"session.new.workspace.search.placeholder": "Search worktrees",
|
||||
"session.new.workspace.search.empty": "No matching worktrees",
|
||||
"settings.tab.workspaces": "Worktrees",
|
||||
"settings.workspaces.description": "Review worktrees and manage disk usage",
|
||||
"settings.workspaces.filter.all": "All projects",
|
||||
"settings.workspaces.empty": "No worktrees yet",
|
||||
"settings.workspaces.empty.description": "Worktrees created in OpenCode will appear here",
|
||||
"settings.workspaces.empty": "No worktrees",
|
||||
"settings.workspaces.count.one": "{{count}} worktree",
|
||||
"settings.workspaces.count.other": "{{count}} worktrees",
|
||||
"settings.workspaces.sessions.one": "{{count}} session in {{project}}",
|
||||
"settings.workspaces.sessions.other": "{{count}} sessions in {{project}}",
|
||||
"settings.workspaces.sessions.filtered.one": "{{count}} session",
|
||||
"settings.workspaces.sessions.filtered.other": "{{count}} sessions",
|
||||
"settings.workspaces.lastActiveSession": "Last active session",
|
||||
"settings.workspaces.deleteAll": "Delete all worktrees",
|
||||
"settings.workspaces.deleteAll.confirm.one": "This will permanently delete {{count}} selected worktree.",
|
||||
"settings.workspaces.deleteAll.confirm.other": "This will permanently delete {{count}} selected worktrees.",
|
||||
"settings.workspaces.deleteWithoutSessions": "Delete worktrees without sessions",
|
||||
"settings.workspaces.deleteWithoutSessions.confirm.one":
|
||||
"This will permanently delete {{count}} worktree without linked sessions.",
|
||||
"settings.workspaces.deleteWithoutSessions.confirm.other":
|
||||
"This will permanently delete {{count}} worktrees without linked sessions.",
|
||||
"settings.workspaces.deleteWithoutSessions.warning":
|
||||
"Worktrees with unmerged changes or active locations will be kept.",
|
||||
"settings.workspaces.delete.button": "Delete worktrees",
|
||||
"settings.workspaces.delete.warning": "This permanently deletes the worktree and its branch.",
|
||||
"settings.workspaces.deleteAll.confirm": "Delete all {{count}} worktrees?",
|
||||
"settings.workspaces.delete.warning":
|
||||
"The worktree directory and branch will be permanently removed, including any unmerged changes shown below.",
|
||||
"settings.workspaces.deleteAll.warning":
|
||||
"Worktrees with unmerged changes, linked sessions, or active locations will be kept.",
|
||||
"The {{count}} selected worktrees in {{project}} will be permanently removed only if each is clean, inactive, and has no linked sessions.",
|
||||
"settings.workspaces.delete.blocked.active": "The active worktree cannot be deleted.",
|
||||
"settings.workspaces.delete.blocked.linked":
|
||||
"Linked sessions will remain, but their working directory will no longer exist.",
|
||||
"Linked sessions will remain, but their working directory will be permanently removed.",
|
||||
"settings.workspaces.default.title": "Default environment",
|
||||
"settings.workspaces.default.description": "Choose where new sessions start",
|
||||
"settings.workspaces.default.lastUsed": "Last used per project",
|
||||
@@ -1286,10 +1272,9 @@ export const dict = {
|
||||
"workspace.move.failed": "Failed to move session",
|
||||
"workspace.lifecycle.creating": "Creating worktree",
|
||||
"workspace.lifecycle.created": "Worktree created",
|
||||
"workspace.lifecycle.deleting": "Deleting…",
|
||||
"workspace.lifecycle.starting": "Starting session",
|
||||
"workspace.onboarding.title": "Isolate sessions with worktrees",
|
||||
"workspace.onboarding.description": "Each gets its own checkout",
|
||||
"workspace.onboarding.description": "Each gets its own checkout, so nothing interferes with your local repository",
|
||||
"workspace.lifecycle.moving": "Moving to worktree",
|
||||
"workspace.lifecycle.set": "Worktree set",
|
||||
"session.summary.title": "Session details",
|
||||
@@ -1308,10 +1293,9 @@ export const dict = {
|
||||
"workspace.status.checking": "Checking for unmerged changes…",
|
||||
"workspace.status.error": "Unable to verify git status.",
|
||||
"workspace.status.clean": "No unmerged changes detected.",
|
||||
"workspace.status.dirty": "Unmerged changes will be lost.",
|
||||
"workspace.status.dirty": "Unmerged changes detected in this worktree.",
|
||||
"workspace.delete.title": "Delete worktree",
|
||||
"workspace.delete.confirm": "Delete “{{name}}”?",
|
||||
"workspace.delete.location": "Location",
|
||||
"workspace.delete.confirm": 'Delete worktree "{{name}}"?',
|
||||
"workspace.delete.button": "Delete worktree",
|
||||
"workspace.reset.title": "Reset worktree",
|
||||
"workspace.reset.confirm": 'Reset worktree "{{name}}"?',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { WorkspaceOnboardingSchema, ProviderTipSchema, WorkspaceTipSchema } from "@/new-session/view"
|
||||
import { WorkspaceOnboardingSchema, ProviderTipSchema } from "@/new-session/view"
|
||||
import { ModelSelectionSchema } from "@/providers/models/selection"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
import { FileViewsSchema } from "@/workspaces/files/view-cache"
|
||||
@@ -12,7 +12,6 @@ describe("persisted consumer schemas", () => {
|
||||
test("onboarding and provider tip retain defaults and validate stored values", () => {
|
||||
const onboarding = Schema.decodeUnknownSync(Persistence.withInitial(WorkspaceOnboardingSchema, { used: false }))
|
||||
const tip = Schema.decodeUnknownSync(Persistence.withInitial(ProviderTipSchema, { dismissedAt: 0 }))
|
||||
const workspaceTip = Schema.decodeUnknownSync(Persistence.withInitial(WorkspaceTipSchema, { dismissedAt: 0 }))
|
||||
expect(onboarding({})).toEqual({ used: false })
|
||||
expect(onboarding({ used: "true" })).toEqual({ used: false })
|
||||
expect(onboarding({ used: true })).toEqual({ used: true })
|
||||
@@ -20,7 +19,6 @@ describe("persisted consumer schemas", () => {
|
||||
expect(tip({ dismissedAt: "yesterday" })).toEqual({ dismissedAt: 0 })
|
||||
expect(tip({ dismissedAt: Infinity })).toEqual({ dismissedAt: 0 })
|
||||
expect(tip({ dismissedAt: 123 })).toEqual({ dismissedAt: 123 })
|
||||
expect(workspaceTip({ dismissedAt: 123 })).toEqual({ dismissedAt: 123 })
|
||||
})
|
||||
|
||||
test("collapse records recover malformed entries without losing valid siblings", () => {
|
||||
|
||||
@@ -91,7 +91,7 @@ export function SessionProjectMenu(props: {
|
||||
when={props.showProjectIcon}
|
||||
fallback={
|
||||
<span class={props.workspace ? "text-v2-icon-icon-accent" : "text-v2-icon-icon-muted"}>
|
||||
<Icon name={props.workspace ? "outline-worktree" : "monitor"} />
|
||||
<Icon name={props.workspace ? "workspace-isolated" : "monitor"} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -58,7 +58,6 @@ export function BackgroundMoveHint(props: { keybind?: string[]; onMove?: () => v
|
||||
type="button"
|
||||
variant="ghost-faint"
|
||||
size="small"
|
||||
icon="outline-arrow-to-corner-top-right"
|
||||
class="max-w-full"
|
||||
aria-label={language.t("session.background.moveInline", { keybind: keybind() })}
|
||||
onClick={() => props.onMove?.()}
|
||||
@@ -185,7 +184,7 @@ function WorkspaceMoveAction(props: {
|
||||
: "flex h-[46px] w-full items-center gap-2 rounded-b-[6px] px-3 pe-9 pt-2.5 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted focus-visible:outline-none"
|
||||
}
|
||||
>
|
||||
<Icon name="outline-worktree" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
<Icon name="workspace-new" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
<span class="min-w-0 truncate">{language.t("workspace.move.title")}</span>
|
||||
</SessionWorkspaceMenu>
|
||||
<button
|
||||
@@ -257,10 +256,7 @@ export function SessionSummaryPanel(props: {
|
||||
gutter={props.mobile ? 4 : -22}
|
||||
class={`${row} hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed`}
|
||||
>
|
||||
<Icon
|
||||
name={props.local ? "monitor" : "outline-worktree"}
|
||||
class={`shrink-0 ${props.local ? "text-v2-icon-icon-muted" : "text-v2-icon-icon-accent"}`}
|
||||
/>
|
||||
<Icon name={props.local ? "monitor" : "workspace-isolated"} class="shrink-0 text-v2-icon-icon-muted" />
|
||||
<span dir="auto" class="min-w-0 flex-1 truncate text-start">
|
||||
{location()}
|
||||
</span>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Project } from "@/runtime/server/types"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { useData } from "@/runtime/server/current"
|
||||
import { useSettingsDialog } from "@/settings/command"
|
||||
import { pathKey } from "@/workspaces/path-key"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { containsDirectory, sameDirectory, workspaceDirectories } from "@/workspaces/paths"
|
||||
@@ -27,6 +28,7 @@ export function SessionWorkspaceMenu(props: {
|
||||
const language = useLanguage()
|
||||
const serverSDK = useServerSDK()
|
||||
const data = useData()
|
||||
const openWorkspaces = useSettingsDialog("workspaces")
|
||||
const [store, setStore] = createStore({ selected: undefined as string | undefined })
|
||||
const [directories, setDirectories] = createSignal(workspaceDirectories(props.project))
|
||||
const blocked = () => props.eligible === false || data.session.status(props.sessionID) === "running"
|
||||
@@ -98,13 +100,13 @@ export function SessionWorkspaceMenu(props: {
|
||||
</Menu.Item>
|
||||
</Show>
|
||||
<Menu.Item disabled={!!store.selected || blocked()} onSelect={() => void move("create")}>
|
||||
<Icon name="plus" />
|
||||
<Icon name="workspace-new" />
|
||||
{language.t("workspace.new")}
|
||||
</Menu.Item>
|
||||
<Show when={workspaces().length > 0}>
|
||||
<Menu.Sub gutter={0} overlap overflowPadding={8}>
|
||||
<Menu.SubTrigger>
|
||||
<Icon name="outline-worktree" />
|
||||
<Icon name="workspace-isolated" />
|
||||
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
|
||||
</Menu.SubTrigger>
|
||||
<Menu.Portal>
|
||||
@@ -112,7 +114,7 @@ export function SessionWorkspaceMenu(props: {
|
||||
<For each={workspaces()}>
|
||||
{(workspace) => (
|
||||
<Menu.Item disabled={!!store.selected || blocked()} onSelect={() => void move(workspace)}>
|
||||
<Icon name="outline-worktree" />
|
||||
<Icon name="workspace-isolated" />
|
||||
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
||||
</Menu.Item>
|
||||
)}
|
||||
@@ -122,6 +124,10 @@ export function SessionWorkspaceMenu(props: {
|
||||
</Menu.Sub>
|
||||
</Show>
|
||||
</Menu.Group>
|
||||
<Menu.Separator class="h-[0.5px] bg-v2-border-border-base" />
|
||||
<Menu.Item onSelect={() => openWorkspaces()}>
|
||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
||||
</Menu.Item>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
|
||||
@@ -1029,92 +1029,25 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--v2-background-bg-layer-01);
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--v2-background-bg-base);
|
||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
|
||||
}
|
||||
|
||||
.settings-workspaces-inventory[data-empty="true"] [data-component="settings-list"] {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.settings-workspaces-row-motion {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
opacity: 1;
|
||||
transition:
|
||||
grid-template-rows 150ms ease-out,
|
||||
opacity 120ms ease-out;
|
||||
}
|
||||
|
||||
.settings-workspaces-row-motion[data-removing="true"] {
|
||||
grid-template-rows: minmax(0, 0fr);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.settings-workspaces-row {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
padding-bottom 150ms ease-out,
|
||||
margin-bottom 150ms ease-out,
|
||||
border-color 120ms ease-out;
|
||||
}
|
||||
|
||||
.settings-workspaces-row-motion:not(:last-child) > .settings-workspaces-row {
|
||||
.settings-workspaces-row:not(:last-child) {
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 0.5px solid var(--v2-border-border-base);
|
||||
}
|
||||
|
||||
.settings-workspaces-row-motion:has(+ .settings-workspaces-row-motion[data-removing="true"]:last-child)
|
||||
> .settings-workspaces-row {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.settings-workspaces-empty-motion {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
grid-template-rows: minmax(0, 0fr);
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
grid-template-rows 150ms ease-out,
|
||||
opacity 120ms ease-out;
|
||||
}
|
||||
|
||||
.settings-workspaces-empty-motion[data-visible="true"] {
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.settings-workspaces-empty-motion > .settings-workspaces-empty {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.settings-workspaces-row-motion {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.settings-workspaces-row,
|
||||
.settings-workspaces-empty-motion {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-workspaces-row-header {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -1155,7 +1088,7 @@
|
||||
color: var(--v2-text-text-base);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 440;
|
||||
font-weight: 530;
|
||||
line-height: var(--line-height-compact);
|
||||
letter-spacing: -0.04px;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1165,11 +1098,6 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.settings-workspaces-path-name {
|
||||
color: var(--v2-text-text-base);
|
||||
font-weight: 530;
|
||||
}
|
||||
|
||||
.settings-workspaces-meta {
|
||||
font-size: 13px;
|
||||
font-weight: 440;
|
||||
@@ -1177,10 +1105,6 @@
|
||||
color: var(--v2-text-text-faint);
|
||||
}
|
||||
|
||||
.settings-workspaces-meta-project {
|
||||
color: var(--v2-text-text-muted);
|
||||
}
|
||||
|
||||
.settings-workspaces-active,
|
||||
.settings-workspaces-more {
|
||||
flex-shrink: 0;
|
||||
@@ -1190,16 +1114,11 @@
|
||||
color: var(--v2-text-text-faint);
|
||||
}
|
||||
|
||||
.settings-workspaces-active {
|
||||
font-size: 13px;
|
||||
line-height: var(--line-height-compact);
|
||||
}
|
||||
|
||||
.settings-workspaces-sessions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 0.5px solid var(--v2-border-border-base);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--v2-background-bg-base);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1210,8 +1129,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-block: 10px;
|
||||
padding-inline: 12px 16px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 440;
|
||||
line-height: 16px;
|
||||
@@ -1231,28 +1149,22 @@
|
||||
|
||||
.settings-workspaces-session-time {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
line-height: var(--line-height-compact);
|
||||
color: var(--v2-text-text-faint);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: var(--v2-text-text-muted);
|
||||
}
|
||||
|
||||
.settings-workspaces-empty {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-block: 48px;
|
||||
font-size: 13px;
|
||||
font-weight: 440;
|
||||
line-height: var(--line-height-compact);
|
||||
color: var(--v2-text-text-muted);
|
||||
}
|
||||
|
||||
.settings-workspaces-empty-title {
|
||||
color: var(--v2-text-text-base);
|
||||
font-weight: 530;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.settings-workspaces-header {
|
||||
padding: 24px 20px 20px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component, createEffect, createMemo, For, Show, onMount, startTransition } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Menu } from "@opencode-ai/ui/menu"
|
||||
@@ -36,7 +35,7 @@ const sections = [
|
||||
[
|
||||
{ value: "servers", icon: "server", label: "status.popover.tab.servers" },
|
||||
{ value: "projects", icon: "folder", label: "settings.tab.projects" },
|
||||
{ value: "workspaces", icon: "outline-worktree", label: "settings.tab.workspaces" },
|
||||
{ value: "workspaces", icon: "workspace-isolated", label: "settings.tab.workspaces" },
|
||||
],
|
||||
[
|
||||
{ value: "providers", icon: "providers", label: "settings.providers.title" },
|
||||
@@ -55,7 +54,6 @@ export const SettingsScreen: Component = () => {
|
||||
const servers = useServers()
|
||||
const tabs = useTabs()
|
||||
const global = useGlobal()
|
||||
const [state, setState] = createStore({ worktreeFilterReset: 0 })
|
||||
let root: HTMLDivElement | undefined
|
||||
|
||||
onMount(() => {
|
||||
@@ -148,14 +146,7 @@ export const SettingsScreen: Component = () => {
|
||||
</Show>
|
||||
<For each={group}>
|
||||
{(section) => (
|
||||
<Menu.RadioItem
|
||||
value={section.value}
|
||||
closeOnSelect
|
||||
onSelect={() => {
|
||||
if (section.value === "workspaces")
|
||||
setState("worktreeFilterReset", (value) => value + 1)
|
||||
}}
|
||||
>
|
||||
<Menu.RadioItem value={section.value} closeOnSelect>
|
||||
<Icon name={section.icon} />
|
||||
{language.t(section.label)}
|
||||
</Menu.RadioItem>
|
||||
@@ -181,12 +172,7 @@ export const SettingsScreen: Component = () => {
|
||||
<div class="flex flex-col gap-1 w-full">
|
||||
<For each={group}>
|
||||
{(section) => (
|
||||
<Tabs.Trigger
|
||||
value={section.value}
|
||||
onClick={() => {
|
||||
if (section.value === "workspaces") setState("worktreeFilterReset", (value) => value + 1)
|
||||
}}
|
||||
>
|
||||
<Tabs.Trigger value={section.value}>
|
||||
<Icon name={section.icon} />
|
||||
{language.t(section.label)}
|
||||
</Tabs.Trigger>
|
||||
@@ -222,10 +208,7 @@ export const SettingsScreen: Component = () => {
|
||||
</Tabs.Content>
|
||||
<SettingsServerScope directory={directory()}>
|
||||
<Tabs.Content value="workspaces" class="settings-panel">
|
||||
<SettingsWorkspaces
|
||||
activeDirectory={directory()}
|
||||
resetProjectFilter={() => state.worktreeFilterReset}
|
||||
/>
|
||||
<SettingsWorkspaces activeDirectory={directory()} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="providers" class="settings-panel">
|
||||
<SettingsProviders directory={directory()} onBack={showProviders} />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { Component } from "solid-js"
|
||||
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
||||
import { For, Show, createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { Key } from "@solid-primitives/keyed"
|
||||
import type { SessionInfo } from "@opencode-ai/client/promise"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -48,7 +46,7 @@ type Workspace = {
|
||||
project: Project
|
||||
}
|
||||
|
||||
export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProjectFilter: () => number }> = (props) => {
|
||||
export const SettingsWorkspaces: Component<{ activeDirectory?: string }> = (props) => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const serverSDK = useServerSDK()
|
||||
@@ -58,12 +56,6 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
const [store, setStore] = createStore({
|
||||
project: "all",
|
||||
transaction: undefined as "confirm" | "running" | undefined,
|
||||
deleting: [] as string[],
|
||||
removing: [] as string[],
|
||||
})
|
||||
createEffect(() => {
|
||||
props.resetProjectFilter()
|
||||
setStore("project", "all")
|
||||
})
|
||||
|
||||
const projectQuery = useQuery(() => ({
|
||||
@@ -119,7 +111,6 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
] as const,
|
||||
queryFn: () => loadSessions(workspaceDirectories()),
|
||||
enabled: serverSDK.connection.status() === "connected" && workspaceDirectories().length > 0,
|
||||
placeholderData: (previous) => previous,
|
||||
refetchOnMount: "always",
|
||||
}))
|
||||
const sessionsByWorkspace = createMemo(() => {
|
||||
@@ -132,29 +123,14 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
)
|
||||
})
|
||||
const workspaceSessions = (workspace: Workspace) => sessionsByWorkspace().get(pathKey(workspace.directory)) ?? []
|
||||
const workspacesWithoutSessions = createMemo(() => {
|
||||
if (sessionQuery.isPending || sessionQuery.isError) return []
|
||||
return filtered().filter((workspace) => workspaceSessions(workspace).length === 0)
|
||||
})
|
||||
const sessionCount = (workspace: Workspace) => {
|
||||
if (sessionQuery.isPending) return language.t("session.messages.loading")
|
||||
if (sessionQuery.isError) return language.t("common.requestFailed")
|
||||
const count = workspaceSessions(workspace).length
|
||||
if (selectedProject() !== "all") return language.plural("settings.workspaces.sessions.filtered", count, { count })
|
||||
const project = projectName(workspace.project)
|
||||
const label = language.plural("settings.workspaces.sessions", count, {
|
||||
return language.plural("settings.workspaces.sessions", count, {
|
||||
count,
|
||||
project,
|
||||
project: projectName(workspace.project),
|
||||
})
|
||||
const start = label.lastIndexOf(project)
|
||||
if (start < 0) return label
|
||||
return (
|
||||
<>
|
||||
{label.slice(0, start)}
|
||||
<span class="settings-workspaces-meta-project">{project}</span>
|
||||
{label.slice(start + project.length)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const lastActive = (workspace: Workspace) => {
|
||||
const updated = workspaceSessions(workspace)[0]?.time.updated
|
||||
@@ -182,64 +158,54 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
}
|
||||
const inspectionMessages = (result: WorkspaceDeleteInspection) => {
|
||||
const messages = [
|
||||
result.active ? language.t("settings.workspaces.delete.blocked.active") : undefined,
|
||||
result.linked ? language.t("settings.workspaces.delete.blocked.linked") : undefined,
|
||||
result.dirty ? language.t("workspace.status.dirty") : undefined,
|
||||
].filter((message): message is string => message !== undefined)
|
||||
return messages
|
||||
return messages.length > 0 ? messages : [language.t("workspace.status.clean")]
|
||||
}
|
||||
const blocked = (result: WorkspaceDeleteInspection) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("workspace.delete.failed.title"),
|
||||
description: result.active
|
||||
? language.t("settings.workspaces.delete.blocked.active")
|
||||
: inspectionMessages(result)[0],
|
||||
description: inspectionMessages(result)[0],
|
||||
})
|
||||
}
|
||||
|
||||
const remove = async (workspace: Workspace, force = false, context = captureDeleteContext()) => {
|
||||
const key = String(pathKey(workspace.directory))
|
||||
setStore("deleting", (items) => [...items, key])
|
||||
try {
|
||||
const preflight = await inspect(workspace, context)
|
||||
if (!force && (preflight.result.active || preflight.result.linked || preflight.result.dirty)) {
|
||||
blocked(preflight.result)
|
||||
return
|
||||
}
|
||||
const removed = await context.sdk.api.worktree
|
||||
.remove({
|
||||
projectID: workspace.project.id,
|
||||
directory: workspace.directory,
|
||||
force,
|
||||
})
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("workspace.delete.failed.title"),
|
||||
description: error instanceof Error ? error.message : language.t("common.requestFailed"),
|
||||
})
|
||||
return false
|
||||
})
|
||||
if (!removed) return
|
||||
setStore("removing", (items) => [...items, key])
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
tabs.store.forEach((tab) => {
|
||||
if (tab.type !== "draft" || tab.server !== context.server) return
|
||||
const directoryMatches = containsDirectory(workspace.directory, tab.directory)
|
||||
const worktreeMatches = tab.worktree && containsDirectory(workspace.directory, tab.worktree)
|
||||
if (!directoryMatches && !worktreeMatches) return
|
||||
tabs.updateDraft(tab.draftID, {
|
||||
directory: directoryMatches ? workspace.project.worktree : tab.directory,
|
||||
worktree: undefined,
|
||||
})
|
||||
})
|
||||
clearWorkspaceTerminals(workspace.directory, platform, context.sdk.scope)
|
||||
await projectQuery.refetch()
|
||||
} finally {
|
||||
setStore("deleting", (items) => items.filter((item) => item !== key))
|
||||
setStore("removing", (items) => items.filter((item) => item !== key))
|
||||
const preflight = await inspect(workspace, context)
|
||||
if (preflight.result.active || (!force && (preflight.result.linked || preflight.result.dirty))) {
|
||||
blocked(preflight.result)
|
||||
return
|
||||
}
|
||||
const removed = await context.sdk.api.worktree
|
||||
.remove({
|
||||
projectID: workspace.project.id,
|
||||
directory: workspace.directory,
|
||||
force,
|
||||
})
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("workspace.delete.failed.title"),
|
||||
description: error instanceof Error ? error.message : language.t("common.requestFailed"),
|
||||
})
|
||||
return false
|
||||
})
|
||||
if (!removed) return
|
||||
tabs.store.forEach((tab) => {
|
||||
if (tab.type !== "draft" || tab.server !== context.server) return
|
||||
const directoryMatches = containsDirectory(workspace.directory, tab.directory)
|
||||
const worktreeMatches = tab.worktree && containsDirectory(workspace.directory, tab.worktree)
|
||||
if (!directoryMatches && !worktreeMatches) return
|
||||
tabs.updateDraft(tab.draftID, {
|
||||
directory: directoryMatches ? workspace.project.worktree : tab.directory,
|
||||
worktree: undefined,
|
||||
})
|
||||
})
|
||||
clearWorkspaceTerminals(workspace.directory, platform, context.sdk.scope)
|
||||
await projectQuery.refetch()
|
||||
}
|
||||
|
||||
let inspectionID = 0
|
||||
@@ -287,30 +253,13 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
if (store.transaction) return
|
||||
const context = captureDeleteContext()
|
||||
const inventory = [...filtered()]
|
||||
const project = projectOptions().find((option) => option.id === selectedProject())?.label ?? selectedProject()
|
||||
setStore("transaction", "confirm")
|
||||
void dialog.push(
|
||||
() => (
|
||||
<DialogDeleteWorkspaces
|
||||
title={language.t("settings.workspaces.deleteAll")}
|
||||
confirmation={language.plural("settings.workspaces.deleteAll.confirm", inventory.length)}
|
||||
warning={language.t("settings.workspaces.deleteAll.warning")}
|
||||
onDelete={() => transact(() => removeAll(inventory, context))}
|
||||
/>
|
||||
),
|
||||
releaseConfirmation,
|
||||
)
|
||||
}
|
||||
const confirmDeleteWithoutSessions = () => {
|
||||
if (store.transaction || workspacesWithoutSessions().length === 0) return
|
||||
const context = captureDeleteContext()
|
||||
const inventory = [...workspacesWithoutSessions()]
|
||||
setStore("transaction", "confirm")
|
||||
void dialog.push(
|
||||
() => (
|
||||
<DialogDeleteWorkspaces
|
||||
title={language.t("settings.workspaces.deleteWithoutSessions")}
|
||||
confirmation={language.plural("settings.workspaces.deleteWithoutSessions.confirm", inventory.length)}
|
||||
warning={language.t("settings.workspaces.deleteWithoutSessions.warning")}
|
||||
<DialogDeleteAllWorkspaces
|
||||
count={inventory.length}
|
||||
project={project}
|
||||
onDelete={() => transact(() => removeAll(inventory, context))}
|
||||
/>
|
||||
),
|
||||
@@ -322,47 +271,44 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
<>
|
||||
<div class="settings-tab-header settings-workspaces-header">
|
||||
<div class="settings-tab-header-row">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="settings-tab-title">{language.t("settings.tab.workspaces")}</h2>
|
||||
<span class="text-11-regular text-v2-text-text-muted">
|
||||
{language.t("settings.workspaces.description")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 class="settings-tab-title">{language.t("settings.tab.workspaces")}</h2>
|
||||
<InlineServerSelect />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-tab-body settings-workspaces">
|
||||
<Show when={filtered().length > 0}>
|
||||
<div class="settings-workspaces-toolbar">
|
||||
<span class="settings-workspaces-count">
|
||||
<div class="settings-workspaces-toolbar">
|
||||
<span class="settings-workspaces-count">
|
||||
<Show when={!projectQuery.isPending && !projectQuery.isError}>
|
||||
{language.plural("settings.workspaces.count", filtered().length)}
|
||||
</span>
|
||||
<div class="settings-workspaces-toolbar-actions">
|
||||
<Show when={projects().length > 1}>
|
||||
<Menu placement="bottom-end" gutter={6}>
|
||||
<Menu.Trigger as={Button} size="small" variant="ghost-muted" class="max-w-48">
|
||||
<span class="min-w-0 truncate">
|
||||
{projectOptions().find((option) => option.id === selectedProject())?.label}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Content>
|
||||
<For each={projectOptions()}>
|
||||
{(option) => (
|
||||
<Menu.Item onSelect={() => setStore("project", option.id)}>
|
||||
<span class="min-w-0 flex-1 truncate">{option.label}</span>
|
||||
<Show when={selectedProject() === option.id}>
|
||||
<Icon name="check" size="small" class="shrink-0" />
|
||||
</Show>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</For>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
</Show>
|
||||
</Show>
|
||||
</span>
|
||||
<div class="settings-workspaces-toolbar-actions">
|
||||
<Show when={projects().length > 1}>
|
||||
<Menu placement="bottom-end" gutter={6}>
|
||||
<Menu.Trigger class="flex h-6 max-w-48 items-center gap-1 rounded-sm px-2 text-13-medium hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed">
|
||||
<span class="min-w-0 truncate">
|
||||
{projectOptions().find((option) => option.id === selectedProject())?.label}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Content>
|
||||
<For each={projectOptions()}>
|
||||
{(option) => (
|
||||
<Menu.Item onSelect={() => setStore("project", option.id)}>
|
||||
<span class="min-w-0 flex-1 truncate">{option.label}</span>
|
||||
<Show when={selectedProject() === option.id}>
|
||||
<Icon name="check" size="small" class="shrink-0" />
|
||||
</Show>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</For>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
</Show>
|
||||
<Show when={filtered().length > 0}>
|
||||
<Menu placement="bottom-end" gutter={4}>
|
||||
<Menu.Trigger
|
||||
as={IconButton}
|
||||
@@ -375,85 +321,78 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Content>
|
||||
<Show when={workspacesWithoutSessions().length > 0}>
|
||||
<Menu.Item onSelect={confirmDeleteWithoutSessions}>
|
||||
{language.t("settings.workspaces.deleteWithoutSessions")}
|
||||
</Menu.Item>
|
||||
<Menu.Separator />
|
||||
</Show>
|
||||
<Menu.Item onSelect={confirmDeleteAll}>
|
||||
<span class="settings-workspaces-delete-all">{language.t("settings.workspaces.deleteAll")}</span>
|
||||
</Menu.Item>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="settings-workspaces-inventory" data-empty={filtered().length === 0}>
|
||||
<SettingsList>
|
||||
<div class="settings-workspaces-empty-motion" data-visible={filtered().length === 0}>
|
||||
<div class="settings-workspaces-inventory">
|
||||
<Show
|
||||
when={filtered().length > 0}
|
||||
fallback={
|
||||
<div class="settings-workspaces-empty">
|
||||
<Show
|
||||
when={!projectQuery.isPending && !projectQuery.isError}
|
||||
fallback={language.t(projectQuery.isPending ? "common.loading" : "common.requestFailed")}
|
||||
>
|
||||
<span class="settings-workspaces-empty-title">{language.t("settings.workspaces.empty")}</span>
|
||||
<span>{language.t("settings.workspaces.empty.description")}</span>
|
||||
</Show>
|
||||
{language.t(
|
||||
projectQuery.isPending
|
||||
? "common.loading"
|
||||
: projectQuery.isError
|
||||
? "common.requestFailed"
|
||||
: "settings.workspaces.empty",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Key each={filtered()} by={(workspace) => `${workspace.project.id}:${pathKey(workspace.directory)}`}>
|
||||
{(workspace) => {
|
||||
const linked = () => workspaceSessions(workspace())
|
||||
const key = () => String(pathKey(workspace().directory))
|
||||
const deleting = () => store.deleting.includes(key())
|
||||
return (
|
||||
<div class="settings-workspaces-row-motion" data-removing={store.removing.includes(key())}>
|
||||
}
|
||||
>
|
||||
<SettingsList>
|
||||
<For each={filtered()}>
|
||||
{(workspace) => {
|
||||
const linked = () => workspaceSessions(workspace)
|
||||
return (
|
||||
<div class="settings-workspaces-row">
|
||||
<div class="settings-workspaces-row-header">
|
||||
<div class="settings-workspaces-copy">
|
||||
<div class="settings-workspaces-main">
|
||||
<WorkspacePath directory={workspace().directory} />
|
||||
<Tooltip
|
||||
value={workspace.directory}
|
||||
placement="top-start"
|
||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||
>
|
||||
<span
|
||||
tabIndex={0}
|
||||
dir="ltr"
|
||||
aria-label={workspace.directory}
|
||||
class="settings-workspaces-path"
|
||||
>
|
||||
{workspace.directory}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span class="settings-workspaces-meta">{sessionCount(workspace())}</span>
|
||||
<span class="settings-workspaces-meta">{sessionCount(workspace)}</span>
|
||||
</div>
|
||||
<div class="settings-workspaces-row-actions">
|
||||
<Show
|
||||
when={deleting()}
|
||||
fallback={
|
||||
<>
|
||||
<Show when={lastActive(workspace())}>
|
||||
{(value) => (
|
||||
<Tooltip
|
||||
value={language.t("settings.workspaces.lastActiveSession")}
|
||||
placement="top-end"
|
||||
>
|
||||
<span tabIndex={0} class="settings-workspaces-active">
|
||||
{value()}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Show>
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
aria-label={language.t("workspace.delete.confirm", {
|
||||
name: getFilename(workspace().directory),
|
||||
})}
|
||||
disabled={!!store.transaction}
|
||||
icon={<Icon name="outline-trash" size="small" />}
|
||||
onClick={() => confirmDelete(workspace())}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span class="settings-workspaces-active">
|
||||
{language.t("workspace.lifecycle.deleting")}
|
||||
</span>
|
||||
<Show when={lastActive(workspace)}>
|
||||
{(value) => (
|
||||
<Tooltip value={language.t("settings.workspaces.lastActiveSession")} placement="top-end">
|
||||
<span tabIndex={0} class="settings-workspaces-active">
|
||||
{value()}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Show>
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
aria-label={language.t("workspace.delete.confirm", {
|
||||
name: getFilename(workspace.directory),
|
||||
})}
|
||||
disabled={!!store.transaction}
|
||||
icon={<Icon name="trash" size="small" />}
|
||||
onClick={() => confirmDelete(workspace)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={linked().length > 0}>
|
||||
@@ -462,7 +401,7 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
{(session) => (
|
||||
<div class="settings-workspaces-session">
|
||||
<span>{sessionLabel(session)}</span>
|
||||
<Show when={linked().length > 1 ? sessionTime(session) : undefined}>
|
||||
<Show when={sessionTime(session)}>
|
||||
{(time) => <span class="settings-workspaces-session-time">{time()}</span>}
|
||||
</Show>
|
||||
</div>
|
||||
@@ -471,48 +410,18 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string; resetProj
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Key>
|
||||
</SettingsList>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</SettingsList>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkspacePath(props: { directory: string }) {
|
||||
const [truncated, setTruncated] = createSignal(false)
|
||||
const name = () => getFilename(props.directory)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
value={props.directory}
|
||||
placement="top-start"
|
||||
disabled={!truncated()}
|
||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||
>
|
||||
<span
|
||||
ref={(element) => createResizeObserver(element, () => setTruncated(element.scrollWidth > element.clientWidth))}
|
||||
tabIndex={truncated() ? 0 : undefined}
|
||||
dir="ltr"
|
||||
aria-label={props.directory}
|
||||
class="settings-workspaces-path"
|
||||
>
|
||||
<span>{props.directory.slice(0, -name().length)}</span>
|
||||
<span class="settings-workspaces-path-name">{name()}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDeleteWorkspaces(props: {
|
||||
title: string
|
||||
confirmation: string
|
||||
warning: string
|
||||
onDelete: () => Promise<void>
|
||||
}) {
|
||||
function DialogDeleteAllWorkspaces(props: { count: number; project: string; onDelete: () => Promise<void> }) {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const remove = () => {
|
||||
@@ -525,12 +434,13 @@ function DialogDeleteWorkspaces(props: {
|
||||
<Dialog fit>
|
||||
<DialogHeader>
|
||||
<DialogTitleGroup
|
||||
title={props.title}
|
||||
title={language.t("settings.workspaces.deleteAll")}
|
||||
description={
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>{props.confirmation}</div>
|
||||
<div>{props.warning}</div>
|
||||
</div>
|
||||
<>
|
||||
{language.t("settings.workspaces.deleteAll.confirm", { count: props.count })}
|
||||
<br />
|
||||
{language.t("settings.workspaces.deleteAll.warning", { count: props.count, project: props.project })}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DialogHeader>
|
||||
@@ -539,7 +449,7 @@ function DialogDeleteWorkspaces(props: {
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="button" variant="danger" onClick={remove}>
|
||||
{language.t("settings.workspaces.delete.button")}
|
||||
{language.t("settings.workspaces.deleteAll")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
@@ -562,7 +472,7 @@ function DialogDeleteWorkspace(props: {
|
||||
staleTime: 0,
|
||||
}))
|
||||
const descriptions = () => {
|
||||
if (status.isPending) return []
|
||||
if (status.isPending) return [language.t("workspace.status.checking")]
|
||||
if (status.isError) return [language.t("workspace.status.error")]
|
||||
if (!status.data) return []
|
||||
return props.inspectionMessages(status.data.result)
|
||||
@@ -577,20 +487,18 @@ function DialogDeleteWorkspace(props: {
|
||||
<Dialog fit>
|
||||
<DialogHeader>
|
||||
<DialogTitleGroup
|
||||
title={language.t("workspace.delete.confirm", { name: getFilename(props.workspace.directory) })}
|
||||
title={language.t("workspace.delete.title")}
|
||||
description={
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-11-regular text-v2-text-text-faint">
|
||||
{language.t(status.isPending ? "workspace.status.checking" : "workspace.delete.location")}
|
||||
</span>
|
||||
<code class="block w-fit max-w-full rounded-[4px] bg-[color-mix(in_oklch,var(--v2-text-text-base)_8%,transparent)] px-1 py-0.5 font-mono text-xs font-medium leading-4 text-v2-text-text-base break-all">
|
||||
{props.workspace.directory}
|
||||
</code>
|
||||
</div>
|
||||
<div>{language.t("settings.workspaces.delete.warning")}</div>
|
||||
<>
|
||||
{language.t("workspace.delete.confirm", { name: getFilename(props.workspace.directory) })}
|
||||
<br />
|
||||
<code class="max-w-full rounded-[4px] bg-[color-mix(in_oklch,var(--v2-text-text-base)_8%,transparent)] px-1 py-0.5 font-mono text-xs font-medium leading-4 text-v2-text-text-base break-all">
|
||||
{props.workspace.directory}
|
||||
</code>
|
||||
<br />
|
||||
{language.t("settings.workspaces.delete.warning")}
|
||||
<For each={descriptions()}>{(description) => <div>{description}</div>}</For>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DialogHeader>
|
||||
@@ -601,7 +509,7 @@ function DialogDeleteWorkspace(props: {
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
disabled={status.isPending || status.isError}
|
||||
disabled={status.isPending || status.isError || status.data?.result.active}
|
||||
onClick={remove}
|
||||
>
|
||||
{language.t("workspace.delete.button")}
|
||||
|
||||
@@ -231,7 +231,6 @@
|
||||
[data-component="button-v2"][data-variant="ghost-muted"] {
|
||||
background-color: transparent;
|
||||
color: var(--v2-text-text-muted);
|
||||
font-weight: 440;
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost-muted"] [data-slot="icon-svg"] {
|
||||
|
||||
@@ -100,14 +100,6 @@ const icons = {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M2 10.668V14.0013H10.6667M13.9974 10.6667V2H2.66406M13.9974 10.668V14.0013H10.6641M2 10V2H5.33333" stroke="currentColor" stroke-miterlimit="10" stroke-linecap="square"/><path d="M10.6693 10.6654V5.33203H5.33594V10.6654H10.6693Z" fill="currentColor"/>`,
|
||||
},
|
||||
"outline-worktree": {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M12 3.11133L14.3885 5.49977L12 7.88822M14.5 5.49972H10.058L5.13456 11.8139M14.5 11.6606L8.5 11.6606M5.50012 11.6597H1.5" stroke="currentColor"/>`,
|
||||
},
|
||||
"outline-trash": {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M2.44434 4.22224H13.5554M5.99994 4.22224V2.44446C5.99994 1.95557 5.99992 1.55557 5.99992 1.55557H9.99989C9.99989 1.55557 9.99994 1.95557 9.99994 2.44446V4.22224M6.55545 7.77779L6.7485 11.7778M9.44437 7.77779L9.2513 11.7778M12.1756 6.88891L11.8666 12.7556C11.8168 13.7068 11.7748 14.4445 11.7748 14.4445H4.22511C4.22511 14.4445 4.18392 13.7067 4.13414 12.7556L3.82509 6.88891" stroke="currentColor"/>`,
|
||||
},
|
||||
close: {
|
||||
viewBox: "0 0 20 20",
|
||||
body: `<path d="M14.4446 5.55566L5.55566 14.4446M5.55566 5.55566L14.4446 14.4446" stroke="currentColor" stroke-linejoin="round"/>`,
|
||||
|
||||
@@ -65,13 +65,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="tooltip-v2"][data-appearance="large"] {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: var(--line-height-compact);
|
||||
letter-spacing: -0.04px;
|
||||
}
|
||||
|
||||
@keyframes tooltipV2In {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -8,7 +8,7 @@ Floating tooltip built on Kobalte's tooltip primitive.
|
||||
- \`value\`: Content rendered inside the floating tooltip.
|
||||
- \`children\`: The trigger element that activates the tooltip on hover/focus.
|
||||
- \`placement\`: Kobalte placement string (e.g. "top", "bottom", "left", "right").
|
||||
- \`appearance\`: \`compact\` (default), \`standard\`, or \`large\`.
|
||||
- \`appearance\`: \`compact\` (default) or \`standard\`.
|
||||
- \`inactive\`: When true, renders only the trigger without tooltip behavior.
|
||||
- \`forceOpen\`: Forces the tooltip to stay open.
|
||||
- Inherits Kobalte Tooltip root props.
|
||||
@@ -49,9 +49,6 @@ export const Appearances = {
|
||||
<Tooltip appearance="compact" value="Compact tooltip">
|
||||
<span>Compact</span>
|
||||
</Tooltip>
|
||||
<Tooltip appearance="large" value="Large tooltip">
|
||||
<span>Large</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import "./tooltip.css"
|
||||
|
||||
export interface TooltipProps extends ComponentProps<typeof Root> {
|
||||
value: JSX.Element
|
||||
appearance?: "standard" | "compact" | "large"
|
||||
appearance?: "standard" | "compact"
|
||||
class?: string
|
||||
contentClass?: string
|
||||
contentStyle?: JSX.CSSProperties
|
||||
|
||||
Reference in New Issue
Block a user