Skip to content

Commit e681d78

Browse files
authored
Merge pull request #87 from httpsphl/fix/usage-crew-name
feat(app): show each bot's crew in tokens by bot
2 parents c7e6338 + 023ce24 commit e681d78

10 files changed

Lines changed: 61 additions & 19 deletions

File tree

‎app/src/features/account/TokensByBot.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Tokens each bot used over a period (spec 8.7), in the usage dialog: the
22
// new text read plus the replies written, with the conversation reread
3-
// each time shown apart.
3+
// each time shown apart. Each bot shows its crew, as two crews can have
4+
// bots with the same name.
45

56
import { useEffect, useState } from "react";
67
import { useT } from "../../i18n";
@@ -99,7 +100,12 @@ function Row({ bot, most }: { bot: BotTokens; most: number }) {
99100
/>
100101
</div>
101102
<span className="text-muted text-xs">
102-
{u.detail(bot.turns, tokens(bot.tokens.cacheRead), reloadedTokens(bot.tokens.reloaded))}
103+
{u.detail(
104+
bot.crew,
105+
bot.turns,
106+
tokens(bot.tokens.cacheRead),
107+
reloadedTokens(bot.tokens.reloaded),
108+
)}
103109
</span>
104110
</div>
105111
</li>
@@ -119,7 +125,7 @@ function Total({ bots }: { bots: BotTokens[] }) {
119125
<span className="tabular-nums">{tokens(used)}</span>
120126
</div>
121127
<span className="text-muted text-xs">
122-
{u.detail(turns, tokens(reread), reloadedTokens(reloaded))}
128+
{u.detail(null, turns, tokens(reread), reloadedTokens(reloaded))}
123129
</span>
124130
</li>
125131
);

‎app/src/features/account/tokens.test.tsx‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ describe("tokens by bot", () => {
3434
const rows = within(list).getAllByRole("listitem");
3535
// Each turn: 12 new, 1,800 into the cache and 640 written count; 24,000 reread do not.
3636
expect(rows.map((row) => row.textContent)).toEqual([
37-
"Scout4.9kWorked 2 times · 48k reread",
38-
"Writer2.5kWorked once · 24k reread",
37+
"Scout4.9kOps · Worked 2 times · 48k reread",
38+
"Writer2.5kOps · Worked once · 24k reread",
3939
"All bots7.4kWorked 3 times · 72k reread",
4040
]);
4141

4242
fireEvent.click(within(dialog).getByRole("radio", { name: "7 days" }));
4343
expect(await within(dialog).findByText("4.9k")).toBeDefined();
44-
expect(dialog.textContent).toContain("Writer4.9kWorked 2 times");
44+
expect(dialog.textContent).toContain("Writer4.9kOps · Worked 2 times");
4545
expect(dialog.textContent).toContain("All bots9.8k");
4646
});
4747

@@ -64,7 +64,27 @@ describe("tokens by bot", () => {
6464
within(list)
6565
.getAllByRole("listitem")
6666
.map((row) => row.textContent),
67-
).toEqual(["Scout4.1kWorked 2 times · 24k reread · 72k reloaded"]);
67+
).toEqual(["Scout4.1kOps · Worked 2 times · 24k reread · 72k reloaded"]);
68+
});
69+
70+
test("bots with the same name in two crews show their crew", async () => {
71+
const fake = new FakeBotloft();
72+
const ops = fake.addCrew("Ops");
73+
const news = fake.addCrew("Newsroom");
74+
fake.chat.turn(fake.addBot(ops.id, "Scout", "Finds sources").id);
75+
fake.chat.turn(fake.addBot(news.id, "Scout", "Finds stories").id);
76+
77+
const dialog = await openUsage(fake);
78+
const list = await within(dialog).findByRole("list", { name: "Tokens by bot" });
79+
expect(
80+
within(list)
81+
.getAllByRole("listitem")
82+
.map((row) => row.textContent),
83+
).toEqual([
84+
"Scout2.5kNewsroom · Worked once · 24k reread",
85+
"Scout2.5kOps · Worked once · 24k reread",
86+
"All bots4.9kWorked 2 times · 48k reread",
87+
]);
6888
});
6989

