Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/features/bots/BotView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export function BotView({ bot, crew }: { bot: Bot; crew: Crew }) {
side,
writing: screens.writing,
open: (panel) => (panel === "browser" ? showBrowser() : showScreen(null)),
close: (panel) => side === panel && setSide(null),
});
const view = stateView(bot, crew.paused, t);
const tabs: Tab<Pane>[] = [
Expand Down
15 changes: 12 additions & 3 deletions app/src/features/bots/useFollowBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// so the owner sees it happen: its browser starting or a request for a
// hand opens the browser (spec 21.8), and a screen it starts writing opens
// the design area (spec 22.5), whatever panel was open. Each opens once per
// start; closing it leaves the button's dot. The browser in the owner's
// hands is never taken away. The owner can turn this off in Settings
// start; closing it leaves the button's dot. When the browser goes to rest
// (spec 21.2) its panel closes, and it opens again once the bot wakes it.
// The browser in the owner's hands is never taken away. The owner can turn this off in Settings
// (`prefs.followBot`): then only the buttons' dots say so. Coming back to a
// bot is opening it again: the panel the owner left is the one open (the
// store keeps it), and what opens by itself here takes its place.
Expand All @@ -20,16 +21,20 @@ export function useFollowBot({
side,
writing,
open,
close,
}: {
bot: Pick<Bot, "id">;
/** The panel open now, if any. */
side: string | null;
/** The screen the bot is writing now, if any. */
writing: string | null;
open(panel: Followed): void;
close(panel: Followed): void;
}): void {
const browser = useApp((state) => state.browsers[bot.id]);
const browsing = browser?.status === "open" || browser?.status === "starting";
// A browser at rest is open, but the bot is not using it.
const resting = browser?.status === "open" && browser.resting;
const browsing = browser?.status === "starting" || (browser?.status === "open" && !resting);
const asking = Boolean(browser?.ask);
const held = browser?.control === "owner";
// What was going on when the bot was opened: a browser left open stays
Expand All @@ -51,6 +56,10 @@ export function useFollowBot({
if (side !== "browser" && prefs.followBot.get()) {
open("browser");
}
} else if (resting && was.browsing && !asking && !held) {
if (side === "browser" && prefs.followBot.get()) {
close("browser");
}
}
}, [bot.id, browsing, asking]);

Expand Down
44 changes: 39 additions & 5 deletions app/src/features/browser/browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,26 +113,60 @@ describe("browser panel", () => {
});
expect(within(panel()).getByText("Live")).toBeDefined();

// The bot's turn ended: the page stays, and the panel says it rests.
// The bot's turn ended: the panel closes by itself, and the button does
// not say the bot is using it.
act(() => {
fake.browser.rest(scout.id);
});
await waitFor(() => expect(toggle().getAttribute("aria-label")).toBe("Show browser"));
// Opened again, the page is still there and the panel says it rests.
fireEvent.click(toggle());
const resting = within(panel()).getByText("Resting");
expect(resting.title).toContain("Scout isn't using the browser");
expect(within(panel()).queryByText("Live")).toBeNull();
expect(within(panel()).getByRole("figure").querySelector("img")).not.toBeNull();
// Closed, its button does not say the bot is using it.
fireEvent.click(toggle());
expect(toggle().getAttribute("aria-label")).toBe("Show browser");

// The owner's hands wake it.
fireEvent.click(toggle());
fireEvent.click(await within(panel()).findByRole("button", { name: "Take control" }));
expect(await within(panel()).findByText("You are in control")).toBeDefined();
expect(within(panel()).queryByText("Resting")).toBeNull();
expect(fake.browser.state(scout.id).resting).toBe(false);
});

test("closes when the browser rests and opens again when the bot wakes it, but not out of the owner's hands", async () => {
const { fake, scout } = crew();
await openScout(fake);
act(() => {
fake.browser.open(scout.id, "https://example.com/", "Example");
});
await waitFor(() => expect(toggle().getAttribute("aria-label")).toBe("Hide browser"));

act(() => {
fake.browser.rest(scout.id);
});
await waitFor(() =>
expect(screen.queryByRole("complementary", { name: "Scout's browser" })).toBeNull(),
);

// The bot's next step wakes it, and the panel follows.
act(() => {
fake.browser.open(scout.id, "https://example.com/next", "Next");
});
await waitFor(() => expect(toggle().getAttribute("aria-label")).toBe("Hide browser"));

// With the browser in the owner's hands, it stays.
await waitFor(() => expect(fake.browser.watching).toBe(scout.id));
act(() => {
fake.browser.frame(scout.id, "AAAA");
});
fireEvent.click(await within(panel()).findByRole("button", { name: "Take control" }));
expect(await within(panel()).findByText("You are in control")).toBeDefined();
act(() => {
fake.browser.rest(scout.id);
});
expect(toggle().getAttribute("aria-label")).toBe("Hide browser");
});

test("opens by itself when the bot starts browsing, and the button marks it once closed", async () => {
const { fake, scout } = crew();
await openScout(fake);
Expand Down
2 changes: 1 addition & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1270,7 +1270,7 @@ Esqueceu a senha? [e5 link "Recuperar acesso"]

### 21.8 App (N2)

- 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 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).
- 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").
Expand Down
Loading