From c7ea1bd3b2d7171dfa873ea145dfd5e0c17432fa Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:43:09 -0300 Subject: [PATCH 1/3] fix(statusline): Drop the duplicated session name from the status row Remove the sidecar task name from the statusline and collapse redundant project and branch labels. Co-Authored-By: Codex --- .specs/features/run-usage-statusline/spec.md | 6 +++++ plugin/statusline.sh | 27 ++++---------------- tests/statusline.test.ts | 27 +++++++++++++------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/.specs/features/run-usage-statusline/spec.md b/.specs/features/run-usage-statusline/spec.md index 8921051..075ccbe 100644 --- a/.specs/features/run-usage-statusline/spec.md +++ b/.specs/features/run-usage-statusline/spec.md @@ -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. diff --git a/plugin/statusline.sh b/plugin/statusline.sh index 0e9db38..f020c7a 100755 --- a/plugin/statusline.sh +++ b/plugin/statusline.sh @@ -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); @@ -275,8 +255,12 @@ 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 @@ -284,7 +268,6 @@ const projectBranch = project && branch : undefined; const fields = [ - taskName && paint(EMBER, clean(taskName)), role && paint(EMBER, clean(role)), projectBranch, contextField(), diff --git a/tests/statusline.test.ts b/tests/statusline.test.ts index 4b653b1..1f52980 100644 --- a/tests/statusline.test.ts +++ b/tests/statusline.test.ts @@ -188,7 +188,7 @@ 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 }, @@ -196,17 +196,26 @@ describe("Claude statusline", () => { 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 result = await render({ payload: { ...payload(0.25), worktree: { branch: "feature/statusline" } } }); + + expect(stripAnsi(result.output)).toBe(`builder · ${project}/feature/statusline · ctx 68% · $0.25`); }); it("keeps the local cost when the run id is absent", async () => { From fc059544d263c6a429e188b8c635ef92cb4c85c6 Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:51:21 -0300 Subject: [PATCH 2/3] test(statusline): Cover near-miss branch names Pin that only an exact project or worktree- branch collapses, so a loose includes or endsWith match fails the suite. --- tests/statusline.test.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/statusline.test.ts b/tests/statusline.test.ts index 1f52980..1bd0203 100644 --- a/tests/statusline.test.ts +++ b/tests/statusline.test.ts @@ -213,9 +213,19 @@ describe("Claude statusline", () => { }); it("keeps the project and branch when they differ", async () => { - const result = await render({ payload: { ...payload(0.25), worktree: { branch: "feature/statusline" } } }); - - expect(stripAnsi(result.output)).toBe(`builder · ${project}/feature/statusline · ctx 68% · $0.25`); + 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 () => { From 3c39732117123c6c865d29f9244d807043634913 Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:07:08 -0300 Subject: [PATCH 3/3] docs(readme): Describe the current statusline layout The README still listed a field and the old ULTRA prefix. Describe the fields plugin/statusline.sh emits and the branch collapse rule. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a6ad2b..0ef6a96 100644 --- a/README.md +++ b/README.md @@ -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 · · · ctx · $`, 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 ` · / · ctx · tok · run $` (or `$` for local usage), showing only the branch when it matches the project name or `worktree-`, 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.