From 6d288e86d880ceb40932e45502c2cbdfa365626a Mon Sep 17 00:00:00 2001 From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:42:50 -0300 Subject: [PATCH] feat(desktop): the app's switches right in the desktop panel The owner found the real mouse and keyboard switch well hidden in the bot's details. The desktop panel now shows the options of the app the bot is using under its last action: the real mouse and keyboard (on grants to use) and "While you are away", the same switches and dialogs as in the details. --- app/src/features/desktop/AppOptions.tsx | 30 ++++++++++++++++++++++ app/src/features/desktop/DesktopGrants.tsx | 2 +- app/src/features/desktop/DesktopPanel.tsx | 2 ++ app/src/features/desktop/panel.test.tsx | 16 ++++++++++++ app/src/i18n/en/desktop.ts | 1 + app/src/i18n/es/desktop.ts | 1 + app/src/i18n/pt-BR/desktop.ts | 1 + docs/spec.md | 2 +- 8 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 app/src/features/desktop/AppOptions.tsx diff --git a/app/src/features/desktop/AppOptions.tsx b/app/src/features/desktop/AppOptions.tsx new file mode 100644 index 00000000..497cf9d9 --- /dev/null +++ b/app/src/features/desktop/AppOptions.tsx @@ -0,0 +1,30 @@ +// The options of the app a bot is using, right in its desktop panel (spec +// 24.9, 24.10): the real mouse and keyboard and use while the owner is +// away, the same switches as in the bot's details, where the owner looks. + +import { useT } from "../../i18n"; +import type { Bot } from "../../lib/protocol.gen"; +import { useDesktopGrants } from "./DesktopGrants"; +import { RealInput } from "./RealInput"; +import { Unattended } from "./Unattended"; + +export function AppOptions({ bot, app }: { bot: Bot; app: string }) { + const words = useT().desktop.panel; + const grants = useDesktopGrants(bot.id); + const named = app.toLowerCase(); + const grant = + grants?.find((grant) => grant.scope === "app" && grant.appName?.toLowerCase() === named) ?? + grants?.find((grant) => grant.scope === "desktop"); + if (!grant) { + return null; + } + return ( +
+ {grant.level === "act" && } + +
+ ); +} diff --git a/app/src/features/desktop/DesktopGrants.tsx b/app/src/features/desktop/DesktopGrants.tsx index a7559a02..d966a064 100644 --- a/app/src/features/desktop/DesktopGrants.tsx +++ b/app/src/features/desktop/DesktopGrants.tsx @@ -12,7 +12,7 @@ import { RealInput } from "./RealInput"; import { Unattended } from "./Unattended"; /** The bot's grants, read when shown and kept current by `bot.desktop`. */ -function useDesktopGrants(botId: Bot["id"]): DesktopGrant[] | null { +export function useDesktopGrants(botId: Bot["id"]): DesktopGrant[] | null { const api = useApi(); const [grants, setGrants] = useState(null); useEffect(() => { diff --git a/app/src/features/desktop/DesktopPanel.tsx b/app/src/features/desktop/DesktopPanel.tsx index 466f4e5a..707dd463 100644 --- a/app/src/features/desktop/DesktopPanel.tsx +++ b/app/src/features/desktop/DesktopPanel.tsx @@ -16,6 +16,7 @@ import { attempt } from "../../ui/toast"; import { BotAvatar } from "../bots/BotAvatar"; import { LiveView, type StageAction } from "../browser/LiveView"; import { ComputerFooter } from "../terminal/ComputerDock"; +import { AppOptions } from "./AppOptions"; import { useDesktopView } from "./useDesktopView"; type Words = ReturnType["desktop"]["panel"]; @@ -144,6 +145,7 @@ export function DesktopPanel({ bot, onClose }: { bot: Bot; onClose(): void }) { {actionText(state?.action ?? null, t)}

+ )}

{t.shortcut}

diff --git a/app/src/features/desktop/panel.test.tsx b/app/src/features/desktop/panel.test.tsx index ef181e96..e0ea3805 100644 --- a/app/src/features/desktop/panel.test.tsx +++ b/app/src/features/desktop/panel.test.tsx @@ -108,4 +108,20 @@ describe("the desktop panel", () => { }); expect(await within(panel()).findByText("Shopping list - Notepad")).toBeDefined(); }); + + test("has the app's options right there, where the owner looks", async () => { + const { fake, scout } = await openScout(); + act(() => { + fake.desktop.grant(scout.id, "C:Windows\notepad.exe", "Notepad", "act"); + fake.desktop.use(scout.id, NOTES); + }); + const options = await within(panel()).findByRole("region", { name: "Options for Notepad" }); + const real = within(options).getByRole("switch", { name: "Real mouse and keyboard: Notepad" }); + fireEvent.click(real); + fireEvent.click(screen.getByRole("button", { name: "Turn on" })); + await waitFor(() => expect(fake.desktop.grants[0]?.realInput).toBe(true)); + expect( + within(options).getByRole("switch", { name: "While you are away: Notepad" }), + ).toBeDefined(); + }); }); diff --git a/app/src/i18n/en/desktop.ts b/app/src/i18n/en/desktop.ts index 83e98cb2..8cafa537 100644 --- a/app/src/i18n/en/desktop.ts +++ b/app/src/i18n/en/desktop.ts @@ -21,6 +21,7 @@ export const desktop = { panel: { label: (bot: string) => `${bot}'s desktop`, screen: (bot: string, app: string) => `${app}, as ${bot} sees it`, + options: (app: string) => `Options for ${app}`, heading: "Desktop", live: "Live", stoppedBadge: "Stopped", diff --git a/app/src/i18n/es/desktop.ts b/app/src/i18n/es/desktop.ts index 41b4783c..2725640e 100644 --- a/app/src/i18n/es/desktop.ts +++ b/app/src/i18n/es/desktop.ts @@ -20,6 +20,7 @@ export const desktop: Messages["desktop"] = { panel: { label: (bot) => `Escritorio de ${bot}`, screen: (bot, app) => `${app}, como lo ve ${bot}`, + options: (app) => `Opciones en ${app}`, heading: "Escritorio", live: "En vivo", stoppedBadge: "Detenido", diff --git a/app/src/i18n/pt-BR/desktop.ts b/app/src/i18n/pt-BR/desktop.ts index 2c66f00f..e7fccead 100644 --- a/app/src/i18n/pt-BR/desktop.ts +++ b/app/src/i18n/pt-BR/desktop.ts @@ -20,6 +20,7 @@ export const desktop: Messages["desktop"] = { panel: { label: (bot) => `Desktop de ${bot}`, screen: (bot, app) => `${app}, como ${bot} vê`, + options: (app) => `Opções no ${app}`, heading: "Desktop", live: "Ao vivo", stoppedBadge: "Parado", diff --git a/docs/spec.md b/docs/spec.md index 63d26d48..f76ea233 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -1785,7 +1785,7 @@ No servidor `botloft` (11), como as do navegador. - o que nunca é liberado (24.3). O botão de confirmar só acende depois de o dono marcar "Entendi os riscos"; a caixa começa desmarcada toda vez. A data em que ele aceitou fica gravada com a permissão (`accepted_risks_at`) e fica lá se ele desliga. - **Sem você na frente**: em cada permissão, de ver ou de usar, um interruptor, desligado de início. Ligar abre a tela de riscos acima; desligar é na hora. O app chama `desktop.setOptions {grantId, unattended, acceptedRisks}`; o daemon recusa ligar sem `acceptedRisks: true`. -- **Mouse e teclado de verdade**: em cada permissão de usar um app, um interruptor, desligado de início (só nas de usar). Ligar abre antes um diálogo, "Deixar usar seu mouse e teclado de verdade no ?", que diz o que muda: quando a acessibilidade não alcança, o bot move o cursor e digita como se fosse o dono; enquanto ele age, o mouse e o teclado são dele, com o aviso na tela; mexer no mouse ou numa tecla o para na hora, e Ctrl+Alt+Esc para todos; nunca campos de senha nem teclas do Windows. Desligar é na hora. O app chama `desktop.setOptions`. +- **Mouse e teclado de verdade**: em cada permissão de usar um app, um interruptor, desligado de início (só nas de usar). Ligar abre antes um diálogo, "Deixar usar seu mouse e teclado de verdade no ?", que diz o que muda: quando a acessibilidade não alcança, o bot move o cursor e digita como se fosse o dono; enquanto ele age, o mouse e o teclado são dele, com o aviso na tela; mexer no mouse ou numa tecla o para na hora, e Ctrl+Alt+Esc para todos; nunca campos de senha nem teclas do Windows. Desligar é na hora. O app chama `desktop.setOptions`. Os mesmos interruptores (mouse e teclado de verdade, nas permissões de usar, e "Sem você na frente") aparecem também no painel "Desktop" (24.9), embaixo da linha do que o bot fez, para o app da janela que ele usa (a permissão desse app pelo nome, senão a do desktop inteiro): é onde o dono está olhando quando o bot pede. ### 24.11 Dados