Skip to content

Commit cfa134f

Browse files
committed
Merge branch 'dev' of github.com:anomalyco/opencode into dev
2 parents 61653a2 + 2a097f3 commit cfa134f

28 files changed

Lines changed: 151 additions & 72 deletions

File tree

packages/app/src/components/prompt-input-v2.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) {
6060
controller={props.controller}
6161
borderUnderlay={props.borderUnderlay}
6262
class={props.class}
63+
attachKeybind={command.keybindParts("file.attach")}
64+
attachShortcut={command.keybind("file.attach")}
6365
modelControl={
6466
<PromptInputV2ModelControl
6567
loading={props.controller.model.loading}
@@ -451,12 +453,14 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
451453
options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })),
452454
current: () => props.controls.agents.current,
453455
onSelect: props.controls.agents.select,
456+
keybind: () => command.keybindParts("agent.cycle"),
454457
}
455458
: undefined,
456459
variant: {
457460
options: () => variants().map((value) => ({ id: value, label: value })),
458461
current: () => props.controls.model.selection.variant.current() ?? "default",
459462
onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value),
463+
keybind: () => command.keybindParts("model.variant.cycle"),
460464
},
461465
submit: {
462466
stopping,

packages/app/src/components/titlebar.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function useTitlebarRightMount() {
6767
return mount
6868
}
6969

70-
export function Titlebar(props: { update?: TitlebarUpdate }) {
70+
export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visible: boolean; toggle: () => void } }) {
7171
const layout = useLayout()
7272
const platform = usePlatform()
7373
const command = useCommand()
@@ -462,7 +462,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
462462
"md:pl-4": !mac(),
463463
}}
464464
>
465-
<ChannelIndicator />
465+
<ChannelIndicator debugTools={props.debugTools} />
466466
<Show when={windows() || linux()}>
467467
<WindowsAppMenu command={command} platform={platform} variant="v2" />
468468
</Show>
@@ -660,9 +660,9 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
660660
</div>
661661
</Show>
662662
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
663-
<ChannelIndicator />
664663
</div>
665664
</div>
665+
<ChannelIndicator debugTools={props.debugTools} />
666666
</div>
667667
</div>
668668

@@ -747,12 +747,27 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
747747
)
748748
}
749749

