Compare commits

..
1 Commits
Author SHA1 Message Date
Shoubhit Dash 177370dae0 fix(tui): paste into custom form answers 2026-08-18 13:13:51 +00:00
446 changed files with 11440 additions and 6581 deletions
-6
View File
@@ -1,6 +0,0 @@
---
"@opencode-ai/plugin": patch
"@opencode-ai/core": patch
---
Add transport-neutral Session model request hooks and provider-scoped hook registration so eligible OpenAI Responses requests can prefer WebSocket without bypassing HTTP-only middleware.
-1
View File
@@ -82,7 +82,6 @@ jobs:
build-cli:
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
+1 -1
View File
@@ -193,7 +193,7 @@ If you find yourself copying a 3-to-5-line snippet between two protocols, lift i
`LLMRequest.system` is the initial privileged prompt that applies ahead of the conversation. `Message.system(...)` is a separate, provider-neutral chronological operator update inside `LLMRequest.messages`; it applies only from its position in history onward and accepts text content only.
Native chronological system messages are route/model-specific. Open Responses lowers them to standard `developer` messages, while Anthropic Messages lowers them to native system messages for Claude Opus 4.8 (`claude-opus-4-8`). Other routes and models intentionally lower the update in place into ordinary user-compatible text using this stable escaped representation:
Native chronological system messages are route/model-specific. Anthropic Messages lowers them natively for Claude Opus 4.8 (`claude-opus-4-8`). Other routes and models intentionally lower the update in place into ordinary user-compatible text using this stable escaped representation:
```text
<system-update>
@@ -905,7 +905,6 @@ const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(f
if (delta?.type === "input_json_delta" && event.index !== undefined) {
if (!delta.partial_json) return [state, NO_EVENTS] satisfies StepResult
if (!state.tools[event.index]) return [state, NO_EVENTS] satisfies StepResult
const result = ToolStream.appendExisting(
ADAPTER,
state.tools,
+8 -7
View File
@@ -90,7 +90,6 @@ const OpenResponsesFunctionCallOutput = Schema.Union([
export const InputItem = Schema.Union([
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
Schema.Struct({ role: Schema.tag("developer"), content: Schema.String }),
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenResponsesInputContent) }),
Schema.Struct({
role: Schema.tag("assistant"),
@@ -154,7 +153,6 @@ export const coreFields = {
tools: optionalArray(Tool),
tool_choice: Schema.optional(ToolChoice),
store: Schema.optional(Schema.Boolean),
truncation: Schema.optional(OpenResponsesOptions.TruncationSchema),
service_tier: Schema.optional(OpenResponsesOptions.ServiceTierSchema),
prompt_cache_key: Schema.optional(Schema.String),
include: optionalArray(OpenResponsesOptions.ResponseIncludableSchema),
@@ -441,10 +439,14 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
for (const message of request.messages) {
if (message.role === "system") {
input.push({
role: "developer",
content: ProviderShared.joinText(yield* ProviderShared.systemUpdateText(extension.name, message)),
})
const part = yield* ProviderShared.wrappedSystemUpdate(extension.name, message)
const previous = input.at(-1)
if (previous && "role" in previous && previous.role === "user")
input[input.length - 1] = {
role: "user",
content: [...previous.content, { type: "input_text", text: part.text }],
}
else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] })
continue
}
@@ -578,7 +580,6 @@ const lowerOptions = (request: LLMRequest) => {
: {}),
...(options.textVerbosity ? { text: { verbosity: options.textVerbosity } } : {}),
...(options.serviceTier ? { service_tier: options.serviceTier } : {}),
...(options.truncation ? { truncation: options.truncation } : {}),
}
}
@@ -16,25 +16,19 @@ export type ResponseIncludable = (typeof ResponseIncludables)[number]
export const ServiceTiers = ["auto", "default", "flex", "priority"] as const
export type ServiceTier = (typeof ServiceTiers)[number]
export const Truncations = ["auto", "disabled"] as const
export type Truncation = (typeof Truncations)[number]
const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"])
const INCLUDABLES = new Set<string>(ResponseIncludables)
const SERVICE_TIERS = new Set<string>(ServiceTiers)
const TRUNCATIONS = new Set<string>(Truncations)
const isTextVerbosity = (value: unknown): value is Schema.Schema.Type<typeof TextVerbosity> =>
typeof value === "string" && TEXT_VERBOSITY.has(value)
const isServiceTier = (value: unknown): value is ServiceTier => typeof value === "string" && SERVICE_TIERS.has(value)
const isTruncation = (value: unknown): value is Truncation => typeof value === "string" && TRUNCATIONS.has(value)
export const ReasoningEffort = Schema.String
export const TextVerbositySchema = TextVerbosity
export const ResponseIncludableSchema = Schema.Literals(ResponseIncludables)
export const ServiceTierSchema = Schema.Literals(ServiceTiers)
export const TruncationSchema = Schema.Literals(Truncations)
export interface Resolved {
readonly instructions?: string
@@ -44,7 +38,6 @@ export interface Resolved {
readonly include?: ReadonlyArray<ResponseIncludable>
readonly textVerbosity?: Schema.Schema.Type<typeof TextVerbosity>
readonly serviceTier?: ServiceTier
readonly truncation?: Truncation
}
export const resolve = (request: LLMRequest): Resolved => {
@@ -64,7 +57,6 @@ export const resolve = (request: LLMRequest): Resolved => {
include: include.length > 0 ? include : undefined,
textVerbosity: isTextVerbosity(input?.textVerbosity) ? input.textVerbosity : undefined,
serviceTier: isServiceTier(input?.serviceTier) ? input.serviceTier : undefined,
truncation: isTruncation(input?.truncation) ? input.truncation : undefined,
}
}
@@ -1,4 +1,4 @@
import type { ResponseIncludable, ServiceTier, Truncation } from "../protocols/utils/open-responses-options.js"
import type { ResponseIncludable, ServiceTier } from "../protocols/utils/open-responses-options.js"
import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema/index.js"
export interface OpenResponsesOptionsInput {
@@ -10,7 +10,6 @@ export interface OpenResponsesOptionsInput {
readonly include?: ReadonlyArray<ResponseIncludable>
readonly textVerbosity?: TextVerbosity
readonly serviceTier?: ServiceTier
readonly truncation?: Truncation
}
export type OpenResponsesProviderOptionsInput = ProviderOptions & {
+3 -13
View File
@@ -7,7 +7,6 @@ import {
type FinishReasonDetails,
type AIError,
type LLMRequest,
type ProviderMetadata,
type UsageInput,
} from "./schema/index.js"
import { Context, Deferred, Effect, Latch, Layer, Queue, Scope, Stream } from "effect"
@@ -34,22 +33,13 @@ export interface LayerOptions {
export class Service extends Context.Service<Service, Interface>()("@opencode/ai/TestLLM") {}
export const complete = (
options: {
readonly reason: FinishReasonDetails
readonly usage?: UsageInput
readonly providerMetadata?: ProviderMetadata
},
options: { readonly reason: FinishReasonDetails; readonly usage?: UsageInput },
...events: readonly LLMEvent[]
) => [
LLMEvent.stepStart({ index: 0 }),
...events,
LLMEvent.stepFinish({
index: 0,
reason: options.reason,
usage: options.usage,
providerMetadata: options.providerMetadata,
}),
LLMEvent.finish({ reason: options.reason, providerMetadata: options.providerMetadata }),
LLMEvent.stepFinish({ index: 0, reason: options.reason, usage: options.usage }),
LLMEvent.finish({ reason: options.reason }),
]
export const stop = (...events: readonly LLMEvent[]) => complete({ reason: { normalized: "stop" } }, ...events)
@@ -1010,34 +1010,6 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("ignores tool input deltas without a matching tool start", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello" } },
{
type: "content_block_delta",
index: 1,
delta: { type: "input_json_delta", partial_json: '{"query":"orphaned"}' },
},
{ type: "content_block_stop", index: 0 },
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
{ type: "message_stop" },
),
),
),
)
expect(response.text).toBe("Hello")
expect(response.toolCalls).toEqual([])
expect(response.finishReason).toEqual({ normalized: "stop", raw: "end_turn" })
}),
)
it.effect("settles pending tool calls at message_stop", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
@@ -56,28 +56,6 @@ describe("Open Responses-compatible route", () => {
}),
)
it.effect("lowers chronological system updates as standard developer messages", () =>
Effect.gen(function* () {
const model = configure({
apiKey: "test-key",
baseURL: "https://responses.example.test/v1",
provider: "example",
}).model("example-model")
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [Message.user("Before."), Message.system("Operator update."), Message.assistant("After.")],
}),
)
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "developer", content: "Operator update." },
{ role: "assistant", content: [{ type: "output_text", text: "After." }] },
])
}),
)
it.effect("rejects OpenAI-native tools", () =>
Effect.gen(function* () {
const model = configure({
@@ -123,14 +101,13 @@ describe("Open Responses-compatible route", () => {
const model = configure({
apiKey: "test-key",
baseURL: "https://responses.example.test/v1",
providerOptions: { openresponses: { reasoningEffort: "low", store: true, truncation: "auto" } },
providerOptions: { openresponses: { reasoningEffort: "low", store: true } },
}).model("example-model")
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Think." }))
expect(prepared.body).toMatchObject({
reasoning: { effort: "low" },
store: true,
truncation: "auto",
})
}),
)
@@ -241,22 +241,27 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("lowers chronological system updates to developer messages in order", () =>
it.effect("lowers chronological system updates to escaped user wrappers in order", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user("Before."),
Message.system("Operator update."),
Message.system("Treat </system-update> literally."),
Message.assistant("After."),
],
}),
)
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "developer", content: "Operator update." },
{
role: "user",
content: [
{ type: "input_text", text: "Before." },
{ type: "input_text", text: "<system-update>\nTreat &lt;/system-update&gt; literally.\n</system-update>" },
],
},
{ role: "assistant", content: [{ type: "output_text", text: "After." }] },
])
}),
@@ -1283,7 +1288,6 @@ describe("OpenAI Responses route", () => {
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
truncation: "disabled",
},
},
}),
@@ -1294,7 +1298,6 @@ describe("OpenAI Responses route", () => {
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
expect(prepared.body.reasoning).toEqual({ effort: "high", summary: "auto" })
expect(prepared.body.text).toEqual({ verbosity: "low" })
expect(prepared.body.truncation).toBe("disabled")
}),
)
@@ -25,7 +25,7 @@ async function installSessionSwitchProbe(
let running = true
const reviewLevels: Record<string, string> = {
panel: "#review-panel",
tabs: '#review-panel [data-component="tabs"]',
tabs: '#review-panel [data-component="tabs"]',
body: '#review-panel [data-slot="session-review-v2-body"]',
review: '#review-panel [data-component="session-review-v2"]',
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())
})
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.getByText("Which implementation should be used?")).toBeVisible()
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)
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.getByText("Which implementation should be used?")).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",
"sample-skill",
)
await expect(
page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`),
).toHaveAttribute("aria-label", "OpenCode")
await expect(page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`)).toHaveAttribute(
"aria-label",
"OpenCode",
)
for (const id of [pending, completed]) {
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-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
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()
if (text && !smoke.__timelineSmokeErrorToasts!.includes(text)) smoke.__timelineSmokeErrorToasts!.push(text)
}
+19 -2
View File
@@ -3,6 +3,7 @@ import { batch, createEffect, onCleanup, onMount, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
@@ -101,8 +102,16 @@ function Cell(props: {
</div>
)
if (props.inline) {
return (
<TooltipV2 value={props.tip} placement="top">
{content()}
</TooltipV2>
)
}
return (
<Tooltip appearance={props.inline ? "compact" : "standard"} value={props.tip} placement="top">
<Tooltip value={props.tip} placement="top">
{content()}
</Tooltip>
)
@@ -142,8 +151,16 @@ function ToggleCell(props: {
</button>
)
if (props.inline) {
return (
<TooltipV2 value={props.tip} placement="top">
{content()}
</TooltipV2>
)
}
return (
<Tooltip appearance={props.inline ? "compact" : "standard"} value={props.tip} placement="top">
<Tooltip value={props.tip} placement="top">
{content()}
</Tooltip>
)
@@ -1,10 +1,10 @@
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { Dialog, DialogBody } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { TextInput } from "@opencode-ai/ui/text-input"
import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command"
@@ -191,7 +191,7 @@ function CommandPaletteView(props: {
<Dialog class="command-palette-v2" size="large">
<DialogBody class="command-palette-v2-body">
<div class="command-palette-v2-search">
<TextInput
<TextInputV2
value={query()}
autofocus
autocomplete="off"
@@ -295,7 +295,7 @@ function PaletteRow(props: {
</div>
</div>
<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>
</Match>
<Match when={props.item.type === "session"}>
@@ -13,7 +13,7 @@ function ConnectProviderDialogStory() {
onMount(open)
return (
<Button variant="neutral" onClick={open}>
<Button variant="secondary" onClick={open}>
Open connect provider dialog
</Button>
)
@@ -29,7 +29,7 @@ function ProviderConnectionDialogStory(props) {
onMount(open)
return (
<Button variant="neutral" onClick={open}>
<Button variant="secondary" onClick={open}>
Open {props.provider} connection dialog
</Button>
)
@@ -5,8 +5,9 @@ import { List } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Spinner } from "@opencode-ai/ui/spinner"
import { TextField } from "@opencode-ai/ui/text-field"
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode-ai/ui/dialog"
import { TextInput } from "@opencode-ai/ui/text-input"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
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 { type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
@@ -74,7 +75,7 @@ export const DialogConnectProvider: Component<{
}
return (
<Dialog
<DialogV2
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]"
>
@@ -98,7 +99,7 @@ export const DialogConnectProvider: Component<{
<Content />
</div>
</DialogBody>
</Dialog>
</DialogV2>
)
}
@@ -167,7 +168,7 @@ function ProviderPicker(props: { directory?: string; onSelect: (provider: string
return (
<div ref={picker} class="flex min-h-0 flex-1 flex-col gap-4" onKeyDown={handleKeyDown}>
<div class="shrink-0 px-1 pt-px">
<TextInput
<TextInputV2
ref={search}
type="search"
class="!w-full [font-family:var(--v2-font-family-sans)]"
@@ -378,7 +379,7 @@ function ProviderConnection(props: {
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")}
</Button>
</Match>
@@ -515,7 +516,7 @@ function ProviderConnection(props: {
<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">
{language.t("provider.connect.apiKey.label", { provider: provider().name })}
<TextInput
<TextInputV2
ref={apiKey}
class="!w-full"
name="apiKey"
@@ -536,9 +537,9 @@ function ProviderConnection(props: {
</div>
)}
</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")}
</Button>
</ButtonV2>
</form>
</div>
)
@@ -584,7 +585,7 @@ function ProviderConnection(props: {
<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">
{language.t("provider.connect.oauth.code.label", { method: controller.currentMethod()?.label ?? "" })}
<TextInput
<TextInputV2
ref={codeInput}
class="!w-full"
name="code"
@@ -604,9 +605,9 @@ function ProviderConnection(props: {
</div>
)}
</Show>
<Button type="submit" variant="contrast">
<ButtonV2 type="submit" variant="contrast">
{language.t("common.continue")}
</Button>
</ButtonV2>
</form>
</div>
)
@@ -1,7 +1,6 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { Dialog } from "@opencode-ai/ui/dialog"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { useMutation } from "@tanstack/solid-query"
@@ -22,21 +21,20 @@ export function DialogCustomProvider(props: Props) {
const language = useLanguage()
return (
<Dialog class="h-full">
<DialogHeader>
<DialogTitle>
<IconButton
tabIndex={-1}
icon={<Icon name="arrow-left" />}
variant="ghost"
onClick={props.onBack}
aria-label={language.t("common.goBack")}
/>
</DialogTitle>
</DialogHeader>
<DialogBody>
<CustomProviderForm />
</DialogBody>
<Dialog
class="h-full"
title={
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={props.onBack}
aria-label={language.t("common.goBack")}
/>
}
transition
>
<CustomProviderForm />
</Dialog>
)
}
@@ -240,7 +238,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
</div>
<IconButton
type="button"
icon={<Icon name="trash" />}
icon="trash"
variant="ghost"
class="mt-1.5"
onClick={() => removeModel(i())}
@@ -284,7 +282,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
</div>
<IconButton
type="button"
icon={<Icon name="trash" />}
icon="trash"
variant="ghost"
class="mt-1.5"
onClick={() => removeHeader(i())}
@@ -303,7 +301,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
class="w-auto self-start"
type="submit"
size="large"
variant="contrast"
variant="primary"
disabled={saveMutation.isPending}
>
{saveMutation.isPending ? language.t("common.saving") : language.t("common.submit")}
@@ -1,11 +1,12 @@
import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogFooter } from "@opencode-ai/ui/dialog"
import { Field } from "@opencode-ai/ui/field"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
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 { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/project-avatar"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Textarea } from "@opencode-ai/ui/textarea"
import { TextInput } from "@opencode-ai/ui/text-input"
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/v2/project-avatar-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { TextareaV2 } from "@opencode-ai/ui/v2/textarea-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { For, Show, createSignal, startTransition } from "solid-js"
import { useLanguage } from "@/context/language"
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
@@ -36,46 +37,46 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
const Footer = () => (
<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")}
</Button>
<Button type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
</ButtonV2>
<ButtonV2 type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
</Button>
</ButtonV2>
</DialogFooter>
)
return (
<Dialog size="x-large" variant="settings" class="project-settings-v2-dialog">
<Tabs
<TabsV2
orientation="vertical"
variant="settings"
value={tab()}
onChange={(value) => void startTransition(() => setTab(value))}
class="project-settings-v2"
>
<Tabs.List>
<TabsV2.List>
<div class="project-settings-v2-nav">
<Tabs.Trigger value="general">
<TabsV2.Trigger value="general">
<ProjectAvatar
fallback={projectName()}
variant={getProjectAvatarVariant(props.project.icon?.color)}
class="!size-4 shrink-0"
/>
<span class="truncate">{projectName()}</span>
</Tabs.Trigger>
<Tabs.Trigger value="scripts">
</TabsV2.Trigger>
<TabsV2.Trigger value="scripts">
<Icon name="code" size="small" />
{language.t("project.settings.scripts")}
</Tabs.Trigger>
<Tabs.Trigger value="extensions">
</TabsV2.Trigger>
<TabsV2.Trigger value="extensions">
<Icon name="extensions" size="small" />
{language.t("settings.tab.extensions")}
</Tabs.Trigger>
</TabsV2.Trigger>
</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">
<div class="project-settings-v2-scroll">
<div class="project-settings-page-header">
@@ -85,7 +86,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
<Field>
<Field.Label>{language.t("dialog.project.edit.name")}</Field.Label>
<TextInput
<TextInputV2
autofocus
appearance="large"
class="!w-full"
@@ -131,7 +132,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
"opacity-0": !model.store.iconHover,
}}
>
<Icon name={model.store.iconOverride ? "close" : "share"} />
<IconV2 name={model.store.iconOverride ? "close" : "share"} />
</span>
</button>
<input
@@ -187,9 +188,9 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
</div>
<Footer />
</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">
<div class="project-settings-v2-scroll">
<div class="project-settings-page-header">
@@ -199,7 +200,7 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
<Field>
<Field.Label>{language.t("dialog.project.edit.worktree.startup")}</Field.Label>
<Field.Prefix>{language.t("dialog.project.edit.worktree.startup.description")}</Field.Prefix>
<Textarea
<TextareaV2
class="!w-full [&_[data-slot=textarea-v2-textarea]]:font-mono"
rows={5}
value={model.store.startup}
@@ -211,12 +212,12 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
</div>
<Footer />
</form>
</Tabs.Content>
</TabsV2.Content>
<Tabs.Content value="extensions" class="project-settings-v2-panel">
<TabsV2.Content value="extensions" class="project-settings-v2-panel">
<ProjectSettingsExtensions />
</Tabs.Content>
</Tabs>
</TabsV2.Content>
</TabsV2>
</Dialog>
)
}
+18 -23
View File
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from "@solidjs/router"
import { useData } from "@/context/server"
import { usePrompt } from "@/context/prompt"
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 { showToast } from "@/utils/toast"
import { useLanguage } from "@/context/language"
@@ -94,28 +94,23 @@ export const DialogFork: Component = () => {
}
return (
<Dialog>
<DialogHeader>
<DialogTitle>{language.t("command.session.fork")}</DialogTitle>
</DialogHeader>
<DialogBody>
<List
class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.fork.empty")}
key={(x) => x.id}
items={messages}
filterKeys={["text"]}
onSelect={handleSelect}
>
{(item) => (
<div class="w-full flex items-center gap-2">
<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>
</div>
)}
</List>
</DialogBody>
<Dialog title={language.t("command.session.fork")}>
<List
class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.fork.empty")}
key={(x) => x.id}
items={messages}
filterKeys={["text"]}
onSelect={handleSelect}
>
{(item) => (
<div class="w-full flex items-center gap-2">
<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>
</div>
)}
</List>
</Dialog>
)
}
@@ -1,12 +1,15 @@
import { Button } from "@opencode-ai/ui/button"
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 { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Switch } from "@opencode-ai/ui/switch"
import { TextInput } from "@opencode-ai/ui/text-input"
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 { For, Show, type Component } from "solid-js"
import { useLocal } from "@/context/local"
@@ -41,80 +44,74 @@ export const DialogManageModels: Component = () => {
}
return (
<Dialog>
<DialogHeader hideClose>
<DialogTitleGroup
title={language.t("dialog.model.manage")}
description={language.t("dialog.model.manage.description")}
/>
<Dialog
title={language.t("dialog.model.manage")}
description={language.t("dialog.model.manage.description")}
action={
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={handleConnectProvider}>
{language.t("command.provider.connect")}
</Button>
</DialogHeader>
<DialogBody>
<List
class="px-3"
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model.list()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.id}
groupHeader={(group) => {
const provider = group.items[0].provider
return (
<>
<span>{provider.name}</span>
<Tooltip
appearance="standard"
placement="top"
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()}>
}
>
<List
class="px-3"
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model.list()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.id}
groupHeader={(group) => {
const provider = group.items[0].provider
return (
<>
<span>{provider.name}</span>
<Tooltip
placement="top"
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
>
<Switch
appearance="standard"
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>
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
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>
)}
</List>
</DialogBody>
</div>
)}
</List>
</Dialog>
)
}
@@ -157,20 +154,20 @@ export const DialogManageModelsV2: Component = () => {
})
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")}>
<DialogTitleGroup
title={language.t("dialog.model.manage")}
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")}
</Button>
</ButtonV2>
</DialogHeader>
<DialogBody class="flex min-h-0 flex-1 flex-col">
<div class="px-4 pt-px pb-3">
<div class="relative">
<TextInput
<TextInputV2
type="search"
appearance="base"
class="!w-full self-stretch"
@@ -185,12 +182,12 @@ export const DialogManageModelsV2: Component = () => {
aria-label={language.t("dialog.model.search.placeholder")}
/>
<Show when={list.filter()}>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="small"
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()}
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>
</div>
<div>
<Switch
<SwitchV2
class="mr-6"
checked={providerVisible(group.category)}
onChange={(checked) => setProviderVisibility(group.category, checked)}
hideLabel
>
{group.items[0].provider.name}
</Switch>
</SwitchV2>
</div>
</div>
<SettingsListV2>
@@ -243,13 +240,13 @@ export const DialogManageModelsV2: Component = () => {
{(item) => (
<SettingsRowV2 title={item.name} description="">
<div>
<Switch
<SwitchV2
checked={local.model.visible({ modelID: item.id, providerID: item.provider.id })}
onChange={(checked) => setModelVisibility(item, checked)}
hideLabel
>
{item.name}
</Switch>
</SwitchV2>
</div>
</SettingsRowV2>
)}
@@ -263,6 +260,6 @@ export const DialogManageModelsV2: Component = () => {
</div>
</div>
</DialogBody>
</Dialog>
</DialogV2>
)
}
@@ -86,12 +86,12 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
<Show
when={isLast()}
fallback={
<Button variant="neutral" size="large" onClick={handleNext}>
<Button variant="secondary" size="large" onClick={handleNext}>
{language.t("dialog.releaseNotes.action.next")}
</Button>
}
>
<Button variant="contrast" size="large" onClick={handleClose}>
<Button variant="primary" size="large" onClick={handleClose}>
{language.t("dialog.releaseNotes.action.getStarted")}
</Button>
</Show>
@@ -1,8 +1,8 @@
import "@pierre/trees/web-components"
import { FileTree } from "@pierre/trees"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { TextInput } from "@opencode-ai/ui/text-input"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createEffect, createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"
import { useGlobal } from "@/context/global"
@@ -28,7 +28,7 @@ import {
pickerRoot,
} from "./directory-picker-domain"
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"
interface DialogSelectDirectoryV2Props {
@@ -293,10 +293,10 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
<DialogHeader>
<DialogTitle>{props.title ?? language.t("command.project.open")}</DialogTitle>
</DialogHeader>
<Divider />
<DividerV2 />
<DialogBody class="directory-picker-v2-body pt-4!">
<div class="directory-picker-v2-path" ref={pathArea}>
<TextInput
<TextInputV2
value={input()}
autofocus
autocomplete="off"
@@ -318,15 +318,15 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
onKeyDown={handleInputKey}
/>
<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>
<Button size="small" variant="ghost" onClick={() => void navigate(pickerRoot(root()) || root())}>
</ButtonV2>
<ButtonV2 size="small" variant="ghost" onClick={() => void navigate(pickerRoot(root()) || root())}>
{language.t("dialog.directory.root")}
</Button>
<Button size="small" variant="ghost" onClick={() => void navigate(pickerParent(root()))}>
</ButtonV2>
<ButtonV2 size="small" variant="ghost" onClick={() => void navigate(pickerParent(root()))}>
{language.t("dialog.directory.parent")}
</Button>
</ButtonV2>
</div>
<Show when={suggestionsOpen() && currentSuggestions().length > 0}>
<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>
</DialogBody>
<DialogFooter>
<Button variant="neutral" onClick={() => dialog.close()}>
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</Button>
<Button variant="contrast" disabled={!policy.result(root(), selected(), rootValid())} onClick={resolve}>
</ButtonV2>
<ButtonV2 variant="contrast" disabled={!policy.result(root(), selected(), rootValid())} onClick={resolve}>
{action[policy.action]}
</Button>
</ButtonV2>
</DialogFooter>
</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 { useData } from "@/context/server"
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 { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/context/language"
@@ -31,71 +31,65 @@ export const DialogSelectMcp: Component = () => {
const totalCount = createMemo(() => items().length)
return (
<Dialog>
<DialogHeader>
<DialogTitleGroup
title={language.t("dialog.mcp.title")}
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
/>
</DialogHeader>
<DialogBody>
<List
class="px-3"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.mcp.empty")}
key={(x) => x?.name ?? ""}
items={items}
filterKeys={["name", "status"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
onSelect={(x) => {
if (!x || x.status === "pending" || toggle.isPending) return
toggle.mutate(x.name)
}}
>
{(i) => {
const mcpStatus = () =>
data.location.mcp.server.list({ directory: sdk().directory })?.find((server) => server.name === i.name)
?.status
const status = () => mcpStatus()?.status
const statusLabel = () => {
const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
if (!key) return
return language.t(key)
}
const error = () => {
const s = mcpStatus()
if (s?.status === "failed") return s.error
}
const enabled = () => status() === "connected"
return (
<div class="w-full flex items-center justify-between gap-x-3">
<div class="flex flex-col gap-0.5 min-w-0">
<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>
<Dialog
title={language.t("dialog.mcp.title")}
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
>
<List
class="px-3"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.mcp.empty")}
key={(x) => x?.name ?? ""}
items={items}
filterKeys={["name", "status"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
onSelect={(x) => {
if (!x || x.status === "pending" || toggle.isPending) return
toggle.mutate(x.name)
}}
>
{(i) => {
const mcpStatus = () =>
data.location.mcp.server.list({ directory: sdk().directory })?.find((server) => server.name === i.name)
?.status
const status = () => mcpStatus()?.status
const statusLabel = () => {
const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
if (!key) return
return language.t(key)
}
const error = () => {
const s = mcpStatus()
if (s?.status === "failed") return s.error
}
const enabled = () => status() === "connected"
return (
<div class="w-full flex items-center justify-between gap-x-3">
<div class="flex flex-col gap-0.5 min-w-0">
<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>
<div onClick={(e) => e.stopPropagation()}>
<Switch
appearance="standard"
checked={enabled()}
disabled={status() === "pending" || (toggle.isPending && toggle.variables === i.name)}
onChange={() => {
if (toggle.isPending) return
toggle.mutate(i.name)
}}
/>
</div>
<Show when={error()}>
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
</Show>
</div>
)
}}
</List>
</DialogBody>
<div onClick={(e) => e.stopPropagation()}>
<Switch
checked={enabled()}
disabled={status() === "pending" || (toggle.isPending && toggle.variables === i.name)}
onChange={() => {
if (toggle.isPending) return
toggle.mutate(i.name)
}}
/>
</div>
</div>
)
}}
</List>
</Dialog>
)
}
@@ -39,7 +39,7 @@ function SelectModelWithoutProviders() {
onMount(open)
return (
<Button variant="neutral" onClick={open}>
<Button variant="secondary" onClick={open}>
Open select model dialog
</Button>
)
@@ -1,8 +1,8 @@
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode-ai/ui/dialog"
import { Badge } from "@opencode-ai/ui/badge"
import { Icon } from "@opencode-ai/ui/icon"
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
import { Icon } from "@opencode-ai/ui/v2/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 { useTheme } from "@opencode-ai/ui/theme"
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
@@ -66,7 +66,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
})
return (
<Dialog
<DialogV2
fit
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]"
@@ -84,7 +84,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
</div>
<For each={freeModels()}>
{(item) => (
<Tooltip
<TooltipV2
class="w-full"
placement="right-start"
gutter={6}
@@ -105,15 +105,15 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
onClick={() => selectModel(item)}
>
<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}>
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
<Tag class="shrink-0">{language.t("model.tag.latest")}</Tag>
</Show>
<Show when={currentKey() === modelKey(item)}>
<Icon name="check" class="ml-auto size-4 shrink-0 text-v2-icon-icon-base" />
</Show>
</button>
</Tooltip>
</TooltipV2>
)}
</For>
</div>
@@ -172,6 +172,6 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
</div>
</div>
</DialogBody>
</Dialog>
</DialogV2>
)
}
@@ -1,9 +1,9 @@
import { Button } from "@opencode-ai/ui/button"
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 { 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 { type Component, Show } from "solid-js"
import { useLocal } from "@/context/local"
@@ -40,113 +40,108 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
}
return (
<Dialog class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none">
<DialogHeader>
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
</DialogHeader>
<DialogBody>
<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>
<List
class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
ref={(ref) => (listRef = ref)}
items={model.list}
current={model.current()}
key={(x) => `${x.provider.id}:${x.id}`}
itemWrapper={(item, node) => (
<Tooltip
appearance="standard"
class="w-full"
placement="right-start"
gutter={12}
value={
<ModelTooltip
model={item}
latest={item.latest}
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
/>
}
<Dialog
title={language.t("dialog.model.select.title")}
class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none"
>
<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>
<List
class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
ref={(ref) => (listRef = ref)}
items={model.list}
current={model.current()}
key={(x) => `${x.provider.id}:${x.id}`}
itemWrapper={(item, node) => (
<Tooltip
class="w-full"
placement="right-start"
gutter={12}
value={
<ModelTooltip
model={item}
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}
</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>
<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"}>
{(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">{language.t("dialog.provider.opencode.tagline")}</div>
</Show>
<Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={i.id === "opencode-go"}>
<>
<div class="text-14-regular text-text-weak">
{language.t("dialog.provider.opencode.tagline")}
{language.t("dialog.provider.opencodeGo.tagline")}
</div>
</Show>
<Show when={i.id === "opencode"}>
<Badge appearance="standard">{language.t("dialog.provider.tag.recommended")}</Badge>
</Show>
<Show when={i.id === "opencode-go"}>
<>
<div class="text-14-regular text-text-weak">
{language.t("dialog.provider.opencodeGo.tagline")}
</div>
<Badge appearance="standard">{language.t("dialog.provider.tag.recommended")}</Badge>
</>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
</div>
)}
</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>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
</div>
)}
</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>
</DialogBody>
</div>
</Dialog>
)
}
@@ -5,14 +5,16 @@ import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
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 { 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 { 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 { useLanguage } from "@/context/language"
import { decode64 } from "@/utils/base64"
@@ -79,7 +81,6 @@ const ModelList: Component<{
}}
itemWrapper={(item, node) => (
<Tooltip
appearance="standard"
class="w-full"
placement="right-start"
gutter={12}
@@ -100,10 +101,10 @@ const ModelList: Component<{
<div class="w-full flex items-center gap-x-2 text-13-regular">
<span class="truncate">{i.name}</span>
<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 when={i.latest}>
<Badge appearance="standard">{language.t("model.tag.latest")}</Badge>
<Tag>{language.t("model.tag.latest")}</Tag>
</Show>
</div>
)}
@@ -192,19 +193,21 @@ export function ModelSelectorPopover(props: {
class="p-1"
action={
<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
icon={<Icon name="plus-small" />}
icon="plus-small"
variant="ghost"
iconSize="normal"
class="size-6"
aria-label={language.t("command.provider.connect")}
onClick={handleConnectProvider}
/>
</Tooltip>
<Tooltip appearance="standard" placement="top" value={language.t("dialog.model.manage")}>
<Tooltip placement="top" value={language.t("dialog.model.manage")}>
<IconButton
icon={<Icon name="sliders" />}
icon="sliders"
variant="ghost"
iconSize="normal"
class="size-6"
aria-label={language.t("dialog.model.manage")}
onClick={handleManage}
@@ -370,10 +373,10 @@ function ModelSelectorPopoverV2View(props: {
})
return (
<Menu open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
<Menu.Trigger as={props.trigger} />
<Menu.Portal>
<Menu.Content
<MenuV2 open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
<MenuV2.Trigger as={props.trigger} />
<MenuV2.Portal>
<MenuV2.Content
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"
onPointerDownOutside={dismiss.preventTriggerRestore}
@@ -446,14 +449,14 @@ function ModelSelectorPopoverV2View(props: {
>
<For each={groups()}>
{(group) => (
<Menu.Group>
<Menu.GroupLabel class="gap-2 px-3">
<MenuV2.Group>
<MenuV2.GroupLabel class="gap-2 px-3">
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
</Menu.GroupLabel>
<Menu.RadioGroup value={props.current}>
</MenuV2.GroupLabel>
<MenuV2.RadioGroup value={props.current}>
<For each={group.items}>
{(item) => (
<Tooltip
<TooltipV2
class="w-full"
placement="right-start"
gutter={6}
@@ -467,7 +470,7 @@ function ModelSelectorPopoverV2View(props: {
/>
}
>
<Menu.RadioItem
<MenuV2.RadioItem
value={modelKey(item)}
data-option-key={modelKey(item)}
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>
<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 when={item.latest}>
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
</Show>
</Menu.RadioItem>
</Tooltip>
</MenuV2.RadioItem>
</TooltipV2>
)}
</For>
</Menu.RadioGroup>
</Menu.Group>
</MenuV2.RadioGroup>
</MenuV2.Group>
)}
</For>
</Show>
@@ -499,7 +502,7 @@ function ModelSelectorPopoverV2View(props: {
</ScrollView>
<div class="h-px bg-v2-border-border-muted" />
<div class="flex flex-col p-0.5">
<Menu.Item
<MenuV2.Item
data-option-key={manageKey}
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === manageKey }}
onMouseEnter={() => {
@@ -510,11 +513,11 @@ function ModelSelectorPopoverV2View(props: {
>
<Icon name="outline-sliders" size="small" />
<span class="min-w-0 flex-1 truncate leading-5">{language.t("dialog.model.manage")}</span>
</Menu.Item>
</MenuV2.Item>
</div>
</Menu.Content>
</Menu.Portal>
</Menu>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
)
}
@@ -537,19 +540,18 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat
}
return (
<Dialog>
<DialogHeader hideClose>
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
<Dialog
title={language.t("dialog.model.select.title")}
action={
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={provider}>
{language.t("command.provider.connect")}
</Button>
</DialogHeader>
<DialogBody>
<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}>
{language.t("dialog.model.manage")}
</Button>
</DialogBody>
}
>
<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}>
{language.t("dialog.model.manage")}
</Button>
</Dialog>
)
}
@@ -1,6 +1,5 @@
import { Button } from "@opencode-ai/ui/button"
import { Menu } from "@opencode-ai/ui/menu"
import { Icon } from "@opencode-ai/ui/icon"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { List } from "@opencode-ai/ui/list"
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">
<Show when={i.type === "http"}>
<Menu appearance="standard">
<Menu.Trigger
<DropdownMenu>
<DropdownMenu.Trigger
as={IconButton}
icon={<Icon name="dot-grid" />}
icon="dot-grid"
variant="ghost"
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
onClick={(e: MouseEvent) => e.stopPropagation()}
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
/>
<Menu.Portal>
<Menu.Content class="mt-1">
<Menu.Item
<DropdownMenu.Portal>
<DropdownMenu.Content class="mt-1">
<DropdownMenu.Item
onSelect={() => {
if (i.type !== "http") return
props.onEdit(i)
}}
>
{language.t("dialog.server.menu.edit")}
</Menu.Item>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
<Menu.Item onSelect={() => props.domain.defaults.set(key)}>
{language.t("dialog.server.menu.default")}
</Menu.Item>
<DropdownMenu.Item onSelect={() => props.domain.defaults.set(key)}>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
<Menu.Item onSelect={() => props.domain.defaults.set(null)}>
{language.t("dialog.server.menu.defaultRemove")}
</Menu.Item>
<DropdownMenu.Item onSelect={() => props.domain.defaults.set(null)}>
<DropdownMenu.ItemLabel>
{language.t("dialog.server.menu.defaultRemove")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<Show when={props.domain.connection.canRemove(key)}>
<Menu.Separator />
<Menu.Item
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => props.domain.connection.remove(key)}
class="text-text-on-critical-base hover:bg-surface-critical-weak"
>
{language.t("dialog.server.menu.delete")}
</Menu.Item>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
</Menu.Content>
</Menu.Portal>
</Menu>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</Show>
</div>
</div>
@@ -207,7 +208,7 @@ export function ServerConnectionList(props: {
<div class="shrink-0 pb-5">
<Button
variant="neutral"
variant="secondary"
icon="plus-small"
size="large"
onClick={props.onAdd}
@@ -243,7 +244,7 @@ export function ServerConnectionForm(props: { form: ServerConnectionFormControll
/>
<div class="shrink-0 pb-5">
<Button
variant="contrast"
variant="primary"
size="large"
onClick={props.form.submit}
disabled={props.form.state.busy()}
@@ -2,7 +2,7 @@ import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { Button } from "@opencode-ai/ui/button"
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"
export type DialogGoUpsellProps = {
@@ -30,22 +30,17 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {
}
return (
<Dialog fit>
<DialogHeader>
<DialogTitleGroup title={props.title} description={props.description} />
</DialogHeader>
<DialogBody>
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
<div class="flex justify-end gap-2">
<Button variant="ghost" size="large" onClick={dismiss}>
{language.t("dialog.usageExceeded.dontShowAgain")}
</Button>
<Button variant="contrast" size="large" onClick={runAction}>
{props.actionLabel}
</Button>
</div>
<Dialog title={props.title} description={props.description} fit>
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
<div class="flex justify-end gap-2">
<Button variant="ghost" size="large" onClick={dismiss}>
{language.t("dialog.usageExceeded.dontShowAgain")}
</Button>
<Button variant="primary" size="large" onClick={runAction}>
{props.actionLabel}
</Button>
</div>
</DialogBody>
</div>
</Dialog>
)
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { useFile } from "@/context/file"
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 {
createEffect,
createMemo,
@@ -13,7 +13,7 @@ import {
} from "solid-js"
import { Dynamic } from "solid-js/web"
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 { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
import {
@@ -1,13 +1,12 @@
import { Icon } from "@opencode-ai/ui/icon"
import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { type Component, For, Show, createEffect, createMemo, createResource, createSignal } from "solid-js"
import { useLanguage } from "@/context/language"
import { useMcpToggle } from "@/context/mcp"
import { useWorkspaceLocation } from "@/context/location"
import { useServerSDK } from "@/context/server-sdk"
import { useData } from "@/context/server"
import { pluginLabel } from "@/utils/plugin"
import { ExternalLink } from "./external-link"
type SkillItem = {
@@ -102,10 +101,10 @@ export const ProjectSettingsExtensions: Component = () => {
() => (serverSDK.connection.status() === "connected" ? directorySDK().directory : undefined),
(directory) => serverSDK.api.plugin.list({ location: { directory } }).then((result) => result.data),
)
const globalPlugins = createMemo(() => (globalPluginList.latest ?? []).map(pluginLabel))
const globalPlugins = createMemo(() => (globalPluginList.latest ?? []).map((item) => item.id))
const projectPlugins = createMemo(() => {
const shared = new Set(globalPlugins())
return (projectPluginList.latest ?? []).map(pluginLabel).filter((name) => !shared.has(name))
return (projectPluginList.latest ?? []).map((item) => item.id).filter((name) => !shared.has(name))
})
const serverSkills = createMemo(() => data.location.skill.list() ?? [])
@@ -149,15 +148,15 @@ export const ProjectSettingsExtensions: Component = () => {
<span>{language.t("project.settings.extensions.description")}</span>
</div>
<Tabs variant="pill" defaultValue="mcps" class="project-settings-extension-tabs">
<Tabs.List>
<Tabs.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</Tabs.Trigger>
<Tabs.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</Tabs.Trigger>
<Tabs.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</Tabs.Trigger>
<TabsV2 variant="pill" defaultValue="mcps" class="project-settings-extension-tabs">
<TabsV2.List>
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
{/* 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-header">
<span>{language.t("project.settings.extensions.added")}</span>
@@ -168,9 +167,9 @@ export const ProjectSettingsExtensions: Component = () => {
</Show>
<SharedSection count={globalMcpNames().length}>{mcpRows(globalMcpNames())}</SharedSection>
</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-header">
<span>{language.t("project.settings.extensions.added")}</span>
@@ -181,9 +180,9 @@ export const ProjectSettingsExtensions: Component = () => {
</Show>
<SharedSection count={globalPlugins().length}>{pluginRows(globalPlugins())}</SharedSection>
</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-header">
<span>{language.t("project.settings.extensions.added")}</span>
@@ -196,8 +195,8 @@ export const ProjectSettingsExtensions: Component = () => {
</Show>
<SharedSection count={serverSkills().length}>{skillRows(serverSkills())}</SharedSection>
</div>
</Tabs.Content>
</Tabs>
</TabsV2.Content>
</TabsV2>
</div>
)
}
+11 -11
View File
@@ -1,10 +1,10 @@
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import type { ReferenceInfo } from "@opencode-ai/client/promise"
import { createEffect, createMemo, on, Show } from "solid-js"
import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
@@ -503,20 +503,20 @@ function PromptInputV2ModelControl(props: {
)
return (
<Show when={!props.loading}>
<Tooltip
<TooltipV2
placement="top"
gutter={4}
value={
<>
{props.title}
<Keybind keys={props.keybind} variant="neutral" />
<KeybindV2 keys={props.keybind} variant="neutral" />
</>
}
>
<Show
when={props.paid}
fallback={
<Button
<ButtonV2
data-action="prompt-model"
data-control-type="dialog"
variant="ghost-muted"
@@ -527,13 +527,13 @@ function PromptInputV2ModelControl(props: {
onClick={props.onUnpaidClick}
>
{content()}
</Button>
</ButtonV2>
}
>
<ModelSelectorPopoverV2
model={props.model}
trigger={(triggerProps) => (
<Button
<ButtonV2
{...triggerProps}
variant="ghost-muted"
size="normal"
@@ -544,12 +544,12 @@ function PromptInputV2ModelControl(props: {
data-control-type="popover"
>
{content()}
</Button>
</ButtonV2>
)}
onClose={props.onClose}
/>
</Show>
</Tooltip>
</TooltipV2>
</Show>
)
}
@@ -9,9 +9,10 @@ import {
type ComponentProps,
} from "solid-js"
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 { 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 { useLanguage } from "@/context/language"
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
@@ -276,8 +277,7 @@ export function PromptProjectSelector(props: {
})
return (
<Menu
appearance="standard"
<DropdownMenu
open={triggerReady() && props.controller.open()}
placement={props.placement ?? "bottom"}
gutter={4}
@@ -287,9 +287,9 @@ export function PromptProjectSelector(props: {
props.controller.setOpen(open)
}}
>
<Menu.Trigger as={ProjectTrigger} ref={setTriggerRef} controller={props.controller} />
<Menu.Portal>
<Menu.Content
<DropdownMenu.Trigger as={ProjectTrigger} ref={setTriggerRef} controller={props.controller} />
<DropdownMenu.Portal>
<DropdownMenu.Content
ref={contentRef}
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"
@@ -360,13 +360,13 @@ export function PromptProjectSelector(props: {
<Show
when={props.controller.servers().length > 1}
fallback={
<Menu.RadioGroup value={selectedValue()}>
<DropdownMenu.RadioGroup value={selectedValue()}>
<For each={props.controller.projects()}>
{(project) => (
<ProjectItem project={project} controller={props.controller} onSelect={selectProject} />
)}
</For>
</Menu.RadioGroup>
</DropdownMenu.RadioGroup>
}
>
<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">
{server!.name}
</div>
<Menu.RadioGroup value={selectedValue()}>
<DropdownMenu.RadioGroup value={selectedValue()}>
<For
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} />
)}
</For>
</Menu.RadioGroup>
</DropdownMenu.RadioGroup>
</div>
)}
</For>
@@ -408,8 +408,8 @@ export function PromptProjectSelector(props: {
/>
}
>
<Menu.Sub>
<Menu.SubTrigger
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
id={props.controller.actionKey()}
data-option-key={props.controller.actionKey()}
class={projectActionClass}
@@ -419,23 +419,24 @@ export function PromptProjectSelector(props: {
onMouseEnter={() => props.controller.setActive(props.controller.actionKey())}
>
<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()}
</span>
</Menu.SubTrigger>
<Menu.Portal>
<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">
<Icon name="chevron-right" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</DropdownMenu.SubTrigger>
<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()}>
{(server) => <ServerAction server={server!} onSelect={selectAction} />}
</For>
</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
</Show>
</div>
</Menu.Content>
</Menu.Portal>
</Menu>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
)
}
@@ -506,7 +507,7 @@ function ProjectItem(props: {
}) {
const key = () => props.controller.projectKey(props.project)
return (
<Menu.RadioItem
<DropdownMenu.RadioItem
id={key()}
value={key()}
data-option-key={key()}
@@ -533,8 +534,11 @@ function ProjectItem(props: {
src={getProjectAvatarSource(props.project.id, props.project.icon)}
variant={getProjectAvatarVariant(props.project.icon?.color)}
/>
<span class="min-w-0 truncate leading-5">{displayName(props.project)}</span>
</Menu.RadioItem>
<DropdownMenu.ItemLabel class="min-w-0 truncate leading-5">{displayName(props.project)}</DropdownMenu.ItemLabel>
<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)
return (
<Menu.Item
<DropdownMenu.Item
id={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"
@@ -569,17 +573,17 @@ function ProjectAction(props: {
onSelect={() => props.onSelect(props.server)}
>
<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()}
</span>
</Menu.Item>
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
)
}
function ServerAction(props: { server: { key: string; name: string }; onSelect: (server: string) => void }) {
return (
<Menu.Item class={projectActionClass} onSelect={() => props.onSelect(props.server.key)}>
<span class="min-w-0 flex-1 truncate leading-5">{props.server.name}</span>
</Menu.Item>
<DropdownMenu.Item class={projectActionClass} onSelect={() => props.onSelect(props.server.key)}>
<DropdownMenu.ItemLabel class="min-w-0 flex-1 truncate leading-5">{props.server.name}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
)
}
@@ -1,7 +1,7 @@
import { createMemo, createSignal, For, Show } from "solid-js"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Icon } from "@opencode-ai/ui/icon"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { getFilename } from "@opencode-ai/util/path"
import { useLanguage } from "@/context/language"
import { sameDirectory } from "@/utils/workspace"
@@ -58,7 +58,7 @@ export function PromptWorkspaceSelector(props: {
return (
<>
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
<Tooltip
<TooltipV2
placement="top"
openDelay={800}
value={
@@ -77,8 +77,8 @@ export function PromptWorkspaceSelector(props: {
contentClass={props.onboarding ? "max-w-[280px]" : undefined}
class="min-w-0"
>
<Menu placement="bottom" gutter={4} onOpenChange={onOpenChange}>
<Menu.Trigger
<MenuV2 placement="bottom" gutter={4} onOpenChange={onOpenChange}>
<MenuV2.Trigger
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"
>
@@ -92,14 +92,14 @@ export function PromptWorkspaceSelector(props: {
/>
</Show>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Content class="w-[200px]">
<Menu.Group>
<Menu.GroupLabel>{language.t("session.new.workspace.runIn")}</Menu.GroupLabel>
<Menu.Item onSelect={() => select("main")}>
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content class="w-[200px]">
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("session.new.workspace.runIn")}</MenuV2.GroupLabel>
<MenuV2.Item onSelect={() => select("main")}>
<Icon name="monitor" />
<Tooltip
<TooltipV2
placement="right"
openDelay={800}
value={
@@ -113,14 +113,14 @@ export function PromptWorkspaceSelector(props: {
class="min-w-0 flex-1"
>
<span class="min-w-0 truncate">{language.t("session.new.workspace.local")}</span>
</Tooltip>
</TooltipV2>
<Show when={selected() === "main"}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</Menu.Item>
<Menu.Item onSelect={() => select("create")}>
</MenuV2.Item>
<MenuV2.Item onSelect={() => select("create")}>
<Icon name="workspace-new" />
<Tooltip
<TooltipV2
placement="right"
openDelay={800}
value={
@@ -134,25 +134,25 @@ export function PromptWorkspaceSelector(props: {
class="min-w-0 flex-1"
>
<span class="min-w-0 truncate">{language.t("workspace.new")}</span>
</Tooltip>
</TooltipV2>
<Show when={selected() === "create"}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</Menu.Item>
</Menu.Group>
</MenuV2.Item>
</MenuV2.Group>
<Show
when={props.workspaces.length > 0}
fallback={
<>
<Menu.Separator class="h-[0.5px]" />
<Menu.Item onSelect={() => (pending = { type: "viewAll" })}>
<MenuV2.Separator class="h-[0.5px]" />
<MenuV2.Item onSelect={() => (pending = { type: "viewAll" })}>
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
</Menu.Item>
</MenuV2.Item>
</>
}
>
<Menu.Separator class="h-[0.5px]" />
<Menu.Sub
<MenuV2.Separator class="h-[0.5px]" />
<MenuV2.Sub
gutter={0}
overlap
overflowPadding={8}
@@ -166,7 +166,7 @@ export function PromptWorkspaceSelector(props: {
requestAnimationFrame(() => searchInput?.focus())
}}
>
<Menu.SubTrigger
<MenuV2.SubTrigger
onKeyDown={(event) => {
if (
event.key === "ArrowRight" ||
@@ -181,9 +181,9 @@ export function PromptWorkspaceSelector(props: {
<span class="min-w-0 flex-1 truncate">
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
</span>
</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
</MenuV2.SubTrigger>
<MenuV2.Portal>
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
<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">
<Icon name="magnifying-glass" size="small" class="shrink-0" />
@@ -211,27 +211,27 @@ export function PromptWorkspaceSelector(props: {
</Show>
<For each={workspaces()}>
{(workspace) => (
<Menu.Item onSelect={() => select(workspace)}>
<MenuV2.Item onSelect={() => select(workspace)}>
<Icon name="workspace-isolated" />
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
<Show when={selected() === workspace}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</Menu.Item>
</MenuV2.Item>
)}
</For>
<Menu.Separator class="h-[0.5px]" />
<Menu.Item onSelect={() => (pending = { type: "viewAll" })}>
<MenuV2.Separator class="h-[0.5px]" />
<MenuV2.Item onSelect={() => (pending = { type: "viewAll" })}>
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
</Menu.Item>
</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
</MenuV2.Item>
</MenuV2.SubContent>
</MenuV2.Portal>
</MenuV2.Sub>
</Show>
</Menu.Content>
</Menu.Portal>
</Menu>
</Tooltip>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</TooltipV2>
<PromptGitStatus branch={props.branch} from={selected() === "create"} class="ms-1" />
</>
)
@@ -255,7 +255,7 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?
return (
<Show when={label()}>
{(value) => (
<Tooltip
<TooltipV2
placement="top"
value={value()}
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" />
<span class="min-w-0 truncate">{value()}</span>
</div>
</Tooltip>
</TooltipV2>
)}
</Show>
)
@@ -1,6 +1,6 @@
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Menu } from "@opencode-ai/ui/menu"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { type Component, Show } from "solid-js"
import type { ServerActionsController } from "@/components/server/server-management-controller"
import { useLanguage } from "@/context/language"
@@ -59,19 +59,19 @@ export const ServerRowMenuView: Component<{
const builtin = () => ServerConnection.builtin(props.server)
const httpServer = () => (props.server.type === "http" ? props.server : undefined)
return (
<Menu gutter={6} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
<Menu.Trigger
as={IconButton}
<MenuV2 gutter={6} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
<MenuV2.Trigger
as={IconButtonV2}
variant="ghost-muted"
size="small"
icon={<Icon name="outline-dots" />}
icon={<IconV2 name="outline-dots" />}
aria-label={props.labels.more}
/>
<Menu.Portal>
<Menu.Content>
<Menu.Group>
<Menu.GroupLabel>{props.labels.server}</Menu.GroupLabel>
<Menu.Item
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Group>
<MenuV2.GroupLabel>{props.labels.server}</MenuV2.GroupLabel>
<MenuV2.Item
disabled={builtin() || !httpServer()}
onSelect={() => {
const server = httpServer()
@@ -79,20 +79,20 @@ export const ServerRowMenuView: Component<{
}}
>
{props.labels.edit}
</Menu.Item>
</MenuV2.Item>
<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 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 when={props.canRemove}>
<Menu.Separator />
<Menu.Item onSelect={props.onRemove}>{props.labels.delete}</Menu.Item>
<MenuV2.Separator />
<MenuV2.Item onSelect={props.onRemove}>{props.labels.delete}</MenuV2.Item>
</Show>
</Menu.Group>
</Menu.Content>
</Menu.Portal>
</Menu>
</MenuV2.Group>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
)
}
@@ -64,7 +64,6 @@ export function ServerRow(props: ServerRowProps) {
return (
<Tooltip
appearance="standard"
class="flex-1 min-w-0"
value={tooltipValue()}
contentStyle={{ "max-width": "none", "white-space": "nowrap" }}
@@ -1,7 +1,8 @@
import { Show, createMemo, type ComponentProps, type JSX } from "solid-js"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
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 { useFile } from "@/context/file"
@@ -15,7 +16,7 @@ import { createSessionTabs } from "@/pages/session/helpers"
interface SessionContextUsageProps {
variant?: "button" | "indicator"
placement?: ComponentProps<typeof Tooltip>["placement"]
placement?: ComponentProps<typeof TooltipV2>["placement"]
}
function ContextTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
@@ -110,21 +111,24 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const circle = () => (
<div class="flex items-center justify-center">
<ProgressCircle
appearance="indicator"
size={16}
strokeWidth={2}
percentage={context()?.usage ?? 0}
style={{
"--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))",
}}
style={
variant() === "indicator"
? {
"--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>
)
const circleV2 = () => (
<div class="flex items-center justify-center">
<ProgressCircle appearance="compact" percentage={context()?.usage ?? 0} />
<ProgressCircleV2 percentage={context()?.usage ?? 0} />
</div>
)
@@ -141,11 +145,11 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
return (
<Show when={params.id}>
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
<TooltipV2 value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
<Show
when={variant() === "indicator"}
fallback={
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
@@ -157,7 +161,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
>
{circle()}
</Show>
</Tooltip>
</TooltipV2>
</Show>
)
}
@@ -1,5 +1,5 @@
import { Menu } from "@opencode-ai/ui/menu"
import { Icon } from "@opencode-ai/ui/icon"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createStore } from "solid-js/store"
import { createSignal, For, Show, type ComponentProps, type JSX } from "solid-js"
@@ -17,7 +17,7 @@ export function SessionWorkspaceMenu(props: {
sessionID: string
project: Project
directory: string
placement?: ComponentProps<typeof Menu>["placement"]
placement?: ComponentProps<typeof MenuV2>["placement"]
gutter?: number
class?: string
contentClass?: string
@@ -71,57 +71,57 @@ export function SessionWorkspaceMenu(props: {
}
return (
<Menu
<MenuV2
placement={props.placement ?? "bottom-end"}
gutter={props.gutter ?? 4}
modal={false}
onOpenChange={onOpenChange}
>
<Menu.Trigger class={props.class} disabled={blocked()}>
<MenuV2.Trigger class={props.class} disabled={blocked()}>
{props.children}
</Menu.Trigger>
<Menu.Portal>
<Menu.Content class={`w-[200px] ${props.contentClass ?? ""}`}>
<Menu.Group>
<Menu.GroupLabel>{language.t("workspace.move.menu.title")}</Menu.GroupLabel>
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content class={`w-[200px] ${props.contentClass ?? ""}`}>
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("workspace.move.menu.title")}</MenuV2.GroupLabel>
<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" />
{language.t("session.new.workspace.local")}
</Menu.Item>
</MenuV2.Item>
</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" />
{language.t("workspace.new")}
</Menu.Item>
</MenuV2.Item>
<Show when={workspaces().length > 0}>
<Menu.Sub gutter={0} overlap overflowPadding={8}>
<Menu.SubTrigger>
<MenuV2.Sub gutter={0} overlap overflowPadding={8}>
<MenuV2.SubTrigger>
<Icon name="workspace-isolated" />
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
</MenuV2.SubTrigger>
<MenuV2.Portal>
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
<For each={workspaces()}>
{(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" />
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
</Menu.Item>
</MenuV2.Item>
)}
</For>
</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
</MenuV2.SubContent>
</MenuV2.Portal>
</MenuV2.Sub>
</Show>
</Menu.Group>
<Menu.Separator class="h-[0.5px] bg-v2-border-border-base" />
<Menu.Item onSelect={() => openWorkspaces()}>
</MenuV2.Group>
<MenuV2.Separator class="h-[0.5px] bg-v2-border-border-base" />
<MenuV2.Item onSelect={() => openWorkspaces()}>
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
)
}
@@ -2,9 +2,10 @@ import { For, Show } from "solid-js"
import { AppIcon } from "@opencode-ai/ui/app-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Menu } from "@opencode-ai/ui/menu"
import { SplitButton, SplitButtonAction, SplitButtonMenuTrigger } from "@opencode-ai/ui/split-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
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 { type OpenApp, useOpenInApp } from "@/components/session/open-in-app"
@@ -14,13 +15,13 @@ export function OpenInAppV2(props: { directory: () => string }) {
return (
<Show when={props.directory() && state.canOpen()}>
<SplitButton class="session-review-v2-open-in-app" onPointerDown={(event) => event.stopPropagation()}>
<Tooltip
<SplitButtonV2 class="session-review-v2-open-in-app" onPointerDown={(event) => event.stopPropagation()}>
<TooltipV2
placement="bottom"
value={language.t("session.header.open.ariaLabel", { app: state.current().label })}
class="flex items-center"
>
<SplitButtonAction
<SplitButtonV2Action
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
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]" />}>
<Spinner class="size-3.5" />
</Show>
</SplitButtonAction>
</Tooltip>
<Menu
</SplitButtonV2Action>
</TooltipV2>
<MenuV2
gutter={4}
modal={false}
placement="bottom-end"
open={state.menu.open}
onOpenChange={(open) => state.setMenu("open", open)}
>
<Menu.Trigger
as={SplitButtonMenuTrigger}
<MenuV2.Trigger
as={SplitButtonV2MenuTrigger}
disabled={state.opening()}
aria-label={language.t("session.header.open.menu")}
onPointerDown={(event) => event.stopPropagation()}
>
<Icon name="chevron-down" size="small" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Content class="open-in-app-v2-menu">
<Menu.Group>
<Menu.GroupLabel>{language.t("session.header.openIn")}</Menu.GroupLabel>
<Menu.RadioGroup
<IconV2 name="chevron-down" size="small" />
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content class="open-in-app-v2-menu">
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("session.header.openIn")}</MenuV2.GroupLabel>
<MenuV2.RadioGroup
value={state.current().id}
onChange={(value) => {
state.selectApp(value as OpenApp)
@@ -62,7 +63,7 @@ export function OpenInAppV2(props: { directory: () => string }) {
>
<For each={state.options()}>
{(option) => (
<Menu.RadioItem
<MenuV2.RadioItem
value={option.id}
disabled={state.opening()}
onSelect={() => {
@@ -73,13 +74,13 @@ export function OpenInAppV2(props: { directory: () => string }) {
>
<AppIcon id={option.icon} />
{option.label}
</Menu.RadioItem>
</MenuV2.RadioItem>
)}
</For>
</Menu.RadioGroup>
</Menu.Group>
<Menu.Separator />
<Menu.Item
</MenuV2.RadioGroup>
</MenuV2.Group>
<MenuV2.Separator />
<MenuV2.Item
onSelect={() => {
state.setMenu("open", false)
state.copyPath()
@@ -87,11 +88,11 @@ export function OpenInAppV2(props: { directory: () => string }) {
>
<Icon name="copy" size="small" class="text-icon-weak" />
{language.t("session.header.open.copyPath")}
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</SplitButton>
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</SplitButtonV2>
</Show>
)
}
@@ -1,3 +1,4 @@
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { createMemo, Show } from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { Portal } from "solid-js/web"
@@ -6,10 +7,10 @@ import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { useSessionLayout } from "@/pages/session/session-layout"
import { StatusPopoverV2 } from "../status-popover"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
import { useTitlebarRightMount } from "../titlebar"
@@ -59,24 +60,24 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
return (
<div class="flex items-center gap-2">
<Show when={props.state.statusVisible}>
<Tooltip appearance="standard" placement="bottom" value={props.state.statusLabel}>
<Tooltip placement="bottom" value={props.state.statusLabel}>
<StatusPopoverV2 />
</Tooltip>
</Show>
<Show when={props.state.reviewVisible}>
<Tooltip
<TooltipV2
class="shrink-0"
placement="bottom"
value={
<>
{props.state.reviewLabel}
<Show when={props.state.reviewKeybind.length > 0}>
<Keybind keys={props.state.reviewKeybind} variant="neutral" />
<KeybindV2 keys={props.state.reviewKeybind} variant="neutral" />
</Show>
</>
}
>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
@@ -86,9 +87,9 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
aria-label={props.state.reviewLabel}
aria-expanded={props.state.reviewOpened}
aria-controls="review-panel"
icon={<Icon name="sidebar-right" />}
icon={<IconV2 name="sidebar-right" />}
/>
</Tooltip>
</TooltipV2>
</Show>
</div>
)
@@ -1,8 +1,9 @@
import { createMemo, Show } from "solid-js"
import type { JSX } from "solid-js"
import { useSortable } from "@dnd-kit/solid/sortable"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { IconButton } from "@opencode-ai/ui/icon-button"
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 { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
@@ -39,29 +40,31 @@ export function SortableTabV2(props: {
<div class="relative">
<Tabs.Trigger
value={props.tab}
onMiddleClick={() => props.onTabClose(props.tab)}
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
closeButton={
<Tooltip
<TooltipV2
value={
<>
{language.t("common.closeTab")}
<Show when={closeTabKeybind().length > 0}>
<Keybind keys={closeTabKeybind()} variant="neutral" />
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
</Show>
</>
}
placement="bottom"
gutter={10}
>
<Tabs.CloseButton
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => props.onTabClose(props.tab)}
aria-label={language.t("common.closeTab")}
/>
</Tooltip>
</TooltipV2>
}
hideCloseButton
onMiddleClick={() => props.onTabClose(props.tab)}
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
>
<Show when={content()}>{(value) => value()}</Show>
</Tabs.Trigger>
@@ -2,8 +2,9 @@ import type { JSX } from "solid-js"
import { Show, createEffect, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useSortable } from "@dnd-kit/solid/sortable"
import { IconButton } from "@opencode-ai/ui/icon-button"
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 { useTerminal, type LocalPTY } from "@/context/terminal"
import { useLanguage } from "@/context/language"
@@ -115,8 +116,8 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
return (
<div ref={sortable.ref} class="h-full flex items-center outline-none focus:outline-none focus-visible:outline-none">
<Menu.Context>
<Menu.Context.Trigger class="relative" as="div">
<MenuV2.Context>
<MenuV2.Context.Trigger class="relative" as="div">
<Tabs.Trigger
value={props.terminal.id}
onMouseDown={(e) => {
@@ -130,11 +131,20 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
if (e.detail > 0) return
focus()
}}
onMiddleClick={close}
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
onMiddleClick={close}
>
<span
class="truncate"
@@ -159,9 +169,9 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
/>
</div>
</Show>
</Menu.Context.Trigger>
<Menu.Context.Portal>
<Menu.Context.Content
</MenuV2.Context.Trigger>
<MenuV2.Context.Portal>
<MenuV2.Context.Content
onCloseAutoFocus={(e) => {
if (!editRequested) return
e.preventDefault()
@@ -169,11 +179,11 @@ export function SortableTerminalTabV2(props: { terminal: LocalPTY; index: number
requestAnimationFrame(() => edit())
}}
>
<Menu.Item onSelect={() => (editRequested = true)}>{language.t("common.rename")}</Menu.Item>
<Menu.Item onSelect={close}>{language.t("common.close")}</Menu.Item>
</Menu.Context.Content>
</Menu.Context.Portal>
</Menu.Context>
<MenuV2.Item onSelect={() => (editRequested = true)}>{language.t("common.rename")}</MenuV2.Item>
<MenuV2.Item onSelect={close}>{language.t("common.close")}</MenuV2.Item>
</MenuV2.Context.Content>
</MenuV2.Context.Portal>
</MenuV2.Context>
</div>
)
}
@@ -1,9 +1,9 @@
import { For, Show, createMemo, lazy, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Button } from "@opencode-ai/ui/button"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextInput } from "@opencode-ai/ui/text-input"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { showToast } from "@/utils/toast"
import fuzzysort from "fuzzysort"
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 { 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 PALETTE_ID = "command.palette"
@@ -387,12 +387,12 @@ function SettingsKeybindsV2View(props: {
<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>
</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")}
</Button>
</ButtonV2>
</div>
<div class="settings-v2-tab-search">
<TextInput
<TextInputV2
type="search"
appearance="base"
value={store.filter}
@@ -405,12 +405,12 @@ function SettingsKeybindsV2View(props: {
aria-label={language.t("settings.shortcuts.search.placeholder")}
/>
<Show when={store.filter}>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="small"
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", "")}
/>
</Show>
@@ -1,6 +1,6 @@
import { Component, createMemo } from "solid-js"
import { Select } from "@opencode-ai/ui/select"
import { TextInput } from "@opencode-ai/ui/text-input"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useLanguage } from "@/context/language"
import { ExternalLink } from "../external-link"
import { SettingsListV2 } from "./parts/list"
@@ -42,7 +42,7 @@ const FontSetting: Component<{
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<div class="w-full sm:w-[220px]">
<TextInput
<TextInputV2
data-action={config().action}
type="text"
appearance="base"
@@ -83,7 +83,8 @@ export const SettingsAppearanceV2: Component = () => {
title={language.t("settings.general.row.colorScheme.title")}
description={language.t("settings.general.row.colorScheme.description")}
>
<Select
<SelectV2
appearance="inline"
data-action="settings-color-scheme"
options={schemeOptions}
current={schemeOptions.find((option) => option === appearance.scheme.current())}
@@ -109,7 +110,8 @@ export const SettingsAppearanceV2: Component = () => {
</>
}
>
<Select
<SelectV2
appearance="inline"
data-action="settings-theme"
options={appearance.theme.options()}
current={appearance.theme.current()}
@@ -1,7 +1,7 @@
import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Divider } from "@opencode-ai/ui/divider"
import { TextInput } from "@opencode-ai/ui/text-input"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useMutation } from "@tanstack/solid-query"
import { type Component, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
@@ -69,12 +69,12 @@ export const DialogServerV2: Component<{
<DialogHeader hideClose={true}>
<DialogTitle>{title()}</DialogTitle>
</DialogHeader>
<Divider />
<DividerV2 />
<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-2">
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.url")}</label>
<TextInput
<TextInputV2
type="text"
appearance="large"
class="!w-full self-stretch"
@@ -92,7 +92,7 @@ export const DialogServerV2: Component<{
</div>
<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>
<TextInput
<TextInputV2
type="text"
appearance="large"
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="flex min-w-0 flex-col gap-2">
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.username")}</label>
<TextInput
<TextInputV2
type="text"
appearance="large"
class="!w-full self-stretch"
@@ -119,7 +119,7 @@ export const DialogServerV2: Component<{
</div>
<div class="flex min-w-0 flex-col gap-2">
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.password")}</label>
<TextInput
<TextInputV2
type="password"
appearance="large"
class="!w-full self-stretch"
@@ -134,12 +134,12 @@ export const DialogServerV2: Component<{
</div>
</DialogBody>
<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")}
</Button>
<Button variant="contrast" disabled={form.state.busy()} onClick={form.submit}>
</ButtonV2>
<ButtonV2 variant="contrast" disabled={form.state.busy()} onClick={form.submit}>
{submitLabel()}
</Button>
</ButtonV2>
</DialogFooter>
</Dialog>
)
@@ -1,6 +1,6 @@
import { Component, createEffect, createMemo, createSignal, startTransition } from "solid-js"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { Icon } from "@opencode-ai/ui/icon"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
@@ -73,66 +73,66 @@ export const DialogSettings: Component<{
return (
<Dialog size="x-large" variant="settings" class="settings-v2-dialog">
<Tabs
<TabsV2
orientation="vertical"
variant="settings"
value={tab()}
onChange={(value) => void startTransition(() => setTab(value))}
class="settings-v2"
>
<Tabs.List>
<TabsV2.List>
<div class="flex flex-col justify-between h-full w-full">
<div class="flex flex-col gap-4 w-full">
{/* Group 1: Preferences */}
<div class="flex flex-col gap-1 w-full">
<Tabs.Trigger value="general">
<TabsV2.Trigger value="general">
<Icon name="sliders" />
{language.t("settings.tab.preferences")}
</Tabs.Trigger>
<Tabs.Trigger value="appearance">
</TabsV2.Trigger>
<TabsV2.Trigger value="appearance">
<Icon name="appearance" />
{language.t("settings.general.section.appearance")}
</Tabs.Trigger>
<Tabs.Trigger value="notifications">
</TabsV2.Trigger>
<TabsV2.Trigger value="notifications">
<Icon name="notifications" />
{language.t("settings.tab.notifications")}
</Tabs.Trigger>
<Tabs.Trigger value="shortcuts">
</TabsV2.Trigger>
<TabsV2.Trigger value="shortcuts">
<Icon name="keyboard" />
{language.t("settings.tab.shortcuts")}
</Tabs.Trigger>
</TabsV2.Trigger>
</div>
{/* Group 2: Environment & Workspaces */}
<div class="flex flex-col gap-1 w-full">
<Tabs.Trigger value="servers">
<TabsV2.Trigger value="servers">
<Icon name="server" />
{language.t("status.popover.tab.servers")}
</Tabs.Trigger>
<Tabs.Trigger value="projects">
</TabsV2.Trigger>
<TabsV2.Trigger value="projects">
<Icon name="folder" />
{language.t("settings.tab.projects")}
</Tabs.Trigger>
<Tabs.Trigger value="workspaces">
</TabsV2.Trigger>
<TabsV2.Trigger value="workspaces">
<Icon name="workspace-isolated" />
{language.t("settings.tab.workspaces")}
</Tabs.Trigger>
</TabsV2.Trigger>
</div>
{/* Group 3: Capabilities & Extensions */}
<div class="flex flex-col gap-1 w-full">
<Tabs.Trigger value="providers">
<TabsV2.Trigger value="providers">
<Icon name="providers" />
{language.t("settings.providers.title")}
</Tabs.Trigger>
<Tabs.Trigger value="models">
</TabsV2.Trigger>
<TabsV2.Trigger value="models">
<Icon name="models" />
{language.t("settings.models.title")}
</Tabs.Trigger>
<Tabs.Trigger value="extensions">
</TabsV2.Trigger>
<TabsV2.Trigger value="extensions">
<Icon name="extensions" />
{language.t("settings.tab.extensions")}
</Tabs.Trigger>
</TabsV2.Trigger>
</div>
</div>
@@ -141,41 +141,41 @@ export const DialogSettings: Component<{
<span>v{platform.version}</span>
</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} />
</Tabs.Content>
<Tabs.Content value="appearance" class="settings-v2-panel">
</TabsV2.Content>
<TabsV2.Content value="appearance" class="settings-v2-panel">
<SettingsAppearanceV2 />
</Tabs.Content>
<Tabs.Content value="notifications" class="settings-v2-panel">
</TabsV2.Content>
<TabsV2.Content value="notifications" class="settings-v2-panel">
<SettingsNotificationsV2 />
</Tabs.Content>
<Tabs.Content value="shortcuts" class="settings-v2-panel">
</TabsV2.Content>
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
<SettingsKeybinds />
</Tabs.Content>
<Tabs.Content value="servers" class="settings-v2-panel">
</TabsV2.Content>
<TabsV2.Content value="servers" class="settings-v2-panel">
<SettingsServersV2 />
</Tabs.Content>
<Tabs.Content value="projects" class="settings-v2-panel">
</TabsV2.Content>
<TabsV2.Content value="projects" class="settings-v2-panel">
<SettingsProjectsV2 />
</Tabs.Content>
</TabsV2.Content>
<SettingsServerScope directory={directory()}>
<Tabs.Content value="workspaces" class="settings-v2-panel">
<TabsV2.Content value="workspaces" class="settings-v2-panel">
<SettingsWorkspacesV2 activeDirectory={directory()} />
</Tabs.Content>
<Tabs.Content value="providers" class="settings-v2-panel">
</TabsV2.Content>
<TabsV2.Content value="providers" class="settings-v2-panel">
<SettingsProvidersV2 directory={directory()} onBack={showProviders} />
</Tabs.Content>
<Tabs.Content value="models" class="settings-v2-panel">
</TabsV2.Content>
<TabsV2.Content value="models" class="settings-v2-panel">
<SettingsModelsV2 />
</Tabs.Content>
<Tabs.Content value="extensions" class="settings-v2-panel">
</TabsV2.Content>
<TabsV2.Content value="extensions" class="settings-v2-panel">
<SettingsExtensionsV2 />
</Tabs.Content>
</TabsV2.Content>
</SettingsServerScope>
</Tabs>
</TabsV2>
</Dialog>
)
}
@@ -1,12 +1,11 @@
import { Component, For, createEffect, createMemo, createResource } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { useLanguage } from "@/context/language"
import { useData } from "@/context/server"
import { useServerSDK } from "@/context/server-sdk"
import { useMcpToggle } from "@/context/mcp"
import { pluginLabel } from "@/utils/plugin"
import { ExternalLink } from "../external-link"
import { InlineServerSelect } from "./parts/server-select"
import "./settings-v2.css"
@@ -45,9 +44,7 @@ export const SettingsExtensionsV2: Component = () => {
() => serverSdk.connection.status() === "connected",
() => serverSdk.api.plugin.list().then((result) => result.data),
)
const plugins = createMemo<PluginRowItem[]>(() =>
(pluginList.latest ?? []).map((item) => ({ name: pluginLabel(item) })),
)
const plugins = createMemo<PluginRowItem[]>(() => (pluginList.latest ?? []).map((item) => ({ name: item.id })))
createEffect(() => {
if (serverSdk.connection.status() !== "connected") return
@@ -68,14 +65,14 @@ export const SettingsExtensionsV2: Component = () => {
</div>
<div class="settings-v2-tab-body">
<Tabs variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
<Tabs.List>
<Tabs.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</Tabs.Trigger>
<Tabs.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</Tabs.Trigger>
<Tabs.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</Tabs.Trigger>
</Tabs.List>
<TabsV2 variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
<TabsV2.List>
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
</TabsV2.List>
<Tabs.Content value="mcps">
<TabsV2.Content value="mcps">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
@@ -99,9 +96,9 @@ export const SettingsExtensionsV2: Component = () => {
</For>
</div>
</div>
</Tabs.Content>
</TabsV2.Content>
<Tabs.Content value="plugins">
<TabsV2.Content value="plugins">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
@@ -122,9 +119,9 @@ export const SettingsExtensionsV2: Component = () => {
</For>
</div>
</div>
</Tabs.Content>
</TabsV2.Content>
<Tabs.Content value="skills">
<TabsV2.Content value="skills">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
@@ -150,8 +147,8 @@ export const SettingsExtensionsV2: Component = () => {
</For>
</div>
</div>
</Tabs.Content>
</Tabs>
</TabsV2.Content>
</TabsV2>
</div>
</>
)
@@ -1,9 +1,9 @@
import { Component, Show, createMemo, createResource } from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { Button } from "@opencode-ai/ui/button"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { TextInput } from "@opencode-ai/ui/text-input"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useUpdaterAction } from "../updater-action"
@@ -100,7 +100,8 @@ const WorkspaceDestinationSetting: Component = () => {
title={language.t("settings.workspaces.default.title")}
description={language.t("settings.workspaces.default.description")}
>
<Select
<SelectV2
appearance="inline"
options={options()}
current={options().find((option) => option.value === settings.workspaces.defaultDestination())}
value={(option) => option.value}
@@ -126,7 +127,8 @@ const ShellSetting: Component<{ controller: ShellSettingsController }> = (props)
title={language.t("settings.general.row.shell.title")}
description={language.t("settings.general.row.shell.description")}
>
<Select
<SelectV2
appearance="inline"
data-action="settings-shell"
options={options()}
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")}
description={language.t("settings.general.row.colorScheme.description")}
>
<Select
<SelectV2
appearance="inline"
data-action="settings-color-scheme"
options={schemeOptions}
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"
options={props.controller.theme.options()}
current={props.controller.theme.current()}
@@ -209,7 +213,7 @@ const FontSetting: Component<{
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<div class="w-full sm:w-[220px]">
<TextInput
<TextInputV2
data-action={config().action}
type="text"
appearance="base"
@@ -250,7 +254,8 @@ const SoundSetting: Component<{
const config = () => soundSettings[props.kind]
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<Select
<SelectV2
appearance="inline"
data-action={config().action}
options={soundOptions}
current={props.channel.current()}
@@ -278,7 +283,8 @@ const LanguageSetting = () => {
title={language.t("settings.general.row.language.title")}
description={language.t("settings.general.row.language.description")}
>
<Select
<SelectV2
appearance="inline"
data-action="settings-language"
options={options()}
placement="bottom-end"
@@ -494,9 +500,9 @@ export const SettingsGeneral: Component<{
title={language.t("settings.updates.row.check.title")}
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)}
</Button>
</ButtonV2>
</SettingsRowV2>
</SettingsListV2>
</div>
@@ -1,9 +1,9 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Switch } from "@opencode-ai/ui/switch"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextInput } from "@opencode-ai/ui/text-input"
import { Switch } from "@opencode-ai/ui/v2/switch-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 { type Component, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
@@ -62,7 +62,7 @@ export const SettingsModelsV2: Component = () => {
<InlineServerSelect />
</div>
<div class="settings-v2-tab-search">
<TextInput
<TextInputV2
type="search"
appearance="base"
value={list.filter()}
@@ -75,12 +75,12 @@ export const SettingsModelsV2: Component = () => {
aria-label={language.t("dialog.model.search.placeholder")}
/>
<Show when={list.filter()}>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="small"
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()}
/>
</Show>
@@ -1,6 +1,6 @@
import { Component } from "solid-js"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { SettingsListV2 } from "./parts/list"
@@ -34,7 +34,8 @@ const SoundSetting: Component<{
const config = () => soundSettings[props.kind]
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<Select
<SelectV2
appearance="inline"
data-action={config().action}
options={soundOptions}
current={props.channel.current()}
@@ -1,5 +1,5 @@
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 { ServerConnection, serverName } from "@/context/servers"
@@ -20,7 +20,8 @@ export const InlineServerSelect: Component<{
return (
<Show when={options().length > 1}>
<Select
<SelectV2
appearance="inline"
data-action="settings-server-select"
options={options()}
current={current()}
@@ -1,7 +1,7 @@
import { Component, For, Show, createMemo, createSignal } from "solid-js"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
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>
</div>
<div class="flex items-center gap-2 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
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) => {
event.stopPropagation()
openProjectSettings(props.project, props.server)
@@ -1,5 +1,5 @@
import { Button } from "@opencode-ai/ui/button"
import { Badge } from "@opencode-ai/ui/badge"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { showToast } from "@/utils/toast"
@@ -160,7 +160,7 @@ export const SettingsProvidersV2: Component<{
/>
<div class="settings-v2-provider-main">
<span class="settings-v2-provider-name truncate">{item.name}</span>
<Badge>{type(item)}</Badge>
<Tag>{type(item)}</Tag>
</div>
</div>
<Show
@@ -171,9 +171,9 @@ export const SettingsProvidersV2: Component<{
</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")}
</Button>
</ButtonV2>
</Show>
</div>
)}
@@ -199,7 +199,7 @@ export const SettingsProvidersV2: Component<{
<div class="settings-v2-provider-main">
<span class="settings-v2-provider-name">{item.name}</span>
<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>
</div>
<Show when={note(item.id)}>
@@ -207,9 +207,9 @@ export const SettingsProvidersV2: Component<{
</Show>
</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")}
</Button>
</ButtonV2>
</div>
)}
</For>
@@ -226,14 +226,14 @@ export const SettingsProvidersV2: Component<{
<div class="settings-v2-provider-copy">
<div class="settings-v2-provider-main">
<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>
<p class="settings-v2-provider-description">
{language.t("settings.providers.custom.description")}
</p>
</div>
</div>
<Button
<ButtonV2
size="normal"
variant="neutral"
icon="plus"
@@ -246,7 +246,7 @@ export const SettingsProvidersV2: Component<{
}}
>
{language.t("common.connect")}
</Button>
</ButtonV2>
</div>
</Show>
</SettingsListV2>
@@ -1,7 +1,7 @@
import { Badge } from "@opencode-ai/ui/badge"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextInput } from "@opencode-ai/ui/text-input"
import { Tag } from "@opencode-ai/ui/v2/badge-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 { useDialog } from "@opencode-ai/ui/context/dialog"
import fuzzysort from "fuzzysort"
import { type Component, For, Show, createMemo } from "solid-js"
@@ -61,7 +61,7 @@ export const SettingsServersV2: Component = () => {
</div>
<Show when={showSearch()}>
<div class="settings-v2-tab-search">
<TextInput
<TextInputV2
type="search"
appearance="base"
value={store.filter}
@@ -74,12 +74,12 @@ export const SettingsServersV2: Component = () => {
aria-label={language.t("dialog.server.search.placeholder")}
/>
<Show when={store.filter}>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="small"
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", "")}
/>
</Show>
@@ -126,7 +126,7 @@ export const SettingsServersV2: Component = () => {
</div>
<div class="settings-v2-servers-actions">
<Show when={controller.defaults.available() && isDefault()}>
<Badge>{language.t("dialog.server.status.default")}</Badge>
<Tag>{language.t("dialog.server.status.default")}</Tag>
</Show>
<ServerRowMenu server={item} domain={controller} onEdit={openEdit} />
</div>
@@ -1,5 +1,5 @@
@import "@opencode-ai/ui/text-input.css";
@import "@opencode-ai/ui/button.css";
@import "@opencode-ai/ui/v2/text-input-v2.css";
@import "@opencode-ai/ui/v2/button-v2.css";
[data-component="tabs-v2"][data-variant="settings"] {
height: 100%;
@@ -3,12 +3,12 @@ import { For, Show, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import type { SessionInfo } from "@opencode-ai/client/promise"
import { useQuery } from "@tanstack/solid-query"
import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogFooter, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogFooter, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/v2/dialog-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
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 { useDialog } from "@opencode-ai/ui/context/dialog"
import { getFilename } from "@opencode-ai/util/path"
import { useLanguage } from "@/context/language"
@@ -273,33 +273,33 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
</span>
<div class="settings-v2-workspaces-toolbar-actions">
<Show when={projects().length > 1}>
<Menu placement="bottom-end" gutter={6}>
<Menu.Trigger class="flex h-6 max-w-48 items-center gap-1 rounded-sm px-2 text-13-medium hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed">
<MenuV2 placement="bottom-end" gutter={6}>
<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">
{projectOptions().find((option) => option.id === selectedProject())?.label}
</span>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</Menu.Trigger>
<Menu.Portal>
<Menu.Content>
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content>
<For each={projectOptions()}>
{(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>
<Show when={selectedProject() === option.id}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</Menu.Item>
</MenuV2.Item>
)}
</For>
</Menu.Content>
</Menu.Portal>
</Menu>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Show>
<Show when={filtered().length > 0}>
<Menu placement="bottom-end" gutter={4}>
<Menu.Trigger
as={IconButton}
<MenuV2 placement="bottom-end" gutter={4}>
<MenuV2.Trigger
as={IconButtonV2}
type="button"
variant="ghost-muted"
size="small"
@@ -307,16 +307,16 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
disabled={!!store.transaction}
icon={<Icon name="outline-dots" size="small" />}
/>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={confirmDeleteAll}>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={confirmDeleteAll}>
<span class="settings-v2-workspaces-delete-all">
{language.t("settings.workspaces.deleteAll")}
</span>
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Show>
</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-copy">
<div class="settings-v2-workspaces-main">
<Tooltip
<TooltipV2
value={workspace.directory}
placement="top-start"
contentClass="max-w-[calc(100vw-32px)] break-all"
@@ -348,21 +348,24 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
>
{workspace.directory}
</span>
</Tooltip>
</TooltipV2>
</div>
<span class="settings-v2-workspaces-meta">{sessionCount(workspace)}</span>
</div>
<div class="settings-v2-workspaces-row-actions">
<Show when={lastActive(workspace)}>
{(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">
{value()}
</span>
</Tooltip>
</TooltipV2>
)}
</Show>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="small"
@@ -425,12 +428,12 @@ function DialogDeleteAllWorkspaces(props: { count: number; project: string; onDe
/>
</DialogHeader>
<DialogFooter>
<Button type="button" variant="neutral" onClick={() => dialog.close()}>
<ButtonV2 type="button" variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</Button>
<Button type="button" variant="danger" onClick={remove}>
</ButtonV2>
<ButtonV2 type="button" variant="danger" onClick={remove}>
{language.t("settings.workspaces.deleteAll")}
</Button>
</ButtonV2>
</DialogFooter>
</Dialog>
)
@@ -483,17 +486,17 @@ function DialogDeleteWorkspace(props: {
/>
</DialogHeader>
<DialogFooter>
<Button type="button" variant="neutral" onClick={() => dialog.close()}>
<ButtonV2 type="button" variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</Button>
<Button
</ButtonV2>
<ButtonV2
type="button"
variant="danger"
disabled={status.isPending || status.isError || status.data?.result.active}
onClick={remove}
>
{language.t("workspace.delete.button")}
</Button>
</ButtonV2>
</DialogFooter>
</Dialog>
)
@@ -6,7 +6,6 @@ import { useMcpToggle } from "@/context/mcp"
import { useWorkspaceLocation } from "@/context/location"
import { useData } from "@/context/server"
import { useServerSDK } from "@/context/server-sdk"
import { pluginLabel } from "@/utils/plugin"
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
const parts = value.split(file)
@@ -39,7 +38,7 @@ export function StatusPopoverBody(props: { shown: boolean }) {
() => (props.shown ? sdk().directory : undefined),
(directory) => serverSDK.api.plugin.list({ location: { directory } }).then((result) => result.data),
)
const plugins = createMemo(() => (pluginList.latest ?? []).map(pluginLabel))
const plugins = createMemo(() => (pluginList.latest ?? []).map((item) => item.id))
const pluginCount = createMemo(() => plugins().length)
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
@@ -48,9 +47,10 @@ export function StatusPopoverBody(props: { shown: boolean }) {
<Tabs
aria-label={language.t("status.popover.ariaLabel")}
class="tabs bg-background-strong rounded-xl overflow-hidden"
data-component="tabs"
data-active="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.Trigger value="mcp" data-slot="tab" class="text-12-regular">
@@ -110,7 +110,6 @@ export function StatusPopoverBody(props: { shown: boolean }) {
</span>
<div onClick={(event) => event.stopPropagation()}>
<Switch
appearance="standard"
checked={enabled()}
disabled={toggleMcp.isPending && toggleMcp.variables === name}
onChange={() => {
@@ -1,6 +1,7 @@
import { Button } from "@opencode-ai/ui/button"
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 { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
import { useLanguage } from "@/context/language"
@@ -156,7 +157,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
<Popover
open={props.state.shown}
onOpenChange={props.state.onOpenChange}
triggerAs={IconButton}
triggerAs={IconButtonV2}
triggerProps={{
variant: "ghost-muted",
size: "large",
@@ -166,7 +167,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
}}
trigger={
<div class="relative size-4">
<Icon name={props.state.shown ? "status-active" : "status"} />
<IconV2 name={props.state.shown ? "status-active" : "status"} />
<div
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 { createResizeObserver } from "@solid-primitives/resize-observer"
import { createMutation } from "@tanstack/solid-query"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { useGlobal, useServerCtx } from "@/context/global"
import { useLanguage } from "@/context/language"
import { ServerConnection, serverName, useServers } from "@/context/servers"
@@ -281,7 +281,7 @@ export function TabNavItem(props: {
</a>
<div data-slot="tab-close">
<IconButton
<IconButtonV2
size="small"
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"
@@ -290,7 +290,7 @@ export function TabNavItem(props: {
event.stopPropagation()
}}
onClick={closeTab}
icon={<Icon name="xmark-small" />}
icon={<IconV2 name="xmark-small" />}
/>
</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]"
>
<span class="flex size-4 shrink-0 items-center justify-center">
<Icon name="edit" />
<IconV2 name="edit" />
</span>
<span
data-titlebar-tab-title
@@ -387,7 +387,7 @@ export function DraftTabItem(props: {
</span>
</a>
<div data-slot="tab-close">
<IconButton
<IconButtonV2
size="small"
variant="ghost-muted"
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"
onClick={closeTab}
icon={<Icon name="xmark-small" />}
icon={<IconV2 name="xmark-small" />}
aria-label={language.t("common.closeTab")}
/>
</div>
+14 -14
View File
@@ -12,10 +12,10 @@ import {
} from "solid-js"
import { createStore } from "solid-js/store"
import { useLocation, useNavigate } from "@solidjs/router"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { LayoutRoute, useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
@@ -330,28 +330,28 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
<Show when={windows() || linux()}>
<WindowsAppMenu command={command} platform={platform} />
</Show>
<Tooltip
<TooltipV2
placement="bottom"
value={
<>
{language.t("home.title")}
<Keybind keys={command.keybindParts("home.toggle")} variant="neutral" />
<KeybindV2 keys={command.keybindParts("home.toggle")} variant="neutral" />
</>
}
class="shrink-0"
>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
icon={<Icon name="grid-plus" />}
icon={<IconV2 name="grid-plus" />}
state={layout.route().type === "home" ? "pressed" : undefined}
onClick={toggleHome}
aria-label={language.t("home.title")}
aria-pressed={layout.route().type === "home"}
/>
</Tooltip>
</TooltipV2>
<TitlebarTabStrip
tabs={tabsStore}
@@ -368,25 +368,25 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
}}
onReorder={(keys) => tabsStoreActions.reorder(keys)}
/>
<Tooltip
<TooltipV2
placement="bottom"
value={
<>
{language.t("command.session.new")}
<Keybind keys={newTabTooltipKeybind(command)} variant="neutral" />
<KeybindV2 keys={newTabTooltipKeybind(command)} variant="neutral" />
</>
}
>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="shrink-0"
icon={<Icon name="plus" />}
icon={<IconV2 name="plus" />}
onClick={openNewTab}
aria-label={language.t("command.session.new")}
/>
</Tooltip>
</TooltipV2>
<div class="flex-1" />
<TitlebarV2Right state={v2RightState()} />
</div>
@@ -1,7 +1,8 @@
import { For, type JSX } from "solid-js"
import { Menu } from "@opencode-ai/ui/menu"
import { For, Show, type JSX } from "solid-js"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
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 { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
@@ -46,32 +47,32 @@ export function WindowsAppMenu(props: {
}
return (
<Menu appearance="standard" gutter={4} modal={false} placement="bottom-start">
<DropdownMenu gutter={4} modal={false} placement="bottom-start">
<div
data-component="desktop-icon-button"
class="flex h-7 w-9 shrink-0 items-center justify-center rounded-[6px] px-1"
>
<Menu.Trigger
as={IconButton}
<DropdownMenu.Trigger
as={IconButtonV2}
variant="ghost-muted"
size="large"
icon={<Icon name="menu" />}
icon={<IconV2 name="menu" />}
aria-label={language.t("desktop.menu.ariaLabel")}
onPointerDown={rememberFocus}
onKeyDown={rememberFocus}
/>
</div>
<Menu.Portal>
<Menu.Content class="desktop-app-menu">
<Menu.Group>
<Menu.GroupLabel class="desktop-app-menu-heading">OpenCode</Menu.GroupLabel>
<DropdownMenu.Portal>
<DropdownMenu.Content class="desktop-app-menu">
<DropdownMenu.Group>
<DropdownMenu.GroupLabel class="desktop-app-menu-heading">OpenCode</DropdownMenu.GroupLabel>
<For each={DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows"))}>
{(menu) => (
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
<For each={menu.items?.filter((entry) => desktopMenuVisible(entry, "windows"))}>
{(entry) => {
// 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 (
<DesktopMenuItem
label={entry.labelKey ? language.t(entry.labelKey) : ""}
@@ -85,28 +86,36 @@ export function WindowsAppMenu(props: {
</DesktopMenuSubmenu>
)}
</For>
</Menu.Group>
</Menu.Content>
</Menu.Portal>
</Menu>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
)
}
function DesktopMenuSubmenu(props: { label: string; children: JSX.Element }) {
return (
<Menu.Sub>
<Menu.SubTrigger>{props.label}</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent class="desktop-app-menu desktop-app-menu-sub">{props.children}</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
<span data-slot="dropdown-menu-item-label">{props.label}</span>
<span data-slot="desktop-app-menu-chevron">
<Icon name="chevron-right" size="small" />
</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 }) {
return (
<Menu.Item disabled={props.disabled} onSelect={props.onSelect} shortcut={props.keybind}>
{props.label}
</Menu.Item>
<DropdownMenu.Item disabled={props.disabled} onSelect={props.onSelect}>
<DropdownMenu.ItemLabel>{props.label}</DropdownMenu.ItemLabel>
<Show when={props.keybind}>
<span data-slot="desktop-app-menu-keybind">{props.keybind}</span>
</Show>
</DropdownMenu.Item>
)
}
+1 -1
View File
@@ -11,7 +11,7 @@ import { decode64 } from "@/utils/base64"
import { same } from "@/utils/same"
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
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 { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
import { requireServerKey } from "@/utils/session-route"
+21 -10
View File
@@ -1,6 +1,6 @@
@import "@opencode-ai/ui/styles/tailwind";
@import "@opencode-ai/session-ui/styles";
@import "@opencode-ai/ui/styles/tokens";
@import "@opencode-ai/ui/v2/styles/tailwind.css";
@import "tw-animate-css";
@font-face {
@@ -30,22 +30,23 @@
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;
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;
}
[data-component="menu-v2-content"].desktop-app-menu-sub {
[data-component="dropdown-menu-sub-content"].desktop-app-menu {
width: max-content;
min-width: 240px;
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;
align-items: center;
height: 28px;
@@ -56,7 +57,10 @@
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;
padding: 0 12px;
gap: 8px;
@@ -64,11 +68,12 @@
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;
}
[data-component="menu-v2-content"].desktop-app-menu [data-slot="menu-v2-item-shortcut"] {
[data-slot="desktop-app-menu-keybind"] {
margin-left: auto;
color: var(--text-weak);
font-size: var(--font-size-x-small);
@@ -76,13 +81,19 @@
white-space: nowrap;
}
[data-slot="desktop-app-menu-chevron"] {
display: flex;
margin-left: auto;
color: var(--icon-base);
}
[data-component="getting-started-actions"] {
display: flex;
flex-direction: column;
gap: 0.75rem; /* gap-3 */
}
[data-component="getting-started-actions"] > [data-component="button-v2"] {
[data-component="getting-started-actions"] > [data-component="button"] {
width: 100%;
}
@@ -92,7 +103,7 @@
align-items: center;
}
[data-component="getting-started-actions"] > [data-component="button-v2"] {
[data-component="getting-started-actions"] > [data-component="button"] {
width: auto;
}
}
@@ -6,11 +6,11 @@ import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/d
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers"
import { RestrictToElement } from "@dnd-kit/dom/modifiers"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
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 { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/context/layout"
import { ServerConnection } from "@/context/servers"
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="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)}>
<Tooltip placement="bottom" value={props.language.t("home.project.add")}>
<IconButton
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
<IconButtonV2
data-action="home-add-project"
variant="ghost-muted"
size="large"
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}
onClick={() => props.onChooseProject(props.servers[0])}
aria-label={props.language.t("home.project.add")}
/>
</Tooltip>
</TooltipV2>
</Show>
</div>
<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"
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>
</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"
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>
</HomeProjectNavButton>
</div>
@@ -234,7 +234,7 @@ function HomeServerRow(props: {
}}
onPointerDown={(event) => event.preventDefault()}
>
<Icon
<IconV2
name="chevron-down"
size="small"
class="transition-transform duration-150 ease-in-out"
@@ -280,17 +280,17 @@ function HomeServerRow(props: {
open={props.contextMenuOpen(contextMenuID())}
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
/>
<Tooltip class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
<IconButton
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
<IconButtonV2
data-action="home-add-project"
variant="ghost-muted"
size="small"
icon={<Icon name="folder-add-left" />}
icon={<IconV2 name="folder-add-left" />}
aria-label={props.language.t("home.project.add")}
disabled={props.health?.healthy === false}
onClick={() => props.onChooseProject(props.server)}
/>
</Tooltip>
</TooltipV2>
</div>
</div>
)
@@ -394,7 +394,7 @@ function HomeProjectEmpty(
disabled={unreachable()}
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>
</HomeProjectNavButton>
<Show when={props.items.length > 0}>
@@ -423,7 +423,7 @@ function HomeRecentlyClosedRow(
return worktree
}
return (
<Tooltip placement="right" value={path()}>
<TooltipV2 placement="right" value={path()}>
<HomeProjectNavButton
type="button"
data-component="home-recently-closed-row"
@@ -434,7 +434,7 @@ function HomeRecentlyClosedRow(
<HomeProjectAvatar project={props.project} outline />
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
</HomeProjectNavButton>
</Tooltip>
</TooltipV2>
)
}
@@ -523,55 +523,55 @@ function HomeProjectRow(
`}
data-menu={props.contextMenuOpen(contextMenuID())}
>
<Menu
<MenuV2
gutter={6}
modal={false}
placement="bottom-end"
open={props.contextMenuOpen(contextMenuID())}
onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)}
>
<Menu.Trigger
as={IconButton}
<MenuV2.Trigger
as={IconButtonV2}
data-action="home-project-menu"
variant="ghost-muted"
size="small"
icon={<Icon name="outline-dots" />}
icon={<IconV2 name="outline-dots" />}
aria-label={props.language.t("common.moreOptions")}
/>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
{props.language.t("command.session.new")}
</Menu.Item>
<Menu.Item onSelect={() => props.onEditProject(props.server, props.project)}>
</MenuV2.Item>
<MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
{props.language.t("dialog.project.edit.title")}
</Menu.Item>
</MenuV2.Item>
<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(
fileManagerApp(platform.platform === "desktop" ? (platform.os ?? "unknown") : "unknown")
.actionLabel,
)}
</Menu.Item>
</MenuV2.Item>
</Show>
<Menu.Item
<MenuV2.Item
disabled={props.unseen === 0}
onSelect={() => props.onClearNotifications(props.server, props.project)}
>
{props.language.t("sidebar.project.clearNotifications")}
</Menu.Item>
<Menu.Separator />
<Menu.Item onSelect={() => props.onCloseProject(props.server, props.project.worktree)}>
</MenuV2.Item>
<MenuV2.Separator />
<MenuV2.Item onSelect={() => props.onCloseProject(props.server, props.project.worktree)}>
{props.language.t("common.close")}
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
<IconButton
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
<IconButtonV2
data-action="home-project-new-session"
variant="ghost-muted"
size="small"
icon={<Icon name="edit" />}
icon={<IconV2 name="edit" />}
aria-label={props.language.t("command.session.new")}
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 { Spinner } from "@opencode-ai/ui/spinner"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { useLanguage } from "@/context/language"
import { ServerConnection } from "@/context/servers"
import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar"
@@ -83,7 +83,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
<Suspense>
<Show when={props.groups.length > 0 && props.canCreateSession}>
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
<Button
<ButtonV2
data-action="home-new-session"
variant="ghost-muted"
size="normal"
@@ -92,7 +92,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
onClick={props.onCreateSession}
>
{props.language.t("command.session.new")}
</Button>
</ButtonV2>
</div>
</Show>
</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
`}
>
<Icon name="magnifying-glass" />
<IconV2 name="magnifying-glass" />
<input
ref={props.onSetSearchInput}
class={`
@@ -317,12 +317,12 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
}}
/>
<Show when={props.searchValue}>
<IconButton
<IconButtonV2
type="button"
variant="ghost-muted"
size="small"
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}
onClick={() => {
props.onSearchClose()
@@ -458,12 +458,12 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
group-hover/session:opacity-100 focus-within:opacity-100
`}
>
<Tooltip class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
<IconButton
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
<IconButtonV2
data-action="home-session-archive"
variant="ghost-muted"
size="large"
icon={<Icon name="archive" />}
icon={<IconV2 name="archive" />}
aria-label={props.language.t("common.archive")}
onClick={(event) => {
event.preventDefault()
@@ -471,7 +471,7 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
void props.onArchiveSession(props.record.session)
}}
/>
</Tooltip>
</TooltipV2>
</div>
</Show>
</div>
@@ -525,9 +525,9 @@ function HomeSessionsEmpty(props: { onNewSession?: () => void; language: ReturnT
</p>
<Show when={props.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")}
</Button>
</ButtonV2>
)}
</Show>
</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 { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
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 { Show } from "solid-js"
@@ -1,7 +1,8 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Icon } from "@opencode-ai/ui/icon"
import { Wordmark } from "@opencode-ai/ui/wordmark"
import { Icon } from "@opencode-ai/ui/v2/icon"
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 { createStore } from "solid-js/store"
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={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">
<PromptInputV2Composer controller={props.input} accentSubmit={props.workspace.selection.workspace()} />
<Show when={props.project.empty()}>
@@ -95,7 +96,7 @@ export function NewSessionStatus(props: { mount: HTMLElement | null; visible: bo
{(mount) => (
<Portal mount={mount}>
<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 />
</Tooltip>
</Show>
@@ -152,7 +153,7 @@ function ProviderTip() {
<Icon name="chevron-down" size="small" class="-rotate-90" />
</span>
</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"
placement="top"
openDelay={1000}
@@ -166,7 +167,7 @@ function ProviderTip() {
>
<Icon name="xmark-small" />
</button>
</Tooltip>
</TooltipV2>
</div>
</div>
</Show>
@@ -21,8 +21,8 @@ export function useNewSessionCommands(input: {
title: language.t("command.palette"),
hidden: true,
onSelect: async () => {
const { DialogCommandPaletteV2 } = await import("@/components/dialog-command-palette-v2")
void dialog.show(() => <DialogCommandPaletteV2 />)
const { DialogSelectFile } = await import("@/components/dialog-select-file")
void dialog.show(() => <DialogSelectFile />)
},
},
{
+26 -6
View File
@@ -30,9 +30,10 @@ import { createStore } from "solid-js/store"
import type { SessionReviewLineComment } from "@opencode-ai/session-ui/session-review"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
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 { 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 { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
import { showToast } from "@/utils/toast"
@@ -194,9 +195,9 @@ function SessionErrorFallback(props: { error: unknown; sessionID?: string; serve
</div>
)}
</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")}
</Button>
</ButtonV2>
</div>
</div>
)
@@ -1038,6 +1039,25 @@ export default function Page() {
return (
<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()}
current={reviewMode()}
label={changesLabel}
@@ -1141,7 +1161,7 @@ export default function Page() {
// updates such as session switches.
const reviewPanelV2Props = () => ({
get title() {
return changesTitle()
return changesTitleV2()
},
get empty() {
return reviewEmptyV2()
@@ -1732,22 +1752,22 @@ export default function Page() {
>
<Tabs.Trigger
value="session"
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
classList={{
"!w-1/2 !max-w-none": true,
"!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")}
>
{language.t("session.tab.session")}
</Tabs.Trigger>
<Tabs.Trigger
value="changes"
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
classList={{
"!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,
}}
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
onClick={() => setStore("mobileTab", "changes")}
>
{hasReview()
@@ -1,6 +1,6 @@
import { useLanguage } from "@/context/language"
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 { createStore } from "solid-js/store"
import { SessionComposerPullout } from "./session-composer-pullout"
@@ -52,7 +52,7 @@ export function SessionBackgroundDock(props: {
<span>
<span class="text-v2-text-text-muted">{moving()}</span>
<span class="pl-2">
<Keybind keys={command.keybindParts("session.background")} variant="neutral" />
<KeybindV2 keys={command.keybindParts("session.background")} variant="neutral" />
</span>
</span>
)}
@@ -1,5 +1,4 @@
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { createResizeObserver } from "@solid-primitives/resize-observer"
@@ -108,7 +107,7 @@ export function SessionComposerPullout(props: {
<IconButton
data-action={`session-${props.name}-toggle-button`}
data-collapsed={props.collapsed ? "true" : "false"}
icon={<Icon name="chevron-down" />}
icon="chevron-down"
size="normal"
variant="ghost"
style={{ transform: `rotate(${value() * 180}deg)` }}
@@ -3,7 +3,6 @@ import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { DockTray } from "@opencode-ai/ui/dock-surface"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { useLanguage } from "@/context/language"
export function SessionFollowupDock(props: {
@@ -49,7 +48,7 @@ export function SessionFollowupDock(props: {
<div class="ml-auto shrink-0">
<IconButton
data-collapsed={store.collapsed ? "true" : "false"}
icon={<Icon name="chevron-down" />}
icon="chevron-down"
size="normal"
variant="ghost"
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>
<Button
size="small"
variant="neutral"
variant="secondary"
class="shrink-0"
disabled={!!props.sending}
onClick={() => props.onSend(item.id)}
@@ -38,14 +38,14 @@ export function SessionPermissionDock(props: {
{language.t("ui.permission.deny")}
</Button>
<Button
variant="neutral"
variant="secondary"
size="normal"
onClick={() => props.onDecide("always")}
disabled={props.responding}
>
{language.t("ui.permission.allowAlways")}
</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")}
</Button>
</div>
@@ -2,7 +2,6 @@ import { For, Show, createEffect, createMemo, onCleanup, onMount, type Component
import { createStore } from "solid-js/store"
import { useMutation } from "@tanstack/solid-query"
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 { Icon } from "@opencode-ai/ui/icon"
import { useSpring } from "@opencode-ai/ui/motion-spring"
@@ -500,14 +499,19 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () =>
</For>
</div>
</Show>
<IconButton
icon={<Icon name="chevron-down" size="small" />}
variant="ghost"
<button
type="button"
data-component="icon-button"
data-icon="chevron-down"
data-size="normal"
data-variant="ghost"
disabled={sending()}
style={{ transform: `rotate(${hidden() * 180}deg)` }}
onClick={store.minimized ? restore : minimize}
aria-label={language.t(store.minimized ? "session.question.restore" : "session.question.minimize")}
/>
>
<Icon name="chevron-down" size="small" />
</button>
</div>
</>
}
@@ -518,12 +522,12 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () =>
</Button>
<div data-slot="question-footer-actions">
<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")}
</Button>
</Show>
<Button
variant={last() ? "contrast" : "neutral"}
variant={last() ? "primary" : "secondary"}
size="large"
disabled={sending()}
onClick={next}
@@ -1,8 +1,8 @@
import { For, Show, createEffect, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { useLanguage } from "@/context/language"
export function SessionRevertDock(props: {
@@ -45,7 +45,7 @@ export function SessionRevertDock(props: {
onClick={toggle}
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
classList={{
"font-[440] shrink-0 cursor-default text-[13px] leading-5 tracking-[-0.04px]": true,
@@ -61,8 +61,8 @@ export function SessionRevertDock(props: {
</span>
</Show>
<div class="ml-auto shrink-0">
<IconButton
icon={<Icon name="outline-chevron-down" size="small" />}
<IconButtonV2
icon={<IconV2 name="outline-chevron-down" size="small" />}
size="large"
variant="ghost-muted"
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">
{item.text}
</span>
<Button
<ButtonV2
size="small"
variant="neutral"
class="shrink-0"
@@ -103,7 +103,7 @@ export function SessionRevertDock(props: {
onClick={() => props.onRestore(item.id)}
>
{language.t("session.revertDock.restore")}
</Button>
</ButtonV2>
</div>
)}
</For>
+15 -15
View File
@@ -7,8 +7,8 @@ import { useFileComponent } from "@opencode-ai/ui/context/file"
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2"
import { sampledChecksum } from "@opencode-ai/util/encode"
import { LineCommentOverflowIcon } from "@opencode-ai/ui/line-comment"
import { Menu } from "@opencode-ai/ui/menu"
import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { Tabs } from "@opencode-ai/ui/tabs"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { showToast } from "@/utils/toast"
@@ -26,7 +26,7 @@ type SessionFileViewProps = {
const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions"
function FileCommentMenu(props: {
function FileCommentMenuV2(props: {
moreLabel: string
editLabel: string
deleteLabel: string
@@ -35,17 +35,17 @@ function FileCommentMenu(props: {
}) {
return (
<div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
<Menu gutter={4}>
<Menu.Trigger as="button" type="button" data-slot="line-comment-v2-overflow" aria-label={props.moreLabel}>
<LineCommentOverflowIcon />
</Menu.Trigger>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={props.onEdit}>{props.editLabel}</Menu.Item>
<Menu.Item onSelect={props.onDelete}>{props.deleteLabel}</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
<MenuV2 gutter={4}>
<MenuV2.Trigger as="button" type="button" data-slot="line-comment-v2-overflow" aria-label={props.moreLabel}>
<LineCommentV2OverflowIcon />
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={props.onEdit}>{props.editLabel}</MenuV2.Item>
<MenuV2.Item onSelect={props.onDelete}>{props.deleteLabel}</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</div>
)
}
@@ -331,7 +331,7 @@ export function SessionFileView(props: SessionFileViewProps) {
},
editSubmitLabel: language.t("common.save"),
renderCommentActions: (_, controls) => (
<FileCommentMenu
<FileCommentMenuV2
moreLabel={language.t("common.moreOptions")}
editLabel={language.t("common.edit")}
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 { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
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 FileTree from "@/components/file-tree"
@@ -312,27 +313,30 @@ export function SessionSidePanel(props: {
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
onMiddleClick={() => tabs().close("context")}
closeButton={
<Tooltip
<TooltipV2
value={
<>
{language.t("common.closeTab")}
<Show when={closeTabKeybind().length > 0}>
<Keybind keys={closeTabKeybind()} variant="neutral" />
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
</Show>
</>
}
placement="bottom"
gutter={10}
>
<Tabs.CloseButton
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => tabs().close("context")}
aria-label={language.t("common.closeTab")}
/>
</Tooltip>
</TooltipV2>
}
hideCloseButton
onMiddleClick={() => tabs().close("context")}
>
<div class="flex items-center gap-2">
<SessionContextUsage variant="indicator" />
@@ -356,27 +360,30 @@ export function SessionSidePanel(props: {
>
<Tabs.Trigger
value={SESSION_OPEN_FILE_TAB}
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
closeButton={
<Tooltip
<TooltipV2
value={
<>
{language.t("common.closeTab")}
<Show when={closeTabKeybind().length > 0}>
<Keybind keys={closeTabKeybind()} variant="neutral" />
<KeybindV2 keys={closeTabKeybind()} variant="neutral" />
</Show>
</>
}
placement="bottom"
gutter={10}
>
<Tabs.CloseButton
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
aria-label={language.t("common.closeTab")}
/>
</Tooltip>
</TooltipV2>
}
hideCloseButton
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
>
<div class="flex items-center gap-1.5 italic">
<Icon name="open-file" size="small" />
@@ -387,26 +394,26 @@ export function SessionSidePanel(props: {
)}
</For>
<div class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
<Tooltip
<TooltipV2
value={
<>
{language.t("command.file.open")}
<Show when={openFileKeybind().length > 0}>
<Keybind keys={openFileKeybind()} variant="neutral" />
<KeybindV2 keys={openFileKeybind()} variant="neutral" />
</Show>
</>
}
placement="bottom"
class="flex items-center"
>
<IconButton
<IconButtonV2
icon={<Icon name="plus-small" />}
variant="ghost-muted"
size="large"
onClick={() => openFileBrowser()}
aria-label={language.t("command.file.open")}
/>
</Tooltip>
</TooltipV2>
</div>
</Tabs.List>
<div
@@ -496,7 +503,7 @@ export function SessionSidePanel(props: {
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
>
<Tabs
variant="surface"
variant="pill"
value={fileTreeTab()}
onChange={setFileTreeTabValue}
class="h-full"
@@ -10,9 +10,8 @@ import { RestrictToElement } from "@dnd-kit/dom/modifiers"
import { Tabs } from "@opencode-ai/ui/tabs"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Keybind } from "@opencode-ai/ui/keybind"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { SortableTerminalTabV2 } from "@/components/session/session-sortable-terminal-tab-v2"
import { Terminal } from "@/components/terminal"
@@ -250,7 +249,7 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
>
<div class="flex flex-col h-full">
<Tabs
variant="panel"
variant="normal"
value={terminal.active()}
onChange={(id) => terminal.open(id)}
class="!h-[52px] !flex-none"
@@ -267,12 +266,12 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
{(pty, index) => <SortableTerminalTabV2 terminal={pty} index={index()} onClose={close} />}
</For>
<div class="h-full flex items-center justify-center">
<Tooltip
<TooltipV2
value={
<>
{language.t("command.terminal.new")}
<Show when={newTerminalKeybind().length > 0}>
<Keybind keys={newTerminalKeybind()} variant="neutral" />
<KeybindV2 keys={newTerminalKeybind()} variant="neutral" />
</Show>
</>
}
@@ -280,12 +279,13 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
class="flex items-center"
>
<IconButton
icon={<Icon name="plus-small" size="large" />}
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => terminal.new({ focus: true })}
aria-label={language.t("command.terminal.new")}
/>
</Tooltip>
</TooltipV2>
</div>
</Tabs.List>
</Tabs>
@@ -1,6 +1,6 @@
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
import { DialogFooter, DialogHeader, DialogTitleGroup, Dialog } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { useNavigate } from "@solidjs/router"
import { createEffect, createMemo, on } from "solid-js"
import { createStore } from "solid-js/store"
@@ -186,7 +186,7 @@ export function createTimelineController(input: { session: TimelineSessionSource
dialog.close()
}
return (
<Dialog fit>
<DialogV2 fit>
<DialogHeader hideClose>
<DialogTitleGroup
title={language.t("session.delete.title")}
@@ -194,14 +194,14 @@ export function createTimelineController(input: { session: TimelineSessionSource
/>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => dialog.close()}>
<ButtonV2 variant="ghost" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</Button>
<Button variant="danger" onClick={confirm}>
</ButtonV2>
<ButtonV2 variant="danger" onClick={confirm}>
{language.t("session.delete.button")}
</Button>
</ButtonV2>
</DialogFooter>
</Dialog>
</DialogV2>
)
}
@@ -15,13 +15,13 @@ import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualIt
import { Card } from "@opencode-ai/ui/card"
import { MessageDivider, SessionShellMessage, type UserActions } from "@opencode-ai/session-ui/message-part"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
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 { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import { Button } from "@opencode-ai/ui/button"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
import { isScrollKeyTarget, scrollKey, scrollKeyOwner, ScrollView } from "@opencode-ai/ui/scroll-view"
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"
}
>
<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>
</SessionWorkspaceMenu>
<button
@@ -154,7 +154,7 @@ function WorkspaceMoveAction(props: {
props.onDismiss()
}}
>
<Icon name="xmark-small" />
<IconV2 name="xmark-small" />
</button>
</div>
)
@@ -203,12 +203,12 @@ function SessionSummaryPanel(props: {
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`}
>
<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>
<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>
<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
when={props.branch}
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`}
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>}>
{(diffs) => (
<Show when={diffs().length > 0} fallback={<span>{language.t("session.review.noChanges")}</span>}>
<span>{language.plural("ui.sessionTurn.diffs.changed", diffs().length)}</span>
<span class="text-v2-text-text-muted">·</span>
<DiffChanges appearance="standard" changes={diffs()} />
<DiffChanges changes={diffs()} />
</Show>
)}
</Show>
@@ -1175,11 +1175,11 @@ function MessageTimelineView(
when={workspaceSession()}
fallback={
<span class="flex size-6 shrink-0 items-center justify-center text-v2-icon-icon-muted">
<Icon name="monitor" />
<IconV2 name="monitor" />
</span>
}
>
<Tooltip
<TooltipV2
placement="bottom-start"
value={sessionDirectory()}
contentClass="max-w-[calc(100vw-32px)] break-all"
@@ -1189,9 +1189,9 @@ function MessageTimelineView(
aria-label={sessionDirectory()}
class="flex size-6 shrink-0 items-center justify-center text-v2-icon-icon-accent"
>
<Icon name="workspace-isolated" />
<IconV2 name="workspace-isolated" />
</span>
</Tooltip>
</TooltipV2>
</Show>
<Show when={parentID()}>
<button
@@ -1268,8 +1268,8 @@ function MessageTimelineView(
onOpenChange={setSummary}
>
<KobaltePopover.Trigger
as={IconButton}
icon={<Icon name="window-analytics" />}
as={IconButtonV2}
icon={<IconV2 name="window-analytics" />}
variant="ghost-muted"
size="large"
state={summaryOpen() ? "pressed" : undefined}
@@ -1300,7 +1300,7 @@ function MessageTimelineView(
)}
</Show>
<Show when={!parentID()}>
<Menu
<MenuV2
gutter={6}
placement="bottom-end"
open={title.menuOpen}
@@ -1309,9 +1309,9 @@ function MessageTimelineView(
if (open) return
}}
>
<Menu.Trigger
as={IconButton}
icon={<Icon name="outline-dots" />}
<MenuV2.Trigger
as={IconButtonV2}
icon={<IconV2 name="outline-dots" />}
variant="ghost-muted"
size="large"
state={share.open || title.pendingShare ? "pressed" : undefined}
@@ -1321,8 +1321,8 @@ function MessageTimelineView(
more = el
}}
/>
<Menu.Portal>
<Menu.Content
<MenuV2.Portal>
<MenuV2.Content
style={{ width: "120px", "min-width": "120px" }}
onCloseAutoFocus={(event) => {
if (title.pendingRename) {
@@ -1340,34 +1340,34 @@ function MessageTimelineView(
}
}}
>
<Menu.Item
<MenuV2.Item
onSelect={() => {
setTitle("pendingRename", true)
setTitle("menuOpen", false)
}}
>
{language.t("common.rename")}
</Menu.Item>
</MenuV2.Item>
<Show when={shareEnabled()}>
<Menu.Item
<MenuV2.Item
onSelect={() => {
setTitle({ pendingShare: true, menuOpen: false })
}}
>
{language.t("session.share.action.share")}...
</Menu.Item>
</MenuV2.Item>
</Show>
<Menu.Item onSelect={() => void props.action.export(id)}>
<MenuV2.Item onSelect={() => void props.action.export(id)}>
{language.t("common.export")}...
</Menu.Item>
</MenuV2.Item>
{/* TODO: Need a V2 session archive API. */}
<Menu.Separator />
<Menu.Item onSelect={() => props.action.showDelete(id)}>
<MenuV2.Separator />
<MenuV2.Item onSelect={() => props.action.showDelete(id)}>
{language.t("common.delete")}...
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
<KobaltePopover
open={share.open}
@@ -1415,7 +1415,7 @@ function MessageTimelineView(
<Show
when={shareUrl()}
fallback={
<Button
<ButtonV2
variant="contrast"
class="w-full"
onClick={() => void props.action.share()}
@@ -1424,7 +1424,7 @@ function MessageTimelineView(
{props.pending.share()
? language.t("session.share.action.publishing")
: language.t("session.share.action.publish")}
</Button>
</ButtonV2>
}
>
<div class="flex flex-col gap-2">
@@ -1441,26 +1441,26 @@ function MessageTimelineView(
>
{shareUrl()}
</div>
<IconButton
<IconButtonV2
type="button"
size="small"
variant="ghost-muted"
icon={<Icon name="outline-copy" />}
icon={<IconV2 name="outline-copy" />}
aria-label={language.t("session.share.copy.copyLink")}
onClick={() => void props.action.copyShareUrl()}
/>
<IconButton
<IconButtonV2
type="button"
size="small"
variant="ghost-muted"
icon={<Icon name="outline-square-arrow" />}
icon={<IconV2 name="outline-square-arrow" />}
aria-label={language.t("session.share.action.view")}
onClick={props.action.viewShare}
disabled={props.pending.unshare()}
/>
</div>
<div class="flex w-full">
<Button
<ButtonV2
variant="outline"
class="w-full"
onClick={() => void props.action.unshare()}
@@ -1469,7 +1469,7 @@ function MessageTimelineView(
{props.pending.unshare()
? language.t("session.share.action.unpublishing")
: language.t("session.share.action.unpublish")}
</Button>
</ButtonV2>
</div>
</div>
</Show>
@@ -4,8 +4,9 @@ import type { SessionStatus } from "@opencode-ai/client/promise"
import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
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 { useI18n } from "@opencode-ai/ui/context"
const GO_UPSELL_FREE_TIER_LAST_SEEN_AT = "go_upsell_last_seen_at"
const GO_UPSELL_FREE_TIER_DONT_SHOW = "go_upsell_dont_show"
@@ -219,8 +219,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const openFile = () => {
void openDialog(
() => import("@/components/dialog-command-palette-v2"),
(x) => dialog.show(() => <x.DialogCommandPaletteV2 onOpenFile={showAllFiles} />),
() => import("@/components/dialog-select-file"),
(x) => dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />),
)
}
@@ -7,7 +7,7 @@ import {
SessionReviewV2Sidebar,
} from "@opencode-ai/session-ui/v2/session-review-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 {
SessionReviewComment,
SessionReviewCommentActions,
@@ -1,5 +1,5 @@
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 { createEffect, createMemo, createSignal, For, Show } from "solid-js"
import { kindChange, kindLabel, type Kind } from "@/components/file-tree-v2"
-8
View File
@@ -1,8 +0,0 @@
import type { PluginInfo } from "@opencode-ai/client"
export function pluginLabel(plugin: PluginInfo) {
if (plugin.id) return plugin.id
if (plugin.source.type === "package") return plugin.source.package
if (plugin.source.type === "local") return plugin.source.path
return plugin.source.type
}
+8 -13
View File
@@ -1,18 +1,15 @@
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
import { Toast, showToast, toaster, type ToastOptions } from "@opencode-ai/ui/toast"
type AppToastOptions = Omit<ToastOptions, "icon"> & {
icon?: IconProps["name"]
}
import type { ToastOptions, ToastVariant } from "@opencode-ai/ui/toast"
import { ToastV2, showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
export function ToastRegion() {
return <Toast.Region />
return <ToastV2.Region />
}
function showAppToast(options: AppToastOptions | string) {
if (typeof options === "string") return showToast(options)
export function showToast(options: ToastOptions | string) {
if (typeof options === "string") return showToastV2(options)
return showToast({
return showToastV2({
...options,
icon: resolveIcon(options.icon, options.variant),
actions: options.actions?.map((action) => ({
@@ -22,13 +19,11 @@ function showAppToast(options: AppToastOptions | string) {
})
}
export { showAppToast as showToast }
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)
if (!name) return
return <Icon name={name} />
+38 -33
View File
@@ -1,15 +1,15 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { Divider } from "@opencode-ai/ui/divider"
import { Loader } from "@opencode-ai/ui/loader"
import { RadioGroup, RadioItem } from "@opencode-ai/ui/radio"
import { TextInput } from "@opencode-ai/ui/text-input"
import { showToast } from "@opencode-ai/ui/toast"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2"
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 { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { showToast } from "@/utils/toast"
import { useWslAddServerProbes } from "./add-server-probes"
import { useWslServers } from "./context"
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>}
>
<div class="settings-v2-wsl-loading">
<Loader />
<LoaderV2 />
</div>
</Show>
</Dialog>
@@ -73,7 +73,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
>
<Dialog fit class="settings-v2-wsl-dialog">
<div class="settings-v2-wsl-loading">
<Loader />
<LoaderV2 />
</div>
</Dialog>
</Show>
@@ -85,13 +85,13 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
{controller.view() === "main" ? language.t("wsl.server.add") : language.t("wsl.onboarding.installDistro")}
</DialogTitle>
</DialogHeader>
<Divider />
<DividerV2 />
<Show
when={controller.view() === "main"}
fallback={
<>
<DialogBody class="settings-v2-wsl-dialog-body settings-v2-wsl-catalog-picker">
<TextInput
<TextInputV2
class="settings-v2-wsl-catalog-search"
appearance="large"
placeholder={language.t("wsl.onboarding.searchDistros")}
@@ -100,7 +100,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
onInput={(event) => controller.setCatalogSearch(event.currentTarget.value)}
/>
<div class="settings-v2-wsl-catalog-list">
<RadioGroup
<RadioGroupV2
hideLabel
class="settings-v2-wsl-distro-group"
label={language.t("wsl.onboarding.installDistro")}
@@ -110,7 +110,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
>
<For each={model().filteredInstallableDistros}>
{(item) => (
<RadioItem
<RadioItemV2
class="settings-v2-wsl-distro-row settings-v2-wsl-catalog-row"
value={item.name}
disabled={model().busy}
@@ -118,23 +118,23 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
/>
)}
</For>
</RadioGroup>
</RadioGroupV2>
</div>
</DialogBody>
<DialogFooter>
<Button variant="neutral" disabled={model().busy} onClick={controller.closeCatalog}>
<ButtonV2 variant="neutral" disabled={model().busy} onClick={controller.closeCatalog}>
{language.t("common.cancel")}
</Button>
<Button
</ButtonV2>
<ButtonV2
variant={model().installingCatalogDistro ? "loading" : "contrast"}
disabled={!model().installingCatalogDistro && (model().busy || !model().catalogTarget)}
style={{ width: "99px" }}
onClick={controller.installCatalogDistro}
>
<Show when={model().installingCatalogDistro} fallback={language.t("wsl.onboarding.installDistro")}>
<Loader />
<LoaderV2 />
</Show>
</Button>
</ButtonV2>
</DialogFooter>
</>
}
@@ -142,9 +142,14 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
<DialogBody class="settings-v2-wsl-dialog-body">
<div class="settings-v2-wsl-section-header">
<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")}
</Button>
</ButtonV2>
</div>
<Show
@@ -160,7 +165,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
}
>
<div class="settings-v2-wsl-distro-list">
<RadioGroup
<RadioGroupV2
hideLabel
class="settings-v2-wsl-distro-group"
label={language.t("wsl.onboarding.installedDistros")}
@@ -172,7 +177,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
{(item) => {
const status = () => model().distroStatuses[item.name] ?? null
return (
<RadioItem
<RadioItemV2
class={`settings-v2-wsl-distro-row${item.version === 1 ? " settings-v2-wsl-distro-row--unsupported" : ""}`}
value={item.name}
disabled={item.version === 1 || model().busy}
@@ -190,7 +195,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
)
}}
</For>
</RadioGroup>
</RadioGroupV2>
</div>
</Show>
@@ -225,19 +230,19 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
</DialogBody>
<DialogFooter>
<Button variant="neutral" disabled={controller.adding()} onClick={controller.close}>
<ButtonV2 variant="neutral" disabled={controller.adding()} onClick={controller.close}>
{language.t("common.cancel")}
</Button>
<Button
</ButtonV2>
<ButtonV2
variant={primaryButton().loading ? "loading" : primaryButton().variant}
disabled={!primaryButton().loading && primaryButton().disabled}
style={primaryButtonStyle()}
onClick={controller.runPrimary}
>
<Show when={primaryButton().loading} fallback={translate(language, primaryButton().label)}>
<Loader />
<LoaderV2 />
</Show>
</Button>
</ButtonV2>
</DialogFooter>
</Show>
</Dialog>
@@ -434,14 +439,14 @@ function DialogWslSetup(props: {
</Show>
</div>
<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")}
</Button>
</ButtonV2>
</Show>
<Show when={props.state !== "unavailable"}>
<Button variant="neutral" onClick={() => dialog.close()}>
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
{language.t("common.close")}
</Button>
</ButtonV2>
</Show>
</div>
</Dialog>
+42 -40
View File
@@ -1,9 +1,9 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Badge } from "@opencode-ai/ui/badge"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Menu } from "@opencode-ai/ui/menu"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { useMutation } from "@tanstack/solid-query"
import fuzzysort from "fuzzysort"
import { type Accessor, For, Show, createMemo } from "solid-js"
@@ -32,22 +32,22 @@ export function AddServerMenu(props: { onAddServer: () => void }) {
<Show
when={platform.wslServers}
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")}
</Button>
</ButtonV2>
}
>
<Menu gutter={4} modal={false} placement="bottom-end">
<Menu.Trigger as={Button} variant="ghost-muted" icon="plus">
<MenuV2 gutter={4} modal={false} placement="bottom-end">
<MenuV2.Trigger as={ButtonV2} variant="ghost-muted" icon="plus">
{language.t("dialog.server.add.button")}
</Menu.Trigger>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={props.onAddServer}>{language.t("dialog.server.add.button")}</Menu.Item>
<Menu.Item onSelect={openAddWsl}>{language.t("wsl.server.add")}</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={props.onAddServer}>{language.t("dialog.server.add.button")}</MenuV2.Item>
<MenuV2.Item onSelect={openAddWsl}>{language.t("wsl.server.add")}</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</Show>
)
}
@@ -113,52 +113,54 @@ export function WslServerSettings(props: {
</div>
<div class="settings-v2-servers-actions">
<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 when={opencodeAction()}>
{(label) => (
<Button
<ButtonV2
size="small"
disabled={busy() || request.isPending}
onClick={() => api && request.mutate(() => api.installOpencode(item.config.distro))}
>
{busy() ? language.t("wsl.server.updating") : language.t(label())}
</Button>
</ButtonV2>
)}
</Show>
<Menu gutter={4} modal={false} placement="bottom-end">
<Menu.Trigger
as={IconButton}
<MenuV2 gutter={4} modal={false} placement="bottom-end">
<MenuV2.Trigger
as={IconButtonV2}
variant="ghost-muted"
size="small"
icon={<Icon name="outline-dots" />}
icon={<IconV2 name="outline-dots" />}
aria-label={language.t("common.moreOptions")}
/>
<Menu.Portal>
<Menu.Content>
<Menu.Group>
<Menu.GroupLabel>{language.t("wsl.server.menu.label")}</Menu.GroupLabel>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("wsl.server.menu.label")}</MenuV2.GroupLabel>
<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")}
</Menu.Item>
</MenuV2.Item>
</Show>
<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")}
</Menu.Item>
</MenuV2.Item>
</Show>
<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")}
</Menu.Item>
</MenuV2.Item>
</Show>
<Menu.Separator />
<Menu.Item onSelect={() => remove(key)}>{language.t("dialog.server.menu.delete")}</Menu.Item>
</Menu.Group>
</Menu.Content>
</Menu.Portal>
</Menu>
<MenuV2.Separator />
<MenuV2.Item onSelect={() => remove(key)}>
{language.t("dialog.server.menu.delete")}
</MenuV2.Item>
</MenuV2.Group>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</div>
</div>
)
+19 -19
View File
@@ -1,44 +1,44 @@
import { beforeEach, describe, expect, test } from "bun:test"
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.
beforeEach(() => {
toaster.dismiss()
toasterV2.dismiss()
})
test("coalesces exact active content", () => {
const first = showToast({ title: "Repeated error", description: "Try again" })
const second = showToast({ title: "Repeated error", description: "Try again" })
const different = showToast({ title: "Repeated error", description: "A different error" })
const first = showToastV2({ title: "Repeated error", description: "Try again" })
const second = showToastV2({ title: "Repeated error", description: "Try again" })
const different = showToastV2({ title: "Repeated error", description: "A different error" })
expect(second).toBe(first)
expect(different).not.toBe(first)
toaster.dismiss(first)
toaster.dismiss(different)
toasterV2.dismiss(first)
toasterV2.dismiss(different)
})
test("allows dismissed content to appear again", () => {
const first = showToast("Dismiss and retry")
toaster.dismiss(first)
const first = showToastV2("Dismiss and retry")
toasterV2.dismiss(first)
const second = showToast("Dismiss and retry")
const second = showToastV2("Dismiss and retry")
expect(second).not.toBe(first)
toaster.dismiss(second)
toasterV2.dismiss(second)
})
test("recreates matching content when it is not the topmost toast", () => {
const first = showToast("First toast")
const topmost = showToast("Topmost toast")
const repeated = showToast("First toast")
const first = showToastV2("First toast")
const topmost = showToastV2("Topmost toast")
const repeated = showToastV2("First toast")
expect(repeated).not.toBe(first)
toaster.dismiss(topmost)
toaster.dismiss(repeated)
toasterV2.dismiss(topmost)
toasterV2.dismiss(repeated)
})
test("creates no reactive computations at call time", () => {
@@ -50,7 +50,7 @@ describe("showToast", () => {
return undefined
}) 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
// never disposed and tracks its dependencies forever; it must only resolve
@@ -59,6 +59,6 @@ describe("showToast", () => {
setTick(1)
expect(reads).toBe(0)
toaster.dismiss(id)
toasterV2.dismiss(id)
})
})
+1 -1
View File
@@ -42,8 +42,8 @@
"solid-js": "catalog:",
"tree-sitter-bash": "0.25.0",
"tree-sitter-powershell": "0.25.10",
"uqr": "0.1.3",
"web-tree-sitter": "0.25.10",
"uqr": "0.1.3",
"ws": "8.21.0"
},
"devDependencies": {
+1 -1
View File
@@ -109,7 +109,7 @@ for (const item of targets) {
external: ["node-gyp"],
format: "esm",
minify: true,
sourcemap: Script.channel === "dev" || Script.channel === "local" ? "inline" : "none",
sourcemap: "inline",
splitting: true,
compile: {
autoloadBunfig: false,
+12 -7
View File
@@ -1,9 +1,8 @@
#!/usr/bin/env bun
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect/service"
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
import { Effect, Schema } from "effect"
import { Schema } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
@@ -64,22 +63,28 @@ try {
})
if (unauthorizedOpenApi.status !== 401)
throw new Error("Compiled service exposed application routes without authentication")
const stopRoute = await fetch(new URL("/api/service/stop", info.url), {
const unauthorizedStop = await fetch(new URL("/api/service/stop", info.url), {
method: "POST",
headers: { ...headers, "content-type": "application/json" },
headers: { "content-type": "application/json" },
body: JSON.stringify({ instanceID: info.id }),
signal: AbortSignal.timeout(5_000),
})
if (stopRoute.status !== 404) throw new Error("Compiled service exposed the removed HTTP stop route")
if (unauthorizedStop.status !== 401) throw new Error("Compiled service accepted unauthenticated stop")
const winner = processes.find((process) => process.pid === info.pid)
const loser = processes.find((process) => process.pid !== info.pid)
if (!winner || !loser) throw new Error("Compiled contenders did not elect one registered owner")
if (!(await exitsWithin(loser, 10_000))) throw new Error("Losing compiled contender did not exit")
await Effect.runPromise(
Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)),
const stopped = await Schema.decodeUnknownPromise(ServiceStatus.StopResponse)(
await fetch(new URL("/api/service/stop", info.url), {
method: "POST",
headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({ instanceID: info.id }),
signal: AbortSignal.timeout(5_000),
}).then((response) => response.json()),
)
if (!stopped.accepted) throw new Error("Compiled service rejected exact-instance stop")
if (!(await exitsWithin(winner, 10_000))) throw new Error("Compiled service did not stop")
for (let attempt = 0; attempt < 200 && (await Bun.file(registration).exists()); attempt++) await Bun.sleep(25)
if (await Bun.file(registration).exists()) throw new Error("Compiled service registration was not removed")
+3 -24
View File
@@ -275,36 +275,15 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Spec.make("stop", { description: "Stop the background server" }),
Spec.make("get", {
description: "Get service configuration",
params: {
key: Argument.string("key").pipe(Argument.withDescription("Service setting or env"), Argument.optional),
name: Argument.string("name").pipe(
Argument.withDescription("Environment variable name"),
Argument.optional,
),
},
params: { key: Argument.string("key").pipe(Argument.optional) },
}),
Spec.make("set", {
description: "Set service configuration",
params: {
key: Argument.string("key").pipe(Argument.withDescription("Service setting or env")),
value: Argument.string("value").pipe(
Argument.withDescription("Setting value or environment variable name"),
),
nestedValue: Argument.string("env-value").pipe(
Argument.withDescription("Environment variable value"),
Argument.optional,
),
},
params: { key: Argument.string("key"), value: Argument.string("value") },
}),
Spec.make("unset", {
description: "Unset service configuration",
params: {
key: Argument.string("key").pipe(Argument.withDescription("Service setting or env")),
name: Argument.string("name").pipe(
Argument.withDescription("Environment variable name"),
Argument.optional,
),
},
params: { key: Argument.string("key") },
}),
],
}),
+1 -11
View File
@@ -4,7 +4,7 @@ import { run } from "@opencode-ai/tui"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Config } from "../../config"
import { Context, Effect, FileSystem, Option, Queue } from "effect"
import { Context, Effect, FileSystem, Option } from "effect"
import { ServerConnection } from "../../services/server-connection"
import { Updater } from "../../services/updater"
import { UpdatePreflight } from "../../services/update-preflight"
@@ -19,21 +19,11 @@ export default Runtime.handler(Commands, (input) =>
if (requestedDirectory !== undefined) process.chdir(requestedDirectory)
const preflight = UpdatePreflight.make()
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
const serviceStarts = yield* Queue.unbounded<{
readonly reason: "missing" | "version-mismatch"
readonly previousVersion?: string
}>()
yield* Queue.take(serviceStarts).pipe(
Effect.flatMap((event) => Effect.logInfo("background service starting", event)),
Effect.forever,
Effect.forkScoped,
)
const server = yield* ServerConnection.resolve({
server: requestedServer,
standalone: input.standalone,
mismatch: "replace",
onStart: (reason, previousVersion) => {
Queue.offerUnsafe(serviceStarts, { reason, previousVersion })
if (reason === "version-mismatch" && preflight.begin(previousVersion)) return
process.stderr.write(
reason === "version-mismatch"
@@ -1,6 +1,6 @@
import { EOL } from "node:os"
import { Effect } from "effect"
import { OpenCode, type PluginInfo } from "@opencode-ai/client"
import { OpenCode } from "@opencode-ai/client"
import { Service } from "@opencode-ai/client/effect/service"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
@@ -14,18 +14,11 @@ export default Runtime.handler(
const endpoint = found ?? (yield* Service.ensure(options))
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.plugin.list({ location: { directory: process.cwd() } }))
const plugins = response.data.toSorted((a, b) => name(a).localeCompare(name(b)))
const plugins = response.data.toSorted((a, b) => a.id.localeCompare(b.id))
if (plugins.length === 0) {
process.stdout.write("No plugins loaded" + EOL)
return
}
process.stdout.write(plugins.map(name).join(EOL) + EOL)
process.stdout.write(plugins.map((plugin) => plugin.id).join(EOL) + EOL)
}),
)
function name(plugin: PluginInfo) {
if (plugin.id) return plugin.id
if (plugin.source.type === "package") return plugin.source.package
if (plugin.source.type === "local") return plugin.source.path
return plugin.source.type
}
@@ -7,8 +7,6 @@ import { ServiceConfig } from "../../../services/service-config"
export default Runtime.handler(
Commands.commands.service.commands.get,
Effect.fn("cli.service.get")(function* (input) {
process.stdout.write(
(yield* ServiceConfig.get(Option.getOrUndefined(input.key), Option.getOrUndefined(input.name))) + EOL,
)
process.stdout.write((yield* ServiceConfig.get(Option.getOrUndefined(input.key))) + EOL)
}),
)

Some files were not shown because too many files have changed in this diff Show More