feat(coding-agent/modes): added collapse animation for todo header bar
- Implemented an animated collapse effect for the interactive mode todo header bar when tasks settle. - Added timer management and render loop updates for the todo bar collapse transition.
This commit is contained in:
@@ -7,12 +7,13 @@
|
|||||||
- Added `qwenTemplateReasoningEffort` to the `models.yml` `compat` schema, so the auto-enabled Qwen 3.8+ template effort dialect (`chat_template_kwargs.reasoning_effort`) can be switched off per provider/model for strict local servers that reject unknown `chat_template_kwargs`.
|
- Added `qwenTemplateReasoningEffort` to the `models.yml` `compat` schema, so the auto-enabled Qwen 3.8+ template effort dialect (`chat_template_kwargs.reasoning_effort`) can be switched off per provider/model for strict local servers that reject unknown `chat_template_kwargs`.
|
||||||
- Added `tokenizer` to custom model and `modelOverrides` configuration. It overrides the catalog-resolved local tokenizer family for a model when a proxy serves a known model id with a different tokenizer.
|
- Added `tokenizer` to custom model and `modelOverrides` configuration. It overrides the catalog-resolved local tokenizer family for a model when a proxy serves a known model id with a different tokenizer.
|
||||||
- Added `extendedContext` setting (`/settings` → Context → General, default on). When off, models with a premium long-context price tier (OpenAI GPT-5.6 Sol/Terra/Luna bill 2x input / 1.5x output above 272K input tokens, on both the API and subscription Codex) are capped at the standard-pricing threshold — they appear as 272K again and compaction fires before a request crosses into premium billing. Toggling mid-session re-clamps or restores the active model's window immediately. Anthropic Claude 4.6+ serves its full 1M window at standard pricing, so no Anthropic model is affected.
|
- Added `extendedContext` setting (`/settings` → Context → General, default on). When off, models with a premium long-context price tier (OpenAI GPT-5.6 Sol/Terra/Luna bill 2x input / 1.5x output above 272K input tokens, on both the API and subscription Codex) are capped at the standard-pricing threshold — they appear as 272K again and compaction fires before a request crosses into premium billing. Toggling mid-session re-clamps or restores the active model's window immediately. Anthropic Claude 4.6+ serves its full 1M window at standard pricing, so no Anthropic model is affected.
|
||||||
|
- Added click-to-toggle and drag-to-reorder controls for list-valued `/settings` editors.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Replaced the single `compaction.strategy` / `compaction.remoteEnabled` policy with ordered `compaction.methodOrder` preferences. The default now tries OpenAI-compatible server compaction, snapcompact, handoff, shake, then soft compaction; unavailable or failed methods advance through that list.
|
|
||||||
|
|
||||||
|
- Replaced the single `compaction.strategy` / `compaction.remoteEnabled` policy with ordered `compaction.methodOrder` preferences. The default now tries OpenAI-compatible server compaction, snapcompact, handoff, shake, then soft compaction; unavailable or failed methods advance through that list.
|
||||||
- `/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.
|
- `/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.
|
- The todo HUD header now draws a summed progress bar counting closed/total tasks across every stage. Once all tasks close, the bar smoothly collapses before the row disappears.
|
||||||
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -451,6 +451,11 @@ const MODEL_CYCLE_TRACK_CLEAR_MS = 4000;
|
|||||||
|
|
||||||
const SUBAGENT_HUD_VISIBLE_LIMIT = 8;
|
const SUBAGENT_HUD_VISIBLE_LIMIT = 8;
|
||||||
const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100;
|
const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100;
|
||||||
|
// Instant, independent of `tasks.todoClearDelay`: fires the moment every todo
|
||||||
|
// closes, shrinking the header bar to nothing before the row is dropped. The
|
||||||
|
// phase/task tree below is unaffected and keeps following the clear delay.
|
||||||
|
const TODO_BAR_COLLAPSE_DURATION_MS = 260;
|
||||||
|
const TODO_BAR_COLLAPSE_TICK_MS = 1000 / 30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
||||||
@@ -556,6 +561,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|||||||
loopLimit: LoopLimitRuntime | undefined = undefined;
|
loopLimit: LoopLimitRuntime | undefined = undefined;
|
||||||
#loopAutoSubmitTimer: NodeJS.Timeout | undefined;
|
#loopAutoSubmitTimer: NodeJS.Timeout | undefined;
|
||||||
#todoAutoClearTimer: NodeJS.Timeout | undefined;
|
#todoAutoClearTimer: NodeJS.Timeout | undefined;
|
||||||
|
#todoBarCollapseTimer: NodeJS.Timeout | undefined;
|
||||||
|
#todoBarCollapseStartedAt: number | undefined;
|
||||||
|
#todoBarCollapsed = false;
|
||||||
|
#todoListWasSettled = false;
|
||||||
#modelCycleClearTimer: NodeJS.Timeout | undefined;
|
#modelCycleClearTimer: NodeJS.Timeout | undefined;
|
||||||
#nextAppearanceRequestToken = 1;
|
#nextAppearanceRequestToken = 1;
|
||||||
#appearanceRefreshRequest: { token: TerminalAppearanceRequestToken; deadline: number } | undefined;
|
#appearanceRefreshRequest: { token: TerminalAppearanceRequestToken; deadline: number } | undefined;
|
||||||
@@ -2215,6 +2224,39 @@ export class InteractiveMode implements InteractiveModeContext {
|
|||||||
this.#todoAutoClearTimer.unref?.();
|
this.#todoAutoClearTimer.unref?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cancelTodoBarCollapseAnimation(): void {
|
||||||
|
if (!this.#todoBarCollapseTimer) return;
|
||||||
|
clearInterval(this.#todoBarCollapseTimer);
|
||||||
|
this.#todoBarCollapseTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
#todoBarCollapseFraction(): number {
|
||||||
|
if (this.#todoBarCollapseStartedAt === undefined) return 0;
|
||||||
|
const elapsed = Date.now() - this.#todoBarCollapseStartedAt;
|
||||||
|
return Math.min(1, Math.max(0, elapsed / TODO_BAR_COLLAPSE_DURATION_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires once, the instant the todo list first fully closes: shrinks the
|
||||||
|
* header bar to nothing over `TODO_BAR_COLLAPSE_DURATION_MS` and then drops
|
||||||
|
* the whole row. Independent of `tasks.todoClearDelay`, which governs when
|
||||||
|
* the remaining phase/task tree disappears — the tree is untouched here.
|
||||||
|
*/
|
||||||
|
#startTodoBarCollapseAnimation(): void {
|
||||||
|
this.#cancelTodoBarCollapseAnimation();
|
||||||
|
this.#todoBarCollapsed = false;
|
||||||
|
this.#todoBarCollapseStartedAt = Date.now();
|
||||||
|
this.#todoBarCollapseTimer = setInterval(() => {
|
||||||
|
if (this.#todoBarCollapseFraction() >= 1) {
|
||||||
|
this.#todoBarCollapsed = true;
|
||||||
|
this.#cancelTodoBarCollapseAnimation();
|
||||||
|
}
|
||||||
|
this.#renderTodoList();
|
||||||
|
this.ui.requestRender();
|
||||||
|
}, TODO_BAR_COLLAPSE_TICK_MS);
|
||||||
|
this.#todoBarCollapseTimer.unref?.();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the ctrl+p model-role cycle chip track into its own anchored
|
* Render the ctrl+p model-role cycle chip track into its own anchored
|
||||||
* container (just above the editor), mirroring the todo HUD: the container is
|
* container (just above the editor), mirroring the todo HUD: the container is
|
||||||
@@ -2293,7 +2335,23 @@ export class InteractiveMode implements InteractiveModeContext {
|
|||||||
#renderTodoList(): void {
|
#renderTodoList(): void {
|
||||||
this.todoContainer.clear();
|
this.todoContainer.clear();
|
||||||
const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
|
const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
|
||||||
if (phases.length === 0) return;
|
if (phases.length === 0) {
|
||||||
|
this.#cancelTodoBarCollapseAnimation();
|
||||||
|
this.#todoBarCollapsed = false;
|
||||||
|
this.#todoBarCollapseStartedAt = undefined;
|
||||||
|
this.#todoListWasSettled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const settled = this.#isTodoListSettled(phases);
|
||||||
|
if (settled && !this.#todoListWasSettled) {
|
||||||
|
this.#startTodoBarCollapseAnimation();
|
||||||
|
} else if (!settled && this.#todoListWasSettled) {
|
||||||
|
this.#cancelTodoBarCollapseAnimation();
|
||||||
|
this.#todoBarCollapsed = false;
|
||||||
|
this.#todoBarCollapseStartedAt = undefined;
|
||||||
|
}
|
||||||
|
this.#todoListWasSettled = settled;
|
||||||
|
|
||||||
const expanded = this.todoExpanded;
|
const expanded = this.todoExpanded;
|
||||||
const multiPhase = phases.length > 1;
|
const multiPhase = phases.length > 1;
|
||||||
const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
|
const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
|
||||||
@@ -2367,20 +2425,34 @@ export class InteractiveMode implements InteractiveModeContext {
|
|||||||
theme,
|
theme,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Header: overall task progress summed across every stage — a bar plus
|
// Header: overall task progress as a bar summed across every stage (no
|
||||||
// closed/total counts. Per-stage rows carry their own counts.
|
// trailing count — the bar itself carries the signal). Once the list
|
||||||
const totalTasks = phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
// fully closes, `#startTodoBarCollapseAnimation` shrinks the bar to
|
||||||
const closedTasks = phases.reduce((sum, phase) => sum + phase.tasks.filter(isClosedTodo).length, 0);
|
// nothing over `TODO_BAR_COLLAPSE_DURATION_MS` and the whole row is then
|
||||||
const barWidth = 20;
|
// dropped — the phase/task tree below is untouched and keeps its own
|
||||||
// Clamp so any progress shows a sliver and only 100% fills the bar.
|
// per-stage counts until the separate `tasks.todoClearDelay` timer fires.
|
||||||
let filledWidth = Math.round((closedTasks / totalTasks) * barWidth);
|
const headerLines: string[] = [""];
|
||||||
if (closedTasks > 0) filledWidth = Math.max(filledWidth, 1);
|
if (!this.#todoBarCollapsed) {
|
||||||
if (closedTasks < totalTasks) filledWidth = Math.min(filledWidth, barWidth - 1);
|
const barWidth = 20;
|
||||||
const bar =
|
const collapseFraction = this.#todoBarCollapseFraction();
|
||||||
theme.fg("accent", theme.progress.filled.repeat(filledWidth)) +
|
let bar: string;
|
||||||
theme.fg("dim", theme.progress.empty.repeat(barWidth - filledWidth));
|
if (collapseFraction > 0) {
|
||||||
const root = `${theme.bold(theme.fg("accent", "Todos"))} ${bar} ${theme.fg("dim", `${closedTasks}/${totalTasks}`)}`;
|
const shrunkWidth = Math.max(0, Math.round(barWidth * (1 - collapseFraction)));
|
||||||
const lines = ["", root, ...phaseTreeLines.map(line => ` ${line}`)];
|
bar = theme.fg("accent", theme.progress.filled.repeat(shrunkWidth));
|
||||||
|
} else {
|
||||||
|
const totalTasks = phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
||||||
|
const closedTasks = phases.reduce((sum, phase) => sum + phase.tasks.filter(isClosedTodo).length, 0);
|
||||||
|
// 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);
|
||||||
|
bar =
|
||||||
|
theme.fg("accent", theme.progress.filled.repeat(filledWidth)) +
|
||||||
|
theme.fg("dim", theme.progress.empty.repeat(barWidth - filledWidth));
|
||||||
|
}
|
||||||
|
headerLines.push(`${theme.bold(theme.fg("accent", "Todos"))} ${bar}`);
|
||||||
|
}
|
||||||
|
const lines = [...headerLines, ...phaseTreeLines.map(line => ` ${line}`)];
|
||||||
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4253,6 +4325,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|||||||
stopSharedSpinnerTicker();
|
stopSharedSpinnerTicker();
|
||||||
this.#liveCommandController.dispose();
|
this.#liveCommandController.dispose();
|
||||||
this.#cancelTodoAutoClearTimer();
|
this.#cancelTodoAutoClearTimer();
|
||||||
|
this.#cancelTodoBarCollapseAnimation();
|
||||||
this.#cancelObserverUiSyncTimer();
|
this.#cancelObserverUiSyncTimer();
|
||||||
this.#cancelGoalContinuation();
|
this.#cancelGoalContinuation();
|
||||||
if (this.#sttController) {
|
if (this.#sttController) {
|
||||||
|
|||||||
Reference in New Issue
Block a user