750-
function ChannelIndicator() {
750+
function ChannelIndicator(props: { debugTools?: { visible: boolean; toggle: () => void } }) {
751+
const channel = import.meta.env.VITE_OPENCODE_CHANNEL
752+
if (channel === "dev" && props.debugTools) {
753+
return (
754+
<button
755+
type="button"
756+
class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono cursor-pointer"
757+
onClick={props.debugTools.toggle}
758+
aria-label="Toggle debug tools"
759+
aria-pressed={props.debugTools.visible}
760+
>
761+
DEV
762+
</button>
763+
)
764+
}
765+
751766
return (
752767
<>
753-
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
768+
{["beta", "dev"].includes(channel) && (
754769
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
755-
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
770+
{channel.toUpperCase()}
756771
</div>
757772
)}
758773
</>

packages/app/src/pages/layout-new.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createEffect, Suspense, type ParentProps } from "solid-js"
2+
import { createStore } from "solid-js/store"
23
import { useNavigate } from "@solidjs/router"
34
import { DebugBar } from "@/components/debug-bar"
45
import { TabsInfoPopup } from "@/components/help-button"
@@ -11,6 +12,7 @@ export default function NewLayout(props: ParentProps) {
1112
const platform = usePlatform()
1213
const navigate = useNavigate()
1314
setNavigate(navigate)
15+
const [state, setState] = createStore({ debugTools: true })
1416

1517
createEffect(() => setV2Toast(true))
1618

@@ -32,11 +34,18 @@ export default function NewLayout(props: ParentProps) {
3234
"padding-bottom": "env(safe-area-inset-bottom, 0px)",
3335
}}
3436
>
35-
<Titlebar update={update} />
37+
<Titlebar
38+
update={update}
39+
debugTools={
40+
import.meta.env.DEV
41+
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
42+
: undefined
43+
}
44+
/>
3645
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
3746
<Suspense>{props.children}</Suspense>
3847
</main>
39-
{import.meta.env.DEV && <DebugBar inline />}
48+
{import.meta.env.DEV && state.debugTools && <DebugBar inline />}
4049
<TabsInfoPopup />
4150
<ToastRegion v2 />
4251
</div>

packages/app/src/pages/layout.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export default function LegacyLayout(props: ParentProps) {
156156
sizing: false,
157157
peek: undefined as string | undefined,
158158
peeked: false,
159+
debugTools: true,
159160
})
160161

161162
const updateVersion = () => {
@@ -2248,7 +2249,14 @@ export default function LegacyLayout(props: ParentProps) {
22482249
return (
22492250
<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">
22502251
{autoselecting() ?? ""}
2251-
<Titlebar update={titlebarUpdate} />
2252+
<Titlebar
2253+
update={titlebarUpdate}
2254+
debugTools={
2255+
import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1"
2256+
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
2257+
: undefined
2258+
}
2259+
/>
22522260
<Show when={updateVersion() !== undefined}>
22532261
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
22542262
</Show>
@@ -2393,7 +2401,7 @@ export default function LegacyLayout(props: ParentProps) {
23932401
</div>
23942402
</div>
23952403
</div>
2396-
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
2404+
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && state.debugTools && <DebugBar />}
23972405
</div>
23982406
<TabsInfoPopup />
23992407
<ToastRegion v2={false} />

packages/llm/src/provider-error.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { LLMError, ProviderErrorEvent } from "./schema"
33

44
const patterns = [
55
/prompt is too long/i,
6+
/request_too_large/i,
67
/input is too long for requested model/i,
78
/exceeds the context window/i,
9+
/exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
810
/input token count.*exceeds the maximum/i,
911
/tokens in request more than max tokens allowed/i,
1012
/maximum prompt length is \d+/i,
1113
/reduce the length of the messages/i,
1214
/maximum context length is \d+ tokens/i,
15+
/exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i,
16+
/input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i,
1317
/exceeds the limit of \d+/i,
1418
/exceeds the available context size/i,
1519
/greater than the context length/i,
@@ -21,11 +25,17 @@ const patterns = [
2125
/input length.*exceeds.*context length/i,
2226
/prompt too long; exceeded (?:max )?context length/i,
2327
/too large for model with \d+ maximum context length/i,
28+
/prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
2429
/model_context_window_exceeded/i,
30+
/too many tokens/i,
31+
/token limit exceeded/i,
2532
]
2633

34+
const exclusions = [/^(throttling error|service unavailable):/i, /rate limit/i, /too many requests/i]
35+
2736
export const isContextOverflow = (message: string) =>
28-
patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
37+
!exclusions.some((pattern) => pattern.test(message)) &&
38+
(patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message))
2939

3040
export const isContextOverflowFailure = (failure: unknown) =>
3141
failure instanceof LLMError

packages/llm/test/provider-error.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,29 @@ import { describe, expect, test } from "bun:test"
22
import { isContextOverflow } from "../src"
33

44
describe("provider error classification", () => {
5-
test("classifies Z.AI GLM token limit messages as context overflow", () => {
6-
expect(isContextOverflow("tokens in request more than max tokens allowed")).toBe(true)
5+
test("classifies provider token limit messages as context overflow", () => {
6+
const messages = [
7+
"tokens in request more than max tokens allowed",
8+
'{"error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}',
9+
"Requested token count exceeds the model's maximum context length of 131072 tokens.",
10+
"Input length (265330) exceeds model's maximum context length (262144).",
11+
"Input length 131393 exceeds the maximum allowed input length of 131040 tokens.",
12+
"The input (516368 tokens) is longer than the model's context length (262144 tokens).",
13+
"Prompt has 5,958,968 tokens, but the configured context size is 256,000 tokens",
14+
"Too many tokens",
15+
"Token limit exceeded",
16+
]
17+
18+
expect(messages.every(isContextOverflow)).toBe(true)
19+
})
20+
21+
test("does not classify rate limits as context overflow", () => {
22+
const messages = [
23+
"Throttling error: Too many tokens, please wait before trying again.",
24+
"Rate limit exceeded, please retry after 30 seconds.",
25+
"Too many requests. Please slow down.",
26+
]
27+
28+
expect(messages.some(isContextOverflow)).toBe(false)
729
})
830
})

packages/opencode/src/provider/provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { ModelStatus } from "./model-status"
3232
import { RuntimeFlags } from "@/effect/runtime-flags"
3333
import { ProviderError } from "./error"
3434

35-
const OPENAI_HEADER_TIMEOUT_DEFAULT = 10_000
35+
const OPENAI_HEADER_TIMEOUT_DEFAULT = 300_000
3636

3737
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
3838
if (typeof ms !== "number" || ms <= 0) return res

packages/opencode/test/provider/header-timeout.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ it.live("OpenAI API auth gets default headerTimeout", () =>
162162
Effect.gen(function* () {
163163
const provider = yield* Provider.Service
164164
const openai = yield* provider.getProvider(ProviderV2.ID.openai)
165-
expect(openai.options.headerTimeout).toBe(10_000)
165+
expect(openai.options.headerTimeout).toBe(300_000)
166166
}),
167167
)
168168
}),

packages/session-ui/src/components/message-part.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,7 @@
891891
width: 16px;
892892
height: 2px;
893893
border-radius: 999px;
894-
background-color: var(--icon-weak-base);
894+
background-color: var(--v2-icon-icon-muted);
895895
transition: background-color 0.2s ease;
896896
}
897897

packages/session-ui/src/v2/components/prompt-input/index.tsx

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export type PromptInputV2Props = {
3939
borderUnderlay?: boolean
4040
class?: string
4141
modelControl?: JSX.Element
42+
attachKeybind?: string[]
43+
attachShortcut?: string
4244
}
4345

4446
export function PromptInputV2(props: PromptInputV2Props) {
@@ -197,9 +199,9 @@ export function PromptInputV2(props: PromptInputV2Props) {
197199
<PromptInputV2AddMenu
198200
disabled={state.mode === "shell"}
199201
title="Add images and files"
200-
keybind={["Mod", "U"]}
202+
keybind={props.attachKeybind ?? ["Mod", "U"]}
201203
attachLabel="Images and files"
202-
attachShortcut="Mod+U"
204+
attachShortcut={props.attachShortcut ?? "Mod+U"}
203205
commandsLabel="Commands"
204206
contextLabel="Context"
205207
shellLabel="Shell command"
@@ -233,7 +235,11 @@ export function PromptInputV2(props: PromptInputV2Props) {
233235
<Show when={view.variant}>
234236
{(control) => (
235237
<Show when={control().options().length > 1}>
236-
<PromptInputV2ConfiguredSelect title="Choose model variant" control={control()} />
238+
<PromptInputV2ConfiguredSelect
239+
title="Choose model variant"
240+
keybind={["Shift", "Mod", "D"]}
241+
control={control()}
242+
/>
237243
</Show>
238244
)}
239245
</Show>
@@ -512,7 +518,7 @@ function PromptInputV2ConfiguredSelect(props: {
512518
return (
513519
<PromptInputV2Select
514520
title={props.title}
515-
keybind={props.keybind}
521+
keybind={props.control.keybind?.() ?? props.keybind}
516522
options={props.control.options()}
517523
current={current()}
518524
currentIcon={
@@ -536,36 +542,45 @@ export function PromptInputV2Select(props: {
536542
onSelect: (id: string) => void
537543
}) {
538544
return (
539-
<MenuV2 gutter={6} modal={false} placement="top-start" onOpenChange={props.onOpenChange}>
540-
<MenuV2.Trigger
541-
as={ButtonV2}
542-
variant="ghost-muted"
543-
size="normal"
544-
class={`max-w-[220px] justify-start ![font-weight:440] ${props.class ?? ""}`}
545-
title={keybindTitle(props.title, props.keybind)}
546-
>
547-
{props.currentIcon}
548-
<span class="truncate capitalize leading-5">
549-
{props.options.find((option) => option.id === props.current)?.label ?? props.current}
550-
</span>
551-
<span class="-ml-0.5 -mr-1 flex shrink-0">
552-
<IconV2 name="chevron-down" />
553-
</span>
554-
</MenuV2.Trigger>
555-
<MenuV2.Portal>
556-
<MenuV2.Content>
557-
<MenuV2.RadioGroup value={props.current} onChange={props.onSelect}>
558-
<For each={props.options}>
559-
{(option) => (
560-
<MenuV2.RadioItem value={option.id} class="capitalize" closeOnSelect>
561-
{option.label}
562-
</MenuV2.RadioItem>
563-
)}
564-
</For>
565-
</MenuV2.RadioGroup>
566-
</MenuV2.Content>
567-
</MenuV2.Portal>
568-
</MenuV2>
545+
<TooltipV2
546+
placement="top"
547+
value={
548+
<>
549+
{props.title}
550+
<KeybindV2 keys={props.keybind ?? []} variant="neutral" />
551+
</>
552+
}
553+
>
554+
<MenuV2 gutter={6} modal={false} placement="top-start" onOpenChange={props.onOpenChange}>
555+
<MenuV2.Trigger
556+
as={ButtonV2}
557+
variant="ghost-muted"
558+
size="normal"
559+
class={`max-w-[220px] justify-start ![font-weight:440] ${props.class ?? ""}`}
560+
>
561+
{props.currentIcon}
562+
<span class="truncate capitalize leading-5">
563+
{props.options.find((option) => option.id === props.current)?.label ?? props.current}
564+
</span>
565+
<span class="-ml-0.5 -mr-1 flex shrink-0">
566+
<IconV2 name="chevron-down" />
567+
</span>
568+
</MenuV2.Trigger>
569+
<MenuV2.Portal>
570+
<MenuV2.Content>
571+
<MenuV2.RadioGroup value={props.current} onChange={props.onSelect}>
572+
<For each={props.options}>
573+
{(option) => (
574+
<MenuV2.RadioItem value={option.id} class="capitalize" closeOnSelect>
575+
{option.label}
576+
</MenuV2.RadioItem>
577+
)}
578+
</For>
579+
</MenuV2.RadioGroup>
580+
</MenuV2.Content>
581+
</MenuV2.Portal>
582+
</MenuV2>
583+
</TooltipV2>
569584
)
570585
}
571586

@@ -688,8 +703,3 @@ function PromptInputV2SuggestionIcon(props: { item: PromptInputV2Suggestion }) {
688703
/>
689704
)
690705
}
691-
692-
function keybindTitle(label: string, keybind?: string[]) {
693-
if (!keybind?.length) return label
694-
return `${label} (${keybind.join("+")})`
695-
}

0 commit comments

Comments
 (0)