diff --git a/app/src/browser-stage.css b/app/src/browser-stage.css new file mode 100644 index 00000000..00bf788f --- /dev/null +++ b/app/src/browser-stage.css @@ -0,0 +1,44 @@ +/* The bot's browser sits on a desk of the bot's color (spec 21.8), so the + panel reads as that bot's computer. The color is mixed with the panel, + so it is soft in the light theme and deep in the dark one. */ + +.bot-stage { + --stage: var(--bot, var(--accent)); + border-radius: 18px; + background: + radial-gradient( + 120% 90% at 15% 0%, + color-mix(in oklab, var(--stage) 55%, white 18%) 0%, + transparent 65% + ), + linear-gradient( + 160deg, + color-mix(in oklab, var(--stage) 78%, var(--panel)) 0%, + color-mix(in oklab, var(--stage) 52%, var(--panel)) 100% + ); + box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--stage) 40%, transparent); + transition: filter 500ms ease; +} + +/* The browser closed: the desk fades with the last picture. */ +.bot-stage[data-dim] { + filter: saturate(0.25) brightness(0.92); +} + +/* The page is a window on the desk. A filter, so the ring of the owner's + hands (a box shadow) stays. */ +.bot-stage figure { + filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.18)) drop-shadow(0 10px 24px rgb(0 0 0 / 0.28)); +} + +/* The pill drops onto the desk's edge when the control changes hands. */ +.control-pill { + animation: control-pill-in 320ms cubic-bezier(0.2, 0.9, 0.3, 1.2); +} + +@keyframes control-pill-in { + from { + opacity: 0; + transform: translateY(-6px) scale(0.96); + } +} diff --git a/app/src/features/browser/BrowserPanel.tsx b/app/src/features/browser/BrowserPanel.tsx index ef041089..6513c94a 100644 --- a/app/src/features/browser/BrowserPanel.tsx +++ b/app/src/features/browser/BrowserPanel.tsx @@ -5,7 +5,7 @@ // or open it in a window of its own to sign in (spec 21.11). import { Globe, LoaderCircle, Maximize2, Minimize2, Moon, X } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { type CSSProperties, useEffect, useRef, useState } from "react"; import { useT } from "../../i18n"; import type { Bot } from "../../lib/protocol.gen"; import { SYSTEM } from "../../lib/system"; @@ -17,7 +17,7 @@ import { EmptyState } from "../../ui/EmptyState"; import { SidePanel } from "../../ui/SidePanel"; import { BotAvatar } from "../bots/BotAvatar"; import { AddressBar } from "./AddressBar"; -import { AskCallout, HeldBar, TakeBar } from "./HandsBars"; +import { AskCallout, ControlPill, HeldNotes, TakeNotes } from "./HandsBars"; import { HandsLayer } from "./HandsLayer"; import { LiveView, useCaption } from "./LiveView"; import { TabStrip } from "./TabStrip"; @@ -28,8 +28,14 @@ import { WindowBar } from "./WindowBar"; /** Around the page in the panel, in px. */ const PAD = 12; -/** Kept under the page, for what the bot just did and the button to take it. */ -const UNDER = 80; +/** + * 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. + */ +const STAGE = 14; +const STAGE_BOTTOM = 30; +/** Kept under the page, for the pill and what the bot just did. */ +const UNDER = 56; export function BrowserPanel({ bot, @@ -58,8 +64,8 @@ export function BrowserPanel({ // 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, - height: inside.height - 2 * PAD - UNDER, + width: inside.width - 2 * PAD - 2 * STAGE, + height: inside.height - 2 * PAD - STAGE - STAGE_BOTTOM - UNDER, }; useFitPage(bot, room, watched); // A new tab is blank: the owner says where it goes. @@ -118,7 +124,7 @@ export function BrowserPanel({ )}
{windowed && } @@ -139,47 +145,57 @@ export function BrowserPanel({ ) : ( <> {live && ask && !hands.held && } - {hands.held && ( - - )} -
-
- - {hands.held && frame && ( - +
+
+ + {hands.held && frame && ( + + )} + + {(status !== "open" || !frame) && ( + )} - - {(status !== "open" || !frame) && ( - - )} +
+ {(hands.held || (live && !ask)) && ( + + )} {status === "open" && !hands.held && ( // Its place is kept while empty, so the page never moves for it. -

+

{caption && ( @@ -188,7 +204,16 @@ export function BrowserPanel({ )}

)} - {live && !hands.held && !ask && } + {hands.held && ( + + )} + {live && !hands.held && !ask && } )}
diff --git a/app/src/features/browser/HandsBars.tsx b/app/src/features/browser/HandsBars.tsx index 9da03be9..130f93de 100644 --- a/app/src/features/browser/HandsBars.tsx +++ b/app/src/features/browser/HandsBars.tsx @@ -1,13 +1,16 @@ -// The owner's controls in the browser panel (spec 21.10): taking the -// browser, the bot's request for a hand, and the bar that says the owner -// has it, with the button that gives it back. Each offers a window of its -// own too, for sites that refuse to sign in here (spec 21.11). +// The owner's controls in the browser panel (spec 21.10): the bot's request +// for a hand, and the pill under the live screen that says who is in +// control, with the button that takes the browser or gives it back. Under +// it, what the owner should know, and a window of its own for sites that +// refuse to sign in here (spec 21.11). -import { Check, Hand, Keyboard, MousePointerClick } from "lucide-react"; +import { Check, ChevronDown, Hand, Keyboard, MousePointerClick } from "lucide-react"; +import { useState } from "react"; import { useT } from "../../i18n"; import type { Bot } from "../../lib/protocol.gen"; import { Button } from "../../ui/Button"; import { Callout } from "../../ui/Callout"; +import { BotAvatar, moodOf } from "../bots/BotAvatar"; import type { Hands } from "./useHands"; import { WindowButton } from "./WindowBar"; @@ -36,8 +39,84 @@ export function AskCallout({ bot, task, hands }: { bot: Bot; task: string; hands ); } -/** The owner has the browser. */ -export function HeldBar({ +/** + * Who is in control, hanging from the bottom of the live screen: the bot, + * with the button to take it, or the owner, with the one to give it back. + */ +export function ControlPill({ bot, hands }: { bot: Bot; hands: Hands }) { + const t = useT().browser.hands; + const held = hands.held; + return ( +
+ {held ? ( + + ) : ( + + )} + + {held ? t.holding : t.botHas(bot.name)} + + +
+ ); +} + +/** + * 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. + */ +function WindowAndWhy({ hands, why }: { hands: Hands; why: string }) { + const t = useT().browser.hands; + const [open, setOpen] = useState(false); + return ( + <> +
+ + +
+ {open &&

{why}

} + + ); +} + +/** Under the pill, while the bot has the browser: how to take it. */ +export function TakeNotes({ bot, hands }: { bot: Bot; hands: Hands }) { + const t = useT().browser; + return ( +
+ +
+ ); +} + +/** Under the pill, while the owner has the browser. */ +export function HeldNotes({ bot, task, tabs, @@ -54,59 +133,18 @@ export function HeldBar({ const t = useT().browser; const Hint = focused ? Keyboard : MousePointerClick; return ( -
-
- -
-

{t.hands.holding}

-

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

- {tabs > 1 &&

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

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

{t.window.hint}

-
- ); -} - -/** Under the live screen: the owner may take the browser at any time. */ -export function TakeBar({ bot, hands }: { bot: Bot; hands: Hands }) { - const t = useT().browser; - return ( -
- - -

- {t.hands.takeWhy(bot.name)} {t.window.why(bot.name)} +

+

+ + {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)}

} +
+ +
); } diff --git a/app/src/features/browser/browser.test.tsx b/app/src/features/browser/browser.test.tsx index 065ce76c..988a0426 100644 --- a/app/src/features/browser/browser.test.tsx +++ b/app/src/features/browser/browser.test.tsx @@ -74,7 +74,7 @@ describe("browser panel", () => { test("gives the page the shape of the room the panel has, 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: 536 by 700 + // The panel under its address row, as the app measures it: 508 by 680 // for the page, once what goes around and under it is set aside. vi.spyOn(Element.prototype, "clientWidth", "get").mockReturnValue(560); vi.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(804); @@ -82,9 +82,9 @@ describe("browser panel", () => { 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: 536, height: 700 }])); + await waitFor(() => expect(resized()).toEqual([{ botId: scout.id, width: 508, height: 680 }])); // As wide as ever, and as tall as the room's shape asks. - expect(fake.browser.size(scout.id)).toEqual({ width: 1280, height: 1671 }); + expect(fake.browser.size(scout.id)).toEqual({ width: 1280, height: 1713 }); 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: 640, y: 1671, label: "More" }); + fake.browser.act(scout.id, "click", { x: 640, y: 1713, label: "More" }); }); const cursor = screenshot.querySelector(".bot-cursor") as HTMLElement; expect(cursor.style.top).toBe("100%"); diff --git a/app/src/features/browser/hands.test.tsx b/app/src/features/browser/hands.test.tsx index 2d976515..d774c73c 100644 --- a/app/src/features/browser/hands.test.tsx +++ b/app/src/features/browser/hands.test.tsx @@ -39,8 +39,15 @@ describe("the owner's hands in the browser", () => { const { fake, scout } = crew(); await openScout(fake); await openPanel(fake, scout.id); + // The pill under the screen says who has it. + const pill = () => + within(panel()) + .getAllByRole("status") + .find((s) => s.closest(".control-pill")); + expect(pill()?.textContent).toContain("Scout is in control"); fireEvent.click(within(panel()).getByRole("button", { name: "Take control" })); expect(await within(panel()).findByText("You are in control")).toBeDefined(); + expect(pill()?.textContent).toContain("Done, give it back to Scout"); expect(called(fake, "browser.take")).toEqual([{ botId: scout.id }]); const page = within(panel()).getByRole("application", { diff --git a/app/src/features/browser/window.test.tsx b/app/src/features/browser/window.test.tsx index 19488414..296c7468 100644 --- a/app/src/features/browser/window.test.tsx +++ b/app/src/features/browser/window.test.tsx @@ -29,6 +29,9 @@ const panel = () => screen.getByRole("complementary", { name: "Scout's browser" describe("the browser in a window of its own", () => { test("opens for signing in, says the bot waits, and goes back when the window closes", async () => { const { fake, scout } = await openScout(); + // Why is folded until the owner asks. + expect(within(panel()).queryByText(/like Google/)).toBeNull(); + fireEvent.click(within(panel()).getByRole("button", { name: "How it works" })); expect(within(panel()).getByText(/like Google/)).toBeDefined(); fireEvent.click(within(panel()).getByRole("button", { name: "Sign in in a window" })); @@ -49,6 +52,7 @@ describe("the browser in a window of its own", () => { await openScout(); fireEvent.click(within(panel()).getByRole("button", { name: "Take control" })); expect(await within(panel()).findByText("You are in control")).toBeDefined(); + fireEvent.click(within(panel()).getByRole("button", { name: "How it works" })); expect(within(panel()).getByText(/won't let you sign in here\?/)).toBeDefined(); expect(within(panel()).getByRole("button", { name: "Sign in in a window" })).toBeDefined(); }); diff --git a/app/src/i18n/en/browser.ts b/app/src/i18n/en/browser.ts index 1a21372b..1bdc5d42 100644 --- a/app/src/i18n/en/browser.ts +++ b/app/src/i18n/en/browser.ts @@ -67,6 +67,10 @@ export const browser = { }, hands: { take: "Take control", + /** In the pill under the live screen while the bot has the browser. */ + botHas: (bot: string) => `${bot} is in control`, + /** Opens why to take the browser, folded under the pill. */ + howItWorks: "How it works", takeWhy: (bot: string) => `To sign in to an account or get past a captcha. ${bot} waits meanwhile.`, takeFailed: "Could not take the browser", diff --git a/app/src/i18n/es/browser.ts b/app/src/i18n/es/browser.ts index e519b773..22fd0671 100644 --- a/app/src/i18n/es/browser.ts +++ b/app/src/i18n/es/browser.ts @@ -68,6 +68,8 @@ export const browser: Messages["browser"] = { }, hands: { take: "Tomar el control", + botHas: (bot) => `${bot} tiene el control`, + howItWorks: "Cómo funciona", takeWhy: (bot) => `Para entrar en una cuenta o pasar un captcha. ${bot} espera mientras tanto.`, takeFailed: "No se pudo tomar el navegador", holding: "Tienes el control", diff --git a/app/src/i18n/pt-BR/browser.ts b/app/src/i18n/pt-BR/browser.ts index 18bfb4d6..38ecba57 100644 --- a/app/src/i18n/pt-BR/browser.ts +++ b/app/src/i18n/pt-BR/browser.ts @@ -69,6 +69,8 @@ export const browser: Messages["browser"] = { }, hands: { take: "Assumir o controle", + botHas: (bot) => `${bot} está no controle`, + howItWorks: "Como funciona", takeWhy: (bot) => `Para entrar numa conta ou passar de um captcha. ${bot} espera enquanto isso.`, takeFailed: "Não foi possível assumir o navegador", diff --git a/app/src/index.css b/app/src/index.css index a3ec5e07..c46d631b 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -18,6 +18,7 @@ @import "./files.css"; @import "./search.css"; @import "./activity.css"; +@import "./browser-stage.css"; @import "./motion-less.css"; @import "./paused.css"; @import "./offscreen.css"; diff --git a/docs/spec.md b/docs/spec.md index 5963eb0e..76dd4ffc 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -1361,7 +1361,8 @@ Esqueceu a senha? [e5 link "Recuperar acesso"] - No cabeçalho do bot, o botão **Navegador** (globo), ao lado de Arquivos. Com quadros chegando, o título do painel ganha a etiqueta "Ao vivo"; com o navegador descansando (21.2), "Em descanso", e a dica diz que ele não gasta o computador e volta na hora em que o bot ou o dono precisar. A tela mostra o quadro tirado ao descansar. Com o navegador aberto e em uso e o painel fechado, o botão ganha um ponto que pulsa; descansando, não. O painel abre sozinho quando o navegador do bot sobe, quando acorda do descanso para o bot e quando ele pede uma mão (21.10); um navegador que já estava aberto quando o dono abriu o bot fica atrás do botão (15.1). Quando o navegador entra em descanso, o painel fecha na hora, deslizando para a direita, e o chat volta a ocupar o espaço; aberto de novo pelo dono, mostra "Em descanso". Nas mãos do dono, ou com um pedido de mão aberto, ele fica. As duas coisas seguem a escolha "Abrir o navegador e as telas quando um bot começar a usar" (15.1): desligada, o painel só abre e fecha pelo botão. - O painel divide o lugar com detalhes e arquivos (15.1) e é redimensionável como eles (`botloft.panel.browser`, 560 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 o lugar da legenda e do botão de assumir (80 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 (a legenda, o pedido de ajuda, a faixa do controle, 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"). +- 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 (14 px dos lados e em cima, 30 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. - 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". @@ -1417,9 +1418,9 @@ O dono pode usar o navegador do bot com as próprias mãos: clicar, arrastar, ro **No app** (21.8): -- Embaixo da tela ao vivo, com o navegador aberto: "Assumir o controle" e a explicação ("Para entrar numa conta ou passar de um captcha. espera enquanto isso."). +- 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. Uma faixa diz "Você está no controle" e, se o bot espera, que ele espera, com "Pronto, devolver para ". Com mais de uma aba, diz também que o bot continua na aba que ficar aberta. 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 faixa 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: 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. ### 21.11 Entrar numa janela de verdade (N3.4) @@ -1431,7 +1432,7 @@ O login do Google, e o de outros sites que olham mais que o user agent e `naviga - **Enquanto a janela está aberta:** `BrowserState` fica `closed`, com `window` e `control: "owner"`. As tools do bot esperam como com o dono no controle (21.10). `browser_close` e o fechamento por falta de uso não a tocam, e quem respondia a um pedido de ajuda não devolve o navegador ao bot. - **Ao fechar a janela:** o navegador volta ao bot, fechado; a próxima tool abre o sem janela no mesmo perfil, com os cookies do login (visto com o Edge 154: um cookie gravado na janela aparece no sem janela, 19). Um pedido de ajuda aberto (`browser_ask_owner`) recebe **Pronto**, e a tool diz ao bot que o navegador fechou e que os logins ficam; ele abre a página de novo com `browser_open`. -**No app** (21.8): "Entrar numa janela" fica ao lado de "Assumir o controle" embaixo da tela ao vivo, no aviso de um pedido de ajuda e na faixa do dono no controle. Embaixo da tela, a explicação ganha: "Para sites que recusam o login aqui, como o Google: o navegador de abre numa janela própria, e o login fica com ." Com a janela aberta, uma faixa no painel diz "Aberto numa janela": entre na conta na janela que abriu e feche-a depois; o bot espera enquanto isso e fica com o login. A janela não fecha pelo app: fechá-la à força pode perder o login ainda não gravado. Onde o dono faz o login pelo painel (o aviso de um pedido de ajuda e a faixa do dono no controle), uma linha lembra: "O site não deixa entrar aqui? Use Entrar numa janela e feche a janela quando terminar." O cartão de ajuda no chat diz o mesmo. +**No app** (21.8): "Entrar numa janela" fica embaixo da pílula do controle (com o bot ou com o dono) e no aviso de um pedido de ajuda. Embaixo da tela, a explicação ganha: "Para sites que recusam o login aqui, como o Google: o navegador de abre numa janela própria, e o login fica com ." Com a janela aberta, uma faixa no painel diz "Aberto numa janela": entre na conta na janela que abriu e feche-a depois; o bot espera enquanto isso e fica com o login. A janela não fecha pelo app: fechá-la à força pode perder o login ainda não gravado. Onde o dono faz o login pelo painel (o aviso de um pedido de ajuda e a faixa do dono no controle), uma linha lembra: "O site não deixa entrar aqui? Use Entrar numa janela e feche a janela quando terminar." O cartão de ajuda no chat diz o mesmo. **Para o bot:** as regras (5.1) dizem que, quando um site recusa o login no navegador dele ("este navegador pode não ser seguro", um login que falha ou volta ao começo), ele pede ajuda com `browser_ask_owner` e diz ao dono, na língua dele, para usar o botão de entrar numa janela; depois abre a página de novo. A descrição de `browser_ask_owner` diz o mesmo.