mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 19:04:37 +00:00
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:
parent
2ec8a27222
commit
192d8d2600
24 changed files with 356 additions and 2910 deletions
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue