summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFayçal Mitidji <[email protected]>2025-12-30 17:52:06 +0100
committerGitHub <[email protected]>2025-12-30 10:52:06 -0600
commitb3784588ae3800460fdbcfcc25d1abae6ff994e1 (patch)
tree4cdb5a4708e8551560f41dfb8ea0eff420533025
parent104d52bc383ec249e6946fee68eb07edf4209fab (diff)
downloadopencode-b3784588ae3800460fdbcfcc25d1abae6ff994e1.tar.gz
opencode-b3784588ae3800460fdbcfcc25d1abae6ff994e1.zip
Fix: High CPU / memory leak when filtering model list window to empty results (#6435)
-rw-r--r--packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx123
1 files changed, 67 insertions, 56 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
index 1e764d66b..2a39bb01e 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
@@ -115,11 +115,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
setStore("selected", currentIndex)
}
}
- scroll.scrollTo(0)
+ scroll?.scrollTo(0)
}),
)
function move(direction: number) {
+ if (flat().length === 0) return
let next = store.selected + direction
if (next < 0) next = flat().length - 1
if (next >= flat().length) next = 0
@@ -129,6 +130,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
function moveTo(next: number) {
setStore("selected", next)
props.onMove?.(selected()!)
+ if (!scroll) return
const target = scroll.getChildren().find((child) => {
return child.id === JSON.stringify(selected()?.value)
})
@@ -172,7 +174,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
}
})
- let scroll: ScrollBoxRenderable
+ let scroll: ScrollBoxRenderable | undefined
const ref: DialogSelectRef<T> = {
get filter() {
return store.filter
@@ -213,61 +215,70 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
/>
</box>
</box>
- <scrollbox
- paddingLeft={1}
- paddingRight={1}
- scrollbarOptions={{ visible: false }}
- ref={(r: ScrollBoxRenderable) => (scroll = r)}
- maxHeight={height()}
+ <Show
+ when={grouped().length > 0}
+ fallback={
+ <box paddingLeft={4} paddingRight={4} paddingTop={1}>
+ <text fg={theme.textMuted}>No results found</text>
+ </box>
+ }
>
- <For each={grouped()}>
- {([category, options], index) => (
- <>
- <Show when={category}>
- <box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
- <text fg={theme.accent} attributes={TextAttributes.BOLD}>
- {category}
- </text>
- </box>
- </Show>
- <For each={options}>
- {(option) => {
- const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
- const current = createMemo(() => isDeepEqual(option.value, props.current))
- return (
- <box
- id={JSON.stringify(option.value)}
- flexDirection="row"
- onMouseUp={() => {
- option.onSelect?.(dialog)
- props.onSelect?.(option)
- }}
- onMouseOver={() => {
- const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
- if (index === -1) return
- moveTo(index)
- }}
- backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
- paddingLeft={current() || option.gutter ? 1 : 3}
- paddingRight={3}
- gap={1}
- >
- <Option
- title={option.title}
- footer={option.footer}
- description={option.description !== category ? option.description : undefined}
- active={active()}
- current={current()}
- gutter={option.gutter}
- />
- </box>
- )
- }}
- </For>
- </>
- )}
- </For>
- </scrollbox>
+ <scrollbox
+ paddingLeft={1}
+ paddingRight={1}
+ scrollbarOptions={{ visible: false }}
+ ref={(r: ScrollBoxRenderable) => (scroll = r)}
+ maxHeight={height()}
+ >
+ <For each={grouped()}>
+ {([category, options], index) => (
+ <>
+ <Show when={category}>
+ <box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
+ <text fg={theme.accent} attributes={TextAttributes.BOLD}>
+ {category}
+ </text>
+ </box>
+ </Show>
+ <For each={options}>
+ {(option) => {
+ const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
+ const current = createMemo(() => isDeepEqual(option.value, props.current))
+ return (
+ <box
+ id={JSON.stringify(option.value)}
+ flexDirection="row"
+ onMouseUp={() => {
+ option.onSelect?.(dialog)
+ props.onSelect?.(option)
+ }}
+ onMouseOver={() => {
+ const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
+ if (index === -1) return
+ moveTo(index)
+ }}
+ backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
+ paddingLeft={current() || option.gutter ? 1 : 3}
+ paddingRight={3}
+ gap={1}
+ >
+ <Option
+ title={option.title}
+ footer={option.footer}
+ description={option.description !== category ? option.description : undefined}
+ active={active()}
+ current={current()}
+ gutter={option.gutter}
+ />
+ </box>
+ )
+ }}
+ </For>
+ </>
+ )}
+ </For>
+ </scrollbox>
+ </Show>
<Show when={keybinds().length} fallback={<box flexShrink={0} />}>
<box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}>
<For each={keybinds()}>