From 71138ffe8c2ccabaf099af0bb8d0eced1a26a484 Mon Sep 17 00:00:00 2001 From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:16:39 -0300 Subject: [PATCH] fix(app): the bot's page fills the desk, and the notes under it fold The page was sized for the panel minus a fixed allowance, but with the owner in control the notes under the pill took more height than that, so the page shrank to fit by height and left wide bands of the desk on both sides. Now the room is measured where the page shows, inside the desk's edge, so the page fills it at its own size. Under the pill, the owner's notes are one row of buttons (teach, window, how it works) with the hints folded, and a lesson's steps scroll in a fixed height, so the page keeps the room. The pill still hangs on the desk's bottom edge, never over the page. --- app/src/features/browser/BrowserPanel.tsx | 20 +++---- app/src/features/browser/HandsBars.tsx | 69 +++++++++++++++++------ app/src/features/browser/Lesson.tsx | 20 +++---- app/src/features/browser/browser.test.tsx | 12 ++-- app/src/features/browser/tabs.test.tsx | 2 + docs/spec.md | 10 ++-- 6 files changed, 80 insertions(+), 53 deletions(-) diff --git a/app/src/features/browser/BrowserPanel.tsx b/app/src/features/browser/BrowserPanel.tsx index f13b93e8..70617023 100644 --- a/app/src/features/browser/BrowserPanel.tsx +++ b/app/src/features/browser/BrowserPanel.tsx @@ -32,12 +32,11 @@ import { WindowBar } from "./WindowBar"; const PAD = 12; /** * The bot's color around the page, in px: on the sides and top, and below, - * where the pill that says who is in control hangs over its edge. + * where the pill that says who is in control hangs over its edge. The pill + * reaches into it less than this, so it never covers the page. */ const STAGE = 10; const STAGE_BOTTOM = 26; -/** Kept under the page, for the pill and what the bot just did. */ -const UNDER = 56; export function BrowserPanel({ bot, @@ -63,13 +62,9 @@ export function BrowserPanel({ const [focused, setFocused] = useState(false); const ask = state?.ask ?? null; const windowed = state?.window === true; - // The page takes the shape of the room the panel has for it (spec 21.3). - // Notices that come and go are not counted: they never resize the page. - const [inside, body] = useRoom(); - const room = inside && { - width: inside.width - 2 * PAD - 2 * STAGE, - height: inside.height - 2 * PAD - STAGE - STAGE_BOTTOM - UNDER, - }; + // The page takes the shape of the room it has on the desk (spec 21.3), so + // it fills it at its own size, with no bands of the desk beside it. + const [room, stage] = useRoom(); useFitPage(bot, room, watched); // A new tab is blank: the owner says where it goes. const address = useRef(null); @@ -126,7 +121,6 @@ export function BrowserPanel({ )}
@@ -159,6 +153,7 @@ export function BrowserPanel({ } >
@@ -207,7 +202,7 @@ export function BrowserPanel({ )}

)} - {hands.held && } + {hands.held && } {hands.held && lesson.steps === null && ( void lesson.start()} /> )} {live && !hands.held && !ask && } diff --git a/app/src/features/browser/HandsBars.tsx b/app/src/features/browser/HandsBars.tsx index c685197a..067e3a27 100644 --- a/app/src/features/browser/HandsBars.tsx +++ b/app/src/features/browser/HandsBars.tsx @@ -5,7 +5,7 @@ // refuse to sign in here (spec 21.11). import { Check, ChevronDown, GraduationCap, Hand, Keyboard, MousePointerClick } from "lucide-react"; -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { useT } from "../../i18n"; import type { Bot } from "../../lib/protocol.gen"; import { Button } from "../../ui/Button"; @@ -91,14 +91,25 @@ export function ControlPill({ /** * The window button, and why to take the browser or use a window, folded: - * an owner who knows it does not read it every time. + * an owner who knows it does not read it every time. One row, so the page + * above keeps the room. */ -function WindowAndWhy({ hands, why }: { hands: Hands; why: string }) { +function WindowAndWhy({ + hands, + why, + children, +}: { + hands: Hands; + why: ReactNode; + /** More buttons, before the window's. */ + children?: ReactNode; +}) { const t = useT().browser.hands; const [open, setOpen] = useState(false); return ( <>
+ {children}
- {open &&

{why}

} + {open && ( +
{why}
+ )} ); } @@ -124,18 +137,25 @@ export function TakeNotes({ bot, hands }: { bot: Bot; hands: Hands }) { const t = useT().browser; return (
- + {`${t.hands.takeWhy(bot.name)} ${t.window.why(bot.name)}`}

} + />
); } -/** Under the pill, while the owner has the browser. */ +/** + * Under the pill, while the owner has the browser: what the bot asked, and + * one row of buttons, with the rest folded, so the page keeps the room. + */ export function HeldNotes({ bot, task, tabs, focused, hands, + onTeach, }: { bot: Bot; task: string | null; @@ -143,22 +163,37 @@ export function HeldNotes({ tabs: number; focused: boolean; hands: Hands; + /** Starts a lesson (spec 21.13). */ + onTeach(): void; }) { const t = useT().browser; const Hint = focused ? Keyboard : MousePointerClick; return (
-

- - {focused ? t.hands.typing : t.hands.clickToType} -

-

- {task ? t.help.asks(bot.name, task) : t.hands.waits(bot.name)} -

- {tabs > 1 &&

{t.hands.leaves(bot.name)}

} -
- -
+ {task && ( +

+ {t.help.asks(bot.name, task)} +

+ )} + +

+ + {focused ? t.hands.typing : t.hands.clickToType} +

+ {!task &&

{t.hands.waits(bot.name)}

} + {tabs > 1 &&

{t.hands.leaves(bot.name)}

} +

{t.lesson.teachWhy(bot.name)}

+

{t.window.hint}

+ + } + > + +
); } diff --git a/app/src/features/browser/Lesson.tsx b/app/src/features/browser/Lesson.tsx index abfb9f16..8491bc1b 100644 --- a/app/src/features/browser/Lesson.tsx +++ b/app/src/features/browser/Lesson.tsx @@ -3,7 +3,7 @@ // end they name the task and make it a routine, or send it for the bot to // remember. -import { AlarmClock, GraduationCap, Send, X } from "lucide-react"; +import { AlarmClock, Send, X } from "lucide-react"; import { type FormEvent, useState } from "react"; import { useT } from "../../i18n"; import type { Bot, BrowserState, LessonStep } from "../../lib/protocol.gen"; @@ -57,18 +57,11 @@ export function useLesson(bot: Pick, state: BrowserState | null): Les }; } -/** Under the owner's pill: start a lesson, or the one being given, live. */ -export function LessonArea({ bot, lesson }: { bot: Bot; lesson: Lesson }) { +/** Under the owner's pill: the lesson being given, live. */ +export function LessonArea({ lesson }: { lesson: Lesson }) { const t = useT().browser.lesson; if (lesson.steps === null) { - return ( -
- -

{t.teachWhy(bot.name)}

-
- ); + return null; } return (
{t.recording}

+ {/* A fixed height: steps coming in never resize the page above. */} {lesson.steps.length === 0 ? ( -

{t.empty}

+

{t.empty}

) : ( -
    +
      {lesson.steps.map((step, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: steps only grow at the end
    1. diff --git a/app/src/features/browser/browser.test.tsx b/app/src/features/browser/browser.test.tsx index afa7bd30..8f7ef197 100644 --- a/app/src/features/browser/browser.test.tsx +++ b/app/src/features/browser/browser.test.tsx @@ -71,20 +71,20 @@ describe("browser panel", () => { expect(within(panel()).queryByText("Opened elsewhere.com")).toBeNull(); }); - test("gives the page the shape of the room the panel has, and shows it as large as fits", async () => { + test("gives the page the shape of the room on the desk, and shows it as large as fits", async () => { const { fake, scout } = crew(); fake.browser.open(scout.id, "https://example.com/", "Example"); - // The panel under its address row, as the app measures it: 516 by 688 - // for the page, once what goes around and under it is set aside. + // The room on the desk, inside its edge, as the app measures it: all + // of it is the page's, so no band of the desk shows beside it. vi.spyOn(Element.prototype, "clientWidth", "get").mockReturnValue(560); vi.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(804); await openScout(fake); fireEvent.click(toggle()); const resized = () => fake.calls.filter((call) => call.method === "browser.resize").map((call) => call.params); - await waitFor(() => expect(resized()).toEqual([{ botId: scout.id, width: 516, height: 688 }])); + await waitFor(() => expect(resized()).toEqual([{ botId: scout.id, width: 560, height: 804 }])); // A narrow room keeps the desktop layout's width, in the room's shape. - expect(fake.browser.size(scout.id)).toEqual({ width: 800, height: 1066 }); + expect(fake.browser.size(scout.id)).toEqual({ width: 800, height: 1148 }); act(() => { fake.browser.frame(scout.id, "AAAA"); @@ -92,7 +92,7 @@ describe("browser panel", () => { // The bot's cursor lands where it acted on the taller page. const screenshot = within(panel()).getByRole("figure", { name: "What Scout sees" }); act(() => { - fake.browser.act(scout.id, "click", { x: 400, y: 1066, label: "More" }); + fake.browser.act(scout.id, "click", { x: 400, y: 1148, label: "More" }); }); const cursor = screenshot.querySelector(".bot-cursor") as HTMLElement; expect(cursor.style.top).toBe("100%"); diff --git a/app/src/features/browser/tabs.test.tsx b/app/src/features/browser/tabs.test.tsx index e39be004..349076d1 100644 --- a/app/src/features/browser/tabs.test.tsx +++ b/app/src/features/browser/tabs.test.tsx @@ -87,6 +87,8 @@ describe("the tabs of the bot's browser", () => { await openPanel(fake, scout.id); fireEvent.click(within(panel()).getByRole("button", { name: "Take control" })); expect(await within(panel()).findByText("You are in control")).toBeDefined(); + // Folded with the rest of what the owner should know. + fireEvent.click(within(panel()).getByRole("button", { name: "How it works" })); expect(within(panel()).getByText("Scout goes on in the tab you leave open.")).toBeDefined(); fireEvent.click(tab("Rye · Example, example.com")); diff --git a/docs/spec.md b/docs/spec.md index b22ae8f6..65c83e74 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -1301,7 +1301,7 @@ Cada bot tem um navegador próprio para pesquisar e usar sites: um Microsoft Edg ### 21.3 Página - **Abas:** o daemon se prende a toda página nova do navegador (`Target.setAutoAttach` no alvo do navegador, com `flatten` e `waitForDebuggerOnStart`) e a configura antes de ela rodar. Os comandos de preparo saem juntos com `Runtime.runIfWaitingForDebugger`, sem esperar resposta um a um: numa aba aberta pela página, `Page.enable` só responde depois que ela roda (visto com o Edge 154). O navegador só conta como aberto, e uma aba nova do dono só recebe um endereço, depois desse preparo: uma página aberta antes carregaria sem o daemon ver os eventos dela. Uma aba aberta pela página (`target=_blank`, `window.open`) vira a aba ativa, como para quem usa o navegador; quando ela fecha, a que era ativa antes dela volta (`Target.activateTarget`, para o navegador mostrar a mesma que o daemon). No máximo 6 abas: acima disso, fecha a que foi ativa há mais tempo. As tools agem sempre na ativa, e o bot não tem tool para trocar de aba; o dono, no controle, troca de aba, abre uma nova e vai a um endereço (21.10). O título de cada aba, para a lista que o dono vê (21.7), é o `document.title`, lido no mundo isolado (21.6) a cada carregamento e a cada mudança de endereço dentro da página: o Edge sem janela avisa quando a aba ganha um endereço, mas nunca o título dela (19). Até a página carregar, a aba fica sem título; um título que a página troca depois disso só aparece no carregamento seguinte. -- **Tela:** 1280 × 800 px, escala 1 (`Emulation.setDeviceMetricsOverride`), enquanto ninguém assiste. Com o painel aberto, a página toma o espaço que o painel tem para ela (`browser.resize`, 21.7): a largura do espaço, entre 800 e 1600 px, para a página aparecer quase no tamanho do app e o dono enxergar e mexer nela, e a altura na mesma proporção do espaço, entre 600 e 2000 px, para a tela ao vivo ocupar o painel em vez de deixar um vazio. Abaixo de 800 px a maioria dos sites troca a versão de computador pela de celular: um painel mais estreito que isso encolhe a imagem, não a página. Alargar o painel ou ocupar todo o espaço dá mais página. O tamanho vale para todas as abas, e volta a 1280 × 800 quando o último que assistia para. O que a página vê do navegador é o de um Edge comum, porque alguns sites recusam o login num navegador que um programa controla (19): o user agent é o do Edge sem a palavra `Headless`, com as marcas e a plataforma de `navigator.userAgentData` (`userAgentMetadata`, lidas uma vez ao abrir, numa página `chrome://version` que fecha em seguida: `about:blank` não tem `userAgentData`), e `navigator.webdriver` é `false` (`--disable-blink-features=AutomationControlled`). O disfarce para aí: nada de script injetado na página, e `outerWidth` e `outerHeight` continuam 0. O login de quem olha mais que isso, como o do Google, se faz numa janela de verdade (21.11). +- **Tela:** 1280 × 800 px, escala 1 (`Emulation.setDeviceMetricsOverride`), enquanto ninguém assiste. Com o painel aberto, a página toma o espaço que ela tem na mesa do painel (`browser.resize`, 21.7), medido onde ela aparece, por dentro da borda da mesa, já sem os avisos e os botões em volta: assim ela o ocupa inteiro, sem faixas da mesa dos lados. Um aviso que aparece ou some (o pedido de ajuda, as notas do controle) muda esse espaço, e a página acompanha. A largura do espaço, entre 800 e 1600 px, para a página aparecer quase no tamanho do app e o dono enxergar e mexer nela, e a altura na mesma proporção do espaço, entre 600 e 2000 px, para a tela ao vivo ocupar o painel em vez de deixar um vazio. Abaixo de 800 px a maioria dos sites troca a versão de computador pela de celular: um painel mais estreito que isso encolhe a imagem, não a página. Alargar o painel ou ocupar todo o espaço dá mais página. O tamanho vale para todas as abas, e volta a 1280 × 800 quando o último que assistia para. O que a página vê do navegador é o de um Edge comum, porque alguns sites recusam o login num navegador que um programa controla (19): o user agent é o do Edge sem a palavra `Headless`, com as marcas e a plataforma de `navigator.userAgentData` (`userAgentMetadata`, lidas uma vez ao abrir, numa página `chrome://version` que fecha em seguida: `about:blank` não tem `userAgentData`), e `navigator.webdriver` é `false` (`--disable-blink-features=AutomationControlled`). O disfarce para aí: nada de script injetado na página, e `outerWidth` e `outerHeight` continuam 0. O login de quem olha mais que isso, como o do Google, se faz numa janela de verdade (21.11). - **Diálogos** (`alert`, `confirm`, `prompt`, "sair da página?"): aceitos na hora; a resposta seguinte da tool diz ao bot o texto que apareceu. - **Escolher arquivo:** o pedido da página é interceptado e cancelado, e o bot lê que enviar arquivos pelo navegador ainda não é possível. - **Downloads:** vão para `\downloads\` (`Browser.setDownloadBehavior`) e aparecem no painel de arquivos (8.4); a tool diz quando um download termina. @@ -1371,7 +1371,7 @@ Esqueceu a senha? [e5 link "Recuperar acesso"] | `browser.list` | | `BrowserState[]`: os navegadores que não estão fechados | | `browser.watch` | `botId` | `BrowserView {state, frame}`: o estado e o último quadro. A conexão passa a receber os quadros desse bot | | `browser.unwatch` | | `null` | -| `browser.resize` | `botId`, `width`, `height` | `null`: o espaço que o painel tem para a página, em pixels do app (de 1 a 10 000 cada). O daemon ajusta a altura da página à proporção desse espaço (21.3). Só da conexão que assiste esse bot; vale também com o navegador fechado, para quando ele abrir. Cada navegador tem um tamanho só: o último pedido vale | +| `browser.resize` | `botId`, `width`, `height` | `null`: o espaço que a página tem na mesa do painel (21.3), em pixels do app (de 1 a 10 000 cada). O daemon ajusta a altura da página à proporção desse espaço (21.3). Só da conexão que assiste esse bot; vale também com o navegador fechado, para quando ele abrir. Cada navegador tem um tamanho só: o último pedido vale | | `browser.take` | `botId` | `BrowserState`: o dono assume o navegador (21.10). A conexão precisa estar assistindo esse bot, e o navegador aberto | | `browser.release` | `botId` | `BrowserState`: devolve o navegador ao bot; um pedido de ajuda aberto recebe **Pronto** | | `browser.input` | `botId`, `input` | `null`: um evento do dono na página. Só da conexão que controla | @@ -1394,7 +1394,7 @@ Esqueceu a senha? [e5 link "Recuperar acesso"] - O painel divide o lugar com detalhes e arquivos (15.1) e é redimensionável como eles (`botloft.panel.computer`, a mesma do terminal e dos arquivos, 600 px de início), com um botão para ocupar todo o espaço que o chat pode ceder. - Em cima, as abas: uma por página aberta, na ordem em que abriram, com o título e, embaixo dele, o site (ou o nome do arquivo); a ativa em destaque, "Nova aba" numa aba em branco, e **+** no fim. Não cabendo, a fila rola para o lado. Sem o controle elas só mostram o que o bot abriu, e a dica diz para assumir o controle; no controle, um clique troca de aba e **+** abre uma nova, já com o cursor na barra do endereço (21.10). - Depois, a barra do endereço: **Recarregar** (a seta circular), que vale a qualquer momento (21.10); o endereço, com o cadeado ou a roda de carregando; e "Abrir no meu navegador" (`open_url`, 15.2). O endereço é só leitura e selecionável; no controle vira um campo: Enter leva a aba até o que o dono digitou (`browser.open`), Esc desfaz. Embaixo, a tela ao vivo ocupa o espaço que sobra no painel: o app mede o painel abaixo da barra do endereço, tira a mesa em volta (10 px dos lados e em cima, 26 px embaixo) e o lugar da pílula e da legenda (56 px) e manda o resto ao daemon (`browser.resize`, 300 ms depois da última mudança, e de novo ao voltar a assistir); cada quadro aparece no maior tamanho que cabe, na proporção da página. Arrastar a borda do painel, alargá-lo ou mudar o tamanho da janela muda a página junto. O que entra e sai do painel (o pedido de ajuda e as linhas embaixo da pílula, 21.10) não entra na conta: a página nunca muda de tamanho por causa disso, só a imagem encolhe um pouco para caber. Sobre a tela, o cursor do bot (a seta, na cor dele) indo até cada ponto de `browser.action`, um anel no clique e uma legenda curta do que ele fez ("Clicou em Entrar", "Escreveu em E-mail", "Abriu exemplo.com"). -- A tela fica sobre uma mesa na cor do bot, para o painel ler como o computador dele: um degradê da cor misturada com o fundo do painel (suave no tema claro, fundo no escuro), com a página como uma janela com sombra. Com o navegador fechado, a mesa perde a cor junto com o último quadro. +- A tela fica sobre uma mesa na cor do bot, para o painel ler como o computador dele: um degradê da cor misturada com o fundo do painel (suave no tema claro, fundo no escuro), com a página como uma janela com sombra, que ocupa a mesa toda menos uma borda fina (10 px; embaixo, 26 px, onde a pílula do controle se pendura sem nunca cobrir a página). Com o navegador fechado, a mesa perde a cor junto com o último quadro. - Sem navegador aberto: o mascote e " ainda não abriu o navegador", com a explicação de que tudo que ele fizer num site aparece ali ao vivo. Depois de fechado, o último quadro fica apagado com "Navegador fechado". Em `failed`, o motivo em "Details". - No chat, a linha de uma tool `browser_*` ganha o ícone do globo e o botão "Ver no navegador", que abre o painel. - O pedido de site (21.5) é um cartão próprio: " quer usar o navegador em **wikipedia.org**", o endereço embaixo, Permitir e Negar. Respondido: "Você permitiu wikipedia.org". @@ -1452,7 +1452,7 @@ O dono pode usar o navegador do bot com as próprias mãos: clicar, arrastar, ro - Embaixo da tela ao vivo, com o navegador aberto, uma pílula pendurada na borda da mesa (21.8) diz quem está no controle: o mascote e " está no controle", com "Assumir o controle". Embaixo dela, "Entrar numa janela" (21.11) e "Como funciona", que abre a explicação ("Para entrar numa conta ou passar de um captcha. espera enquanto isso.", e o porquê da janela). A explicação começa dobrada: quem já sabe não precisa tê-la na tela toda vez. Com um pedido de ajuda aberto, quem oferece assumir é o aviso do pedido, e a pílula não aparece. Num painel estreito, a frase da pílula sai da tela (fica para o leitor de tela) e o botão diz o resto. Ao trocar de mãos, a pílula cai de novo na borda. - Com um pedido aberto, em cima da tela: " precisa de você" com a tarefa e "Assumir o controle". O botão do navegador no cabeçalho ganha o ponto de atenção. -- No controle: a tela ganha a borda de destaque, e o cursor do bot some. A pílula, com a borda de destaque, diz "Você está no controle", com "Pronto, devolver para ". Embaixo dela: se o bot espera, que ele espera (ou o que ele pediu); com mais de uma aba, que o bot continua na aba que ficar aberta. Depois, "Entrar numa janela" e o "Como funciona" dobrado com a dica da janela. As abas e o endereço passam a responder (21.8). O teclado vai para a página enquanto a tela tem o foco; sem foco, a linha embaixo da pílula diz "Clique na tela para digitar". Esc vai para a página, não devolve. +- No controle: a tela ganha a borda de destaque, e o cursor do bot some. A pílula, com a borda de destaque, diz "Você está no controle", com "Pronto, devolver para ". Embaixo dela, o que o bot pediu, numa linha, e uma fileira só de botões: "Ensinar uma tarefa" (21.13), "Entrar numa janela" e "Como funciona". O resto começa dobrado em "Como funciona", para a página ficar com o espaço: "Clique na tela para digitar" (ou, com a tela em foco, que o que se digita vai para a página), que o bot espera, que ele continua na aba que ficar aberta (com mais de uma aba), o porquê de ensinar e a dica da janela. As abas e o endereço passam a responder (21.8). O teclado vai para a página enquanto a tela tem o foco. Esc vai para a página, não devolve. ### 21.11 Entrar numa janela de verdade (N3.4) @@ -1493,7 +1493,7 @@ O dono mostra ao bot, uma vez, como fazer uma tarefa no navegador dele; o que fe - Enter, Escape e Tab sem Ctrl, Alt ou Meta viram `press`; - um endereço que o dono digita (`browser.open`) vira `open`. - **O que nunca é guardado.** O que o dono digita, cola ou escolhe: o script não lê valor de campo. Um endereço guarda só o esquema, o site e o caminho, sem usuário e senha, query e fragmento, onde viajam tokens e dados pessoais. O rótulo de cada passo tem no máximo 80 caracteres, e uma lição, 100 passos. Os passos ficam só em memória, em `BrowserState.lesson`, e não vão para o log. -- **No app.** Embaixo da pílula, "Ensinar uma tarefa" e a explicação de que o bot guarda cada passo, nunca o que é digitado. Gravando, uma faixa "Gravando a lição" mostra os passos ao vivo, numerados, com "Cancelar"; o botão da pílula vira "Terminar a lição". Terminada, um diálogo pede o nome da tarefa e mostra os passos, cada um com um X para tirar, e oferece: +- **No app.** Embaixo da pílula, "Ensinar uma tarefa", com a explicação de que o bot guarda cada passo, nunca o que é digitado, dobrada em "Como funciona" (21.10). Gravando, uma faixa "Gravando a lição" mostra os passos ao vivo, numerados, numa altura fixa que rola, para a página não mudar de tamanho a cada passo, com "Cancelar"; o botão da pílula vira "Terminar a lição". Terminada, um diálogo pede o nome da tarefa e mostra os passos, cada um com um X para tirar, e oferece: - **Fazer virar rotina…**: abre o diálogo de rotina nova (20.9) com o nome e o pedido já escritos: a lição em palavras e "Faça isso agora."; - **Mandar para guardar**: manda ao bot, como message do dono, a lição em palavras e o pedido de guardá-la na memória (5.1) para fazer quando o dono pedir. - **A lição em palavras** é montada pelo app, no idioma do dono, e pode ser editada na rotina: "Como fazer "" no navegador, do jeito que eu mostrei:", os passos numerados ("Abra ", "Clique em """, "Escreva em """, "Escreva a senha em "": peça para eu digitar", "Aperte Enter") e, se houve texto digitado, que o dono não mostra o quê: o bot usa o que a tarefa pede ou pergunta. Senhas, o bot pede com `browser_ask_owner` (21.10).