diff --git a/config.yaml b/config.yaml index acf31ffb..f0888810 100644 --- a/config.yaml +++ b/config.yaml @@ -86,6 +86,7 @@ schedules: tui: name: madz cursorChar: "█" + renderWindow: 100 agent: recursionLimit: 1000 autoContinueLimit: 1000 diff --git a/coverage.txt b/coverage.txt index 11c4dede..180032cc 100644 --- a/coverage.txt +++ b/coverage.txt @@ -16,10 +16,13 @@ ℹ research.js | 100.00 | 100.00 | 100.00 | ℹ search.js | 100.00 | 100.00 | 100.00 | ℹ security-audit.js | 100.00 | 100.00 | 100.00 | +ℹ seo-analyst.js | 100.00 | 100.00 | 100.00 | ℹ testing.js | 100.00 | 100.00 | 100.00 | +ℹ text-editor.js | 100.00 | 100.00 | 100.00 | +ℹ translator.js | 100.00 | 100.00 | 100.00 | ℹ config | | | | ℹ config.js | 100.00 | 100.00 | 100.00 | -ℹ loader.js | 92.63 | 90.48 | 81.82 | 94-96 121 123 173-177 187-190 +ℹ loader.js | 92.82 | 90.48 | 81.82 | 98-100 125 127 178-182 192-195 ℹ patch.js | 54.72 | 100.00 | 0.00 | 11-15 25-37 48-53 ℹ schemas | | | | ℹ agent.js | 100.00 | 100.00 | 100.00 | @@ -30,6 +33,7 @@ ℹ sandbox.js | 100.00 | 100.00 | 100.00 | ℹ schedules.js | 100.00 | 100.00 | 100.00 | ℹ skillAgentMap.js | 100.00 | 100.00 | 100.00 | +ℹ subAgentsTemperature.js | 100.00 | 100.00 | 100.00 | ℹ telemetry.js | 100.00 | 100.00 | 100.00 | ℹ tui.js | 100.00 | 100.00 | 100.00 | ℹ memory | | | | @@ -63,8 +67,8 @@ ℹ shared | | | | ℹ logger.js | 76.61 | 41.67 | 81.82 | 26-34 39 41-43 64-65 73-77 100-106 112-116 131 163-164 166-167 184-185 191-192 198-199 205-206 209-213 216 ℹ skills | | | | -ℹ agentMapper.js | 73.33 | 50.00 | 100.00 | 12-13 23-25 27-29 -ℹ discoverer.js | 94.69 | 85.94 | 100.00 | 63-68 192-193 197-198 233-235 +ℹ agentMapper.js | 83.33 | 62.50 | 100.00 | 12-13 23-25 +ℹ discoverer.js | 94.58 | 85.71 | 100.00 | 63-68 187-188 192-193 228-230 ℹ registry.js | 65.87 | 43.48 | 42.11 | 38-78 106-107 128-129 146-147 155-164 175-177 180-182 208-214 222-226 234-238 245-246 260-267 276-283 291-292 ℹ types.js | 100.00 | 100.00 | 100.00 | ℹ validator.js | 83.21 | 70.59 | 80.00 | 19-20 27-28 68 70 72-73 78 82-84 105-107 119-121 130-134 @@ -106,13 +110,14 @@ ℹ index.js | 100.00 | 94.12 | 100.00 | ℹ memory.js | 96.52 | 83.56 | 93.33 | 55 98-99 194-198 298-300 ℹ moa.js | 100.00 | 94.44 | 84.62 | +ℹ namecom | | | | +ℹ index.js | 70.84 | 100.00 | 0.00 | 21-29 36-44 53-112 129-151 199-202 223-226 229-231 240-242 245-248 251-253 267-271 306-310 425-448 ℹ pdfGenerate.js | 90.48 | 80.00 | 82.61 | 39-40 59-60 66-70 74-75 77 97 113-114 124-125 141-156 164-166 168-169 171 179-188 224-228 249 331-332 366-367 450-451 469-471 473-475 477-479 481-483 504-505 582-583 614-615 660-661 787-790 -ℹ process.js | 89.47 | 76.67 | 80.00 | 38-41 93-94 100-102 108-109 116-117 124-125 127 +ℹ process.js | 87.07 | 76.47 | 80.00 | 41-44 55-58 95 107 137-138 153-154 214-215 226-227 233-235 241-242 244-250 252-258 260 ℹ reflection.js | 95.18 | 82.00 | 91.67 | 58-62 127-128 151-152 206-207 ℹ sampling.js | 94.97 | 81.82 | 80.00 | 27 180-188 ℹ scanAgents.js | 100.00 | 80.00 | 100.00 | ℹ session_search.js | 97.06 | 71.19 | 94.12 | 71-72 118-119 128 181-182 -ℹ shell.js | 97.22 | 77.27 | 90.00 | 40 69-70 ℹ skills.js | 88.47 | 85.48 | 100.00 | 68-69 96-97 124-132 143-150 170-177 193-195 210-211 ℹ spreadsheet | | | | ℹ csv.js | 37.06 | 100.00 | 0.00 | 24-68 84-120 129-131 140-145 155-170 @@ -131,13 +136,13 @@ ℹ inputPanel.js | 100.00 | 100.00 | 100.00 | ℹ markdownText.js | 72.95 | 78.82 | 83.02 | 16-18 40-118 158 182-184 262-263 274-275 304-310 325-333 336-338 348-354 369-390 401-402 453-454 457-458 464 ℹ messageBubble.js | 85.30 | 53.13 | 71.43 | 139-144 163-166 181-190 195-202 207-214 255-259 -ℹ messageList.js | 79.81 | 72.73 | 50.00 | 68 85-88 113-136 146-169 178 185-190 233 241 249 258-264 273 281-286 308-310 344-345 388 +ℹ messageList.js | 79.76 | 72.73 | 50.00 | 64 81-84 109-133 143-166 175 182-187 230 238 246 255-261 270 278-283 305-307 348-349 392 ℹ messages.js | 100.00 | 94.44 | 100.00 | ℹ panels.js | 100.00 | 100.00 | 100.00 | ℹ statusBar.js | 90.82 | 81.25 | 100.00 | 22-23 34-40 ℹ workspace | | | | ℹ loadAgents.js | 100.00 | 87.50 | 100.00 | ℹ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -ℹ all files | 66.52 | 83.08 | 56.62 | +ℹ all files | 66.68 | 83.26 | 51.57 | ℹ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ℹ end of coverage report diff --git a/src/config/schemas/tui.js b/src/config/schemas/tui.js index 75dd7e99..276ad080 100644 --- a/src/config/schemas/tui.js +++ b/src/config/schemas/tui.js @@ -3,4 +3,5 @@ import { z } from "zod"; export const TuiSchema = z.object({ name: z.string().default("madz"), cursorChar: z.string().default("\u2588"), + renderWindow: z.number().int().min(1).default(100), }); diff --git a/src/tui/app.js b/src/tui/app.js index fe69d8cc..5fd1f0f7 100644 --- a/src/tui/app.js +++ b/src/tui/app.js @@ -950,6 +950,7 @@ export default function App({ }, React.createElement(ConversationPanel, { assistantName: config?.tui?.name || "Assistant", + renderWindow: config?.tui?.renderWindow ?? 100, messageListRef, }), ), diff --git a/src/tui/conversationPanel.js b/src/tui/conversationPanel.js index 6e097dae..509cdfdf 100644 --- a/src/tui/conversationPanel.js +++ b/src/tui/conversationPanel.js @@ -67,6 +67,7 @@ export function getBubbleStyle(role) { * @param {Object} props * @param {Array} [props.messages] - Messages to display (for session restore) * @param {string} [props.assistantName] - Name for assistant messages + * @param {number} [props.renderWindow] - Number of messages to render (from config) * @param {React.Ref} [props.scrollRef] - Optional external scroll ref * @param {React.Ref} [props.messageListRef] - Optional ref for imperative access * @returns {React.ReactElement} @@ -74,6 +75,7 @@ export function getBubbleStyle(role) { export function ConversationPanel({ messages = [], assistantName = "Assistant", + renderWindow, scrollRef: externalScrollRef, messageListRef, }) { @@ -93,6 +95,7 @@ export function ConversationPanel({ React.createElement(MessageList, { ref: panelRef, assistantName, + renderWindow, scrollRef: externalScrollRef, }), ); diff --git a/src/tui/messageList.js b/src/tui/messageList.js index f9844c0f..b8a4e7bb 100644 --- a/src/tui/messageList.js +++ b/src/tui/messageList.js @@ -22,11 +22,6 @@ export function PubSubProvider({ subscribe, unsubscribe, publish, children }) { ); } -/** - * Maximum number of messages to render in the React tree. - */ -const MAX_RENDER_MESSAGES = 100; - // Monotonic counter for generating stable message IDs. let _messageIdCounter = 0; @@ -40,12 +35,18 @@ let _messageIdCounter = 0; * @param {Object} props * @param {Array} [props.messages] - Initial messages array for session restore * @param {string} [props.assistantName] - Name to display for assistant messages + * @param {number} [props.renderWindow] - Number of messages to render (from config) * @param {React.Ref} [props.forwardRef] - For exposed imperative API * @param {React.Ref} [props.scrollRef] - Forwarded scroll ref for external keyboard nav * @returns {React.ReactElement} */ export const MessageList = forwardRef(function MessageList( - { messages: _messages = [], assistantName = "Assistant", scrollRef: externalScrollRef }, + { + messages: _messages = [], + assistantName = "Assistant", + renderWindow, + scrollRef: externalScrollRef, + }, forwardRef, ) { const internalRef = useRef(null); @@ -132,6 +133,7 @@ export const MessageList = forwardRef(function MessageList( idsRef.current.push(id); idToIdxRef.current.set(id, idsRef.current.length - 1); + triggerRender(); return id; }, @@ -329,17 +331,24 @@ export const MessageList = forwardRef(function MessageList( lastMsgCountRef.current = idsRef.current.length; }; - // Render the last MAX_RENDER_MESSAGES as MessageBubble elements. + // Render the last N messages as MessageBubble elements. // Each bubble subscribes to its own pub/sub topic for streaming updates. // Children array is stabilized in a ref — only rebuilt when message count // changes (new message added, pruned, or cleared). This lets Ink's diffing // reuse existing elements and only update the one bubble that changed. + // + // The data layer stores all messages without a cap. The ScrollView handles + // scrolling through the full conversation history. The render window keeps + // the React tree bounded; pub/sub topics for messages far from the current + // view are pruned to keep memory bounded. const childrenRef = useRef(null); - const renderData = idsRef.current.slice(-MAX_RENDER_MESSAGES); - - // Prune pub/sub topics for messages that fell off the render slice. - const prunedIds = idsRef.current.slice(0, -MAX_RENDER_MESSAGES); + // Virtual render window — keeps the React tree bounded while the data + // layer stores all messages. The ScrollView scrolls through the full + // conversation history; only the last N messages are rendered as bubbles. + // Configurable via `tui.renderWindow` in config.yaml. + const renderData = idsRef.current.slice(-renderWindow); + const prunedIds = idsRef.current.slice(0, idsRef.current.length - renderWindow); for (const id of prunedIds) { topicsRef.current.delete(`msg-${id}`); } diff --git a/tests/unit/messageListApi.test.js b/tests/unit/messageListApi.test.js index adf718da..56112969 100644 --- a/tests/unit/messageListApi.test.js +++ b/tests/unit/messageListApi.test.js @@ -6,6 +6,10 @@ import assert from "node:assert"; * Simulates the imperative API used by MessageList without React. * Tests the addMessage, updateMessage, clear, setMessages workflow * including pub/sub topic management. + * + * Note: The data layer stores all messages without a cap. + * The render layer uses a virtual window (last 100 messages) for + * performance, but the underlying data structures grow unbounded. */ describe("messageList imperative API simulation", () => { let pubsub; diff --git a/tests/unit/tui.test.js b/tests/unit/tui.test.js index efba3d50..ac517f69 100644 --- a/tests/unit/tui.test.js +++ b/tests/unit/tui.test.js @@ -1165,7 +1165,7 @@ describe("TUI - scroll throttle behavior", () => { }); describe("MessageList - render window limits React tree size", () => { - it("uses MAX_RENDER_MESSAGES window from messageList", async () => { + it("mounts and unmounts without error", async () => { const { MessageList } = await import("../../src/tui/messageList.js"); const { unmount: um } = render( React.createElement(MessageList, {