From 192d8d260076e9d425dd3d06ce2c3d218ed0c176 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 11 Aug 2025 02:31:49 +0200 Subject: [PATCH] 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 --- packages/agent/src/agent.js | 771 ------------------ packages/agent/src/args.js | 164 ---- packages/agent/src/cli.js | 9 - packages/agent/src/main.js | 364 --------- .../agent/src/renderers/console-renderer.js | 196 ----- packages/agent/src/renderers/json-renderer.js | 53 -- packages/agent/src/renderers/tui-renderer.js | 386 --------- packages/agent/src/session-manager.js | 194 ----- packages/agent/src/tools/tools.js | 316 ------- packages/pods/src/models.json | 8 +- packages/tui/README.md | 29 +- packages/tui/src/autocomplete.ts | 40 - packages/tui/src/components/text-editor.ts | 116 --- packages/tui/src/index.ts | 2 - packages/tui/src/logger.ts | 105 --- packages/tui/src/tui.ts | 75 +- packages/tui/test/demo.ts | 98 --- packages/tui/test/file-browser.ts | 55 ++ .../tui/test/layout-shift-artifacts.test.ts | 151 ++++ packages/tui/test/multi-layout.ts | 9 +- ...nt-tui-ctrl-c-display-artifact-analysis.md | 82 ++ ...93139-agent-tui-ctrl-c-display-artifact.md | 36 + todos/todos.md | 6 - tsconfig.json | 1 + 24 files changed, 356 insertions(+), 2910 deletions(-) delete mode 100644 packages/agent/src/agent.js delete mode 100644 packages/agent/src/args.js delete mode 100644 packages/agent/src/cli.js delete mode 100644 packages/agent/src/main.js delete mode 100644 packages/agent/src/renderers/console-renderer.js delete mode 100644 packages/agent/src/renderers/json-renderer.js delete mode 100644 packages/agent/src/renderers/tui-renderer.js delete mode 100644 packages/agent/src/session-manager.js delete mode 100644 packages/agent/src/tools/tools.js delete mode 100644 packages/tui/src/logger.ts delete mode 100644 packages/tui/test/demo.ts create mode 100644 packages/tui/test/file-browser.ts create mode 100644 packages/tui/test/layout-shift-artifacts.test.ts create mode 100644 todos/done/20250110-193139-agent-tui-ctrl-c-display-artifact-analysis.md create mode 100644 todos/done/20250110-193139-agent-tui-ctrl-c-display-artifact.md diff --git a/packages/agent/src/agent.js b/packages/agent/src/agent.js deleted file mode 100644 index 3f95f208..00000000 --- a/packages/agent/src/agent.js +++ /dev/null @@ -1,771 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -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 }; - } -}; -var __rest = (this && this.__rest) || function (s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Agent = void 0; -exports.callModelResponsesApi = callModelResponsesApi; -exports.callModelChatCompletionsApi = callModelChatCompletionsApi; -var openai_1 = require("openai"); -var tools_js_1 = require("./tools/tools.js"); -// Cache for model reasoning support detection per API type -var modelReasoningSupport = new Map(); -// Provider detection based on base URL -function detectProvider(baseURL) { - if (!baseURL) - return "openai"; - if (baseURL.includes("api.openai.com")) - return "openai"; - if (baseURL.includes("generativelanguage.googleapis.com")) - return "gemini"; - if (baseURL.includes("api.groq.com")) - return "groq"; - if (baseURL.includes("api.anthropic.com")) - return "anthropic"; - if (baseURL.includes("openrouter.ai")) - return "openrouter"; - return "other"; -} -// Parse provider-specific reasoning from message content -function parseReasoningFromMessage(message, baseURL) { - var provider = detectProvider(baseURL); - var reasoningTexts = []; - var cleanContent = message.content || ""; - switch (provider) { - case "gemini": - // Gemini returns thinking in tags - if (cleanContent.includes("")) { - var thoughtMatches = cleanContent.matchAll(/([\s\S]*?)<\/thought>/g); - for (var _i = 0, thoughtMatches_1 = thoughtMatches; _i < thoughtMatches_1.length; _i++) { - var match = thoughtMatches_1[_i]; - reasoningTexts.push(match[1].trim()); - } - // Remove all thought tags from the response - cleanContent = cleanContent.replace(/[\s\S]*?<\/thought>/g, "").trim(); - } - break; - case "groq": - // Groq returns reasoning in a separate field when reasoning_format is "parsed" - if (message.reasoning) { - reasoningTexts.push(message.reasoning); - } - break; - case "openrouter": - // OpenRouter returns reasoning in message.reasoning field - if (message.reasoning) { - reasoningTexts.push(message.reasoning); - } - break; - default: - // Other providers don't embed reasoning in message content - break; - } - return { cleanContent: cleanContent, reasoningTexts: reasoningTexts }; -} -// Adjust request options based on provider-specific requirements -function adjustRequestForProvider(requestOptions, api, baseURL, supportsReasoning) { - var provider = detectProvider(baseURL); - // Handle provider-specific adjustments - switch (provider) { - case "gemini": - if (api === "completions" && supportsReasoning && requestOptions.reasoning_effort) { - // Gemini needs extra_body for thinking content - // Can't use both reasoning_effort and thinking_config - var budget = requestOptions.reasoning_effort === "low" - ? 1024 - : requestOptions.reasoning_effort === "medium" - ? 8192 - : 24576; - requestOptions.extra_body = { - google: { - thinking_config: { - thinking_budget: budget, - include_thoughts: true, - }, - }, - }; - // Remove reasoning_effort when using thinking_config - delete requestOptions.reasoning_effort; - } - break; - case "groq": - if (api === "responses" && requestOptions.reasoning) { - // Groq responses API doesn't support reasoning.summary - delete requestOptions.reasoning.summary; - } - else if (api === "completions" && supportsReasoning && requestOptions.reasoning_effort) { - // Groq Chat Completions uses reasoning_format instead of reasoning_effort alone - requestOptions.reasoning_format = "parsed"; - // Keep reasoning_effort for Groq - } - break; - case "anthropic": - // Anthropic's OpenAI compatibility has its own quirks - // But thinking content isn't available via OpenAI compat layer - break; - case "openrouter": - // OpenRouter uses a unified reasoning parameter format - if (api === "completions" && supportsReasoning && requestOptions.reasoning_effort) { - // Convert reasoning_effort to OpenRouter's reasoning format - requestOptions.reasoning = { - effort: requestOptions.reasoning_effort === "low" - ? "low" - : requestOptions.reasoning_effort === "minimal" - ? "low" - : requestOptions.reasoning_effort === "medium" - ? "medium" - : "high", - }; - delete requestOptions.reasoning_effort; - } - break; - default: - // OpenAI and others use standard format - break; - } - return requestOptions; -} -function checkReasoningSupport(client, model, api, baseURL) { - return __awaiter(this, void 0, void 0, function () { - var cacheKey, cached, supportsReasoning, provider, testRequest, error_1, testRequest, error_2, existing; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - cacheKey = model; - cached = modelReasoningSupport.get(cacheKey); - if (cached && cached[api] !== undefined) { - return [2 /*return*/, cached[api]]; - } - supportsReasoning = false; - provider = detectProvider(baseURL); - if (!(api === "responses")) return [3 /*break*/, 5]; - _a.label = 1; - case 1: - _a.trys.push([1, 3, , 4]); - testRequest = { - model: model, - input: "test", - max_output_tokens: 1024, - reasoning: { - effort: "low", // Use low instead of minimal to ensure we get summaries - }, - }; - return [4 /*yield*/, client.responses.create(testRequest)]; - case 2: - _a.sent(); - supportsReasoning = true; - return [3 /*break*/, 4]; - case 3: - error_1 = _a.sent(); - supportsReasoning = false; - return [3 /*break*/, 4]; - case 4: return [3 /*break*/, 8]; - case 5: - _a.trys.push([5, 7, , 8]); - testRequest = { - model: model, - messages: [{ role: "user", content: "test" }], - max_completion_tokens: 1024, - }; - // Add provider-specific reasoning parameters - if (provider === "gemini") { - // Gemini uses extra_body for thinking - testRequest.extra_body = { - google: { - thinking_config: { - thinking_budget: 100, // Minimum viable budget for test - include_thoughts: true, - }, - }, - }; - } - else if (provider === "groq") { - // Groq uses both reasoning_format and reasoning_effort - testRequest.reasoning_format = "parsed"; - testRequest.reasoning_effort = "low"; - } - else { - // Others use reasoning_effort - testRequest.reasoning_effort = "minimal"; - } - return [4 /*yield*/, client.chat.completions.create(testRequest)]; - case 6: - _a.sent(); - supportsReasoning = true; - return [3 /*break*/, 8]; - case 7: - error_2 = _a.sent(); - supportsReasoning = false; - return [3 /*break*/, 8]; - case 8: - existing = modelReasoningSupport.get(cacheKey) || {}; - existing[api] = supportsReasoning; - modelReasoningSupport.set(cacheKey, existing); - return [2 /*return*/, supportsReasoning]; - } - }); - }); -} -function callModelResponsesApi(client, model, messages, signal, eventReceiver, supportsReasoning, baseURL) { - return __awaiter(this, void 0, void 0, function () { - var conversationDone, requestOptions, response, usage, output, _i, output_1, item, type, message, _a, reasoningItems, _b, reasoningItems_1, content, _c, _d, content, result, toolResultMsg, e_1, errorMsg; - var _e, _f; - return __generator(this, function (_g) { - switch (_g.label) { - case 0: - conversationDone = false; - _g.label = 1; - case 1: - if (!!conversationDone) return [3 /*break*/, 31]; - if (!(signal === null || signal === void 0 ? void 0 : signal.aborted)) return [3 /*break*/, 3]; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "interrupted" }))]; - case 2: - _g.sent(); - throw new Error("Interrupted"); - case 3: - requestOptions = __assign({ model: model, input: messages, tools: tools_js_1.toolsForResponses, tool_choice: "auto", parallel_tool_calls: true, max_output_tokens: 2000 }, (supportsReasoning && { - reasoning: { - effort: "minimal", // Use minimal effort for responses API - summary: "detailed", // Request detailed reasoning summaries - }, - })); - // Apply provider-specific adjustments - requestOptions = adjustRequestForProvider(requestOptions, "responses", baseURL, supportsReasoning); - return [4 /*yield*/, client.responses.create(requestOptions, { signal: signal })]; - case 4: - response = _g.sent(); - // Report token usage if available (responses API format) - if (response.usage) { - usage = response.usage; - eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ - type: "token_usage", - inputTokens: usage.input_tokens || 0, - outputTokens: usage.output_tokens || 0, - totalTokens: usage.total_tokens || 0, - cacheReadTokens: ((_e = usage.input_tokens_details) === null || _e === void 0 ? void 0 : _e.cached_tokens) || 0, - cacheWriteTokens: 0, // Not available in API - reasoningTokens: ((_f = usage.output_tokens_details) === null || _f === void 0 ? void 0 : _f.reasoning_tokens) || 0, - }); - } - output = response.output; - if (!output) - return [3 /*break*/, 31]; - _i = 0, output_1 = output; - _g.label = 5; - case 5: - if (!(_i < output_1.length)) return [3 /*break*/, 30]; - item = output_1[_i]; - // gpt-oss vLLM quirk: need to remove type from "message" events - if (item.id === "message") { - type = item.type, message = __rest(item, ["type"]); - messages.push(item); - } - else { - messages.push(item); - } - _a = item.type; - switch (_a) { - case "reasoning": return [3 /*break*/, 6]; - case "message": return [3 /*break*/, 11]; - case "function_call": return [3 /*break*/, 19]; - } - return [3 /*break*/, 28]; - case 6: - reasoningItems = item.content || item.summary || []; - _b = 0, reasoningItems_1 = reasoningItems; - _g.label = 7; - case 7: - if (!(_b < reasoningItems_1.length)) return [3 /*break*/, 10]; - content = reasoningItems_1[_b]; - if (!(content.type === "reasoning_text" || content.type === "summary_text")) return [3 /*break*/, 9]; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "reasoning", text: content.text }))]; - case 8: - _g.sent(); - _g.label = 9; - case 9: - _b++; - return [3 /*break*/, 7]; - case 10: return [3 /*break*/, 29]; - case 11: - _c = 0, _d = item.content || []; - _g.label = 12; - case 12: - if (!(_c < _d.length)) return [3 /*break*/, 18]; - content = _d[_c]; - if (!(content.type === "output_text")) return [3 /*break*/, 14]; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "assistant_message", text: content.text }))]; - case 13: - _g.sent(); - return [3 /*break*/, 16]; - case 14: - if (!(content.type === "refusal")) return [3 /*break*/, 16]; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "error", message: "Refusal: ".concat(content.refusal) }))]; - case 15: - _g.sent(); - _g.label = 16; - case 16: - conversationDone = true; - _g.label = 17; - case 17: - _c++; - return [3 /*break*/, 12]; - case 18: return [3 /*break*/, 29]; - case 19: - if (!(signal === null || signal === void 0 ? void 0 : signal.aborted)) return [3 /*break*/, 21]; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "interrupted" }))]; - case 20: - _g.sent(); - throw new Error("Interrupted"); - case 21: - _g.trys.push([21, 25, , 27]); - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ - type: "tool_call", - toolCallId: item.call_id || "", - name: item.name, - args: item.arguments, - }))]; - case 22: - _g.sent(); - return [4 /*yield*/, (0, tools_js_1.executeTool)(item.name, item.arguments, signal)]; - case 23: - result = _g.sent(); - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ - type: "tool_result", - toolCallId: item.call_id || "", - result: result, - isError: false, - }))]; - case 24: - _g.sent(); - toolResultMsg = { - type: "function_call_output", - call_id: item.call_id, - output: result, - }; - messages.push(toolResultMsg); - return [3 /*break*/, 27]; - case 25: - e_1 = _g.sent(); - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ - type: "tool_result", - toolCallId: item.call_id || "", - result: e_1.message, - isError: true, - }))]; - case 26: - _g.sent(); - errorMsg = { - type: "function_call_output", - call_id: item.id, - output: e_1.message, - isError: true, - }; - messages.push(errorMsg); - return [3 /*break*/, 27]; - case 27: return [3 /*break*/, 29]; - case 28: - { - eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "error", message: "Unknown output type in LLM response: ".concat(item.type) }); - return [3 /*break*/, 29]; - } - _g.label = 29; - case 29: - _i++; - return [3 /*break*/, 5]; - case 30: return [3 /*break*/, 1]; - case 31: return [2 /*return*/]; - } - }); - }); -} -function callModelChatCompletionsApi(client, model, messages, signal, eventReceiver, supportsReasoning, baseURL) { - return __awaiter(this, void 0, void 0, function () { - var assistantResponded, requestOptions, response, message, usage, assistantMsg, _i, _a, toolCall, funcName, funcArgs, result, toolMsg, e_2, errorMsg, _b, cleanContent, reasoningTexts, _c, reasoningTexts_1, reasoning, finalMsg; - var _d, _e; - return __generator(this, function (_f) { - switch (_f.label) { - case 0: - assistantResponded = false; - _f.label = 1; - case 1: - if (!!assistantResponded) return [3 /*break*/, 23]; - if (!(signal === null || signal === void 0 ? void 0 : signal.aborted)) return [3 /*break*/, 3]; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "interrupted" }))]; - case 2: - _f.sent(); - throw new Error("Interrupted"); - case 3: - requestOptions = __assign({ model: model, messages: messages, tools: tools_js_1.toolsForChat, tool_choice: "auto", max_completion_tokens: 2000 }, (supportsReasoning && { - reasoning_effort: "low", // Use low effort for completions API - })); - // Apply provider-specific adjustments - requestOptions = adjustRequestForProvider(requestOptions, "completions", baseURL, supportsReasoning); - return [4 /*yield*/, client.chat.completions.create(requestOptions, { signal: signal })]; - case 4: - response = _f.sent(); - message = response.choices[0].message; - if (!response.usage) return [3 /*break*/, 6]; - usage = response.usage; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ - type: "token_usage", - inputTokens: usage.prompt_tokens || 0, - outputTokens: usage.completion_tokens || 0, - totalTokens: usage.total_tokens || 0, - cacheReadTokens: ((_d = usage.prompt_tokens_details) === null || _d === void 0 ? void 0 : _d.cached_tokens) || 0, - cacheWriteTokens: 0, // Not available in API - reasoningTokens: ((_e = usage.completion_tokens_details) === null || _e === void 0 ? void 0 : _e.reasoning_tokens) || 0, - }))]; - case 5: - _f.sent(); - _f.label = 6; - case 6: - if (!(message.tool_calls && message.tool_calls.length > 0)) return [3 /*break*/, 16]; - assistantMsg = { - role: "assistant", - content: message.content || null, - tool_calls: message.tool_calls, - }; - messages.push(assistantMsg); - _i = 0, _a = message.tool_calls; - _f.label = 7; - case 7: - if (!(_i < _a.length)) return [3 /*break*/, 15]; - toolCall = _a[_i]; - if (!(signal === null || signal === void 0 ? void 0 : signal.aborted)) return [3 /*break*/, 9]; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "interrupted" }))]; - case 8: - _f.sent(); - throw new Error("Interrupted"); - case 9: - _f.trys.push([9, 13, , 14]); - funcName = toolCall.type === "function" ? toolCall.function.name : toolCall.custom.name; - funcArgs = toolCall.type === "function" ? toolCall.function.arguments : toolCall.custom.input; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "tool_call", toolCallId: toolCall.id, name: funcName, args: funcArgs }))]; - case 10: - _f.sent(); - return [4 /*yield*/, (0, tools_js_1.executeTool)(funcName, funcArgs, signal)]; - case 11: - result = _f.sent(); - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "tool_result", toolCallId: toolCall.id, result: result, isError: false }))]; - case 12: - _f.sent(); - toolMsg = { - role: "tool", - tool_call_id: toolCall.id, - content: result, - }; - messages.push(toolMsg); - return [3 /*break*/, 14]; - case 13: - e_2 = _f.sent(); - eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "tool_result", toolCallId: toolCall.id, result: e_2.message, isError: true }); - errorMsg = { - role: "tool", - tool_call_id: toolCall.id, - content: e_2.message, - }; - messages.push(errorMsg); - return [3 /*break*/, 14]; - case 14: - _i++; - return [3 /*break*/, 7]; - case 15: return [3 /*break*/, 22]; - case 16: - if (!message.content) return [3 /*break*/, 22]; - _b = parseReasoningFromMessage(message, baseURL), cleanContent = _b.cleanContent, reasoningTexts = _b.reasoningTexts; - _c = 0, reasoningTexts_1 = reasoningTexts; - _f.label = 17; - case 17: - if (!(_c < reasoningTexts_1.length)) return [3 /*break*/, 20]; - reasoning = reasoningTexts_1[_c]; - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "reasoning", text: reasoning }))]; - case 18: - _f.sent(); - _f.label = 19; - case 19: - _c++; - return [3 /*break*/, 17]; - case 20: - // Emit the cleaned assistant message - return [4 /*yield*/, (eventReceiver === null || eventReceiver === void 0 ? void 0 : eventReceiver.on({ type: "assistant_message", text: cleanContent }))]; - case 21: - // Emit the cleaned assistant message - _f.sent(); - finalMsg = { role: "assistant", content: cleanContent }; - messages.push(finalMsg); - assistantResponded = true; - _f.label = 22; - case 22: return [3 /*break*/, 1]; - case 23: return [2 /*return*/]; - } - }); - }); -} -var Agent = /** @class */ (function () { - function Agent(config, renderer, sessionManager) { - var _this = this; - this.messages = []; - this.abortController = null; - this.supportsReasoning = null; - this.config = config; - this.client = new openai_1.default({ - apiKey: config.apiKey, - baseURL: config.baseURL, - }); - // Use provided renderer or default to console - this.renderer = renderer; - this.sessionManager = sessionManager; - this.comboReceiver = { - on: function (event) { return __awaiter(_this, void 0, void 0, function () { - var _a, _b; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: return [4 /*yield*/, ((_a = this.renderer) === null || _a === void 0 ? void 0 : _a.on(event))]; - case 1: - _c.sent(); - return [4 /*yield*/, ((_b = this.sessionManager) === null || _b === void 0 ? void 0 : _b.on(event))]; - case 2: - _c.sent(); - return [2 /*return*/]; - } - }); - }); }, - }; - // Initialize with system prompt if provided - if (config.systemPrompt) { - this.messages.push({ role: "system", content: config.systemPrompt }); - } - // Start session logging if we have a session manager - if (sessionManager) { - sessionManager.startSession(this.config); - // Emit session_start event - this.comboReceiver.on({ - type: "session_start", - sessionId: sessionManager.getSessionId(), - model: config.model, - api: config.api, - baseURL: config.baseURL, - systemPrompt: config.systemPrompt, - }); - } - } - Agent.prototype.ask = function (userMessage) { - return __awaiter(this, void 0, void 0, function () { - var userMsg, _a, e_3, errorMessage; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - // Render user message through the event system - this.comboReceiver.on({ type: "user_message", text: userMessage }); - userMsg = { role: "user", content: userMessage }; - this.messages.push(userMsg); - // Create a new AbortController for this chat session - this.abortController = new AbortController(); - _b.label = 1; - case 1: - _b.trys.push([1, 9, 10, 11]); - return [4 /*yield*/, this.comboReceiver.on({ type: "assistant_start" })]; - case 2: - _b.sent(); - if (!(this.supportsReasoning === null)) return [3 /*break*/, 4]; - _a = this; - return [4 /*yield*/, checkReasoningSupport(this.client, this.config.model, this.config.api, this.config.baseURL)]; - case 3: - _a.supportsReasoning = _b.sent(); - _b.label = 4; - case 4: - if (!(this.config.api === "responses")) return [3 /*break*/, 6]; - return [4 /*yield*/, callModelResponsesApi(this.client, this.config.model, this.messages, this.abortController.signal, this.comboReceiver, this.supportsReasoning, this.config.baseURL)]; - case 5: - _b.sent(); - return [3 /*break*/, 8]; - case 6: return [4 /*yield*/, callModelChatCompletionsApi(this.client, this.config.model, this.messages, this.abortController.signal, this.comboReceiver, this.supportsReasoning, this.config.baseURL)]; - case 7: - _b.sent(); - _b.label = 8; - case 8: return [3 /*break*/, 11]; - case 9: - e_3 = _b.sent(); - errorMessage = e_3 instanceof Error ? e_3.message : String(e_3); - if (errorMessage === "Interrupted" || this.abortController.signal.aborted) { - return [2 /*return*/]; - } - throw e_3; - case 10: - this.abortController = null; - return [7 /*endfinally*/]; - case 11: return [2 /*return*/]; - } - }); - }); - }; - Agent.prototype.interrupt = function () { - var _a; - (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort(); - }; - Agent.prototype.setEvents = function (events) { - // Reconstruct messages from events based on API type - this.messages = []; - if (this.config.api === "responses") { - // Responses API format - if (this.config.systemPrompt) { - this.messages.push({ - type: "system", - content: [{ type: "system_text", text: this.config.systemPrompt }], - }); - } - for (var _i = 0, events_1 = events; _i < events_1.length; _i++) { - var event_1 = events_1[_i]; - switch (event_1.type) { - case "user_message": - this.messages.push({ - type: "user", - content: [{ type: "input_text", text: event_1.text }], - }); - break; - case "reasoning": - // Add reasoning message - this.messages.push({ - type: "reasoning", - content: [{ type: "reasoning_text", text: event_1.text }], - }); - break; - case "tool_call": - // Add function call - this.messages.push({ - type: "function_call", - id: event_1.toolCallId, - name: event_1.name, - arguments: event_1.args, - }); - break; - case "tool_result": - // Add function result - this.messages.push({ - type: "function_call_output", - call_id: event_1.toolCallId, - output: event_1.result, - }); - break; - case "assistant_message": - // Add final message - this.messages.push({ - type: "message", - content: [{ type: "output_text", text: event_1.text }], - }); - break; - } - } - } - else { - // Chat Completions API format - if (this.config.systemPrompt) { - this.messages.push({ role: "system", content: this.config.systemPrompt }); - } - // Track tool calls in progress - var pendingToolCalls = []; - for (var _a = 0, events_2 = events; _a < events_2.length; _a++) { - var event_2 = events_2[_a]; - switch (event_2.type) { - case "user_message": - this.messages.push({ role: "user", content: event_2.text }); - break; - case "assistant_start": - // Reset pending tool calls for new assistant response - pendingToolCalls = []; - break; - case "tool_call": - // Accumulate tool calls - pendingToolCalls.push({ - id: event_2.toolCallId, - type: "function", - function: { - name: event_2.name, - arguments: event_2.args, - }, - }); - break; - case "tool_result": - // When we see the first tool result, add the assistant message with all tool calls - if (pendingToolCalls.length > 0) { - this.messages.push({ - role: "assistant", - content: null, - tool_calls: pendingToolCalls, - }); - pendingToolCalls = []; - } - // Add the tool result - this.messages.push({ - role: "tool", - tool_call_id: event_2.toolCallId, - content: event_2.result, - }); - break; - case "assistant_message": - // Final assistant response (no tool calls) - this.messages.push({ role: "assistant", content: event_2.text }); - break; - // Skip other event types (thinking, error, interrupted, token_usage) - } - } - } - }; - return Agent; -}()); -exports.Agent = Agent; diff --git a/packages/agent/src/args.js b/packages/agent/src/args.js deleted file mode 100644 index 1e0a8319..00000000 --- a/packages/agent/src/args.js +++ /dev/null @@ -1,164 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseArgs = parseArgs; -exports.printHelp = printHelp; -var os_1 = require("os"); -var path_1 = require("path"); -function parseArgs(defs, args) { - var result = { _: [] }; - var aliasMap = {}; - // Build alias map and set defaults - for (var _i = 0, _a = Object.entries(defs); _i < _a.length; _i++) { - var _b = _a[_i], key = _b[0], def = _b[1]; - if (def.alias) { - aliasMap[def.alias] = key; - } - if (def.default !== undefined) { - result[key] = def.default; - } - else if (def.type === "flag" || def.type === "boolean") { - result[key] = false; - } - } - // Parse arguments - for (var i = 0; i < args.length; i++) { - var arg = args[i]; - // Check if it's a flag - if (arg.startsWith("--")) { - var flagName = arg.slice(2); - var key = aliasMap[flagName] || flagName; - var def = defs[key]; - if (!def) { - // Unknown flag, add to positional args - result._.push(arg); - continue; - } - if (def.type === "flag") { - // Simple on/off flag - result[key] = true; - } - else if (i + 1 < args.length) { - // Flag with value - var value = args[++i]; - var parsedValue = void 0; - switch (def.type) { - case "boolean": - parsedValue = value === "true" || value === "1" || value === "yes"; - break; - case "int": - parsedValue = parseInt(value, 10); - if (Number.isNaN(parsedValue)) { - throw new Error("Invalid integer value for --".concat(key, ": ").concat(value)); - } - break; - case "float": - parsedValue = parseFloat(value); - if (Number.isNaN(parsedValue)) { - throw new Error("Invalid float value for --".concat(key, ": ").concat(value)); - } - break; - case "string": - parsedValue = value; - break; - case "file": { - // Resolve ~ to home directory and make absolute - var path = value; - if (path.startsWith("~")) { - path = path.replace("~", (0, os_1.homedir)()); - } - parsedValue = (0, path_1.resolve)(path); - break; - } - } - // Validate against choices if specified - if (def.choices) { - var validValues = def.choices.map(function (c) { return (typeof c === "string" ? c : c.value); }); - if (!validValues.includes(parsedValue)) { - throw new Error("Invalid value for --".concat(key, ": \"").concat(parsedValue, "\". Valid choices: ").concat(validValues.join(", "))); - } - } - result[key] = parsedValue; - } - else { - throw new Error("Flag --".concat(key, " requires a value")); - } - } - else if (arg.startsWith("-") && arg.length === 2) { - // Short flag like -h - var flagChar = arg[1]; - var key = aliasMap[flagChar] || flagChar; - var def = defs[key]; - if (!def) { - result._.push(arg); - continue; - } - if (def.type === "flag") { - result[key] = true; - } - else { - throw new Error("Short flag -".concat(flagChar, " cannot have a value")); - } - } - else { - // Positional argument - result._.push(arg); - } - } - return result; -} -function printHelp(defs, usage) { - console.log(usage); - console.log("\nOptions:"); - for (var _i = 0, _a = Object.entries(defs); _i < _a.length; _i++) { - var _b = _a[_i], key = _b[0], def = _b[1]; - var line = " --".concat(key); - if (def.alias) { - line += ", -".concat(def.alias); - } - if (def.type !== "flag") { - if (def.choices) { - // Show choices instead of type - var simpleChoices = def.choices.filter(function (c) { return typeof c === "string"; }); - if (simpleChoices.length === def.choices.length) { - // All choices are simple strings - line += " <".concat(simpleChoices.join("|"), ">"); - } - else { - // Has descriptions, just show the type - var typeStr = def.type === "file" ? "path" : def.type; - line += " <".concat(typeStr, ">"); - } - } - else { - var typeStr = def.type === "file" ? "path" : def.type; - line += " <".concat(typeStr, ">"); - } - } - if (def.description) { - // Pad to align descriptions - line = line.padEnd(30) + def.description; - } - if (def.default !== undefined && def.type !== "flag" && def.showDefault !== false) { - if (typeof def.showDefault === "string") { - line += " (default: ".concat(def.showDefault, ")"); - } - else { - line += " (default: ".concat(def.default, ")"); - } - } - console.log(line); - // Print choices with descriptions if available - if (def.choices) { - var hasDescriptions = def.choices.some(function (c) { return typeof c === "object" && c.description; }); - if (hasDescriptions) { - for (var _c = 0, _d = def.choices; _c < _d.length; _c++) { - var choice = _d[_c]; - if (typeof choice === "object") { - var choiceLine = " ".concat(choice.value).padEnd(30) + (choice.description || ""); - console.log(choiceLine); - } - } - } - } - } -} diff --git a/packages/agent/src/cli.js b/packages/agent/src/cli.js deleted file mode 100644 index 8dd86bc0..00000000 --- a/packages/agent/src/cli.js +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env node -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -var main_js_1 = require("./main.js"); -// Run as CLI - this file should always be executed, not imported -(0, main_js_1.main)(process.argv.slice(2)).catch(function (err) { - console.error(err); - process.exit(1); -}); diff --git a/packages/agent/src/main.js b/packages/agent/src/main.js deleted file mode 100644 index 9135838c..00000000 --- a/packages/agent/src/main.js +++ /dev/null @@ -1,364 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -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.main = main; -var chalk_1 = require("chalk"); -var readline_1 = require("readline"); -var agent_js_1 = require("./agent.js"); -var args_js_1 = require("./args.js"); -var console_renderer_js_1 = require("./renderers/console-renderer.js"); -var json_renderer_js_1 = require("./renderers/json-renderer.js"); -var tui_renderer_js_1 = require("./renderers/tui-renderer.js"); -var session_manager_js_1 = require("./session-manager.js"); -// Define argument structure -var argDefs = { - "base-url": { - type: "string", - default: "https://api.openai.com/v1", - description: "API base URL", - }, - "api-key": { - type: "string", - default: process.env.OPENAI_API_KEY || "", - description: "API key", - showDefault: "$OPENAI_API_KEY", - }, - model: { - type: "string", - default: "gpt-5-mini", - description: "Model name", - }, - api: { - type: "string", - default: "completions", - description: "API type", - choices: [ - { value: "completions", description: "OpenAI Chat Completions API (most models)" }, - { value: "responses", description: "OpenAI Responses API (GPT-OSS models)" }, - ], - }, - "system-prompt": { - type: "string", - default: "You are a helpful assistant.", - description: "System prompt", - }, - continue: { - type: "flag", - alias: "c", - description: "Continue previous session", - }, - json: { - type: "flag", - description: "Output as JSONL", - }, - help: { - type: "flag", - alias: "h", - description: "Show this help message", - }, -}; -function printHelp() { - var usage = "Usage: pi-agent [options] [messages...]\n\nExamples:\n# Single message (default OpenAI, GPT-5 Mini, OPENAI_API_KEY env var)\npi-agent \"What is 2+2?\"\n\n# Multiple messages processed sequentially\npi-agent \"What is 2+2?\" \"What about 3+3?\"\n\n# Interactive chat mode (no messages = interactive)\npi-agent\n\n# Continue most recently modified session in current directory\npi-agent --continue \"Follow up question\"\n\n# GPT-OSS via Groq\npi-agent --base-url https://api.groq.com/openai/v1 --api-key $GROQ_API_KEY --model openai/gpt-oss-120b\n\n# GLM 4.5 via OpenRouter\npi-agent --base-url https://openrouter.ai/api/v1 --api-key $OPENROUTER_API_KEY --model z-ai/glm-4.5\n\n# Claude via Anthropic (no prompt caching support - see https://docs.anthropic.com/en/api/openai-sdk)\npi-agent --base-url https://api.anthropic.com/v1 --api-key $ANTHROPIC_API_KEY --model claude-opus-4-1-20250805"; - (0, args_js_1.printHelp)(argDefs, usage); -} -function runJsonInteractiveMode(config, sessionManager) { - return __awaiter(this, void 0, void 0, function () { - var rl, renderer, agent, isProcessing, pendingMessage, processMessage; - var _this = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - rl = (0, readline_1.createInterface)({ - input: process.stdin, - output: process.stdout, - terminal: false, // Don't interpret control characters - }); - renderer = new json_renderer_js_1.JsonRenderer(); - agent = new agent_js_1.Agent(config, renderer, sessionManager); - isProcessing = false; - pendingMessage = null; - processMessage = function (content) { return __awaiter(_this, void 0, void 0, function () { - var e_1, msg; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - isProcessing = true; - _a.label = 1; - case 1: - _a.trys.push([1, 3, 5, 8]); - return [4 /*yield*/, agent.ask(content)]; - case 2: - _a.sent(); - return [3 /*break*/, 8]; - case 3: - e_1 = _a.sent(); - return [4 /*yield*/, renderer.on({ type: "error", message: e_1.message })]; - case 4: - _a.sent(); - return [3 /*break*/, 8]; - case 5: - isProcessing = false; - if (!pendingMessage) return [3 /*break*/, 7]; - msg = pendingMessage; - pendingMessage = null; - return [4 /*yield*/, processMessage(msg)]; - case 6: - _a.sent(); - _a.label = 7; - case 7: return [7 /*endfinally*/]; - case 8: return [2 /*return*/]; - } - }); - }); }; - // Listen for lines from stdin - rl.on("line", function (line) { - try { - var command = JSON.parse(line); - switch (command.type) { - case "interrupt": - agent.interrupt(); - isProcessing = false; - break; - case "message": - if (!command.content) { - renderer.on({ type: "error", message: "Message content is required" }); - return; - } - if (isProcessing) { - // Queue the message for when the agent is done - pendingMessage = command.content; - } - else { - processMessage(command.content); - } - break; - default: - renderer.on({ type: "error", message: "Unknown command type: ".concat(command.type) }); - } - } - catch (e) { - renderer.on({ type: "error", message: "Invalid JSON: ".concat(e) }); - } - }); - // Wait for stdin to close - return [4 /*yield*/, new Promise(function (resolve) { - rl.on("close", function () { - resolve(); - }); - })]; - case 1: - // Wait for stdin to close - _a.sent(); - return [2 /*return*/]; - } - }); - }); -} -function runTuiInteractiveMode(agentConfig, sessionManager) { - return __awaiter(this, void 0, void 0, function () { - var sessionData, renderer, agent, _i, _a, sessionEvent, event_1, userInput, e_2; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - sessionData = sessionManager.getSessionData(); - if (sessionData) { - console.log(chalk_1.default.dim("Resuming session with ".concat(sessionData.events.length, " events"))); - } - renderer = new tui_renderer_js_1.TuiRenderer(); - // Initialize TUI BEFORE creating the agent to prevent double init - return [4 /*yield*/, renderer.init()]; - case 1: - // Initialize TUI BEFORE creating the agent to prevent double init - _b.sent(); - agent = new agent_js_1.Agent(agentConfig, renderer, sessionManager); - renderer.setInterruptCallback(function () { - agent.interrupt(); - }); - if (!sessionData) return [3 /*break*/, 6]; - agent.setEvents(sessionData ? sessionData.events.map(function (e) { return e.event; }) : []); - _i = 0, _a = sessionData.events; - _b.label = 2; - case 2: - if (!(_i < _a.length)) return [3 /*break*/, 6]; - sessionEvent = _a[_i]; - event_1 = sessionEvent.event; - if (!(event_1.type === "assistant_start")) return [3 /*break*/, 3]; - renderer.renderAssistantLabel(); - return [3 /*break*/, 5]; - case 3: return [4 /*yield*/, renderer.on(event_1)]; - case 4: - _b.sent(); - _b.label = 5; - case 5: - _i++; - return [3 /*break*/, 2]; - case 6: - if (!true) return [3 /*break*/, 13]; - return [4 /*yield*/, renderer.getUserInput()]; - case 7: - userInput = _b.sent(); - _b.label = 8; - case 8: - _b.trys.push([8, 10, , 12]); - return [4 /*yield*/, agent.ask(userInput)]; - case 9: - _b.sent(); - return [3 /*break*/, 12]; - case 10: - e_2 = _b.sent(); - return [4 /*yield*/, renderer.on({ type: "error", message: e_2.message })]; - case 11: - _b.sent(); - return [3 /*break*/, 12]; - case 12: return [3 /*break*/, 6]; - case 13: return [2 /*return*/]; - } - }); - }); -} -function runSingleShotMode(agentConfig, sessionManager, messages, jsonOutput) { - return __awaiter(this, void 0, void 0, function () { - var sessionData, renderer, agent, _i, messages_1, msg, e_3; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - sessionData = sessionManager.getSessionData(); - renderer = jsonOutput ? new json_renderer_js_1.JsonRenderer() : new console_renderer_js_1.ConsoleRenderer(); - agent = new agent_js_1.Agent(agentConfig, renderer, sessionManager); - if (sessionData) { - if (!jsonOutput) { - console.log(chalk_1.default.dim("Resuming session with ".concat(sessionData.events.length, " events"))); - } - agent.setEvents(sessionData ? sessionData.events.map(function (e) { return e.event; }) : []); - } - _i = 0, messages_1 = messages; - _a.label = 1; - case 1: - if (!(_i < messages_1.length)) return [3 /*break*/, 7]; - msg = messages_1[_i]; - _a.label = 2; - case 2: - _a.trys.push([2, 4, , 6]); - return [4 /*yield*/, agent.ask(msg)]; - case 3: - _a.sent(); - return [3 /*break*/, 6]; - case 4: - e_3 = _a.sent(); - return [4 /*yield*/, renderer.on({ type: "error", message: e_3.message })]; - case 5: - _a.sent(); - return [3 /*break*/, 6]; - case 6: - _i++; - return [3 /*break*/, 1]; - case 7: return [2 /*return*/]; - } - }); - }); -} -// Main function to use Agent as standalone CLI -function main(args) { - return __awaiter(this, void 0, void 0, function () { - var parsed, baseURL, apiKey, model, continueSession, api, systemPrompt, jsonOutput, messages, isInteractive, sessionManager, agentConfig, sessionData; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - parsed = (0, args_js_1.parseArgs)(argDefs, args); - // Show help if requested - if (parsed.help) { - printHelp(); - return [2 /*return*/]; - } - baseURL = parsed["base-url"]; - apiKey = parsed["api-key"]; - model = parsed.model; - continueSession = parsed.continue; - api = parsed.api; - systemPrompt = parsed["system-prompt"]; - jsonOutput = parsed.json; - messages = parsed._; - if (!apiKey) { - throw new Error("API key required (use --api-key or set OPENAI_API_KEY)"); - } - isInteractive = messages.length === 0; - sessionManager = new session_manager_js_1.SessionManager(continueSession); - agentConfig = { - apiKey: apiKey, - baseURL: baseURL, - model: model, - api: api, - systemPrompt: systemPrompt, - }; - if (continueSession) { - sessionData = sessionManager.getSessionData(); - if (sessionData) { - agentConfig = __assign(__assign({}, sessionData.config), { apiKey: apiKey }); - } - } - if (!isInteractive) return [3 /*break*/, 5]; - if (!jsonOutput) return [3 /*break*/, 2]; - return [4 /*yield*/, runJsonInteractiveMode(agentConfig, sessionManager)]; - case 1: - _a.sent(); - return [3 /*break*/, 4]; - case 2: return [4 /*yield*/, runTuiInteractiveMode(agentConfig, sessionManager)]; - case 3: - _a.sent(); - _a.label = 4; - case 4: return [3 /*break*/, 7]; - case 5: return [4 /*yield*/, runSingleShotMode(agentConfig, sessionManager, messages, jsonOutput)]; - case 6: - _a.sent(); - _a.label = 7; - case 7: return [2 /*return*/]; - } - }); - }); -} diff --git a/packages/agent/src/renderers/console-renderer.js b/packages/agent/src/renderers/console-renderer.js deleted file mode 100644 index 5f185a55..00000000 --- a/packages/agent/src/renderers/console-renderer.js +++ /dev/null @@ -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; diff --git a/packages/agent/src/renderers/json-renderer.js b/packages/agent/src/renderers/json-renderer.js deleted file mode 100644 index 1f14b62a..00000000 --- a/packages/agent/src/renderers/json-renderer.js +++ /dev/null @@ -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; diff --git a/packages/agent/src/renderers/tui-renderer.js b/packages/agent/src/renderers/tui-renderer.js deleted file mode 100644 index d159975d..00000000 --- a/packages/agent/src/renderers/tui-renderer.js +++ /dev/null @@ -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; diff --git a/packages/agent/src/session-manager.js b/packages/agent/src/session-manager.js deleted file mode 100644 index 106353f5..00000000 --- a/packages/agent/src/session-manager.js +++ /dev/null @@ -1,194 +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.SessionManager = void 0; -var crypto_1 = require("crypto"); -var fs_1 = require("fs"); -var os_1 = require("os"); -var path_1 = require("path"); -// Simple UUID v4 generator -function uuidv4() { - var bytes = (0, crypto_1.randomBytes)(16); - bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4 - bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 10 - var hex = bytes.toString("hex"); - return "".concat(hex.slice(0, 8), "-").concat(hex.slice(8, 12), "-").concat(hex.slice(12, 16), "-").concat(hex.slice(16, 20), "-").concat(hex.slice(20, 32)); -} -var SessionManager = /** @class */ (function () { - function SessionManager(continueSession) { - if (continueSession === void 0) { continueSession = false; } - this.sessionDir = this.getSessionDirectory(); - if (continueSession) { - var mostRecent = this.findMostRecentlyModifiedSession(); - if (mostRecent) { - this.sessionFile = mostRecent; - // Load session ID from file - this.loadSessionId(); - } - else { - // No existing session, create new - this.initNewSession(); - } - } - else { - this.initNewSession(); - } - } - SessionManager.prototype.getSessionDirectory = function () { - var cwd = process.cwd(); - var safePath = "--" + cwd.replace(/^\//, "").replace(/\//g, "-") + "--"; - var piConfigDir = (0, path_1.resolve)(process.env.PI_CONFIG_DIR || (0, path_1.join)((0, os_1.homedir)(), ".pi")); - var sessionDir = (0, path_1.join)(piConfigDir, "sessions", safePath); - if (!(0, fs_1.existsSync)(sessionDir)) { - (0, fs_1.mkdirSync)(sessionDir, { recursive: true }); - } - return sessionDir; - }; - SessionManager.prototype.initNewSession = function () { - this.sessionId = uuidv4(); - var timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - this.sessionFile = (0, path_1.join)(this.sessionDir, "".concat(timestamp, "_").concat(this.sessionId, ".jsonl")); - }; - SessionManager.prototype.findMostRecentlyModifiedSession = function () { - var _this = this; - var _a; - try { - var files = (0, fs_1.readdirSync)(this.sessionDir) - .filter(function (f) { return f.endsWith(".jsonl"); }) - .map(function (f) { return ({ - name: f, - path: (0, path_1.join)(_this.sessionDir, f), - mtime: (0, fs_1.statSync)((0, path_1.join)(_this.sessionDir, f)).mtime, - }); }) - .sort(function (a, b) { return b.mtime.getTime() - a.mtime.getTime(); }); - return ((_a = files[0]) === null || _a === void 0 ? void 0 : _a.path) || null; - } - catch (_b) { - return null; - } - }; - SessionManager.prototype.loadSessionId = function () { - if (!(0, fs_1.existsSync)(this.sessionFile)) - return; - var lines = (0, fs_1.readFileSync)(this.sessionFile, "utf8").trim().split("\n"); - for (var _i = 0, lines_1 = lines; _i < lines_1.length; _i++) { - var line = lines_1[_i]; - try { - var entry = JSON.parse(line); - if (entry.type === "session") { - this.sessionId = entry.id; - return; - } - } - catch (_a) { - // Skip malformed lines - } - } - // If no session entry found, create new ID - this.sessionId = uuidv4(); - }; - SessionManager.prototype.startSession = function (config) { - var entry = { - type: "session", - id: this.sessionId, - timestamp: new Date().toISOString(), - cwd: process.cwd(), - config: config, - }; - (0, fs_1.appendFileSync)(this.sessionFile, JSON.stringify(entry) + "\n"); - }; - SessionManager.prototype.on = function (event) { - return __awaiter(this, void 0, void 0, function () { - var entry; - return __generator(this, function (_a) { - entry = { - type: "event", - timestamp: new Date().toISOString(), - event: event, - }; - (0, fs_1.appendFileSync)(this.sessionFile, JSON.stringify(entry) + "\n"); - return [2 /*return*/]; - }); - }); - }; - SessionManager.prototype.getSessionData = function () { - if (!(0, fs_1.existsSync)(this.sessionFile)) - return null; - var config = null; - var events = []; - var totalUsage = { - type: "token_usage", - inputTokens: 0, - outputTokens: 0, - totalTokens: 0, - cacheReadTokens: 0, - cacheWriteTokens: 0, - reasoningTokens: 0, - }; - var lines = (0, fs_1.readFileSync)(this.sessionFile, "utf8").trim().split("\n"); - for (var _i = 0, lines_2 = lines; _i < lines_2.length; _i++) { - var line = lines_2[_i]; - try { - var entry = JSON.parse(line); - if (entry.type === "session") { - config = entry.config; - this.sessionId = entry.id; - } - else if (entry.type === "event") { - var eventEntry = entry; - events.push(eventEntry); - if (eventEntry.event.type === "token_usage") { - totalUsage = entry.event; - } - } - } - catch (_a) { - // Skip malformed lines - } - } - return config ? { config: config, events: events, totalUsage: totalUsage } : null; - }; - SessionManager.prototype.getSessionId = function () { - return this.sessionId; - }; - SessionManager.prototype.getSessionFile = function () { - return this.sessionFile; - }; - return SessionManager; -}()); -exports.SessionManager = SessionManager; diff --git a/packages/agent/src/tools/tools.js b/packages/agent/src/tools/tools.js deleted file mode 100644 index 44bbb0ec..00000000 --- a/packages/agent/src/tools/tools.js +++ /dev/null @@ -1,316 +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.toolsForChat = exports.toolsForResponses = void 0; -exports.executeTool = executeTool; -var node_child_process_1 = require("node:child_process"); -var node_fs_1 = require("node:fs"); -var node_path_1 = require("node:path"); -var glob_1 = require("glob"); -// For GPT-OSS models via responses API -exports.toolsForResponses = [ - { - type: "function", - name: "read", - description: "Read contents of a file", - parameters: { - type: "object", - properties: { - path: { - type: "string", - description: "Path to the file to read", - }, - }, - required: ["path"], - }, - }, - { - type: "function", - name: "list", - description: "List contents of a directory", - parameters: { - type: "object", - properties: { - path: { - type: "string", - description: "Path to the directory (default: current directory)", - }, - }, - }, - }, - { - type: "function", - name: "bash", - description: "Execute a command in Bash", - parameters: { - type: "object", - properties: { - command: { - type: "string", - description: "Command to execute", - }, - }, - required: ["command"], - }, - }, - { - type: "function", - name: "glob", - description: "Find files matching a glob pattern", - parameters: { - type: "object", - properties: { - pattern: { - type: "string", - description: "Glob pattern to match files (e.g., '**/*.ts', 'src/**/*.json')", - }, - path: { - type: "string", - description: "Directory to search in (default: current directory)", - }, - }, - required: ["pattern"], - }, - }, - { - type: "function", - name: "rg", - description: "Search using ripgrep.", - parameters: { - type: "object", - properties: { - args: { - type: "string", - description: 'Arguments to pass directly to ripgrep. Examples: "-l prompt" or "-i TODO" or "--type ts className" or "functionName src/". Never add quotes around the search pattern.', - }, - }, - required: ["args"], - }, - }, -]; -// For standard chat API (OpenAI format) -exports.toolsForChat = exports.toolsForResponses.map(function (tool) { return ({ - type: "function", - function: { - name: tool.name, - description: tool.description, - parameters: tool.parameters, - }, -}); }); -// Helper to execute commands with abort support -function execWithAbort(command, signal) { - return __awaiter(this, void 0, void 0, function () { - return __generator(this, function (_a) { - return [2 /*return*/, new Promise(function (resolve, reject) { - var _a, _b; - var child = (0, node_child_process_1.spawn)(command, { - shell: true, - signal: signal, - }); - var stdout = ""; - var stderr = ""; - var MAX_OUTPUT_SIZE = 1024 * 1024; // 1MB limit - var outputTruncated = false; - (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on("data", function (data) { - var chunk = data.toString(); - if (stdout.length + chunk.length > MAX_OUTPUT_SIZE) { - if (!outputTruncated) { - stdout += "\n... [Output truncated - exceeded 1MB limit] ..."; - outputTruncated = true; - } - } - else { - stdout += chunk; - } - }); - (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on("data", function (data) { - var chunk = data.toString(); - if (stderr.length + chunk.length > MAX_OUTPUT_SIZE) { - if (!outputTruncated) { - stderr += "\n... [Output truncated - exceeded 1MB limit] ..."; - outputTruncated = true; - } - } - else { - stderr += chunk; - } - }); - child.on("error", function (error) { - reject(error); - }); - child.on("close", function (code) { - if (signal === null || signal === void 0 ? void 0 : signal.aborted) { - reject(new Error("Interrupted")); - } - else if (code !== 0 && code !== null) { - // For some commands like ripgrep, exit code 1 is normal (no matches) - if (code === 1 && command.includes("rg")) { - resolve(""); // No matches for ripgrep - } - else if (stderr && !stdout) { - reject(new Error(stderr)); - } - else { - resolve(stdout || ""); - } - } - else { - resolve(stdout || stderr || ""); - } - }); - // Kill the process if signal is aborted - if (signal) { - signal.addEventListener("abort", function () { - child.kill("SIGTERM"); - }, { once: true }); - } - })]; - }); - }); -} -function executeTool(name, args, signal) { - return __awaiter(this, void 0, void 0, function () { - var parsed, _a, path, file, stats, MAX_FILE_SIZE, fd, buffer, data, path, dir, entries, command, output, e_1, pattern, searchPath, matches, e_2, args_1, cmd, output, e_3; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - parsed = JSON.parse(args); - _a = name; - switch (_a) { - case "read": return [3 /*break*/, 1]; - case "list": return [3 /*break*/, 2]; - case "bash": return [3 /*break*/, 3]; - case "glob": return [3 /*break*/, 7]; - case "rg": return [3 /*break*/, 11]; - } - return [3 /*break*/, 15]; - case 1: - { - path = parsed.path; - if (!path) - return [2 /*return*/, "Error: path parameter is required"]; - file = (0, node_path_1.resolve)(path); - if (!(0, node_fs_1.existsSync)(file)) - return [2 /*return*/, "File not found: ".concat(file)]; - stats = (0, node_fs_1.statSync)(file); - MAX_FILE_SIZE = 1024 * 1024; - if (stats.size > MAX_FILE_SIZE) { - fd = (0, node_fs_1.openSync)(file, "r"); - buffer = Buffer.alloc(MAX_FILE_SIZE); - (0, node_fs_1.readSync)(fd, buffer, 0, MAX_FILE_SIZE, 0); - (0, node_fs_1.closeSync)(fd); - return [2 /*return*/, buffer.toString("utf8") + "\n\n... [File truncated - exceeded 1MB limit] ..."]; - } - data = (0, node_fs_1.readFileSync)(file, "utf8"); - return [2 /*return*/, data]; - } - _b.label = 2; - case 2: - { - path = parsed.path || "."; - dir = (0, node_path_1.resolve)(path); - if (!(0, node_fs_1.existsSync)(dir)) - return [2 /*return*/, "Directory not found: ".concat(dir)]; - entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true }); - return [2 /*return*/, entries.map(function (entry) { return (entry.isDirectory() ? entry.name + "/" : entry.name); }).join("\n")]; - } - _b.label = 3; - case 3: - command = parsed.command; - if (!command) - return [2 /*return*/, "Error: command parameter is required"]; - _b.label = 4; - case 4: - _b.trys.push([4, 6, , 7]); - return [4 /*yield*/, execWithAbort(command, signal)]; - case 5: - output = _b.sent(); - return [2 /*return*/, output || "Command executed successfully"]; - case 6: - e_1 = _b.sent(); - if (e_1.message === "Interrupted") { - throw e_1; // Re-throw interruption - } - throw new Error("Command failed: ".concat(e_1.message)); - case 7: - pattern = parsed.pattern; - if (!pattern) - return [2 /*return*/, "Error: pattern parameter is required"]; - searchPath = parsed.path || process.cwd(); - _b.label = 8; - case 8: - _b.trys.push([8, 10, , 11]); - return [4 /*yield*/, (0, glob_1.glob)(pattern, { - cwd: searchPath, - dot: true, - nodir: false, - mark: true, // Add / to directories - })]; - case 9: - matches = _b.sent(); - if (matches.length === 0) { - return [2 /*return*/, "No files found matching the pattern"]; - } - // Sort by modification time (most recent first) if possible - return [2 /*return*/, matches.sort().join("\n")]; - case 10: - e_2 = _b.sent(); - return [2 /*return*/, "Glob error: ".concat(e_2.message)]; - case 11: - args_1 = parsed.args; - if (!args_1) - return [2 /*return*/, "Error: args parameter is required"]; - cmd = "rg ".concat(args_1, " < /dev/null"); - _b.label = 12; - case 12: - _b.trys.push([12, 14, , 15]); - return [4 /*yield*/, execWithAbort(cmd, signal)]; - case 13: - output = _b.sent(); - return [2 /*return*/, output.trim() || "No matches found"]; - case 14: - e_3 = _b.sent(); - if (e_3.message === "Interrupted") { - throw e_3; // Re-throw interruption - } - return [2 /*return*/, "ripgrep error: ".concat(e_3.message)]; - case 15: return [2 /*return*/, "Unknown tool: ".concat(name)]; - } - }); - }); -} diff --git a/packages/pods/src/models.json b/packages/pods/src/models.json index d7a3b2df..450f1447 100644 --- a/packages/pods/src/models.json +++ b/packages/pods/src/models.json @@ -231,13 +231,7 @@ { "gpuCount": 1, "gpuTypes": ["H200"], - "args": [ - "--tool-call-parser", - "glm45", - "--reasoning-parser", - "glm45", - "--enable-auto-tool-choice" - ], + "args": ["--tool-call-parser", "glm45", "--reasoning-parser", "glm45", "--enable-auto-tool-choice"], "env": { "VLLM_ATTENTION_BACKEND": "XFORMERS" }, diff --git a/packages/tui/README.md b/packages/tui/README.md index 4defb7ea..2c1e0308 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -268,7 +268,7 @@ The TUI uses a three-strategy rendering system that minimizes redraws to only wh - Action: Updates only specific changed lines (typically 1-2 lines) - Example: Loading spinner animation, updating status text -2. **Partial Re-render** +2. **Partial Re-render** - When: Line count changes or structural changes within viewport - Action: Clears from first change to end of screen, re-renders tail - Example: Adding new messages to a chat, expanding text editor @@ -438,16 +438,16 @@ test("my TUI test", async () => { const terminal = new VirtualTerminal(80, 24); const ui = new TUI(terminal); ui.start(); - + ui.addChild(new TextComponent("Hello")); - + // Wait for render await new Promise(resolve => process.nextTick(resolve)); - + // Get rendered output const viewport = await terminal.flushAndGetViewport(); assert.strictEqual(viewport[0], "Hello"); - + ui.stop(); }); ``` @@ -469,33 +469,33 @@ describe("My Feature", () => { const terminal = new VirtualTerminal(80, 24); const ui = new TUI(terminal); ui.start(); - + // Setup components const container = new Container(); ui.addChild(container); - + // Initial render await new Promise(resolve => process.nextTick(resolve)); await terminal.flush(); - + // Check viewport (visible content) let viewport = terminal.getViewport(); assert.strictEqual(viewport.length, 24); - + // Check scrollback buffer (all content including history) let scrollBuffer = terminal.getScrollBuffer(); - + // Simulate user input terminal.sendInput("Hello"); - + // Wait for processing await new Promise(resolve => process.nextTick(resolve)); await terminal.flush(); - + // Verify changes viewport = terminal.getViewport(); // ... assertions - + ui.stop(); }); }); @@ -560,9 +560,6 @@ Monitor real-time performance metrics: # Install dependencies (from monorepo root) npm install -# Build the package -npm run build - # Run type checking npm run check diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 39d9c3a8..dfef465c 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -2,7 +2,6 @@ import { readdirSync, statSync } from "fs"; import mimeTypes from "mime-types"; import { homedir } from "os"; import { basename, dirname, extname, join } from "path"; -import { logger } from "./logger.js"; function isAttachableFile(filePath: string): boolean { const mimeType = mimeTypes.lookup(filePath); @@ -142,12 +141,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { cursorLine: number, cursorCol: number, ): { items: AutocompleteItem[]; prefix: string } | null { - logger.debug("CombinedAutocompleteProvider", "getSuggestions called", { - lines, - cursorLine, - cursorCol, - }); - const currentLine = lines[cursorLine] || ""; const textBeforeCursor = currentLine.slice(0, cursorCol); @@ -202,10 +195,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { // Check for file paths - triggered by Tab or if we detect a path pattern const pathMatch = this.extractPathPrefix(textBeforeCursor, false); - logger.debug("CombinedAutocompleteProvider", "Path match check", { - textBeforeCursor, - pathMatch, - }); if (pathMatch !== null) { const suggestions = this.getFileSuggestions(pathMatch); @@ -342,11 +331,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { // Get file/directory suggestions for a given path prefix private getFileSuggestions(prefix: string): AutocompleteItem[] { - logger.debug("CombinedAutocompleteProvider", "getFileSuggestions called", { - prefix, - basePath: this.basePath, - }); - try { let searchDir: string; let searchPrefix: string; @@ -399,11 +383,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { searchPrefix = file; } - logger.debug("CombinedAutocompleteProvider", "Searching directory", { - searchDir, - searchPrefix, - }); - const entries = readdirSync(searchDir); const suggestions: AutocompleteItem[] = []; @@ -479,17 +458,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { return a.label.localeCompare(b.label); }); - logger.debug("CombinedAutocompleteProvider", "Returning suggestions", { - count: suggestions.length, - firstFew: suggestions.slice(0, 3).map((s) => s.label), - }); - return suggestions.slice(0, 10); // Limit to 10 suggestions } catch (e) { // Directory doesn't exist or not accessible - logger.error("CombinedAutocompleteProvider", "Error reading directory", { - error: e instanceof Error ? e.message : String(e), - }); return []; } } @@ -500,12 +471,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { cursorLine: number, cursorCol: number, ): { items: AutocompleteItem[]; prefix: string } | null { - logger.debug("CombinedAutocompleteProvider", "getForceFileSuggestions called", { - lines, - cursorLine, - cursorCol, - }); - const currentLine = lines[cursorLine] || ""; const textBeforeCursor = currentLine.slice(0, cursorCol); @@ -516,11 +481,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { // Force extract path prefix - this will always return something const pathMatch = this.extractPathPrefix(textBeforeCursor, true); - logger.debug("CombinedAutocompleteProvider", "Forced path match", { - textBeforeCursor, - pathMatch, - }); - if (pathMatch !== null) { const suggestions = this.getFileSuggestions(pathMatch); if (suggestions.length === 0) return null; diff --git a/packages/tui/src/components/text-editor.ts b/packages/tui/src/components/text-editor.ts index 39f8d47c..1837d795 100644 --- a/packages/tui/src/components/text-editor.ts +++ b/packages/tui/src/components/text-editor.ts @@ -1,6 +1,5 @@ import chalk from "chalk"; import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js"; -import { logger } from "../logger.js"; import { type Component, type ComponentRenderResult, getNextComponentId } from "../tui.js"; import { SelectList } from "./select-list.js"; @@ -44,12 +43,10 @@ export class TextEditor implements Component { if (config) { this.config = { ...this.config, ...config }; } - logger.componentLifecycle("TextEditor", "created", { config: this.config }); } configure(config: Partial): void { this.config = { ...this.config, ...config }; - logger.info("TextEditor", "Configuration updated", { config: this.config }); } setAutocompleteProvider(provider: AutocompleteProvider): void { @@ -127,48 +124,22 @@ export class TextEditor implements Component { } handleInput(data: string): void { - logger.keyInput("TextEditor", data); - logger.debug("TextEditor", "Current state before input", { - lines: this.state.lines, - cursorLine: this.state.cursorLine, - cursorCol: this.state.cursorCol, - }); - // Handle special key combinations first // Ctrl+C - Exit (let parent handle this) if (data.charCodeAt(0) === 3) { - logger.debug("TextEditor", "Ctrl+C received, returning to parent"); return; } // Handle paste - detect when we get a lot of text at once const isPaste = data.length > 10 || (data.length > 2 && data.includes("\n")); - logger.debug("TextEditor", "Paste detection", { - dataLength: data.length, - includesNewline: data.includes("\n"), - includesTabs: data.includes("\t"), - tabCount: (data.match(/\t/g) || []).length, - isPaste, - data: JSON.stringify(data), - charCodes: Array.from(data).map((c) => c.charCodeAt(0)), - }); - if (isPaste) { - logger.info("TextEditor", "Handling as paste"); this.handlePaste(data); return; } // Handle autocomplete special keys first (but don't block other input) if (this.isAutocompleting && this.autocompleteList) { - logger.debug("TextEditor", "Autocomplete active, handling input", { - data, - charCode: data.charCodeAt(0), - isEscape: data === "\x1b", - isArrowOrEnter: data === "\x1b[A" || data === "\x1b[B" || data === "\r", - }); - // Escape - cancel autocomplete if (data === "\x1b") { this.cancelAutocomplete(); @@ -216,15 +187,10 @@ export class TextEditor implements Component { } // For other keys (like regular typing), DON'T return here // Let them fall through to normal character handling - logger.debug("TextEditor", "Autocomplete active but falling through to normal handling"); } // Tab key - context-aware completion (but not when already autocompleting) if (data === "\t" && !this.isAutocompleting) { - logger.debug("TextEditor", "Tab key pressed, determining context", { - isAutocompleting: this.isAutocompleting, - hasProvider: !!this.autocompleteProvider, - }); this.handleTabCompletion(); return; } @@ -263,12 +229,6 @@ export class TextEditor implements Component { // Plain Enter = submit const result = this.state.lines.join("\n").trim(); - logger.info("TextEditor", "Submit triggered", { - result, - rawResult: JSON.stringify(this.state.lines.join("\n")), - lines: this.state.lines, - resultLines: result.split("\n"), - }); // Reset editor this.state = { @@ -283,10 +243,7 @@ export class TextEditor implements Component { } if (this.onSubmit) { - logger.info("TextEditor", "Calling onSubmit callback", { result }); this.onSubmit(result); - } else { - logger.warn("TextEditor", "No onSubmit callback set"); } } // Backspace @@ -322,13 +279,7 @@ export class TextEditor implements Component { } // Regular characters (printable ASCII) else if (data.charCodeAt(0) >= 32 && data.charCodeAt(0) <= 126) { - logger.debug("TextEditor", "Inserting character", { char: data, charCode: data.charCodeAt(0) }); this.insertCharacter(data); - } else { - logger.warn("TextEditor", "Unhandled input", { - data, - charCodes: Array.from(data).map((c) => c.charCodeAt(0)), - }); } } @@ -458,12 +409,6 @@ export class TextEditor implements Component { } private handlePaste(pastedText: string): void { - logger.debug("TextEditor", "Processing paste", { - pastedText: JSON.stringify(pastedText), - hasTab: pastedText.includes("\t"), - tabCount: (pastedText.match(/\t/g) || []).length, - }); - // Clean the pasted text const cleanText = pastedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); @@ -667,11 +612,6 @@ export class TextEditor implements Component { // Autocomplete methods private tryTriggerAutocomplete(explicitTab: boolean = false): void { - logger.debug("TextEditor", "tryTriggerAutocomplete called", { - explicitTab, - hasProvider: !!this.autocompleteProvider, - }); - if (!this.autocompleteProvider) return; // Check if we should trigger file completion on Tab @@ -680,15 +620,6 @@ export class TextEditor implements Component { const shouldTrigger = !provider.shouldTriggerFileCompletion || provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol); - - logger.debug("TextEditor", "Tab file completion check", { - hasShouldTriggerMethod: !!provider.shouldTriggerFileCompletion, - shouldTrigger, - lines: this.state.lines, - cursorLine: this.state.cursorLine, - cursorCol: this.state.cursorCol, - }); - if (!shouldTrigger) { return; } @@ -700,12 +631,6 @@ export class TextEditor implements Component { this.state.cursorCol, ); - logger.debug("TextEditor", "Autocomplete suggestions", { - hasSuggestions: !!suggestions, - itemCount: suggestions?.items.length || 0, - prefix: suggestions?.prefix, - }); - if (suggestions && suggestions.items.length > 0) { this.autocompletePrefix = suggestions.prefix; this.autocompleteList = new SelectList(suggestions.items, 5); @@ -723,57 +648,16 @@ export class TextEditor implements Component { // Check if we're in a slash command context if (beforeCursor.trimStart().startsWith("/")) { - logger.debug("TextEditor", "Tab in slash command context", { beforeCursor }); this.handleSlashCommandCompletion(); - } else { - logger.debug("TextEditor", "Tab in file completion context", { beforeCursor }); - this.forceFileAutocomplete(); } } private handleSlashCommandCompletion(): void { // For now, fall back to regular autocomplete (slash commands) // This can be extended later to handle command-specific argument completion - logger.debug("TextEditor", "Handling slash command completion"); this.tryTriggerAutocomplete(true); } - private forceFileAutocomplete(): void { - logger.debug("TextEditor", "forceFileAutocomplete called", { - hasProvider: !!this.autocompleteProvider, - }); - - if (!this.autocompleteProvider) return; - - // Check if provider has the force method - const provider = this.autocompleteProvider as any; - if (!provider.getForceFileSuggestions) { - logger.debug("TextEditor", "Provider doesn't support forced file completion, falling back to regular"); - this.tryTriggerAutocomplete(true); - return; - } - - const suggestions = provider.getForceFileSuggestions( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - ); - - logger.debug("TextEditor", "Forced file autocomplete suggestions", { - hasSuggestions: !!suggestions, - itemCount: suggestions?.items.length || 0, - prefix: suggestions?.prefix, - }); - - if (suggestions && suggestions.items.length > 0) { - this.autocompletePrefix = suggestions.prefix; - this.autocompleteList = new SelectList(suggestions.items, 5); - this.isAutocompleting = true; - } else { - this.cancelAutocomplete(); - } - } - private cancelAutocomplete(): void { this.isAutocompleting = false; this.autocompleteList = undefined as any; diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index ba94a286..0e9c61e9 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -19,8 +19,6 @@ export { TextComponent } from "./components/text-component.js"; export { TextEditor, type TextEditorConfig } from "./components/text-editor.js"; // Whitespace component export { WhitespaceComponent } from "./components/whitespace-component.js"; -// Logger for debugging -export { type LoggerConfig, logger } from "./logger.js"; // Terminal interface and implementations export { ProcessTerminal, type Terminal } from "./terminal.js"; export { diff --git a/packages/tui/src/logger.ts b/packages/tui/src/logger.ts deleted file mode 100644 index eb90dcc8..00000000 --- a/packages/tui/src/logger.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { appendFileSync, writeFileSync } from "fs"; -import { join } from "path"; - -export interface LoggerConfig { - enabled: boolean; - logFile: string; - logLevel: "debug" | "info" | "warn" | "error"; -} - -class Logger { - private config: LoggerConfig = { - enabled: false, - logFile: "tui-debug.log", // Will be resolved when needed - logLevel: "debug", - }; - - configure(config: Partial): void { - this.config = { ...this.config, ...config }; - - if (this.config.enabled) { - // Clear log file on startup - try { - // Resolve log file path when needed - const logFile = this.config.logFile.startsWith("/") - ? this.config.logFile - : join(process.cwd(), this.config.logFile); - - writeFileSync(logFile, `=== TUI Debug Log Started ${new Date().toISOString()} ===\n`); - } catch (error) { - // Silently fail if we can't write to log file - } - } - } - - private shouldLog(level: string): boolean { - if (!this.config.enabled) return false; - - const levels = ["debug", "info", "warn", "error"]; - const currentLevel = levels.indexOf(this.config.logLevel); - const messageLevel = levels.indexOf(level); - - return messageLevel >= currentLevel; - } - - private log(level: string, component: string, message: string, data?: any): void { - if (!this.shouldLog(level)) return; - - try { - const timestamp = new Date().toISOString(); - const dataStr = data ? ` | Data: ${JSON.stringify(data)}` : ""; - const logLine = `[${timestamp}] ${level.toUpperCase()} [${component}] ${message}${dataStr}\n`; - - // Resolve log file path when needed - const logFile = this.config.logFile.startsWith("/") - ? this.config.logFile - : join(process.cwd(), this.config.logFile); - - appendFileSync(logFile, logLine); - } catch (error) { - // Silently fail if we can't write to log file - } - } - - debug(component: string, message: string, data?: any): void { - this.log("debug", component, message, data); - } - - info(component: string, message: string, data?: any): void { - this.log("info", component, message, data); - } - - warn(component: string, message: string, data?: any): void { - this.log("warn", component, message, data); - } - - error(component: string, message: string, data?: any): void { - this.log("error", component, message, data); - } - - // Specific TUI logging methods - keyInput(component: string, keyData: string): void { - this.debug(component, "Key input received", { - keyData, - charCodes: Array.from(keyData).map((c) => c.charCodeAt(0)), - }); - } - - render(component: string, renderResult: any): void { - this.debug(component, "Render result", renderResult); - } - - focus(component: string, focused: boolean): void { - this.info(component, `Focus ${focused ? "gained" : "lost"}`); - } - - componentLifecycle(component: string, action: string, details?: any): void { - this.info(component, `Component ${action}`, details); - } - - stateChange(component: string, property: string, oldValue: any, newValue: any): void { - this.debug(component, `State change: ${property}`, { oldValue, newValue }); - } -} - -export const logger = new Logger(); diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index f08d45aa..8ee690b0 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -1,5 +1,4 @@ import process from "process"; -import { logger } from "./logger.js"; import { ProcessTerminal, type Terminal } from "./terminal.js"; /** @@ -40,8 +39,9 @@ export interface Padding { */ export class Container implements Component { readonly id: number; - protected children: (Component | Container)[] = []; + public children: (Component | Container)[] = []; private tui?: TUI; + private previousChildCount: number = 0; constructor() { this.id = getNextComponentId(); @@ -108,6 +108,12 @@ export class Container implements Component { const lines: string[] = []; let changed = false; + // Check if the number of children changed (important for detecting clears) + if (this.children.length !== this.previousChildCount) { + changed = true; + this.previousChildCount = this.children.length; + } + for (const child of this.children) { const result = child.render(width); lines.push(...result.lines); @@ -162,13 +168,6 @@ export class TUI extends Container { // Use provided terminal or default to ProcessTerminal this.terminal = terminal || new ProcessTerminal(); - - logger.componentLifecycle("TUI", "created"); - } - - configureLogging(config: Parameters[0]): void { - logger.configure(config); - logger.info("TUI", "Logging configured", config); } setFocus(component: Component): void { @@ -321,11 +320,6 @@ export class TUI extends Container { // Save what we rendered this.previousLines = lines; this.totalLinesRedrawn += lines.length; - - logger.debug("TUI", "Initial render", { - commandsExecuted: commands.length, - linesRendered: lines.length, - }); } private renderDifferentialSurgical(currentCommands: RenderCommand[], termHeight: number): void { @@ -344,7 +338,7 @@ export class TUI extends Container { let firstChangeOffset = -1; let hasLineCountChange = false; let hasStructuralChange = false; - const changedLines: Array<{lineIndex: number, newContent: string}> = []; + const changedLines: Array<{ lineIndex: number; newContent: string }> = []; let currentLineOffset = 0; @@ -373,15 +367,14 @@ export class TUI extends Container { // Content change with same line count - track individual line changes if (current.changed) { for (let j = 0; j < current.lines.length; j++) { - const oldLine = currentLineOffset + j < this.previousLines.length - ? this.previousLines[currentLineOffset + j] - : ""; + const oldLine = + currentLineOffset + j < this.previousLines.length ? this.previousLines[currentLineOffset + j] : ""; const newLine = current.lines[j]; if (oldLine !== newLine) { changedLines.push({ lineIndex: currentLineOffset + j, - newContent: newLine + newContent: newLine, }); if (firstChangeOffset === -1) { firstChangeOffset = currentLineOffset + j; @@ -417,7 +410,6 @@ export class TUI extends Container { if (newLines.length > 0) output += "\r\n"; linesRedrawn = newLines.length; - } else if (hasStructuralChange || hasLineCountChange) { // Strategy: PARTIAL - changes in viewport but with shifts, clear from change to end // After rendering with a final newline, cursor is one line below the last content line @@ -433,6 +425,9 @@ export class TUI extends Container { output += `\x1b[${linesToMoveUp}A`; } + // Clear from cursor to end of screen + // First ensure we're at the beginning of the line + output += "\r"; output += "\x1b[0J"; // Clear from cursor to end of screen const linesToRender = newLines.slice(firstChangeOffset); @@ -443,19 +438,11 @@ export class TUI extends Container { if (linesToRender.length > 0) output += "\r\n"; linesRedrawn = linesToRender.length; - } else { // Strategy: SURGICAL - only content changes with same line counts, update only changed lines // The cursor starts at the line after our last content let currentCursorLine = totalOldLines; - logger.debug("TUI", "SURGICAL strategy", { - totalOldLines, - totalNewLines, - changedLines: changedLines.map(c => ({ line: c.lineIndex, content: c.newContent.substring(0, 30) })), - currentCursorLine - }); - for (const change of changedLines) { // Move cursor to the line that needs updating const linesToMove = currentCursorLine - change.lineIndex; @@ -497,21 +484,9 @@ export class TUI extends Container { // Save what we rendered this.previousLines = newLines; this.totalLinesRedrawn += linesRedrawn; - - logger.debug("TUI", "Surgical differential render", { - strategy: changePositionInViewport < 0 ? "FULL" : - (hasStructuralChange || hasLineCountChange) ? "PARTIAL" : "SURGICAL", - linesRedrawn, - firstChangeOffset, - changePositionInViewport, - hasStructuralChange, - hasLineCountChange, - surgicalChanges: changedLines.length, - totalNewLines, - totalOldLines, - }); } + // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Keeping this around as reference for LLM private renderDifferential(currentCommands: RenderCommand[], termHeight: number): void { const viewportHeight = termHeight - 1; // Leave one line for cursor @@ -611,14 +586,6 @@ export class TUI extends Container { // Save what we rendered this.previousLines = newLines; this.totalLinesRedrawn += linesRedrawn; - - logger.debug("TUI", "Differential render", { - linesRedrawn, - firstChangedLineOffset, - changePositionInViewport, - totalNewLines, - totalOldLines, - }); } private handleResize(): void { @@ -628,8 +595,6 @@ export class TUI extends Container { } private handleKeypress(data: string): void { - logger.keyInput("TUI", data); - if (this.onGlobalKeyPress) { const shouldForward = this.onGlobalKeyPress(data); if (!shouldForward) { @@ -639,16 +604,8 @@ export class TUI extends Container { } if (this.focusedComponent?.handleInput) { - logger.debug("TUI", "Forwarding input to focused component", { - componentType: this.focusedComponent.constructor.name, - }); this.focusedComponent.handleInput(data); this.requestRender(); - } else { - logger.warn("TUI", "No focused component to handle input", { - focusedComponent: this.focusedComponent?.constructor.name || "none", - hasHandleInput: this.focusedComponent?.handleInput ? "yes" : "no", - }); } } } diff --git a/packages/tui/test/demo.ts b/packages/tui/test/demo.ts deleted file mode 100644 index 971c13d5..00000000 --- a/packages/tui/test/demo.ts +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env node - -import { - CombinedAutocompleteProvider, - Container, - MarkdownComponent, - TextComponent, - TextEditor, - TUI, -} from "../src/index.js"; - -// Create TUI manager -const ui = new TUI(); -ui.configureLogging({ - enabled: true, - logLevel: "debug", - logFile: "tui-debug.log", -}); - -// Create a chat container that will hold messages -const chatContainer = new Container(); -const editor = new TextEditor(); - -// Set up autocomplete with slash commands -const autocompleteProvider = new CombinedAutocompleteProvider( - [ - { name: "clear", description: "Clear chat history" }, - { name: "clear-last", description: "Clear last message" }, - { name: "exit", description: "Exit the application" }, - ], - process.cwd(), -); -editor.setAutocompleteProvider(autocompleteProvider); - -// Add components to UI -ui.addChild(new TextComponent("Differential Rendering TUI")); -ui.addChild(chatContainer); -ui.addChild(editor); - -// Set focus to the editor (index 2) -ui.setFocus(editor); - -// Test with Claude's multiline text -const testText = `Root level: -- CLAUDE.md -- README.md -- biome.json -- package.json -- package-lock.json -- tsconfig.json -- tui-debug.log - -Directories: -- \`data/\` (JSON test files) -- \`dist/\` -- \`docs/\` (markdown documentation) -- \`node_modules/\` -- \`src/\` (TypeScript source files)`; - -// Pre-fill the editor with the test text -editor.setText(testText); - -// Handle editor submissions -editor.onSubmit = (text: string) => { - text = text.trim(); - - if (text === "/clear") { - chatContainer.clear(); - ui.requestRender(); - return; - } - - if (text === "/clear-last") { - const count = chatContainer.getChildCount(); - if (count > 0) { - chatContainer.removeChildAt(count - 1); - ui.requestRender(); - } - return; - } - - if (text === "/exit") { - ui.stop(); - return; - } - - if (text) { - // Create new message component and add to chat container - const message = new MarkdownComponent(text); - chatContainer.addChild(message); - - // Manually trigger re-render - ui.requestRender(); - } -}; - -// Start the UI -ui.start(); diff --git a/packages/tui/test/file-browser.ts b/packages/tui/test/file-browser.ts new file mode 100644 index 00000000..d00d8c2a --- /dev/null +++ b/packages/tui/test/file-browser.ts @@ -0,0 +1,55 @@ +import { TUI, SelectList } from "../src/index.js"; +import { readdirSync, statSync } from "fs"; +import { join } from "path"; + +const ui = new TUI(); +ui.start(); +let currentPath = process.cwd(); + +function createFileList(path: string) { + const entries = readdirSync(path).map((entry) => { + const fullPath = join(path, entry); + const isDir = statSync(fullPath).isDirectory(); + return { + value: entry, + label: entry, + description: isDir ? "directory" : "file", + }; + }); + + // Add parent directory option + if (path !== "/") { + entries.unshift({ + value: "..", + label: "..", + description: "parent directory", + }); + } + + return entries; +} + +function showDirectory(path: string) { + ui.clear(); + + const entries = createFileList(path); + const fileList = new SelectList(entries, 10); + + fileList.onSelect = (item) => { + if (item.value === "..") { + currentPath = join(currentPath, ".."); + showDirectory(currentPath); + } else if (item.description === "directory") { + currentPath = join(currentPath, item.value); + showDirectory(currentPath); + } else { + console.log(`Selected file: ${join(currentPath, item.value)}`); + ui.stop(); + } + }; + + ui.addChild(fileList); + ui.setFocus(fileList); +} + +showDirectory(currentPath); \ No newline at end of file diff --git a/packages/tui/test/layout-shift-artifacts.test.ts b/packages/tui/test/layout-shift-artifacts.test.ts new file mode 100644 index 00000000..43f9a3cd --- /dev/null +++ b/packages/tui/test/layout-shift-artifacts.test.ts @@ -0,0 +1,151 @@ +import { describe, test } from "node:test"; +import assert from "node:assert"; +import { TextEditor, TextComponent, Container, TUI } from "../src/index.js"; +import { VirtualTerminal } from "./virtual-terminal.js"; + +describe("Layout shift artifacts", () => { + test("clears artifacts when components shift positions dynamically (like agent Ctrl+C)", async () => { + const term = new VirtualTerminal(80, 20); + const ui = new TUI(term); + + // Simulate agent's layout: header, chat container, status container, editor + const header = new TextComponent(">> pi interactive chat <<<"); + const chatContainer = new Container(); + const statusContainer = new Container(); + const editor = new TextEditor({ multiline: false }); + + // Add some chat content + chatContainer.addChild(new TextComponent("[user]")); + chatContainer.addChild(new TextComponent("Hello")); + chatContainer.addChild(new TextComponent("[assistant]")); + chatContainer.addChild(new TextComponent("Hi there!")); + + ui.addChild(header); + ui.addChild(chatContainer); + ui.addChild(statusContainer); + ui.addChild(editor); + + // Initial render + ui.start(); + await new Promise(resolve => process.nextTick(resolve)); + await term.flush(); + + // Capture initial state + const initialViewport = term.getViewport(); + + // Simulate what happens when Ctrl+C is pressed (like in agent) + statusContainer.clear(); + const hint = new TextComponent("Press Ctrl+C again to exit"); + statusContainer.addChild(hint); + ui.requestRender(); + + // Wait for render + await new Promise(resolve => process.nextTick(resolve)); + await term.flush(); + + // Capture state with status message + const withStatusViewport = term.getViewport(); + + // Simulate the timeout that clears the hint (like agent does after 500ms) + statusContainer.clear(); + ui.requestRender(); + + // Wait for render + await new Promise(resolve => process.nextTick(resolve)); + await term.flush(); + + // Capture final state + const finalViewport = term.getViewport(); + + // Check for artifacts - look for duplicate bottom borders on consecutive lines + let foundDuplicateBorder = false; + for (let i = 0; i < finalViewport.length - 1; i++) { + const currentLine = finalViewport[i]; + const nextLine = finalViewport[i + 1]; + + // Check if we have duplicate bottom borders (the artifact) + if (currentLine.includes("╰") && currentLine.includes("╯") && + nextLine.includes("╰") && nextLine.includes("╯")) { + foundDuplicateBorder = true; + } + } + + // The test should FAIL if we find duplicate borders (indicating the bug exists) + assert.strictEqual(foundDuplicateBorder, false, "Found duplicate bottom borders - rendering artifact detected!"); + + // Also check that there's only one bottom border total + const bottomBorderCount = finalViewport.filter((line) => + line.includes("╰") + ).length; + assert.strictEqual(bottomBorderCount, 1, `Expected 1 bottom border, found ${bottomBorderCount}`); + + // Verify the editor is back in its original position + const finalEditorStartLine = finalViewport.findIndex((line) => + line.includes("╭") + ); + const initialEditorStartLine = initialViewport.findIndex((line) => + line.includes("╭") + ); + assert.strictEqual(finalEditorStartLine, initialEditorStartLine); + + ui.stop(); + }); + + test("handles rapid addition and removal of components", async () => { + const term = new VirtualTerminal(80, 20); + const ui = new TUI(term); + + const header = new TextComponent("Header"); + const editor = new TextEditor({ multiline: false }); + + ui.addChild(header); + ui.addChild(editor); + + // Initial render + ui.start(); + await new Promise(resolve => process.nextTick(resolve)); + await term.flush(); + + // Rapidly add and remove a status message + const status = new TextComponent("Temporary Status"); + + // Add status + ui.children.splice(1, 0, status); + ui.requestRender(); + await new Promise(resolve => process.nextTick(resolve)); + await term.flush(); + + // Remove status immediately + ui.children.splice(1, 1); + ui.requestRender(); + await new Promise(resolve => process.nextTick(resolve)); + await term.flush(); + + // Final output check + const finalViewport = term.getViewport(); + + // Should only have one set of borders for the editor + const topBorderCount = finalViewport.filter((line) => + line.includes("╭") && line.includes("╮") + ).length; + const bottomBorderCount = finalViewport.filter((line) => + line.includes("╰") && line.includes("╯") + ).length; + + assert.strictEqual(topBorderCount, 1); + assert.strictEqual(bottomBorderCount, 1); + + // Check no duplicate lines + for (let i = 0; i < finalViewport.length - 1; i++) { + const currentLine = finalViewport[i]; + const nextLine = finalViewport[i + 1]; + + // If current line is a bottom border, next line should not be a bottom border + if (currentLine.includes("╰") && currentLine.includes("╯")) { + assert.strictEqual(nextLine.includes("╰") && nextLine.includes("╯"), false); + } + } + + ui.stop(); + }); +}); \ No newline at end of file diff --git a/packages/tui/test/multi-layout.ts b/packages/tui/test/multi-layout.ts index 741e20dd..a8178443 100644 --- a/packages/tui/test/multi-layout.ts +++ b/packages/tui/test/multi-layout.ts @@ -3,7 +3,7 @@ import { TUI, Container, TextComponent, TextEditor, MarkdownComponent } from ".. /** * Multi-Component Layout Demo - * + * * Demonstrates: * - Complex layout with multiple containers * - Header, sidebar, main content, and footer areas @@ -65,13 +65,6 @@ ui.addChild(mainContent); ui.addChild(footer); ui.setFocus(inputArea); -// Configure debug logging -ui.configureLogging({ - enabled: true, - level: "info", - logFile: "tui-debug.log", -}); - // Handle Ctrl+C to exit ui.onGlobalKeyPress = (data: string) => { if (data === "\x03") { diff --git a/todos/done/20250110-193139-agent-tui-ctrl-c-display-artifact-analysis.md b/todos/done/20250110-193139-agent-tui-ctrl-c-display-artifact-analysis.md new file mode 100644 index 00000000..1576fdae --- /dev/null +++ b/todos/done/20250110-193139-agent-tui-ctrl-c-display-artifact-analysis.md @@ -0,0 +1,82 @@ +## Analysis of TUI Differential Rendering and Layout Shift Artifacts + +### Key Findings + +**1. The Surgical Differential Rendering Implementation** + +The TUI uses a three-strategy rendering system in `renderDifferentialSurgical` (lines 331-513 in `/Users/badlogic/workspaces/pi-mono/packages/tui/src/tui.ts`): + +- **SURGICAL**: Updates only changed lines with same line counts +- **PARTIAL**: Re-renders from first change when structure/line counts shift +- **FULL**: Clears scrollback when changes are above viewport + +**2. The Critical Gap: Cursor Positioning in SURGICAL Strategy** + +The artifact issue lies in the SURGICAL strategy's cursor positioning logic (lines 447-493). When components are added and removed dynamically, the cursor positioning calculations become incorrect, leading to incomplete clearing of old content. + +**Specific Problem Areas:** + +```typescript +// Lines 484-492: Cursor repositioning after surgical updates +const lastContentLine = totalNewLines - 1; +const linesToMove = lastContentLine - currentCursorLine; +if (linesToMove > 0) { + output += `\x1b[${linesToMove}B`; +} else if (linesToMove < 0) { + output += `\x1b[${-linesToMove}A`; +} +// Now add final newline to position cursor on next line +output += "\r\n"; +``` + +**3. Component Change Detection Issues** + +The system determines changes by comparing: +- Component IDs (structural changes) +- Line counts (hasLineCountChange) +- Content with same line counts (changedLines array) + +However, when a status message is added temporarily then removed, the detection logic may not properly identify all affected lines that need clearing. + +**4. Missing Test Coverage** + +Current tests in `/Users/badlogic/workspaces/pi-mono/packages/tui/test/` don't cover the specific scenario of: +- Dynamic addition of components that cause layout shifts +- Temporary status messages that appear and disappear +- Components moving back to original positions after removals + +### The Agent Scenario Analysis + +The agent likely does this sequence: +1. Has header, chat container, text editor in vertical layout +2. Adds a status message component between chat and editor +3. Editor shifts down (differential render uses PARTIAL strategy) +4. After delay, removes status message +5. Editor shifts back up (this is where artifacts remain) + +The issue is that when the editor moves back up, the SURGICAL strategy is chosen (same component structure, just content changes), but it doesn't properly clear the old border lines that were drawn when the editor was in the lower position. + +### Root Cause + +The differential rendering assumes that when using SURGICAL updates, only content within existing component boundaries changes. However, when components shift positions due to additions/removals, old rendered content at previous positions isn't being cleared properly. + +**Specific gap:** The SURGICAL strategy clears individual lines with `\x1b[2K` but doesn't account for situations where component positions have changed, leaving artifacts from the previous render at the old positions. + +### Test Creation Recommendation + +A test reproducing this would: + +```typescript +test("clears artifacts when components shift positions dynamically", async () => { + // 1. Setup: header, container, editor + // 2. Add status message (causes editor to shift down) + // 3. Remove status message (editor shifts back up) + // 4. Verify no border artifacts remain at old editor position +}); +``` + +The test should specifically check that after the removal, there are no stray border characters (`╭`, `╮`, `│`, `╰`, `╯`) left at the position where the editor was temporarily located. + +### Proposed Fix Direction + +The PARTIAL strategy should be used more aggressively when components are added/removed, even if the final structure looks identical, to ensure complete clearing of old content. Alternatively, the SURGICAL strategy needs enhanced logic to detect and clear content at previous component positions. \ No newline at end of file diff --git a/todos/done/20250110-193139-agent-tui-ctrl-c-display-artifact.md b/todos/done/20250110-193139-agent-tui-ctrl-c-display-artifact.md new file mode 100644 index 00000000..b90b0730 --- /dev/null +++ b/todos/done/20250110-193139-agent-tui-ctrl-c-display-artifact.md @@ -0,0 +1,36 @@ +# Agent/TUI: Ctrl+C Display Artifact +**Status:** Done +**Agent PID:** 36116 + +## Original Todo +agent/tui: when pressing ctrl + c, the editor gets pushed down by one line, after a second it gets pushed up again, leaving an artifact (duplicate bottom border). Should replicate this in a test: + Press Ctrl+C again to exit + ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ > │ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + ↑967 ↓12 ⚒ 4 + +## Description +Create a test in the TUI package that reproduces the rendering artifact issue when components dynamically shift positions (like when a status message appears and disappears). The test will verify that when components move back to their original positions after a temporary layout change, no visual artifacts (duplicate borders) remain. If the test reveals a bug in the TUI's differential rendering, fix it. + +*Read [analysis.md](./analysis.md) in full for detailed codebase research and context* + +## Implementation Plan +Create a test that reproduces the layout shift artifact issue in the TUI differential rendering, then fix the rendering logic if needed to properly clear old content when components shift positions. + +- [x] Create test file `packages/tui/test/layout-shift-artifacts.test.ts` that reproduces the issue +- [x] Test should create components in vertical layout, add a temporary component causing shifts, remove it, and verify no artifacts +- [x] Run test to confirm it reproduces the artifact issue +- [x] Fix the differential rendering logic in `packages/tui/src/tui.ts` to properly clear content when components shift +- [x] Verify all tests pass (including the new one) after fix +- [x] Run `npm run check` to ensure code quality + +## Notes +The issue was NOT in the differential rendering strategy as initially thought. The real bug was in the Container component: + +When a Container is cleared (has 0 children), it wasn't reporting as "changed" because the render method only checked if any children reported changes. Since there were no children after clearing, `changed` remained false, and the differential renderer didn't know to re-render that area. + +The fix: Container now tracks `previousChildCount` and reports as changed when the number of children changes (especially important for going from N children to 0). + +This ensures that when statusContainer.clear() is called in the agent, the differential renderer properly clears and re-renders that section of the screen. \ No newline at end of file diff --git a/todos/todos.md b/todos/todos.md index 3266ea8d..10d7e2a7 100644 --- a/todos/todos.md +++ b/todos/todos.md @@ -1,7 +1,3 @@ -- agent/tui: broken rendering of resumed session messages - - start session, "read all README.md files except in node_modules - - stop session - - resume session, messages are cut off? - pods: pi start outputs all models that can be run on the pod. however, it doesn't check the vllm version. e.g. gpt-oss can only run via vllm+gpt-oss. glm4.5 can only run on vllm nightly. @@ -41,8 +37,6 @@ - agent: test for basic functionality, including thinking, completions & responses API support for all the known providers and their endpoints. -- agent: token usage gets overwritten with each message that has usage data. however, if the latest data doesn't have a specific usage field, we record undefined i think? also, {"type":"token_usage" "inputTokens":240,"outputTokens":35,"totalTokens":275,"cacheReadTokens":0,"cacheWriteTokens":0} doesn't contain reasoningToken? do we lack initialization? See case "token_usage": in renderers. probably need to check if lastXXX > current and use lastXXX. - - agent: groq responses api throws on second message ``` ➜ pi-mono git:(main) ✗ npx tsx packages/agent/src/cli.ts --base-url https://api.groq.com/openai/v1 --api-key $GROQ_API_KEY --model openai/gpt-oss-120b --api responses diff --git a/tsconfig.json b/tsconfig.json index a0d4bd4e..311ac672 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { + "noEmit": true, "paths": { "@mariozechner/pi-tui": ["./packages/tui/src/index.ts"], "@mariozechner/pi-agent": ["./packages/agent/src/index.ts"],