diff --git a/app/src/features/account/TokensByBot.tsx b/app/src/features/account/TokensByBot.tsx
index 3a9d57c7..a0cc7e82 100644
--- a/app/src/features/account/TokensByBot.tsx
+++ b/app/src/features/account/TokensByBot.tsx
@@ -1,6 +1,7 @@
// Tokens each bot used over a period (spec 8.7), in the usage dialog: the
-// new text read plus the replies written, with the conversation reread
-// each time shown apart.
+// new text read plus the replies written, with the conversation sent
+// again after a pause shown apart. Each bot shows its crew, as two crews can have
+// bots with the same name.
import { useEffect, useState } from "react";
import { useT } from "../../i18n";
@@ -99,7 +100,7 @@ function Row({ bot, most }: { bot: BotTokens; most: number }) {
/>
- {u.detail(bot.turns, tokens(bot.tokens.cacheRead), reloadedTokens(bot.tokens.reloaded))}
+ {u.detail(bot.crew, bot.turns, reloadedTokens(bot.tokens.reloaded))}
@@ -110,7 +111,6 @@ function Total({ bots }: { bots: BotTokens[] }) {
const u = useT().account.usage.tokens;
const used = bots.reduce((sum, bot) => sum + usedTokens(bot.tokens), 0);
const turns = bots.reduce((sum, bot) => sum + bot.turns, 0);
- const reread = bots.reduce((sum, bot) => sum + bot.tokens.cacheRead, 0);
const reloaded = bots.reduce((sum, bot) => sum + bot.tokens.reloaded, 0);
return (
@@ -118,9 +118,7 @@ function Total({ bots }: { bots: BotTokens[] }) {
{u.total}
{tokens(used)}
-
- {u.detail(turns, tokens(reread), reloadedTokens(reloaded))}
-
+ {u.detail(null, turns, reloadedTokens(reloaded))}
);
}
diff --git a/app/src/features/account/tokens.test.tsx b/app/src/features/account/tokens.test.tsx
index 5d756b71..168a3b6d 100644
--- a/app/src/features/account/tokens.test.tsx
+++ b/app/src/features/account/tokens.test.tsx
@@ -34,14 +34,14 @@ describe("tokens by bot", () => {
const rows = within(list).getAllByRole("listitem");
// Each turn: 12 new, 1,800 into the cache and 640 written count; 24,000 reread do not.
expect(rows.map((row) => row.textContent)).toEqual([
- "Scout4.9kWorked 2 times · 48k reread",
- "Writer2.5kWorked once · 24k reread",
- "All bots7.4kWorked 3 times · 72k reread",
+ "Scout4.9kOps · Worked 2 times",
+ "Writer2.5kOps · Worked once",
+ "All bots7.4kWorked 3 times",
]);
fireEvent.click(within(dialog).getByRole("radio", { name: "7 days" }));
expect(await within(dialog).findByText("4.9k")).toBeDefined();
- expect(dialog.textContent).toContain("Writer4.9kWorked 2 times");
+ expect(dialog.textContent).toContain("Writer4.9kOps · Worked 2 times");
expect(dialog.textContent).toContain("All bots9.8k");
});
@@ -64,7 +64,27 @@ describe("tokens by bot", () => {
within(list)
.getAllByRole("listitem")
.map((row) => row.textContent),
- ).toEqual(["Scout4.1kWorked 2 times · 24k reread · 72k reloaded"]);
+ ).toEqual(["Scout4.1kOps · Worked 2 times · 72k reloaded"]);
+ });
+
+ test("bots with the same name in two crews show their crew", async () => {
+ const fake = new FakeBotloft();
+ const ops = fake.addCrew("Ops");
+ const news = fake.addCrew("Newsroom");
+ fake.chat.turn(fake.addBot(ops.id, "Scout", "Finds sources").id);
+ fake.chat.turn(fake.addBot(news.id, "Scout", "Finds stories").id);
+
+ const dialog = await openUsage(fake);
+ const list = await within(dialog).findByRole("list", { name: "Tokens by bot" });
+ expect(
+ within(list)
+ .getAllByRole("listitem")
+ .map((row) => row.textContent),
+ ).toEqual([
+ "Scout2.5kNewsroom · Worked once",
+ "Scout2.5kOps · Worked once",
+ "All bots4.9kWorked 2 times",
+ ]);
});
test("a period with no work says so", async () => {
diff --git a/app/src/features/chat/BotRun.tsx b/app/src/features/chat/BotRun.tsx
index 0529324b..4c2b1394 100644
--- a/app/src/features/chat/BotRun.tsx
+++ b/app/src/features/chat/BotRun.tsx
@@ -37,7 +37,6 @@ function TurnEnd({ turn }: { turn: TurnItem }) {
const detail = used && {
read: tokens(used.input + used.cacheWrite - used.reloaded),
wrote: tokens(used.output),
- reread: tokens(used.cacheRead),
reloaded,
};
return (
diff --git a/app/src/features/chat/chat.test.tsx b/app/src/features/chat/chat.test.tsx
index 3a45a272..1587597a 100644
--- a/app/src/features/chat/chat.test.tsx
+++ b/app/src/features/chat/chat.test.tsx
@@ -91,9 +91,7 @@ describe("chat", () => {
expect(within(chat()).getByText("Command")).toBeDefined();
expect(within(chat()).getByText("npm test")).toBeDefined();
const done = within(chat()).getByText("Done in 4.2 s · 2.5k tokens");
- expect(done.title).toBe(
- "Took 4.2 s. Read 1.8k new tokens and wrote 640; reread 24k of the conversation, which weighs much less.",
- );
+ expect(done.title).toBe("Took 4.2 s. Read 1.8k new tokens and wrote 640.");
});
test("an approval waits for the owner, and a denial carries a note", async () => {
diff --git a/app/src/features/chat/context.test.tsx b/app/src/features/chat/context.test.tsx
index c682a61e..894dc64c 100644
--- a/app/src/features/chat/context.test.tsx
+++ b/app/src/features/chat/context.test.tsx
@@ -53,7 +53,7 @@ describe("conversation space", () => {
});
const done = screen.getByText("Done in 4.2 s · 1.6k tokens · reloaded the conversation (72k)");
expect(done.title).toBe(
- "Took 4.2 s. Read 638 new tokens and wrote 983; reread 0 of the conversation, which weighs much less. It also sent 72k of the conversation again, as the model no longer had it after a pause.",
+ "Took 4.2 s. Read 638 new tokens and wrote 983. It also sent 72k of the conversation again, as the model no longer had it after a pause.",
);
});
diff --git a/app/src/i18n/en/account.ts b/app/src/i18n/en/account.ts
index 179f4303..2abd985f 100644
--- a/app/src/i18n/en/account.ts
+++ b/app/src/i18n/en/account.ts
@@ -30,14 +30,15 @@ export const account = {
tokens: {
title: "Tokens by bot",
intro:
- "Tokens are the pieces of text a bot reads and writes. The count adds the new text read and the replies written. The conversation reread each time shows apart, as it weighs much less, and so does the conversation sent again after a pause.",
+ "Tokens are the pieces of text a bot reads and writes. The count adds the new text read and the replies written. The conversation sent again after a pause shows apart.",
period: "Period",
periods: { today: "Today", week: "7 days", month: "30 days", all: "All time" },
empty: "No bot worked in this period.",
failed: "Could not load the tokens",
archived: "archived",
- detail: (times: number, reread: string, reloaded: string | null) =>
- `Worked ${times === 1 ? "once" : `${times} times`} · ${reread} reread${
+ /** `crew` is null on the total of all bots. */
+ detail: (crew: string | null, times: number, reloaded: string | null) =>
+ `${crew === null ? "" : `${crew} · `}Worked ${times === 1 ? "once" : `${times} times`}${
reloaded === null ? "" : ` · ${reloaded} reloaded`
}`,
total: "All bots",
diff --git a/app/src/i18n/en/chat.ts b/app/src/i18n/en/chat.ts
index caedd348..13f646dc 100644
--- a/app/src/i18n/en/chat.ts
+++ b/app/src/i18n/en/chat.ts
@@ -6,7 +6,6 @@
export interface TurnTokens {
read: string;
wrote: string;
- reread: string;
/** The conversation sent again after a pause, if it was worth telling. */
reloaded: string | null;
}
@@ -64,7 +63,7 @@ export const chat = {
`Took ${time}${
tokens === null
? ""
- : `. Read ${tokens.read} new tokens and wrote ${tokens.wrote}; reread ${tokens.reread} of the conversation, which weighs much less.${
+ : `. Read ${tokens.read} new tokens and wrote ${tokens.wrote}.${
tokens.reloaded === null
? ""
: ` It also sent ${tokens.reloaded} of the conversation again, as the model no longer had it after a pause.`
diff --git a/app/src/i18n/es/account.ts b/app/src/i18n/es/account.ts
index 2338f1ec..f80635ce 100644
--- a/app/src/i18n/es/account.ts
+++ b/app/src/i18n/es/account.ts
@@ -30,14 +30,15 @@ export const account: Messages["account"] = {
tokens: {
title: "Tokens por bot",
intro:
- "Los tokens son los trozos de texto que un bot lee y escribe. La cuenta suma el texto nuevo leído y las respuestas escritas. La conversación releída cada vez aparece aparte, porque pesa mucho menos, y también la conversación enviada de nuevo tras una pausa.",
+ "Los tokens son los trozos de texto que un bot lee y escribe. La cuenta suma el texto nuevo leído y las respuestas escritas. La conversación enviada de nuevo tras una pausa aparece aparte.",
period: "Período",
periods: { today: "Hoy", week: "7 días", month: "30 días", all: "Todo" },
empty: "Ningún bot trabajó en este período.",
failed: "No se pudieron cargar los tokens",
archived: "archivado",
- detail: (times: number, reread: string, reloaded: string | null) =>
- `Trabajó ${times === 1 ? "1 vez" : `${times} veces`} · ${reread} releídos${
+ /** `crew` is null on the total of all bots. */
+ detail: (crew: string | null, times: number, reloaded: string | null) =>
+ `${crew === null ? "" : `${crew} · `}Trabajó ${times === 1 ? "1 vez" : `${times} veces`}${
reloaded === null ? "" : ` · ${reloaded} recargados`
}`,
total: "Todos los bots",
diff --git a/app/src/i18n/es/chat.ts b/app/src/i18n/es/chat.ts
index bc938d6f..6b96e918 100644
--- a/app/src/i18n/es/chat.ts
+++ b/app/src/i18n/es/chat.ts
@@ -57,7 +57,7 @@ export const chat: Messages["chat"] = {
`Tardó ${time}${
tokens === null
? ""
- : `. Leyó ${tokens.read} tokens nuevos y escribió ${tokens.wrote}; releyó ${tokens.reread} de la conversación, que pesa mucho menos.${
+ : `. Leyó ${tokens.read} tokens nuevos y escribió ${tokens.wrote}.${
tokens.reloaded === null
? ""
: ` También volvió a enviar ${tokens.reloaded} de la conversación, que el modelo ya no tenía tras una pausa.`
diff --git a/app/src/i18n/pt-BR/account.ts b/app/src/i18n/pt-BR/account.ts
index 45777e6d..eae2c4e4 100644
--- a/app/src/i18n/pt-BR/account.ts
+++ b/app/src/i18n/pt-BR/account.ts
@@ -30,14 +30,15 @@ export const account: Messages["account"] = {
tokens: {
title: "Tokens por bot",
intro:
- "Tokens são os pedaços de texto que um bot lê e escreve. A conta soma o texto novo lido e as respostas escritas. A conversa relida a cada vez aparece à parte, porque pesa bem menos, e a conversa mandada de novo depois de uma pausa também.",
+ "Tokens são os pedaços de texto que um bot lê e escreve. A conta soma o texto novo lido e as respostas escritas. A conversa mandada de novo depois de uma pausa aparece à parte.",
period: "Período",
periods: { today: "Hoje", week: "7 dias", month: "30 dias", all: "Tudo" },
empty: "Nenhum bot trabalhou nesse período.",
failed: "Não deu para carregar os tokens",
archived: "arquivado",
- detail: (times: number, reread: string, reloaded: string | null) =>
- `Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`} · ${reread} relidos${
+ /** `crew` is null on the total of all bots. */
+ detail: (crew: string | null, times: number, reloaded: string | null) =>
+ `${crew === null ? "" : `${crew} · `}Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`}${
reloaded === null ? "" : ` · ${reloaded} recarregados`
}`,
total: "Todos os bots",
diff --git a/app/src/i18n/pt-BR/chat.ts b/app/src/i18n/pt-BR/chat.ts
index 2a2f75f0..b5971f91 100644
--- a/app/src/i18n/pt-BR/chat.ts
+++ b/app/src/i18n/pt-BR/chat.ts
@@ -57,7 +57,7 @@ export const chat: Messages["chat"] = {
`Levou ${time}${
tokens === null
? ""
- : `. Leu ${tokens.read} tokens novos e escreveu ${tokens.wrote}; releu ${tokens.reread} da conversa, que pesa bem menos.${
+ : `. Leu ${tokens.read} tokens novos e escreveu ${tokens.wrote}.${
tokens.reloaded === null
? ""
: ` Também mandou de novo ${tokens.reloaded} da conversa, que o modelo já não tinha depois de uma pausa.`
diff --git a/app/src/lib/fakeUsage.ts b/app/src/lib/fakeUsage.ts
index 38670d1f..d1c42cd3 100644
--- a/app/src/lib/fakeUsage.ts
+++ b/app/src/lib/fakeUsage.ts
@@ -20,6 +20,7 @@ export function usageHandlers(fake: FakeBotloft): Pick
botId: bot.id,
name: bot.name,
color: bot.color,
+ crew: fake.crew(bot.crewId, false).name,
archived: bot.archivedAt !== null,
turns: 0,
tokens: { input: 0, cacheWrite: 0, reloaded: 0, cacheRead: 0, output: 0 },
@@ -37,7 +38,10 @@ export function usageHandlers(fake: FakeBotloft): Pick
};
}
return [...byBot.values()].sort(
- (a, b) => usedTokens(b.tokens) - usedTokens(a.tokens) || a.name.localeCompare(b.name),
+ (a, b) =>
+ usedTokens(b.tokens) - usedTokens(a.tokens) ||
+ a.name.localeCompare(b.name) ||
+ a.crew.localeCompare(b.crew),
);
},
};
diff --git a/app/src/lib/format.ts b/app/src/lib/format.ts
index 808e4631..68ec96cb 100644
--- a/app/src/lib/format.ts
+++ b/app/src/lib/format.ts
@@ -111,8 +111,8 @@ export function tokens(count: number): string {
/**
* The tokens a turn counts for (spec 8.7): new input read, with what went
* into the prompt cache, and what the model wrote. The conversation reread
- * from the cache weighs much less and is shown apart, and so is the
- * conversation written to the cache again after it expired.
+ * from the cache is left out, and the conversation written to the cache
+ * again after it expired is shown apart.
*/
export function usedTokens(usage: TokenUsage): number {
return usage.input + usage.cacheWrite - usage.reloaded + usage.output;
diff --git a/app/src/lib/protocol.gen.ts b/app/src/lib/protocol.gen.ts
index 756a998c..f47fce18 100644
--- a/app/src/lib/protocol.gen.ts
+++ b/app/src/lib/protocol.gen.ts
@@ -785,6 +785,10 @@ since: number, };
* One bot's tokens since the time asked for.
*/
export type BotTokens = { botId: BotId, name: string, color: string,
+/**
+ * The name of the bot's crew, so two bots with one name stay apart.
+ */
+crew: string,
/**
* The bot is archived.
*/
diff --git a/crates/botloft-core/src/protocol/usage.rs b/crates/botloft-core/src/protocol/usage.rs
index d0be44e2..a3ef13a4 100644
--- a/crates/botloft-core/src/protocol/usage.rs
+++ b/crates/botloft-core/src/protocol/usage.rs
@@ -52,6 +52,8 @@ pub struct BotTokens {
pub bot_id: BotId,
pub name: String,
pub color: String,
+ /// The name of the bot's crew, so two bots with one name stay apart.
+ pub crew: String,
/// The bot is archived.
pub archived: bool,
/// Turns that reported tokens.
diff --git a/crates/botloft-store/src/usage.rs b/crates/botloft-store/src/usage.rs
index 41e9380d..190c6fb4 100644
--- a/crates/botloft-store/src/usage.rs
+++ b/crates/botloft-store/src/usage.rs
@@ -20,20 +20,22 @@ impl Store {
SUM(json_extract(c.data, '$.tokens.cacheWrite')), \
SUM(json_extract(c.data, '$.tokens.cacheRead')), \
SUM(json_extract(c.data, '$.tokens.output')) AS output, \
- COALESCE(SUM(json_extract(c.data, '$.tokens.reloaded')), 0) AS reloaded \
- FROM chat_items c JOIN bots b ON b.id = c.bot_id \
+ COALESCE(SUM(json_extract(c.data, '$.tokens.reloaded')), 0) AS reloaded, \
+ w.name \
+ FROM chat_items c JOIN bots b ON b.id = c.bot_id JOIN crews w ON w.id = b.crew_id \
WHERE c.kind = 'turn' AND c.created_at >= ?1 \
AND json_type(c.data, '$.tokens') = 'object' \
GROUP BY b.id \
ORDER BY SUM(json_extract(c.data, '$.tokens.input')) \
+ SUM(json_extract(c.data, '$.tokens.cacheWrite')) - reloaded + output DESC, \
- b.name",
+ b.name, w.name",
)?;
let rows = stmt.query_map(params![since], |row| {
Ok(BotTokens {
bot_id: parse_column(row, 0)?,
name: row.get(1)?,
color: row.get(2)?,
+ crew: row.get(10)?,
archived: row.get(3)?,
turns: row.get(4)?,
tokens: TokenUsage {
@@ -104,6 +106,7 @@ mod tests {
assert_eq!(order, [writer, scout], "the bot that used the most first");
let scout_tokens = &all[1];
assert_eq!(scout_tokens.name, "scout");
+ assert_eq!(scout_tokens.crew, "Ops");
assert!(!scout_tokens.archived);
assert_eq!(scout_tokens.turns, 2);
assert_eq!(
diff --git a/docs/spec.md b/docs/spec.md
index 2a989728..10f0a8ab 100644
--- a/docs/spec.md
+++ b/docs/spec.md
@@ -413,10 +413,10 @@ A compactação custa um pedido ao modelo com a conversa inteira (barato com o c
O dono vê quantos tokens cada bot gasta: em cada turno, no chat, e somados por bot, no diálogo de uso. Nunca em dinheiro: o `total_cost_usd` do Claude Code é preço de API e soma o processo inteiro (19), e quem usa um plano do Claude não paga isso.
- **De onde vem.** O `usage` do `result` é a soma dos pedidos ao modelo daquele turno (19). O item `turn` guarda `tokens`: `input` (`input_tokens`), `cacheWrite` (`cache_creation_input_tokens`), `cacheRead` (`cache_read_input_tokens`) e `output` (`output_tokens`, com o pensamento), e ainda `reloaded`, a conta do daemon descrita abaixo (0 nos itens antigos). O `modelUsage` não serve: ele acumula entre turnos.
-- **O que conta.** O número em destaque é `input + cacheWrite - reloaded + output`: o texto novo que o modelo leu e o que escreveu. O Claude Code manda quase tudo que é novo (a mensagem, o resultado de uma ferramenta) para o cache, por isso o `input` sozinho fica perto de zero e o novo está no `cacheWrite`. O `cacheRead` é quase todo a conversa relida a cada pedido, pesa bem menos e numa conversa longa seria a maior parte do total; ele aparece à parte, como "relidos".
+- **O que conta.** O número em destaque é `input + cacheWrite - reloaded + output`: o texto novo que o modelo leu e o que escreveu. O Claude Code manda quase tudo que é novo (a mensagem, o resultado de uma ferramenta) para o cache, por isso o `input` sozinho fica perto de zero e o novo está no `cacheWrite`. O `cacheRead` é quase todo a conversa relida a cada pedido, pesa bem menos e numa conversa longa seria a maior parte do total. O app não o mostra: um número "relidos" ao lado do gasto confundia quem lê. Ele continua gravado no item `turn` e somado no `usage.tokens`.
- **A conversa recarregada.** Cada pedido manda a conversa inteira. Quando o cache expirou, porque o bot ficou parado ou o processo reiniciou, a parte da conversa que o cache não tinha mais é gravada de novo e vem no `cacheWrite`, misturada com o que é novo; num turno curto depois de uma pausa ela é quase tudo (19). O daemon separa essa parte em `reloaded`, sem guardar nada: para cada pedido da conversa principal (sem subagentes nem respostas sintéticas, contado uma vez por `message.id`), o que a conversa já tinha e não veio do cache, `min(cacheWrite, antes - cacheRead)`, onde `antes` é `input + cacheWrite + cacheRead` do pedido anterior ou, sem pedido visto, o tamanho que o Claude Code respondeu (8.6). Uma conversa nova ou recém-compactada não tem `antes` e não conta nada. O valor é limitado ao `cacheWrite` do `result`. O app mostra à parte só a partir de mil tokens, como "recarregou a conversa (72 mil)" no chat e "recarregados" no diálogo de uso.
-- **No chat.** A linha do fim do turno diz a duração e os tokens ("Concluído em 4,2 s · 2,5 mil tokens"), e a conversa recarregada quando houve; o detalhe (novos, escritos, relidos, recarregados) fica no título da linha. A unidade é o turno, não a mensagem: mensagens que chegam juntas entram num turno só, e não dá para dividir os tokens entre elas.
-- **Por bot.** `usage.tokens {since}` soma os itens `turn` com `tokens` terminados a partir de `since` (ms Unix), por bot, com nome, cor e se está arquivado, do que mais gastou para o que menos. Bots sem turno no período ficam de fora; bots excluídos levam os itens junto (7.6). O app oferece hoje (desde a meia-noite local), 7 dias, 30 dias e tudo, com o total dos bots embaixo.
+- **No chat.** A linha do fim do turno diz a duração e os tokens ("Concluído em 4,2 s · 2,5 mil tokens"), e a conversa recarregada quando houve; o detalhe (novos, escritos, recarregados) fica no título da linha. A unidade é o turno, não a mensagem: mensagens que chegam juntas entram num turno só, e não dá para dividir os tokens entre elas.
+- **Por bot.** `usage.tokens {since}` soma os itens `turn` com `tokens` terminados a partir de `since` (ms Unix), por bot, com nome, cor, o nome da equipe (dois bots podem ter o mesmo nome em equipes diferentes) e se está arquivado, do que mais gastou para o que menos. Bots sem turno no período ficam de fora; bots excluídos levam os itens junto (7.6). O app oferece hoje (desde a meia-noite local), 7 dias, 30 dias e tudo, com o total dos bots embaixo.
- Uma compactação (8.6) não vira item `turn` e não entra na conta.
## 9. Mensagens e entrega
@@ -624,7 +624,7 @@ Endpoint: `ws://127.0.0.1:45710/rpc`. Mensagens seguem JSON-RPC 2.0: requests co
| `browser.list`, `browser.watch`, `browser.unwatch`, `browser.take`, `browser.release`, `browser.input` | seção 21.7 | o navegador dos bots, a tela ao vivo e o dono no controle |
| `screens.list` | seção 22.4 | as telas HTML do bot |
| `settings.get` | | `Settings`: `startWithWindows`, `keepAwake` e `approvalWaitMinutes`, como estão no `config.toml` (6) |
-| `usage.tokens` | `since` (ms Unix, não negativo) | `BotTokens[]`: `botId`, `name`, `color`, `archived`, `turns` e `tokens` somados (8.7) |
+| `usage.tokens` | `since` (ms Unix, não negativo) | `BotTokens[]`: `botId`, `name`, `color`, `crew` (nome da equipe), `archived`, `turns` e `tokens` somados (8.7) |
| `settings.update` | `startWithWindows?, keepAwake?, approvalWaitMinutes?` (1 a 1440) | `Settings`; grava o que veio e aplica na hora (6, 14). Se a tarefa não pode mudar, nada é gravado e volta um erro |
`Crew` traz `workFolder`, o caminho da pasta de trabalho (a escolhida ou a `shared\`), `workFolderChosen` e `leadBotId`, o chefe (10.2). `Bot` traz também `permissionMode`, `model`, `modelInUse`, `effort` e `effortDefault` (7.4), `context` (8.6; `null` enquanto o Claude Code não disse o tamanho da conversa) e `lastActivity`: o último item do chat resumido em uma linha, para a lista de conversas: `kind` (`owner`, `message`, `reply`, `tool`, `approval`, `notice`), `text`, `tool` e `at`. O `text` não tem palavras do daemon: a mensagem do dono vem sem "You:", e uma ferramenta ou um pedido vêm só com o resumo (num comando, com a explicação do bot, se ele deu uma, 10.1), com a ferramenta à parte em `tool`; o app completa no idioma do dono ("Mandar uma mensagem · @writer", "Aguardando aprovação: rodar um comando").