From 9576e74f06a501cffc097d5fba894d38289d8a74 Mon Sep 17 00:00:00 2001 From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:56:04 -0300 Subject: [PATCH 1/2] feat(browser): set the live screen on the bot's color, with a control pill The bot's browser now sits on a desk of the bot's color, and a pill hanging from its edge says who is in control, with the button to take the browser or give it back. Why and how to take it is folded under "How it works", so an owner who knows it does not read it every time. --- app/src/browser-stage.css | 44 ++++++ app/src/features/browser/BrowserPanel.tsx | 111 +++++++++------ app/src/features/browser/HandsBars.tsx | 156 ++++++++++++++-------- app/src/features/browser/browser.test.tsx | 8 +- app/src/features/browser/hands.test.tsx | 7 + app/src/features/browser/window.test.tsx | 4 + app/src/i18n/en/browser.ts | 4 + app/src/i18n/es/browser.ts | 2 + app/src/i18n/pt-BR/browser.ts | 2 + app/src/index.css | 1 + docs/spec.md | 9 +- 11 files changed, 238 insertions(+), 110 deletions(-) create mode 100644 app/src/browser-stage.css 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..3ee15075 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, a explicação ("Para entrar numa conta ou passar de um captcha. espera enquanto isso."). 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. 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. From eb25933831c6f12bf72e4ade056917b40570fa8e Mon Sep 17 00:00:00 2001 From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:56:20 -0300 Subject: [PATCH 2/2] docs(spec): say the browser help is folded under How it works --- docs/spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec.md b/docs/spec.md index 3ee15075..76dd4ffc 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -1418,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, 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, a explicação ("Para entrar numa conta ou passar de um captcha. espera enquanto isso."). 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. +- 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. 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: 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)