feat(coding-agent): added summed progress bar and stage overflow summary to todo HUD
- Replaced the stage-position counter in the todo HUD header with a summed progress bar counting closed and total tasks across all stages. - Added a trailing overflow summary row to announce hidden stages past the collapsed cap. - Introduced theme symbol support for filled and empty progress bar glyphs across unicode, nerd, and ascii presets.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
### Changed
|
||||
|
||||
- `/settings` rows can now carry a risk note: a warning glyph on the row plus a warning-colored line above the description. `External Thinking` (`externalThinking`, `--external-thinking`) is the first user — providers have flagged the request shape it produces as abuse, up to account-level enforcement, so both the settings entry and `--help` now say so.
|
||||
- The todo HUD header now draws a summed progress bar — `Todos ━━──────── 44/355` — counting closed/total tasks across every stage, instead of the stage-position counter (`Todos · 4/14`). Bar glyphs follow the symbol preset (`progress.filled`/`progress.empty`: heavy/light lines for unicode and nerd, `=`/`-` for ascii). Stages hidden past the collapsed cap are now announced with a trailing `… n more stages` row, since the header no longer implies them.
|
||||
- Token counting is now scoped to the model being billed rather than to a process-global tokenizer: session maintenance, stats, advisors, `/context`, snapcompact inline imaging, and `compress` each count through the owning agent's `Tokenizer` (`agent.tokenizer`). Message counting is `Tokenizer.countMessage`/`countMessages` (replacing the free `estimateTokens(message, tokenizer)` helper; the legacy shim keeps a compat `estimateTokens` export for legacy pi extensions). `estimateToolSchemaTokens`, `estimateSkillsTokens`, `computeNonMessageTokens`, and `computeNonMessageBreakdown` take an explicit tokenizer; standalone prompt inspection intentionally keeps the default estimate because it has no resolved catalog model.
|
||||
- The advisor runtime's `maintainContext` hook now receives the pending update as a message instead of a pre-computed token count — sizing it needs the advisor model's tokenizer, which the host owns.
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ import type { ConfiguredThinkingLevel } from "../thinking";
|
||||
import { tinyTitleClient } from "../tiny/title-client";
|
||||
import type { LspStartupServerInfo } from "../tools";
|
||||
import { normalizeLocalScheme } from "../tools/path-utils";
|
||||
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
|
||||
import { formatMoreItems, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
|
||||
import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
|
||||
import {
|
||||
formatPhaseDisplayName,
|
||||
@@ -2298,7 +2298,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
const multiPhase = phases.length > 1;
|
||||
const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
|
||||
// Fixed budgets keep the HUD bounded regardless of plan size / progress.
|
||||
const subsequentStageCap = 4; // stages shown after the active one (header count implies the rest)
|
||||
const subsequentStageCap = 4; // stages shown after the active one (a trailing summary row covers the rest)
|
||||
const activeTaskCap = 5; // open tasks previewed for the active stage
|
||||
|
||||
const activeDescs = this.#getActiveSubagentDescriptions();
|
||||
@@ -2336,7 +2336,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
|
||||
// One phase node. The active stage is highlighted with normal-brightness task
|
||||
// progress; other stages render their whole row (name + progress) in the
|
||||
// brighter muted gray. The root header carries overall stage progression.
|
||||
// brighter muted gray. The root header carries the summed progress bar.
|
||||
const renderPhase = (phase: TodoPhase, oneBased: number, isActive: boolean): string | string[] => {
|
||||
const label = multiPhase ? formatPhaseDisplayName(phase.name, oneBased) : phase.name;
|
||||
// Closed, not just completed: the collapsed task window hides abandoned
|
||||
@@ -2351,24 +2351,35 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
return [header, ...renderTasks(phase)];
|
||||
};
|
||||
|
||||
// Collapsed: active stage + a bounded number of following stages (the
|
||||
// header's "n/total" count implies any not shown). Expanded: every stage
|
||||
// Collapsed: active stage + a bounded number of following stages, with a
|
||||
// "… n more stages" row for anything past the cap. Expanded: every stage
|
||||
// from the top. Roman numerals stay tied to the real phase index.
|
||||
const baseIdx = expanded ? 0 : activeIdx;
|
||||
const phaseSlice = expanded ? phases.slice(baseIdx) : phases.slice(baseIdx, baseIdx + 1 + subsequentStageCap);
|
||||
const hiddenStages = phases.length - baseIdx - phaseSlice.length;
|
||||
const phaseTreeLines = renderTreeList(
|
||||
{
|
||||
items: phaseSlice,
|
||||
expanded: true,
|
||||
expanded,
|
||||
trailingSummary: hiddenStages > 0 ? formatMoreItems(hiddenStages, "stage") : "",
|
||||
renderItem: (phase, ctx) => renderPhase(phase, baseIdx + ctx.index + 1, baseIdx + ctx.index === activeIdx),
|
||||
},
|
||||
theme,
|
||||
);
|
||||
|
||||
// Header carries overall stage progression, e.g. "Todos · 1/8".
|
||||
const root =
|
||||
theme.bold(theme.fg("accent", "Todos")) +
|
||||
(multiPhase ? theme.fg("dim", ` · ${activeIdx + 1}/${phases.length}`) : "");
|
||||
// Header: overall task progress summed across every stage — a bar plus
|
||||
// closed/total counts. Per-stage rows carry their own counts.
|
||||
const totalTasks = phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
||||
const closedTasks = phases.reduce((sum, phase) => sum + phase.tasks.filter(isClosedTodo).length, 0);
|
||||
const barWidth = 20;
|
||||
// Clamp so any progress shows a sliver and only 100% fills the bar.
|
||||
let filledWidth = Math.round((closedTasks / totalTasks) * barWidth);
|
||||
if (closedTasks > 0) filledWidth = Math.max(filledWidth, 1);
|
||||
if (closedTasks < totalTasks) filledWidth = Math.min(filledWidth, barWidth - 1);
|
||||
const bar =
|
||||
theme.fg("accent", theme.progress.filled.repeat(filledWidth)) +
|
||||
theme.fg("dim", theme.progress.empty.repeat(barWidth - filledWidth));
|
||||
const root = `${theme.bold(theme.fg("accent", "Todos"))} ${bar} ${theme.fg("dim", `${closedTasks}/${totalTasks}`)}`;
|
||||
const lines = ["", root, ...phaseTreeLines.map(line => ` ${line}`)];
|
||||
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ export type SymbolKey =
|
||||
| "tree.vertical"
|
||||
| "tree.horizontal"
|
||||
| "tree.hook"
|
||||
// Progress Bar
|
||||
| "progress.filled"
|
||||
| "progress.empty"
|
||||
// Box Drawing - Rounded
|
||||
| "boxRound.topLeft"
|
||||
| "boxRound.topRight"
|
||||
@@ -242,6 +245,9 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
||||
"tree.vertical": "│",
|
||||
"tree.horizontal": "─",
|
||||
"tree.hook": "└",
|
||||
// Progress bar
|
||||
"progress.filled": "━",
|
||||
"progress.empty": "─",
|
||||
// Box (rounded)
|
||||
"boxRound.topLeft": "╭",
|
||||
"boxRound.topRight": "╮",
|
||||
@@ -472,6 +478,11 @@ const NERD_SYMBOLS: SymbolMap = {
|
||||
"tree.horizontal": "─",
|
||||
// pick: └ | alt: ╰ ⎿ ↳
|
||||
"tree.hook": "└",
|
||||
// Progress Bar (same as unicode)
|
||||
// pick: ━ | alt: ▰ ▮ ■
|
||||
"progress.filled": "━",
|
||||
// pick: ─ | alt: ▱ ▯ ╌
|
||||
"progress.empty": "─",
|
||||
// Box Drawing - Rounded (same as unicode)
|
||||
// pick: ╭ | alt: ┌ ┏ ╔
|
||||
"boxRound.topLeft": "╭",
|
||||
@@ -762,6 +773,9 @@ const ASCII_SYMBOLS: SymbolMap = {
|
||||
"tree.vertical": "|",
|
||||
"tree.horizontal": "-",
|
||||
"tree.hook": "`-",
|
||||
// Progress Bar
|
||||
"progress.filled": "=",
|
||||
"progress.empty": "-",
|
||||
// Box Drawing - Rounded (ASCII fallback)
|
||||
"boxRound.topLeft": "+",
|
||||
"boxRound.topRight": "+",
|
||||
|
||||
@@ -423,6 +423,13 @@ export class Theme {
|
||||
};
|
||||
}
|
||||
|
||||
get progress() {
|
||||
return {
|
||||
filled: this.#symbols["progress.filled"],
|
||||
empty: this.#symbols["progress.empty"],
|
||||
};
|
||||
}
|
||||
|
||||
get boxRound() {
|
||||
return {
|
||||
topLeft: this.#symbols["boxRound.topLeft"],
|
||||
|
||||
@@ -297,9 +297,11 @@ describe("InteractiveMode todo HUD anchor", () => {
|
||||
|
||||
// Lightened: no boxed top/bottom rules.
|
||||
expect(lines.some(line => line === "─".repeat(80))).toBe(false);
|
||||
// Root header carries overall stage progression (on stage 1 of 2).
|
||||
// Root header carries the summed task progress bar (1 of 4 tasks closed).
|
||||
const root = lines.find(line => line.includes("Todos"));
|
||||
expect(root).toContain("1/2");
|
||||
expect(root).toContain("1/4");
|
||||
expect(root).toContain(theme.progress.filled);
|
||||
expect(root).toContain(theme.progress.empty);
|
||||
// Active stage: highlighted header with its own task progress, expanded as a
|
||||
// connector tree; the just-completed task stays as the lead row so progress
|
||||
// is visible while the stage still has open work.
|
||||
@@ -322,7 +324,7 @@ describe("InteractiveMode todo HUD anchor", () => {
|
||||
expect(mode.todoContainer.render(80)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("omits the stage count and roman numeral for a single-phase list", () => {
|
||||
it("keeps the summed progress bar but omits the roman numeral for a single-phase list", () => {
|
||||
mode.setTodos([
|
||||
{
|
||||
name: "Tasks",
|
||||
@@ -336,16 +338,17 @@ describe("InteractiveMode todo HUD anchor", () => {
|
||||
.render(80)
|
||||
.flatMap(line => line.split("\n"))
|
||||
.map(line => Bun.stripANSI(line));
|
||||
// One stage → no redundant "1/1" stage count on the root.
|
||||
// One stage → the root still carries the summed bar and task counts.
|
||||
const root = lines.find(line => line.includes("Todos"));
|
||||
expect(root).not.toContain("/");
|
||||
expect(root).toContain("0/2");
|
||||
expect(root).toContain(theme.progress.empty);
|
||||
// The stage keeps its task progress; no roman numeral for a lone stage.
|
||||
expect(lines.some(line => line.includes("Tasks") && line.includes("0/2"))).toBe(true);
|
||||
expect(lines.some(line => line.includes("I. Tasks"))).toBe(false);
|
||||
expect(lines.some(line => line.includes("alpha"))).toBe(true);
|
||||
});
|
||||
|
||||
it("caps the visible stage list and leaves the hidden ones to the header count", () => {
|
||||
it("caps the visible stage list and summarizes the hidden ones in an overflow row", () => {
|
||||
const stage = (name: string): TodoPhase => ({ name, tasks: [{ content: `${name} task`, status: "pending" }] });
|
||||
mode.setTodos([
|
||||
stage("Discovery"),
|
||||
@@ -360,14 +363,15 @@ describe("InteractiveMode todo HUD anchor", () => {
|
||||
.render(80)
|
||||
.flatMap(line => line.split("\n"))
|
||||
.map(line => Bun.stripANSI(line));
|
||||
// Active stage + four following stages render; the rest are dropped.
|
||||
// Active stage + four following stages render; the rest collapse into a
|
||||
// trailing "… n more stages" row.
|
||||
expect(lines.some(line => line.includes("II. Two"))).toBe(true);
|
||||
expect(lines.some(line => line.includes("V. Five"))).toBe(true);
|
||||
expect(lines.some(line => line.includes("Six"))).toBe(false);
|
||||
// No overflow row — the header's "1/7" implies the hidden stages.
|
||||
expect(lines.some(line => line.includes("more"))).toBe(false);
|
||||
expect(lines.some(line => line.includes("2 more stages"))).toBe(true);
|
||||
// Root header sums tasks across every stage, hidden ones included.
|
||||
const root = lines.find(line => line.includes("Todos"));
|
||||
expect(root).toContain("1/7");
|
||||
expect(root).toContain("0/7");
|
||||
});
|
||||
|
||||
it("anchors the todo HUD as a native-scrollback live region while populated", () => {
|
||||
|
||||
Reference in New Issue
Block a user