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