From ceae7f34fd20e0d1db7f782da2fd52d620aa3d8f Mon Sep 17 00:00:00 2001 From: DarkPhilosophy <19309990+DarkPhilosophy@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:21:18 +0300 Subject: [PATCH] feat(advisor): scrollbar in instructions editor, quota/cost in config preview, merge upstream credential-blocking --- .../src/modes/components/advisor-config.ts | 29 +++++++++++++++-- .../src/modes/components/hook-editor.ts | 1 + .../modes/controllers/selector-controller.ts | 1 + packages/tui/src/components/editor.ts | 31 ++++++++++++++++++- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/modes/components/advisor-config.ts b/packages/coding-agent/src/modes/components/advisor-config.ts index ce2ffc744..400cafd1f 100644 --- a/packages/coding-agent/src/modes/components/advisor-config.ts +++ b/packages/coding-agent/src/modes/components/advisor-config.ts @@ -38,6 +38,7 @@ import { import type { ModelRegistry } from "../../config/model-registry"; import { formatModelSelectorValue } from "../../config/model-resolver"; import type { Settings } from "../../config/settings"; +import type { PerAdvisorStat } from "../../session/agent-session"; import { getSelectListTheme, theme } from "../theme/theme"; import { HookEditorComponent } from "./hook-editor"; import { ModelSelectorComponent } from "./model-selector"; @@ -63,6 +64,8 @@ export interface AdvisorConfigCallbacks { requestRender: () => void; /** Surface a transient status/warning line to the user. */ notify: (message: string) => void; + /** Live advisor usage stats; lets the preview show tokens/cost per advisor. */ + getAdvisorStats?: () => PerAdvisorStat[]; } export interface AdvisorConfigDeps { @@ -276,8 +279,30 @@ export class AdvisorConfigOverlayComponent implements Component { "", theme.fg("dim", "Instructions:"), ]; - const instr = advisor.instructions?.trim(); - lines.push(...(instr ? wrap(instr, bodyWidth) : [theme.fg("muted", "(none)")])); + // Show live usage stats when available from the session. + const stats = this.#cb.getAdvisorStats?.(); + if (stats) { + const match = stats.find(s => s.name === (advisor.name || "default")); + if (match && (match.status === "running" || match.status === "quota_exhausted")) { + lines.push("", theme.fg("dim", "Usage:")); + const spendParts: string[] = [ + `${match.tokens.input.toLocaleString()} in`, + `${match.tokens.output.toLocaleString()} out`, + ]; + if (match.tokens.cacheRead > 0) spendParts.push(`${match.tokens.cacheRead.toLocaleString()} cache`); + lines.push(theme.fg("dim", ` Tokens: ${spendParts.join(", ")}`)); + if (match.cost > 0) lines.push(theme.fg("dim", ` Cost: $${match.cost.toFixed(4)}`)); + if (match.contextWindow > 0) { + const pct = Math.round((match.contextTokens / match.contextWindow) * 100); + lines.push( + theme.fg( + "dim", + ` Context: ${match.contextTokens.toLocaleString()}/${match.contextWindow.toLocaleString()} (${pct}%)`, + ), + ); + } + } + } return lines.map(line => truncateToWidth(line, bodyWidth)); } diff --git a/packages/coding-agent/src/modes/components/hook-editor.ts b/packages/coding-agent/src/modes/components/hook-editor.ts index e14a2a846..25dde94cc 100644 --- a/packages/coding-agent/src/modes/components/hook-editor.ts +++ b/packages/coding-agent/src/modes/components/hook-editor.ts @@ -69,6 +69,7 @@ export class HookEditorComponent extends Container { // submit hint off-screen. Caller may override via options.maxHeight. const termRows = this.#tui.terminal?.rows ?? process.stdout.rows ?? 40; this.#editor.setMaxHeight(options?.maxHeight ?? Math.max(3, termRows - 12)); + this.#editor.setScrollbarVisible(true); if (prefill) { this.#editor.setText(prefill); } diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index 92b8a1ac7..4c365dddd 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -275,6 +275,7 @@ export class SelectorController { close: done, requestRender: () => this.ctx.ui.requestRender(), notify: message => this.ctx.showStatus(message), + getAdvisorStats: () => this.ctx.session.getAdvisorStats().advisors, }); overlayHandle = this.ctx.ui.showOverlay(overlay, { anchor: "bottom-center", diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 8a631d790..1d187b1d4 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -403,6 +403,10 @@ export class Editor implements Component, Focusable { #paddingXOverride: number | undefined; #maxHeight?: number; #scrollOffset: number = 0; + /** When true, the right border shows a scrollbar track/thumb when content + * overflows {@link #maxHeight}. Enabled by {@link HookEditorComponent} and + * other multi-line consumers; single-line consumers are unaffected. */ + #scrollbarVisible = false; // Emacs-style kill ring #killRing = new KillRing(); @@ -549,6 +553,11 @@ export class Editor implements Component, Focusable { // Don't reset scrollOffset — #updateScrollOffset will clamp it on next render } + /** Enable/disable the right-border scrollbar. Only shown when content overflows. */ + setScrollbarVisible(visible: boolean): void { + this.#scrollbarVisible = visible; + } + setPaddingX(paddingX: number): void { this.#paddingXOverride = Math.max(0, paddingX); } @@ -825,6 +834,22 @@ export class Editor implements Component, Focusable { const visibleLayoutLines = layoutLines.slice(this.#scrollOffset, this.#scrollOffset + visibleContentHeight); const result: string[] = []; + // Scrollbar: shown only when content overflows and the caller opted in. + const needsScrollbar = this.#scrollbarVisible && layoutLines.length > visibleContentHeight; + let scrollbarThumb: { start: number; end: number } | null = null; + if (needsScrollbar && visibleContentHeight > 0) { + const thumbSize = Math.max( + 1, + Math.min( + Math.floor((visibleContentHeight * visibleContentHeight) / layoutLines.length), + visibleContentHeight, + ), + ); + const travel = visibleContentHeight - thumbSize; + const maxOffset = Math.max(0, layoutLines.length - visibleContentHeight); + const start = maxOffset === 0 ? 0 : Math.round((this.#scrollOffset / maxOffset) * travel); + scrollbarThumb = { start, end: start + thumbSize }; + } if (borderVisible) { // Render top border: ╭─ [status content] ────────────────╮ @@ -1024,7 +1049,11 @@ export class Editor implements Component, Focusable { result.push(`${bottomLeft}${displayText}${linePad}${bottomRightAdjusted}`); } else { const leftBorder = this.borderColor(`${box.vertical}${padding(paddingX)}`); - const rightBorder = this.borderColor(`${padding(Math.max(0, rightChromeCells - 1))}${box.vertical}`); + // When scrollbar is active, replace the right border vertical with a + // thumb glyph (█) on lines inside the thumb range, keeping the track (│) elsewhere. + const inThumb = scrollbarThumb && visibleIndex >= scrollbarThumb.start && visibleIndex < scrollbarThumb.end; + const rightGlyph = inThumb ? "█" : box.vertical; + const rightBorder = this.borderColor(`${padding(Math.max(0, rightChromeCells - 1))}${rightGlyph}`); result.push(leftBorder + displayText + linePad + rightBorder); } }