diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index 2d305f54e639..9533a9e2ba66 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -26,6 +26,7 @@ export function DialogSessionList() { const sdk = useSDK() const local = useLocal() const toast = useToast() + const [filter, setFilter] = createSignal("") const [search, setSearch] = createDebouncedSignal("", 150) const [toDelete, setToDelete] = createSignal() const quickSwitch1 = useCommandShortcut("session.quick_switch.1") @@ -55,11 +56,16 @@ export function DialogSessionList() { const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) const sessions = createMemo(() => { - const query = search() + const query = filter() if (!query) return data.session.list() const result = searchResults() return result?.query === query ? result.sessions : [] }) + const searching = createMemo(() => { + const query = filter() + if (!query) return false + return query !== search() || searchResults.loading || searchResults()?.query !== query + }) const quickSwitchHint = createMemo(() => { const first = quickSwitch1() @@ -118,9 +124,18 @@ export function DialogSessionList() { + No sessions yet + + } skipFilter={true} current={currentSessionID()} - onFilter={setSearch} + onFilter={(query) => { + setFilter(query) + setSearch(query) + }} onMove={() => setToDelete(undefined)} onSelect={(option) => { route.navigate({ type: "session", sessionID: option.value }) diff --git a/packages/tui/src/component/dialog-skill.tsx b/packages/tui/src/component/dialog-skill.tsx index d49d054c91e3..44ef66aae00a 100644 --- a/packages/tui/src/component/dialog-skill.tsx +++ b/packages/tui/src/component/dialog-skill.tsx @@ -57,6 +57,7 @@ export function DialogSkill(props: DialogSkillProps) { {errorMessage(loadError())} - ) : undefined + ) : ( + + No skills available + + ) } /> ) diff --git a/packages/tui/src/component/dialog-tag.tsx b/packages/tui/src/component/dialog-tag.tsx index aa32c98ab7a6..226f67287974 100644 --- a/packages/tui/src/component/dialog-tag.tsx +++ b/packages/tui/src/component/dialog-tag.tsx @@ -40,6 +40,7 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) { { props.onSelect?.(option.value) dialog.clear() diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx index e5887599f942..9a7bf48b8bee 100644 --- a/packages/tui/src/ui/dialog-select.tsx +++ b/packages/tui/src/ui/dialog-select.tsx @@ -26,6 +26,7 @@ export interface DialogSelectProps { placeholder?: string footer?: JSX.Element emptyView?: JSX.Element + loading?: boolean options: DialogSelectOption[] flat?: boolean ref?: (ref: DialogSelectRef) => void @@ -603,11 +604,29 @@ export function DialogSelect(props: DialogSelectProps) { 0} fallback={ - props.emptyView ?? ( - - No results found - - ) + + Loading... + + } + > + + No matching results + + } + > + {props.emptyView ?? ( + + No items + + )} + + } > JSX.Element) { + const config = createTuiResolvedConfig() + + function Harness() { + const renderer = useRenderer() + const keymap = createDefaultOpenTuiKeymap(renderer) + const off = registerOpencodeKeymap(keymap, renderer, config) + onCleanup(off) + + return ( + + + + Promise.resolve({}) }}> + + {content()} + + + + + + ) + } + + const app = await testRender(() => , { width: 80, height: 20 }) + app.renderer.start() + return app +} + +test("distinguishes loading, unfiltered empty, and filtered no-match states", async () => { + const [loading, setLoading] = createSignal(true) + const app = await mountDialogSelect(() => ( + Could not load skills} /> + )) + try { + await app.waitForFrame((frame) => frame.includes("Loading...")) + + setLoading(false) + await app.waitForFrame((frame) => frame.includes("Could not load skills")) + + await app.mockInput.typeText("missing") + await app.waitForFrame((frame) => frame.includes("No matching results")) + expect(app.captureCharFrame()).not.toContain("Could not load skills") + } finally { + app.renderer.destroy() + } +}) + +test("uses a generic fallback for an unfiltered empty list", async () => { + const app = await mountDialogSelect(() => ) + try { + await app.waitForFrame((frame) => frame.includes("No items")) + } finally { + app.renderer.destroy() + } +})