From fcf04210daa3b95ef12715529c071ea1a4554a0f Mon Sep 17 00:00:00 2001 From: Fabio Rehm Date: Tue, 15 Sep 2026 07:38:23 -0300 Subject: [PATCH] fix(provider-pi): add select keyboard shortcuts --- .../PluginPendingInteractionComposer.test.tsx | 70 ++++++++++++++++++- plugins/provider-pi/app.tsx | 63 ++++++++++++----- 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/apps/app/src/components/plugin/PluginPendingInteractionComposer.test.tsx b/apps/app/src/components/plugin/PluginPendingInteractionComposer.test.tsx index fd406085ef..5b7ebbc27d 100644 --- a/apps/app/src/components/plugin/PluginPendingInteractionComposer.test.tsx +++ b/apps/app/src/components/plugin/PluginPendingInteractionComposer.test.tsx @@ -4,8 +4,9 @@ import { useEffect, useState } from "react"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, describe, expect, it, vi } from "vitest"; -import type { PluginPendingInteraction } from "@bb/domain"; +import { defaultAppSettings, type PluginPendingInteraction } from "@bb/domain"; import type { PluginPendingInteractionProps } from "@get-bb/plugin-sdk"; +import { loadPluginApp } from "@get-bb/plugin-sdk/testing/app"; import { resetPluginSlotStoreForTest, setPluginSlotRegistrations, @@ -14,10 +15,43 @@ import { import { resetAllCrashedPluginSlotsForTest } from "./PluginSlotMount"; import { PluginPendingInteractionComposer } from "./PluginPendingInteractionComposer"; import { makePluginRegistrationSet } from "@/test/fixtures/plugins"; +import { AppCommandProvider } from "@/components/commands/AppCommandProvider"; + +vi.mock("@/hooks/queries/system-queries", () => ({ + useSystemConfig: () => ({ + data: { + generalSettings: { ...defaultAppSettings }, + keybindings: [1, 2, 3].map((digit) => ({ + command: `question.select.${digit}`, + desktopOnly: false, + shortcut: { + key: String(digit), + mod: false, + meta: false, + control: false, + alt: false, + shift: false, + }, + when: { all: ["questionOpen"], none: [] }, + })), + }, + }), +})); +vi.mock("@/lib/bb-desktop", () => ({ getBbDesktopInfo: () => null })); +const pane = vi.hoisted(() => ({ isFocused: true })); +vi.mock("@/views/thread-detail/PaneContext", () => ({ + useOptionalPaneContext: () => pane, +})); + +const piApp = await loadPluginApp(() => + import("../../../../../plugins/provider-pi/app"), +); function renderComposer(ui: React.ReactElement) { return render( - {ui}, + + {ui} + , ); } @@ -164,6 +198,38 @@ describe("PluginPendingInteractionComposer", () => { ).toBe("true"); }); + it("selects a pi option with its displayed number key", () => { + setPluginSlotRegistrations( + "provider-pi", + registrations(piApp.pendingInteractions), + ); + const data = { + requestId: "ui-1", + method: "select" as const, + options: ["Allow once", "Deny"], + }; + renderComposer( + , + ); + + expect(screen.getByText("1", { selector: "kbd" })).toBeDefined(); + expect(screen.getByText("2", { selector: "kbd" })).toBeDefined(); + fireEvent.keyDown(window, { key: "2" }); + expect( + (screen.getByRole("radio", { name: "Deny" }) as HTMLInputElement) + .checked, + ).toBe(true); + }); + it("mounts only the renderer registered by the interaction's plugin", () => { function WrongRenderer() { return
wrong plugin renderer
; diff --git a/plugins/provider-pi/app.tsx b/plugins/provider-pi/app.tsx index be72a1dbeb..f28f778631 100644 --- a/plugins/provider-pi/app.tsx +++ b/plugins/provider-pi/app.tsx @@ -1,9 +1,10 @@ -import { useMemo, useState, type FormEvent } from "react"; +import { useMemo, useState, useEffect, type FormEvent } from "react"; import { definePluginApp, type PluginPendingInteractionProps, } from "@get-bb/plugin-sdk/app"; import { Button } from "@bb/shared-ui/button"; +import { useQuestionFormHost } from "@bb/shared-ui/question-form-host"; import { cn } from "@bb/shared-ui/lib/utils"; import { PI_EXTENSION_UI_RENDERER_ID, @@ -26,10 +27,22 @@ function ExtensionUiInteraction({ cancel, }: PluginPendingInteractionProps) { const request = useMemo(() => parseRequest(interaction.payload), [interaction.payload]); + const { shortcuts, registerChoiceHandler } = useQuestionFormHost(); const [text, setText] = useState(request?.prefill ?? ""); const [selected, setSelected] = useState(null); const [busy, setBusy] = useState(false); + useEffect(() => { + if (busy || request?.method !== "select") return; + const options = request.options ?? []; + return registerChoiceHandler((index) => { + const option = options[index]; + if (option === undefined) return false; + setSelected(option); + return true; + }); + }, [busy, request, registerChoiceHandler]); + if (!request) { return (
@@ -72,24 +85,36 @@ function ExtensionUiInteraction({ {request.message ?

{request.message}

: null} {request.method === "select" ? (
- {(request.options ?? []).map((option) => ( - - ))} + {(request.options ?? []).map((option, index) => { + const shortcut = shortcuts.get(String(index)); + return ( + + ); + })}
) : null} {request.method === "input" ? (