mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 07:03:25 +00:00
Merge PR #36: Add Shift+Tab thinking level cycling with visual border feedback
This commit is contained in:
commit
973a129407
4 changed files with 104 additions and 16 deletions
|
|
@ -6,8 +6,15 @@ import { Editor } from "@mariozechner/pi-tui";
|
||||||
export class CustomEditor extends Editor {
|
export class CustomEditor extends Editor {
|
||||||
public onEscape?: () => void;
|
public onEscape?: () => void;
|
||||||
public onCtrlC?: () => void;
|
public onCtrlC?: () => void;
|
||||||
|
public onShiftTab?: () => void;
|
||||||
|
|
||||||
handleInput(data: string): 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
|
// Intercept Escape key - but only if autocomplete is NOT active
|
||||||
// (let parent handle escape for autocomplete cancellation)
|
// (let parent handle escape for autocomplete cancellation)
|
||||||
if (data === "\x1b" && this.onEscape && !this.isShowingAutocomplete()) {
|
if (data === "\x1b" && this.onEscape && !this.isShowingAutocomplete()) {
|
||||||
|
|
|
||||||
|
|
@ -85,30 +85,40 @@ export class FooterComponent {
|
||||||
|
|
||||||
const statsLeft = statsParts.join(" ");
|
const statsLeft = statsParts.join(" ");
|
||||||
|
|
||||||
// Add model name on the right side
|
// Add model name on the right side, plus thinking level if model supports it
|
||||||
let modelName = this.state.model?.id || "no-model";
|
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 statsLeftWidth = visibleWidth(statsLeft);
|
||||||
const modelWidth = visibleWidth(modelName);
|
const rightSideWidth = visibleWidth(rightSide);
|
||||||
|
|
||||||
// Calculate available space for padding (minimum 2 spaces between stats and model)
|
// Calculate available space for padding (minimum 2 spaces between stats and model)
|
||||||
const minPadding = 2;
|
const minPadding = 2;
|
||||||
const totalNeeded = statsLeftWidth + minPadding + modelWidth;
|
const totalNeeded = statsLeftWidth + minPadding + rightSideWidth;
|
||||||
|
|
||||||
let statsLine: string;
|
let statsLine: string;
|
||||||
if (totalNeeded <= width) {
|
if (totalNeeded <= width) {
|
||||||
// Both fit - add padding to right-align model
|
// Both fit - add padding to right-align model
|
||||||
const padding = " ".repeat(width - statsLeftWidth - modelWidth);
|
const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
|
||||||
statsLine = statsLeft + padding + modelName;
|
statsLine = statsLeft + padding + rightSide;
|
||||||
} else {
|
} else {
|
||||||
// Need to truncate model name
|
// Need to truncate right side
|
||||||
const availableForModel = width - statsLeftWidth - minPadding;
|
const availableForRight = width - statsLeftWidth - minPadding;
|
||||||
if (availableForModel > 3) {
|
if (availableForRight > 3) {
|
||||||
// Truncate model name to fit
|
// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)
|
||||||
modelName = modelName.substring(0, availableForModel);
|
const plainRightSide = rightSide.replace(/\x1b\[[0-9;]*m/g, "");
|
||||||
const padding = " ".repeat(width - statsLeftWidth - visibleWidth(modelName));
|
const truncatedPlain = plainRightSide.substring(0, availableForRight);
|
||||||
statsLine = statsLeft + padding + modelName;
|
// For simplicity, just use plain truncated version (loses color, but fits)
|
||||||
|
const padding = " ".repeat(width - statsLeftWidth - truncatedPlain.length);
|
||||||
|
statsLine = statsLeft + padding + truncatedPlain;
|
||||||
} else {
|
} else {
|
||||||
// Not enough space for model name at all
|
// Not enough space for right side at all
|
||||||
statsLine = statsLeft;
|
statsLine = statsLeft;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { AssistantMessage, Message } from "@mariozechner/pi-ai";
|
||||||
import type { SlashCommand } from "@mariozechner/pi-tui";
|
import type { SlashCommand } from "@mariozechner/pi-tui";
|
||||||
import {
|
import {
|
||||||
|
|
@ -172,6 +172,9 @@ export class TuiRenderer {
|
||||||
chalk.dim("ctrl+k") +
|
chalk.dim("ctrl+k") +
|
||||||
chalk.gray(" to delete line") +
|
chalk.gray(" to delete line") +
|
||||||
"\n" +
|
"\n" +
|
||||||
|
chalk.dim("shift+tab") +
|
||||||
|
chalk.gray(" to cycle thinking") +
|
||||||
|
"\n" +
|
||||||
chalk.dim("/") +
|
chalk.dim("/") +
|
||||||
chalk.gray(" for commands") +
|
chalk.gray(" for commands") +
|
||||||
"\n" +
|
"\n" +
|
||||||
|
|
@ -229,6 +232,10 @@ export class TuiRenderer {
|
||||||
this.handleCtrlC();
|
this.handleCtrlC();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.editor.onShiftTab = () => {
|
||||||
|
this.cycleThinkingLevel();
|
||||||
|
};
|
||||||
|
|
||||||
// Handle editor submission
|
// Handle editor submission
|
||||||
this.editor.onSubmit = async (text: string) => {
|
this.editor.onSubmit = async (text: string) => {
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
|
|
@ -503,6 +510,9 @@ export class TuiRenderer {
|
||||||
// Update footer with loaded state
|
// Update footer with loaded state
|
||||||
this.footer.updateState(state);
|
this.footer.updateState(state);
|
||||||
|
|
||||||
|
// Update editor border color based on current thinking level
|
||||||
|
this.updateEditorBorderColor();
|
||||||
|
|
||||||
// Render messages
|
// Render messages
|
||||||
for (let i = 0; i < state.messages.length; i++) {
|
for (let i = 0; i < state.messages.length; i++) {
|
||||||
const message = state.messages[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 {
|
clearEditor(): void {
|
||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
|
|
@ -621,6 +686,9 @@ export class TuiRenderer {
|
||||||
// Save thinking level change to session
|
// Save thinking level change to session
|
||||||
this.sessionManager.saveThinkingLevelChange(level);
|
this.sessionManager.saveThinkingLevelChange(level);
|
||||||
|
|
||||||
|
// Update border color
|
||||||
|
this.updateEditorBorderColor();
|
||||||
|
|
||||||
// Show confirmation message with proper spacing
|
// Show confirmation message with proper spacing
|
||||||
this.chatContainer.addChild(new Spacer(1));
|
this.chatContainer.addChild(new Spacer(1));
|
||||||
const confirmText = new Text(chalk.dim(`Thinking level: ${level}`), 1, 0);
|
const confirmText = new Text(chalk.dim(`Thinking level: ${level}`), 1, 0);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ export class Editor implements Component {
|
||||||
|
|
||||||
private config: TextEditorConfig = {};
|
private config: TextEditorConfig = {};
|
||||||
|
|
||||||
|
// Border color (can be changed dynamically)
|
||||||
|
public borderColor: (str: string) => string = chalk.gray;
|
||||||
|
|
||||||
// Autocomplete support
|
// Autocomplete support
|
||||||
private autocompleteProvider?: AutocompleteProvider;
|
private autocompleteProvider?: AutocompleteProvider;
|
||||||
private autocompleteList?: SelectList;
|
private autocompleteList?: SelectList;
|
||||||
|
|
@ -61,7 +64,7 @@ export class Editor implements Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render(width: number): string[] {
|
render(width: number): string[] {
|
||||||
const horizontal = chalk.gray("─");
|
const horizontal = this.borderColor("─");
|
||||||
|
|
||||||
// Layout the text - use full width
|
// Layout the text - use full width
|
||||||
const layoutLines = this.layoutText(width);
|
const layoutLines = this.layoutText(width);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue