mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-29 06:31:55 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f77f5a343e |
@@ -88,7 +88,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={6}
|
||||
openDelay={0}
|
||||
delay="intent"
|
||||
contentStyle={{ "font-family": "var(--v2-font-family-sans)" }}
|
||||
value={
|
||||
<ModelTooltip
|
||||
|
||||
@@ -452,7 +452,7 @@ export function ModelSelectorPopoverV2(props: {
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={6}
|
||||
openDelay={0}
|
||||
delay="intent"
|
||||
value={
|
||||
<ModelTooltip
|
||||
model={item}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
||||
</span>
|
||||
}
|
||||
placement="top"
|
||||
openDelay={800}
|
||||
{...(!props.newLayoutDesigns ? { openDelay: 800 } : {})}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
|
||||
@@ -52,12 +52,7 @@ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (p
|
||||
<For each={props.comments ?? []}>
|
||||
{(item) => (
|
||||
<div class="relative group shrink-0">
|
||||
<TooltipV2
|
||||
value={item.comment}
|
||||
placement="top"
|
||||
openDelay={800}
|
||||
contentClass="max-w-[300px] break-words"
|
||||
>
|
||||
<TooltipV2 value={item.comment} placement="top" contentClass="max-w-[300px] break-words">
|
||||
<CommentCardV2
|
||||
comment={item.comment ?? ""}
|
||||
path={item.path}
|
||||
|
||||
@@ -103,7 +103,6 @@ describe("normalizeProviderList", () => {
|
||||
)
|
||||
|
||||
expect(result.connected).toEqual(["openai"])
|
||||
expect(result.defaultModel).toEqual({ providerID: "openai", modelID: "gpt-5" })
|
||||
expect(result.default).toEqual({ openai: "gpt-5" })
|
||||
expect(result.all.get("openai")?.models["gpt-old"]).toBeUndefined()
|
||||
expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({
|
||||
@@ -114,10 +113,6 @@ describe("normalizeProviderList", () => {
|
||||
variants: { high: {} },
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves an empty current default", () => {
|
||||
expect(normalizeProviderList([] as ProviderListOutput["data"], [], null).defaultModel).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("directoryKey", () => {
|
||||
|
||||
@@ -139,7 +139,6 @@ export function normalizeProviderList(
|
||||
return {
|
||||
all,
|
||||
connected: providers.map((provider) => provider.id),
|
||||
defaultModel: defaultModel ? { providerID: defaultModel.providerID, modelID: defaultModel.id } : null,
|
||||
default: Object.fromEntries(
|
||||
providers.flatMap((provider) => {
|
||||
const model =
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createStore } from "solid-js/store"
|
||||
import { useModels } from "@/context/models"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { resolveDefaultModel } from "@/hooks/provider-catalog"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
@@ -150,8 +149,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const configuredModel = () => {
|
||||
const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model)
|
||||
if (!model) return
|
||||
const configured = sync().data.config.model
|
||||
if (!configured) return
|
||||
const [providerID, modelID] = configured.split("/")
|
||||
const model = { providerID, modelID }
|
||||
if (validModel(model)) return model
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||
import { resolveDefaultModel, selectProviderCatalog } from "./provider-catalog"
|
||||
import { selectProviderCatalog } from "./provider-catalog"
|
||||
|
||||
const catalog = (id: string): NormalizedProviderListResponse => ({
|
||||
all: new Map([[id, { id, name: id, source: "api", env: [], options: {}, models: {} }]]),
|
||||
@@ -57,21 +57,3 @@ test("falls back to the global catalog for route consumers", () => {
|
||||
}),
|
||||
).toBe(global)
|
||||
})
|
||||
|
||||
test("uses the current server default model", () => {
|
||||
expect(resolveDefaultModel({ providerID: "openai", modelID: "gpt-5" }, "anthropic/claude")).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
})
|
||||
})
|
||||
|
||||
test("does not use legacy config when the current server has no default", () => {
|
||||
expect(resolveDefaultModel(null, "anthropic/claude")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("uses config for legacy servers", () => {
|
||||
expect(resolveDefaultModel(undefined, "anthropic/claude")).toEqual({
|
||||
providerID: "anthropic",
|
||||
modelID: "claude",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,13 +25,3 @@ export function selectProviderCatalog(input: ProviderCatalogInput) {
|
||||
if (input.explicit) return emptyProviderCatalog
|
||||
return input.global
|
||||
}
|
||||
|
||||
export function resolveDefaultModel(
|
||||
current: NormalizedProviderListResponse["defaultModel"],
|
||||
legacy: string | undefined,
|
||||
) {
|
||||
if (current !== undefined) return current ?? undefined
|
||||
if (!legacy) return undefined
|
||||
const [providerID, modelID] = legacy.split("/")
|
||||
return { providerID, modelID }
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ export function useProviders(directory: Accessor<string | undefined>) {
|
||||
return {
|
||||
all: () => providers().all,
|
||||
default: () => providers().default,
|
||||
defaultModel: () => providers().defaultModel,
|
||||
popular: () =>
|
||||
pipe(
|
||||
providers().all,
|
||||
|
||||
@@ -263,7 +263,6 @@ function ProviderTip(props: { ready: () => boolean; connected: () => boolean; op
|
||||
<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}
|
||||
value={language.t("common.dismiss")}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -6,7 +6,6 @@ import { usePrompt } from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { resolveDefaultModel } from "@/hooks/provider-catalog"
|
||||
|
||||
export function createPromptModelSelection(input: { agent: () => { model?: ModelKey; variant?: string } | undefined }) {
|
||||
const sdk = useSDK()
|
||||
@@ -22,8 +21,10 @@ export function createPromptModelSelection(input: { agent: () => { model?: Model
|
||||
}
|
||||
|
||||
const configured = () => {
|
||||
const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model)
|
||||
if (!model) return
|
||||
const value = sync().data.config.model
|
||||
if (!value) return
|
||||
const [providerID, modelID] = value.split("/")
|
||||
const model = { providerID, modelID }
|
||||
if (valid(model)) return model
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,6 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
|
||||
export type NormalizedProviderListResponse = {
|
||||
all: Map<string, Provider>
|
||||
defaultModel?: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
} | null
|
||||
default: {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ export function CommentCardV2(props: {
|
||||
return (
|
||||
<TooltipV2
|
||||
placement="top"
|
||||
openDelay={1000}
|
||||
value={props.title ?? props.comment}
|
||||
disabled={!props.tooltip || !truncated()}
|
||||
class={props.wide ? "w-full" : undefined}
|
||||
|
||||
@@ -385,12 +385,7 @@ export function PromptInputV2Attachments(props: {
|
||||
<For each={props.comments ?? []}>
|
||||
{(comment) => (
|
||||
<div class="relative group shrink-0">
|
||||
<TooltipV2
|
||||
value={comment.comment}
|
||||
placement="top"
|
||||
openDelay={800}
|
||||
contentClass="max-w-[300px] break-words"
|
||||
>
|
||||
<TooltipV2 value={comment.comment} placement="top" contentClass="max-w-[300px] break-words">
|
||||
<CommentCardV2
|
||||
comment={comment.comment ?? ""}
|
||||
path={comment.path}
|
||||
|
||||
@@ -214,7 +214,6 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
</Show>
|
||||
<div class="flex items-center">
|
||||
<TooltipV2
|
||||
openDelay={2000}
|
||||
inactive={!prev()}
|
||||
value={
|
||||
<>
|
||||
@@ -234,7 +233,6 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
/>
|
||||
</TooltipV2>
|
||||
<TooltipV2
|
||||
openDelay={2000}
|
||||
inactive={!next()}
|
||||
value={
|
||||
<>
|
||||
@@ -268,12 +266,12 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
|
||||
aria-label={i18n.t("ui.sessionReviewV2.expandMode")}
|
||||
>
|
||||
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
|
||||
<TooltipV2 value={i18n.t("ui.sessionReviewV2.showAllLines")}>
|
||||
<SegmentedControlItemV2 value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
|
||||
<Icon name="expand" />
|
||||
</SegmentedControlItemV2>
|
||||
</TooltipV2>
|
||||
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
|
||||
<TooltipV2 value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
|
||||
<SegmentedControlItemV2 value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
|
||||
<Icon name="collapse" />
|
||||
</SegmentedControlItemV2>
|
||||
@@ -289,12 +287,12 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
|
||||
aria-label={i18n.t("ui.sessionReviewV2.diffView")}
|
||||
>
|
||||
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
|
||||
<TooltipV2 value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
|
||||
<SegmentedControlItemV2 value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
|
||||
<Icon name="unified" />
|
||||
</SegmentedControlItemV2>
|
||||
</TooltipV2>
|
||||
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
|
||||
<TooltipV2 value={i18n.t("ui.sessionReviewV2.splitDiff")}>
|
||||
<SegmentedControlItemV2 value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
|
||||
<Icon name="split" />
|
||||
</SegmentedControlItemV2>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
const OPEN_DELAY = 1_000
|
||||
|
||||
// Kobalte warms every tooltip globally. Keep intent previews isolated so opening
|
||||
// a model picker never inherits warm state from an unrelated tooltip.
|
||||
let warm = false
|
||||
let reset: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
export function openTooltipIntent(open: () => void) {
|
||||
clearTimeout(reset)
|
||||
reset = undefined
|
||||
if (warm) {
|
||||
open()
|
||||
return
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
warm = true
|
||||
open()
|
||||
}, OPEN_DELAY)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
|
||||
export function closeTooltipIntent() {
|
||||
clearTimeout(reset)
|
||||
// Adjacent triggers enter before the next task; leaving the group does not.
|
||||
reset = setTimeout(() => {
|
||||
warm = false
|
||||
reset = undefined
|
||||
})
|
||||
}
|
||||
|
||||
export function resetTooltipIntent() {
|
||||
clearTimeout(reset)
|
||||
reset = undefined
|
||||
warm = false
|
||||
}
|
||||
@@ -2,19 +2,22 @@ import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip"
|
||||
import { createEffect, Match, onCleanup, splitProps, Switch, type JSX } from "solid-js"
|
||||
import type { ComponentProps } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { closeTooltipIntent, openTooltipIntent, resetTooltipIntent } from "./tooltip-intent"
|
||||
import "./tooltip-v2.css"
|
||||
|
||||
export interface TooltipV2Props extends ComponentProps<typeof KobalteTooltip> {
|
||||
export interface TooltipV2Props extends Omit<ComponentProps<typeof KobalteTooltip>, "openDelay"> {
|
||||
value: JSX.Element
|
||||
class?: string
|
||||
contentClass?: string
|
||||
contentStyle?: JSX.CSSProperties
|
||||
inactive?: boolean
|
||||
delay?: "standard" | "intent"
|
||||
forceOpen?: boolean
|
||||
}
|
||||
|
||||
export function TooltipV2(props: TooltipV2Props) {
|
||||
let ref: HTMLDivElement | undefined
|
||||
let cancelIntent: (() => void) | undefined
|
||||
const [state, setState] = createStore({
|
||||
open: false,
|
||||
block: false,
|
||||
@@ -26,19 +29,37 @@ export function TooltipV2(props: TooltipV2Props) {
|
||||
"contentClass",
|
||||
"contentStyle",
|
||||
"inactive",
|
||||
"delay",
|
||||
"forceOpen",
|
||||
"ignoreSafeArea",
|
||||
"value",
|
||||
])
|
||||
|
||||
const close = () => setState("open", false)
|
||||
|
||||
const inside = () => {
|
||||
const active = document.activeElement
|
||||
if (!ref || !active) return false
|
||||
return ref.contains(active)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
cancelIntent?.()
|
||||
cancelIntent = undefined
|
||||
if (local.delay === "intent") closeTooltipIntent()
|
||||
setState("open", false)
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
if (local.delay !== "intent" || inside()) {
|
||||
setState("open", true)
|
||||
return
|
||||
}
|
||||
if (cancelIntent) return
|
||||
cancelIntent = openTooltipIntent(() => {
|
||||
cancelIntent = undefined
|
||||
setState("open", true)
|
||||
})
|
||||
}
|
||||
|
||||
const drop = (expand = state.expand) => {
|
||||
if (expand) return
|
||||
if (ref?.matches(":hover")) return
|
||||
@@ -80,6 +101,11 @@ export function TooltipV2(props: TooltipV2Props) {
|
||||
onCleanup(() => obs.disconnect())
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
cancelIntent?.()
|
||||
if (local.delay === "intent") resetTooltipIntent()
|
||||
})
|
||||
|
||||
let justClickedTrigger = false
|
||||
|
||||
return (
|
||||
@@ -88,8 +114,8 @@ export function TooltipV2(props: TooltipV2Props) {
|
||||
<Match when={true}>
|
||||
<KobalteTooltip
|
||||
gutter={4}
|
||||
openDelay={400}
|
||||
skipDelayDuration={300}
|
||||
openDelay={local.delay === "intent" ? 0 : 400}
|
||||
skipDelayDuration={local.delay === "intent" ? 0 : 300}
|
||||
{...others}
|
||||
closeDelay={0}
|
||||
ignoreSafeArea={local.ignoreSafeArea ?? true}
|
||||
@@ -101,7 +127,11 @@ export function TooltipV2(props: TooltipV2Props) {
|
||||
justClickedTrigger = false
|
||||
return
|
||||
}
|
||||
setState("open", open)
|
||||
if (open) {
|
||||
show()
|
||||
return
|
||||
}
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<KobalteTooltip.Trigger
|
||||
|
||||
Reference in New Issue
Block a user