mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-18 21:53:12 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 122d17df24 | |||
| 9aacd7ed24 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@opencode-ai/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Simplify interrupt continuation: the steer-scoped resume decision now lives in SessionExecution as a post-cleanup inbox check, and the run coordinator drops its continuation state machine. Wakes arriving during cancellation cleanup now restart a normal full drain, and interrupting an idle session with continue now resumes pending steering input. Recovery-applied moves now end with the same full wake as inbox-admitted moves, retrying any stranded inbox work at the new location.
|
||||||
@@ -25,7 +25,7 @@ async function installSessionSwitchProbe(
|
|||||||
let running = true
|
let running = true
|
||||||
const reviewLevels: Record<string, string> = {
|
const reviewLevels: Record<string, string> = {
|
||||||
panel: "#review-panel",
|
panel: "#review-panel",
|
||||||
tabs: '#review-panel [data-component="tabs"]',
|
tabs: '#review-panel [data-component="tabs"]',
|
||||||
body: '#review-panel [data-slot="session-review-v2-body"]',
|
body: '#review-panel [data-slot="session-review-v2-body"]',
|
||||||
review: '#review-panel [data-component="session-review-v2"]',
|
review: '#review-panel [data-component="session-review-v2"]',
|
||||||
preview: '#review-panel [data-slot="session-review-v2-preview"]',
|
preview: '#review-panel [data-slot="session-review-v2-preview"]',
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ test("shows a pending question dock", async ({ page }) => {
|
|||||||
rejectRequests.push(request.url())
|
rejectRequests.push(request.url())
|
||||||
})
|
})
|
||||||
|
|
||||||
await question.getByRole("button", { name: "Minimize question" }).click()
|
await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
|
||||||
await expect(question).toBeVisible()
|
await expect(question).toBeVisible()
|
||||||
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
|
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
|
||||||
await expect(question.getByText("Select one answer")).toBeHidden()
|
await expect(question.getByText("Select one answer")).toBeHidden()
|
||||||
@@ -63,7 +63,7 @@ test("shows a pending question dock", async ({ page }) => {
|
|||||||
await expect(page.locator('[data-component="question-minimized-dock"]')).toHaveCount(0)
|
await expect(page.locator('[data-component="question-minimized-dock"]')).toHaveCount(0)
|
||||||
expect(rejectRequests).toEqual([])
|
expect(rejectRequests).toEqual([])
|
||||||
|
|
||||||
await question.getByRole("button", { name: "Restore question" }).click()
|
await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
|
||||||
await expect(question).toBeVisible()
|
await expect(question).toBeVisible()
|
||||||
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
|
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
|
||||||
await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible()
|
await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible()
|
||||||
|
|||||||
@@ -131,14 +131,15 @@ test("labels V2 skill tools from IDs and result metadata", async ({ page }) => {
|
|||||||
"aria-label",
|
"aria-label",
|
||||||
"sample-skill",
|
"sample-skill",
|
||||||
)
|
)
|
||||||
await expect(
|
await expect(page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`)).toHaveAttribute(
|
||||||
page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`),
|
"aria-label",
|
||||||
).toHaveAttribute("aria-label", "OpenCode")
|
"OpenCode",
|
||||||
|
)
|
||||||
for (const id of [pending, completed]) {
|
for (const id of [pending, completed]) {
|
||||||
const skill = page.locator(`[data-timeline-part-id="${id}"]`)
|
const skill = page.locator(`[data-timeline-part-id="${id}"]`)
|
||||||
await expect(skill.locator('[data-slot="skill-tool-label"]')).toHaveText("Skill")
|
await expect(skill.locator('[data-slot="skill-tool-label"]')).toHaveText("Skill")
|
||||||
await expect(skill.locator('[data-slot="skill-tool-separator"]')).toHaveText("·")
|
await expect(skill.locator('[data-slot="skill-tool-separator"]')).toHaveText("·")
|
||||||
await expect(skill.locator('use[href="#opencode-v2-icon-post-skill"]')).toBeVisible()
|
await expect(skill.locator('use[href="#opencode-icon-post-skill"]')).toBeVisible()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ async function configureSmokePage(page: Page, directory: string) {
|
|||||||
}
|
}
|
||||||
let recordFrame: number | undefined
|
let recordFrame: number | undefined
|
||||||
const record = () => {
|
const record = () => {
|
||||||
for (const toast of document.querySelectorAll<HTMLElement>(".toast-v2--error")) {
|
for (const toast of document.querySelectorAll<HTMLElement>('[data-component="toast"][data-variant="error"]')) {
|
||||||
const text = toast.textContent?.trim()
|
const text = toast.textContent?.trim()
|
||||||
if (text && !smoke.__timelineSmokeErrorToasts!.includes(text)) smoke.__timelineSmokeErrorToasts!.push(text)
|
if (text && !smoke.__timelineSmokeErrorToasts!.includes(text)) smoke.__timelineSmokeErrorToasts!.push(text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { batch, createEffect, onCleanup, onMount, Show } from "solid-js"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
|
|
||||||
@@ -101,8 +102,16 @@ function Cell(props: {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (props.inline) {
|
||||||
|
return (
|
||||||
|
<TooltipV2 value={props.tip} placement="top">
|
||||||
|
{content()}
|
||||||
|
</TooltipV2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip appearance={props.inline ? "compact" : "standard"} value={props.tip} placement="top">
|
<Tooltip value={props.tip} placement="top">
|
||||||
{content()}
|
{content()}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
@@ -142,8 +151,16 @@ function ToggleCell(props: {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (props.inline) {
|
||||||
|
return (
|
||||||
|
<TooltipV2 value={props.tip} placement="top">
|
||||||
|
{content()}
|
||||||
|
</TooltipV2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip appearance={props.inline ? "compact" : "standard"} value={props.tip} placement="top">
|
<Tooltip value={props.tip} placement="top">
|
||||||
{content()}
|
{content()}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { Dialog, DialogBody } from "@opencode-ai/ui/dialog"
|
import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
|
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command"
|
import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command"
|
||||||
@@ -191,7 +191,7 @@ function CommandPaletteView(props: {
|
|||||||
<Dialog class="command-palette-v2" size="large">
|
<Dialog class="command-palette-v2" size="large">
|
||||||
<DialogBody class="command-palette-v2-body">
|
<DialogBody class="command-palette-v2-body">
|
||||||
<div class="command-palette-v2-search">
|
<div class="command-palette-v2-search">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
value={query()}
|
value={query()}
|
||||||
autofocus
|
autofocus
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -295,7 +295,7 @@ function PaletteRow(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.item.keybind}>
|
<Show when={props.item.keybind}>
|
||||||
<Keybind keys={formatKeybindParts(props.item.keybind ?? "", props.language.t)} variant="neutral" />
|
<KeybindV2 keys={formatKeybindParts(props.item.keybind ?? "", props.language.t)} variant="neutral" />
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.item.type === "session"}>
|
<Match when={props.item.type === "session"}>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function ConnectProviderDialogStory() {
|
|||||||
onMount(open)
|
onMount(open)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="neutral" onClick={open}>
|
<Button variant="secondary" onClick={open}>
|
||||||
Open connect provider dialog
|
Open connect provider dialog
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
@@ -29,7 +29,7 @@ function ProviderConnectionDialogStory(props) {
|
|||||||
onMount(open)
|
onMount(open)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="neutral" onClick={open}>
|
<Button variant="secondary" onClick={open}>
|
||||||
Open {props.provider} connection dialog
|
Open {props.provider} connection dialog
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { List } from "@opencode-ai/ui/list"
|
|||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode-ai/ui/dialog"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
|
import { type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -74,7 +75,7 @@ export const DialogConnectProvider: Component<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<DialogV2
|
||||||
containerClass="!h-[min(calc(100vh_-_16px),512px)] !w-[min(calc(100vw_-_16px),640px)]"
|
containerClass="!h-[min(calc(100vh_-_16px),512px)] !w-[min(calc(100vw_-_16px),640px)]"
|
||||||
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
|
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
|
||||||
>
|
>
|
||||||
@@ -98,7 +99,7 @@ export const DialogConnectProvider: Component<{
|
|||||||
<Content />
|
<Content />
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
</Dialog>
|
</DialogV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@ function ProviderPicker(props: { directory?: string; onSelect: (provider: string
|
|||||||
return (
|
return (
|
||||||
<div ref={picker} class="flex min-h-0 flex-1 flex-col gap-4" onKeyDown={handleKeyDown}>
|
<div ref={picker} class="flex min-h-0 flex-1 flex-col gap-4" onKeyDown={handleKeyDown}>
|
||||||
<div class="shrink-0 px-1 pt-px">
|
<div class="shrink-0 px-1 pt-px">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
ref={search}
|
ref={search}
|
||||||
type="search"
|
type="search"
|
||||||
class="!w-full [font-family:var(--v2-font-family-sans)]"
|
class="!w-full [font-family:var(--v2-font-family-sans)]"
|
||||||
@@ -378,7 +379,7 @@ function ProviderConnection(props: {
|
|||||||
setFormStore("value", field.key, value)
|
setFormStore("value", field.key, value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button class="w-auto" type="submit" size="large" variant="contrast" disabled={!valid()}>
|
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
|
||||||
{language.t("common.continue")}
|
{language.t("common.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -515,7 +516,7 @@ function ProviderConnection(props: {
|
|||||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
|
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
|
||||||
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
|
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
|
||||||
{language.t("provider.connect.apiKey.label", { provider: provider().name })}
|
{language.t("provider.connect.apiKey.label", { provider: provider().name })}
|
||||||
<TextInput
|
<TextInputV2
|
||||||
ref={apiKey}
|
ref={apiKey}
|
||||||
class="!w-full"
|
class="!w-full"
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
@@ -536,9 +537,9 @@ function ProviderConnection(props: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Button type="submit" variant="contrast" data-action="provider-connect-submit">
|
<ButtonV2 type="submit" variant="contrast" data-action="provider-connect-submit">
|
||||||
{language.t("common.continue")}
|
{language.t("common.continue")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -584,7 +585,7 @@ function ProviderConnection(props: {
|
|||||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
|
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
|
||||||
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
|
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
|
||||||
{language.t("provider.connect.oauth.code.label", { method: controller.currentMethod()?.label ?? "" })}
|
{language.t("provider.connect.oauth.code.label", { method: controller.currentMethod()?.label ?? "" })}
|
||||||
<TextInput
|
<TextInputV2
|
||||||
ref={codeInput}
|
ref={codeInput}
|
||||||
class="!w-full"
|
class="!w-full"
|
||||||
name="code"
|
name="code"
|
||||||
@@ -604,9 +605,9 @@ function ProviderConnection(props: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Button type="submit" variant="contrast">
|
<ButtonV2 type="submit" variant="contrast">
|
||||||
{language.t("common.continue")}
|
{language.t("common.continue")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
@@ -22,21 +21,20 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog class="h-full">
|
<Dialog
|
||||||
<DialogHeader>
|
class="h-full"
|
||||||
<DialogTitle>
|
title={
|
||||||
<IconButton
|
<IconButton
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
icon={<Icon name="arrow-left" />}
|
icon="arrow-left"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={props.onBack}
|
onClick={props.onBack}
|
||||||
aria-label={language.t("common.goBack")}
|
aria-label={language.t("common.goBack")}
|
||||||
/>
|
/>
|
||||||
</DialogTitle>
|
}
|
||||||
</DialogHeader>
|
transition
|
||||||
<DialogBody>
|
>
|
||||||
<CustomProviderForm />
|
<CustomProviderForm />
|
||||||
</DialogBody>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -240,7 +238,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
|
|||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={<Icon name="trash" />}
|
icon="trash"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="mt-1.5"
|
class="mt-1.5"
|
||||||
onClick={() => removeModel(i())}
|
onClick={() => removeModel(i())}
|
||||||
@@ -284,7 +282,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
|
|||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={<Icon name="trash" />}
|
icon="trash"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="mt-1.5"
|
class="mt-1.5"
|
||||||
onClick={() => removeHeader(i())}
|
onClick={() => removeHeader(i())}
|
||||||
@@ -303,7 +301,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
|
|||||||
class="w-auto self-start"
|
class="w-auto self-start"
|
||||||
type="submit"
|
type="submit"
|
||||||
size="large"
|
size="large"
|
||||||
variant="contrast"
|
variant="primary"
|
||||||
disabled={saveMutation.isPending}
|
disabled={saveMutation.isPending}
|
||||||
>
|
>
|
||||||
{saveMutation.isPending ? language.t("common.saving") : language.t("common.submit")}
|
{saveMutation.isPending ? language.t("common.saving") : language.t("common.submit")}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Dialog, DialogFooter } from "@opencode-ai/ui/dialog"
|
import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Field } from "@opencode-ai/ui/field"
|
import { Field } from "@opencode-ai/ui/v2/field-v2"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/project-avatar"
|
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
||||||
import { Textarea } from "@opencode-ai/ui/textarea"
|
import { TextareaV2 } from "@opencode-ai/ui/v2/textarea-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { For, Show, createSignal, startTransition } from "solid-js"
|
import { For, Show, createSignal, startTransition } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
|
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
|
||||||
@@ -36,46 +37,46 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
|
|||||||
|
|
||||||
const Footer = () => (
|
const Footer = () => (
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
|
<ButtonV2 type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
|
<ButtonV2 type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
|
||||||
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
|
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="x-large" variant="settings" class="project-settings-v2-dialog">
|
<Dialog size="x-large" variant="settings" class="project-settings-v2-dialog">
|
||||||
<Tabs
|
<TabsV2
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
variant="settings"
|
variant="settings"
|
||||||
value={tab()}
|
value={tab()}
|
||||||
onChange={(value) => void startTransition(() => setTab(value))}
|
onChange={(value) => void startTransition(() => setTab(value))}
|
||||||
class="project-settings-v2"
|
class="project-settings-v2"
|
||||||
>
|
>
|
||||||
<Tabs.List>
|
<TabsV2.List>
|
||||||
<div class="project-settings-v2-nav">
|
<div class="project-settings-v2-nav">
|
||||||
<Tabs.Trigger value="general">
|
<TabsV2.Trigger value="general">
|
||||||
<ProjectAvatar
|
<ProjectAvatar
|
||||||
fallback={projectName()}
|
fallback={projectName()}
|
||||||
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
||||||
class="!size-4 shrink-0"
|
class="!size-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span class="truncate">{projectName()}</span>
|
<span class="truncate">{projectName()}</span>
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="scripts">
|
<TabsV2.Trigger value="scripts">
|
||||||
<Icon name="code" size="small" />
|
<Icon name="code" size="small" />
|
||||||
{language.t("project.settings.scripts")}
|
{language.t("project.settings.scripts")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="extensions">
|
<TabsV2.Trigger value="extensions">
|
||||||
<Icon name="extensions" size="small" />
|
<Icon name="extensions" size="small" />
|
||||||
{language.t("settings.tab.extensions")}
|
{language.t("settings.tab.extensions")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.List>
|
</TabsV2.List>
|
||||||
|
|
||||||
<Tabs.Content value="general" class="project-settings-v2-panel">
|
<TabsV2.Content value="general" class="project-settings-v2-panel">
|
||||||
<form onSubmit={model.submit} class="project-settings-v2-form">
|
<form onSubmit={model.submit} class="project-settings-v2-form">
|
||||||
<div class="project-settings-v2-scroll">
|
<div class="project-settings-v2-scroll">
|
||||||
<div class="project-settings-page-header">
|
<div class="project-settings-page-header">
|
||||||
@@ -85,7 +86,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
|
|||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<Field.Label>{language.t("dialog.project.edit.name")}</Field.Label>
|
<Field.Label>{language.t("dialog.project.edit.name")}</Field.Label>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
autofocus
|
autofocus
|
||||||
appearance="large"
|
appearance="large"
|
||||||
class="!w-full"
|
class="!w-full"
|
||||||
@@ -131,7 +132,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
|
|||||||
"opacity-0": !model.store.iconHover,
|
"opacity-0": !model.store.iconHover,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name={model.store.iconOverride ? "close" : "share"} />
|
<IconV2 name={model.store.iconOverride ? "close" : "share"} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
@@ -187,9 +188,9 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
|
|||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</form>
|
</form>
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
|
|
||||||
<Tabs.Content value="scripts" class="project-settings-v2-panel">
|
<TabsV2.Content value="scripts" class="project-settings-v2-panel">
|
||||||
<form onSubmit={model.submit} class="project-settings-v2-form">
|
<form onSubmit={model.submit} class="project-settings-v2-form">
|
||||||
<div class="project-settings-v2-scroll">
|
<div class="project-settings-v2-scroll">
|
||||||
<div class="project-settings-page-header">
|
<div class="project-settings-page-header">
|
||||||
@@ -199,7 +200,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
|
|||||||
<Field>
|
<Field>
|
||||||
<Field.Label>{language.t("dialog.project.edit.worktree.startup")}</Field.Label>
|
<Field.Label>{language.t("dialog.project.edit.worktree.startup")}</Field.Label>
|
||||||
<Field.Prefix>{language.t("dialog.project.edit.worktree.startup.description")}</Field.Prefix>
|
<Field.Prefix>{language.t("dialog.project.edit.worktree.startup.description")}</Field.Prefix>
|
||||||
<Textarea
|
<TextareaV2
|
||||||
class="!w-full [&_[data-slot=textarea-v2-textarea]]:font-mono"
|
class="!w-full [&_[data-slot=textarea-v2-textarea]]:font-mono"
|
||||||
rows={5}
|
rows={5}
|
||||||
value={model.store.startup}
|
value={model.store.startup}
|
||||||
@@ -211,12 +212,12 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
|
|||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</form>
|
</form>
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
|
|
||||||
<Tabs.Content value="extensions" class="project-settings-v2-panel">
|
<TabsV2.Content value="extensions" class="project-settings-v2-panel">
|
||||||
<ProjectSettingsExtensions />
|
<ProjectSettingsExtensions />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
</Tabs>
|
</TabsV2>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from "@solidjs/router"
|
|||||||
import { useData } from "@/context/server"
|
import { useData } from "@/context/server"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -94,28 +94,23 @@ export const DialogFork: Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog title={language.t("command.session.fork")}>
|
||||||
<DialogHeader>
|
<List
|
||||||
<DialogTitle>{language.t("command.session.fork")}</DialogTitle>
|
class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
||||||
</DialogHeader>
|
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
||||||
<DialogBody>
|
emptyMessage={language.t("dialog.fork.empty")}
|
||||||
<List
|
key={(x) => x.id}
|
||||||
class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
items={messages}
|
||||||
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
filterKeys={["text"]}
|
||||||
emptyMessage={language.t("dialog.fork.empty")}
|
onSelect={handleSelect}
|
||||||
key={(x) => x.id}
|
>
|
||||||
items={messages}
|
{(item) => (
|
||||||
filterKeys={["text"]}
|
<div class="w-full flex items-center gap-2">
|
||||||
onSelect={handleSelect}
|
<span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
|
||||||
>
|
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
|
||||||
{(item) => (
|
</div>
|
||||||
<div class="w-full flex items-center gap-2">
|
)}
|
||||||
<span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
|
</List>
|
||||||
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</DialogBody>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
|
import { Dialog as DialogV2, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
|
import { Switch as SwitchV2 } from "@opencode-ai/ui/v2/switch-v2"
|
||||||
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { For, Show, type Component } from "solid-js"
|
import { For, Show, type Component } from "solid-js"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
@@ -41,80 +44,74 @@ export const DialogManageModels: Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog
|
||||||
<DialogHeader hideClose>
|
title={language.t("dialog.model.manage")}
|
||||||
<DialogTitleGroup
|
description={language.t("dialog.model.manage.description")}
|
||||||
title={language.t("dialog.model.manage")}
|
action={
|
||||||
description={language.t("dialog.model.manage.description")}
|
|
||||||
/>
|
|
||||||
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={handleConnectProvider}>
|
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={handleConnectProvider}>
|
||||||
{language.t("command.provider.connect")}
|
{language.t("command.provider.connect")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogHeader>
|
}
|
||||||
<DialogBody>
|
>
|
||||||
<List
|
<List
|
||||||
class="px-3"
|
class="px-3"
|
||||||
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
||||||
emptyMessage={language.t("dialog.model.empty")}
|
emptyMessage={language.t("dialog.model.empty")}
|
||||||
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
||||||
items={local.model.list()}
|
items={local.model.list()}
|
||||||
filterKeys={["provider.name", "name", "id"]}
|
filterKeys={["provider.name", "name", "id"]}
|
||||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||||
groupBy={(x) => x.provider.id}
|
groupBy={(x) => x.provider.id}
|
||||||
groupHeader={(group) => {
|
groupHeader={(group) => {
|
||||||
const provider = group.items[0].provider
|
const provider = group.items[0].provider
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span>{provider.name}</span>
|
<span>{provider.name}</span>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
appearance="standard"
|
placement="top"
|
||||||
placement="top"
|
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
|
||||||
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
|
>
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
appearance="standard"
|
|
||||||
class="-mr-1"
|
|
||||||
checked={providerVisible(provider.id)}
|
|
||||||
onChange={(checked) => setProviderVisibility(provider.id, checked)}
|
|
||||||
hideLabel
|
|
||||||
>
|
|
||||||
{provider.name}
|
|
||||||
</Switch>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
sortGroupsBy={(a, b) => {
|
|
||||||
const aRank = providerRank(a.items[0].provider.id)
|
|
||||||
const bRank = providerRank(b.items[0].provider.id)
|
|
||||||
const aPopular = aRank >= 0
|
|
||||||
const bPopular = bRank >= 0
|
|
||||||
if (aPopular && !bPopular) return -1
|
|
||||||
if (!aPopular && bPopular) return 1
|
|
||||||
return aRank - bRank
|
|
||||||
}}
|
|
||||||
onSelect={(x) => {
|
|
||||||
if (!x) return
|
|
||||||
const key = { modelID: x.id, providerID: x.provider.id }
|
|
||||||
local.model.setVisibility(key, !local.model.visible(key))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(i) => (
|
|
||||||
<div class="w-full flex items-center justify-between gap-x-3">
|
|
||||||
<span>{i.name}</span>
|
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
|
||||||
<Switch
|
<Switch
|
||||||
appearance="standard"
|
class="-mr-1"
|
||||||
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
|
checked={providerVisible(provider.id)}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => setProviderVisibility(provider.id, checked)}
|
||||||
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
hideLabel
|
||||||
}}
|
>
|
||||||
/>
|
{provider.name}
|
||||||
</div>
|
</Switch>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
sortGroupsBy={(a, b) => {
|
||||||
|
const aRank = providerRank(a.items[0].provider.id)
|
||||||
|
const bRank = providerRank(b.items[0].provider.id)
|
||||||
|
const aPopular = aRank >= 0
|
||||||
|
const bPopular = bRank >= 0
|
||||||
|
if (aPopular && !bPopular) return -1
|
||||||
|
if (!aPopular && bPopular) return 1
|
||||||
|
return aRank - bRank
|
||||||
|
}}
|
||||||
|
onSelect={(x) => {
|
||||||
|
if (!x) return
|
||||||
|
const key = { modelID: x.id, providerID: x.provider.id }
|
||||||
|
local.model.setVisibility(key, !local.model.visible(key))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(i) => (
|
||||||
|
<div class="w-full flex items-center justify-between gap-x-3">
|
||||||
|
<span>{i.name}</span>
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Switch
|
||||||
|
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
|
||||||
|
onChange={(checked) => {
|
||||||
|
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</List>
|
)}
|
||||||
</DialogBody>
|
</List>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -157,20 +154,20 @@ export const DialogManageModelsV2: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="large" variant="settings" class="settings-v2-manage-models-dialog">
|
<DialogV2 size="large" variant="settings" class="settings-v2-manage-models-dialog">
|
||||||
<DialogHeader hideClose={true} closeLabel={language.t("common.close")}>
|
<DialogHeader hideClose={true} closeLabel={language.t("common.close")}>
|
||||||
<DialogTitleGroup
|
<DialogTitleGroup
|
||||||
title={language.t("dialog.model.manage")}
|
title={language.t("dialog.model.manage")}
|
||||||
description={language.t("dialog.model.manage.description")}
|
description={language.t("dialog.model.manage.description")}
|
||||||
/>
|
/>
|
||||||
<Button variant="neutral" icon="plus" onClick={handleConnectProvider}>
|
<ButtonV2 variant="neutral" icon="plus" onClick={handleConnectProvider}>
|
||||||
{language.t("command.provider.connect")}
|
{language.t("command.provider.connect")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogBody class="flex min-h-0 flex-1 flex-col">
|
<DialogBody class="flex min-h-0 flex-1 flex-col">
|
||||||
<div class="px-4 pt-px pb-3">
|
<div class="px-4 pt-px pb-3">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
type="search"
|
type="search"
|
||||||
appearance="base"
|
appearance="base"
|
||||||
class="!w-full self-stretch"
|
class="!w-full self-stretch"
|
||||||
@@ -185,12 +182,12 @@ export const DialogManageModelsV2: Component = () => {
|
|||||||
aria-label={language.t("dialog.model.search.placeholder")}
|
aria-label={language.t("dialog.model.search.placeholder")}
|
||||||
/>
|
/>
|
||||||
<Show when={list.filter()}>
|
<Show when={list.filter()}>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
class="settings-v2-tab-search-clear"
|
class="settings-v2-tab-search-clear"
|
||||||
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
|
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||||
onClick={() => list.clear()}
|
onClick={() => list.clear()}
|
||||||
aria-label={language.t("common.clear")}
|
aria-label={language.t("common.clear")}
|
||||||
/>
|
/>
|
||||||
@@ -228,14 +225,14 @@ export const DialogManageModelsV2: Component = () => {
|
|||||||
<h3 class="settings-v2-section-title">{group.items[0].provider.name}</h3>
|
<h3 class="settings-v2-section-title">{group.items[0].provider.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Switch
|
<SwitchV2
|
||||||
class="mr-6"
|
class="mr-6"
|
||||||
checked={providerVisible(group.category)}
|
checked={providerVisible(group.category)}
|
||||||
onChange={(checked) => setProviderVisibility(group.category, checked)}
|
onChange={(checked) => setProviderVisibility(group.category, checked)}
|
||||||
hideLabel
|
hideLabel
|
||||||
>
|
>
|
||||||
{group.items[0].provider.name}
|
{group.items[0].provider.name}
|
||||||
</Switch>
|
</SwitchV2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SettingsListV2>
|
<SettingsListV2>
|
||||||
@@ -243,13 +240,13 @@ export const DialogManageModelsV2: Component = () => {
|
|||||||
{(item) => (
|
{(item) => (
|
||||||
<SettingsRowV2 title={item.name} description="">
|
<SettingsRowV2 title={item.name} description="">
|
||||||
<div>
|
<div>
|
||||||
<Switch
|
<SwitchV2
|
||||||
checked={local.model.visible({ modelID: item.id, providerID: item.provider.id })}
|
checked={local.model.visible({ modelID: item.id, providerID: item.provider.id })}
|
||||||
onChange={(checked) => setModelVisibility(item, checked)}
|
onChange={(checked) => setModelVisibility(item, checked)}
|
||||||
hideLabel
|
hideLabel
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Switch>
|
</SwitchV2>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRowV2>
|
</SettingsRowV2>
|
||||||
)}
|
)}
|
||||||
@@ -263,6 +260,6 @@ export const DialogManageModelsV2: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
</Dialog>
|
</DialogV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
|||||||
<Show
|
<Show
|
||||||
when={isLast()}
|
when={isLast()}
|
||||||
fallback={
|
fallback={
|
||||||
<Button variant="neutral" size="large" onClick={handleNext}>
|
<Button variant="secondary" size="large" onClick={handleNext}>
|
||||||
{language.t("dialog.releaseNotes.action.next")}
|
{language.t("dialog.releaseNotes.action.next")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button variant="contrast" size="large" onClick={handleClose}>
|
<Button variant="primary" size="large" onClick={handleClose}>
|
||||||
{language.t("dialog.releaseNotes.action.getStarted")}
|
{language.t("dialog.releaseNotes.action.getStarted")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import "@pierre/trees/web-components"
|
import "@pierre/trees/web-components"
|
||||||
import { FileTree } from "@pierre/trees"
|
import { FileTree } from "@pierre/trees"
|
||||||
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
|
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { createEffect, createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"
|
import { createEffect, createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
pickerRoot,
|
pickerRoot,
|
||||||
} from "./directory-picker-domain"
|
} from "./directory-picker-domain"
|
||||||
import "./dialog-select-directory-v2.css"
|
import "./dialog-select-directory-v2.css"
|
||||||
import { Divider } from "@opencode-ai/ui/divider"
|
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
|
|
||||||
interface DialogSelectDirectoryV2Props {
|
interface DialogSelectDirectoryV2Props {
|
||||||
@@ -293,10 +293,10 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{props.title ?? language.t("command.project.open")}</DialogTitle>
|
<DialogTitle>{props.title ?? language.t("command.project.open")}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Divider />
|
<DividerV2 />
|
||||||
<DialogBody class="directory-picker-v2-body pt-4!">
|
<DialogBody class="directory-picker-v2-body pt-4!">
|
||||||
<div class="directory-picker-v2-path" ref={pathArea}>
|
<div class="directory-picker-v2-path" ref={pathArea}>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
value={input()}
|
value={input()}
|
||||||
autofocus
|
autofocus
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -318,15 +318,15 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
|||||||
onKeyDown={handleInputKey}
|
onKeyDown={handleInputKey}
|
||||||
/>
|
/>
|
||||||
<div class="directory-picker-v2-actions">
|
<div class="directory-picker-v2-actions">
|
||||||
<Button size="small" variant="ghost" onClick={() => void navigate(home())}>
|
<ButtonV2 size="small" variant="ghost" onClick={() => void navigate(home())}>
|
||||||
~
|
~
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button size="small" variant="ghost" onClick={() => void navigate(pickerRoot(root()) || root())}>
|
<ButtonV2 size="small" variant="ghost" onClick={() => void navigate(pickerRoot(root()) || root())}>
|
||||||
{language.t("dialog.directory.root")}
|
{language.t("dialog.directory.root")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button size="small" variant="ghost" onClick={() => void navigate(pickerParent(root()))}>
|
<ButtonV2 size="small" variant="ghost" onClick={() => void navigate(pickerParent(root()))}>
|
||||||
{language.t("dialog.directory.parent")}
|
{language.t("dialog.directory.parent")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
<Show when={suggestionsOpen() && currentSuggestions().length > 0}>
|
<Show when={suggestionsOpen() && currentSuggestions().length > 0}>
|
||||||
<div id="directory-picker-v2-suggestions" role="listbox" class="directory-picker-v2-suggestions">
|
<div id="directory-picker-v2-suggestions" role="listbox" class="directory-picker-v2-suggestions">
|
||||||
@@ -379,12 +379,12 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
|||||||
<div class="directory-picker-v2-selection">{policy.result(root(), selected(), rootValid())}</div>
|
<div class="directory-picker-v2-selection">{policy.result(root(), selected(), rootValid())}</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="neutral" onClick={() => dialog.close()}>
|
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button variant="contrast" disabled={!policy.result(root(), selected(), rootValid())} onClick={resolve}>
|
<ButtonV2 variant="contrast" disabled={!policy.result(root(), selected(), rootValid())} onClick={resolve}>
|
||||||
{action[policy.action]}
|
{action[policy.action]}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
|
import { List } from "@opencode-ai/ui/list"
|
||||||
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
|
import { createMemo, createSignal, lazy, Match, Show, Switch } from "solid-js"
|
||||||
|
import { formatKeybind } from "@/context/command"
|
||||||
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
|
import { decode64 } from "@/utils/base64"
|
||||||
|
import { getRelativeTime } from "@/utils/time"
|
||||||
|
import {
|
||||||
|
createCommandPaletteFileEntry,
|
||||||
|
createCommandPaletteFileOpener,
|
||||||
|
createCommandPaletteModel,
|
||||||
|
uniqueCommandPaletteEntries,
|
||||||
|
type CommandPaletteEntry,
|
||||||
|
} from "./command-palette"
|
||||||
|
import { DialogCommandPaletteV2 } from "./dialog-command-palette-v2"
|
||||||
|
|
||||||
|
const DialogSelectFileV2 = lazy(() =>
|
||||||
|
import("./dialog-select-directory-v2").then((module) => ({ default: module.DialogSelectDirectoryV2 })),
|
||||||
|
)
|
||||||
|
type DialogSelectFileMode = "all" | "files"
|
||||||
|
|
||||||
|
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const filesOnly = () => props.mode === "files"
|
||||||
|
|
||||||
|
if (!filesOnly()) {
|
||||||
|
return <DialogCommandPaletteV2 onOpenFile={props.onOpenFile} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesOnly() && platform.platform === "desktop") {
|
||||||
|
return <DialogSelectFileDesktopV2 onOpenFile={props.onOpenFile} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DialogSelectFileLegacy filesOnly={filesOnly} onOpenFile={props.onOpenFile} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogSelectFileDesktopV2(props: { onOpenFile?: (path: string) => void }) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const serverSDK = useServerSDK()
|
||||||
|
const { params } = useSessionLayout()
|
||||||
|
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||||
|
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogSelectFileV2
|
||||||
|
server={serverSDK.server}
|
||||||
|
mode="file"
|
||||||
|
start={projectDirectory()}
|
||||||
|
title={language.t("session.header.searchFiles")}
|
||||||
|
onSelect={(result) => {
|
||||||
|
if (typeof result !== "string") return
|
||||||
|
openFile(result)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?: (path: string) => void }) {
|
||||||
|
const palette = createCommandPaletteModel(props)
|
||||||
|
const [grouped, setGrouped] = createSignal(false)
|
||||||
|
|
||||||
|
const items = async (text: string) => {
|
||||||
|
const query = text.trim()
|
||||||
|
setGrouped(query.length > 0)
|
||||||
|
|
||||||
|
if (!query && props.filesOnly()) {
|
||||||
|
const loaded = palette.file.tree.state("")?.loaded
|
||||||
|
const pending = loaded ? Promise.resolve() : palette.file.tree.list("")
|
||||||
|
const next = uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
|
||||||
|
|
||||||
|
if (loaded || next.length > 0) {
|
||||||
|
void pending
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
await pending
|
||||||
|
return uniqueCommandPaletteEntries([...palette.recentFileEntries(), ...palette.rootFileEntries()])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
||||||
|
|
||||||
|
if (props.filesOnly()) {
|
||||||
|
const files = await palette.file.searchFiles(query)
|
||||||
|
const category = palette.language.t("palette.group.files")
|
||||||
|
return files.map((path) => createCommandPaletteFileEntry(path, category))
|
||||||
|
}
|
||||||
|
|
||||||
|
const [files, nextSessions] = await Promise.all([
|
||||||
|
palette.file.searchFiles(query),
|
||||||
|
Promise.resolve(palette.sessions(query)),
|
||||||
|
])
|
||||||
|
const category = palette.language.t("palette.group.files")
|
||||||
|
const entries = files.map((path) => createCommandPaletteFileEntry(path, category))
|
||||||
|
return [...palette.commandEntries(), ...nextSessions, ...entries]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
|
||||||
|
<List
|
||||||
|
class="px-3"
|
||||||
|
search={{
|
||||||
|
placeholder: props.filesOnly()
|
||||||
|
? palette.language.t("session.header.searchFiles")
|
||||||
|
: palette.language.t("palette.search.placeholder"),
|
||||||
|
autofocus: true,
|
||||||
|
hideIcon: true,
|
||||||
|
}}
|
||||||
|
emptyMessage={palette.language.t("palette.empty")}
|
||||||
|
loadingMessage={palette.language.t("common.loading")}
|
||||||
|
items={items}
|
||||||
|
key={(item) => item.id}
|
||||||
|
filterKeys={["title", "description", "category"]}
|
||||||
|
skipFilter={(item) => item.type === "file"}
|
||||||
|
groupBy={grouped() ? (item) => item.category : () => ""}
|
||||||
|
onMove={(item: CommandPaletteEntry | undefined) => palette.highlight(item)}
|
||||||
|
onSelect={(item: CommandPaletteEntry | undefined) => palette.select(item)}
|
||||||
|
>
|
||||||
|
{(item) => (
|
||||||
|
<Switch
|
||||||
|
fallback={
|
||||||
|
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Match when={item.type === "command"}>
|
||||||
|
<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}>
|
||||||
|
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", palette.language.t)}</Keybind>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={item.type === "session"}>
|
||||||
|
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||||
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
|
<Icon name="bubble-5" size="small" class="shrink-0 text-icon-weak" />
|
||||||
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
<span
|
||||||
|
class="text-14-regular text-text-strong truncate"
|
||||||
|
classList={{ "opacity-70": !!item.archived }}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<Show when={item.description}>
|
||||||
|
<span
|
||||||
|
class="text-14-regular text-text-weak truncate"
|
||||||
|
classList={{ "opacity-70": !!item.archived }}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={item.updated}>
|
||||||
|
<span class="text-12-regular text-text-weak whitespace-nowrap ml-2">
|
||||||
|
{getRelativeTime(new Date(item.updated!).toISOString(), palette.language.t)}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, createMemo, Show } from "solid-js"
|
import { Component, createMemo, Show } from "solid-js"
|
||||||
import { useData } from "@/context/server"
|
import { useData } from "@/context/server"
|
||||||
import { useWorkspaceLocation } from "@/context/location"
|
import { useWorkspaceLocation } from "@/context/location"
|
||||||
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -31,71 +31,65 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
const totalCount = createMemo(() => items().length)
|
const totalCount = createMemo(() => items().length)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog
|
||||||
<DialogHeader>
|
title={language.t("dialog.mcp.title")}
|
||||||
<DialogTitleGroup
|
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
|
||||||
title={language.t("dialog.mcp.title")}
|
>
|
||||||
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
|
<List
|
||||||
/>
|
class="px-3"
|
||||||
</DialogHeader>
|
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
||||||
<DialogBody>
|
emptyMessage={language.t("dialog.mcp.empty")}
|
||||||
<List
|
key={(x) => x?.name ?? ""}
|
||||||
class="px-3"
|
items={items}
|
||||||
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
filterKeys={["name", "status"]}
|
||||||
emptyMessage={language.t("dialog.mcp.empty")}
|
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||||
key={(x) => x?.name ?? ""}
|
onSelect={(x) => {
|
||||||
items={items}
|
if (!x || x.status === "pending" || toggle.isPending) return
|
||||||
filterKeys={["name", "status"]}
|
toggle.mutate(x.name)
|
||||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
}}
|
||||||
onSelect={(x) => {
|
>
|
||||||
if (!x || x.status === "pending" || toggle.isPending) return
|
{(i) => {
|
||||||
toggle.mutate(x.name)
|
const mcpStatus = () =>
|
||||||
}}
|
data.location.mcp.server.list({ directory: sdk().directory })?.find((server) => server.name === i.name)
|
||||||
>
|
?.status
|
||||||
{(i) => {
|
const status = () => mcpStatus()?.status
|
||||||
const mcpStatus = () =>
|
const statusLabel = () => {
|
||||||
data.location.mcp.server.list({ directory: sdk().directory })?.find((server) => server.name === i.name)
|
const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
|
||||||
?.status
|
if (!key) return
|
||||||
const status = () => mcpStatus()?.status
|
return language.t(key)
|
||||||
const statusLabel = () => {
|
}
|
||||||
const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
|
const error = () => {
|
||||||
if (!key) return
|
const s = mcpStatus()
|
||||||
return language.t(key)
|
if (s?.status === "failed") return s.error
|
||||||
}
|
}
|
||||||
const error = () => {
|
const enabled = () => status() === "connected"
|
||||||
const s = mcpStatus()
|
return (
|
||||||
if (s?.status === "failed") return s.error
|
<div class="w-full flex items-center justify-between gap-x-3">
|
||||||
}
|
<div class="flex flex-col gap-0.5 min-w-0">
|
||||||
const enabled = () => status() === "connected"
|
<div class="flex items-center gap-2">
|
||||||
return (
|
<span class="truncate">{i.name}</span>
|
||||||
<div class="w-full flex items-center justify-between gap-x-3">
|
<Show when={statusLabel()}>
|
||||||
<div class="flex flex-col gap-0.5 min-w-0">
|
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="truncate">{i.name}</span>
|
|
||||||
<Show when={statusLabel()}>
|
|
||||||
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<Show when={error()}>
|
|
||||||
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<Show when={error()}>
|
||||||
<Switch
|
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
||||||
appearance="standard"
|
</Show>
|
||||||
checked={enabled()}
|
|
||||||
disabled={status() === "pending" || (toggle.isPending && toggle.variables === i.name)}
|
|
||||||
onChange={() => {
|
|
||||||
if (toggle.isPending) return
|
|
||||||
toggle.mutate(i.name)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
}}
|
<Switch
|
||||||
</List>
|
checked={enabled()}
|
||||||
</DialogBody>
|
disabled={status() === "pending" || (toggle.isPending && toggle.variables === i.name)}
|
||||||
|
onChange={() => {
|
||||||
|
if (toggle.isPending) return
|
||||||
|
toggle.mutate(i.name)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</List>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function SelectModelWithoutProviders() {
|
|||||||
onMount(open)
|
onMount(open)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="neutral" onClick={open}>
|
<Button variant="secondary" onClick={open}>
|
||||||
Open select model dialog
|
Open select model dialog
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode-ai/ui/dialog"
|
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Badge } from "@opencode-ai/ui/badge"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useTheme } from "@opencode-ai/ui/theme"
|
import { useTheme } from "@opencode-ai/ui/theme"
|
||||||
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
|
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
|
||||||
@@ -66,7 +66,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<DialogV2
|
||||||
fit
|
fit
|
||||||
containerClass="!h-auto max-h-[calc(100vh_-_16px)] !w-[min(calc(100vw_-_16px),640px)]"
|
containerClass="!h-auto max-h-[calc(100vh_-_16px)] !w-[min(calc(100vw_-_16px),640px)]"
|
||||||
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
|
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
|
||||||
@@ -84,7 +84,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
|||||||
</div>
|
</div>
|
||||||
<For each={freeModels()}>
|
<For each={freeModels()}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
gutter={6}
|
gutter={6}
|
||||||
@@ -105,15 +105,15 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
|||||||
onClick={() => selectModel(item)}
|
onClick={() => selectModel(item)}
|
||||||
>
|
>
|
||||||
<span class="min-w-0 truncate">{displayModelName(item.name)}</span>
|
<span class="min-w-0 truncate">{displayModelName(item.name)}</span>
|
||||||
<Badge class="shrink-0">{language.t("model.tag.free")}</Badge>
|
<Tag class="shrink-0">{language.t("model.tag.free")}</Tag>
|
||||||
<Show when={item.latest}>
|
<Show when={item.latest}>
|
||||||
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
|
<Tag class="shrink-0">{language.t("model.tag.latest")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={currentKey() === modelKey(item)}>
|
<Show when={currentKey() === modelKey(item)}>
|
||||||
<Icon name="check" class="ml-auto size-4 shrink-0 text-v2-icon-icon-base" />
|
<Icon name="check" class="ml-auto size-4 shrink-0 text-v2-icon-icon-base" />
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,6 +172,6 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
</Dialog>
|
</DialogV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Badge } from "@opencode-ai/ui/badge"
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { type Component, Show } from "solid-js"
|
import { type Component, Show } from "solid-js"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
@@ -40,113 +40,108 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none">
|
<Dialog
|
||||||
<DialogHeader>
|
title={language.t("dialog.model.select.title")}
|
||||||
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
|
class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none"
|
||||||
</DialogHeader>
|
>
|
||||||
<DialogBody>
|
<div class="flex flex-col gap-3 px-2.5" onKeyDown={handleKeyDown}>
|
||||||
<div class="flex flex-col gap-3 px-2.5" onKeyDown={handleKeyDown}>
|
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
|
||||||
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
|
<List
|
||||||
<List
|
class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
|
||||||
class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
|
ref={(ref) => (listRef = ref)}
|
||||||
ref={(ref) => (listRef = ref)}
|
items={model.list}
|
||||||
items={model.list}
|
current={model.current()}
|
||||||
current={model.current()}
|
key={(x) => `${x.provider.id}:${x.id}`}
|
||||||
key={(x) => `${x.provider.id}:${x.id}`}
|
itemWrapper={(item, node) => (
|
||||||
itemWrapper={(item, node) => (
|
<Tooltip
|
||||||
<Tooltip
|
class="w-full"
|
||||||
appearance="standard"
|
placement="right-start"
|
||||||
class="w-full"
|
gutter={12}
|
||||||
placement="right-start"
|
value={
|
||||||
gutter={12}
|
<ModelTooltip
|
||||||
value={
|
model={item}
|
||||||
<ModelTooltip
|
latest={item.latest}
|
||||||
model={item}
|
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
|
||||||
latest={item.latest}
|
/>
|
||||||
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
|
}
|
||||||
/>
|
>
|
||||||
}
|
{node}
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
onSelect={(x) => {
|
||||||
|
model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||||
|
recent: true,
|
||||||
|
})
|
||||||
|
dialog.close()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(i) => (
|
||||||
|
<div class="w-full flex items-center gap-x-2.5">
|
||||||
|
<span>{i.name}</span>
|
||||||
|
<Tag>{language.t("model.tag.free")}</Tag>
|
||||||
|
<Show when={i.latest}>
|
||||||
|
<Tag>{language.t("model.tag.latest")}</Tag>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
<div class="px-1.5 pb-1.5">
|
||||||
|
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
|
||||||
|
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
|
||||||
|
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<List
|
||||||
|
class="w-full px-3"
|
||||||
|
key={(p) => p.id}
|
||||||
|
items={providers.popular}
|
||||||
|
activeIcon="plus-small"
|
||||||
|
sortBy={(a, b) => {
|
||||||
|
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||||
|
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}}
|
||||||
|
onSelect={(x) => {
|
||||||
|
if (!x) return
|
||||||
|
connect(x.id)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{node}
|
{(i) => (
|
||||||
</Tooltip>
|
<div class="w-full flex items-center gap-x-3">
|
||||||
)}
|
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
|
||||||
onSelect={(x) => {
|
<span>{i.name}</span>
|
||||||
model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
<Show when={i.id === "opencode"}>
|
||||||
recent: true,
|
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
||||||
})
|
</Show>
|
||||||
dialog.close()
|
<Show when={i.id === "opencode"}>
|
||||||
}}
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
>
|
</Show>
|
||||||
{(i) => (
|
<Show when={i.id === "opencode-go"}>
|
||||||
<div class="w-full flex items-center gap-x-2.5">
|
<>
|
||||||
<span>{i.name}</span>
|
|
||||||
<Badge appearance="standard">{language.t("model.tag.free")}</Badge>
|
|
||||||
<Show when={i.latest}>
|
|
||||||
<Badge appearance="standard">{language.t("model.tag.latest")}</Badge>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
<div class="px-1.5 pb-1.5">
|
|
||||||
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
|
|
||||||
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
|
|
||||||
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<List
|
|
||||||
class="w-full px-3"
|
|
||||||
key={(p) => p.id}
|
|
||||||
items={providers.popular}
|
|
||||||
activeIcon="plus-small"
|
|
||||||
sortBy={(a, b) => {
|
|
||||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
|
||||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
}}
|
|
||||||
onSelect={(x) => {
|
|
||||||
if (!x) return
|
|
||||||
connect(x.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(i) => (
|
|
||||||
<div class="w-full flex items-center gap-x-3">
|
|
||||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
|
|
||||||
<span>{i.name}</span>
|
|
||||||
<Show when={i.id === "opencode"}>
|
|
||||||
<div class="text-14-regular text-text-weak">
|
<div class="text-14-regular text-text-weak">
|
||||||
{language.t("dialog.provider.opencode.tagline")}
|
{language.t("dialog.provider.opencodeGo.tagline")}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
<Show when={i.id === "opencode"}>
|
</>
|
||||||
<Badge appearance="standard">{language.t("dialog.provider.tag.recommended")}</Badge>
|
</Show>
|
||||||
</Show>
|
<Show when={i.id === "anthropic"}>
|
||||||
<Show when={i.id === "opencode-go"}>
|
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
||||||
<>
|
</Show>
|
||||||
<div class="text-14-regular text-text-weak">
|
</div>
|
||||||
{language.t("dialog.provider.opencodeGo.tagline")}
|
)}
|
||||||
</div>
|
</List>
|
||||||
<Badge appearance="standard">{language.t("dialog.provider.tag.recommended")}</Badge>
|
<Button
|
||||||
</>
|
variant="ghost"
|
||||||
</Show>
|
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
||||||
<Show when={i.id === "anthropic"}>
|
icon="dot-grid"
|
||||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
onClick={all}
|
||||||
</Show>
|
>
|
||||||
</div>
|
{language.t("dialog.provider.viewAll")}
|
||||||
)}
|
</Button>
|
||||||
</List>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
|
||||||
icon="dot-grid"
|
|
||||||
onClick={all}
|
|
||||||
>
|
|
||||||
{language.t("dialog.provider.viewAll")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import { useLocal } from "@/context/local"
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Badge } from "@opencode-ai/ui/badge"
|
|
||||||
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { Tag as TagV2 } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { ModelTooltip } from "./model-tooltip"
|
import { ModelTooltip } from "./model-tooltip"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
@@ -79,7 +81,6 @@ const ModelList: Component<{
|
|||||||
}}
|
}}
|
||||||
itemWrapper={(item, node) => (
|
itemWrapper={(item, node) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
appearance="standard"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
gutter={12}
|
gutter={12}
|
||||||
@@ -100,10 +101,10 @@ const ModelList: Component<{
|
|||||||
<div class="w-full flex items-center gap-x-2 text-13-regular">
|
<div class="w-full flex items-center gap-x-2 text-13-regular">
|
||||||
<span class="truncate">{i.name}</span>
|
<span class="truncate">{i.name}</span>
|
||||||
<Show when={isFree(i.provider.id, i.cost)}>
|
<Show when={isFree(i.provider.id, i.cost)}>
|
||||||
<Badge appearance="standard">{language.t("model.tag.free")}</Badge>
|
<Tag>{language.t("model.tag.free")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={i.latest}>
|
<Show when={i.latest}>
|
||||||
<Badge appearance="standard">{language.t("model.tag.latest")}</Badge>
|
<Tag>{language.t("model.tag.latest")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -192,19 +193,21 @@ export function ModelSelectorPopover(props: {
|
|||||||
class="p-1"
|
class="p-1"
|
||||||
action={
|
action={
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Tooltip appearance="standard" placement="top" value={language.t("command.provider.connect")}>
|
<Tooltip placement="top" value={language.t("command.provider.connect")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<Icon name="plus-small" />}
|
icon="plus-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
iconSize="normal"
|
||||||
class="size-6"
|
class="size-6"
|
||||||
aria-label={language.t("command.provider.connect")}
|
aria-label={language.t("command.provider.connect")}
|
||||||
onClick={handleConnectProvider}
|
onClick={handleConnectProvider}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip appearance="standard" placement="top" value={language.t("dialog.model.manage")}>
|
<Tooltip placement="top" value={language.t("dialog.model.manage")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<Icon name="sliders" />}
|
icon="sliders"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
iconSize="normal"
|
||||||
class="size-6"
|
class="size-6"
|
||||||
aria-label={language.t("dialog.model.manage")}
|
aria-label={language.t("dialog.model.manage")}
|
||||||
onClick={handleManage}
|
onClick={handleManage}
|
||||||
@@ -370,10 +373,10 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
|
<MenuV2 open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
|
||||||
<Menu.Trigger as={props.trigger} />
|
<MenuV2.Trigger as={props.trigger} />
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content
|
<MenuV2.Content
|
||||||
ref={(element: HTMLDivElement) => (contentRef = element)}
|
ref={(element: HTMLDivElement) => (contentRef = element)}
|
||||||
class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none"
|
class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none"
|
||||||
onPointerDownOutside={dismiss.preventTriggerRestore}
|
onPointerDownOutside={dismiss.preventTriggerRestore}
|
||||||
@@ -446,14 +449,14 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
>
|
>
|
||||||
<For each={groups()}>
|
<For each={groups()}>
|
||||||
{(group) => (
|
{(group) => (
|
||||||
<Menu.Group>
|
<MenuV2.Group>
|
||||||
<Menu.GroupLabel class="gap-2 px-3">
|
<MenuV2.GroupLabel class="gap-2 px-3">
|
||||||
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
|
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
|
||||||
</Menu.GroupLabel>
|
</MenuV2.GroupLabel>
|
||||||
<Menu.RadioGroup value={props.current}>
|
<MenuV2.RadioGroup value={props.current}>
|
||||||
<For each={group.items}>
|
<For each={group.items}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
gutter={6}
|
gutter={6}
|
||||||
@@ -467,7 +470,7 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Menu.RadioItem
|
<MenuV2.RadioItem
|
||||||
value={modelKey(item)}
|
value={modelKey(item)}
|
||||||
data-option-key={modelKey(item)}
|
data-option-key={modelKey(item)}
|
||||||
data-selected-model={props.current === modelKey(item) ? true : undefined}
|
data-selected-model={props.current === modelKey(item) ? true : undefined}
|
||||||
@@ -481,17 +484,17 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
>
|
>
|
||||||
<span class="min-w-0 truncate leading-5">{item.name}</span>
|
<span class="min-w-0 truncate leading-5">{item.name}</span>
|
||||||
<Show when={isFree(item.provider.id, item.cost)}>
|
<Show when={isFree(item.provider.id, item.cost)}>
|
||||||
<Badge class="shrink-0">{language.t("model.tag.free")}</Badge>
|
<TagV2 class="shrink-0">{language.t("model.tag.free")}</TagV2>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={item.latest}>
|
<Show when={item.latest}>
|
||||||
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
|
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.RadioItem>
|
</MenuV2.RadioItem>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Menu.RadioGroup>
|
</MenuV2.RadioGroup>
|
||||||
</Menu.Group>
|
</MenuV2.Group>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -499,7 +502,7 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
<div class="h-px bg-v2-border-border-muted" />
|
<div class="h-px bg-v2-border-border-muted" />
|
||||||
<div class="flex flex-col p-0.5">
|
<div class="flex flex-col p-0.5">
|
||||||
<Menu.Item
|
<MenuV2.Item
|
||||||
data-option-key={manageKey}
|
data-option-key={manageKey}
|
||||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === manageKey }}
|
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === manageKey }}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
@@ -510,11 +513,11 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
>
|
>
|
||||||
<Icon name="outline-sliders" size="small" />
|
<Icon name="outline-sliders" size="small" />
|
||||||
<span class="min-w-0 flex-1 truncate leading-5">{language.t("dialog.model.manage")}</span>
|
<span class="min-w-0 flex-1 truncate leading-5">{language.t("dialog.model.manage")}</span>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,19 +540,18 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog
|
||||||
<DialogHeader hideClose>
|
title={language.t("dialog.model.select.title")}
|
||||||
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
|
action={
|
||||||
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={provider}>
|
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={provider}>
|
||||||
{language.t("command.provider.connect")}
|
{language.t("command.provider.connect")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogHeader>
|
}
|
||||||
<DialogBody>
|
>
|
||||||
<ModelList provider={props.provider} model={props.model} onSelect={() => dialog.close()} />
|
<ModelList provider={props.provider} model={props.model} onSelect={() => dialog.close()} />
|
||||||
<Button variant="ghost" class="ml-3 mt-5 mb-6 text-text-base self-start" onClick={manage}>
|
<Button variant="ghost" class="ml-3 mt-5 mb-6 text-text-base self-start" onClick={manage}>
|
||||||
{language.t("dialog.model.manage")}
|
{language.t("dialog.model.manage")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogBody>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
@@ -157,47 +156,49 @@ export function ServerConnectionList(props: {
|
|||||||
/>
|
/>
|
||||||
<div class="flex items-center justify-center gap-4 pl-4">
|
<div class="flex items-center justify-center gap-4 pl-4">
|
||||||
<Show when={i.type === "http"}>
|
<Show when={i.type === "http"}>
|
||||||
<Menu appearance="standard">
|
<DropdownMenu>
|
||||||
<Menu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
icon={<Icon name="dot-grid" />}
|
icon="dot-grid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
<Menu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<Menu.Content class="mt-1">
|
<DropdownMenu.Content class="mt-1">
|
||||||
<Menu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (i.type !== "http") return
|
if (i.type !== "http") return
|
||||||
props.onEdit(i)
|
props.onEdit(i)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("dialog.server.menu.edit")}
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||||
</Menu.Item>
|
</DropdownMenu.Item>
|
||||||
<Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
|
<Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
|
||||||
<Menu.Item onSelect={() => props.domain.defaults.set(key)}>
|
<DropdownMenu.Item onSelect={() => props.domain.defaults.set(key)}>
|
||||||
{language.t("dialog.server.menu.default")}
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel>
|
||||||
</Menu.Item>
|
</DropdownMenu.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
|
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
|
||||||
<Menu.Item onSelect={() => props.domain.defaults.set(null)}>
|
<DropdownMenu.Item onSelect={() => props.domain.defaults.set(null)}>
|
||||||
{language.t("dialog.server.menu.defaultRemove")}
|
<DropdownMenu.ItemLabel>
|
||||||
</Menu.Item>
|
{language.t("dialog.server.menu.defaultRemove")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.domain.connection.canRemove(key)}>
|
<Show when={props.domain.connection.canRemove(key)}>
|
||||||
<Menu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<Menu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => props.domain.connection.remove(key)}
|
onSelect={() => props.domain.connection.remove(key)}
|
||||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||||
>
|
>
|
||||||
{language.t("dialog.server.menu.delete")}
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||||
</Menu.Item>
|
</DropdownMenu.Item>
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Content>
|
</DropdownMenu.Content>
|
||||||
</Menu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</Menu>
|
</DropdownMenu>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +208,7 @@ export function ServerConnectionList(props: {
|
|||||||
|
|
||||||
<div class="shrink-0 pb-5">
|
<div class="shrink-0 pb-5">
|
||||||
<Button
|
<Button
|
||||||
variant="neutral"
|
variant="secondary"
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={props.onAdd}
|
onClick={props.onAdd}
|
||||||
@@ -243,7 +244,7 @@ export function ServerConnectionForm(props: { form: ServerConnectionFormControll
|
|||||||
/>
|
/>
|
||||||
<div class="shrink-0 pb-5">
|
<div class="shrink-0 pb-5">
|
||||||
<Button
|
<Button
|
||||||
variant="contrast"
|
variant="primary"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={props.form.submit}
|
onClick={props.form.submit}
|
||||||
disabled={props.form.state.busy()}
|
disabled={props.form.state.busy()}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { usePlatform } from "@/context/platform"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { JSX } from "solid-js"
|
import { JSX } from "solid-js"
|
||||||
|
|
||||||
export type DialogGoUpsellProps = {
|
export type DialogGoUpsellProps = {
|
||||||
@@ -30,22 +30,17 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog fit>
|
<Dialog title={props.title} description={props.description} fit>
|
||||||
<DialogHeader>
|
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||||
<DialogTitleGroup title={props.title} description={props.description} />
|
<div class="flex justify-end gap-2">
|
||||||
</DialogHeader>
|
<Button variant="ghost" size="large" onClick={dismiss}>
|
||||||
<DialogBody>
|
{language.t("dialog.usageExceeded.dontShowAgain")}
|
||||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
</Button>
|
||||||
<div class="flex justify-end gap-2">
|
<Button variant="primary" size="large" onClick={runAction}>
|
||||||
<Button variant="ghost" size="large" onClick={dismiss}>
|
{props.actionLabel}
|
||||||
{language.t("dialog.usageExceeded.dontShowAgain")}
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button variant="contrast" size="large" onClick={runAction}>
|
|
||||||
{props.actionLabel}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import "@opencode-ai/ui/file-tree.css"
|
import "@opencode-ai/ui/v2/file-tree-v2.css"
|
||||||
import {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import type { FileNode } from "@/types"
|
import type { FileNode } from "@/types"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree"
|
import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree"
|
||||||
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
|
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
||||||
import { type Component, For, Show, createEffect, createMemo, createResource, createSignal } from "solid-js"
|
import { type Component, For, Show, createEffect, createMemo, createResource, createSignal } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useMcpToggle } from "@/context/mcp"
|
import { useMcpToggle } from "@/context/mcp"
|
||||||
import { useWorkspaceLocation } from "@/context/location"
|
import { useWorkspaceLocation } from "@/context/location"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useData } from "@/context/server"
|
import { useData } from "@/context/server"
|
||||||
import { pluginLabels } from "@/utils/plugin"
|
import { pluginLabel } from "@/utils/plugin"
|
||||||
import { ExternalLink } from "./external-link"
|
import { ExternalLink } from "./external-link"
|
||||||
|
|
||||||
type SkillItem = {
|
type SkillItem = {
|
||||||
@@ -102,10 +102,10 @@ export const ProjectSettingsExtensions: Component = () => {
|
|||||||
() => (serverSDK.connection.status() === "connected" ? directorySDK().directory : undefined),
|
() => (serverSDK.connection.status() === "connected" ? directorySDK().directory : undefined),
|
||||||
(directory) => serverSDK.api.plugin.list({ location: { directory } }).then((result) => result.data),
|
(directory) => serverSDK.api.plugin.list({ location: { directory } }).then((result) => result.data),
|
||||||
)
|
)
|
||||||
const globalPlugins = createMemo(() => pluginLabels(globalPluginList.latest ?? []))
|
const globalPlugins = createMemo(() => (globalPluginList.latest ?? []).map(pluginLabel))
|
||||||
const projectPlugins = createMemo(() => {
|
const projectPlugins = createMemo(() => {
|
||||||
const shared = new Set(globalPlugins())
|
const shared = new Set(globalPlugins())
|
||||||
return pluginLabels(projectPluginList.latest ?? []).filter((name) => !shared.has(name))
|
return (projectPluginList.latest ?? []).map(pluginLabel).filter((name) => !shared.has(name))
|
||||||
})
|
})
|
||||||
|
|
||||||
const serverSkills = createMemo(() => data.location.skill.list() ?? [])
|
const serverSkills = createMemo(() => data.location.skill.list() ?? [])
|
||||||
@@ -149,15 +149,15 @@ export const ProjectSettingsExtensions: Component = () => {
|
|||||||
<span>{language.t("project.settings.extensions.description")}</span>
|
<span>{language.t("project.settings.extensions.description")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs variant="pill" defaultValue="mcps" class="project-settings-extension-tabs">
|
<TabsV2 variant="pill" defaultValue="mcps" class="project-settings-extension-tabs">
|
||||||
<Tabs.List>
|
<TabsV2.List>
|
||||||
<Tabs.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</Tabs.Trigger>
|
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</Tabs.Trigger>
|
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</Tabs.Trigger>
|
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
|
||||||
{/* TODO: Restore LSP status when V2 exposes it. */}
|
{/* TODO: Restore LSP status when V2 exposes it. */}
|
||||||
</Tabs.List>
|
</TabsV2.List>
|
||||||
|
|
||||||
<Tabs.Content value="mcps">
|
<TabsV2.Content value="mcps">
|
||||||
<div class="project-settings-extension-section">
|
<div class="project-settings-extension-section">
|
||||||
<div class="project-settings-extension-section-header">
|
<div class="project-settings-extension-section-header">
|
||||||
<span>{language.t("project.settings.extensions.added")}</span>
|
<span>{language.t("project.settings.extensions.added")}</span>
|
||||||
@@ -168,9 +168,9 @@ export const ProjectSettingsExtensions: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
<SharedSection count={globalMcpNames().length}>{mcpRows(globalMcpNames())}</SharedSection>
|
<SharedSection count={globalMcpNames().length}>{mcpRows(globalMcpNames())}</SharedSection>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
|
|
||||||
<Tabs.Content value="plugins">
|
<TabsV2.Content value="plugins">
|
||||||
<div class="project-settings-extension-section">
|
<div class="project-settings-extension-section">
|
||||||
<div class="project-settings-extension-section-header">
|
<div class="project-settings-extension-section-header">
|
||||||
<span>{language.t("project.settings.extensions.added")}</span>
|
<span>{language.t("project.settings.extensions.added")}</span>
|
||||||
@@ -181,9 +181,9 @@ export const ProjectSettingsExtensions: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
<SharedSection count={globalPlugins().length}>{pluginRows(globalPlugins())}</SharedSection>
|
<SharedSection count={globalPlugins().length}>{pluginRows(globalPlugins())}</SharedSection>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
|
|
||||||
<Tabs.Content value="skills">
|
<TabsV2.Content value="skills">
|
||||||
<div class="project-settings-extension-section">
|
<div class="project-settings-extension-section">
|
||||||
<div class="project-settings-extension-section-header">
|
<div class="project-settings-extension-section-header">
|
||||||
<span>{language.t("project.settings.extensions.added")}</span>
|
<span>{language.t("project.settings.extensions.added")}</span>
|
||||||
@@ -196,8 +196,8 @@ export const ProjectSettingsExtensions: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
<SharedSection count={serverSkills().length}>{skillRows(serverSkills())}</SharedSection>
|
<SharedSection count={serverSkills().length}>{skillRows(serverSkills())}</SharedSection>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
</Tabs>
|
</TabsV2>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import type { ReferenceInfo } from "@opencode-ai/client/promise"
|
import type { ReferenceInfo } from "@opencode-ai/client/promise"
|
||||||
import { createEffect, createMemo, on, Show } from "solid-js"
|
import { createEffect, createMemo, on, Show } from "solid-js"
|
||||||
import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
|
import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
|
||||||
@@ -503,20 +503,20 @@ function PromptInputV2ModelControl(props: {
|
|||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Show when={!props.loading}>
|
<Show when={!props.loading}>
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{props.title}
|
{props.title}
|
||||||
<Keybind keys={props.keybind} variant="neutral" />
|
<KeybindV2 keys={props.keybind} variant="neutral" />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={props.paid}
|
when={props.paid}
|
||||||
fallback={
|
fallback={
|
||||||
<Button
|
<ButtonV2
|
||||||
data-action="prompt-model"
|
data-action="prompt-model"
|
||||||
data-control-type="dialog"
|
data-control-type="dialog"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
@@ -527,13 +527,13 @@ function PromptInputV2ModelControl(props: {
|
|||||||
onClick={props.onUnpaidClick}
|
onClick={props.onUnpaidClick}
|
||||||
>
|
>
|
||||||
{content()}
|
{content()}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ModelSelectorPopoverV2
|
<ModelSelectorPopoverV2
|
||||||
model={props.model}
|
model={props.model}
|
||||||
trigger={(triggerProps) => (
|
trigger={(triggerProps) => (
|
||||||
<Button
|
<ButtonV2
|
||||||
{...triggerProps}
|
{...triggerProps}
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="normal"
|
size="normal"
|
||||||
@@ -544,12 +544,12 @@ function PromptInputV2ModelControl(props: {
|
|||||||
data-control-type="popover"
|
data-control-type="popover"
|
||||||
>
|
>
|
||||||
{content()}
|
{content()}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
)}
|
)}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import {
|
|||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { getProjectAvatarVariant } from "@/context/layout"
|
import { getProjectAvatarVariant } from "@/context/layout"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
@@ -276,8 +277,7 @@ export function PromptProjectSelector(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<DropdownMenu
|
||||||
appearance="standard"
|
|
||||||
open={triggerReady() && props.controller.open()}
|
open={triggerReady() && props.controller.open()}
|
||||||
placement={props.placement ?? "bottom"}
|
placement={props.placement ?? "bottom"}
|
||||||
gutter={4}
|
gutter={4}
|
||||||
@@ -287,9 +287,9 @@ export function PromptProjectSelector(props: {
|
|||||||
props.controller.setOpen(open)
|
props.controller.setOpen(open)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu.Trigger as={ProjectTrigger} ref={setTriggerRef} controller={props.controller} />
|
<DropdownMenu.Trigger as={ProjectTrigger} ref={setTriggerRef} controller={props.controller} />
|
||||||
<Menu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<Menu.Content
|
<DropdownMenu.Content
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
id="prompt-project-menu"
|
id="prompt-project-menu"
|
||||||
class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none [&[data-closed]]:!animate-none"
|
class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none [&[data-closed]]:!animate-none"
|
||||||
@@ -360,13 +360,13 @@ export function PromptProjectSelector(props: {
|
|||||||
<Show
|
<Show
|
||||||
when={props.controller.servers().length > 1}
|
when={props.controller.servers().length > 1}
|
||||||
fallback={
|
fallback={
|
||||||
<Menu.RadioGroup value={selectedValue()}>
|
<DropdownMenu.RadioGroup value={selectedValue()}>
|
||||||
<For each={props.controller.projects()}>
|
<For each={props.controller.projects()}>
|
||||||
{(project) => (
|
{(project) => (
|
||||||
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
|
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Menu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For
|
<For
|
||||||
@@ -381,7 +381,7 @@ export function PromptProjectSelector(props: {
|
|||||||
<div class="flex h-7 select-none items-center pl-1.5 pr-3 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-faint">
|
<div class="flex h-7 select-none items-center pl-1.5 pr-3 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-faint">
|
||||||
{server!.name}
|
{server!.name}
|
||||||
</div>
|
</div>
|
||||||
<Menu.RadioGroup value={selectedValue()}>
|
<DropdownMenu.RadioGroup value={selectedValue()}>
|
||||||
<For
|
<For
|
||||||
each={props.controller.projects().filter((project) => project.server?.key === server!.key)}
|
each={props.controller.projects().filter((project) => project.server?.key === server!.key)}
|
||||||
>
|
>
|
||||||
@@ -389,7 +389,7 @@ export function PromptProjectSelector(props: {
|
|||||||
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
|
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Menu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -408,8 +408,8 @@ export function PromptProjectSelector(props: {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Menu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<Menu.SubTrigger
|
<DropdownMenu.SubTrigger
|
||||||
id={props.controller.actionKey()}
|
id={props.controller.actionKey()}
|
||||||
data-option-key={props.controller.actionKey()}
|
data-option-key={props.controller.actionKey()}
|
||||||
class={projectActionClass}
|
class={projectActionClass}
|
||||||
@@ -419,23 +419,24 @@ export function PromptProjectSelector(props: {
|
|||||||
onMouseEnter={() => props.controller.setActive(props.controller.actionKey())}
|
onMouseEnter={() => props.controller.setActive(props.controller.actionKey())}
|
||||||
>
|
>
|
||||||
<Icon name="plus" size="small" />
|
<Icon name="plus" size="small" />
|
||||||
<span class="min-w-0 flex-1 truncate leading-5">
|
<span data-slot="dropdown-menu-item-label" class="min-w-0 flex-1 truncate leading-5">
|
||||||
{props.controller.labels.add()}
|
{props.controller.labels.add()}
|
||||||
</span>
|
</span>
|
||||||
</Menu.SubTrigger>
|
<Icon name="chevron-right" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<Menu.Portal>
|
</DropdownMenu.SubTrigger>
|
||||||
<Menu.SubContent class="min-w-[180px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0.5 shadow-[var(--v2-elevation-floating)] focus:outline-none">
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.SubContent class="min-w-[180px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0.5 shadow-[var(--v2-elevation-floating)] focus:outline-none">
|
||||||
<For each={props.controller.servers()}>
|
<For each={props.controller.servers()}>
|
||||||
{(server) => <ServerAction server={server!} onSelect={selectAction} />}
|
{(server) => <ServerAction server={server!} onSelect={selectAction} />}
|
||||||
</For>
|
</For>
|
||||||
</Menu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</Menu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</Menu.Sub>
|
</DropdownMenu.Sub>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Content>
|
</DropdownMenu.Content>
|
||||||
</Menu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</Menu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,7 +507,7 @@ function ProjectItem(props: {
|
|||||||
}) {
|
}) {
|
||||||
const key = () => props.controller.projectKey(props.project)
|
const key = () => props.controller.projectKey(props.project)
|
||||||
return (
|
return (
|
||||||
<Menu.RadioItem
|
<DropdownMenu.RadioItem
|
||||||
id={key()}
|
id={key()}
|
||||||
value={key()}
|
value={key()}
|
||||||
data-option-key={key()}
|
data-option-key={key()}
|
||||||
@@ -533,8 +534,11 @@ function ProjectItem(props: {
|
|||||||
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
||||||
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
||||||
/>
|
/>
|
||||||
<span class="min-w-0 truncate leading-5">{displayName(props.project)}</span>
|
<DropdownMenu.ItemLabel class="min-w-0 truncate leading-5">{displayName(props.project)}</DropdownMenu.ItemLabel>
|
||||||
</Menu.RadioItem>
|
<DropdownMenu.ItemIndicator style={{ width: "14px", height: "14px", right: "12px" }}>
|
||||||
|
<IconV2 name="check" size="small" class="shrink-0 text-v2-icon-icon-base" />
|
||||||
|
</DropdownMenu.ItemIndicator>
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,7 +552,7 @@ function ProjectAction(props: {
|
|||||||
}) {
|
}) {
|
||||||
const key = () => props.controller.actionKey(props.server)
|
const key = () => props.controller.actionKey(props.server)
|
||||||
return (
|
return (
|
||||||
<Menu.Item
|
<DropdownMenu.Item
|
||||||
id={key()}
|
id={key()}
|
||||||
data-option-key={key()}
|
data-option-key={key()}
|
||||||
class="h-7 gap-2 rounded-sm px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base data-[highlighted]:!bg-v2-overlay-simple-overlay-hover"
|
class="h-7 gap-2 rounded-sm px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base data-[highlighted]:!bg-v2-overlay-simple-overlay-hover"
|
||||||
@@ -569,17 +573,17 @@ function ProjectAction(props: {
|
|||||||
onSelect={() => props.onSelect(props.server)}
|
onSelect={() => props.onSelect(props.server)}
|
||||||
>
|
>
|
||||||
<Icon name="plus" size="small" />
|
<Icon name="plus" size="small" />
|
||||||
<span class="min-w-0 truncate leading-5">
|
<DropdownMenu.ItemLabel class="min-w-0 truncate leading-5">
|
||||||
{props.controller.labels.add()}
|
{props.controller.labels.add()}
|
||||||
</span>
|
</DropdownMenu.ItemLabel>
|
||||||
</Menu.Item>
|
</DropdownMenu.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServerAction(props: { server: { key: string; name: string }; onSelect: (server: string) => void }) {
|
function ServerAction(props: { server: { key: string; name: string }; onSelect: (server: string) => void }) {
|
||||||
return (
|
return (
|
||||||
<Menu.Item class={projectActionClass} onSelect={() => props.onSelect(props.server.key)}>
|
<DropdownMenu.Item class={projectActionClass} onSelect={() => props.onSelect(props.server.key)}>
|
||||||
<span class="min-w-0 flex-1 truncate leading-5">{props.server.name}</span>
|
<DropdownMenu.ItemLabel class="min-w-0 flex-1 truncate leading-5">{props.server.name}</DropdownMenu.ItemLabel>
|
||||||
</Menu.Item>
|
</DropdownMenu.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { sameDirectory } from "@/utils/workspace"
|
import { sameDirectory } from "@/utils/workspace"
|
||||||
@@ -58,7 +58,7 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="top"
|
placement="top"
|
||||||
openDelay={800}
|
openDelay={800}
|
||||||
value={
|
value={
|
||||||
@@ -77,8 +77,8 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
contentClass={props.onboarding ? "max-w-[280px]" : undefined}
|
contentClass={props.onboarding ? "max-w-[280px]" : undefined}
|
||||||
class="min-w-0"
|
class="min-w-0"
|
||||||
>
|
>
|
||||||
<Menu placement="bottom" gutter={4} onOpenChange={onOpenChange}>
|
<MenuV2 placement="bottom" gutter={4} onOpenChange={onOpenChange}>
|
||||||
<Menu.Trigger
|
<MenuV2.Trigger
|
||||||
aria-description={language.t("session.new.workspace.trigger.tooltip")}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -92,14 +92,14 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
</Menu.Trigger>
|
</MenuV2.Trigger>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content class="w-[200px]">
|
<MenuV2.Content class="w-[200px]">
|
||||||
<Menu.Group>
|
<MenuV2.Group>
|
||||||
<Menu.GroupLabel>{language.t("session.new.workspace.runIn")}</Menu.GroupLabel>
|
<MenuV2.GroupLabel>{language.t("session.new.workspace.runIn")}</MenuV2.GroupLabel>
|
||||||
<Menu.Item onSelect={() => select("main")}>
|
<MenuV2.Item onSelect={() => select("main")}>
|
||||||
<Icon name="monitor" />
|
<Icon name="monitor" />
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="right"
|
placement="right"
|
||||||
openDelay={800}
|
openDelay={800}
|
||||||
value={
|
value={
|
||||||
@@ -113,14 +113,14 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
>
|
>
|
||||||
<span class="min-w-0 truncate">{language.t("session.new.workspace.local")}</span>
|
<span class="min-w-0 truncate">{language.t("session.new.workspace.local")}</span>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
<Show when={selected() === "main"}>
|
<Show when={selected() === "main"}>
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
<Icon name="check" size="small" class="shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
<Menu.Item onSelect={() => select("create")}>
|
<MenuV2.Item onSelect={() => select("create")}>
|
||||||
<Icon name="workspace-new" />
|
<Icon name="workspace-new" />
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="right"
|
placement="right"
|
||||||
openDelay={800}
|
openDelay={800}
|
||||||
value={
|
value={
|
||||||
@@ -134,25 +134,25 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
>
|
>
|
||||||
<span class="min-w-0 truncate">{language.t("workspace.new")}</span>
|
<span class="min-w-0 truncate">{language.t("workspace.new")}</span>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
<Show when={selected() === "create"}>
|
<Show when={selected() === "create"}>
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
<Icon name="check" size="small" class="shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Menu.Group>
|
</MenuV2.Group>
|
||||||
<Show
|
<Show
|
||||||
when={props.workspaces.length > 0}
|
when={props.workspaces.length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<Menu.Separator class="h-[0.5px]" />
|
<MenuV2.Separator class="h-[0.5px]" />
|
||||||
<Menu.Item onSelect={() => (pending = { type: "viewAll" })}>
|
<MenuV2.Item onSelect={() => (pending = { type: "viewAll" })}>
|
||||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Menu.Separator class="h-[0.5px]" />
|
<MenuV2.Separator class="h-[0.5px]" />
|
||||||
<Menu.Sub
|
<MenuV2.Sub
|
||||||
gutter={0}
|
gutter={0}
|
||||||
overlap
|
overlap
|
||||||
overflowPadding={8}
|
overflowPadding={8}
|
||||||
@@ -166,7 +166,7 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
requestAnimationFrame(() => searchInput?.focus())
|
requestAnimationFrame(() => searchInput?.focus())
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu.SubTrigger
|
<MenuV2.SubTrigger
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (
|
if (
|
||||||
event.key === "ArrowRight" ||
|
event.key === "ArrowRight" ||
|
||||||
@@ -181,9 +181,9 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
<span class="min-w-0 flex-1 truncate">
|
<span class="min-w-0 flex-1 truncate">
|
||||||
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
|
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
|
||||||
</span>
|
</span>
|
||||||
</Menu.SubTrigger>
|
</MenuV2.SubTrigger>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
|
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
|
||||||
<Show when={props.workspaces.length >= 10}>
|
<Show when={props.workspaces.length >= 10}>
|
||||||
<div class="flex h-7 items-center gap-2 rounded-sm ps-3 pe-2 text-v2-icon-icon-muted">
|
<div class="flex h-7 items-center gap-2 rounded-sm ps-3 pe-2 text-v2-icon-icon-muted">
|
||||||
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
||||||
@@ -211,27 +211,27 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
<For each={workspaces()}>
|
<For each={workspaces()}>
|
||||||
{(workspace) => (
|
{(workspace) => (
|
||||||
<Menu.Item onSelect={() => select(workspace)}>
|
<MenuV2.Item onSelect={() => select(workspace)}>
|
||||||
<Icon name="workspace-isolated" />
|
<Icon name="workspace-isolated" />
|
||||||
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
||||||
<Show when={selected() === workspace}>
|
<Show when={selected() === workspace}>
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
<Icon name="check" size="small" class="shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Menu.Separator class="h-[0.5px]" />
|
<MenuV2.Separator class="h-[0.5px]" />
|
||||||
<Menu.Item onSelect={() => (pending = { type: "viewAll" })}>
|
<MenuV2.Item onSelect={() => (pending = { type: "viewAll" })}>
|
||||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Menu.SubContent>
|
</MenuV2.SubContent>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu.Sub>
|
</MenuV2.Sub>
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
<PromptGitStatus branch={props.branch} from={selected() === "create"} class="ms-1" />
|
<PromptGitStatus branch={props.branch} from={selected() === "create"} class="ms-1" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -255,7 +255,7 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?
|
|||||||
return (
|
return (
|
||||||
<Show when={label()}>
|
<Show when={label()}>
|
||||||
{(value) => (
|
{(value) => (
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="top"
|
placement="top"
|
||||||
value={value()}
|
value={value()}
|
||||||
class={`min-w-0 max-w-[220px] ${props.class ?? ""}`}
|
class={`min-w-0 max-w-[220px] ${props.class ?? ""}`}
|
||||||
@@ -265,7 +265,7 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?
|
|||||||
<Icon name={icon()} size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
<Icon name={icon()} size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<span class="min-w-0 truncate">{value()}</span>
|
<span class="min-w-0 truncate">{value()}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { type Component, Show } from "solid-js"
|
import { type Component, Show } from "solid-js"
|
||||||
import type { ServerActionsController } from "@/components/server/server-management-controller"
|
import type { ServerActionsController } from "@/components/server/server-management-controller"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -59,19 +59,19 @@ export const ServerRowMenuView: Component<{
|
|||||||
const builtin = () => ServerConnection.builtin(props.server)
|
const builtin = () => ServerConnection.builtin(props.server)
|
||||||
const httpServer = () => (props.server.type === "http" ? props.server : undefined)
|
const httpServer = () => (props.server.type === "http" ? props.server : undefined)
|
||||||
return (
|
return (
|
||||||
<Menu gutter={6} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
|
<MenuV2 gutter={6} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
|
||||||
<Menu.Trigger
|
<MenuV2.Trigger
|
||||||
as={IconButton}
|
as={IconButtonV2}
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<Icon name="outline-dots" />}
|
icon={<IconV2 name="outline-dots" />}
|
||||||
aria-label={props.labels.more}
|
aria-label={props.labels.more}
|
||||||
/>
|
/>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content>
|
<MenuV2.Content>
|
||||||
<Menu.Group>
|
<MenuV2.Group>
|
||||||
<Menu.GroupLabel>{props.labels.server}</Menu.GroupLabel>
|
<MenuV2.GroupLabel>{props.labels.server}</MenuV2.GroupLabel>
|
||||||
<Menu.Item
|
<MenuV2.Item
|
||||||
disabled={builtin() || !httpServer()}
|
disabled={builtin() || !httpServer()}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const server = httpServer()
|
const server = httpServer()
|
||||||
@@ -79,20 +79,20 @@ export const ServerRowMenuView: Component<{
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.labels.edit}
|
{props.labels.edit}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
<Show when={props.canDefault && !props.isDefault}>
|
<Show when={props.canDefault && !props.isDefault}>
|
||||||
<Menu.Item onSelect={props.onSetDefault}>{props.labels.default}</Menu.Item>
|
<MenuV2.Item onSelect={props.onSetDefault}>{props.labels.default}</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.canDefault && props.isDefault}>
|
<Show when={props.canDefault && props.isDefault}>
|
||||||
<Menu.Item onSelect={props.onRemoveDefault}>{props.labels.defaultRemove}</Menu.Item>
|
<MenuV2.Item onSelect={props.onRemoveDefault}>{props.labels.defaultRemove}</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.canRemove}>
|
<Show when={props.canRemove}>
|
||||||
<Menu.Separator />
|
<MenuV2.Separator />
|
||||||
<Menu.Item onSelect={props.onRemove}>{props.labels.delete}</Menu.Item>
|
<MenuV2.Item onSelect={props.onRemove}>{props.labels.delete}</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Group>
|
</MenuV2.Group>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export function ServerRow(props: ServerRowProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
appearance="standard"
|
|
||||||
class="flex-1 min-w-0"
|
class="flex-1 min-w-0"
|
||||||
value={tooltipValue()}
|
value={tooltipValue()}
|
||||||
contentStyle={{ "max-width": "none", "white-space": "nowrap" }}
|
contentStyle={{ "max-width": "none", "white-space": "nowrap" }}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Show, createMemo, type ComponentProps, type JSX } from "solid-js"
|
import { Show, createMemo, type ComponentProps, type JSX } from "solid-js"
|
||||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
|
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
@@ -15,7 +16,7 @@ import { createSessionTabs } from "@/pages/session/helpers"
|
|||||||
|
|
||||||
interface SessionContextUsageProps {
|
interface SessionContextUsageProps {
|
||||||
variant?: "button" | "indicator"
|
variant?: "button" | "indicator"
|
||||||
placement?: ComponentProps<typeof Tooltip>["placement"]
|
placement?: ComponentProps<typeof TooltipV2>["placement"]
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
|
function ContextTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
|
||||||
@@ -110,21 +111,24 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
const circle = () => (
|
const circle = () => (
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<ProgressCircle
|
<ProgressCircle
|
||||||
appearance="indicator"
|
|
||||||
size={16}
|
size={16}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
percentage={context()?.usage ?? 0}
|
percentage={context()?.usage ?? 0}
|
||||||
style={{
|
style={
|
||||||
"--progress-circle-background": "var(--v2-background-bg-layer-04, var(--border-weak-base))",
|
variant() === "indicator"
|
||||||
"--progress-circle-background-overlay": "var(--v2-overlay-simple-overlay-pressed, transparent)",
|
? {
|
||||||
"--progress-circle-progress": "var(--v2-icon-icon-base, var(--icon-base))",
|
"--progress-circle-background": "var(--v2-background-bg-layer-04, var(--border-weak-base))",
|
||||||
}}
|
"--progress-circle-background-overlay": "var(--v2-overlay-simple-overlay-pressed, transparent)",
|
||||||
|
"--progress-circle-progress": "var(--v2-icon-icon-base, var(--icon-base))",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
const circleV2 = () => (
|
const circleV2 = () => (
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<ProgressCircle appearance="compact" percentage={context()?.usage ?? 0} />
|
<ProgressCircleV2 percentage={context()?.usage ?? 0} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,11 +145,11 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={params.id}>
|
<Show when={params.id}>
|
||||||
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
|
<TooltipV2 value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
|
||||||
<Show
|
<Show
|
||||||
when={variant() === "indicator"}
|
when={variant() === "indicator"}
|
||||||
fallback={
|
fallback={
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
@@ -157,7 +161,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
>
|
>
|
||||||
{circle()}
|
{circle()}
|
||||||
</Show>
|
</Show>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSignal, For, Show, type ComponentProps, type JSX } from "solid-js"
|
import { createSignal, For, Show, type ComponentProps, type JSX } from "solid-js"
|
||||||
@@ -17,7 +17,7 @@ export function SessionWorkspaceMenu(props: {
|
|||||||
sessionID: string
|
sessionID: string
|
||||||
project: Project
|
project: Project
|
||||||
directory: string
|
directory: string
|
||||||
placement?: ComponentProps<typeof Menu>["placement"]
|
placement?: ComponentProps<typeof MenuV2>["placement"]
|
||||||
gutter?: number
|
gutter?: number
|
||||||
class?: string
|
class?: string
|
||||||
contentClass?: string
|
contentClass?: string
|
||||||
@@ -71,57 +71,57 @@ export function SessionWorkspaceMenu(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<MenuV2
|
||||||
placement={props.placement ?? "bottom-end"}
|
placement={props.placement ?? "bottom-end"}
|
||||||
gutter={props.gutter ?? 4}
|
gutter={props.gutter ?? 4}
|
||||||
modal={false}
|
modal={false}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
>
|
>
|
||||||
<Menu.Trigger class={props.class} disabled={blocked()}>
|
<MenuV2.Trigger class={props.class} disabled={blocked()}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Menu.Trigger>
|
</MenuV2.Trigger>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content class={`w-[200px] ${props.contentClass ?? ""}`}>
|
<MenuV2.Content class={`w-[200px] ${props.contentClass ?? ""}`}>
|
||||||
<Menu.Group>
|
<MenuV2.Group>
|
||||||
<Menu.GroupLabel>{language.t("workspace.move.menu.title")}</Menu.GroupLabel>
|
<MenuV2.GroupLabel>{language.t("workspace.move.menu.title")}</MenuV2.GroupLabel>
|
||||||
<Show when={pathKey(props.directory) !== pathKey(props.project.worktree)}>
|
<Show when={pathKey(props.directory) !== pathKey(props.project.worktree)}>
|
||||||
<Menu.Item disabled={!!store.selected || blocked()} onSelect={() => void move(props.project.worktree)}>
|
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move(props.project.worktree)}>
|
||||||
<Icon name="monitor" />
|
<Icon name="monitor" />
|
||||||
{language.t("session.new.workspace.local")}
|
{language.t("session.new.workspace.local")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Menu.Item disabled={!!store.selected || blocked()} onSelect={() => void move("create")}>
|
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move("create")}>
|
||||||
<Icon name="workspace-new" />
|
<Icon name="workspace-new" />
|
||||||
{language.t("workspace.new")}
|
{language.t("workspace.new")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
<Show when={workspaces().length > 0}>
|
<Show when={workspaces().length > 0}>
|
||||||
<Menu.Sub gutter={0} overlap overflowPadding={8}>
|
<MenuV2.Sub gutter={0} overlap overflowPadding={8}>
|
||||||
<Menu.SubTrigger>
|
<MenuV2.SubTrigger>
|
||||||
<Icon name="workspace-isolated" />
|
<Icon name="workspace-isolated" />
|
||||||
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
|
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
|
||||||
</Menu.SubTrigger>
|
</MenuV2.SubTrigger>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
|
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
|
||||||
<For each={workspaces()}>
|
<For each={workspaces()}>
|
||||||
{(workspace) => (
|
{(workspace) => (
|
||||||
<Menu.Item disabled={!!store.selected || blocked()} onSelect={() => void move(workspace)}>
|
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move(workspace)}>
|
||||||
<Icon name="workspace-isolated" />
|
<Icon name="workspace-isolated" />
|
||||||
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Menu.SubContent>
|
</MenuV2.SubContent>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu.Sub>
|
</MenuV2.Sub>
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Group>
|
</MenuV2.Group>
|
||||||
<Menu.Separator class="h-[0.5px] bg-v2-border-border-base" />
|
<MenuV2.Separator class="h-[0.5px] bg-v2-border-border-base" />
|
||||||
<Menu.Item onSelect={() => openWorkspaces()}>
|
<MenuV2.Item onSelect={() => openWorkspaces()}>
|
||||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { For, Show } from "solid-js"
|
|||||||
import { AppIcon } from "@opencode-ai/ui/app-icon"
|
import { AppIcon } from "@opencode-ai/ui/app-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { SplitButton, SplitButtonAction, SplitButtonMenuTrigger } from "@opencode-ai/ui/split-button"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { SplitButtonV2, SplitButtonV2Action, SplitButtonV2MenuTrigger } from "@opencode-ai/ui/v2/split-button-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { type OpenApp, useOpenInApp } from "@/components/session/open-in-app"
|
import { type OpenApp, useOpenInApp } from "@/components/session/open-in-app"
|
||||||
|
|
||||||
@@ -14,13 +15,13 @@ export function OpenInAppV2(props: { directory: () => string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.directory() && state.canOpen()}>
|
<Show when={props.directory() && state.canOpen()}>
|
||||||
<SplitButton class="session-review-v2-open-in-app" onPointerDown={(event) => event.stopPropagation()}>
|
<SplitButtonV2 class="session-review-v2-open-in-app" onPointerDown={(event) => event.stopPropagation()}>
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
value={language.t("session.header.open.ariaLabel", { app: state.current().label })}
|
value={language.t("session.header.open.ariaLabel", { app: state.current().label })}
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
>
|
>
|
||||||
<SplitButtonAction
|
<SplitButtonV2Action
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -33,28 +34,28 @@ export function OpenInAppV2(props: { directory: () => string }) {
|
|||||||
<Show when={state.opening()} fallback={<AppIcon id={state.current().icon} class="size-[18px]" />}>
|
<Show when={state.opening()} fallback={<AppIcon id={state.current().icon} class="size-[18px]" />}>
|
||||||
<Spinner class="size-3.5" />
|
<Spinner class="size-3.5" />
|
||||||
</Show>
|
</Show>
|
||||||
</SplitButtonAction>
|
</SplitButtonV2Action>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
<Menu
|
<MenuV2
|
||||||
gutter={4}
|
gutter={4}
|
||||||
modal={false}
|
modal={false}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
open={state.menu.open}
|
open={state.menu.open}
|
||||||
onOpenChange={(open) => state.setMenu("open", open)}
|
onOpenChange={(open) => state.setMenu("open", open)}
|
||||||
>
|
>
|
||||||
<Menu.Trigger
|
<MenuV2.Trigger
|
||||||
as={SplitButtonMenuTrigger}
|
as={SplitButtonV2MenuTrigger}
|
||||||
disabled={state.opening()}
|
disabled={state.opening()}
|
||||||
aria-label={language.t("session.header.open.menu")}
|
aria-label={language.t("session.header.open.menu")}
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Icon name="chevron-down" size="small" />
|
<IconV2 name="chevron-down" size="small" />
|
||||||
</Menu.Trigger>
|
</MenuV2.Trigger>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content class="open-in-app-v2-menu">
|
<MenuV2.Content class="open-in-app-v2-menu">
|
||||||
<Menu.Group>
|
<MenuV2.Group>
|
||||||
<Menu.GroupLabel>{language.t("session.header.openIn")}</Menu.GroupLabel>
|
<MenuV2.GroupLabel>{language.t("session.header.openIn")}</MenuV2.GroupLabel>
|
||||||
<Menu.RadioGroup
|
<MenuV2.RadioGroup
|
||||||
value={state.current().id}
|
value={state.current().id}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
state.selectApp(value as OpenApp)
|
state.selectApp(value as OpenApp)
|
||||||
@@ -62,7 +63,7 @@ export function OpenInAppV2(props: { directory: () => string }) {
|
|||||||
>
|
>
|
||||||
<For each={state.options()}>
|
<For each={state.options()}>
|
||||||
{(option) => (
|
{(option) => (
|
||||||
<Menu.RadioItem
|
<MenuV2.RadioItem
|
||||||
value={option.id}
|
value={option.id}
|
||||||
disabled={state.opening()}
|
disabled={state.opening()}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
@@ -73,13 +74,13 @@ export function OpenInAppV2(props: { directory: () => string }) {
|
|||||||
>
|
>
|
||||||
<AppIcon id={option.icon} />
|
<AppIcon id={option.icon} />
|
||||||
{option.label}
|
{option.label}
|
||||||
</Menu.RadioItem>
|
</MenuV2.RadioItem>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Menu.RadioGroup>
|
</MenuV2.RadioGroup>
|
||||||
</Menu.Group>
|
</MenuV2.Group>
|
||||||
<Menu.Separator />
|
<MenuV2.Separator />
|
||||||
<Menu.Item
|
<MenuV2.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
state.setMenu("open", false)
|
state.setMenu("open", false)
|
||||||
state.copyPath()
|
state.copyPath()
|
||||||
@@ -87,11 +88,11 @@ export function OpenInAppV2(props: { directory: () => string }) {
|
|||||||
>
|
>
|
||||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||||
{language.t("session.header.open.copyPath")}
|
{language.t("session.header.open.copyPath")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
</SplitButton>
|
</SplitButtonV2>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { createMemo, Show } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
@@ -6,10 +7,10 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { StatusPopoverV2 } from "../status-popover"
|
import { StatusPopoverV2 } from "../status-popover"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
|
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
|
||||||
import { useTitlebarRightMount } from "../titlebar"
|
import { useTitlebarRightMount } from "../titlebar"
|
||||||
|
|
||||||
@@ -59,24 +60,24 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
|||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Show when={props.state.statusVisible}>
|
<Show when={props.state.statusVisible}>
|
||||||
<Tooltip appearance="standard" placement="bottom" value={props.state.statusLabel}>
|
<Tooltip placement="bottom" value={props.state.statusLabel}>
|
||||||
<StatusPopoverV2 />
|
<StatusPopoverV2 />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.state.reviewVisible}>
|
<Show when={props.state.reviewVisible}>
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{props.state.reviewLabel}
|
{props.state.reviewLabel}
|
||||||
<Show when={props.state.reviewKeybind.length > 0}>
|
<Show when={props.state.reviewKeybind.length > 0}>
|
||||||
<Keybind keys={props.state.reviewKeybind} variant="neutral" />
|
<KeybindV2 keys={props.state.reviewKeybind} variant="neutral" />
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
@@ -86,9 +87,9 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
|||||||
aria-label={props.state.reviewLabel}
|
aria-label={props.state.reviewLabel}
|
||||||
aria-expanded={props.state.reviewOpened}
|
aria-expanded={props.state.reviewOpened}
|
||||||
aria-controls="review-panel"
|
aria-controls="review-panel"
|
||||||
icon={<Icon name="sidebar-right" />}
|
icon={<IconV2 name="sidebar-right" />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createMemo, Show } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { useSortable } from "@dnd-kit/solid/sortable"
|
import { useSortable } from "@dnd-kit/solid/sortable"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -39,29 +40,31 @@ export function SortableTabV2(props: {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value={props.tab}
|
value={props.tab}
|
||||||
onMiddleClick={() => props.onTabClose(props.tab)}
|
|
||||||
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
|
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{language.t("common.closeTab")}
|
{language.t("common.closeTab")}
|
||||||
<Show when={closeTabKeybind().length > 0}>
|
<Show when={closeTabKeybind().length > 0}>
|
||||||
<Keybind keys={closeTabKeybind()} variant="neutral" />
|
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
gutter={10}
|
gutter={10}
|
||||||
>
|
>
|
||||||
<Tabs.CloseButton
|
<IconButton
|
||||||
|
icon="close-small"
|
||||||
|
variant="ghost"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
onClick={() => props.onTabClose(props.tab)}
|
onClick={() => props.onTabClose(props.tab)}
|
||||||
aria-label={language.t("common.closeTab")}
|
aria-label={language.t("common.closeTab")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
}
|
}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
|
onMiddleClick={() => props.onTabClose(props.tab)}
|
||||||
|
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
|
||||||
>
|
>
|
||||||
<Show when={content()}>{(value) => value()}</Show>
|
<Show when={content()}>{(value) => value()}</Show>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import type { JSX } from "solid-js"
|
|||||||
import { Show, createEffect, onCleanup } from "solid-js"
|
import { Show, createEffect, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useSortable } from "@dnd-kit/solid/sortable"
|
import { useSortable } from "@dnd-kit/solid/sortable"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
|
import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
|
||||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -115,8 +116,8 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={sortable.ref} class="h-full flex items-center outline-none focus:outline-none focus-visible:outline-none">
|
<div ref={sortable.ref} class="h-full flex items-center outline-none focus:outline-none focus-visible:outline-none">
|
||||||
<Menu.Context>
|
<MenuV2.Context>
|
||||||
<Menu.Context.Trigger class="relative" as="div">
|
<MenuV2.Context.Trigger class="relative" as="div">
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value={props.terminal.id}
|
value={props.terminal.id}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
@@ -130,11 +131,20 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
|
|||||||
if (e.detail > 0) return
|
if (e.detail > 0) return
|
||||||
focus()
|
focus()
|
||||||
}}
|
}}
|
||||||
onMiddleClick={close}
|
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tabs.CloseButton class="h-5 w-5" onClick={close} aria-label={language.t("terminal.close")} />
|
<IconButton
|
||||||
|
icon="close-small"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-5 w-5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
close()
|
||||||
|
}}
|
||||||
|
aria-label={language.t("terminal.close")}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
|
onMiddleClick={close}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="truncate"
|
class="truncate"
|
||||||
@@ -159,9 +169,9 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Context.Trigger>
|
</MenuV2.Context.Trigger>
|
||||||
<Menu.Context.Portal>
|
<MenuV2.Context.Portal>
|
||||||
<Menu.Context.Content
|
<MenuV2.Context.Content
|
||||||
onCloseAutoFocus={(e) => {
|
onCloseAutoFocus={(e) => {
|
||||||
if (!editRequested) return
|
if (!editRequested) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -169,11 +179,11 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
|
|||||||
requestAnimationFrame(() => edit())
|
requestAnimationFrame(() => edit())
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu.Item onSelect={() => (editRequested = true)}>{language.t("common.rename")}</Menu.Item>
|
<MenuV2.Item onSelect={() => (editRequested = true)}>{language.t("common.rename")}</MenuV2.Item>
|
||||||
<Menu.Item onSelect={close}>{language.t("common.close")}</Menu.Item>
|
<MenuV2.Item onSelect={close}>{language.t("common.close")}</MenuV2.Item>
|
||||||
</Menu.Context.Content>
|
</MenuV2.Context.Content>
|
||||||
</Menu.Context.Portal>
|
</MenuV2.Context.Portal>
|
||||||
</Menu.Context>
|
</MenuV2.Context>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { For, Show, createMemo, lazy, onCleanup } from "solid-js"
|
import { For, Show, createMemo, lazy, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { DEFAULT_PALETTE_KEYBIND, formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { DEFAULT_PALETTE_KEYBIND, formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { SettingsListV2 } from "./settings-v2/parts/list"
|
import { SettingsListV2 } from "./settings-v2/parts/list"
|
||||||
|
|
||||||
const Icon = lazy(() => import("@opencode-ai/ui/icon").then((module) => ({ default: module.Icon })))
|
const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon })))
|
||||||
|
|
||||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
const PALETTE_ID = "command.palette"
|
const PALETTE_ID = "command.palette"
|
||||||
@@ -387,12 +387,12 @@ function SettingsKeybindsV2View(props: {
|
|||||||
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
|
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
|
||||||
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.shortcuts.description")}</span>
|
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.shortcuts.description")}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides}>
|
<ButtonV2 variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides}>
|
||||||
{language.t("settings.shortcuts.reset.button")}
|
{language.t("settings.shortcuts.reset.button")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-v2-tab-search">
|
<div class="settings-v2-tab-search">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
type="search"
|
type="search"
|
||||||
appearance="base"
|
appearance="base"
|
||||||
value={store.filter}
|
value={store.filter}
|
||||||
@@ -405,12 +405,12 @@ function SettingsKeybindsV2View(props: {
|
|||||||
aria-label={language.t("settings.shortcuts.search.placeholder")}
|
aria-label={language.t("settings.shortcuts.search.placeholder")}
|
||||||
/>
|
/>
|
||||||
<Show when={store.filter}>
|
<Show when={store.filter}>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
class="settings-v2-tab-search-clear"
|
class="settings-v2-tab-search-clear"
|
||||||
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
|
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||||
onClick={() => setStore("filter", "")}
|
onClick={() => setStore("filter", "")}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, createMemo } from "solid-js"
|
import { Component, createMemo } from "solid-js"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ExternalLink } from "../external-link"
|
import { ExternalLink } from "../external-link"
|
||||||
import { SettingsListV2 } from "./parts/list"
|
import { SettingsListV2 } from "./parts/list"
|
||||||
@@ -42,7 +42,7 @@ const FontSetting: Component<{
|
|||||||
return (
|
return (
|
||||||
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
||||||
<div class="w-full sm:w-[220px]">
|
<div class="w-full sm:w-[220px]">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
data-action={config().action}
|
data-action={config().action}
|
||||||
type="text"
|
type="text"
|
||||||
appearance="base"
|
appearance="base"
|
||||||
@@ -83,7 +83,8 @@ export const SettingsAppearanceV2: Component = () => {
|
|||||||
title={language.t("settings.general.row.colorScheme.title")}
|
title={language.t("settings.general.row.colorScheme.title")}
|
||||||
description={language.t("settings.general.row.colorScheme.description")}
|
description={language.t("settings.general.row.colorScheme.description")}
|
||||||
>
|
>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action="settings-color-scheme"
|
data-action="settings-color-scheme"
|
||||||
options={schemeOptions}
|
options={schemeOptions}
|
||||||
current={schemeOptions.find((option) => option === appearance.scheme.current())}
|
current={schemeOptions.find((option) => option === appearance.scheme.current())}
|
||||||
@@ -109,7 +110,8 @@ export const SettingsAppearanceV2: Component = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action="settings-theme"
|
data-action="settings-theme"
|
||||||
options={appearance.theme.options()}
|
options={appearance.theme.options()}
|
||||||
current={appearance.theme.current()}
|
current={appearance.theme.current()}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
|
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Divider } from "@opencode-ai/ui/divider"
|
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { type Component, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
import { type Component, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||||
@@ -69,12 +69,12 @@ export const DialogServerV2: Component<{
|
|||||||
<DialogHeader hideClose={true}>
|
<DialogHeader hideClose={true}>
|
||||||
<DialogTitle>{title()}</DialogTitle>
|
<DialogTitle>{title()}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Divider />
|
<DividerV2 />
|
||||||
<DialogBody class="flex w-full min-w-0 flex-1 flex-col px-4 pt-4 pb-2">
|
<DialogBody class="flex w-full min-w-0 flex-1 flex-col px-4 pt-4 pb-2">
|
||||||
<div class="flex w-full min-w-0 flex-col gap-6">
|
<div class="flex w-full min-w-0 flex-col gap-6">
|
||||||
<div class="flex w-full min-w-0 flex-col gap-2">
|
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||||
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.url")}</label>
|
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.url")}</label>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
type="text"
|
type="text"
|
||||||
appearance="large"
|
appearance="large"
|
||||||
class="!w-full self-stretch"
|
class="!w-full self-stretch"
|
||||||
@@ -92,7 +92,7 @@ export const DialogServerV2: Component<{
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex w-full min-w-0 flex-col gap-2">
|
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||||
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.name")}</label>
|
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.name")}</label>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
type="text"
|
type="text"
|
||||||
appearance="large"
|
appearance="large"
|
||||||
class="!w-full self-stretch"
|
class="!w-full self-stretch"
|
||||||
@@ -106,7 +106,7 @@ export const DialogServerV2: Component<{
|
|||||||
<div class="grid w-full min-w-0 grid-cols-2 gap-4">
|
<div class="grid w-full min-w-0 grid-cols-2 gap-4">
|
||||||
<div class="flex min-w-0 flex-col gap-2">
|
<div class="flex min-w-0 flex-col gap-2">
|
||||||
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.username")}</label>
|
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.username")}</label>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
type="text"
|
type="text"
|
||||||
appearance="large"
|
appearance="large"
|
||||||
class="!w-full self-stretch"
|
class="!w-full self-stretch"
|
||||||
@@ -119,7 +119,7 @@ export const DialogServerV2: Component<{
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex min-w-0 flex-col gap-2">
|
<div class="flex min-w-0 flex-col gap-2">
|
||||||
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.password")}</label>
|
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.password")}</label>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
type="password"
|
type="password"
|
||||||
appearance="large"
|
appearance="large"
|
||||||
class="!w-full self-stretch"
|
class="!w-full self-stretch"
|
||||||
@@ -134,12 +134,12 @@ export const DialogServerV2: Component<{
|
|||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="neutral" disabled={form.state.busy()} onClick={() => dialog.close()}>
|
<ButtonV2 variant="neutral" disabled={form.state.busy()} onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button variant="contrast" disabled={form.state.busy()} onClick={form.submit}>
|
<ButtonV2 variant="contrast" disabled={form.state.busy()} onClick={form.submit}>
|
||||||
{submitLabel()}
|
{submitLabel()}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, createEffect, createMemo, createSignal, startTransition } from "solid-js"
|
import { Component, createEffect, createMemo, createSignal, startTransition } from "solid-js"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
@@ -73,66 +73,66 @@ export const DialogSettings: Component<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="x-large" variant="settings" class="settings-v2-dialog">
|
<Dialog size="x-large" variant="settings" class="settings-v2-dialog">
|
||||||
<Tabs
|
<TabsV2
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
variant="settings"
|
variant="settings"
|
||||||
value={tab()}
|
value={tab()}
|
||||||
onChange={(value) => void startTransition(() => setTab(value))}
|
onChange={(value) => void startTransition(() => setTab(value))}
|
||||||
class="settings-v2"
|
class="settings-v2"
|
||||||
>
|
>
|
||||||
<Tabs.List>
|
<TabsV2.List>
|
||||||
<div class="flex flex-col justify-between h-full w-full">
|
<div class="flex flex-col justify-between h-full w-full">
|
||||||
<div class="flex flex-col gap-4 w-full">
|
<div class="flex flex-col gap-4 w-full">
|
||||||
{/* Group 1: Preferences */}
|
{/* Group 1: Preferences */}
|
||||||
<div class="flex flex-col gap-1 w-full">
|
<div class="flex flex-col gap-1 w-full">
|
||||||
<Tabs.Trigger value="general">
|
<TabsV2.Trigger value="general">
|
||||||
<Icon name="sliders" />
|
<Icon name="sliders" />
|
||||||
{language.t("settings.tab.preferences")}
|
{language.t("settings.tab.preferences")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="appearance">
|
<TabsV2.Trigger value="appearance">
|
||||||
<Icon name="appearance" />
|
<Icon name="appearance" />
|
||||||
{language.t("settings.general.section.appearance")}
|
{language.t("settings.general.section.appearance")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="notifications">
|
<TabsV2.Trigger value="notifications">
|
||||||
<Icon name="notifications" />
|
<Icon name="notifications" />
|
||||||
{language.t("settings.tab.notifications")}
|
{language.t("settings.tab.notifications")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="shortcuts">
|
<TabsV2.Trigger value="shortcuts">
|
||||||
<Icon name="keyboard" />
|
<Icon name="keyboard" />
|
||||||
{language.t("settings.tab.shortcuts")}
|
{language.t("settings.tab.shortcuts")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Group 2: Environment & Workspaces */}
|
{/* Group 2: Environment & Workspaces */}
|
||||||
<div class="flex flex-col gap-1 w-full">
|
<div class="flex flex-col gap-1 w-full">
|
||||||
<Tabs.Trigger value="servers">
|
<TabsV2.Trigger value="servers">
|
||||||
<Icon name="server" />
|
<Icon name="server" />
|
||||||
{language.t("status.popover.tab.servers")}
|
{language.t("status.popover.tab.servers")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="projects">
|
<TabsV2.Trigger value="projects">
|
||||||
<Icon name="folder" />
|
<Icon name="folder" />
|
||||||
{language.t("settings.tab.projects")}
|
{language.t("settings.tab.projects")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="workspaces">
|
<TabsV2.Trigger value="workspaces">
|
||||||
<Icon name="workspace-isolated" />
|
<Icon name="workspace-isolated" />
|
||||||
{language.t("settings.tab.workspaces")}
|
{language.t("settings.tab.workspaces")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Group 3: Capabilities & Extensions */}
|
{/* Group 3: Capabilities & Extensions */}
|
||||||
<div class="flex flex-col gap-1 w-full">
|
<div class="flex flex-col gap-1 w-full">
|
||||||
<Tabs.Trigger value="providers">
|
<TabsV2.Trigger value="providers">
|
||||||
<Icon name="providers" />
|
<Icon name="providers" />
|
||||||
{language.t("settings.providers.title")}
|
{language.t("settings.providers.title")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="models">
|
<TabsV2.Trigger value="models">
|
||||||
<Icon name="models" />
|
<Icon name="models" />
|
||||||
{language.t("settings.models.title")}
|
{language.t("settings.models.title")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="extensions">
|
<TabsV2.Trigger value="extensions">
|
||||||
<Icon name="extensions" />
|
<Icon name="extensions" />
|
||||||
{language.t("settings.tab.extensions")}
|
{language.t("settings.tab.extensions")}
|
||||||
</Tabs.Trigger>
|
</TabsV2.Trigger>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -141,41 +141,41 @@ export const DialogSettings: Component<{
|
|||||||
<span>v{platform.version}</span>
|
<span>v{platform.version}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.List>
|
</TabsV2.List>
|
||||||
|
|
||||||
<Tabs.Content value="general" class="settings-v2-panel">
|
<TabsV2.Content value="general" class="settings-v2-panel">
|
||||||
<SettingsGeneral server={server()} sessionID={props.sessionID} />
|
<SettingsGeneral server={server()} sessionID={props.sessionID} />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<Tabs.Content value="appearance" class="settings-v2-panel">
|
<TabsV2.Content value="appearance" class="settings-v2-panel">
|
||||||
<SettingsAppearanceV2 />
|
<SettingsAppearanceV2 />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<Tabs.Content value="notifications" class="settings-v2-panel">
|
<TabsV2.Content value="notifications" class="settings-v2-panel">
|
||||||
<SettingsNotificationsV2 />
|
<SettingsNotificationsV2 />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<Tabs.Content value="shortcuts" class="settings-v2-panel">
|
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
|
||||||
<SettingsKeybinds />
|
<SettingsKeybinds />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<Tabs.Content value="servers" class="settings-v2-panel">
|
<TabsV2.Content value="servers" class="settings-v2-panel">
|
||||||
<SettingsServersV2 />
|
<SettingsServersV2 />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<Tabs.Content value="projects" class="settings-v2-panel">
|
<TabsV2.Content value="projects" class="settings-v2-panel">
|
||||||
<SettingsProjectsV2 />
|
<SettingsProjectsV2 />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<SettingsServerScope directory={directory()}>
|
<SettingsServerScope directory={directory()}>
|
||||||
<Tabs.Content value="workspaces" class="settings-v2-panel">
|
<TabsV2.Content value="workspaces" class="settings-v2-panel">
|
||||||
<SettingsWorkspacesV2 activeDirectory={directory()} />
|
<SettingsWorkspacesV2 activeDirectory={directory()} />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<Tabs.Content value="providers" class="settings-v2-panel">
|
<TabsV2.Content value="providers" class="settings-v2-panel">
|
||||||
<SettingsProvidersV2 directory={directory()} onBack={showProviders} />
|
<SettingsProvidersV2 directory={directory()} onBack={showProviders} />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<Tabs.Content value="models" class="settings-v2-panel">
|
<TabsV2.Content value="models" class="settings-v2-panel">
|
||||||
<SettingsModelsV2 />
|
<SettingsModelsV2 />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
<Tabs.Content value="extensions" class="settings-v2-panel">
|
<TabsV2.Content value="extensions" class="settings-v2-panel">
|
||||||
<SettingsExtensionsV2 />
|
<SettingsExtensionsV2 />
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
</SettingsServerScope>
|
</SettingsServerScope>
|
||||||
</Tabs>
|
</TabsV2>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Component, For, createEffect, createMemo, createResource } from "solid-js"
|
import { Component, For, createEffect, createMemo, createResource } from "solid-js"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useData } from "@/context/server"
|
import { useData } from "@/context/server"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useMcpToggle } from "@/context/mcp"
|
import { useMcpToggle } from "@/context/mcp"
|
||||||
import { pluginLabels } from "@/utils/plugin"
|
import { pluginLabel } from "@/utils/plugin"
|
||||||
import { ExternalLink } from "../external-link"
|
import { ExternalLink } from "../external-link"
|
||||||
import { InlineServerSelect } from "./parts/server-select"
|
import { InlineServerSelect } from "./parts/server-select"
|
||||||
import "./settings-v2.css"
|
import "./settings-v2.css"
|
||||||
@@ -45,7 +45,9 @@ export const SettingsExtensionsV2: Component = () => {
|
|||||||
() => serverSdk.connection.status() === "connected",
|
() => serverSdk.connection.status() === "connected",
|
||||||
() => serverSdk.api.plugin.list().then((result) => result.data),
|
() => serverSdk.api.plugin.list().then((result) => result.data),
|
||||||
)
|
)
|
||||||
const plugins = createMemo<PluginRowItem[]>(() => pluginLabels(pluginList.latest ?? []).map((name) => ({ name })))
|
const plugins = createMemo<PluginRowItem[]>(() =>
|
||||||
|
(pluginList.latest ?? []).map((item) => ({ name: pluginLabel(item) })),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (serverSdk.connection.status() !== "connected") return
|
if (serverSdk.connection.status() !== "connected") return
|
||||||
@@ -66,14 +68,14 @@ export const SettingsExtensionsV2: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-v2-tab-body">
|
<div class="settings-v2-tab-body">
|
||||||
<Tabs variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
|
<TabsV2 variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
|
||||||
<Tabs.List>
|
<TabsV2.List>
|
||||||
<Tabs.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</Tabs.Trigger>
|
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</Tabs.Trigger>
|
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
|
||||||
<Tabs.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</Tabs.Trigger>
|
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
|
||||||
</Tabs.List>
|
</TabsV2.List>
|
||||||
|
|
||||||
<Tabs.Content value="mcps">
|
<TabsV2.Content value="mcps">
|
||||||
<div class="settings-v2-section">
|
<div class="settings-v2-section">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-13-medium text-v2-text-text-base">
|
<span class="text-13-medium text-v2-text-text-base">
|
||||||
@@ -97,9 +99,9 @@ export const SettingsExtensionsV2: Component = () => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
|
|
||||||
<Tabs.Content value="plugins">
|
<TabsV2.Content value="plugins">
|
||||||
<div class="settings-v2-section">
|
<div class="settings-v2-section">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-13-medium text-v2-text-text-base">
|
<span class="text-13-medium text-v2-text-text-base">
|
||||||
@@ -120,9 +122,9 @@ export const SettingsExtensionsV2: Component = () => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
|
|
||||||
<Tabs.Content value="skills">
|
<TabsV2.Content value="skills">
|
||||||
<div class="settings-v2-section">
|
<div class="settings-v2-section">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-13-medium text-v2-text-text-base">
|
<span class="text-13-medium text-v2-text-text-base">
|
||||||
@@ -148,8 +150,8 @@ export const SettingsExtensionsV2: Component = () => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</TabsV2.Content>
|
||||||
</Tabs>
|
</TabsV2>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Component, Show, createMemo, createResource } from "solid-js"
|
import { Component, Show, createMemo, createResource } from "solid-js"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useUpdaterAction } from "../updater-action"
|
import { useUpdaterAction } from "../updater-action"
|
||||||
@@ -100,7 +100,8 @@ const WorkspaceDestinationSetting: Component = () => {
|
|||||||
title={language.t("settings.workspaces.default.title")}
|
title={language.t("settings.workspaces.default.title")}
|
||||||
description={language.t("settings.workspaces.default.description")}
|
description={language.t("settings.workspaces.default.description")}
|
||||||
>
|
>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
options={options()}
|
options={options()}
|
||||||
current={options().find((option) => option.value === settings.workspaces.defaultDestination())}
|
current={options().find((option) => option.value === settings.workspaces.defaultDestination())}
|
||||||
value={(option) => option.value}
|
value={(option) => option.value}
|
||||||
@@ -126,7 +127,8 @@ const ShellSetting: Component<{ controller: ShellSettingsController }> = (props)
|
|||||||
title={language.t("settings.general.row.shell.title")}
|
title={language.t("settings.general.row.shell.title")}
|
||||||
description={language.t("settings.general.row.shell.description")}
|
description={language.t("settings.general.row.shell.description")}
|
||||||
>
|
>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action="settings-shell"
|
data-action="settings-shell"
|
||||||
options={options()}
|
options={options()}
|
||||||
current={options().find((option) => option.value === props.controller.current()) ?? options()[0]}
|
current={options().find((option) => option.value === props.controller.current()) ?? options()[0]}
|
||||||
@@ -154,7 +156,8 @@ const AppearanceSection: Component<{ controller: AppearanceSettingsController }>
|
|||||||
title={language.t("settings.general.row.colorScheme.title")}
|
title={language.t("settings.general.row.colorScheme.title")}
|
||||||
description={language.t("settings.general.row.colorScheme.description")}
|
description={language.t("settings.general.row.colorScheme.description")}
|
||||||
>
|
>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action="settings-color-scheme"
|
data-action="settings-color-scheme"
|
||||||
options={schemeOptions}
|
options={schemeOptions}
|
||||||
current={schemeOptions.find((option) => option === props.controller.scheme.current())}
|
current={schemeOptions.find((option) => option === props.controller.scheme.current())}
|
||||||
@@ -180,7 +183,8 @@ const AppearanceSection: Component<{ controller: AppearanceSettingsController }>
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action="settings-theme"
|
data-action="settings-theme"
|
||||||
options={props.controller.theme.options()}
|
options={props.controller.theme.options()}
|
||||||
current={props.controller.theme.current()}
|
current={props.controller.theme.current()}
|
||||||
@@ -209,7 +213,7 @@ const FontSetting: Component<{
|
|||||||
return (
|
return (
|
||||||
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
||||||
<div class="w-full sm:w-[220px]">
|
<div class="w-full sm:w-[220px]">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
data-action={config().action}
|
data-action={config().action}
|
||||||
type="text"
|
type="text"
|
||||||
appearance="base"
|
appearance="base"
|
||||||
@@ -250,7 +254,8 @@ const SoundSetting: Component<{
|
|||||||
const config = () => soundSettings[props.kind]
|
const config = () => soundSettings[props.kind]
|
||||||
return (
|
return (
|
||||||
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action={config().action}
|
data-action={config().action}
|
||||||
options={soundOptions}
|
options={soundOptions}
|
||||||
current={props.channel.current()}
|
current={props.channel.current()}
|
||||||
@@ -278,7 +283,8 @@ const LanguageSetting = () => {
|
|||||||
title={language.t("settings.general.row.language.title")}
|
title={language.t("settings.general.row.language.title")}
|
||||||
description={language.t("settings.general.row.language.description")}
|
description={language.t("settings.general.row.language.description")}
|
||||||
>
|
>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action="settings-language"
|
data-action="settings-language"
|
||||||
options={options()}
|
options={options()}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@@ -494,9 +500,9 @@ export const SettingsGeneral: Component<{
|
|||||||
title={language.t("settings.updates.row.check.title")}
|
title={language.t("settings.updates.row.check.title")}
|
||||||
description={language.t("settings.updates.row.check.description")}
|
description={language.t("settings.updates.row.check.description")}
|
||||||
>
|
>
|
||||||
<Button size="normal" variant="neutral" disabled={!updater.action().run} onClick={() => updater.run()}>
|
<ButtonV2 size="normal" variant="neutral" disabled={!updater.action().run} onClick={() => updater.run()}>
|
||||||
{language.t(updater.action().label)}
|
{language.t(updater.action().label)}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</SettingsRowV2>
|
</SettingsRowV2>
|
||||||
</SettingsListV2>
|
</SettingsListV2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { type Component, For, Show } from "solid-js"
|
import { type Component, For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -62,7 +62,7 @@ export const SettingsModelsV2: Component = () => {
|
|||||||
<InlineServerSelect />
|
<InlineServerSelect />
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-v2-tab-search">
|
<div class="settings-v2-tab-search">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
type="search"
|
type="search"
|
||||||
appearance="base"
|
appearance="base"
|
||||||
value={list.filter()}
|
value={list.filter()}
|
||||||
@@ -75,12 +75,12 @@ export const SettingsModelsV2: Component = () => {
|
|||||||
aria-label={language.t("dialog.model.search.placeholder")}
|
aria-label={language.t("dialog.model.search.placeholder")}
|
||||||
/>
|
/>
|
||||||
<Show when={list.filter()}>
|
<Show when={list.filter()}>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
class="settings-v2-tab-search-clear"
|
class="settings-v2-tab-search-clear"
|
||||||
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
|
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||||
onClick={() => list.clear()}
|
onClick={() => list.clear()}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component } from "solid-js"
|
import { Component } from "solid-js"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { SettingsListV2 } from "./parts/list"
|
import { SettingsListV2 } from "./parts/list"
|
||||||
@@ -34,7 +34,8 @@ const SoundSetting: Component<{
|
|||||||
const config = () => soundSettings[props.kind]
|
const config = () => soundSettings[props.kind]
|
||||||
return (
|
return (
|
||||||
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action={config().action}
|
data-action={config().action}
|
||||||
options={soundOptions}
|
options={soundOptions}
|
||||||
current={props.channel.current()}
|
current={props.channel.current()}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Show, createMemo, type Component } from "solid-js"
|
import { Show, createMemo, type Component } from "solid-js"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { ServerConnection, serverName } from "@/context/servers"
|
import { ServerConnection, serverName } from "@/context/servers"
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ export const InlineServerSelect: Component<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={options().length > 1}>
|
<Show when={options().length > 1}>
|
||||||
<Select
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
data-action="settings-server-select"
|
data-action="settings-server-select"
|
||||||
options={options()}
|
options={options()}
|
||||||
current={current()}
|
current={current()}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
@@ -51,11 +51,11 @@ export const SettingsProjectsV2: Component = () => {
|
|||||||
<span class="text-13-medium text-v2-text-text-base truncate">{name()}</span>
|
<span class="text-13-medium text-v2-text-text-base truncate">{name()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div class="flex items-center gap-2 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<Icon name="settings-gear" size="small" class="text-v2-icon-icon-muted" />}
|
icon={<IconV2 name="settings-gear" size="small" class="text-v2-icon-icon-muted" />}
|
||||||
onClick={(event: MouseEvent) => {
|
onClick={(event: MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
openProjectSettings(props.project, props.server)
|
openProjectSettings(props.project, props.server)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Badge } from "@opencode-ai/ui/badge"
|
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
@@ -160,7 +160,7 @@ export const SettingsProvidersV2: Component<{
|
|||||||
/>
|
/>
|
||||||
<div class="settings-v2-provider-main">
|
<div class="settings-v2-provider-main">
|
||||||
<span class="settings-v2-provider-name truncate">{item.name}</span>
|
<span class="settings-v2-provider-name truncate">{item.name}</span>
|
||||||
<Badge>{type(item)}</Badge>
|
<Tag>{type(item)}</Tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<Show
|
||||||
@@ -171,9 +171,9 @@ export const SettingsProvidersV2: Component<{
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button size="normal" variant="ghost-muted" onClick={() => void disconnect(item.id, item.name)}>
|
<ButtonV2 size="normal" variant="ghost-muted" onClick={() => void disconnect(item.id, item.name)}>
|
||||||
{language.t("common.disconnect")}
|
{language.t("common.disconnect")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -199,7 +199,7 @@ export const SettingsProvidersV2: Component<{
|
|||||||
<div class="settings-v2-provider-main">
|
<div class="settings-v2-provider-main">
|
||||||
<span class="settings-v2-provider-name">{item.name}</span>
|
<span class="settings-v2-provider-name">{item.name}</span>
|
||||||
<Show when={item.id === "opencode" || item.id === "opencode-go"}>
|
<Show when={item.id === "opencode" || item.id === "opencode-go"}>
|
||||||
<Badge>{language.t("dialog.provider.tag.recommended")}</Badge>
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={note(item.id)}>
|
<Show when={note(item.id)}>
|
||||||
@@ -207,9 +207,9 @@ export const SettingsProvidersV2: Component<{
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button size="normal" variant="neutral" icon="plus" onClick={() => connect(item.id)}>
|
<ButtonV2 size="normal" variant="neutral" icon="plus" onClick={() => connect(item.id)}>
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -226,14 +226,14 @@ export const SettingsProvidersV2: Component<{
|
|||||||
<div class="settings-v2-provider-copy">
|
<div class="settings-v2-provider-copy">
|
||||||
<div class="settings-v2-provider-main">
|
<div class="settings-v2-provider-main">
|
||||||
<span class="settings-v2-provider-name">{language.t("provider.custom.title")}</span>
|
<span class="settings-v2-provider-name">{language.t("provider.custom.title")}</span>
|
||||||
<Badge>{language.t("settings.providers.tag.custom")}</Badge>
|
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-v2-provider-description">
|
<p class="settings-v2-provider-description">
|
||||||
{language.t("settings.providers.custom.description")}
|
{language.t("settings.providers.custom.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<ButtonV2
|
||||||
size="normal"
|
size="normal"
|
||||||
variant="neutral"
|
variant="neutral"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
@@ -246,7 +246,7 @@ export const SettingsProvidersV2: Component<{
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</SettingsListV2>
|
</SettingsListV2>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Badge } from "@opencode-ai/ui/badge"
|
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { type Component, For, Show, createMemo } from "solid-js"
|
import { type Component, For, Show, createMemo } from "solid-js"
|
||||||
@@ -61,7 +61,7 @@ export const SettingsServersV2: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Show when={showSearch()}>
|
<Show when={showSearch()}>
|
||||||
<div class="settings-v2-tab-search">
|
<div class="settings-v2-tab-search">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
type="search"
|
type="search"
|
||||||
appearance="base"
|
appearance="base"
|
||||||
value={store.filter}
|
value={store.filter}
|
||||||
@@ -74,12 +74,12 @@ export const SettingsServersV2: Component = () => {
|
|||||||
aria-label={language.t("dialog.server.search.placeholder")}
|
aria-label={language.t("dialog.server.search.placeholder")}
|
||||||
/>
|
/>
|
||||||
<Show when={store.filter}>
|
<Show when={store.filter}>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
class="settings-v2-tab-search-clear"
|
class="settings-v2-tab-search-clear"
|
||||||
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
|
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||||
onClick={() => setStore("filter", "")}
|
onClick={() => setStore("filter", "")}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -126,7 +126,7 @@ export const SettingsServersV2: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-v2-servers-actions">
|
<div class="settings-v2-servers-actions">
|
||||||
<Show when={controller.defaults.available() && isDefault()}>
|
<Show when={controller.defaults.available() && isDefault()}>
|
||||||
<Badge>{language.t("dialog.server.status.default")}</Badge>
|
<Tag>{language.t("dialog.server.status.default")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
<ServerRowMenu server={item} domain={controller} onEdit={openEdit} />
|
<ServerRowMenu server={item} domain={controller} onEdit={openEdit} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import "@opencode-ai/ui/text-input.css";
|
@import "@opencode-ai/ui/v2/text-input-v2.css";
|
||||||
@import "@opencode-ai/ui/button.css";
|
@import "@opencode-ai/ui/v2/button-v2.css";
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="settings"] {
|
[data-component="tabs-v2"][data-variant="settings"] {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { For, Show, createMemo } from "solid-js"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import type { SessionInfo } from "@opencode-ai/client/promise"
|
import type { SessionInfo } from "@opencode-ai/client/promise"
|
||||||
import { useQuery } from "@tanstack/solid-query"
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Dialog, DialogFooter, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
|
import { Dialog, DialogFooter, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -273,33 +273,33 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
|
|||||||
</span>
|
</span>
|
||||||
<div class="settings-v2-workspaces-toolbar-actions">
|
<div class="settings-v2-workspaces-toolbar-actions">
|
||||||
<Show when={projects().length > 1}>
|
<Show when={projects().length > 1}>
|
||||||
<Menu placement="bottom-end" gutter={6}>
|
<MenuV2 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">
|
<MenuV2.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">
|
<span class="min-w-0 truncate">
|
||||||
{projectOptions().find((option) => option.id === selectedProject())?.label}
|
{projectOptions().find((option) => option.id === selectedProject())?.label}
|
||||||
</span>
|
</span>
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
</Menu.Trigger>
|
</MenuV2.Trigger>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content>
|
<MenuV2.Content>
|
||||||
<For each={projectOptions()}>
|
<For each={projectOptions()}>
|
||||||
{(option) => (
|
{(option) => (
|
||||||
<Menu.Item onSelect={() => setStore("project", option.id)}>
|
<MenuV2.Item onSelect={() => setStore("project", option.id)}>
|
||||||
<span class="min-w-0 flex-1 truncate">{option.label}</span>
|
<span class="min-w-0 flex-1 truncate">{option.label}</span>
|
||||||
<Show when={selectedProject() === option.id}>
|
<Show when={selectedProject() === option.id}>
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
<Icon name="check" size="small" class="shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={filtered().length > 0}>
|
<Show when={filtered().length > 0}>
|
||||||
<Menu placement="bottom-end" gutter={4}>
|
<MenuV2 placement="bottom-end" gutter={4}>
|
||||||
<Menu.Trigger
|
<MenuV2.Trigger
|
||||||
as={IconButton}
|
as={IconButtonV2}
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -307,16 +307,16 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
|
|||||||
disabled={!!store.transaction}
|
disabled={!!store.transaction}
|
||||||
icon={<Icon name="outline-dots" size="small" />}
|
icon={<Icon name="outline-dots" size="small" />}
|
||||||
/>
|
/>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content>
|
<MenuV2.Content>
|
||||||
<Menu.Item onSelect={confirmDeleteAll}>
|
<MenuV2.Item onSelect={confirmDeleteAll}>
|
||||||
<span class="settings-v2-workspaces-delete-all">
|
<span class="settings-v2-workspaces-delete-all">
|
||||||
{language.t("settings.workspaces.deleteAll")}
|
{language.t("settings.workspaces.deleteAll")}
|
||||||
</span>
|
</span>
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,7 +335,7 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
|
|||||||
<div class="settings-v2-workspaces-row-header">
|
<div class="settings-v2-workspaces-row-header">
|
||||||
<div class="settings-v2-workspaces-copy">
|
<div class="settings-v2-workspaces-copy">
|
||||||
<div class="settings-v2-workspaces-main">
|
<div class="settings-v2-workspaces-main">
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
value={workspace.directory}
|
value={workspace.directory}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||||
@@ -348,21 +348,24 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
|
|||||||
>
|
>
|
||||||
{workspace.directory}
|
{workspace.directory}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</div>
|
</div>
|
||||||
<span class="settings-v2-workspaces-meta">{sessionCount(workspace)}</span>
|
<span class="settings-v2-workspaces-meta">{sessionCount(workspace)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-v2-workspaces-row-actions">
|
<div class="settings-v2-workspaces-row-actions">
|
||||||
<Show when={lastActive(workspace)}>
|
<Show when={lastActive(workspace)}>
|
||||||
{(value) => (
|
{(value) => (
|
||||||
<Tooltip value={language.t("settings.workspaces.lastActiveSession")} placement="top-end">
|
<TooltipV2
|
||||||
|
value={language.t("settings.workspaces.lastActiveSession")}
|
||||||
|
placement="top-end"
|
||||||
|
>
|
||||||
<span tabIndex={0} class="settings-v2-workspaces-active">
|
<span tabIndex={0} class="settings-v2-workspaces-active">
|
||||||
{value()}
|
{value()}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -425,12 +428,12 @@ function DialogDeleteAllWorkspaces(props: { count: number; project: string; onDe
|
|||||||
/>
|
/>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="neutral" onClick={() => dialog.close()}>
|
<ButtonV2 type="button" variant="neutral" onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button type="button" variant="danger" onClick={remove}>
|
<ButtonV2 type="button" variant="danger" onClick={remove}>
|
||||||
{language.t("settings.workspaces.deleteAll")}
|
{language.t("settings.workspaces.deleteAll")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
@@ -483,17 +486,17 @@ function DialogDeleteWorkspace(props: {
|
|||||||
/>
|
/>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="neutral" onClick={() => dialog.close()}>
|
<ButtonV2 type="button" variant="neutral" onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button
|
<ButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
disabled={status.isPending || status.isError || status.data?.result.active}
|
disabled={status.isPending || status.isError || status.data?.result.active}
|
||||||
onClick={remove}
|
onClick={remove}
|
||||||
>
|
>
|
||||||
{language.t("workspace.delete.button")}
|
{language.t("workspace.delete.button")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useMcpToggle } from "@/context/mcp"
|
|||||||
import { useWorkspaceLocation } from "@/context/location"
|
import { useWorkspaceLocation } from "@/context/location"
|
||||||
import { useData } from "@/context/server"
|
import { useData } from "@/context/server"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { pluginLabels } from "@/utils/plugin"
|
import { pluginLabel } from "@/utils/plugin"
|
||||||
|
|
||||||
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
||||||
const parts = value.split(file)
|
const parts = value.split(file)
|
||||||
@@ -39,7 +39,7 @@ export function StatusPopoverBody(props: { shown: boolean }) {
|
|||||||
() => (props.shown ? sdk().directory : undefined),
|
() => (props.shown ? sdk().directory : undefined),
|
||||||
(directory) => serverSDK.api.plugin.list({ location: { directory } }).then((result) => result.data),
|
(directory) => serverSDK.api.plugin.list({ location: { directory } }).then((result) => result.data),
|
||||||
)
|
)
|
||||||
const plugins = createMemo(() => pluginLabels(pluginList.latest ?? []))
|
const plugins = createMemo(() => (pluginList.latest ?? []).map(pluginLabel))
|
||||||
const pluginCount = createMemo(() => plugins().length)
|
const pluginCount = createMemo(() => plugins().length)
|
||||||
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
|
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
|
||||||
|
|
||||||
@@ -48,9 +48,10 @@ export function StatusPopoverBody(props: { shown: boolean }) {
|
|||||||
<Tabs
|
<Tabs
|
||||||
aria-label={language.t("status.popover.ariaLabel")}
|
aria-label={language.t("status.popover.ariaLabel")}
|
||||||
class="tabs bg-background-strong rounded-xl overflow-hidden"
|
class="tabs bg-background-strong rounded-xl overflow-hidden"
|
||||||
|
data-component="tabs"
|
||||||
data-active="mcp"
|
data-active="mcp"
|
||||||
defaultValue="mcp"
|
defaultValue="mcp"
|
||||||
variant="underline"
|
variant="alt"
|
||||||
>
|
>
|
||||||
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
||||||
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
||||||
@@ -110,7 +111,6 @@ export function StatusPopoverBody(props: { shown: boolean }) {
|
|||||||
</span>
|
</span>
|
||||||
<div onClick={(event) => event.stopPropagation()}>
|
<div onClick={(event) => event.stopPropagation()}>
|
||||||
<Switch
|
<Switch
|
||||||
appearance="standard"
|
|
||||||
checked={enabled()}
|
checked={enabled()}
|
||||||
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
|
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -156,7 +157,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
|
|||||||
<Popover
|
<Popover
|
||||||
open={props.state.shown}
|
open={props.state.shown}
|
||||||
onOpenChange={props.state.onOpenChange}
|
onOpenChange={props.state.onOpenChange}
|
||||||
triggerAs={IconButton}
|
triggerAs={IconButtonV2}
|
||||||
triggerProps={{
|
triggerProps={{
|
||||||
variant: "ghost-muted",
|
variant: "ghost-muted",
|
||||||
size: "large",
|
size: "large",
|
||||||
@@ -166,7 +167,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
|
|||||||
}}
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
<div class="relative size-4">
|
<div class="relative size-4">
|
||||||
<Icon name={props.state.shown ? "status-active" : "status"} />
|
<IconV2 name={props.state.shown ? "status-active" : "status"} />
|
||||||
<div
|
<div
|
||||||
class={`absolute -top-1 -right-1 size-2 rounded-full border border-[var(--v2-background-bg-deep)] ${serverStatusDotClass(props.state)}`}
|
class={`absolute -top-1 -right-1 size-2 rounded-full border border-[var(--v2-background-bg-deep)] ${serverStatusDotClass(props.state)}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } fro
|
|||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { createMutation } from "@tanstack/solid-query"
|
import { createMutation } from "@tanstack/solid-query"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { useGlobal, useServerCtx } from "@/context/global"
|
import { useGlobal, useServerCtx } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection, serverName, useServers } from "@/context/servers"
|
import { ServerConnection, serverName, useServers } from "@/context/servers"
|
||||||
@@ -281,7 +281,7 @@ export function TabNavItem(props: {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div data-slot="tab-close">
|
<div data-slot="tab-close">
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
size="small"
|
size="small"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
|
class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
|
||||||
@@ -290,7 +290,7 @@ export function TabNavItem(props: {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}}
|
}}
|
||||||
onClick={closeTab}
|
onClick={closeTab}
|
||||||
icon={<Icon name="xmark-small" />}
|
icon={<IconV2 name="xmark-small" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -377,7 +377,7 @@ export function DraftTabItem(props: {
|
|||||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
||||||
>
|
>
|
||||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||||
<Icon name="edit" />
|
<IconV2 name="edit" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
data-titlebar-tab-title
|
data-titlebar-tab-title
|
||||||
@@ -387,7 +387,7 @@ export function DraftTabItem(props: {
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<div data-slot="tab-close">
|
<div data-slot="tab-close">
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
size="small"
|
size="small"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
@@ -400,7 +400,7 @@ export function DraftTabItem(props: {
|
|||||||
}}
|
}}
|
||||||
class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
|
class="hover-reveal relative z-10 group-hover:opacity-100 group-data-[active=true]:opacity-100 group-data-[editing=true]:opacity-100"
|
||||||
onClick={closeTab}
|
onClick={closeTab}
|
||||||
icon={<Icon name="xmark-small" />}
|
icon={<IconV2 name="xmark-small" />}
|
||||||
aria-label={language.t("common.closeTab")}
|
aria-label={language.t("common.closeTab")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import {
|
|||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLocation, useNavigate } from "@solidjs/router"
|
import { useLocation, useNavigate } from "@solidjs/router"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
|
|
||||||
import { LayoutRoute, useLayout } from "@/context/layout"
|
import { LayoutRoute, useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
@@ -330,28 +330,28 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
<Show when={windows() || linux()}>
|
<Show when={windows() || linux()}>
|
||||||
<WindowsAppMenu command={command} platform={platform} />
|
<WindowsAppMenu command={command} platform={platform} />
|
||||||
</Show>
|
</Show>
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{language.t("home.title")}
|
{language.t("home.title")}
|
||||||
<Keybind keys={command.keybindParts("home.toggle")} variant="neutral" />
|
<KeybindV2 keys={command.keybindParts("home.toggle")} variant="neutral" />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
class="!w-9 shrink-0"
|
class="!w-9 shrink-0"
|
||||||
icon={<Icon name="grid-plus" />}
|
icon={<IconV2 name="grid-plus" />}
|
||||||
state={layout.route().type === "home" ? "pressed" : undefined}
|
state={layout.route().type === "home" ? "pressed" : undefined}
|
||||||
onClick={toggleHome}
|
onClick={toggleHome}
|
||||||
aria-label={language.t("home.title")}
|
aria-label={language.t("home.title")}
|
||||||
aria-pressed={layout.route().type === "home"}
|
aria-pressed={layout.route().type === "home"}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
|
|
||||||
<TitlebarTabStrip
|
<TitlebarTabStrip
|
||||||
tabs={tabsStore}
|
tabs={tabsStore}
|
||||||
@@ -368,25 +368,25 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
}}
|
}}
|
||||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{language.t("command.session.new")}
|
{language.t("command.session.new")}
|
||||||
<Keybind keys={newTabTooltipKeybind(command)} variant="neutral" />
|
<KeybindV2 keys={newTabTooltipKeybind(command)} variant="neutral" />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
icon={<Icon name="plus" />}
|
icon={<IconV2 name="plus" />}
|
||||||
onClick={openNewTab}
|
onClick={openNewTab}
|
||||||
aria-label={language.t("command.session.new")}
|
aria-label={language.t("command.session.new")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<TitlebarV2Right state={v2RightState()} />
|
<TitlebarV2Right state={v2RightState()} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { For, type JSX } from "solid-js"
|
import { For, Show, type JSX } from "solid-js"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
|
import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
|
||||||
@@ -46,32 +47,32 @@ export function WindowsAppMenu(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu appearance="standard" gutter={4} modal={false} placement="bottom-start">
|
<DropdownMenu gutter={4} modal={false} placement="bottom-start">
|
||||||
<div
|
<div
|
||||||
data-component="desktop-icon-button"
|
data-component="desktop-icon-button"
|
||||||
class="flex h-7 w-9 shrink-0 items-center justify-center rounded-[6px] px-1"
|
class="flex h-7 w-9 shrink-0 items-center justify-center rounded-[6px] px-1"
|
||||||
>
|
>
|
||||||
<Menu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={IconButton}
|
as={IconButtonV2}
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
icon={<Icon name="menu" />}
|
icon={<IconV2 name="menu" />}
|
||||||
aria-label={language.t("desktop.menu.ariaLabel")}
|
aria-label={language.t("desktop.menu.ariaLabel")}
|
||||||
onPointerDown={rememberFocus}
|
onPointerDown={rememberFocus}
|
||||||
onKeyDown={rememberFocus}
|
onKeyDown={rememberFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Menu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<Menu.Content class="desktop-app-menu">
|
<DropdownMenu.Content class="desktop-app-menu">
|
||||||
<Menu.Group>
|
<DropdownMenu.Group>
|
||||||
<Menu.GroupLabel class="desktop-app-menu-heading">OpenCode</Menu.GroupLabel>
|
<DropdownMenu.GroupLabel class="desktop-app-menu-heading">OpenCode</DropdownMenu.GroupLabel>
|
||||||
<For each={DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows"))}>
|
<For each={DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows"))}>
|
||||||
{(menu) => (
|
{(menu) => (
|
||||||
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
|
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
|
||||||
<For each={menu.items?.filter((entry) => desktopMenuVisible(entry, "windows"))}>
|
<For each={menu.items?.filter((entry) => desktopMenuVisible(entry, "windows"))}>
|
||||||
{(entry) => {
|
{(entry) => {
|
||||||
// Static menu data: an early return keeps the union narrowing a Show fallback would lose.
|
// Static menu data: an early return keeps the union narrowing a Show fallback would lose.
|
||||||
if (entry.type === "separator") return <Menu.Separator />
|
if (entry.type === "separator") return <DropdownMenu.Separator />
|
||||||
return (
|
return (
|
||||||
<DesktopMenuItem
|
<DesktopMenuItem
|
||||||
label={entry.labelKey ? language.t(entry.labelKey) : ""}
|
label={entry.labelKey ? language.t(entry.labelKey) : ""}
|
||||||
@@ -85,28 +86,36 @@ export function WindowsAppMenu(props: {
|
|||||||
</DesktopMenuSubmenu>
|
</DesktopMenuSubmenu>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Menu.Group>
|
</DropdownMenu.Group>
|
||||||
</Menu.Content>
|
</DropdownMenu.Content>
|
||||||
</Menu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</Menu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DesktopMenuSubmenu(props: { label: string; children: JSX.Element }) {
|
function DesktopMenuSubmenu(props: { label: string; children: JSX.Element }) {
|
||||||
return (
|
return (
|
||||||
<Menu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<Menu.SubTrigger>{props.label}</Menu.SubTrigger>
|
<DropdownMenu.SubTrigger>
|
||||||
<Menu.Portal>
|
<span data-slot="dropdown-menu-item-label">{props.label}</span>
|
||||||
<Menu.SubContent class="desktop-app-menu desktop-app-menu-sub">{props.children}</Menu.SubContent>
|
<span data-slot="desktop-app-menu-chevron">
|
||||||
</Menu.Portal>
|
<Icon name="chevron-right" size="small" />
|
||||||
</Menu.Sub>
|
</span>
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.SubContent class="desktop-app-menu">{props.children}</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DesktopMenuItem(props: { label: string; keybind?: string; disabled?: boolean; onSelect: () => void }) {
|
function DesktopMenuItem(props: { label: string; keybind?: string; disabled?: boolean; onSelect: () => void }) {
|
||||||
return (
|
return (
|
||||||
<Menu.Item disabled={props.disabled} onSelect={props.onSelect} shortcut={props.keybind}>
|
<DropdownMenu.Item disabled={props.disabled} onSelect={props.onSelect}>
|
||||||
{props.label}
|
<DropdownMenu.ItemLabel>{props.label}</DropdownMenu.ItemLabel>
|
||||||
</Menu.Item>
|
<Show when={props.keybind}>
|
||||||
|
<span data-slot="desktop-app-menu-keybind">{props.keybind}</span>
|
||||||
|
</Show>
|
||||||
|
</DropdownMenu.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { decode64 } from "@/utils/base64"
|
|||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
||||||
import { createPathHelpers } from "./file/path"
|
import { createPathHelpers } from "./file/path"
|
||||||
import type { ProjectAvatarVariant } from "@opencode-ai/ui/project-avatar"
|
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { SessionStateKey } from "@/utils/server-scope"
|
import { SessionStateKey } from "@/utils/server-scope"
|
||||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
||||||
import { requireServerKey } from "@/utils/session-route"
|
import { requireServerKey } from "@/utils/session-route"
|
||||||
|
|||||||
+21
-10
@@ -1,6 +1,6 @@
|
|||||||
@import "@opencode-ai/ui/styles/tailwind";
|
@import "@opencode-ai/ui/styles/tailwind";
|
||||||
@import "@opencode-ai/session-ui/styles";
|
@import "@opencode-ai/session-ui/styles";
|
||||||
@import "@opencode-ai/ui/styles/tokens";
|
@import "@opencode-ai/ui/v2/styles/tailwind.css";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -30,22 +30,23 @@
|
|||||||
container-name: getting-started;
|
container-name: getting-started;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="menu-v2-content"].desktop-app-menu {
|
[data-component="dropdown-menu-content"].desktop-app-menu,
|
||||||
|
[data-component="dropdown-menu-sub-content"].desktop-app-menu {
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="menu-v2-content"].desktop-app-menu:not(.desktop-app-menu-sub) {
|
[data-component="dropdown-menu-content"].desktop-app-menu {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="menu-v2-content"].desktop-app-menu-sub {
|
[data-component="dropdown-menu-sub-content"].desktop-app-menu {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
max-width: min(320px, calc(100vw - 24px));
|
max-width: min(320px, calc(100vw - 24px));
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="menu-v2-content"].desktop-app-menu [data-slot="menu-v2-group-label"] {
|
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-group-label"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@@ -56,7 +57,10 @@
|
|||||||
color: var(--text-weak);
|
color: var(--text-weak);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="menu-v2-content"].desktop-app-menu [data-component="menu-v2-item"] {
|
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-item"],
|
||||||
|
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-sub-trigger"],
|
||||||
|
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-item"],
|
||||||
|
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-sub-trigger"] {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -64,11 +68,12 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="menu-v2-content"].desktop-app-menu [data-slot="menu-v2-item-content"] {
|
[data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-item-label"],
|
||||||
|
[data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-item-label"] {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="menu-v2-content"].desktop-app-menu [data-slot="menu-v2-item-shortcut"] {
|
[data-slot="desktop-app-menu-keybind"] {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
color: var(--text-weak);
|
color: var(--text-weak);
|
||||||
font-size: var(--font-size-x-small);
|
font-size: var(--font-size-x-small);
|
||||||
@@ -76,13 +81,19 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-slot="desktop-app-menu-chevron"] {
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--icon-base);
|
||||||
|
}
|
||||||
|
|
||||||
[data-component="getting-started-actions"] {
|
[data-component="getting-started-actions"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem; /* gap-3 */
|
gap: 0.75rem; /* gap-3 */
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="getting-started-actions"] > [data-component="button-v2"] {
|
[data-component="getting-started-actions"] > [data-component="button"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +103,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="getting-started-actions"] > [data-component="button-v2"] {
|
[data-component="getting-started-actions"] > [data-component="button"] {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/d
|
|||||||
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers"
|
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers"
|
||||||
import { RestrictToElement } from "@dnd-kit/dom/modifiers"
|
import { RestrictToElement } from "@dnd-kit/dom/modifiers"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/context/layout"
|
import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/context/layout"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/servers"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -82,18 +82,18 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
|
|||||||
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
||||||
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
||||||
<Show when={props.servers.length === 1 && !(props.projects.length === 0 && props.recentlyClosed.length > 0)}>
|
<Show when={props.servers.length === 1 && !(props.projects.length === 0 && props.recentlyClosed.length > 0)}>
|
||||||
<Tooltip placement="bottom" value={props.language.t("home.project.add")}>
|
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
data-action="home-add-project"
|
data-action="home-add-project"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
|
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
|
||||||
icon={<Icon name="folder-add-left" />}
|
icon={<IconV2 name="folder-add-left" />}
|
||||||
disabled={props.serverHealth(props.servers[0])?.healthy === false}
|
disabled={props.serverHealth(props.servers[0])?.healthy === false}
|
||||||
onClick={() => props.onChooseProject(props.servers[0])}
|
onClick={() => props.onChooseProject(props.servers[0])}
|
||||||
aria-label={props.language.t("home.project.add")}
|
aria-label={props.language.t("home.project.add")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
|
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
|
||||||
@@ -161,7 +161,7 @@ export function HomeUtilityNav(props: {
|
|||||||
class="text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted"
|
class="text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted"
|
||||||
onClick={props.onOpenSettings}
|
onClick={props.onOpenSettings}
|
||||||
>
|
>
|
||||||
<Icon name="settings-gear" size="small" />
|
<IconV2 name="settings-gear" size="small" />
|
||||||
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.settings")}</span>
|
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.settings")}</span>
|
||||||
</HomeProjectNavButton>
|
</HomeProjectNavButton>
|
||||||
<HomeProjectNavButton
|
<HomeProjectNavButton
|
||||||
@@ -169,7 +169,7 @@ export function HomeUtilityNav(props: {
|
|||||||
class="text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted"
|
class="text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted"
|
||||||
onClick={props.onOpenHelp}
|
onClick={props.onOpenHelp}
|
||||||
>
|
>
|
||||||
<Icon name="help" size="small" />
|
<IconV2 name="help" size="small" />
|
||||||
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.help")}</span>
|
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.help")}</span>
|
||||||
</HomeProjectNavButton>
|
</HomeProjectNavButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,7 +234,7 @@ function HomeServerRow(props: {
|
|||||||
}}
|
}}
|
||||||
onPointerDown={(event) => event.preventDefault()}
|
onPointerDown={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<Icon
|
<IconV2
|
||||||
name="chevron-down"
|
name="chevron-down"
|
||||||
size="small"
|
size="small"
|
||||||
class="transition-transform duration-150 ease-in-out"
|
class="transition-transform duration-150 ease-in-out"
|
||||||
@@ -280,17 +280,17 @@ function HomeServerRow(props: {
|
|||||||
open={props.contextMenuOpen(contextMenuID())}
|
open={props.contextMenuOpen(contextMenuID())}
|
||||||
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
|
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
|
||||||
/>
|
/>
|
||||||
<Tooltip class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
|
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
data-action="home-add-project"
|
data-action="home-add-project"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<Icon name="folder-add-left" />}
|
icon={<IconV2 name="folder-add-left" />}
|
||||||
aria-label={props.language.t("home.project.add")}
|
aria-label={props.language.t("home.project.add")}
|
||||||
disabled={props.health?.healthy === false}
|
disabled={props.health?.healthy === false}
|
||||||
onClick={() => props.onChooseProject(props.server)}
|
onClick={() => props.onChooseProject(props.server)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -394,7 +394,7 @@ function HomeProjectEmpty(
|
|||||||
disabled={unreachable()}
|
disabled={unreachable()}
|
||||||
onClick={() => props.onChooseProject(props.server)}
|
onClick={() => props.onChooseProject(props.server)}
|
||||||
>
|
>
|
||||||
<Icon name="folder-add-left" size="small" />
|
<IconV2 name="folder-add-left" size="small" />
|
||||||
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("home.project.add")}</span>
|
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("home.project.add")}</span>
|
||||||
</HomeProjectNavButton>
|
</HomeProjectNavButton>
|
||||||
<Show when={props.items.length > 0}>
|
<Show when={props.items.length > 0}>
|
||||||
@@ -423,7 +423,7 @@ function HomeRecentlyClosedRow(
|
|||||||
return worktree
|
return worktree
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="right" value={path()}>
|
<TooltipV2 placement="right" value={path()}>
|
||||||
<HomeProjectNavButton
|
<HomeProjectNavButton
|
||||||
type="button"
|
type="button"
|
||||||
data-component="home-recently-closed-row"
|
data-component="home-recently-closed-row"
|
||||||
@@ -434,7 +434,7 @@ function HomeRecentlyClosedRow(
|
|||||||
<HomeProjectAvatar project={props.project} outline />
|
<HomeProjectAvatar project={props.project} outline />
|
||||||
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
|
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
|
||||||
</HomeProjectNavButton>
|
</HomeProjectNavButton>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,55 +523,55 @@ function HomeProjectRow(
|
|||||||
`}
|
`}
|
||||||
data-menu={props.contextMenuOpen(contextMenuID())}
|
data-menu={props.contextMenuOpen(contextMenuID())}
|
||||||
>
|
>
|
||||||
<Menu
|
<MenuV2
|
||||||
gutter={6}
|
gutter={6}
|
||||||
modal={false}
|
modal={false}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
open={props.contextMenuOpen(contextMenuID())}
|
open={props.contextMenuOpen(contextMenuID())}
|
||||||
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
|
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
|
||||||
>
|
>
|
||||||
<Menu.Trigger
|
<MenuV2.Trigger
|
||||||
as={IconButton}
|
as={IconButtonV2}
|
||||||
data-action="home-project-menu"
|
data-action="home-project-menu"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<Icon name="outline-dots" />}
|
icon={<IconV2 name="outline-dots" />}
|
||||||
aria-label={props.language.t("common.moreOptions")}
|
aria-label={props.language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content>
|
<MenuV2.Content>
|
||||||
<Menu.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
|
<MenuV2.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
|
||||||
{props.language.t("command.session.new")}
|
{props.language.t("command.session.new")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
<Menu.Item onSelect={() => props.onEditProject(props.server, props.project)}>
|
<MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
|
||||||
{props.language.t("dialog.project.edit.title")}
|
{props.language.t("dialog.project.edit.title")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
<Show when={props.canRevealProject(props.server)}>
|
<Show when={props.canRevealProject(props.server)}>
|
||||||
<Menu.Item onSelect={() => props.onRevealProject(props.server, props.project)}>
|
<MenuV2.Item onSelect={() => props.onRevealProject(props.server, props.project)}>
|
||||||
{props.language.t(
|
{props.language.t(
|
||||||
fileManagerApp(platform.platform === "desktop" ? (platform.os ?? "unknown") : "unknown")
|
fileManagerApp(platform.platform === "desktop" ? (platform.os ?? "unknown") : "unknown")
|
||||||
.actionLabel,
|
.actionLabel,
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Menu.Item
|
<MenuV2.Item
|
||||||
disabled={props.unseen === 0}
|
disabled={props.unseen === 0}
|
||||||
onSelect={() => props.onClearNotifications(props.server, props.project)}
|
onSelect={() => props.onClearNotifications(props.server, props.project)}
|
||||||
>
|
>
|
||||||
{props.language.t("sidebar.project.clearNotifications")}
|
{props.language.t("sidebar.project.clearNotifications")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
<Menu.Separator />
|
<MenuV2.Separator />
|
||||||
<Menu.Item onSelect={() => props.onCloseProject(props.server, props.project.worktree)}>
|
<MenuV2.Item onSelect={() => props.onCloseProject(props.server, props.project.worktree)}>
|
||||||
{props.language.t("common.close")}
|
{props.language.t("common.close")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
data-action="home-project-new-session"
|
data-action="home-project-new-session"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<Icon name="edit" />}
|
icon={<IconV2 name="edit" />}
|
||||||
aria-label={props.language.t("command.session.new")}
|
aria-label={props.language.t("command.session.new")}
|
||||||
onClick={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}
|
onClick={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { SessionInfo } from "@opencode-ai/client/promise"
|
|||||||
import { createMemo, For, Show, Suspense } from "solid-js"
|
import { createMemo, For, Show, Suspense } from "solid-js"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/servers"
|
||||||
import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar"
|
import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar"
|
||||||
@@ -83,7 +83,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<Show when={props.groups.length > 0 && props.canCreateSession}>
|
<Show when={props.groups.length > 0 && props.canCreateSession}>
|
||||||
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
|
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
|
||||||
<Button
|
<ButtonV2
|
||||||
data-action="home-new-session"
|
data-action="home-new-session"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="normal"
|
size="normal"
|
||||||
@@ -92,7 +92,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
|
|||||||
onClick={props.onCreateSession}
|
onClick={props.onCreateSession}
|
||||||
>
|
>
|
||||||
{props.language.t("command.session.new")}
|
{props.language.t("command.session.new")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -273,7 +273,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
|||||||
duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02 focus-within:bg-v2-background-bg-layer-02
|
duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02 focus-within:bg-v2-background-bg-layer-02
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Icon name="magnifying-glass" />
|
<IconV2 name="magnifying-glass" />
|
||||||
<input
|
<input
|
||||||
ref={props.onSetSearchInput}
|
ref={props.onSetSearchInput}
|
||||||
class={`
|
class={`
|
||||||
@@ -317,12 +317,12 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Show when={props.searchValue}>
|
<Show when={props.searchValue}>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
class="relative z-20 shrink-0"
|
class="relative z-20 shrink-0"
|
||||||
icon={<Icon name="close" size="large" class="text-v2-icon-icon-muted" />}
|
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||||
aria-label={props.searchPlaceholder}
|
aria-label={props.searchPlaceholder}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onSearchClose()
|
props.onSearchClose()
|
||||||
@@ -458,12 +458,12 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
|
|||||||
group-hover/session:opacity-100 focus-within:opacity-100
|
group-hover/session:opacity-100 focus-within:opacity-100
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Tooltip class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
|
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
data-action="home-session-archive"
|
data-action="home-session-archive"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
icon={<Icon name="archive" />}
|
icon={<IconV2 name="archive" />}
|
||||||
aria-label={props.language.t("common.archive")}
|
aria-label={props.language.t("common.archive")}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -471,7 +471,7 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
|
|||||||
void props.onArchiveSession(props.record.session)
|
void props.onArchiveSession(props.record.session)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -525,9 +525,9 @@ function HomeSessionsEmpty(props: { onNewSession?: () => void; language: ReturnT
|
|||||||
</p>
|
</p>
|
||||||
<Show when={props.onNewSession}>
|
<Show when={props.onNewSession}>
|
||||||
{(onNewSession) => (
|
{(onNewSession) => (
|
||||||
<Button data-action="home-new-session" variant="neutral" size="normal" icon="edit" onClick={onNewSession()}>
|
<ButtonV2 data-action="home-new-session" variant="neutral" size="normal" icon="edit" onClick={onNewSession()}>
|
||||||
{props.language.t("command.session.new")}
|
{props.language.t("command.session.new")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
|
import type { LocalProject } from "@/context/layout"
|
||||||
|
import { getProjectAvatarVariant } from "@/context/layout"
|
||||||
import type { ServerConnection } from "@/context/servers"
|
import type { ServerConnection } from "@/context/servers"
|
||||||
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { SessionProgressIndicatorV2 } from "@opencode-ai/session-ui/v2/session-progress-indicator-v2"
|
import { SessionProgressIndicatorV2 } from "@opencode-ai/session-ui/v2/session-progress-indicator-v2"
|
||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Wordmark } from "@opencode-ai/ui/wordmark"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
|
import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2"
|
||||||
import { Show, createMemo, createSignal } from "solid-js"
|
import { Show, createMemo, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
@@ -46,7 +47,7 @@ export function NewSessionView(props: {
|
|||||||
>
|
>
|
||||||
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
|
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
|
||||||
<div class={NEW_SESSION_CONTENT_WIDTH}>
|
<div class={NEW_SESSION_CONTENT_WIDTH}>
|
||||||
<Wordmark class="h-auto w-full text-v2-background-bg-inverse" />
|
<WordmarkV2 class="h-auto w-full text-v2-background-bg-inverse" />
|
||||||
<div class="mt-8 flex flex-col gap-8">
|
<div class="mt-8 flex flex-col gap-8">
|
||||||
<PromptInputV2Composer controller={props.input} accentSubmit={props.workspace.selection.workspace()} />
|
<PromptInputV2Composer controller={props.input} accentSubmit={props.workspace.selection.workspace()} />
|
||||||
<Show when={props.project.empty()}>
|
<Show when={props.project.empty()}>
|
||||||
@@ -95,7 +96,7 @@ export function NewSessionStatus(props: { mount: HTMLElement | null; visible: bo
|
|||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount}>
|
<Portal mount={mount}>
|
||||||
<Show when={props.visible}>
|
<Show when={props.visible}>
|
||||||
<Tooltip appearance="standard" placement="bottom" value={language.t("status.popover.trigger")}>
|
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||||
<StatusPopoverV2 />
|
<StatusPopoverV2 />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -152,7 +153,7 @@ function ProviderTip() {
|
|||||||
<Icon name="chevron-down" size="small" class="-rotate-90" />
|
<Icon name="chevron-down" size="small" class="-rotate-90" />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
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"
|
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"
|
placement="top"
|
||||||
openDelay={1000}
|
openDelay={1000}
|
||||||
@@ -166,7 +167,7 @@ function ProviderTip() {
|
|||||||
>
|
>
|
||||||
<Icon name="xmark-small" />
|
<Icon name="xmark-small" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export function useNewSessionCommands(input: {
|
|||||||
title: language.t("command.palette"),
|
title: language.t("command.palette"),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
const { DialogCommandPaletteV2 } = await import("@/components/dialog-command-palette-v2")
|
const { DialogSelectFile } = await import("@/components/dialog-select-file")
|
||||||
void dialog.show(() => <DialogCommandPaletteV2 />)
|
void dialog.show(() => <DialogSelectFile />)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ import { createStore } from "solid-js/store"
|
|||||||
import type { SessionReviewLineComment } from "@opencode-ai/session-ui/session-review"
|
import type { SessionReviewLineComment } from "@opencode-ai/session-ui/session-review"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
|
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
||||||
import { isScrollKeyTarget, scrollKey, scrollKeyOwner } from "@opencode-ai/ui/scroll-view"
|
import { isScrollKeyTarget, scrollKey, scrollKeyOwner } from "@opencode-ai/ui/scroll-view"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
@@ -194,9 +195,9 @@ function SessionErrorFallback(props: { error: unknown; sessionID?: string; serve
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Button variant="neutral" size="normal" icon="xmark-small" onClick={closeTab}>
|
<ButtonV2 variant="neutral" size="normal" icon="xmark-small" onClick={closeTab}>
|
||||||
{language.t("session.error.notFound.closeTab")}
|
{language.t("session.error.notFound.closeTab")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -1038,6 +1039,25 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
options={changesOptions()}
|
||||||
|
current={reviewMode()}
|
||||||
|
label={changesLabel}
|
||||||
|
onSelect={(option) => option && controller.layout.view().review.setMode(option)}
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
valueClass="text-14-medium"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changesTitleV2 = () => {
|
||||||
|
if (!canReview()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectV2
|
||||||
|
appearance="inline"
|
||||||
options={changesOptions()}
|
options={changesOptions()}
|
||||||
current={reviewMode()}
|
current={reviewMode()}
|
||||||
label={changesLabel}
|
label={changesLabel}
|
||||||
@@ -1141,7 +1161,7 @@ export default function Page() {
|
|||||||
// updates such as session switches.
|
// updates such as session switches.
|
||||||
const reviewPanelV2Props = () => ({
|
const reviewPanelV2Props = () => ({
|
||||||
get title() {
|
get title() {
|
||||||
return changesTitle()
|
return changesTitleV2()
|
||||||
},
|
},
|
||||||
get empty() {
|
get empty() {
|
||||||
return reviewEmptyV2()
|
return reviewEmptyV2()
|
||||||
@@ -1732,22 +1752,22 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="session"
|
value="session"
|
||||||
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
|
|
||||||
classList={{
|
classList={{
|
||||||
"!w-1/2 !max-w-none": true,
|
"!w-1/2 !max-w-none": true,
|
||||||
"!border-b-0 !border-t !border-border-weak-base [&:has([data-selected])]:!border-t-transparent": bottom,
|
"!border-b-0 !border-t !border-border-weak-base [&:has([data-selected])]:!border-t-transparent": bottom,
|
||||||
}}
|
}}
|
||||||
|
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
|
||||||
onClick={() => setStore("mobileTab", "session")}
|
onClick={() => setStore("mobileTab", "session")}
|
||||||
>
|
>
|
||||||
{language.t("session.tab.session")}
|
{language.t("session.tab.session")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="changes"
|
value="changes"
|
||||||
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
|
|
||||||
classList={{
|
classList={{
|
||||||
"!w-1/2 !max-w-none !border-r-0": true,
|
"!w-1/2 !max-w-none !border-r-0": true,
|
||||||
"!border-b-0 !border-t !border-border-weak-base [&:has([data-selected])]:!border-t-transparent": bottom,
|
"!border-b-0 !border-t !border-border-weak-base [&:has([data-selected])]:!border-t-transparent": bottom,
|
||||||
}}
|
}}
|
||||||
|
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
|
||||||
onClick={() => setStore("mobileTab", "changes")}
|
onClick={() => setStore("mobileTab", "changes")}
|
||||||
>
|
>
|
||||||
{hasReview()
|
{hasReview()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { For, createMemo } from "solid-js"
|
import { For, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { SessionComposerPullout } from "./session-composer-pullout"
|
import { SessionComposerPullout } from "./session-composer-pullout"
|
||||||
@@ -52,7 +52,7 @@ export function SessionBackgroundDock(props: {
|
|||||||
<span>
|
<span>
|
||||||
<span class="text-v2-text-text-muted">{moving()}</span>
|
<span class="text-v2-text-text-muted">{moving()}</span>
|
||||||
<span class="pl-2">
|
<span class="pl-2">
|
||||||
<Keybind keys={command.keybindParts("session.background")} variant="neutral" />
|
<KeybindV2 keys={command.keybindParts("session.background")} variant="neutral" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
@@ -108,7 +107,7 @@ export function SessionComposerPullout(props: {
|
|||||||
<IconButton
|
<IconButton
|
||||||
data-action={`session-${props.name}-toggle-button`}
|
data-action={`session-${props.name}-toggle-button`}
|
||||||
data-collapsed={props.collapsed ? "true" : "false"}
|
data-collapsed={props.collapsed ? "true" : "false"}
|
||||||
icon={<Icon name="chevron-down" />}
|
icon="chevron-down"
|
||||||
size="normal"
|
size="normal"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
style={{ transform: `rotate(${value() * 180}deg)` }}
|
style={{ transform: `rotate(${value() * 180}deg)` }}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { createStore } from "solid-js/store"
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
export function SessionFollowupDock(props: {
|
export function SessionFollowupDock(props: {
|
||||||
@@ -49,7 +48,7 @@ export function SessionFollowupDock(props: {
|
|||||||
<div class="ml-auto shrink-0">
|
<div class="ml-auto shrink-0">
|
||||||
<IconButton
|
<IconButton
|
||||||
data-collapsed={store.collapsed ? "true" : "false"}
|
data-collapsed={store.collapsed ? "true" : "false"}
|
||||||
icon={<Icon name="chevron-down" />}
|
icon="chevron-down"
|
||||||
size="normal"
|
size="normal"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
||||||
@@ -80,7 +79,7 @@ export function SessionFollowupDock(props: {
|
|||||||
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
|
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="neutral"
|
variant="secondary"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
disabled={!!props.sending}
|
disabled={!!props.sending}
|
||||||
onClick={() => props.onSend(item.id)}
|
onClick={() => props.onSend(item.id)}
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ export function SessionPermissionDock(props: {
|
|||||||
{language.t("ui.permission.deny")}
|
{language.t("ui.permission.deny")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="neutral"
|
variant="secondary"
|
||||||
size="normal"
|
size="normal"
|
||||||
onClick={() => props.onDecide("always")}
|
onClick={() => props.onDecide("always")}
|
||||||
disabled={props.responding}
|
disabled={props.responding}
|
||||||
>
|
>
|
||||||
{language.t("ui.permission.allowAlways")}
|
{language.t("ui.permission.allowAlways")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contrast" size="normal" onClick={() => props.onDecide("once")} disabled={props.responding}>
|
<Button variant="primary" size="normal" onClick={() => props.onDecide("once")} disabled={props.responding}>
|
||||||
{language.t("ui.permission.allowOnce")}
|
{language.t("ui.permission.allowOnce")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { For, Show, createEffect, createMemo, onCleanup, onMount, type Component
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
||||||
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
@@ -500,14 +499,19 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () =>
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<IconButton
|
<button
|
||||||
icon={<Icon name="chevron-down" size="small" />}
|
type="button"
|
||||||
variant="ghost"
|
data-component="icon-button"
|
||||||
|
data-icon="chevron-down"
|
||||||
|
data-size="normal"
|
||||||
|
data-variant="ghost"
|
||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
style={{ transform: `rotate(${hidden() * 180}deg)` }}
|
style={{ transform: `rotate(${hidden() * 180}deg)` }}
|
||||||
onClick={store.minimized ? restore : minimize}
|
onClick={store.minimized ? restore : minimize}
|
||||||
aria-label={language.t(store.minimized ? "session.question.restore" : "session.question.minimize")}
|
aria-label={language.t(store.minimized ? "session.question.restore" : "session.question.minimize")}
|
||||||
/>
|
>
|
||||||
|
<Icon name="chevron-down" size="small" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -518,12 +522,12 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () =>
|
|||||||
</Button>
|
</Button>
|
||||||
<div data-slot="question-footer-actions">
|
<div data-slot="question-footer-actions">
|
||||||
<Show when={store.tab > 0}>
|
<Show when={store.tab > 0}>
|
||||||
<Button variant="neutral" size="large" disabled={sending()} onClick={back}>
|
<Button variant="secondary" size="large" disabled={sending()} onClick={back}>
|
||||||
{language.t("ui.common.back")}
|
{language.t("ui.common.back")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
<Button
|
<Button
|
||||||
variant={last() ? "contrast" : "neutral"}
|
variant={last() ? "primary" : "secondary"}
|
||||||
size="large"
|
size="large"
|
||||||
disabled={sending()}
|
disabled={sending()}
|
||||||
onClick={next}
|
onClick={next}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { For, Show, createEffect, createMemo } from "solid-js"
|
import { For, Show, createEffect, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
export function SessionRevertDock(props: {
|
export function SessionRevertDock(props: {
|
||||||
@@ -45,7 +45,7 @@ export function SessionRevertDock(props: {
|
|||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
onKeyDown={onHeaderKeyDown}
|
onKeyDown={onHeaderKeyDown}
|
||||||
>
|
>
|
||||||
<Icon name="outline-reset" size="normal" class="text-v2-icon-icon-muted" />
|
<IconV2 name="outline-reset" size="normal" class="text-v2-icon-icon-muted" />
|
||||||
<span
|
<span
|
||||||
classList={{
|
classList={{
|
||||||
"font-[440] shrink-0 cursor-default text-[13px] leading-5 tracking-[-0.04px]": true,
|
"font-[440] shrink-0 cursor-default text-[13px] leading-5 tracking-[-0.04px]": true,
|
||||||
@@ -61,8 +61,8 @@ export function SessionRevertDock(props: {
|
|||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="ml-auto shrink-0">
|
<div class="ml-auto shrink-0">
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
icon={<Icon name="outline-chevron-down" size="small" />}
|
icon={<IconV2 name="outline-chevron-down" size="small" />}
|
||||||
size="large"
|
size="large"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
||||||
@@ -95,7 +95,7 @@ export function SessionRevertDock(props: {
|
|||||||
<span class="min-w-0 flex-1 truncate text-[13px] font-[400] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
|
<span class="min-w-0 flex-1 truncate text-[13px] font-[400] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
|
||||||
{item.text}
|
{item.text}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<ButtonV2
|
||||||
size="small"
|
size="small"
|
||||||
variant="neutral"
|
variant="neutral"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
@@ -103,7 +103,7 @@ export function SessionRevertDock(props: {
|
|||||||
onClick={() => props.onRestore(item.id)}
|
onClick={() => props.onRestore(item.id)}
|
||||||
>
|
>
|
||||||
{language.t("session.revertDock.restore")}
|
{language.t("session.revertDock.restore")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { useFileComponent } from "@opencode-ai/ui/context/file"
|
|||||||
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
||||||
import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2"
|
import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2"
|
||||||
import { sampledChecksum } from "@opencode-ai/util/encode"
|
import { sampledChecksum } from "@opencode-ai/util/encode"
|
||||||
import { LineCommentOverflowIcon } from "@opencode-ai/ui/line-comment"
|
import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
@@ -26,7 +26,7 @@ type SessionFileViewProps = {
|
|||||||
|
|
||||||
const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions"
|
const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions"
|
||||||
|
|
||||||
function FileCommentMenu(props: {
|
function FileCommentMenuV2(props: {
|
||||||
moreLabel: string
|
moreLabel: string
|
||||||
editLabel: string
|
editLabel: string
|
||||||
deleteLabel: string
|
deleteLabel: string
|
||||||
@@ -35,17 +35,17 @@ function FileCommentMenu(props: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
|
<div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
|
||||||
<Menu gutter={4}>
|
<MenuV2 gutter={4}>
|
||||||
<Menu.Trigger as="button" type="button" data-slot="line-comment-v2-overflow" aria-label={props.moreLabel}>
|
<MenuV2.Trigger as="button" type="button" data-slot="line-comment-v2-overflow" aria-label={props.moreLabel}>
|
||||||
<LineCommentOverflowIcon />
|
<LineCommentV2OverflowIcon />
|
||||||
</Menu.Trigger>
|
</MenuV2.Trigger>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content>
|
<MenuV2.Content>
|
||||||
<Menu.Item onSelect={props.onEdit}>{props.editLabel}</Menu.Item>
|
<MenuV2.Item onSelect={props.onEdit}>{props.editLabel}</MenuV2.Item>
|
||||||
<Menu.Item onSelect={props.onDelete}>{props.deleteLabel}</Menu.Item>
|
<MenuV2.Item onSelect={props.onDelete}>{props.deleteLabel}</MenuV2.Item>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -331,7 +331,7 @@ export function SessionFileView(props: SessionFileViewProps) {
|
|||||||
},
|
},
|
||||||
editSubmitLabel: language.t("common.save"),
|
editSubmitLabel: language.t("common.save"),
|
||||||
renderCommentActions: (_, controls) => (
|
renderCommentActions: (_, controls) => (
|
||||||
<FileCommentMenu
|
<FileCommentMenuV2
|
||||||
moreLabel={language.t("common.moreOptions")}
|
moreLabel={language.t("common.moreOptions")}
|
||||||
editLabel={language.t("common.edit")}
|
editLabel={language.t("common.edit")}
|
||||||
deleteLabel={language.t("common.delete")}
|
deleteLabel={language.t("common.delete")}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||||
|
|
||||||
import FileTree from "@/components/file-tree"
|
import FileTree from "@/components/file-tree"
|
||||||
@@ -312,27 +313,30 @@ export function SessionSidePanel(props: {
|
|||||||
<Show when={contextOpen()}>
|
<Show when={contextOpen()}>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="context"
|
value="context"
|
||||||
onMiddleClick={() => tabs().close("context")}
|
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{language.t("common.closeTab")}
|
{language.t("common.closeTab")}
|
||||||
<Show when={closeTabKeybind().length > 0}>
|
<Show when={closeTabKeybind().length > 0}>
|
||||||
<Keybind keys={closeTabKeybind()} variant="neutral" />
|
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
gutter={10}
|
gutter={10}
|
||||||
>
|
>
|
||||||
<Tabs.CloseButton
|
<IconButton
|
||||||
|
icon="close-small"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-5 w-5"
|
||||||
onClick={() => tabs().close("context")}
|
onClick={() => tabs().close("context")}
|
||||||
aria-label={language.t("common.closeTab")}
|
aria-label={language.t("common.closeTab")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
}
|
}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
|
onMiddleClick={() => tabs().close("context")}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<SessionContextUsage variant="indicator" />
|
<SessionContextUsage variant="indicator" />
|
||||||
@@ -356,27 +360,30 @@ export function SessionSidePanel(props: {
|
|||||||
>
|
>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value={SESSION_OPEN_FILE_TAB}
|
value={SESSION_OPEN_FILE_TAB}
|
||||||
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{language.t("common.closeTab")}
|
{language.t("common.closeTab")}
|
||||||
<Show when={closeTabKeybind().length > 0}>
|
<Show when={closeTabKeybind().length > 0}>
|
||||||
<Keybind keys={closeTabKeybind()} variant="neutral" />
|
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
gutter={10}
|
gutter={10}
|
||||||
>
|
>
|
||||||
<Tabs.CloseButton
|
<IconButton
|
||||||
|
icon="close-small"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-5 w-5"
|
||||||
onClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
onClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
||||||
aria-label={language.t("common.closeTab")}
|
aria-label={language.t("common.closeTab")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
}
|
}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
|
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1.5 italic">
|
<div class="flex items-center gap-1.5 italic">
|
||||||
<Icon name="open-file" size="small" />
|
<Icon name="open-file" size="small" />
|
||||||
@@ -387,26 +394,26 @@ export function SessionSidePanel(props: {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<div class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
|
<div class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{language.t("command.file.open")}
|
{language.t("command.file.open")}
|
||||||
<Show when={openFileKeybind().length > 0}>
|
<Show when={openFileKeybind().length > 0}>
|
||||||
<Keybind keys={openFileKeybind()} variant="neutral" />
|
<KeybindV2 keys={openFileKeybind()} variant="neutral" />
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
icon={<Icon name="plus-small" />}
|
icon={<Icon name="plus-small" />}
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => openFileBrowser()}
|
onClick={() => openFileBrowser()}
|
||||||
aria-label={language.t("command.file.open")}
|
aria-label={language.t("command.file.open")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<div
|
<div
|
||||||
@@ -496,7 +503,7 @@ export function SessionSidePanel(props: {
|
|||||||
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
variant="surface"
|
variant="pill"
|
||||||
value={fileTreeTab()}
|
value={fileTreeTab()}
|
||||||
onChange={setFileTreeTabValue}
|
onChange={setFileTreeTabValue}
|
||||||
class="h-full"
|
class="h-full"
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ import { RestrictToElement } from "@dnd-kit/dom/modifiers"
|
|||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
|
||||||
|
|
||||||
import { SortableTerminalTabV2 } from "@/components/session/session-sortable-terminal-tab-v2"
|
import { SortableTerminalTabV2 } from "@/components/session/session-sortable-terminal-tab-v2"
|
||||||
import { Terminal } from "@/components/terminal"
|
import { Terminal } from "@/components/terminal"
|
||||||
@@ -250,7 +249,7 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<Tabs
|
<Tabs
|
||||||
variant="panel"
|
variant="normal"
|
||||||
value={terminal.active()}
|
value={terminal.active()}
|
||||||
onChange={(id) => terminal.open(id)}
|
onChange={(id) => terminal.open(id)}
|
||||||
class="!h-[52px] !flex-none"
|
class="!h-[52px] !flex-none"
|
||||||
@@ -267,12 +266,12 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
|
|||||||
{(pty, index) => <SortableTerminalTabV2 terminal={pty} index={index()} onClose={close} />}
|
{(pty, index) => <SortableTerminalTabV2 terminal={pty} index={index()} onClose={close} />}
|
||||||
</For>
|
</For>
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{language.t("command.terminal.new")}
|
{language.t("command.terminal.new")}
|
||||||
<Show when={newTerminalKeybind().length > 0}>
|
<Show when={newTerminalKeybind().length > 0}>
|
||||||
<Keybind keys={newTerminalKeybind()} variant="neutral" />
|
<KeybindV2 keys={newTerminalKeybind()} variant="neutral" />
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -280,12 +279,13 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
|
|||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<Icon name="plus-small" size="large" />}
|
icon="plus-small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
iconSize="large"
|
||||||
onClick={() => terminal.new({ focus: true })}
|
onClick={() => terminal.new({ focus: true })}
|
||||||
aria-label={language.t("command.terminal.new")}
|
aria-label={language.t("command.terminal.new")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||||
import { DialogFooter, DialogHeader, DialogTitleGroup, Dialog } from "@opencode-ai/ui/dialog"
|
import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, on } from "solid-js"
|
import { createEffect, createMemo, on } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -186,7 +186,7 @@ export function createTimelineController(input: { session: TimelineSessionSource
|
|||||||
dialog.close()
|
dialog.close()
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Dialog fit>
|
<DialogV2 fit>
|
||||||
<DialogHeader hideClose>
|
<DialogHeader hideClose>
|
||||||
<DialogTitleGroup
|
<DialogTitleGroup
|
||||||
title={language.t("session.delete.title")}
|
title={language.t("session.delete.title")}
|
||||||
@@ -194,14 +194,14 @@ export function createTimelineController(input: { session: TimelineSessionSource
|
|||||||
/>
|
/>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" onClick={() => dialog.close()}>
|
<ButtonV2 variant="ghost" onClick={() => dialog.close()}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button variant="danger" onClick={confirm}>
|
<ButtonV2 variant="danger" onClick={confirm}>
|
||||||
{language.t("session.delete.button")}
|
{language.t("session.delete.button")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</DialogV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualIt
|
|||||||
import { Card } from "@opencode-ai/ui/card"
|
import { Card } from "@opencode-ai/ui/card"
|
||||||
import { MessageDivider, SessionShellMessage, type UserActions } from "@opencode-ai/session-ui/message-part"
|
import { MessageDivider, SessionShellMessage, type UserActions } from "@opencode-ai/session-ui/message-part"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
|
||||||
import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
|
import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
|
||||||
import { isScrollKeyTarget, scrollKey, scrollKeyOwner, ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { isScrollKeyTarget, scrollKey, scrollKeyOwner, ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
||||||
@@ -138,7 +138,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"
|
: "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="workspace-new" class="shrink-0 text-v2-icon-icon-muted" />
|
<IconV2 name="workspace-new" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<span class="min-w-0 truncate">{language.t("workspace.move.title")}</span>
|
<span class="min-w-0 truncate">{language.t("workspace.move.title")}</span>
|
||||||
</SessionWorkspaceMenu>
|
</SessionWorkspaceMenu>
|
||||||
<button
|
<button
|
||||||
@@ -154,7 +154,7 @@ function WorkspaceMoveAction(props: {
|
|||||||
props.onDismiss()
|
props.onDismiss()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="xmark-small" />
|
<IconV2 name="xmark-small" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -203,12 +203,12 @@ function SessionSummaryPanel(props: {
|
|||||||
gutter={-22}
|
gutter={-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`}
|
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" : "workspace-isolated"} class="shrink-0 text-v2-icon-icon-muted" />
|
<IconV2 name={props.local ? "monitor" : "workspace-isolated"} class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<span class="min-w-0 flex-1 truncate text-start">{location()}</span>
|
<span class="min-w-0 flex-1 truncate text-start">{location()}</span>
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
<IconV2 name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
</SessionWorkspaceMenu>
|
</SessionWorkspaceMenu>
|
||||||
<div class={row}>
|
<div class={row}>
|
||||||
<Icon name="branch" class="shrink-0 text-v2-icon-icon-muted" />
|
<IconV2 name="branch" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<Show
|
<Show
|
||||||
when={props.branch}
|
when={props.branch}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -235,13 +235,13 @@ function SessionSummaryPanel(props: {
|
|||||||
class={`${row} hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none`}
|
class={`${row} hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none`}
|
||||||
onClick={props.onReview}
|
onClick={props.onReview}
|
||||||
>
|
>
|
||||||
<Icon name="review" class="shrink-0 text-v2-icon-icon-muted" />
|
<IconV2 name="review" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<Show when={props.diffs} fallback={<span>{language.t("session.review.loadingChanges")}</span>}>
|
<Show when={props.diffs} fallback={<span>{language.t("session.review.loadingChanges")}</span>}>
|
||||||
{(diffs) => (
|
{(diffs) => (
|
||||||
<Show when={diffs().length > 0} fallback={<span>{language.t("session.review.noChanges")}</span>}>
|
<Show when={diffs().length > 0} fallback={<span>{language.t("session.review.noChanges")}</span>}>
|
||||||
<span>{language.plural("ui.sessionTurn.diffs.changed", diffs().length)}</span>
|
<span>{language.plural("ui.sessionTurn.diffs.changed", diffs().length)}</span>
|
||||||
<span class="text-v2-text-text-muted">·</span>
|
<span class="text-v2-text-text-muted">·</span>
|
||||||
<DiffChanges appearance="standard" changes={diffs()} />
|
<DiffChanges changes={diffs()} />
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -1175,11 +1175,11 @@ function MessageTimelineView(
|
|||||||
when={workspaceSession()}
|
when={workspaceSession()}
|
||||||
fallback={
|
fallback={
|
||||||
<span class="flex size-6 shrink-0 items-center justify-center text-v2-icon-icon-muted">
|
<span class="flex size-6 shrink-0 items-center justify-center text-v2-icon-icon-muted">
|
||||||
<Icon name="monitor" />
|
<IconV2 name="monitor" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<TooltipV2
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
value={sessionDirectory()}
|
value={sessionDirectory()}
|
||||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||||
@@ -1189,9 +1189,9 @@ function MessageTimelineView(
|
|||||||
aria-label={sessionDirectory()}
|
aria-label={sessionDirectory()}
|
||||||
class="flex size-6 shrink-0 items-center justify-center text-v2-icon-icon-accent"
|
class="flex size-6 shrink-0 items-center justify-center text-v2-icon-icon-accent"
|
||||||
>
|
>
|
||||||
<Icon name="workspace-isolated" />
|
<IconV2 name="workspace-isolated" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={parentID()}>
|
<Show when={parentID()}>
|
||||||
<button
|
<button
|
||||||
@@ -1268,8 +1268,8 @@ function MessageTimelineView(
|
|||||||
onOpenChange={setSummary}
|
onOpenChange={setSummary}
|
||||||
>
|
>
|
||||||
<KobaltePopover.Trigger
|
<KobaltePopover.Trigger
|
||||||
as={IconButton}
|
as={IconButtonV2}
|
||||||
icon={<Icon name="window-analytics" />}
|
icon={<IconV2 name="window-analytics" />}
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
state={summaryOpen() ? "pressed" : undefined}
|
state={summaryOpen() ? "pressed" : undefined}
|
||||||
@@ -1300,7 +1300,7 @@ function MessageTimelineView(
|
|||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!parentID()}>
|
<Show when={!parentID()}>
|
||||||
<Menu
|
<MenuV2
|
||||||
gutter={6}
|
gutter={6}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
open={title.menuOpen}
|
open={title.menuOpen}
|
||||||
@@ -1309,9 +1309,9 @@ function MessageTimelineView(
|
|||||||
if (open) return
|
if (open) return
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu.Trigger
|
<MenuV2.Trigger
|
||||||
as={IconButton}
|
as={IconButtonV2}
|
||||||
icon={<Icon name="outline-dots" />}
|
icon={<IconV2 name="outline-dots" />}
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="large"
|
size="large"
|
||||||
state={share.open || title.pendingShare ? "pressed" : undefined}
|
state={share.open || title.pendingShare ? "pressed" : undefined}
|
||||||
@@ -1321,8 +1321,8 @@ function MessageTimelineView(
|
|||||||
more = el
|
more = el
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content
|
<MenuV2.Content
|
||||||
style={{ width: "120px", "min-width": "120px" }}
|
style={{ width: "120px", "min-width": "120px" }}
|
||||||
onCloseAutoFocus={(event) => {
|
onCloseAutoFocus={(event) => {
|
||||||
if (title.pendingRename) {
|
if (title.pendingRename) {
|
||||||
@@ -1340,34 +1340,34 @@ function MessageTimelineView(
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<MenuV2.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setTitle("pendingRename", true)
|
setTitle("pendingRename", true)
|
||||||
setTitle("menuOpen", false)
|
setTitle("menuOpen", false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("common.rename")}
|
{language.t("common.rename")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
<Show when={shareEnabled()}>
|
<Show when={shareEnabled()}>
|
||||||
<Menu.Item
|
<MenuV2.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setTitle({ pendingShare: true, menuOpen: false })
|
setTitle({ pendingShare: true, menuOpen: false })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("session.share.action.share")}...
|
{language.t("session.share.action.share")}...
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Menu.Item onSelect={() => void props.action.export(id)}>
|
<MenuV2.Item onSelect={() => void props.action.export(id)}>
|
||||||
{language.t("common.export")}...
|
{language.t("common.export")}...
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
{/* TODO: Need a V2 session archive API. */}
|
{/* TODO: Need a V2 session archive API. */}
|
||||||
<Menu.Separator />
|
<MenuV2.Separator />
|
||||||
<Menu.Item onSelect={() => props.action.showDelete(id)}>
|
<MenuV2.Item onSelect={() => props.action.showDelete(id)}>
|
||||||
{language.t("common.delete")}...
|
{language.t("common.delete")}...
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
|
|
||||||
<KobaltePopover
|
<KobaltePopover
|
||||||
open={share.open}
|
open={share.open}
|
||||||
@@ -1415,7 +1415,7 @@ function MessageTimelineView(
|
|||||||
<Show
|
<Show
|
||||||
when={shareUrl()}
|
when={shareUrl()}
|
||||||
fallback={
|
fallback={
|
||||||
<Button
|
<ButtonV2
|
||||||
variant="contrast"
|
variant="contrast"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={() => void props.action.share()}
|
onClick={() => void props.action.share()}
|
||||||
@@ -1424,7 +1424,7 @@ function MessageTimelineView(
|
|||||||
{props.pending.share()
|
{props.pending.share()
|
||||||
? language.t("session.share.action.publishing")
|
? language.t("session.share.action.publishing")
|
||||||
: language.t("session.share.action.publish")}
|
: language.t("session.share.action.publish")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@@ -1441,26 +1441,26 @@ function MessageTimelineView(
|
|||||||
>
|
>
|
||||||
{shareUrl()}
|
{shareUrl()}
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
icon={<Icon name="outline-copy" />}
|
icon={<IconV2 name="outline-copy" />}
|
||||||
aria-label={language.t("session.share.copy.copyLink")}
|
aria-label={language.t("session.share.copy.copyLink")}
|
||||||
onClick={() => void props.action.copyShareUrl()}
|
onClick={() => void props.action.copyShareUrl()}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
icon={<Icon name="outline-square-arrow" />}
|
icon={<IconV2 name="outline-square-arrow" />}
|
||||||
aria-label={language.t("session.share.action.view")}
|
aria-label={language.t("session.share.action.view")}
|
||||||
onClick={props.action.viewShare}
|
onClick={props.action.viewShare}
|
||||||
disabled={props.pending.unshare()}
|
disabled={props.pending.unshare()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<Button
|
<ButtonV2
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={() => void props.action.unshare()}
|
onClick={() => void props.action.unshare()}
|
||||||
@@ -1469,7 +1469,7 @@ function MessageTimelineView(
|
|||||||
{props.pending.unshare()
|
{props.pending.unshare()
|
||||||
? language.t("session.share.action.unpublishing")
|
? language.t("session.share.action.unpublishing")
|
||||||
: language.t("session.share.action.unpublish")}
|
: language.t("session.share.action.unpublish")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import type { SessionStatus } from "@opencode-ai/client/promise"
|
|||||||
import { onCleanup } from "solid-js"
|
import { onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useSessionLayout } from "./session-layout"
|
import { useSessionLayout } from "./session-layout"
|
||||||
import { useDialog, useI18n } from "@opencode-ai/ui/context"
|
import { useDialog } from "@opencode-ai/ui/context"
|
||||||
import { DialogUsageExceeded } from "@/components/dialog-usage-exceeded"
|
import { DialogUsageExceeded } from "@/components/dialog-usage-exceeded"
|
||||||
|
import { useI18n } from "@opencode-ai/ui/context"
|
||||||
|
|
||||||
const GO_UPSELL_FREE_TIER_LAST_SEEN_AT = "go_upsell_last_seen_at"
|
const GO_UPSELL_FREE_TIER_LAST_SEEN_AT = "go_upsell_last_seen_at"
|
||||||
const GO_UPSELL_FREE_TIER_DONT_SHOW = "go_upsell_dont_show"
|
const GO_UPSELL_FREE_TIER_DONT_SHOW = "go_upsell_dont_show"
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
|
|
||||||
const openFile = () => {
|
const openFile = () => {
|
||||||
void openDialog(
|
void openDialog(
|
||||||
() => import("@/components/dialog-command-palette-v2"),
|
() => import("@/components/dialog-select-file"),
|
||||||
(x) => dialog.show(() => <x.DialogCommandPaletteV2 onOpenFile={showAllFiles} />),
|
(x) => dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
SessionReviewV2Sidebar,
|
SessionReviewV2Sidebar,
|
||||||
} from "@opencode-ai/session-ui/v2/session-review-v2"
|
} from "@opencode-ai/session-ui/v2/session-review-v2"
|
||||||
import { SessionReviewFilePreviewV2 } from "@opencode-ai/session-ui/v2/session-review-file-preview-v2"
|
import { SessionReviewFilePreviewV2 } from "@opencode-ai/session-ui/v2/session-review-file-preview-v2"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2"
|
||||||
import type {
|
import type {
|
||||||
SessionReviewComment,
|
SessionReviewComment,
|
||||||
SessionReviewCommentActions,
|
SessionReviewCommentActions,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import "@opencode-ai/ui/file-tree.css"
|
import "@opencode-ai/ui/v2/file-tree-v2.css"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
|
||||||
import { kindChange, kindLabel, type Kind } from "@/components/file-tree-v2"
|
import { kindChange, kindLabel, type Kind } from "@/components/file-tree-v2"
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import type { PluginInfo } from "@opencode-ai/client"
|
|
||||||
import { pluginLabels } from "./plugin"
|
|
||||||
|
|
||||||
describe("pluginLabels", () => {
|
|
||||||
test("omits built-in plugins", () => {
|
|
||||||
const plugins: PluginInfo[] = [
|
|
||||||
{ id: "opencode.internal", source: { type: "builtin" }, status: "active", tui: false },
|
|
||||||
{ id: "package-plugin", source: { type: "package", package: "example" }, status: "active", tui: false },
|
|
||||||
{ id: "local-plugin", source: { type: "local", path: "/tmp/plugin.ts" }, status: "active", tui: false },
|
|
||||||
{ id: "sdk-plugin", source: { type: "sdk" }, status: "active", tui: false },
|
|
||||||
]
|
|
||||||
|
|
||||||
expect(pluginLabels(plugins)).toEqual(["package-plugin", "local-plugin", "sdk-plugin"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,7 +6,3 @@ export function pluginLabel(plugin: PluginInfo) {
|
|||||||
if (plugin.source.type === "local") return plugin.source.path
|
if (plugin.source.type === "local") return plugin.source.path
|
||||||
return plugin.source.type
|
return plugin.source.type
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pluginLabels(plugins: readonly PluginInfo[]) {
|
|
||||||
return plugins.filter((plugin) => plugin.source.type !== "builtin").map(pluginLabel)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
|
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
|
||||||
import { Toast, showToast, toaster, type ToastOptions } from "@opencode-ai/ui/toast"
|
import type { ToastOptions, ToastVariant } from "@opencode-ai/ui/toast"
|
||||||
|
import { ToastV2, showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||||
type AppToastOptions = Omit<ToastOptions, "icon"> & {
|
|
||||||
icon?: IconProps["name"]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ToastRegion() {
|
export function ToastRegion() {
|
||||||
return <Toast.Region />
|
return <ToastV2.Region />
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAppToast(options: AppToastOptions | string) {
|
export function showToast(options: ToastOptions | string) {
|
||||||
if (typeof options === "string") return showToast(options)
|
if (typeof options === "string") return showToastV2(options)
|
||||||
|
|
||||||
return showToast({
|
return showToastV2({
|
||||||
...options,
|
...options,
|
||||||
icon: resolveIcon(options.icon, options.variant),
|
icon: resolveIcon(options.icon, options.variant),
|
||||||
actions: options.actions?.map((action) => ({
|
actions: options.actions?.map((action) => ({
|
||||||
@@ -22,13 +19,11 @@ function showAppToast(options: AppToastOptions | string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { showAppToast as showToast }
|
|
||||||
|
|
||||||
export function dismissToast(toastId: number) {
|
export function dismissToast(toastId: number) {
|
||||||
return toaster.dismiss(toastId)
|
return toasterV2.dismiss(toastId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastOptions["variant"]) {
|
function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastVariant | undefined) {
|
||||||
const name = icon ?? (variant === "success" ? "check" : undefined)
|
const name = icon ?? (variant === "success" ? "check" : undefined)
|
||||||
if (!name) return
|
if (!name) return
|
||||||
return <Icon name={name} />
|
return <Icon name={name} />
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Divider } from "@opencode-ai/ui/divider"
|
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { Loader } from "@opencode-ai/ui/loader"
|
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
||||||
import { RadioGroup, RadioItem } from "@opencode-ai/ui/radio"
|
import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2"
|
||||||
import { TextInput } from "@opencode-ai/ui/text-input"
|
import { RadioGroupV2, RadioItemV2 } from "@opencode-ai/ui/v2/radio-v2"
|
||||||
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { createMemo, For, Show } from "solid-js"
|
import { createMemo, For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { showToast } from "@/utils/toast"
|
|
||||||
import { useWslAddServerProbes } from "./add-server-probes"
|
import { useWslAddServerProbes } from "./add-server-probes"
|
||||||
import { useWslServers } from "./context"
|
import { useWslServers } from "./context"
|
||||||
import { addServerViewModel, type AddServerText } from "./settings-model"
|
import { addServerViewModel, type AddServerText } from "./settings-model"
|
||||||
@@ -50,7 +50,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
fallback={<div class="settings-v2-wsl-loading">{controller.loadError()}</div>}
|
fallback={<div class="settings-v2-wsl-loading">{controller.loadError()}</div>}
|
||||||
>
|
>
|
||||||
<div class="settings-v2-wsl-loading">
|
<div class="settings-v2-wsl-loading">
|
||||||
<Loader />
|
<LoaderV2 />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -73,7 +73,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
>
|
>
|
||||||
<Dialog fit class="settings-v2-wsl-dialog">
|
<Dialog fit class="settings-v2-wsl-dialog">
|
||||||
<div class="settings-v2-wsl-loading">
|
<div class="settings-v2-wsl-loading">
|
||||||
<Loader />
|
<LoaderV2 />
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -85,13 +85,13 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
{controller.view() === "main" ? language.t("wsl.server.add") : language.t("wsl.onboarding.installDistro")}
|
{controller.view() === "main" ? language.t("wsl.server.add") : language.t("wsl.onboarding.installDistro")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Divider />
|
<DividerV2 />
|
||||||
<Show
|
<Show
|
||||||
when={controller.view() === "main"}
|
when={controller.view() === "main"}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<DialogBody class="settings-v2-wsl-dialog-body settings-v2-wsl-catalog-picker">
|
<DialogBody class="settings-v2-wsl-dialog-body settings-v2-wsl-catalog-picker">
|
||||||
<TextInput
|
<TextInputV2
|
||||||
class="settings-v2-wsl-catalog-search"
|
class="settings-v2-wsl-catalog-search"
|
||||||
appearance="large"
|
appearance="large"
|
||||||
placeholder={language.t("wsl.onboarding.searchDistros")}
|
placeholder={language.t("wsl.onboarding.searchDistros")}
|
||||||
@@ -100,7 +100,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
onInput={(event) => controller.setCatalogSearch(event.currentTarget.value)}
|
onInput={(event) => controller.setCatalogSearch(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<div class="settings-v2-wsl-catalog-list">
|
<div class="settings-v2-wsl-catalog-list">
|
||||||
<RadioGroup
|
<RadioGroupV2
|
||||||
hideLabel
|
hideLabel
|
||||||
class="settings-v2-wsl-distro-group"
|
class="settings-v2-wsl-distro-group"
|
||||||
label={language.t("wsl.onboarding.installDistro")}
|
label={language.t("wsl.onboarding.installDistro")}
|
||||||
@@ -110,7 +110,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
>
|
>
|
||||||
<For each={model().filteredInstallableDistros}>
|
<For each={model().filteredInstallableDistros}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<RadioItem
|
<RadioItemV2
|
||||||
class="settings-v2-wsl-distro-row settings-v2-wsl-catalog-row"
|
class="settings-v2-wsl-distro-row settings-v2-wsl-catalog-row"
|
||||||
value={item.name}
|
value={item.name}
|
||||||
disabled={model().busy}
|
disabled={model().busy}
|
||||||
@@ -118,23 +118,23 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</RadioGroup>
|
</RadioGroupV2>
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="neutral" disabled={model().busy} onClick={controller.closeCatalog}>
|
<ButtonV2 variant="neutral" disabled={model().busy} onClick={controller.closeCatalog}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button
|
<ButtonV2
|
||||||
variant={model().installingCatalogDistro ? "loading" : "contrast"}
|
variant={model().installingCatalogDistro ? "loading" : "contrast"}
|
||||||
disabled={!model().installingCatalogDistro && (model().busy || !model().catalogTarget)}
|
disabled={!model().installingCatalogDistro && (model().busy || !model().catalogTarget)}
|
||||||
style={{ width: "99px" }}
|
style={{ width: "99px" }}
|
||||||
onClick={controller.installCatalogDistro}
|
onClick={controller.installCatalogDistro}
|
||||||
>
|
>
|
||||||
<Show when={model().installingCatalogDistro} fallback={language.t("wsl.onboarding.installDistro")}>
|
<Show when={model().installingCatalogDistro} fallback={language.t("wsl.onboarding.installDistro")}>
|
||||||
<Loader />
|
<LoaderV2 />
|
||||||
</Show>
|
</Show>
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -142,9 +142,14 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
<DialogBody class="settings-v2-wsl-dialog-body">
|
<DialogBody class="settings-v2-wsl-dialog-body">
|
||||||
<div class="settings-v2-wsl-section-header">
|
<div class="settings-v2-wsl-section-header">
|
||||||
<span class="settings-v2-wsl-section-title">{language.t("wsl.onboarding.installedDistros")}</span>
|
<span class="settings-v2-wsl-section-title">{language.t("wsl.onboarding.installedDistros")}</span>
|
||||||
<Button variant="ghost-muted" size="small" disabled={model().busy} onClick={controller.refreshDistros}>
|
<ButtonV2
|
||||||
|
variant="ghost-muted"
|
||||||
|
size="small"
|
||||||
|
disabled={model().busy}
|
||||||
|
onClick={controller.refreshDistros}
|
||||||
|
>
|
||||||
{language.t("wsl.onboarding.checkAgain")}
|
{language.t("wsl.onboarding.checkAgain")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
@@ -160,7 +165,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="settings-v2-wsl-distro-list">
|
<div class="settings-v2-wsl-distro-list">
|
||||||
<RadioGroup
|
<RadioGroupV2
|
||||||
hideLabel
|
hideLabel
|
||||||
class="settings-v2-wsl-distro-group"
|
class="settings-v2-wsl-distro-group"
|
||||||
label={language.t("wsl.onboarding.installedDistros")}
|
label={language.t("wsl.onboarding.installedDistros")}
|
||||||
@@ -172,7 +177,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
{(item) => {
|
{(item) => {
|
||||||
const status = () => model().distroStatuses[item.name] ?? null
|
const status = () => model().distroStatuses[item.name] ?? null
|
||||||
return (
|
return (
|
||||||
<RadioItem
|
<RadioItemV2
|
||||||
class={`settings-v2-wsl-distro-row${item.version === 1 ? " settings-v2-wsl-distro-row--unsupported" : ""}`}
|
class={`settings-v2-wsl-distro-row${item.version === 1 ? " settings-v2-wsl-distro-row--unsupported" : ""}`}
|
||||||
value={item.name}
|
value={item.name}
|
||||||
disabled={item.version === 1 || model().busy}
|
disabled={item.version === 1 || model().busy}
|
||||||
@@ -190,7 +195,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</RadioGroup>
|
</RadioGroupV2>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@@ -225,19 +230,19 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
</DialogBody>
|
</DialogBody>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="neutral" disabled={controller.adding()} onClick={controller.close}>
|
<ButtonV2 variant="neutral" disabled={controller.adding()} onClick={controller.close}>
|
||||||
{language.t("common.cancel")}
|
{language.t("common.cancel")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
<Button
|
<ButtonV2
|
||||||
variant={primaryButton().loading ? "loading" : primaryButton().variant}
|
variant={primaryButton().loading ? "loading" : primaryButton().variant}
|
||||||
disabled={!primaryButton().loading && primaryButton().disabled}
|
disabled={!primaryButton().loading && primaryButton().disabled}
|
||||||
style={primaryButtonStyle()}
|
style={primaryButtonStyle()}
|
||||||
onClick={controller.runPrimary}
|
onClick={controller.runPrimary}
|
||||||
>
|
>
|
||||||
<Show when={primaryButton().loading} fallback={translate(language, primaryButton().label)}>
|
<Show when={primaryButton().loading} fallback={translate(language, primaryButton().label)}>
|
||||||
<Loader />
|
<LoaderV2 />
|
||||||
</Show>
|
</Show>
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Show>
|
</Show>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -434,14 +439,14 @@ function DialogWslSetup(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.state === "unavailable" && props.installable}>
|
<Show when={props.state === "unavailable" && props.installable}>
|
||||||
<Button variant="neutral" disabled={props.busy} onClick={props.onInstall}>
|
<ButtonV2 variant="neutral" disabled={props.busy} onClick={props.onInstall}>
|
||||||
{language.t("wsl.onboarding.installWsl")}
|
{language.t("wsl.onboarding.installWsl")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.state !== "unavailable"}>
|
<Show when={props.state !== "unavailable"}>
|
||||||
<Button variant="neutral" onClick={() => dialog.close()}>
|
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
|
||||||
{language.t("common.close")}
|
{language.t("common.close")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Badge } from "@opencode-ai/ui/badge"
|
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Menu } from "@opencode-ai/ui/menu"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { type Accessor, For, Show, createMemo } from "solid-js"
|
import { type Accessor, For, Show, createMemo } from "solid-js"
|
||||||
@@ -32,22 +32,22 @@ export function AddServerMenu(props: { onAddServer: () => void }) {
|
|||||||
<Show
|
<Show
|
||||||
when={platform.wslServers}
|
when={platform.wslServers}
|
||||||
fallback={
|
fallback={
|
||||||
<Button variant="ghost-muted" icon="plus" onClick={props.onAddServer}>
|
<ButtonV2 variant="ghost-muted" icon="plus" onClick={props.onAddServer}>
|
||||||
{language.t("dialog.server.add.button")}
|
{language.t("dialog.server.add.button")}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Menu gutter={4} modal={false} placement="bottom-end">
|
<MenuV2 gutter={4} modal={false} placement="bottom-end">
|
||||||
<Menu.Trigger as={Button} variant="ghost-muted" icon="plus">
|
<MenuV2.Trigger as={ButtonV2} variant="ghost-muted" icon="plus">
|
||||||
{language.t("dialog.server.add.button")}
|
{language.t("dialog.server.add.button")}
|
||||||
</Menu.Trigger>
|
</MenuV2.Trigger>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content>
|
<MenuV2.Content>
|
||||||
<Menu.Item onSelect={props.onAddServer}>{language.t("dialog.server.add.button")}</Menu.Item>
|
<MenuV2.Item onSelect={props.onAddServer}>{language.t("dialog.server.add.button")}</MenuV2.Item>
|
||||||
<Menu.Item onSelect={openAddWsl}>{language.t("wsl.server.add")}</Menu.Item>
|
<MenuV2.Item onSelect={openAddWsl}>{language.t("wsl.server.add")}</MenuV2.Item>
|
||||||
</Menu.Content>
|
</MenuV2.Content>
|
||||||
</Menu.Portal>
|
</MenuV2.Portal>
|
||||||
</Menu>
|
</MenuV2>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -113,52 +113,54 @@ export function WslServerSettings(props: {
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-v2-servers-actions">
|
<div class="settings-v2-servers-actions">
|
||||||
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
|
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
|
||||||
<Badge>{language.t("dialog.server.status.default")}</Badge>
|
<Tag>{language.t("dialog.server.status.default")}</Tag>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={opencodeAction()}>
|
<Show when={opencodeAction()}>
|
||||||
{(label) => (
|
{(label) => (
|
||||||
<Button
|
<ButtonV2
|
||||||
size="small"
|
size="small"
|
||||||
disabled={busy() || request.isPending}
|
disabled={busy() || request.isPending}
|
||||||
onClick={() => api && request.mutate(() => api.installOpencode(item.config.distro))}
|
onClick={() => api && request.mutate(() => api.installOpencode(item.config.distro))}
|
||||||
>
|
>
|
||||||
{busy() ? language.t("wsl.server.updating") : language.t(label())}
|
{busy() ? language.t("wsl.server.updating") : language.t(label())}
|
||||||
</Button>
|
</ButtonV2>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Menu gutter={4} modal={false} placement="bottom-end">
|
<MenuV2 gutter={4} modal={false} placement="bottom-end">
|
||||||
<Menu.Trigger
|
<MenuV2.Trigger
|
||||||
as={IconButton}
|
as={IconButtonV2}
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<Icon name="outline-dots" />}
|
icon={<IconV2 name="outline-dots" />}
|
||||||
aria-label={language.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
<Menu.Portal>
|
<MenuV2.Portal>
|
||||||
<Menu.Content>
|
<MenuV2.Content>
|
||||||
<Menu.Group>
|
<MenuV2.Group>
|
||||||
<Menu.GroupLabel>{language.t("wsl.server.menu.label")}</Menu.GroupLabel>
|
<MenuV2.GroupLabel>{language.t("wsl.server.menu.label")}</MenuV2.GroupLabel>
|
||||||
<Show when={wslRuntimeRetryable(item.runtime)}>
|
<Show when={wslRuntimeRetryable(item.runtime)}>
|
||||||
<Menu.Item onSelect={() => api && request.mutate(() => api.startServer(key))}>
|
<MenuV2.Item onSelect={() => api && request.mutate(() => api.startServer(key))}>
|
||||||
{language.t("wsl.server.retryStart")}
|
{language.t("wsl.server.retryStart")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
|
<Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
|
||||||
<Menu.Item onSelect={() => props.domain.defaults.set(key)}>
|
<MenuV2.Item onSelect={() => props.domain.defaults.set(key)}>
|
||||||
{language.t("dialog.server.menu.default")}
|
{language.t("dialog.server.menu.default")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
|
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
|
||||||
<Menu.Item onSelect={() => props.domain.defaults.set(null)}>
|
<MenuV2.Item onSelect={() => props.domain.defaults.set(null)}>
|
||||||
{language.t("dialog.server.menu.defaultRemove")}
|
{language.t("dialog.server.menu.defaultRemove")}
|
||||||
</Menu.Item>
|
</MenuV2.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Menu.Separator />
|
<MenuV2.Separator />
|
||||||
<Menu.Item onSelect={() => remove(key)}>{language.t("dialog.server.menu.delete")}</Menu.Item>
|
<MenuV2.Item onSelect={() => remove(key)}>
|
||||||
</Menu.Group>
|
{language.t("dialog.server.menu.delete")}
|
||||||
</Menu.Content>
|
</MenuV2.Item>
|
||||||
</Menu.Portal>
|
</MenuV2.Group>
|
||||||
</Menu>
|
</MenuV2.Content>
|
||||||
|
</MenuV2.Portal>
|
||||||
|
</MenuV2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
import { beforeEach, describe, expect, test } from "bun:test"
|
import { beforeEach, describe, expect, test } from "bun:test"
|
||||||
import { createSignal, type JSX } from "solid-js"
|
import { createSignal, type JSX } from "solid-js"
|
||||||
import { showToast, toaster } from "@opencode-ai/ui/toast"
|
import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||||
|
|
||||||
describe("showToast", () => {
|
describe("showToastV2", () => {
|
||||||
// The toast registry is module state, so each test starts from an empty stack.
|
// The toast registry is module state, so each test starts from an empty stack.
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
toaster.dismiss()
|
toasterV2.dismiss()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("coalesces exact active content", () => {
|
test("coalesces exact active content", () => {
|
||||||
const first = showToast({ title: "Repeated error", description: "Try again" })
|
const first = showToastV2({ title: "Repeated error", description: "Try again" })
|
||||||
const second = showToast({ title: "Repeated error", description: "Try again" })
|
const second = showToastV2({ title: "Repeated error", description: "Try again" })
|
||||||
const different = showToast({ title: "Repeated error", description: "A different error" })
|
const different = showToastV2({ title: "Repeated error", description: "A different error" })
|
||||||
|
|
||||||
expect(second).toBe(first)
|
expect(second).toBe(first)
|
||||||
expect(different).not.toBe(first)
|
expect(different).not.toBe(first)
|
||||||
|
|
||||||
toaster.dismiss(first)
|
toasterV2.dismiss(first)
|
||||||
toaster.dismiss(different)
|
toasterV2.dismiss(different)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("allows dismissed content to appear again", () => {
|
test("allows dismissed content to appear again", () => {
|
||||||
const first = showToast("Dismiss and retry")
|
const first = showToastV2("Dismiss and retry")
|
||||||
toaster.dismiss(first)
|
toasterV2.dismiss(first)
|
||||||
|
|
||||||
const second = showToast("Dismiss and retry")
|
const second = showToastV2("Dismiss and retry")
|
||||||
expect(second).not.toBe(first)
|
expect(second).not.toBe(first)
|
||||||
|
|
||||||
toaster.dismiss(second)
|
toasterV2.dismiss(second)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("recreates matching content when it is not the topmost toast", () => {
|
test("recreates matching content when it is not the topmost toast", () => {
|
||||||
const first = showToast("First toast")
|
const first = showToastV2("First toast")
|
||||||
const topmost = showToast("Topmost toast")
|
const topmost = showToastV2("Topmost toast")
|
||||||
const repeated = showToast("First toast")
|
const repeated = showToastV2("First toast")
|
||||||
|
|
||||||
expect(repeated).not.toBe(first)
|
expect(repeated).not.toBe(first)
|
||||||
|
|
||||||
toaster.dismiss(topmost)
|
toasterV2.dismiss(topmost)
|
||||||
toaster.dismiss(repeated)
|
toasterV2.dismiss(repeated)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("creates no reactive computations at call time", () => {
|
test("creates no reactive computations at call time", () => {
|
||||||
@@ -50,7 +50,7 @@ describe("showToast", () => {
|
|||||||
return undefined
|
return undefined
|
||||||
}) as unknown as JSX.Element
|
}) as unknown as JSX.Element
|
||||||
|
|
||||||
const id = showToast({ description: "test", icon })
|
const id = showToastV2({ description: "test", icon })
|
||||||
|
|
||||||
// Resolving the icon at call time creates an ownerless computation that is
|
// Resolving the icon at call time creates an ownerless computation that is
|
||||||
// never disposed and tracks its dependencies forever; it must only resolve
|
// never disposed and tracks its dependencies forever; it must only resolve
|
||||||
@@ -59,6 +59,6 @@ describe("showToast", () => {
|
|||||||
setTick(1)
|
setTick(1)
|
||||||
expect(reads).toBe(0)
|
expect(reads).toBe(0)
|
||||||
|
|
||||||
toaster.dismiss(id)
|
toasterV2.dismiss(id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -785,7 +785,7 @@ const layer = Layer.effect(
|
|||||||
payload,
|
payload,
|
||||||
delivery: input.delivery ?? "steer",
|
delivery: input.delivery ?? "steer",
|
||||||
})
|
})
|
||||||
const recovered = yield* SessionInbox.serialized(
|
yield* SessionInbox.serialized(
|
||||||
input.sessionID,
|
input.sessionID,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const latest = yield* result.get(input.sessionID)
|
const latest = yield* result.get(input.sessionID)
|
||||||
@@ -796,25 +796,16 @@ const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
const moved = [SessionEvent.Moved, { sessionID: input.sessionID, ...payload }] as const
|
const moved = [SessionEvent.Moved, { sessionID: input.sessionID, ...payload }] as const
|
||||||
const first = cancellations[0]
|
const first = cancellations[0]
|
||||||
if (!first) {
|
if (!first) return yield* bus.publish(...moved).pipe(Effect.asVoid)
|
||||||
yield* bus.publish(...moved)
|
return yield* bus.publishAll([first, ...cancellations.slice(1), moved])
|
||||||
return true
|
|
||||||
}
|
|
||||||
yield* bus.publishAll([first, ...cancellations.slice(1), moved])
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
yield* SessionInbox.admit(db, bus, {
|
yield* SessionInbox.admit(db, bus, {
|
||||||
id: SessionMessage.ID.create(),
|
id: SessionMessage.ID.create(),
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
item,
|
item,
|
||||||
})
|
})
|
||||||
return false
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (recovered) {
|
|
||||||
yield* execution.wakeActive(input.sessionID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
yield* execution.wake(input.sessionID)
|
yield* execution.wake(input.sessionID)
|
||||||
}),
|
}),
|
||||||
compact: Effect.fn("Session.compact")(function* (input) {
|
compact: Effect.fn("Session.compact")(function* (input) {
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ export interface Interface {
|
|||||||
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, SessionRunner.RunError>
|
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, SessionRunner.RunError>
|
||||||
/** Registers newly recorded work. Repeated wakeups may coalesce. */
|
/** Registers newly recorded work. Repeated wakeups may coalesce. */
|
||||||
readonly wake: (sessionID: SessionSchema.ID) => Effect.Effect<void>
|
readonly wake: (sessionID: SessionSchema.ID) => Effect.Effect<void>
|
||||||
/** Wakes only an active execution, preserving its current input eligibility. */
|
|
||||||
readonly wakeActive: (sessionID: SessionSchema.ID) => Effect.Effect<void>
|
|
||||||
/** Interrupt active work owned by this process. Idle interruption is a no-op. */
|
/** Interrupt active work owned by this process. Idle interruption is a no-op. */
|
||||||
readonly interrupt: (sessionID: SessionSchema.ID, options?: { readonly continue?: boolean }) => Effect.Effect<void>
|
readonly interrupt: (sessionID: SessionSchema.ID, options?: { readonly continue?: boolean }) => Effect.Effect<void>
|
||||||
/** Resolves once this process owns no active execution for the Session. Returns immediately when idle and never starts work. */
|
/** Resolves once this process owns no active execution for the Session. Returns immediately when idle and never starts work. */
|
||||||
@@ -137,16 +135,15 @@ export const layer = Layer.effect(
|
|||||||
return Service.of({
|
return Service.of({
|
||||||
active: coordinator.active,
|
active: coordinator.active,
|
||||||
interrupt: (sessionID, options) =>
|
interrupt: (sessionID, options) =>
|
||||||
coordinator.interrupt(
|
Effect.gen(function* () {
|
||||||
sessionID,
|
yield* coordinator.interrupt(sessionID, "user")
|
||||||
"user",
|
if (!options?.continue) return
|
||||||
options?.continue
|
// Resume only steering input from the interrupted intent. Queued next-turn work
|
||||||
? { continue: { request: "steer", when: SessionInbox.has(db, sessionID, "steer") } }
|
// stays parked: a steer-scoped drain never promotes queue-delivery rows.
|
||||||
: undefined,
|
if (yield* SessionInbox.has(db, sessionID, "steer")) yield* coordinator.wake(sessionID, "steer")
|
||||||
),
|
}),
|
||||||
resume: coordinator.run,
|
resume: coordinator.run,
|
||||||
wake: coordinator.wake,
|
wake: coordinator.wake,
|
||||||
wakeActive: coordinator.wakeActive,
|
|
||||||
awaitIdle: coordinator.awaitIdle,
|
awaitIdle: coordinator.awaitIdle,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -165,7 +162,6 @@ export const noopLayer = Layer.succeed(
|
|||||||
active: Effect.succeed(new Set()),
|
active: Effect.succeed(new Set()),
|
||||||
resume: () => Effect.void,
|
resume: () => Effect.void,
|
||||||
wake: () => Effect.void,
|
wake: () => Effect.void,
|
||||||
wakeActive: () => Effect.void,
|
|
||||||
interrupt: () => Effect.void,
|
interrupt: () => Effect.void,
|
||||||
awaitIdle: () => Effect.void,
|
awaitIdle: () => Effect.void,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,25 +10,17 @@ export interface Coordinator<Key, E, Reason = never> {
|
|||||||
/** Starts an execution while idle, or joins the active execution and returns its exit. */
|
/** Starts an execution while idle, or joins the active execution and returns its exit. */
|
||||||
readonly run: (key: Key) => Effect.Effect<void, E>
|
readonly run: (key: Key) => Effect.Effect<void, E>
|
||||||
/** Rings the doorbell: an idle key starts an execution; an active one drains again before settling. */
|
/** Rings the doorbell: an idle key starts an execution; an active one drains again before settling. */
|
||||||
readonly wake: (key: Key, request?: Request) => Effect.Effect<void>
|
readonly wake: (key: Key, scope?: Promotable) => Effect.Effect<void>
|
||||||
/** Rings the current execution's doorbell with its existing request. Idle keys remain idle. */
|
|
||||||
readonly wakeActive: (key: Key) => Effect.Effect<void>
|
|
||||||
/** Stops the active execution, clears its doorbell, and waits for cleanup. No-op when idle. */
|
/** Stops the active execution, clears its doorbell, and waits for cleanup. No-op when idle. */
|
||||||
readonly interrupt: (
|
readonly interrupt: (key: Key, reason?: Reason) => Effect.Effect<void>
|
||||||
key: Key,
|
|
||||||
reason?: Reason,
|
|
||||||
options?: { readonly continue?: { readonly request: Request; readonly when: Effect.Effect<boolean> } },
|
|
||||||
) => Effect.Effect<void>
|
|
||||||
/** Resolves once no execution is active for the key. Returns immediately when already idle and never starts work. */
|
/** Resolves once no execution is active for the key. Returns immediately when already idle and never starts work. */
|
||||||
readonly awaitIdle: (key: Key) => Effect.Effect<void>
|
readonly awaitIdle: (key: Key) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Request = Promotable
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One execution is a busy period for one key: one fiber that drains from the first wake
|
* One execution is a busy period for one key: one fiber that drains from the first wake
|
||||||
* until the key would stay idle. `pendingWake` is the doorbell: work recorded during the
|
* until the key would stay idle. `pendingWake` is the doorbell: work recorded during the
|
||||||
* execution rings it with its eligibility request, and the execution loop drains again
|
* execution rings it with the scope that work needs, and the execution loop drains again
|
||||||
* instead of ending. The doorbell closes the gap between a drain's last eligibility check
|
* instead of ending. The doorbell closes the gap between a drain's last eligibility check
|
||||||
* and the idle transition, since those cannot be one atomic step. `done` resolves joiners
|
* and the idle transition, since those cannot be one atomic step. `done` resolves joiners
|
||||||
* with this execution's exit.
|
* with this execution's exit.
|
||||||
@@ -36,15 +28,10 @@ export type Request = Promotable
|
|||||||
type Execution<E, Reason> = {
|
type Execution<E, Reason> = {
|
||||||
readonly done: Deferred.Deferred<void, E>
|
readonly done: Deferred.Deferred<void, E>
|
||||||
owner?: Fiber.Fiber<void>
|
owner?: Fiber.Fiber<void>
|
||||||
request: Request
|
scope: Promotable
|
||||||
pendingWake?: Request
|
pendingWake?: Promotable
|
||||||
stopping: boolean
|
stopping: boolean
|
||||||
interruptionReason?: Reason
|
interruptionReason?: Reason
|
||||||
continuation?: {
|
|
||||||
readonly request: Request
|
|
||||||
readonly when: Effect.Effect<boolean>
|
|
||||||
signaled: boolean
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,7 +46,7 @@ type Execution<E, Reason> = {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const make = <Key, E, Reason = never>(options: {
|
export const make = <Key, E, Reason = never>(options: {
|
||||||
readonly drain: (key: Key, force: boolean, request: Request) => Effect.Effect<void, E>
|
readonly drain: (key: Key, force: boolean, scope: Promotable) => Effect.Effect<void, E>
|
||||||
/** Runs once when a process-local busy period begins, before its first drain. */
|
/** Runs once when a process-local busy period begins, before its first drain. */
|
||||||
readonly started?: (key: Key) => Effect.Effect<void>
|
readonly started?: (key: Key) => Effect.Effect<void>
|
||||||
/**
|
/**
|
||||||
@@ -73,11 +60,11 @@ export const make = <Key, E, Reason = never>(options: {
|
|||||||
const fork = yield* FiberSet.makeRuntime<never, void, never>()
|
const fork = yield* FiberSet.makeRuntime<never, void, never>()
|
||||||
|
|
||||||
const loop = (key: Key, execution: Execution<E, Reason>, force: boolean): Effect.Effect<void, E> =>
|
const loop = (key: Key, execution: Execution<E, Reason>, force: boolean): Effect.Effect<void, E> =>
|
||||||
Effect.suspend(() => options.drain(key, force, execution.request)).pipe(
|
Effect.suspend(() => options.drain(key, force, execution.scope)).pipe(
|
||||||
Effect.flatMap(() =>
|
Effect.flatMap(() =>
|
||||||
Effect.suspend(() => {
|
Effect.suspend(() => {
|
||||||
if (execution.stopping || execution.pendingWake === undefined) return Effect.void
|
if (execution.stopping || execution.pendingWake === undefined) return Effect.void
|
||||||
execution.request = execution.pendingWake
|
execution.scope = execution.pendingWake
|
||||||
execution.pendingWake = undefined
|
execution.pendingWake = undefined
|
||||||
// Trampoline so drains that complete synchronously cannot grow the stack.
|
// Trampoline so drains that complete synchronously cannot grow the stack.
|
||||||
return Effect.yieldNow.pipe(Effect.andThen(loop(key, execution, false)))
|
return Effect.yieldNow.pipe(Effect.andThen(loop(key, execution, false)))
|
||||||
@@ -85,10 +72,10 @@ export const make = <Key, E, Reason = never>(options: {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const start = (key: Key, force: boolean, request: Request) => {
|
const start = (key: Key, force: boolean, scope: Promotable) => {
|
||||||
const execution: Execution<E, Reason> = {
|
const execution: Execution<E, Reason> = {
|
||||||
done: Deferred.makeUnsafe<void, E>(),
|
done: Deferred.makeUnsafe<void, E>(),
|
||||||
request,
|
scope,
|
||||||
stopping: false,
|
stopping: false,
|
||||||
}
|
}
|
||||||
executions.set(key, execution)
|
executions.set(key, execution)
|
||||||
@@ -104,7 +91,7 @@ export const make = <Key, E, Reason = never>(options: {
|
|||||||
execution.owner = undefined
|
execution.owner = undefined
|
||||||
}).pipe(Effect.andThen(options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void)),
|
}).pipe(Effect.andThen(options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void)),
|
||||||
),
|
),
|
||||||
Effect.onExit((exit) => finish(key, execution, exit)),
|
Effect.onExit((exit) => Effect.sync(() => settle(key, execution, exit))),
|
||||||
Effect.exit,
|
Effect.exit,
|
||||||
Effect.asVoid,
|
Effect.asVoid,
|
||||||
),
|
),
|
||||||
@@ -114,22 +101,12 @@ export const make = <Key, E, Reason = never>(options: {
|
|||||||
|
|
||||||
// A doorbell that survives the execution loop (rung after the loop decided to end, or
|
// A doorbell that survives the execution loop (rung after the loop decided to end, or
|
||||||
// during failure or interruption cleanup) starts a fresh execution for the remaining work.
|
// during failure or interruption cleanup) starts a fresh execution for the remaining work.
|
||||||
const settle = (key: Key, execution: Execution<E, Reason>, exit: Exit.Exit<void, E>, resume: boolean) => {
|
const settle = (key: Key, execution: Execution<E, Reason>, exit: Exit.Exit<void, E>) => {
|
||||||
if (resume && execution.continuation) start(key, false, execution.continuation.request)
|
if (execution.pendingWake) start(key, false, execution.pendingWake)
|
||||||
else if (execution.pendingWake) start(key, false, execution.pendingWake)
|
|
||||||
else executions.delete(key)
|
else executions.delete(key)
|
||||||
Deferred.doneUnsafe(execution.done, exit)
|
Deferred.doneUnsafe(execution.done, exit)
|
||||||
}
|
}
|
||||||
|
|
||||||
const finish = (key: Key, execution: Execution<E, Reason>, exit: Exit.Exit<void, E>) => {
|
|
||||||
if (!execution.continuation) return Effect.sync(() => settle(key, execution, exit, false))
|
|
||||||
return execution.continuation.when.pipe(
|
|
||||||
Effect.flatMap((ready) =>
|
|
||||||
Effect.sync(() => settle(key, execution, exit, ready || execution.continuation?.signaled === true)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const run = (key: Key): Effect.Effect<void, E> =>
|
const run = (key: Key): Effect.Effect<void, E> =>
|
||||||
Effect.suspend(() => {
|
Effect.suspend(() => {
|
||||||
const execution = executions.get(key)
|
const execution = executions.get(key)
|
||||||
@@ -141,55 +118,26 @@ export const make = <Key, E, Reason = never>(options: {
|
|||||||
return Deferred.await(start(key, true, "input").done)
|
return Deferred.await(start(key, true, "input").done)
|
||||||
})
|
})
|
||||||
|
|
||||||
const wake = (key: Key, request: Request = "input") =>
|
const wake = (key: Key, scope: Promotable = "input") =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const execution = executions.get(key)
|
const execution = executions.get(key)
|
||||||
if (execution !== undefined) {
|
if (execution !== undefined) {
|
||||||
if (execution.stopping) {
|
// Coalesced wakes keep the widest scope: "input" subsumes "steer".
|
||||||
if (execution.continuation) execution.continuation.signaled = true
|
execution.pendingWake = execution.pendingWake === "input" ? "input" : scope
|
||||||
else execution.continuation = { request, when: Effect.succeed(true), signaled: true }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Coalesced wakes keep the widest request: "input" subsumes "steer".
|
|
||||||
execution.pendingWake = execution.pendingWake === "input" ? "input" : request
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
start(key, false, request)
|
start(key, false, scope)
|
||||||
})
|
})
|
||||||
|
|
||||||
const wakeActive = (key: Key) =>
|
const interrupt = (key: Key, reason?: Reason): Effect.Effect<void> =>
|
||||||
Effect.suspend(() => {
|
Effect.suspend(() => {
|
||||||
const execution = executions.get(key)
|
const execution = executions.get(key)
|
||||||
return execution ? wake(key, execution.request) : Effect.void
|
if (execution?.owner === undefined || execution.stopping) return Effect.void
|
||||||
})
|
|
||||||
|
|
||||||
const interrupt = (
|
|
||||||
key: Key,
|
|
||||||
reason?: Reason,
|
|
||||||
options?: { readonly continue?: { readonly request: Request; readonly when: Effect.Effect<boolean> } },
|
|
||||||
): Effect.Effect<void> =>
|
|
||||||
Effect.suspend(() => {
|
|
||||||
const execution = executions.get(key)
|
|
||||||
if (execution === undefined) return Effect.void
|
|
||||||
if (execution.stopping) {
|
|
||||||
if (options?.continue)
|
|
||||||
execution.continuation = {
|
|
||||||
...options.continue,
|
|
||||||
signaled: execution.continuation?.signaled ?? false,
|
|
||||||
}
|
|
||||||
return Deferred.await(execution.done).pipe(Effect.exit, Effect.asVoid)
|
|
||||||
}
|
|
||||||
if (execution.owner === undefined) {
|
|
||||||
if (!options?.continue) return Effect.void
|
|
||||||
execution.stopping = true
|
|
||||||
execution.pendingWake = undefined
|
|
||||||
execution.continuation = { ...options.continue, signaled: false }
|
|
||||||
return Deferred.await(execution.done).pipe(Effect.exit, Effect.asVoid)
|
|
||||||
}
|
|
||||||
execution.stopping = true
|
execution.stopping = true
|
||||||
|
// Wakes recorded so far belong to the interrupted intent; the interrupt claims them.
|
||||||
|
// Wakes arriving during cleanup are new admissions and restart normally at settle.
|
||||||
execution.pendingWake = undefined
|
execution.pendingWake = undefined
|
||||||
execution.interruptionReason = reason
|
execution.interruptionReason = reason
|
||||||
if (options?.continue) execution.continuation = { ...options.continue, signaled: false }
|
|
||||||
return Fiber.interrupt(execution.owner)
|
return Fiber.interrupt(execution.owner)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -202,5 +150,5 @@ export const make = <Key, E, Reason = never>(options: {
|
|||||||
return Deferred.await(execution.done).pipe(Effect.exit, Effect.andThen(awaitIdle(key)))
|
return Deferred.await(execution.done).pipe(Effect.exit, Effect.andThen(awaitIdle(key)))
|
||||||
})
|
})
|
||||||
|
|
||||||
return { active: Effect.sync(() => new Set(executions.keys())), run, wake, wakeActive, interrupt, awaitIdle }
|
return { active: Effect.sync(() => new Set(executions.keys())), run, wake, interrupt, awaitIdle }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
|||||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||||
import { UserInterruptedError } from "@opencode-ai/core/session/error"
|
import { UserInterruptedError } from "@opencode-ai/core/session/error"
|
||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
|
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||||
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { SessionRunner } from "@opencode-ai/core/session/runner/index"
|
import { SessionRunner } from "@opencode-ai/core/session/runner/index"
|
||||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionInboxTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
import { Context, Deferred, Effect, Exit, Fiber, Layer, LayerMap, Scope } from "effect"
|
import { Context, Deferred, Effect, Exit, Fiber, Layer, LayerMap, Scope } from "effect"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
@@ -290,6 +292,113 @@ describe("SessionExecution lifecycle", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("SessionExecution interrupt continuation", () => {
|
||||||
|
it.effect("resumes only steering input after an interrupt with continue", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const database = yield* Database.Service
|
||||||
|
const sessionID = Session.ID.make("ses_continue_steer")
|
||||||
|
yield* seedSessions(database, [sessionID])
|
||||||
|
yield* seedInbox(database, sessionID, ["steer", "queue"])
|
||||||
|
|
||||||
|
const draining = yield* Deferred.make<void>()
|
||||||
|
const drains: Array<{ force: boolean; promotable?: SessionInbox.Promotable }> = []
|
||||||
|
const scope = yield* Scope.make()
|
||||||
|
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void))
|
||||||
|
const context = yield* buildExecution(scope, (input) =>
|
||||||
|
Effect.suspend(() => {
|
||||||
|
drains.push({ force: input.force, promotable: input.promotable })
|
||||||
|
if (drains.length > 1) return Effect.void
|
||||||
|
return Deferred.succeed(draining, undefined).pipe(Effect.andThen(Effect.never))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const execution = Context.get(context, SessionExecution.Service)
|
||||||
|
yield* execution.resume(sessionID).pipe(Effect.forkScoped)
|
||||||
|
yield* Deferred.await(draining)
|
||||||
|
|
||||||
|
yield* execution.interrupt(sessionID, { continue: true })
|
||||||
|
yield* execution.awaitIdle(sessionID)
|
||||||
|
|
||||||
|
// The successor drain is steer-scoped: queued next-turn work stays parked.
|
||||||
|
expect(drains).toEqual([
|
||||||
|
{ force: true, promotable: "input" },
|
||||||
|
{ force: false, promotable: "steer" },
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("stays parked after an interrupt with continue when only queued work remains", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const database = yield* Database.Service
|
||||||
|
const sessionID = Session.ID.make("ses_continue_parked")
|
||||||
|
yield* seedSessions(database, [sessionID])
|
||||||
|
yield* seedInbox(database, sessionID, ["queue"])
|
||||||
|
|
||||||
|
const draining = yield* Deferred.make<void>()
|
||||||
|
const drains: Array<SessionInbox.Promotable | undefined> = []
|
||||||
|
const scope = yield* Scope.make()
|
||||||
|
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void))
|
||||||
|
const context = yield* buildExecution(scope, (input) =>
|
||||||
|
Effect.suspend(() => {
|
||||||
|
drains.push(input.promotable)
|
||||||
|
return Deferred.succeed(draining, undefined).pipe(Effect.andThen(Effect.never))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const execution = Context.get(context, SessionExecution.Service)
|
||||||
|
yield* execution.resume(sessionID).pipe(Effect.forkScoped)
|
||||||
|
yield* Deferred.await(draining)
|
||||||
|
|
||||||
|
yield* execution.interrupt(sessionID, { continue: true })
|
||||||
|
yield* execution.awaitIdle(sessionID)
|
||||||
|
|
||||||
|
expect(drains).toEqual(["input"])
|
||||||
|
expect(yield* execution.active).toEqual(new Set())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("an idle interrupt with continue resumes pending steers", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const database = yield* Database.Service
|
||||||
|
const sessionID = Session.ID.make("ses_continue_idle")
|
||||||
|
yield* seedSessions(database, [sessionID])
|
||||||
|
yield* seedInbox(database, sessionID, ["steer"])
|
||||||
|
|
||||||
|
const drains: Array<{ force: boolean; promotable?: SessionInbox.Promotable }> = []
|
||||||
|
const scope = yield* Scope.make()
|
||||||
|
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void))
|
||||||
|
const context = yield* buildExecution(scope, (input) =>
|
||||||
|
Effect.sync(() => void drains.push({ force: input.force, promotable: input.promotable })),
|
||||||
|
)
|
||||||
|
const execution = Context.get(context, SessionExecution.Service)
|
||||||
|
|
||||||
|
yield* execution.interrupt(sessionID, { continue: true })
|
||||||
|
yield* execution.awaitIdle(sessionID)
|
||||||
|
|
||||||
|
expect(drains).toEqual([{ force: false, promotable: "steer" }])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function seedInbox(
|
||||||
|
database: Database.Service["Service"],
|
||||||
|
sessionID: Session.ID,
|
||||||
|
deliveries: ReadonlyArray<SessionInbox.Delivery>,
|
||||||
|
) {
|
||||||
|
return database.db
|
||||||
|
.insert(SessionInboxTable)
|
||||||
|
.values(
|
||||||
|
deliveries.map((delivery, index) => ({
|
||||||
|
id: SessionMessage.ID.create(),
|
||||||
|
session_id: sessionID,
|
||||||
|
type: "compaction" as const,
|
||||||
|
payload: {},
|
||||||
|
delivery,
|
||||||
|
enqueued_seq: index + 1,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
}
|
||||||
|
|
||||||
function seedSessions(
|
function seedSessions(
|
||||||
database: Database.Service["Service"],
|
database: Database.Service["Service"],
|
||||||
sessionIDs: ReadonlyArray<Session.ID>,
|
sessionIDs: ReadonlyArray<Session.ID>,
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ const execution = Layer.succeed(
|
|||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
wakeCalls.push(sessionID)
|
wakeCalls.push(sessionID)
|
||||||
}),
|
}),
|
||||||
wakeActive: () => Effect.void,
|
|
||||||
awaitIdle: () => Effect.void,
|
awaitIdle: () => Effect.void,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
|
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
|
||||||
|
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||||
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
@@ -269,31 +270,24 @@ describe("SessionRunCoordinator", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("replaces a settlement-window wake with a steer continuation", () =>
|
it.effect("a settlement-window wake starts a fresh execution with its own scope", () =>
|
||||||
Effect.scoped(
|
Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const settling = yield* Deferred.make<void>()
|
const settling = yield* Deferred.make<void>()
|
||||||
const release = yield* Deferred.make<void>()
|
const release = yield* Deferred.make<void>()
|
||||||
const requests: SessionRunCoordinator.Request[] = []
|
const scopes: SessionInbox.Promotable[] = []
|
||||||
const coordinator = yield* SessionRunCoordinator.make({
|
const coordinator = yield* SessionRunCoordinator.make({
|
||||||
drain: (_key, _force, request) => Effect.sync(() => requests.push(request)),
|
drain: (_key, _force, scope) => Effect.sync(() => scopes.push(scope)),
|
||||||
settled: () => Deferred.succeed(settling, undefined).pipe(Effect.andThen(Deferred.await(release))),
|
settled: () => Deferred.succeed(settling, undefined).pipe(Effect.andThen(Deferred.await(release))),
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* coordinator.wake("session", "input")
|
yield* coordinator.wake("session", "steer")
|
||||||
yield* Deferred.await(settling)
|
yield* Deferred.await(settling)
|
||||||
yield* coordinator.wake("session", "input")
|
yield* coordinator.wake("session", "input")
|
||||||
const interrupted = yield* coordinator
|
|
||||||
.interrupt("session", undefined, {
|
|
||||||
continue: { request: "steer", when: Effect.succeed(true) },
|
|
||||||
})
|
|
||||||
.pipe(Effect.forkChild)
|
|
||||||
yield* Effect.yieldNow
|
|
||||||
yield* Deferred.succeed(release, undefined)
|
yield* Deferred.succeed(release, undefined)
|
||||||
yield* Fiber.join(interrupted)
|
|
||||||
yield* coordinator.awaitIdle("session")
|
yield* coordinator.awaitIdle("session")
|
||||||
|
|
||||||
expect(requests).toEqual(["input", "steer"])
|
expect(scopes).toEqual(["steer", "input"])
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -371,17 +365,17 @@ describe("SessionRunCoordinator", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("coalesces drain requests with input taking precedence", () =>
|
it.effect("coalesces drain scopes with input taking precedence", () =>
|
||||||
Effect.scoped(
|
Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const firstStarted = yield* Deferred.make<void>()
|
const firstStarted = yield* Deferred.make<void>()
|
||||||
const release = yield* Deferred.make<void>()
|
const release = yield* Deferred.make<void>()
|
||||||
const requests: SessionRunCoordinator.Request[] = []
|
const scopes: SessionInbox.Promotable[] = []
|
||||||
const coordinator = yield* SessionRunCoordinator.make({
|
const coordinator = yield* SessionRunCoordinator.make({
|
||||||
drain: (_key, _force, request) =>
|
drain: (_key, _force, scope) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
requests.push(request)
|
scopes.push(scope)
|
||||||
if (requests.length !== 1) return
|
if (scopes.length !== 1) return
|
||||||
yield* Deferred.succeed(firstStarted, undefined)
|
yield* Deferred.succeed(firstStarted, undefined)
|
||||||
yield* Deferred.await(release)
|
yield* Deferred.await(release)
|
||||||
}),
|
}),
|
||||||
@@ -394,22 +388,22 @@ describe("SessionRunCoordinator", () => {
|
|||||||
yield* Deferred.succeed(release, undefined)
|
yield* Deferred.succeed(release, undefined)
|
||||||
yield* coordinator.awaitIdle("session")
|
yield* coordinator.awaitIdle("session")
|
||||||
|
|
||||||
expect(requests).toEqual(["steer", "input"])
|
expect(scopes).toEqual(["steer", "input"])
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("does not carry a completed input request into a steer drain", () =>
|
it.effect("does not carry a completed input scope into a steer drain", () =>
|
||||||
Effect.scoped(
|
Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const firstStarted = yield* Deferred.make<void>()
|
const firstStarted = yield* Deferred.make<void>()
|
||||||
const release = yield* Deferred.make<void>()
|
const release = yield* Deferred.make<void>()
|
||||||
const requests: SessionRunCoordinator.Request[] = []
|
const scopes: SessionInbox.Promotable[] = []
|
||||||
const coordinator = yield* SessionRunCoordinator.make({
|
const coordinator = yield* SessionRunCoordinator.make({
|
||||||
drain: (_key, _force, request) =>
|
drain: (_key, _force, scope) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
requests.push(request)
|
scopes.push(scope)
|
||||||
if (requests.length !== 1) return
|
if (scopes.length !== 1) return
|
||||||
yield* Deferred.succeed(firstStarted, undefined)
|
yield* Deferred.succeed(firstStarted, undefined)
|
||||||
yield* Deferred.await(release)
|
yield* Deferred.await(release)
|
||||||
}),
|
}),
|
||||||
@@ -421,89 +415,23 @@ describe("SessionRunCoordinator", () => {
|
|||||||
yield* Deferred.succeed(release, undefined)
|
yield* Deferred.succeed(release, undefined)
|
||||||
yield* coordinator.awaitIdle("session")
|
yield* coordinator.awaitIdle("session")
|
||||||
|
|
||||||
expect(requests).toEqual(["input", "steer"])
|
expect(scopes).toEqual(["input", "steer"])
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("an active wake inherits scope without starting idle work", () =>
|
it.effect("a cleanup-era wake starts a successor with its own scope", () =>
|
||||||
Effect.scoped(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const firstStarted = yield* Deferred.make<void>()
|
|
||||||
const release = yield* Deferred.make<void>()
|
|
||||||
const requests: SessionRunCoordinator.Request[] = []
|
|
||||||
const coordinator = yield* SessionRunCoordinator.make({
|
|
||||||
drain: (_key, _force, request) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
requests.push(request)
|
|
||||||
if (requests.length !== 1) return
|
|
||||||
yield* Deferred.succeed(firstStarted, undefined)
|
|
||||||
yield* Deferred.await(release)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
yield* coordinator.wakeActive("session")
|
|
||||||
yield* coordinator.wake("session", "steer")
|
|
||||||
yield* Deferred.await(firstStarted)
|
|
||||||
yield* coordinator.wakeActive("session")
|
|
||||||
yield* Deferred.succeed(release, undefined)
|
|
||||||
yield* coordinator.awaitIdle("session")
|
|
||||||
|
|
||||||
expect(requests).toEqual(["steer", "steer"])
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("coalesces overlapping interrupt continuations into one steer successor", () =>
|
|
||||||
Effect.scoped(
|
Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const firstStarted = yield* Deferred.make<void>()
|
const firstStarted = yield* Deferred.make<void>()
|
||||||
const cleanupStarted = yield* Deferred.make<void>()
|
const cleanupStarted = yield* Deferred.make<void>()
|
||||||
const cleanupGate = yield* Deferred.make<void>()
|
const cleanupGate = yield* Deferred.make<void>()
|
||||||
const requests: SessionRunCoordinator.Request[] = []
|
const scopes: SessionInbox.Promotable[] = []
|
||||||
const coordinator = yield* SessionRunCoordinator.make({
|
const coordinator = yield* SessionRunCoordinator.make({
|
||||||
drain: (_key, _force, request) =>
|
drain: (_key, _force, scope) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
requests.push(request)
|
scopes.push(scope)
|
||||||
if (requests.length !== 1) return
|
if (scopes.length !== 1) return
|
||||||
yield* Deferred.succeed(firstStarted, undefined)
|
|
||||||
yield* Effect.never.pipe(
|
|
||||||
Effect.onInterrupt(() =>
|
|
||||||
Deferred.succeed(cleanupStarted, undefined).pipe(Effect.andThen(Deferred.await(cleanupGate))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
const continuation = { continue: { request: "steer" as const, when: Effect.succeed(false) } }
|
|
||||||
|
|
||||||
yield* coordinator.wake("session")
|
|
||||||
yield* Deferred.await(firstStarted)
|
|
||||||
const first = yield* coordinator.interrupt("session", undefined, continuation).pipe(Effect.forkChild)
|
|
||||||
yield* Deferred.await(cleanupStarted)
|
|
||||||
const second = yield* coordinator.interrupt("session", undefined, continuation).pipe(Effect.forkChild)
|
|
||||||
yield* Effect.yieldNow
|
|
||||||
yield* coordinator.wake("session", "input")
|
|
||||||
yield* Deferred.succeed(cleanupGate, undefined)
|
|
||||||
yield* Effect.all([Fiber.join(first), Fiber.join(second)])
|
|
||||||
yield* coordinator.awaitIdle("session")
|
|
||||||
|
|
||||||
expect(requests).toEqual(["input", "steer"])
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("a continuing interrupt replaces a cleanup-era input wake", () =>
|
|
||||||
Effect.scoped(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const firstStarted = yield* Deferred.make<void>()
|
|
||||||
const cleanupStarted = yield* Deferred.make<void>()
|
|
||||||
const cleanupGate = yield* Deferred.make<void>()
|
|
||||||
const requests: SessionRunCoordinator.Request[] = []
|
|
||||||
const coordinator = yield* SessionRunCoordinator.make({
|
|
||||||
drain: (_key, _force, request) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
requests.push(request)
|
|
||||||
if (requests.length !== 1) return
|
|
||||||
yield* Deferred.succeed(firstStarted, undefined)
|
yield* Deferred.succeed(firstStarted, undefined)
|
||||||
yield* Effect.never.pipe(
|
yield* Effect.never.pipe(
|
||||||
Effect.onInterrupt(() =>
|
Effect.onInterrupt(() =>
|
||||||
@@ -515,45 +443,16 @@ describe("SessionRunCoordinator", () => {
|
|||||||
|
|
||||||
yield* coordinator.wake("session", "input")
|
yield* coordinator.wake("session", "input")
|
||||||
yield* Deferred.await(firstStarted)
|
yield* Deferred.await(firstStarted)
|
||||||
const plain = yield* coordinator.interrupt("session").pipe(Effect.forkChild)
|
const interrupt = yield* coordinator.interrupt("session").pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(cleanupStarted)
|
yield* Deferred.await(cleanupStarted)
|
||||||
|
// A new admission during cancellation restarts normally: interruption only
|
||||||
|
// claims the wakes recorded before it.
|
||||||
yield* coordinator.wake("session", "input")
|
yield* coordinator.wake("session", "input")
|
||||||
const continuing = yield* coordinator
|
|
||||||
.interrupt("session", undefined, {
|
|
||||||
continue: { request: "steer", when: Effect.succeed(false) },
|
|
||||||
})
|
|
||||||
.pipe(Effect.forkChild)
|
|
||||||
yield* Effect.yieldNow
|
|
||||||
yield* Deferred.succeed(cleanupGate, undefined)
|
yield* Deferred.succeed(cleanupGate, undefined)
|
||||||
yield* Effect.all([Fiber.join(plain), Fiber.join(continuing)])
|
yield* Fiber.join(interrupt)
|
||||||
yield* coordinator.awaitIdle("session")
|
yield* coordinator.awaitIdle("session")
|
||||||
|
|
||||||
expect(requests).toEqual(["input", "steer"])
|
expect(scopes).toEqual(["input", "input"])
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("does not start a conditional continuation without eligible work", () =>
|
|
||||||
Effect.scoped(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const started = yield* Deferred.make<void>()
|
|
||||||
const requests: SessionRunCoordinator.Request[] = []
|
|
||||||
const coordinator = yield* SessionRunCoordinator.make({
|
|
||||||
drain: (_key, _force, request) =>
|
|
||||||
Effect.sync(() => requests.push(request)).pipe(
|
|
||||||
Effect.andThen(Deferred.succeed(started, undefined)),
|
|
||||||
Effect.andThen(Effect.never),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
yield* coordinator.wake("session")
|
|
||||||
yield* Deferred.await(started)
|
|
||||||
yield* coordinator.interrupt("session", undefined, {
|
|
||||||
continue: { request: "steer", when: Effect.succeed(false) },
|
|
||||||
})
|
|
||||||
yield* coordinator.awaitIdle("session")
|
|
||||||
|
|
||||||
expect(requests).toEqual(["input"])
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ const execution = (llmClient: Layer.Layer<typeof LLMClient.Service>) =>
|
|||||||
active: coordinator.active,
|
active: coordinator.active,
|
||||||
resume: coordinator.run,
|
resume: coordinator.run,
|
||||||
wake: coordinator.wake,
|
wake: coordinator.wake,
|
||||||
wakeActive: coordinator.wakeActive,
|
|
||||||
interrupt: (sessionID) => coordinator.interrupt(sessionID),
|
interrupt: (sessionID) => coordinator.interrupt(sessionID),
|
||||||
awaitIdle: coordinator.awaitIdle,
|
awaitIdle: coordinator.awaitIdle,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -436,7 +436,6 @@ const execution = Layer.effect(
|
|||||||
active: coordinator.active,
|
active: coordinator.active,
|
||||||
resume: coordinator.run,
|
resume: coordinator.run,
|
||||||
wake: coordinator.wake,
|
wake: coordinator.wake,
|
||||||
wakeActive: coordinator.wakeActive,
|
|
||||||
interrupt: (sessionID) => coordinator.interrupt(sessionID),
|
interrupt: (sessionID) => coordinator.interrupt(sessionID),
|
||||||
awaitIdle: coordinator.awaitIdle,
|
awaitIdle: coordinator.awaitIdle,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ const executionNode = makeGlobalNode({
|
|||||||
active: Effect.succeed(new Set()),
|
active: Effect.succeed(new Set()),
|
||||||
resume: complete,
|
resume: complete,
|
||||||
wake: () => Effect.void,
|
wake: () => Effect.void,
|
||||||
wakeActive: () => Effect.void,
|
|
||||||
interrupt: () => Effect.void,
|
interrupt: () => Effect.void,
|
||||||
awaitIdle: (id) => complete(id).pipe(Effect.exit, Effect.asVoid),
|
awaitIdle: (id) => complete(id).pipe(Effect.exit, Effect.asVoid),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ const executionNode = makeGlobalNode({
|
|||||||
active: Effect.succeed(new Set()),
|
active: Effect.succeed(new Set()),
|
||||||
resume: complete,
|
resume: complete,
|
||||||
wake: () => Effect.void,
|
wake: () => Effect.void,
|
||||||
wakeActive: () => Effect.void,
|
|
||||||
interrupt: () => Effect.void,
|
interrupt: () => Effect.void,
|
||||||
awaitIdle: (sessionID) => complete(sessionID).pipe(Effect.exit, Effect.asVoid),
|
awaitIdle: (sessionID) => complete(sessionID).pipe(Effect.exit, Effect.asVoid),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { OpenCode, type MigrationV1StatusOutput } from "@opencode-ai/client/promise"
|
import { OpenCode, type MigrationV1StatusOutput } from "@opencode-ai/client/promise"
|
||||||
import { useLanguage } from "@opencode-ai/app"
|
import { useLanguage } from "@opencode-ai/app"
|
||||||
import { Loader } from "@opencode-ai/ui/loader"
|
import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2"
|
||||||
import { showToast, toaster, Toast } from "@opencode-ai/ui/toast"
|
import { showToastV2, toasterV2, ToastV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||||
import { createRoot, createSignal, onCleanup, onMount } from "solid-js"
|
import { createRoot, createSignal, onCleanup, onMount } from "solid-js"
|
||||||
import type { ServerReadyData } from "../shared/ipc-contract"
|
import type { ServerReadyData } from "../shared/ipc-contract"
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export function MigrationStatus(props: { server: ServerReadyData }) {
|
|||||||
let disposeToast: (() => void) | undefined
|
let disposeToast: (() => void) | undefined
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
if (toastID !== undefined) toaster.dismiss(toastID)
|
if (toastID !== undefined) toasterV2.dismiss(toastID)
|
||||||
toastID = undefined
|
toastID = undefined
|
||||||
disposeToast?.()
|
disposeToast?.()
|
||||||
disposeToast = undefined
|
disposeToast = undefined
|
||||||
@@ -23,22 +23,22 @@ export function MigrationStatus(props: { server: ServerReadyData }) {
|
|||||||
|
|
||||||
const show = () => {
|
const show = () => {
|
||||||
if (toastID !== undefined) return
|
if (toastID !== undefined) return
|
||||||
toastID = toaster.show(
|
toastID = toasterV2.show(
|
||||||
({ toastId }) =>
|
({ toastId }) =>
|
||||||
createRoot((dispose) => {
|
createRoot((dispose) => {
|
||||||
disposeToast?.()
|
disposeToast?.()
|
||||||
disposeToast = dispose
|
disposeToast = dispose
|
||||||
return (
|
return (
|
||||||
<Toast toastId={toastId}>
|
<ToastV2 toastId={toastId}>
|
||||||
<div data-slot="toast-v2-header" class="col-span-full">
|
<div data-slot="toast-v2-header" class="col-span-full">
|
||||||
<Toast.Icon>
|
<ToastV2.Icon>
|
||||||
<Loader />
|
<LoaderV2 />
|
||||||
</Toast.Icon>
|
</ToastV2.Icon>
|
||||||
<Toast.Content>
|
<ToastV2.Content>
|
||||||
<Toast.Title dir="auto">{format(progress())}</Toast.Title>
|
<ToastV2.Title dir="auto">{format(progress())}</ToastV2.Title>
|
||||||
</Toast.Content>
|
</ToastV2.Content>
|
||||||
</div>
|
</div>
|
||||||
</Toast>
|
</ToastV2>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
{ persistent: true },
|
{ persistent: true },
|
||||||
@@ -69,7 +69,7 @@ export function MigrationStatus(props: { server: ServerReadyData }) {
|
|||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
if (abort.signal.aborted) return
|
if (abort.signal.aborted) return
|
||||||
hide()
|
hide()
|
||||||
showToast({
|
showToastV2({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: language.t("toast.migration.failed.title"),
|
title: language.t("toast.migration.failed.title"),
|
||||||
description: error instanceof Error ? error.message : String(error),
|
description: error instanceof Error ? error.message : String(error),
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
@import "@opencode-ai/ui/styles/tailwind";
|
@import "@opencode-ai/ui/styles/tailwind";
|
||||||
@import "@opencode-ai/session-ui/styles";
|
@import "@opencode-ai/session-ui/styles";
|
||||||
@import "@opencode-ai/ui/styles/tokens";
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { createMemo, createSignal, ErrorBoundary, For, Match, Show, Switch } fro
|
|||||||
import { Share } from "~/core/share"
|
import { Share } from "~/core/share"
|
||||||
import { Logo, Mark } from "@opencode-ai/ui/logo"
|
import { Logo, Mark } from "@opencode-ai/ui/logo"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { iife } from "@opencode-ai/core/util/iife"
|
import { iife } from "@opencode-ai/core/util/iife"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
@@ -284,14 +283,14 @@ export default function () {
|
|||||||
as={"a"}
|
as={"a"}
|
||||||
href="https://github.com/anomalyco/opencode"
|
href="https://github.com/anomalyco/opencode"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
icon={<Icon name="github" />}
|
icon="github"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
as={"a"}
|
as={"a"}
|
||||||
href="https://opencode.ai/discord"
|
href="https://opencode.ai/discord"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
icon={<Icon name="discord" />}
|
icon="discord"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -279,7 +279,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
[data-slot="icon-svg"] {
|
[data-component="icon"] {
|
||||||
color: var(--task-agent-color, var(--v2-icon-icon-base));
|
color: var(--task-agent-color, var(--v2-icon-icon-base));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const lineCommentStyles = `
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="line-comment"] [data-slot="icon-svg"] {
|
[data-component="line-comment"] [data-component="icon"] {
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
|||||||
<Button size="small" variant="ghost" onClick={split.onCancel}>
|
<Button size="small" variant="ghost" onClick={split.onCancel}>
|
||||||
{split.cancelLabel ?? i18n.t("ui.common.cancel")}
|
{split.cancelLabel ?? i18n.t("ui.common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" variant="contrast" disabled={split.value.trim().length === 0} onClick={submit}>
|
<Button size="small" variant="primary" disabled={split.value.trim().length === 0} onClick={submit}>
|
||||||
{split.submitLabel ?? i18n.t("ui.lineComment.submit")}
|
{split.submitLabel ?? i18n.t("ui.lineComment.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user