Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ schedules:
tui:
name: madz
cursorChar: "█"
renderWindow: 100
agent:
recursionLimit: 1000
autoContinueLimit: 1000
Expand Down
19 changes: 12 additions & 7 deletions coverage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 | | | |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions src/config/schemas/tui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
1 change: 1 addition & 0 deletions src/tui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,7 @@ export default function App({
},
React.createElement(ConversationPanel, {
assistantName: config?.tui?.name || "Assistant",
renderWindow: config?.tui?.renderWindow ?? 100,
messageListRef,
}),
),
Expand Down
3 changes: 3 additions & 0 deletions src/tui/conversationPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ 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}
*/
export function ConversationPanel({
messages = [],
assistantName = "Assistant",
renderWindow,
scrollRef: externalScrollRef,
messageListRef,
}) {
Expand All @@ -93,6 +95,7 @@ export function ConversationPanel({
React.createElement(MessageList, {
ref: panelRef,
assistantName,
renderWindow,
scrollRef: externalScrollRef,
}),
);
Expand Down
31 changes: 20 additions & 11 deletions src/tui/messageList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -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}`);
}
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/messageListApi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/tui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down