7090
test("a period with no work says so", async () => {

‎app/src/i18n/en/account.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ export const account = {
3636
empty: "No bot worked in this period.",
3737
failed: "Could not load the tokens",
3838
archived: "archived",
39-
detail: (times: number, reread: string, reloaded: string | null) =>
40-
`Worked ${times === 1 ? "once" : `${times} times`} · ${reread} reread${
39+
/** `crew` is null on the total of all bots. */
40+
detail: (crew: string | null, times: number, reread: string, reloaded: string | null) =>
41+
`${crew === null ? "" : `${crew} · `}Worked ${times === 1 ? "once" : `${times} times`} · ${reread} reread${
4142
reloaded === null ? "" : ` · ${reloaded} reloaded`
4243
}`,
4344
total: "All bots",

‎app/src/i18n/es/account.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ export const account: Messages["account"] = {
3636
empty: "Ningún bot trabajó en este período.",
3737
failed: "No se pudieron cargar los tokens",
3838
archived: "archivado",
39-
detail: (times: number, reread: string, reloaded: string | null) =>
40-
`Trabajó ${times === 1 ? "1 vez" : `${times} veces`} · ${reread} releídos${
39+
/** `crew` is null on the total of all bots. */
40+
detail: (crew: string | null, times: number, reread: string, reloaded: string | null) =>
41+
`${crew === null ? "" : `${crew} · `}Trabajó ${times === 1 ? "1 vez" : `${times} veces`} · ${reread} releídos${
4142
reloaded === null ? "" : ` · ${reloaded} recargados`
4243
}`,
4344
total: "Todos los bots",

‎app/src/i18n/pt-BR/account.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ export const account: Messages["account"] = {
3636
empty: "Nenhum bot trabalhou nesse período.",
3737
failed: "Não deu para carregar os tokens",
3838
archived: "arquivado",
39-
detail: (times: number, reread: string, reloaded: string | null) =>
40-
`Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`} · ${reread} relidos${
39+
/** `crew` is null on the total of all bots. */
40+
detail: (crew: string | null, times: number, reread: string, reloaded: string | null) =>
41+
`${crew === null ? "" : `${crew} · `}Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`} · ${reread} relidos${
4142
reloaded === null ? "" : ` · ${reloaded} recarregados`
4243
}`,
4344
total: "Todos os bots",

‎app/src/lib/fakeUsage.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function usageHandlers(fake: FakeBotloft): Pick<Handlers, "usage.tokens">
2020
botId: bot.id,
2121
name: bot.name,
2222
color: bot.color,
23+
crew: fake.crew(bot.crewId, false).name,
2324
archived: bot.archivedAt !== null,
2425
turns: 0,
2526
tokens: { input: 0, cacheWrite: 0, reloaded: 0, cacheRead: 0, output: 0 },
@@ -37,7 +38,10 @@ export function usageHandlers(fake: FakeBotloft): Pick<Handlers, "usage.tokens">
3738
};
3839
}
3940
return [...byBot.values()].sort(
40-
(a, b) => usedTokens(b.tokens) - usedTokens(a.tokens) || a.name.localeCompare(b.name),
41+
(a, b) =>
42+
usedTokens(b.tokens) - usedTokens(a.tokens) ||
43+
a.name.localeCompare(b.name) ||
44+
a.crew.localeCompare(b.crew),
4145
);
4246
},
4347
};

‎app/src/lib/protocol.gen.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,10 @@ since: number, };
785785
* One bot's tokens since the time asked for.
786786
*/
787787
export type BotTokens = { botId: BotId, name: string, color: string,
788+
/**
789+
* The name of the bot's crew, so two bots with one name stay apart.
790+
*/
791+
crew: string,
788792
/**
789793
* The bot is archived.
790794
*/

‎crates/botloft-core/src/protocol/usage.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub struct BotTokens {
5252
pub bot_id: BotId,
5353
pub name: String,
5454
pub color: String,
55+
/// The name of the bot's crew, so two bots with one name stay apart.
56+
pub crew: String,
5557
/// The bot is archived.
5658
pub archived: bool,
5759
/// Turns that reported tokens.

‎crates/botloft-store/src/usage.rs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,22 @@ impl Store {
2020
SUM(json_extract(c.data, '$.tokens.cacheWrite')), \
2121
SUM(json_extract(c.data, '$.tokens.cacheRead')), \
2222
SUM(json_extract(c.data, '$.tokens.output')) AS output, \
23-
COALESCE(SUM(json_extract(c.data, '$.tokens.reloaded')), 0) AS reloaded \
24-
FROM chat_items c JOIN bots b ON b.id = c.bot_id \
23+
COALESCE(SUM(json_extract(c.data, '$.tokens.reloaded')), 0) AS reloaded, \
24+
w.name \
25+
FROM chat_items c JOIN bots b ON b.id = c.bot_id JOIN crews w ON w.id = b.crew_id \
2526
WHERE c.kind = 'turn' AND c.created_at >= ?1 \
2627
AND json_type(c.data, '$.tokens') = 'object' \
2728
GROUP BY b.id \
2829
ORDER BY SUM(json_extract(c.data, '$.tokens.input')) \
2930
+ SUM(json_extract(c.data, '$.tokens.cacheWrite')) - reloaded + output DESC, \
30-
b.name",
31+
b.name, w.name",
3132
)?;
3233
let rows = stmt.query_map(params![since], |row| {
3334
Ok(BotTokens {
3435
bot_id: parse_column(row, 0)?,
3536
name: row.get(1)?,
3637
color: row.get(2)?,
38+
crew: row.get(10)?,
3739
archived: row.get(3)?,
3840
turns: row.get(4)?,
3941
tokens: TokenUsage {
@@ -104,6 +106,7 @@ mod tests {
104106
assert_eq!(order, [writer, scout], "the bot that used the most first");
105107
let scout_tokens = &all[1];
106108
assert_eq!(scout_tokens.name, "scout");
109+
assert_eq!(scout_tokens.crew, "Ops");
107110
assert!(!scout_tokens.archived);
108111
assert_eq!(scout_tokens.turns, 2);
109112
assert_eq!(

‎docs/spec.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ O dono vê quantos tokens cada bot gasta: em cada turno, no chat, e somados por
416416
- **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".
417417
- **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.
418418
- **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.
419-
- **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.
419+
- **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.
420420
- Uma compactação (8.6) não vira item `turn` e não entra na conta.
421421

422422
## 9. Mensagens e entrega
@@ -624,7 +624,7 @@ Endpoint: `ws://127.0.0.1:45710/rpc`. Mensagens seguem JSON-RPC 2.0: requests co
624624
| `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 |
625625
| `screens.list` | seção 22.4 | as telas HTML do bot |
626626
| `settings.get` | | `Settings`: `startWithWindows`, `keepAwake` e `approvalWaitMinutes`, como estão no `config.toml` (6) |
627-
| `usage.tokens` | `since` (ms Unix, não negativo) | `BotTokens[]`: `botId`, `name`, `color`, `archived`, `turns` e `tokens` somados (8.7) |
627+
| `usage.tokens` | `since` (ms Unix, não negativo) | `BotTokens[]`: `botId`, `name`, `color`, `crew` (nome da equipe), `archived`, `turns` e `tokens` somados (8.7) |
628628
| `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 |
629629

630630
`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").

0 commit comments

Comments
 (0)