Browse Source

Fix: High CPU / memory leak when filtering model list window to empty results (#6435)

Fayçal Mitidji 1 month ago
parent
commit
b3784588ae
1 changed files with 67 additions and 56 deletions
  1. 67 56
      packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

+ 67 - 56
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

@@ -115,11 +115,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
           setStore("selected", currentIndex)
           setStore("selected", currentIndex)
         }
         }
       }
       }
-      scroll.scrollTo(0)
+      scroll?.scrollTo(0)
     }),
     }),
   )
   )
 
 
   function move(direction: number) {
   function move(direction: number) {
+    if (flat().length === 0) return
     let next = store.selected + direction
     let next = store.selected + direction
     if (next < 0) next = flat().length - 1
     if (next < 0) next = flat().length - 1
     if (next >= flat().length) next = 0
     if (next >= flat().length) next = 0
@@ -129,6 +130,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
   function moveTo(next: number) {
   function moveTo(next: number) {
     setStore("selected", next)
     setStore("selected", next)
     props.onMove?.(selected()!)
     props.onMove?.(selected()!)
+    if (!scroll) return
     const target = scroll.getChildren().find((child) => {
     const target = scroll.getChildren().find((child) => {
       return child.id === JSON.stringify(selected()?.value)
       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> = {
   const ref: DialogSelectRef<T> = {
     get filter() {
     get filter() {
       return store.filter
       return store.filter
@@ -213,61 +215,70 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
           />
           />
         </box>
         </box>
       </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} />}>
       <Show when={keybinds().length} fallback={<box flexShrink={0} />}>
         <box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}>
         <box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}>
           <For each={keybinds()}>
           <For each={keybinds()}>