Compare commits

..
Author SHA1 Message Date
Aiden Cline c335873950 fix(provider): omit deprecated Gemini sampling defaults 2026-07-26 04:31:39 +00:00
29 changed files with 860 additions and 794 deletions
+682 -516
View File
File diff suppressed because it is too large Load Diff
@@ -88,7 +88,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
class="w-full"
placement="right-start"
gutter={6}
delay="intent"
openDelay={0}
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}
delay="intent"
openDelay={0}
value={
<ModelTooltip
model={item}
@@ -47,7 +47,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
</span>
}
placement="top"
{...(!props.newLayoutDesigns ? { openDelay: 800 } : {})}
openDelay={800}
>
<div
classList={{
@@ -52,7 +52,12 @@ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (p
<For each={props.comments ?? []}>
{(item) => (
<div class="relative group shrink-0">
<TooltipV2 value={item.comment} placement="top" contentClass="max-w-[300px] break-words">
<TooltipV2
value={item.comment}
placement="top"
openDelay={800}
contentClass="max-w-[300px] break-words"
>
<CommentCardV2
comment={item.comment ?? ""}
path={item.path}
@@ -1,13 +0,0 @@
import { describe, expect, test } from "bun:test"
import { macTitlebarLeftPadding } from "./titlebar-padding"
describe("macOS titlebar padding", () => {
test("reserves traffic light space while windowed", () => {
expect(macTitlebarLeftPadding(1, false)).toBe("84px")
expect(macTitlebarLeftPadding(2, false)).toBe("42px")
})
test("does not reserve traffic light space in fullscreen", () => {
expect(macTitlebarLeftPadding(1, true)).toBe("0px")
})
})
@@ -1,6 +0,0 @@
const macTrafficLightsWidth = 84
export function macTitlebarLeftPadding(zoom: number, fullscreen: boolean) {
if (fullscreen) return "0px"
return `${macTrafficLightsWidth / zoom}px`
}
+110 -9
View File
@@ -5,6 +5,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Button } from "@opencode-ai/ui/button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { useTheme } from "@opencode-ai/ui/theme/context"
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"
@@ -28,8 +29,28 @@ import type { PromptSession } from "@/context/prompt"
import "./titlebar.css"
import { newTabTooltipKeybind } from "./command-tooltip-keybind"
import { normalizeSessionInfo } from "@/utils/session"
import { macTitlebarLeftPadding } from "./titlebar-padding"
type TauriDesktopWindow = {
startDragging?: () => Promise<void>
toggleMaximize?: () => Promise<void>
}
type TauriThemeWindow = {
setTheme?: (theme?: "light" | "dark" | null) => Promise<void>
}
type TauriApi = {
window?: {
getCurrentWindow?: () => TauriDesktopWindow
}
webviewWindow?: {
getCurrentWebviewWindow?: () => TauriThemeWindow
}
}
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
const legacyTitlebarHeight = 40
const v2TitlebarHeight = 36
const minTitlebarZoom = 0.25
@@ -53,6 +74,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
const command = useCommand()
const language = useLanguage()
const settings = useSettings()
const theme = useTheme()
const server = useServer()
const navigate = useNavigate()
const location = useLocation()
@@ -63,6 +85,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
const electronWindows = createMemo(() => windows() && !tauriApi())
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
const web = createMemo(() => platform.platform === "web")
const zoom = () => platform.webviewZoom?.() ?? 1
@@ -153,6 +176,56 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
},
])
const getWin = () => {
if (platform.platform !== "desktop") return
return currentDesktopWindow()
}
createEffect(() => {
if (platform.platform !== "desktop") return
const scheme = theme.colorScheme()
const value = scheme === "system" ? null : scheme
const win = currentThemeWindow()
if (!win?.setTheme) return
void win.setTheme(value).catch(() => undefined)
})
const interactive = (target: EventTarget | null) => {
if (!(target instanceof Element)) return false
const selector =
"button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']"
return !!target.closest(selector)
}
const drag = (e: MouseEvent) => {
if (platform.platform !== "desktop") return
if (e.buttons !== 1) return
if (interactive(e.target)) return
const win = getWin()
if (!win?.startDragging) return
e.preventDefault()
void win.startDragging().catch(() => undefined)
}
const maximize = (e: MouseEvent) => {
if (platform.platform !== "desktop") return
if (interactive(e.target)) return
if (e.target instanceof Element && e.target.closest("[data-tauri-decorum-tb]")) return
const win = getWin()
if (!win?.toggleMaximize) return
e.preventDefault()
void win.toggleMaximize().catch(() => undefined)
}
return (
<header
data-slot={useV2Titlebar() ? "titlebar-v2" : undefined}
@@ -164,13 +237,17 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
}}
style={{
"min-height": minHeight(),
// Keep visible native macOS traffic lights clear even when the desktop window is narrow.
"padding-left": mac() ? macTitlebarLeftPadding(zoom(), platform.windowFullscreen?.() ?? false) : 0,
width: windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
"max-width": windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
"align-self": windows() ? "flex-start" : undefined,
// Keep native macOS traffic lights clear even when the desktop window is narrow.
"padding-left": mac() ? `${84 / zoom()}px` : 0,
width: electronWindows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
"max-width": electronWindows()
? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))`
: undefined,
"align-self": electronWindows() ? "flex-start" : undefined,
}}
data-tauri-drag-region
onMouseDown={drag}
onDblClick={maximize}
>
<Switch>
<Match when={useV2Titlebar()}>
@@ -343,7 +420,16 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
title: "",
keybind: `mod+option+ArrowLeft,ctrl+shift+tab`,
hidden: true,
onSelect: tabs.previous,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab === currentTab())
if (index === -1) return
index -= 1
if (index === -1) index = tabsStore.length - 1
const next = tabsStore[index]
if (next) tabs.select(next)
},
},
{
id: `tab.next`,
@@ -351,7 +437,16 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
title: "",
keybind: `mod+option+ArrowRight,ctrl+tab`,
hidden: true,
onSelect: tabs.next,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab === currentTab())
if (index === -1) return
index += 1
if (index === tabsStore.length) index = 0
const next = tabsStore[index]
if (next) tabs.select(next)
},
},
].filter((v) => v !== undefined)
})
@@ -433,6 +528,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
</Show>
<div class="flex-1" />
<TitlebarV2Right state={v2RightState()} />
<Show when={windows() && !electronWindows()}>
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>
)
}}
@@ -452,6 +550,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
<WindowsAppMenu command={command} platform={platform} />
</Show>
<Show when={mac()}>
{/*<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />*/}
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
@@ -581,10 +680,12 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
"pr-2": !windows(),
}}
data-tauri-drag-region
onMouseDown={drag}
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
<Show when={windows()}>
<div class="shrink-0" style={{ width: windowsControlsWidth() }} />
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>
</div>
-3
View File
@@ -97,9 +97,6 @@ type PlatformBase = {
/** Webview zoom level (desktop only) */
webviewZoom?: Accessor<number>
/** Whether the native desktop window is fullscreen */
windowFullscreen?: Accessor<boolean>
/** Get whether native pinch/Ctrl-scroll zoom gestures are enabled (desktop only) */
getPinchZoomEnabled?(): Promise<boolean> | boolean
@@ -1,43 +0,0 @@
import { describe, expect, test } from "bun:test"
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"
function history(): TabHistory {
return { stack: [], index: -1 }
}
describe("tab history", () => {
test("moves backward and forward through selected tabs", () => {
const selected = ["a", "b", "c"].reduce(rememberTab, history())
const available = new Set(selected.stack)
const previous = previousTab(selected, available)
expect(previous?.key).toBe("b")
const first = previousTab(previous!.state, available)
expect(first?.key).toBe("a")
const next = nextTab(first!.state, available)
expect(next?.key).toBe("b")
})
test("replaces forward history after a new selection", () => {
const selected = ["a", "b", "c"].reduce(rememberTab, history())
const previous = previousTab(selected, new Set(selected.stack))
const next = rememberTab(previous!.state, "d")
expect(next).toEqual({ stack: ["a", "b", "d"], index: 2 })
expect(nextTab(next, new Set(next.stack))).toBeUndefined()
})
test("skips tabs that are no longer open", () => {
const selected = ["a", "b", "c"].reduce(rememberTab, history())
expect(previousTab(selected, new Set(["a", "c"]))?.key).toBe("a")
})
test("skips a repeated current tab after closing the previous selection", () => {
const selected = ["a", "b", "c", "b"].reduce(rememberTab, history())
expect(previousTab(selected, new Set(["a", "b"]))?.key).toBe("a")
})
})
-28
View File
@@ -1,28 +0,0 @@
const MAX_TAB_HISTORY = 100
export type TabHistory = {
stack: string[]
index: number
}
export function rememberTab(state: TabHistory, key: string): TabHistory {
if (state.stack[state.index] === key) return state
const stack = state.stack.slice(0, state.index + 1).concat(key).slice(-MAX_TAB_HISTORY)
return { stack, index: stack.length - 1 }
}
export function previousTab(state: TabHistory, available: Set<string>) {
return move(state, -1, available)
}
export function nextTab(state: TabHistory, available: Set<string>) {
return move(state, 1, available)
}
function move(state: TabHistory, offset: -1 | 1, available: Set<string>) {
const current = state.stack[state.index]
for (let index = state.index + offset; index >= 0 && index < state.stack.length; index += offset) {
const key = state.stack[index]
if (key && key !== current && available.has(key)) return { state: { ...state, index }, key }
}
}
-16
View File
@@ -12,7 +12,6 @@ import { sessionHref } from "@/utils/session-route"
import { createTabMemory } from "./tab-memory"
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
import { createDraftPromptSession, type PromptModel } from "./prompt-state"
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"
export type SessionTab = {
type: "session"
@@ -80,7 +79,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const memory = createTabMemory(getOwner())
const closing = new Set<string>()
let history: TabHistory = { stack: [], index: -1 }
let recentWrite = 0
let recentValue: string | undefined
@@ -152,21 +150,10 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const navigateTab = (tab: Tab) => {
const href = tabHref(tab)
history = rememberTab(history, tabKey(tab))
setRecentKey(tabKey(tab))
navigate(href)
}
const moveHistory = (direction: "previous" | "next") => {
const available = new Set(store.map(tabKey))
const result = direction === "previous" ? previousTab(history, available) : nextTab(history, available)
if (!result) return
const tab = store.find((item) => tabKey(item) === result.key)
if (!tab) return
history = result.state
navigateTab(tab)
}
const removeTab = (index: number) => {
const tab = store[index]
if (!tab) return
@@ -372,11 +359,8 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
select: navigateTab,
remember(tab: Tab) {
const key = tabKey(tab)
history = rememberTab(history, key)
if (recentKey() !== key) setRecentKey(key)
},
previous: () => moveHistory("previous"),
next: () => moveHistory("next"),
toggleHome(input: { home: boolean; current?: Tab }) {
if (input.home) {
const tab = store.find((tab) => tabKey(tab) === recentKey())
-11
View File
@@ -2,17 +2,6 @@ import { describe, expect, test } from "bun:test"
import { DESKTOP_MENU } from "./desktop-menu"
describe("desktop menu", () => {
test("navigates between tabs", () => {
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
(item) => item.type === "item" && (item.label === "Previous Tab" || item.label === "Next Tab"),
)
expect(items).toEqual([
{ type: "item", label: "Previous Tab", command: "tab.prev", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Tab", command: "tab.next", accelerator: { macos: "Option+Down" } },
])
})
test("exports logs through the desktop command registry", () => {
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
(item) => item.type === "item" && item.label === "Export Logs...",
+2 -2
View File
@@ -168,8 +168,8 @@ export const DESKTOP_MENU: DesktopMenu[] = [
{ type: "item", label: "Back", command: "common.goBack", accelerator: { macos: "Cmd+[" } },
{ type: "item", label: "Forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } },
{ type: "separator" },
{ type: "item", label: "Previous Tab", command: "tab.prev", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Tab", command: "tab.next", accelerator: { macos: "Option+Down" } },
{ type: "item", label: "Previous Session", command: "session.previous", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Session", command: "session.next", accelerator: { macos: "Option+Down" } },
{ type: "separator" },
{
type: "item",
+1
View File
@@ -263,6 +263,7 @@ 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
-5
View File
@@ -227,11 +227,6 @@ export function registerIpcHandlers(deps: Deps) {
return win?.isFocused() ?? false
})
ipcMain.handle("get-window-fullscreen", (event: IpcMainInvokeEvent) => {
const win = BrowserWindow.fromWebContents(event.sender)
return win?.isFullScreen() ?? false
})
ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => {
const win = BrowserWindow.fromWebContents(event.sender)
win?.focus()
-6
View File
@@ -219,7 +219,6 @@ export function createMainWindow(id: string = randomUUID()) {
state.manage(win)
registerWindow(win, id)
wireFullscreen(win)
loadWindow(win, "index.html")
wireZoom(win)
@@ -473,11 +472,6 @@ function wireZoom(win: BrowserWindow) {
})
}
function wireFullscreen(win: BrowserWindow) {
win.on("enter-full-screen", () => win.webContents.send("window-fullscreen-changed", true))
win.on("leave-full-screen", () => win.webContents.send("window-fullscreen-changed", false))
}
function clampZoom(value: number) {
return Math.min(Math.max(value, minZoomLevel), maxZoomLevel)
}
-6
View File
@@ -100,12 +100,6 @@ const api: ElectronAPI = {
readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"),
showNotification: (title, body) => ipcRenderer.send("show-notification", title, body),
getWindowFocused: () => ipcRenderer.invoke("get-window-focused"),
getWindowFullscreen: () => ipcRenderer.invoke("get-window-fullscreen"),
onWindowFullscreenChanged: (cb) => {
const handler = (_: unknown, fullscreen: boolean) => cb(fullscreen)
ipcRenderer.on("window-fullscreen-changed", handler)
return () => ipcRenderer.removeListener("window-fullscreen-changed", handler)
},
setWindowFocus: () => ipcRenderer.invoke("set-window-focus"),
showWindow: () => ipcRenderer.invoke("show-window"),
relaunch: () => ipcRenderer.send("relaunch"),
-2
View File
@@ -91,8 +91,6 @@ export type ElectronAPI = {
readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null>
showNotification: (title: string, body?: string) => void
getWindowFocused: () => Promise<boolean>
getWindowFullscreen: () => Promise<boolean>
onWindowFullscreenChanged: (cb: (fullscreen: boolean) => void) => () => void
setWindowFocus: () => Promise<void>
showWindow: () => Promise<void>
relaunch: () => void
-6
View File
@@ -30,10 +30,6 @@ import "./styles.css"
import { Splash } from "@opencode-ai/ui/logo"
import { useTheme } from "@opencode-ai/ui/theme/context"
const [windowFullscreen, setWindowFullscreen] = createSignal(false)
window.api.onWindowFullscreenChanged(setWindowFullscreen)
void window.api.getWindowFullscreen().then(setWindowFullscreen)
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(t("error.dev.rootNotFound"))
@@ -298,8 +294,6 @@ const createPlatform = (windowState: DesktopWindowState): Platform => {
webviewZoom,
windowFullscreen,
getPinchZoomEnabled: () => window.api.getPinchZoomEnabled(),
setPinchZoomEnabled,
+6 -3
View File
@@ -516,12 +516,14 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re
return msgs
}
const GEMINI_OMITS_SAMPLING = /gemini-(?:3[.-](?:5-flash-lite|(?:[6-9]|\d{2,}))|(?:[4-9]|\d{2,}))(?:[.-]|$)/
export function temperature(model: Provider.Model) {
const id = model.id.toLowerCase()
if (id.includes("north-mini-code")) return 1.0
if (id.includes("qwen")) return 0.55
if (id.includes("claude")) return undefined
if (id.includes("gemini")) return 1.0
if (id.includes("gemini")) return GEMINI_OMITS_SAMPLING.test(id) ? undefined : 1.0
if (id.includes("glm-4.6")) return 1.0
if (id.includes("glm-4.7")) return 1.0
if (id.includes("minimax-m2")) return 1.0
@@ -538,7 +540,8 @@ export function temperature(model: Provider.Model) {
export function topP(model: Provider.Model) {
const id = model.id.toLowerCase()
if (id.includes("qwen")) return 1
if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
if (id.includes("gemini")) return GEMINI_OMITS_SAMPLING.test(id) ? undefined : 0.95
if (["minimax-m2", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
return 0.95
}
return undefined
@@ -550,7 +553,7 @@ export function topK(model: Provider.Model) {
if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
return 20
}
if (id.includes("gemini")) return 64
if (id.includes("gemini")) return GEMINI_OMITS_SAMPLING.test(id) ? undefined : 64
return undefined
}
@@ -3190,6 +3190,32 @@ describe("ProviderTransform.temperature - Cohere North", () => {
})
})
describe("ProviderTransform sampling defaults - Gemini", () => {
const model = (id: string) =>
({
id: `google/${id}`,
api: { id },
}) as any
test.each(["gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-4-pro"])(
"omits deprecated sampling controls for %s",
(id) => {
expect(ProviderTransform.temperature(model(id))).toBeUndefined()
expect(ProviderTransform.topP(model(id))).toBeUndefined()
expect(ProviderTransform.topK(model(id))).toBeUndefined()
},
)
test.each(["gemini-2.5-flash", "gemini-3.1-flash-lite", "gemini-3.5-flash"])(
"preserves sampling defaults for %s",
(id) => {
expect(ProviderTransform.temperature(model(id))).toBe(1)
expect(ProviderTransform.topP(model(id))).toBe(0.95)
expect(ProviderTransform.topK(model(id))).toBe(64)
},
)
})
describe("ProviderTransform.reasoningVariants", () => {
const model = (reasoning_options: ModelsDev.Model["reasoning_options"]) => ({ reasoning_options }) as ModelsDev.Model
const target = (npm: string, id = "test-model") =>
@@ -32,6 +32,7 @@ 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,7 +385,12 @@ export function PromptInputV2Attachments(props: {
<For each={props.comments ?? []}>
{(comment) => (
<div class="relative group shrink-0">
<TooltipV2 value={comment.comment} placement="top" contentClass="max-w-[300px] break-words">
<TooltipV2
value={comment.comment}
placement="top"
openDelay={800}
contentClass="max-w-[300px] break-words"
>
<CommentCardV2
comment={comment.comment ?? ""}
path={comment.path}
@@ -214,6 +214,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
</Show>
<div class="flex items-center">
<TooltipV2
openDelay={2000}
inactive={!prev()}
value={
<>
@@ -233,6 +234,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
/>
</TooltipV2>
<TooltipV2
openDelay={2000}
inactive={!next()}
value={
<>
@@ -266,12 +268,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 value={i18n.t("ui.sessionReviewV2.showAllLines")}>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
<SegmentedControlItemV2 value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
<Icon name="expand" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<SegmentedControlItemV2 value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<Icon name="collapse" />
</SegmentedControlItemV2>
@@ -287,12 +289,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 value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<SegmentedControlItemV2 value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<Icon name="unified" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 value={i18n.t("ui.sessionReviewV2.splitDiff")}>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
<SegmentedControlItemV2 value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
<Icon name="split" />
</SegmentedControlItemV2>
+1 -5
View File
@@ -104,6 +104,7 @@ const appGlobalBindingCommands = [
] as const
const appBindingCommands = [
"command.palette.show",
"model.list",
"model.cycle_recent",
"model.cycle_recent_reverse",
@@ -962,11 +963,6 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
commands: appCommands(),
}))
useBindings(() => ({
enabled: () => dialog.stack.length === 0,
bindings: tuiConfig.keybinds.get(COMMAND_PALETTE_COMMAND),
}))
useBindings(() => ({
mode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather("app", appBindingCommands),
+4 -34
View File
@@ -5,13 +5,7 @@ import { testRender, useRenderer } from "@opentui/solid"
import { expect, test } from "bun:test"
import { onCleanup } from "solid-js"
import { TuiKeybind } from "../src/config/keybind"
import {
COMMAND_PALETTE_COMMAND,
getOpencodeModeStack,
OPENCODE_BASE_MODE,
OpencodeKeymapProvider,
registerOpencodeKeymap,
} from "../src/keymap"
import { getOpencodeModeStack, OPENCODE_BASE_MODE, OpencodeKeymapProvider, registerOpencodeKeymap } from "../src/keymap"
function createResolvedKeymapConfig(input: TuiKeybind.KeybindOverrides = {}) {
const keybinds = TuiKeybind.parse(input)
@@ -79,14 +73,12 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
const offKeymap = registerOpencodeKeymap(keymap, renderer, config)
const offGlobal = keymap.registerLayer({
commands: [
{ name: COMMAND_PALETTE_COMMAND, run() {} },
{ name: "session.list", run() {} },
{ name: "session.new", run() {} },
{ name: "session.page.up", run() {} },
{ name: "session.first", run() {} },
],
bindings: config.keybinds.gather("test.global", [
COMMAND_PALETTE_COMMAND,
"session.list",
"session.new",
"session.page.up",
@@ -103,14 +95,7 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
Array.from(
keymap.getCommandBindings({
visibility: "active",
commands: [
COMMAND_PALETTE_COMMAND,
"session.list",
"session.new",
"session.page.up",
"session.first",
"model.list",
],
commands: ["session.list", "session.new", "session.page.up", "session.first", "model.list"],
}),
([command, bindings]) => [command, bindings.length],
),
@@ -140,24 +125,9 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
const app = await testRender(() => <Harness />)
try {
expect(counts).toEqual({
base: {
[COMMAND_PALETTE_COMMAND]: 1,
"session.list": 1,
"session.new": 1,
"session.page.up": 2,
"session.first": 2,
"model.list": 1,
},
question: {
[COMMAND_PALETTE_COMMAND]: 1,
"session.list": 1,
"session.new": 1,
"session.page.up": 2,
"session.first": 2,
"model.list": 0,
},
base: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 1 },
question: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 0 },
autocomplete: {
[COMMAND_PALETTE_COMMAND]: 1,
"session.list": 1,
"session.new": 1,
"session.page.up": 2,
@@ -1,35 +0,0 @@
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
}
+6 -36
View File
@@ -2,22 +2,19 @@ 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 Omit<ComponentProps<typeof KobalteTooltip>, "openDelay"> {
export interface TooltipV2Props extends ComponentProps<typeof KobalteTooltip> {
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,
@@ -29,37 +26,19 @@ 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
@@ -101,11 +80,6 @@ export function TooltipV2(props: TooltipV2Props) {
onCleanup(() => obs.disconnect())
})
onCleanup(() => {
cancelIntent?.()
if (local.delay === "intent") resetTooltipIntent()
})
let justClickedTrigger = false
return (
@@ -114,8 +88,8 @@ export function TooltipV2(props: TooltipV2Props) {
<Match when={true}>
<KobalteTooltip
gutter={4}
openDelay={local.delay === "intent" ? 0 : 400}
skipDelayDuration={local.delay === "intent" ? 0 : 300}
openDelay={400}
skipDelayDuration={300}
{...others}
closeDelay={0}
ignoreSafeArea={local.ignoreSafeArea ?? true}
@@ -127,11 +101,7 @@ export function TooltipV2(props: TooltipV2Props) {
justClickedTrigger = false
return
}
if (open) {
show()
return
}
close()
setState("open", open)
}}
>
<KobalteTooltip.Trigger