- renderDifferential now correctly handles content that exceeds viewport - When changes are above viewport, do full re-render with scrollback clear - When changes are in viewport, do partial re-render from change point - All tests pass, correctly preserves 100 items in scrollback - Issue: Still re-renders too much (entire tail from first change)
387 lines
21 KiB
JavaScript
387 lines
21 KiB
JavaScript
"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;
|