fix(tui): Container change detection for proper differential rendering

Fixed rendering artifact where duplicate bottom borders appeared when components
dynamically shifted positions (e.g., Ctrl+C in agent clearing status container).

Root cause: Container wasn't reporting as "changed" when cleared (0 children),
causing differential renderer to skip re-rendering that area.

Solution: Container now tracks previousChildCount and reports changed when
child count changes, ensuring proper re-rendering when containers are cleared.

- Added comprehensive test reproducing the layout shift artifact
- Fixed Container to track and report child count changes
- All tests pass including new layout shift artifact test
This commit is contained in:
Mario Zechner 2025-08-11 02:31:49 +02:00
parent 2ec8a27222
commit 192d8d2600
24 changed files with 356 additions and 2910 deletions

View file

@ -1,196 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConsoleRenderer = void 0;
var chalk_1 = require("chalk");
var ConsoleRenderer = /** @class */ (function () {
function ConsoleRenderer() {
this.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
this.currentFrame = 0;
this.animationInterval = null;
this.isAnimating = false;
this.animationLine = "";
this.isTTY = process.stdout.isTTY;
this.toolCallCount = 0;
this.lastInputTokens = 0;
this.lastOutputTokens = 0;
this.lastCacheReadTokens = 0;
this.lastCacheWriteTokens = 0;
this.lastReasoningTokens = 0;
}
ConsoleRenderer.prototype.startAnimation = function (text) {
var _this = this;
if (text === void 0) { text = "Thinking"; }
if (this.isAnimating || !this.isTTY)
return;
this.isAnimating = true;
this.currentFrame = 0;
// Write initial frame
this.animationLine = "".concat(chalk_1.default.cyan(this.frames[this.currentFrame]), " ").concat(chalk_1.default.dim(text));
process.stdout.write(this.animationLine);
this.animationInterval = setInterval(function () {
// Clear current line
process.stdout.write("\r".concat(" ".repeat(_this.animationLine.length), "\r"));
// Update frame
_this.currentFrame = (_this.currentFrame + 1) % _this.frames.length;
_this.animationLine = "".concat(chalk_1.default.cyan(_this.frames[_this.currentFrame]), " ").concat(chalk_1.default.dim(text));
process.stdout.write(_this.animationLine);
}, 80);
};
ConsoleRenderer.prototype.stopAnimation = function () {
if (!this.isAnimating)
return;
if (this.animationInterval) {
clearInterval(this.animationInterval);
this.animationInterval = null;
}
// Clear the animation line
process.stdout.write("\r".concat(" ".repeat(this.animationLine.length), "\r"));
this.isAnimating = false;
this.animationLine = "";
};
ConsoleRenderer.prototype.displayMetrics = function () {
// Build metrics display
var metricsText = chalk_1.default.dim("\u2191".concat(this.lastInputTokens.toLocaleString(), " \u2193").concat(this.lastOutputTokens.toLocaleString()));
// Add reasoning tokens if present
if (this.lastReasoningTokens > 0) {
metricsText += chalk_1.default.dim(" \u26A1".concat(this.lastReasoningTokens.toLocaleString()));
}
// Add cache info if available
if (this.lastCacheReadTokens > 0 || this.lastCacheWriteTokens > 0) {
var cacheText = [];
if (this.lastCacheReadTokens > 0) {
cacheText.push("\u27F2".concat(this.lastCacheReadTokens.toLocaleString()));
}
if (this.lastCacheWriteTokens > 0) {
cacheText.push("\u27F3".concat(this.lastCacheWriteTokens.toLocaleString()));
}
metricsText += chalk_1.default.dim(" (".concat(cacheText.join(" "), ")"));
}
// Add tool call count
if (this.toolCallCount > 0) {
metricsText += chalk_1.default.dim(" \u2692 ".concat(this.toolCallCount));
}
console.log(metricsText);
console.log();
};
ConsoleRenderer.prototype.on = function (event) {
return __awaiter(this, void 0, void 0, function () {
var lines, maxLines, truncated, toShow, text;
return __generator(this, function (_a) {
// Stop animation for any new event except token_usage
if (event.type !== "token_usage" && this.isAnimating) {
this.stopAnimation();
}
switch (event.type) {
case "session_start":
console.log(chalk_1.default.blue("[Session started] ID: ".concat(event.sessionId, ", Model: ").concat(event.model, ", API: ").concat(event.api, ", Base URL: ").concat(event.baseURL)));
console.log(chalk_1.default.dim("System Prompt: ".concat(event.systemPrompt, "\n")));
break;
case "assistant_start":
console.log(chalk_1.default.hex("#FFA500")("[assistant]"));
this.startAnimation();
break;
case "reasoning":
this.stopAnimation();
console.log(chalk_1.default.dim("[thinking]"));
console.log(chalk_1.default.dim(event.text));
console.log();
// Resume animation after showing thinking
this.startAnimation("Processing");
break;
case "tool_call":
this.stopAnimation();
this.toolCallCount++;
console.log(chalk_1.default.yellow("[tool] ".concat(event.name, "(").concat(event.args, ")")));
// Resume animation while tool executes
this.startAnimation("Running ".concat(event.name));
break;
case "tool_result": {
this.stopAnimation();
lines = event.result.split("\n");
maxLines = 10;
truncated = lines.length > maxLines;
toShow = truncated ? lines.slice(0, maxLines) : lines;
text = toShow.join("\n");
console.log(event.isError ? chalk_1.default.red(text) : chalk_1.default.gray(text));
if (truncated) {
console.log(chalk_1.default.dim("... (".concat(lines.length - maxLines, " more lines)")));
}
console.log();
// Resume animation after tool result
this.startAnimation("Thinking");
break;
}
case "assistant_message":
this.stopAnimation();
console.log(event.text);
console.log();
// Display metrics after assistant message
this.displayMetrics();
break;
case "error":
this.stopAnimation();
console.error(chalk_1.default.red("[error] ".concat(event.message, "\n")));
break;
case "user_message":
console.log(chalk_1.default.green("[user]"));
console.log(event.text);
console.log();
break;
case "interrupted":
this.stopAnimation();
console.log(chalk_1.default.red("[Interrupted by user]\n"));
break;
case "token_usage":
// Store token usage for display after assistant message
this.lastInputTokens = event.inputTokens;
this.lastOutputTokens = event.outputTokens;
this.lastCacheReadTokens = event.cacheReadTokens;
this.lastCacheWriteTokens = event.cacheWriteTokens;
this.lastReasoningTokens = event.reasoningTokens;
// Don't stop animation for this event
break;
}
return [2 /*return*/];
});
});
};
return ConsoleRenderer;
}());
exports.ConsoleRenderer = ConsoleRenderer;

View file

@ -1,53 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonRenderer = void 0;
var JsonRenderer = /** @class */ (function () {
function JsonRenderer() {
}
JsonRenderer.prototype.on = function (event) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
console.log(JSON.stringify(event));
return [2 /*return*/];
});
});
};
return JsonRenderer;
}());
exports.JsonRenderer = JsonRenderer;

View file

@ -1,386 +0,0 @@
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TuiRenderer = void 0;
var pi_tui_1 = require("@mariozechner/pi-tui");
var chalk_1 = require("chalk");
var LoadingAnimation = /** @class */ (function (_super) {
__extends(LoadingAnimation, _super);
function LoadingAnimation(ui) {
var _this = _super.call(this, "", { bottom: 1 }) || this;
_this.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
_this.currentFrame = 0;
_this.intervalId = null;
_this.ui = null;
_this.ui = ui;
_this.start();
return _this;
}
LoadingAnimation.prototype.start = function () {
var _this = this;
this.updateDisplay();
this.intervalId = setInterval(function () {
_this.currentFrame = (_this.currentFrame + 1) % _this.frames.length;
_this.updateDisplay();
}, 80);
};
LoadingAnimation.prototype.stop = function () {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
};
LoadingAnimation.prototype.updateDisplay = function () {
var frame = this.frames[this.currentFrame];
this.setText("".concat(chalk_1.default.cyan(frame), " ").concat(chalk_1.default.dim("Thinking...")));
if (this.ui) {
this.ui.requestRender();
}
};
return LoadingAnimation;
}(pi_tui_1.TextComponent));
var TuiRenderer = /** @class */ (function () {
function TuiRenderer() {
this.isInitialized = false;
this.currentLoadingAnimation = null;
this.lastSigintTime = 0;
this.lastInputTokens = 0;
this.lastOutputTokens = 0;
this.lastCacheReadTokens = 0;
this.lastCacheWriteTokens = 0;
this.lastReasoningTokens = 0;
this.toolCallCount = 0;
this.tokenStatusComponent = null;
this.ui = new pi_tui_1.TUI();
this.chatContainer = new pi_tui_1.Container();
this.statusContainer = new pi_tui_1.Container();
this.editor = new pi_tui_1.TextEditor();
this.tokenContainer = new pi_tui_1.Container();
// Setup autocomplete for file paths and slash commands
var autocompleteProvider = new pi_tui_1.CombinedAutocompleteProvider([], process.cwd());
this.editor.setAutocompleteProvider(autocompleteProvider);
}
TuiRenderer.prototype.init = function () {
return __awaiter(this, void 0, void 0, function () {
var header;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (this.isInitialized)
return [2 /*return*/];
header = new pi_tui_1.TextComponent(chalk_1.default.gray(chalk_1.default.blueBright(">> pi interactive chat <<<")) +
"\n" +
chalk_1.default.dim("Press Escape to interrupt while processing") +
"\n" +
chalk_1.default.dim("Press CTRL+C to clear the text editor") +
"\n" +
chalk_1.default.dim("Press CTRL+C twice quickly to exit"), { bottom: 1 });
// Setup UI layout
this.ui.addChild(header);
this.ui.addChild(this.chatContainer);
this.ui.addChild(this.statusContainer);
this.ui.addChild(new pi_tui_1.WhitespaceComponent(1));
this.ui.addChild(this.editor);
this.ui.addChild(this.tokenContainer);
this.ui.setFocus(this.editor);
// Set up global key handler for Escape and Ctrl+C
this.ui.onGlobalKeyPress = function (data) {
// Intercept Escape key when processing
if (data === "\x1b" && _this.currentLoadingAnimation) {
// Call interrupt callback if set
if (_this.onInterruptCallback) {
_this.onInterruptCallback();
}
// Stop the loading animation immediately
if (_this.currentLoadingAnimation) {
_this.currentLoadingAnimation.stop();
_this.statusContainer.clear();
_this.currentLoadingAnimation = null;
}
// Don't show message here - the interrupted event will handle it
// Re-enable editor submission
_this.editor.disableSubmit = false;
_this.ui.requestRender();
// Don't forward to editor
return false;
}
// Handle Ctrl+C (raw mode sends \x03)
if (data === "\x03") {
var now = Date.now();
var timeSinceLastCtrlC = now - _this.lastSigintTime;
if (timeSinceLastCtrlC < 500) {
// Second Ctrl+C within 500ms - exit
_this.stop();
process.exit(0);
}
else {
// First Ctrl+C - clear the editor
_this.clearEditor();
_this.lastSigintTime = now;
}
// Don't forward to editor
return false;
}
// Forward all other keys
return true;
};
// Handle editor submission
this.editor.onSubmit = function (text) {
text = text.trim();
if (!text)
return;
if (_this.onInputCallback) {
_this.onInputCallback(text);
}
};
// Start the UI
return [4 /*yield*/, this.ui.start()];
case 1:
// Start the UI
_a.sent();
this.isInitialized = true;
return [2 /*return*/];
}
});
});
};
TuiRenderer.prototype.on = function (event) {
return __awaiter(this, void 0, void 0, function () {
var thinkingContainer, thinkingLines, _i, thinkingLines_1, line, lines, maxLines, truncated, toShow, resultContainer, _a, toShow_1, line;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!!this.isInitialized) return [3 /*break*/, 2];
return [4 /*yield*/, this.init()];
case 1:
_b.sent();
_b.label = 2;
case 2:
switch (event.type) {
case "assistant_start":
this.chatContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.hex("#FFA500")("[assistant]")));
// Disable editor submission while processing
this.editor.disableSubmit = true;
// Start loading animation in the status container
this.statusContainer.clear();
this.currentLoadingAnimation = new LoadingAnimation(this.ui);
this.statusContainer.addChild(this.currentLoadingAnimation);
break;
case "reasoning": {
thinkingContainer = new pi_tui_1.Container();
thinkingContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.dim("[thinking]")));
thinkingLines = event.text.split("\n");
for (_i = 0, thinkingLines_1 = thinkingLines; _i < thinkingLines_1.length; _i++) {
line = thinkingLines_1[_i];
thinkingContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.dim(line)));
}
thinkingContainer.addChild(new pi_tui_1.WhitespaceComponent(1));
this.chatContainer.addChild(thinkingContainer);
break;
}
case "tool_call":
this.toolCallCount++;
this.updateTokenDisplay();
this.chatContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.yellow("[tool] ".concat(event.name, "(").concat(event.args, ")"))));
break;
case "tool_result": {
lines = event.result.split("\n");
maxLines = 10;
truncated = lines.length > maxLines;
toShow = truncated ? lines.slice(0, maxLines) : lines;
resultContainer = new pi_tui_1.Container();
for (_a = 0, toShow_1 = toShow; _a < toShow_1.length; _a++) {
line = toShow_1[_a];
resultContainer.addChild(new pi_tui_1.TextComponent(event.isError ? chalk_1.default.red(line) : chalk_1.default.gray(line)));
}
if (truncated) {
resultContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.dim("... (".concat(lines.length - maxLines, " more lines)"))));
}
resultContainer.addChild(new pi_tui_1.WhitespaceComponent(1));
this.chatContainer.addChild(resultContainer);
break;
}
case "assistant_message":
// Stop loading animation when assistant responds
if (this.currentLoadingAnimation) {
this.currentLoadingAnimation.stop();
this.currentLoadingAnimation = null;
this.statusContainer.clear();
}
// Re-enable editor submission
this.editor.disableSubmit = false;
// Use MarkdownComponent for rich formatting
this.chatContainer.addChild(new pi_tui_1.MarkdownComponent(event.text));
this.chatContainer.addChild(new pi_tui_1.WhitespaceComponent(1));
break;
case "error":
// Stop loading animation on error
if (this.currentLoadingAnimation) {
this.currentLoadingAnimation.stop();
this.currentLoadingAnimation = null;
this.statusContainer.clear();
}
// Re-enable editor submission
this.editor.disableSubmit = false;
this.chatContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.red("[error] ".concat(event.message)), { bottom: 1 }));
break;
case "user_message":
// Render user message
this.chatContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.green("[user]")));
this.chatContainer.addChild(new pi_tui_1.TextComponent(event.text, { bottom: 1 }));
break;
case "token_usage":
// Store the latest token counts (not cumulative since prompt includes full context)
this.lastInputTokens = event.inputTokens;
this.lastOutputTokens = event.outputTokens;
this.lastCacheReadTokens = event.cacheReadTokens;
this.lastCacheWriteTokens = event.cacheWriteTokens;
this.lastReasoningTokens = event.reasoningTokens;
this.updateTokenDisplay();
break;
case "interrupted":
// Stop the loading animation
if (this.currentLoadingAnimation) {
this.currentLoadingAnimation.stop();
this.currentLoadingAnimation = null;
this.statusContainer.clear();
}
// Show interrupted message
this.chatContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.red("[Interrupted by user]"), { bottom: 1 }));
// Re-enable editor submission
this.editor.disableSubmit = false;
break;
}
this.ui.requestRender();
return [2 /*return*/];
}
});
});
};
TuiRenderer.prototype.updateTokenDisplay = function () {
// Clear and update token display
this.tokenContainer.clear();
// Build token display text
var tokenText = chalk_1.default.dim("\u2191".concat(this.lastInputTokens.toLocaleString(), " \u2193").concat(this.lastOutputTokens.toLocaleString()));
// Add reasoning tokens if present
if (this.lastReasoningTokens > 0) {
tokenText += chalk_1.default.dim(" \u26A1".concat(this.lastReasoningTokens.toLocaleString()));
}
// Add cache info if available
if (this.lastCacheReadTokens > 0 || this.lastCacheWriteTokens > 0) {
var cacheText = [];
if (this.lastCacheReadTokens > 0) {
cacheText.push("\u27F2".concat(this.lastCacheReadTokens.toLocaleString()));
}
if (this.lastCacheWriteTokens > 0) {
cacheText.push("\u27F3".concat(this.lastCacheWriteTokens.toLocaleString()));
}
tokenText += chalk_1.default.dim(" (".concat(cacheText.join(" "), ")"));
}
// Add tool call count
if (this.toolCallCount > 0) {
tokenText += chalk_1.default.dim(" \u2692 ".concat(this.toolCallCount));
}
this.tokenStatusComponent = new pi_tui_1.TextComponent(tokenText);
this.tokenContainer.addChild(this.tokenStatusComponent);
};
TuiRenderer.prototype.getUserInput = function () {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve) {
_this.onInputCallback = function (text) {
_this.onInputCallback = undefined; // Clear callback
resolve(text);
};
})];
});
});
};
TuiRenderer.prototype.setInterruptCallback = function (callback) {
this.onInterruptCallback = callback;
};
TuiRenderer.prototype.clearEditor = function () {
var _this = this;
this.editor.setText("");
// Show hint in status container
this.statusContainer.clear();
var hint = new pi_tui_1.TextComponent(chalk_1.default.dim("Press Ctrl+C again to exit"));
this.statusContainer.addChild(hint);
this.ui.requestRender();
// Clear the hint after 500ms
setTimeout(function () {
_this.statusContainer.clear();
_this.ui.requestRender();
}, 500);
};
TuiRenderer.prototype.renderAssistantLabel = function () {
// Just render the assistant label without starting animations
// Used for restored session history
this.chatContainer.addChild(new pi_tui_1.TextComponent(chalk_1.default.hex("#FFA500")("[assistant]")));
this.ui.requestRender();
};
TuiRenderer.prototype.stop = function () {
if (this.currentLoadingAnimation) {
this.currentLoadingAnimation.stop();
this.currentLoadingAnimation = null;
}
if (this.isInitialized) {
this.ui.stop();
this.isInitialized = false;
}
};
return TuiRenderer;
}());
exports.TuiRenderer = TuiRenderer;