diff --git a/packages/coding-agent/src/tui/custom-editor.ts b/packages/coding-agent/src/tui/custom-editor.ts index f01075a0..57930fab 100644 --- a/packages/coding-agent/src/tui/custom-editor.ts +++ b/packages/coding-agent/src/tui/custom-editor.ts @@ -6,8 +6,17 @@ import { Editor } from "@mariozechner/pi-tui"; export class CustomEditor extends Editor { public onEscape?: () => void; public onCtrlC?: () => void; + public onTab?: () => boolean; handleInput(data: string): void { + // Intercept Tab key when autocomplete is not showing + if (data === "\t" && !this.isShowingAutocomplete() && this.onTab) { + const handled = this.onTab(); + if (handled) { + 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..2cf391f8 100644 --- a/packages/coding-agent/src/tui/footer.ts +++ b/packages/coding-agent/src/tui/footer.ts @@ -85,30 +85,41 @@ 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"; + const thinkingHint = chalk.dim(" (tab to cycle)"); + rightSide = `${modelName} • ${thinkingLevel}${thinkingHint}`; + } + 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 12c826a9..2b64cdd7 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 { @@ -169,6 +169,9 @@ export class TuiRenderer { chalk.dim("ctrl+k") + chalk.gray(" to delete line") + "\n" + + chalk.dim("tab") + + chalk.gray(" to cycle thinking") + + "\n" + chalk.dim("/") + chalk.gray(" for commands") + "\n" + @@ -210,6 +213,10 @@ export class TuiRenderer { this.handleCtrlC(); }; + this.editor.onTab = () => { + return this.cycleThinkingLevel(); + }; + // Handle editor submission this.editor.onSubmit = async (text: string) => { text = text.trim(); @@ -572,6 +579,32 @@ export class TuiRenderer { } } + private cycleThinkingLevel(): boolean { + // Only cycle if model supports thinking + if (!this.agent.state.model?.reasoning) { + return false; // Not handled, let default Tab behavior continue + } + + 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); + + // Show brief notification + this.chatContainer.addChild(new Spacer(1)); + this.chatContainer.addChild(new Text(chalk.dim(`Thinking level: ${nextLevel}`), 1, 0)); + this.ui.requestRender(); + + return true; // Handled + } + clearEditor(): void { this.editor.setText(""); this.ui.requestRender();