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
6 changes: 6 additions & 0 deletions .specs/features/run-usage-statusline/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ Quando o agregado não estiver disponível, o layout degradado é:

Teste de aceitação: fornecer um payload nativo com role `builder`, projeto `codedeck`, branch `main`, contexto restante de 68%, janela local de 1.000.000 tokens e custo local de 0.25, junto com um agregado mockado de workers com 1.200 input, 800 output, 300 cached, custo 0.40 e `sessionCount` 2. Verificar exatamente `builder · codedeck/main · ctx 68% · 2.3k tok · run $0.65 · 2 agents`, sem somar a janela nativa.

9.1. A statusline SHALL NOT renderizar o nome da tarefa do sidecar de nome da sessão. O role SHALL ser o primeiro campo, pois o Claude Code já mostra o nome da sessão na borda do campo de entrada.

9.2. WHEN a branch for igual ao basename do projeto, ou igual a `worktree-` seguido pelo basename do projeto, THEN a statusline SHALL renderizar somente a branch no lugar de `projeto/branch`.

9.3. WHEN a branch for diferente dos dois casos anteriores, THEN a statusline SHALL continuar renderizando `projeto/branch`.

10. A statusline SHALL manter a coloração por faixa já existente para contexto e custo.

A formatação SHALL continuar suprimindo valores conhecidos abaixo de $0.01, como já faz `plugin/statusline.sh`. Quando `costComplete` for `false`, a soma parcial SHALL mostrar o marcador de custo desconhecido, por exemplo `run $0.42?`, e esse marcador SHALL continuar visível mesmo quando a parcela numérica for suprimida. O estado de custo desconhecido deve ser diferente de `$0.00` e do campo ausente. Um campo nativo ausente SHALL preservar o comportamento de ausência já existente, sem virar custo zero.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ npx codedeck run "review the diff on this branch" --agent codex --role reviewer

### What the session looks like

`open` hands Claude Code a settings payload built at launch, not a file on disk. It carries five things: the `codedeck-ultra` theme, the fullscreen renderer, a spinner vocabulary of its own, CodeDeck's tips in place of the built-in ones, and a startup line naming the role, the model, the effort and whether permissions are bypassed. The status line under the prompt reads `▌ULTRA <role> · <model> · <branch> · ctx <remaining> · $<cost>`, dropping any field the session cannot answer.
`open` hands Claude Code a settings payload built at launch, not a file on disk. It carries five things: the `codedeck-ultra` theme, the fullscreen renderer, a spinner vocabulary of its own, CodeDeck's tips in place of the built-in ones, and a startup line naming the role, the model, the effort and whether permissions are bypassed. The status line under the prompt reads `<role> · <project>/<branch> · ctx <remaining> · <tokens> tok · run $<cost>` (or `$<cost>` for local usage), showing only the branch when it matches the project name or `worktree-<project>`, and dropping any field the session cannot answer.

The payload is generated rather than shipped because of `${CLAUDE_PLUGIN_ROOT}`. Claude Code expands it only for hooks declared in a plugin's `hooks/hooks.json`, never for `statusLine.command`, and the failure is silent: no status line, no error, not even under `--debug`. `open` knows the real plugin directory, so it writes the resolved path.

Expand Down
27 changes: 5 additions & 22 deletions plugin/statusline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,6 @@ const role =
tail(payload.agent?.name) ??
tail(payload.session_name);

const taskName = (() => {
const sessionFile = text(process.env.CODEDECK_SESSION_FILE);
const sessionId = payload.session_id;
if (
!sessionFile ||
typeof sessionId !== "string" ||
!/^[0-9a-fA-F-]{8,}$/.test(sessionId)
) return undefined;

try {
const value = readFileSync(`${sessionFile}.${sessionId}.name`, "utf8")
.replace(/\s+/g, " ")
.trim()
.slice(0, 30);
return value || undefined;
} catch {
return undefined;
}
})();

const cwd = firstText(payload.workspace?.current_dir, payload.cwd) ?? process.cwd();
const project = text(basename(cwd)) ?? (cwd === "/" ? "/" : undefined);

Expand Down Expand Up @@ -275,16 +255,19 @@ const localField = () => {
return costAmount(local);
};

const collapsedProjectBranch = project && branch &&
(branch === project || branch === `worktree-${project}`);
const projectBranch = project && branch
? paint(TEXT, clean(project)) + paint(MUTED, "/") + paint(BLOOD, clean(branch))
? collapsedProjectBranch
? paint(BLOOD, clean(branch))
: paint(TEXT, clean(project)) + paint(MUTED, "/") + paint(BLOOD, clean(branch))
: project
? paint(TEXT, clean(project))
: branch
? paint(BLOOD, clean(branch))
: undefined;

const fields = [
taskName && paint(EMBER, clean(taskName)),
role && paint(EMBER, clean(role)),
projectBranch,
contextField(),
Expand Down
37 changes: 28 additions & 9 deletions tests/statusline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,25 +188,44 @@ describe("Claude statusline", () => {
expect(negativeCost.args).toEqual(["usage", "run-example", "--json"]);
});

it("renders the task name from its sidecar before the role", async () => {
it("omits a present task name sidecar and starts with the role", async () => {
const sessionId = "92d88cce-bdbc-46db-8573-916afd32f6f7";
const result = await render({
payload: { ...payload(0.25), session_id: sessionId },
sessionId,
taskName: " fix\tapi\nclient now with more words than allowed ",
});

expect(stripAnsi(result.output)).toBe(`fix api client now with more w · builder · ${project}/main · ctx 68% · $0.25`);
expect(stripAnsi(result.output)).toBe(`builder · ${project}/main · ctx 68% · $0.25`);
expect(stripAnsi(result.output)).not.toContain("fix api client");
});

it("omits the task name when its sidecar is missing", async () => {
const sessionId = "92d88cce-bdbc-46db-8573-916afd32f6f7";
const result = await render({
payload: { ...payload(0.25), session_id: sessionId },
sessionId,
});
it("renders only the branch when it matches the project name", async () => {
const result = await render({ payload: { ...payload(0.25), worktree: { branch: project } } });

expect(stripAnsi(result.output)).toBe(`builder · ${project}/main · ctx 68% · $0.25`);
expect(stripAnsi(result.output)).toBe(`builder · ${project} · ctx 68% · $0.25`);
});

it("renders only the branch when it is the project worktree branch", async () => {
const result = await render({ payload: { ...payload(0.25), worktree: { branch: `worktree-${project}` } } });

expect(stripAnsi(result.output)).toBe(`builder · worktree-${project} · ctx 68% · $0.25`);
});

it("keeps the project and branch when they differ", async () => {
const branches = [
"feature/statusline",
`worktree-${project}-x`,
`${project}-fix`,
`fix-${project}`,
];
const results = await Promise.all(branches.map((branch) =>
render({ payload: { ...payload(0.25), worktree: { branch } } }),
));

for (const [index, branch] of branches.entries()) {
expect(stripAnsi(results[index].output)).toBe(`builder · ${project}/${branch} · ctx 68% · $0.25`);
}
});

it("keeps the local cost when the run id is absent", async () => {
Expand Down
Loading