diff --git a/packages/coding-agent/src/tui/custom-editor.ts b/packages/coding-agent/src/tui/custom-editor.ts index f01075a0..08527dff 100644 --- a/packages/coding-agent/src/tui/custom-editor.ts +++ b/packages/coding-agent/src/tui/custom-editor.ts @@ -6,8 +6,15 @@ import { Editor } from "@mariozechner/pi-tui"; export class CustomEditor extends Editor { public onEscape?: () => void; public onCtrlC?: () => void; + public onShiftTab?: () => void; handleInput(data: string): void { + // Intercept Shift+Tab for thinking level cycling + if (data === "\x1b[Z" && this.onShiftTab) { + this.onShiftTab(); + return; + } + // Intercept Escape key - but only if autocomplete is NOT active // (let parent handle escape for autocomplete cancellation) if (data === "\x1b" && this.onEscape && !this.isShowingAutocomplete()) { diff --git a/packages/coding-agent/src/tui/footer.ts b/packages/coding-agent/src/tui/footer.ts index 0ececaff..88f2e34c 100644 --- a/packages/coding-agent/src/tui/footer.ts +++ b/packages/coding-agent/src/tui/footer.ts @@ -85,30 +85,40 @@ export class FooterComponent { const statsLeft = statsParts.join(" "); - // Add model name on the right side - let modelName = this.state.model?.id || "no-model"; + // Add model name on the right side, plus thinking level if model supports it + const modelName = this.state.model?.id || "no-model"; + + // Add thinking level hint if model supports reasoning + let rightSide = modelName; + if (this.state.model?.reasoning) { + const thinkingLevel = this.state.thinkingLevel || "off"; + rightSide = `${modelName} • Thinking: ${thinkingLevel}`; + } + const statsLeftWidth = visibleWidth(statsLeft); - const modelWidth = visibleWidth(modelName); + const rightSideWidth = visibleWidth(rightSide); // Calculate available space for padding (minimum 2 spaces between stats and model) const minPadding = 2; - const totalNeeded = statsLeftWidth + minPadding + modelWidth; + const totalNeeded = statsLeftWidth + minPadding + rightSideWidth; let statsLine: string; if (totalNeeded <= width) { // Both fit - add padding to right-align model - const padding = " ".repeat(width - statsLeftWidth - modelWidth); - statsLine = statsLeft + padding + modelName; + const padding = " ".repeat(width - statsLeftWidth - rightSideWidth); + statsLine = statsLeft + padding + rightSide; } else { - // Need to truncate model name - const availableForModel = width - statsLeftWidth - minPadding; - if (availableForModel > 3) { - // Truncate model name to fit - modelName = modelName.substring(0, availableForModel); - const padding = " ".repeat(width - statsLeftWidth - visibleWidth(modelName)); - statsLine = statsLeft + padding + modelName; + // Need to truncate right side + const availableForRight = width - statsLeftWidth - minPadding; + if (availableForRight > 3) { + // Truncate to fit (strip ANSI codes for length calculation, then truncate raw string) + const plainRightSide = rightSide.replace(/\x1b\[[0-9;]*m/g, ""); + const truncatedPlain = plainRightSide.substring(0, availableForRight); + // For simplicity, just use plain truncated version (loses color, but fits) + const padding = " ".repeat(width - statsLeftWidth - truncatedPlain.length); + statsLine = statsLeft + padding + truncatedPlain; } else { - // Not enough space for model name at all + // Not enough space for right side at all statsLine = statsLeft; } } diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index 30afb97e..3d690e3f 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -1,4 +1,4 @@ -import type { Agent, AgentEvent, AgentState } from "@mariozechner/pi-agent"; +import type { Agent, AgentEvent, AgentState, ThinkingLevel } from "@mariozechner/pi-agent"; import type { AssistantMessage, Message } from "@mariozechner/pi-ai"; import type { SlashCommand } from "@mariozechner/pi-tui"; import { @@ -172,6 +172,9 @@ export class TuiRenderer { chalk.dim("ctrl+k") + chalk.gray(" to delete line") + "\n" + + chalk.dim("shift+tab") + + chalk.gray(" to cycle thinking") + + "\n" + chalk.dim("/") + chalk.gray(" for commands") + "\n" + @@ -229,6 +232,10 @@ export class TuiRenderer { this.handleCtrlC(); }; + this.editor.onShiftTab = () => { + this.cycleThinkingLevel(); + }; + // Handle editor submission this.editor.onSubmit = async (text: string) => { text = text.trim(); @@ -503,6 +510,9 @@ export class TuiRenderer { // Update footer with loaded state this.footer.updateState(state); + // Update editor border color based on current thinking level + this.updateEditorBorderColor(); + // Render messages for (let i = 0; i < state.messages.length; i++) { const message = state.messages[i]; @@ -591,6 +601,61 @@ export class TuiRenderer { } } + private getThinkingBorderColor(level: ThinkingLevel): (str: string) => string { + // More thinking = more color (gray → dim colors → bright colors) + switch (level) { + case "off": + return chalk.gray; + case "minimal": + return chalk.dim.blue; + case "low": + return chalk.blue; + case "medium": + return chalk.cyan; + case "high": + return chalk.magenta; + default: + return chalk.gray; + } + } + + private updateEditorBorderColor(): void { + const level = this.agent.state.thinkingLevel || "off"; + const color = this.getThinkingBorderColor(level); + this.editor.borderColor = color; + this.ui.requestRender(); + } + + private cycleThinkingLevel(): void { + // Only cycle if model supports thinking + if (!this.agent.state.model?.reasoning) { + this.chatContainer.addChild(new Spacer(1)); + this.chatContainer.addChild(new Text(chalk.dim("Current model does not support thinking"), 1, 0)); + this.ui.requestRender(); + return; + } + + const levels: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"]; + const currentLevel = this.agent.state.thinkingLevel || "off"; + const currentIndex = levels.indexOf(currentLevel); + const nextIndex = (currentIndex + 1) % levels.length; + const nextLevel = levels[nextIndex]; + + // Apply the new thinking level + this.agent.setThinkingLevel(nextLevel); + + // Save thinking level change to session + this.sessionManager.saveThinkingLevelChange(nextLevel); + + // Update border color + this.updateEditorBorderColor(); + + // Show brief notification + this.chatContainer.addChild(new Spacer(1)); + this.chatContainer.addChild(new Text(chalk.dim(`Thinking level: ${nextLevel}`), 1, 0)); + this.ui.requestRender(); + } + clearEditor(): void { this.editor.setText(""); this.ui.requestRender(); @@ -621,6 +686,9 @@ export class TuiRenderer { // Save thinking level change to session this.sessionManager.saveThinkingLevelChange(level); + // Update border color + this.updateEditorBorderColor(); + // Show confirmation message with proper spacing this.chatContainer.addChild(new Spacer(1)); const confirmText = new Text(chalk.dim(`Thinking level: ${level}`), 1, 0); diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 4374775d..6195e404 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -28,6 +28,9 @@ export class Editor implements Component { private config: TextEditorConfig = {}; + // Border color (can be changed dynamically) + public borderColor: (str: string) => string = chalk.gray; + // Autocomplete support private autocompleteProvider?: AutocompleteProvider; private autocompleteList?: SelectList; @@ -61,7 +64,7 @@ export class Editor implements Component { } render(width: number): string[] { - const horizontal = chalk.gray("─"); + const horizontal = this.borderColor("─"); // Layout the text - use full width const layoutLines = this.layoutText(width);