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 (
+
{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