Merge branch 'dev' of github.com:anomalyco/opencode into dev
This commit is contained in:
commit
cfa134f042
28 changed files with 151 additions and 72 deletions
|
|
@ -60,6 +60,8 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) {
|
||||||
controller={props.controller}
|
controller={props.controller}
|
||||||
borderUnderlay={props.borderUnderlay}
|
borderUnderlay={props.borderUnderlay}
|
||||||
class={props.class}
|
class={props.class}
|
||||||
|
attachKeybind={command.keybindParts("file.attach")}
|
||||||
|
attachShortcut={command.keybind("file.attach")}
|
||||||
modelControl={
|
modelControl={
|
||||||
<PromptInputV2ModelControl
|
<PromptInputV2ModelControl
|
||||||
loading={props.controller.model.loading}
|
loading={props.controller.model.loading}
|
||||||
|
|
@ -451,12 +453,14 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
|
||||||
options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })),
|
options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })),
|
||||||
current: () => props.controls.agents.current,
|
current: () => props.controls.agents.current,
|
||||||
onSelect: props.controls.agents.select,
|
onSelect: props.controls.agents.select,
|
||||||
|
keybind: () => command.keybindParts("agent.cycle"),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
variant: {
|
variant: {
|
||||||
options: () => variants().map((value) => ({ id: value, label: value })),
|
options: () => variants().map((value) => ({ id: value, label: value })),
|
||||||
current: () => props.controls.model.selection.variant.current() ?? "default",
|
current: () => props.controls.model.selection.variant.current() ?? "default",
|
||||||
onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value),
|
onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value),
|
||||||
|
keybind: () => command.keybindParts("model.variant.cycle"),
|
||||||
},
|
},
|
||||||
submit: {
|
submit: {
|
||||||
stopping,
|
stopping,
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export function useTitlebarRightMount() {
|
||||||
return mount
|
return mount
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Titlebar(props: { update?: TitlebarUpdate }) {
|
export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visible: boolean; toggle: () => void } }) {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
|
|
@ -462,7 +462,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||||
"md:pl-4": !mac(),
|
"md:pl-4": !mac(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChannelIndicator />
|
<ChannelIndicator debugTools={props.debugTools} />
|
||||||
<Show when={windows() || linux()}>
|
<Show when={windows() || linux()}>
|
||||||
<WindowsAppMenu command={command} platform={platform} variant="v2" />
|
<WindowsAppMenu command={command} platform={platform} variant="v2" />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -660,9 +660,9 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||||
<ChannelIndicator />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ChannelIndicator debugTools={props.debugTools} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -747,12 +747,27 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChannelIndicator() {
|
function ChannelIndicator(props: { debugTools?: { visible: boolean; toggle: () => void } }) {
|
||||||
|
const channel = import.meta.env.VITE_OPENCODE_CHANNEL
|
||||||
|
if (channel === "dev" && props.debugTools) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono cursor-pointer"
|
||||||
|
onClick={props.debugTools.toggle}
|
||||||
|
aria-label="Toggle debug tools"
|
||||||
|
aria-pressed={props.debugTools.visible}
|
||||||
|
>
|
||||||
|
DEV
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
{["beta", "dev"].includes(channel) && (
|
||||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||||
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
{channel.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
import { TabsInfoPopup } from "@/components/help-button"
|
import { TabsInfoPopup } from "@/components/help-button"
|
||||||
|
|
@ -11,6 +12,7 @@ export default function NewLayout(props: ParentProps) {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
setNavigate(navigate)
|
setNavigate(navigate)
|
||||||
|
const [state, setState] = createStore({ debugTools: true })
|
||||||
|
|
||||||
createEffect(() => setV2Toast(true))
|
createEffect(() => setV2Toast(true))
|
||||||
|
|
||||||
|
|
@ -32,11 +34,18 @@ export default function NewLayout(props: ParentProps) {
|
||||||
"padding-bottom": "env(safe-area-inset-bottom, 0px)",
|
"padding-bottom": "env(safe-area-inset-bottom, 0px)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Titlebar update={update} />
|
<Titlebar
|
||||||
|
update={update}
|
||||||
|
debugTools={
|
||||||
|
import.meta.env.DEV
|
||||||
|
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
|
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
|
||||||
<Suspense>{props.children}</Suspense>
|
<Suspense>{props.children}</Suspense>
|
||||||
</main>
|
</main>
|
||||||
{import.meta.env.DEV && <DebugBar inline />}
|
{import.meta.env.DEV && state.debugTools && <DebugBar inline />}
|
||||||
<TabsInfoPopup />
|
<TabsInfoPopup />
|
||||||
<ToastRegion v2 />
|
<ToastRegion v2 />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ export default function LegacyLayout(props: ParentProps) {
|
||||||
sizing: false,
|
sizing: false,
|
||||||
peek: undefined as string | undefined,
|
peek: undefined as string | undefined,
|
||||||
peeked: false,
|
peeked: false,
|
||||||
|
debugTools: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateVersion = () => {
|
const updateVersion = () => {
|
||||||
|
|
@ -2248,7 +2249,14 @@ export default function LegacyLayout(props: ParentProps) {
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||||
{autoselecting() ?? ""}
|
{autoselecting() ?? ""}
|
||||||
<Titlebar update={titlebarUpdate} />
|
<Titlebar
|
||||||
|
update={titlebarUpdate}
|
||||||
|
debugTools={
|
||||||
|
import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1"
|
||||||
|
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Show when={updateVersion() !== undefined}>
|
<Show when={updateVersion() !== undefined}>
|
||||||
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
|
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -2393,7 +2401,7 @@ export default function LegacyLayout(props: ParentProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
|
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && state.debugTools && <DebugBar />}
|
||||||
</div>
|
</div>
|
||||||
<TabsInfoPopup />
|
<TabsInfoPopup />
|
||||||
<ToastRegion v2={false} />
|
<ToastRegion v2={false} />
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@ import { LLMError, ProviderErrorEvent } from "./schema"
|
||||||
|
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/prompt is too long/i,
|
/prompt is too long/i,
|
||||||
|
/request_too_large/i,
|
||||||
/input is too long for requested model/i,
|
/input is too long for requested model/i,
|
||||||
/exceeds the context window/i,
|
/exceeds the context window/i,
|
||||||
|
/exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
|
||||||
/input token count.*exceeds the maximum/i,
|
/input token count.*exceeds the maximum/i,
|
||||||
/tokens in request more than max tokens allowed/i,
|
/tokens in request more than max tokens allowed/i,
|
||||||
/maximum prompt length is \d+/i,
|
/maximum prompt length is \d+/i,
|
||||||
/reduce the length of the messages/i,
|
/reduce the length of the messages/i,
|
||||||
/maximum context length is \d+ tokens/i,
|
/maximum context length is \d+ tokens/i,
|
||||||
|
/exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i,
|
||||||
|
/input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i,
|
||||||
/exceeds the limit of \d+/i,
|
/exceeds the limit of \d+/i,
|
||||||
/exceeds the available context size/i,
|
/exceeds the available context size/i,
|
||||||
/greater than the context length/i,
|
/greater than the context length/i,
|
||||||
|
|
@ -21,11 +25,17 @@ const patterns = [
|
||||||
/input length.*exceeds.*context length/i,
|
/input length.*exceeds.*context length/i,
|
||||||
/prompt too long; exceeded (?:max )?context length/i,
|
/prompt too long; exceeded (?:max )?context length/i,
|
||||||
/too large for model with \d+ maximum context length/i,
|
/too large for model with \d+ maximum context length/i,
|
||||||
|
/prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
|
||||||
/model_context_window_exceeded/i,
|
/model_context_window_exceeded/i,
|
||||||
|
/too many tokens/i,
|
||||||
|
/token limit exceeded/i,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const exclusions = [/^(throttling error|service unavailable):/i, /rate limit/i, /too many requests/i]
|
||||||
|
|
||||||
export const isContextOverflow = (message: string) =>
|
export const isContextOverflow = (message: string) =>
|
||||||
patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
|
!exclusions.some((pattern) => pattern.test(message)) &&
|
||||||
|
(patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message))
|
||||||
|
|
||||||
export const isContextOverflowFailure = (failure: unknown) =>
|
export const isContextOverflowFailure = (failure: unknown) =>
|
||||||
failure instanceof LLMError
|
failure instanceof LLMError
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,29 @@ import { describe, expect, test } from "bun:test"
|
||||||
import { isContextOverflow } from "../src"
|
import { isContextOverflow } from "../src"
|
||||||
|
|
||||||
describe("provider error classification", () => {
|
describe("provider error classification", () => {
|
||||||
test("classifies Z.AI GLM token limit messages as context overflow", () => {
|
test("classifies provider token limit messages as context overflow", () => {
|
||||||
expect(isContextOverflow("tokens in request more than max tokens allowed")).toBe(true)
|
const messages = [
|
||||||
|
"tokens in request more than max tokens allowed",
|
||||||
|
'{"error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}',
|
||||||
|
"Requested token count exceeds the model's maximum context length of 131072 tokens.",
|
||||||
|
"Input length (265330) exceeds model's maximum context length (262144).",
|
||||||
|
"Input length 131393 exceeds the maximum allowed input length of 131040 tokens.",
|
||||||
|
"The input (516368 tokens) is longer than the model's context length (262144 tokens).",
|
||||||
|
"Prompt has 5,958,968 tokens, but the configured context size is 256,000 tokens",
|
||||||
|
"Too many tokens",
|
||||||
|
"Token limit exceeded",
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(messages.every(isContextOverflow)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not classify rate limits as context overflow", () => {
|
||||||
|
const messages = [
|
||||||
|
"Throttling error: Too many tokens, please wait before trying again.",
|
||||||
|
"Rate limit exceeded, please retry after 30 seconds.",
|
||||||
|
"Too many requests. Please slow down.",
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(messages.some(isContextOverflow)).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { ModelStatus } from "./model-status"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { ProviderError } from "./error"
|
import { ProviderError } from "./error"
|
||||||
|
|
||||||
const OPENAI_HEADER_TIMEOUT_DEFAULT = 10_000
|
const OPENAI_HEADER_TIMEOUT_DEFAULT = 300_000
|
||||||
|
|
||||||
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
||||||
if (typeof ms !== "number" || ms <= 0) return res
|
if (typeof ms !== "number" || ms <= 0) return res
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ it.live("OpenAI API auth gets default headerTimeout", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const provider = yield* Provider.Service
|
const provider = yield* Provider.Service
|
||||||
const openai = yield* provider.getProvider(ProviderV2.ID.openai)
|
const openai = yield* provider.getProvider(ProviderV2.ID.openai)
|
||||||
expect(openai.options.headerTimeout).toBe(10_000)
|
expect(openai.options.headerTimeout).toBe(300_000)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -891,7 +891,7 @@
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background-color: var(--icon-weak-base);
|
background-color: var(--v2-icon-icon-muted);
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ export type PromptInputV2Props = {
|
||||||
borderUnderlay?: boolean
|
borderUnderlay?: boolean
|
||||||
class?: string
|
class?: string
|
||||||
modelControl?: JSX.Element
|
modelControl?: JSX.Element
|
||||||
|
attachKeybind?: string[]
|
||||||
|
attachShortcut?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PromptInputV2(props: PromptInputV2Props) {
|
export function PromptInputV2(props: PromptInputV2Props) {
|
||||||
|
|
@ -197,9 +199,9 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||||
<PromptInputV2AddMenu
|
<PromptInputV2AddMenu
|
||||||
disabled={state.mode === "shell"}
|
disabled={state.mode === "shell"}
|
||||||
title="Add images and files"
|
title="Add images and files"
|
||||||
keybind={["Mod", "U"]}
|
keybind={props.attachKeybind ?? ["Mod", "U"]}
|
||||||
attachLabel="Images and files"
|
attachLabel="Images and files"
|
||||||
attachShortcut="Mod+U"
|
attachShortcut={props.attachShortcut ?? "Mod+U"}
|
||||||
commandsLabel="Commands"
|
commandsLabel="Commands"
|
||||||
contextLabel="Context"
|
contextLabel="Context"
|
||||||
shellLabel="Shell command"
|
shellLabel="Shell command"
|
||||||
|
|
@ -233,7 +235,11 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||||
<Show when={view.variant}>
|
<Show when={view.variant}>
|
||||||
{(control) => (
|
{(control) => (
|
||||||
<Show when={control().options().length > 1}>
|
<Show when={control().options().length > 1}>
|
||||||
<PromptInputV2ConfiguredSelect title="Choose model variant" control={control()} />
|
<PromptInputV2ConfiguredSelect
|
||||||
|
title="Choose model variant"
|
||||||
|
keybind={["Shift", "Mod", "D"]}
|
||||||
|
control={control()}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -512,7 +518,7 @@ function PromptInputV2ConfiguredSelect(props: {
|
||||||
return (
|
return (
|
||||||
<PromptInputV2Select
|
<PromptInputV2Select
|
||||||
title={props.title}
|
title={props.title}
|
||||||
keybind={props.keybind}
|
keybind={props.control.keybind?.() ?? props.keybind}
|
||||||
options={props.control.options()}
|
options={props.control.options()}
|
||||||
current={current()}
|
current={current()}
|
||||||
currentIcon={
|
currentIcon={
|
||||||
|
|
@ -536,13 +542,21 @@ export function PromptInputV2Select(props: {
|
||||||
onSelect: (id: string) => void
|
onSelect: (id: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
<TooltipV2
|
||||||
|
placement="top"
|
||||||
|
value={
|
||||||
|
<>
|
||||||
|
{props.title}
|
||||||
|
<KeybindV2 keys={props.keybind ?? []} variant="neutral" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<MenuV2 gutter={6} modal={false} placement="top-start" onOpenChange={props.onOpenChange}>
|
<MenuV2 gutter={6} modal={false} placement="top-start" onOpenChange={props.onOpenChange}>
|
||||||
<MenuV2.Trigger
|
<MenuV2.Trigger
|
||||||
as={ButtonV2}
|
as={ButtonV2}
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
size="normal"
|
size="normal"
|
||||||
class={`max-w-[220px] justify-start ![font-weight:440] ${props.class ?? ""}`}
|
class={`max-w-[220px] justify-start ![font-weight:440] ${props.class ?? ""}`}
|
||||||
title={keybindTitle(props.title, props.keybind)}
|
|
||||||
>
|
>
|
||||||
{props.currentIcon}
|
{props.currentIcon}
|
||||||
<span class="truncate capitalize leading-5">
|
<span class="truncate capitalize leading-5">
|
||||||
|
|
@ -566,6 +580,7 @@ export function PromptInputV2Select(props: {
|
||||||
</MenuV2.Content>
|
</MenuV2.Content>
|
||||||
</MenuV2.Portal>
|
</MenuV2.Portal>
|
||||||
</MenuV2>
|
</MenuV2>
|
||||||
|
</TooltipV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -688,8 +703,3 @@ function PromptInputV2SuggestionIcon(props: { item: PromptInputV2Suggestion }) {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function keybindTitle(label: string, keybind?: string[]) {
|
|
||||||
if (!keybind?.length) return label
|
|
||||||
return `${label} (${keybind.join("+")})`
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export type PromptInputV2SelectControl = {
|
||||||
options: Accessor<PromptInputV2Option[]>
|
options: Accessor<PromptInputV2Option[]>
|
||||||
current: Accessor<string>
|
current: Accessor<string>
|
||||||
onSelect: (id: string) => void
|
onSelect: (id: string) => void
|
||||||
|
keybind?: Accessor<string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptInputV2ViewConfig = {
|
export type PromptInputV2ViewConfig = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue