@jungjaehoon/mama-os 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +13 -16
  2. package/dist/gateways/discord.d.ts +4 -0
  3. package/dist/gateways/discord.d.ts.map +1 -1
  4. package/dist/gateways/discord.js +40 -2
  5. package/dist/gateways/discord.js.map +1 -1
  6. package/dist/gateways/image-analyzer.d.ts.map +1 -1
  7. package/dist/gateways/image-analyzer.js +10 -1
  8. package/dist/gateways/image-analyzer.js.map +1 -1
  9. package/dist/gateways/slack.d.ts.map +1 -1
  10. package/dist/gateways/slack.js +3 -0
  11. package/dist/gateways/slack.js.map +1 -1
  12. package/dist/multi-agent/agent-process-manager.d.ts +10 -0
  13. package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
  14. package/dist/multi-agent/agent-process-manager.js +36 -2
  15. package/dist/multi-agent/agent-process-manager.js.map +1 -1
  16. package/dist/multi-agent/multi-agent-base.d.ts +19 -0
  17. package/dist/multi-agent/multi-agent-base.d.ts.map +1 -1
  18. package/dist/multi-agent/multi-agent-base.js +96 -0
  19. package/dist/multi-agent/multi-agent-base.js.map +1 -1
  20. package/dist/multi-agent/multi-agent-discord.d.ts.map +1 -1
  21. package/dist/multi-agent/multi-agent-discord.js +36 -0
  22. package/dist/multi-agent/multi-agent-discord.js.map +1 -1
  23. package/dist/multi-agent/multi-agent-slack.d.ts.map +1 -1
  24. package/dist/multi-agent/multi-agent-slack.js +38 -2
  25. package/dist/multi-agent/multi-agent-slack.js.map +1 -1
  26. package/dist/multi-agent/types.d.ts +8 -2
  27. package/dist/multi-agent/types.d.ts.map +1 -1
  28. package/dist/multi-agent/types.js.map +1 -1
  29. package/dist/multi-agent/workflow-engine.d.ts +80 -0
  30. package/dist/multi-agent/workflow-engine.d.ts.map +1 -0
  31. package/dist/multi-agent/workflow-engine.js +395 -0
  32. package/dist/multi-agent/workflow-engine.js.map +1 -0
  33. package/dist/multi-agent/workflow-types.d.ts +111 -0
  34. package/dist/multi-agent/workflow-types.d.ts.map +1 -0
  35. package/dist/multi-agent/workflow-types.js +9 -0
  36. package/dist/multi-agent/workflow-types.js.map +1 -0
  37. package/package.json +1 -1
  38. package/public/viewer/js/modules/chat.js +34 -15
  39. package/public/viewer/js/modules/dashboard.js +41 -46
  40. package/public/viewer/js/utils/dom.js +15 -15
  41. package/public/viewer/src/modules/chat.ts +36 -15
  42. package/public/viewer/src/modules/dashboard.ts +43 -52
  43. package/public/viewer/src/utils/dom.ts +16 -16
  44. package/public/viewer/viewer.css +45 -2
  45. package/public/viewer/viewer.html +16 -7
@@ -43,6 +43,7 @@ export class ChatModule {
43
43
  history = [];
44
44
  historyPrefix = 'mama_chat_history_';
45
45
  maxHistoryMessages = 50;
46
+ maxDomMessages = 100; // Limit DOM elements for performance
46
47
  historyExpiryMs = 24 * 60 * 60 * 1000;
47
48
  checkpointCooldown = false;
48
49
  COOLDOWN_MS = 60 * 1000;
@@ -80,7 +81,10 @@ export class ChatModule {
80
81
  }
81
82
  }
82
83
  async autoCheckpoint() {
83
- // Auto-checkpoint disabled - use /checkpoint command for manual saves
84
+ // DISABLED: Auto-checkpoint was saving raw conversation history to MAMA memory.
85
+ // Checkpoints should only be saved manually via /checkpoint command with proper summaries.
86
+ // The viewer chat uses localStorage for session persistence instead.
87
+ logger.info('Auto-checkpoint disabled (use /checkpoint for manual saves)');
84
88
  return;
85
89
  }
86
90
  // =============================================
@@ -1358,7 +1362,7 @@ export class ChatModule {
1358
1362
  }
1359
1363
  }
1360
1364
  /**
1361
- * Restore chat history
1365
+ * Restore chat history (optimized with DocumentFragment)
1362
1366
  */
1363
1367
  restoreHistory(sessionId) {
1364
1368
  const history = this.loadHistory(sessionId);
@@ -1371,7 +1375,11 @@ export class ChatModule {
1371
1375
  return false;
1372
1376
  }
1373
1377
  this.removePlaceholder();
1374
- history.forEach((msg) => {
1378
+ // Use DocumentFragment for batch DOM insertion
1379
+ const fragment = document.createDocumentFragment();
1380
+ // Limit to last N messages for DOM performance
1381
+ const messagesToRender = history.slice(-this.maxDomMessages);
1382
+ messagesToRender.forEach((msg) => {
1375
1383
  const msgEl = document.createElement('div');
1376
1384
  msgEl.className = `chat-message ${msg.role}`;
1377
1385
  if (msg.role === 'user') {
@@ -1404,14 +1412,15 @@ export class ChatModule {
1404
1412
  <div class="message-content">${escapeHtml(msg.content)}</div>
1405
1413
  `;
1406
1414
  }
1407
- container.appendChild(msgEl);
1415
+ fragment.appendChild(msgEl);
1408
1416
  });
1417
+ container.appendChild(fragment);
1409
1418
  scrollToBottom(container);
1410
1419
  showToast('Previous conversation restored');
1411
1420
  return true;
1412
1421
  }
1413
1422
  /**
1414
- * Display history received from server
1423
+ * Display history received from server (optimized with DocumentFragment)
1415
1424
  */
1416
1425
  displayHistory(messages) {
1417
1426
  const container = getElementByIdOrNull('chat-messages');
@@ -1424,8 +1433,12 @@ export class ChatModule {
1424
1433
  return;
1425
1434
  }
1426
1435
  container.innerHTML = '';
1427
- this.history = [];
1428
- messages.forEach((msg) => {
1436
+ this.history = messages;
1437
+ // Use DocumentFragment for batch DOM insertion
1438
+ const fragment = document.createDocumentFragment();
1439
+ // Limit to last N messages for DOM performance
1440
+ const messagesToRender = messages.slice(-this.maxDomMessages);
1441
+ messagesToRender.forEach((msg) => {
1429
1442
  const msgEl = document.createElement('div');
1430
1443
  msgEl.className = `chat-message ${msg.role}`;
1431
1444
  const timestamp = msg.timestamp ? new Date(msg.timestamp) : new Date();
@@ -1446,10 +1459,11 @@ export class ChatModule {
1446
1459
  <div class="message-content">${escapeHtml(msg.content)}</div>
1447
1460
  `;
1448
1461
  }
1449
- container.appendChild(msgEl);
1462
+ fragment.appendChild(msgEl);
1450
1463
  });
1464
+ container.appendChild(fragment);
1451
1465
  scrollToBottom(container);
1452
- logger.info('Displayed', messages.length, 'history messages');
1466
+ logger.info('Displayed', messagesToRender.length, 'history messages');
1453
1467
  }
1454
1468
  /**
1455
1469
  * Clear chat history
@@ -1734,10 +1748,15 @@ export class ChatModule {
1734
1748
  if (!panel) {
1735
1749
  return;
1736
1750
  }
1737
- const shouldOpen = forceState !== undefined ? forceState : panel.classList.contains('hidden');
1751
+ const isClosed = panel.classList.contains('chat-panel-closed');
1752
+ const shouldOpen = forceState !== undefined ? forceState : isClosed;
1738
1753
  if (shouldOpen) {
1739
- panel.classList.remove('hidden');
1740
- panel.classList.add('animate-slide-up');
1754
+ // Lazy session init on first open
1755
+ if (!this.ws) {
1756
+ this.initSession();
1757
+ }
1758
+ panel.classList.remove('chat-panel-closed');
1759
+ panel.classList.add('chat-panel-open', 'animate-slide-up');
1741
1760
  this.restorePanelState(panel);
1742
1761
  if (bubble) {
1743
1762
  bubble.classList.add('scale-0');
@@ -1755,8 +1774,8 @@ export class ChatModule {
1755
1774
  }
1756
1775
  }
1757
1776
  else {
1758
- panel.classList.add('hidden');
1759
- panel.classList.remove('animate-slide-up');
1777
+ panel.classList.add('chat-panel-closed');
1778
+ panel.classList.remove('chat-panel-open', 'animate-slide-up');
1760
1779
  if (bubble) {
1761
1780
  bubble.classList.remove('scale-0');
1762
1781
  }
@@ -1811,7 +1830,7 @@ export class ChatModule {
1811
1830
  */
1812
1831
  isFloatingOpen() {
1813
1832
  const panel = getElementByIdOrNull('chat-panel');
1814
- return Boolean(panel && !panel.classList.contains('hidden'));
1833
+ return Boolean(panel && panel.classList.contains('chat-panel-open'));
1815
1834
  }
1816
1835
  /**
1817
1836
  * Show unread badge on bubble when panel is closed
@@ -72,68 +72,48 @@ export class DashboardModule {
72
72
  }
73
73
  /**
74
74
  * Load dashboard status from API
75
- * Uses Promise.allSettled for parallel loading to improve performance
76
75
  */
77
76
  async loadStatus() {
78
77
  try {
79
- // Load all data in parallel for better performance
80
- const [dashboardResult, multiAgentResult, delegationsResult, cronResult, tokenResult, mcpResult,] = await Promise.allSettled([
81
- API.get('/api/dashboard/status'),
82
- API.get('/api/multi-agent/status'),
83
- API.get('/api/multi-agent/delegations?limit=10'),
84
- API.getCronJobs(),
85
- Promise.all([API.getTokenSummary(), API.getTokensByAgent()]),
86
- API.get('/api/mcp-servers'),
87
- ]);
88
- // Process dashboard status (required)
89
- if (dashboardResult.status === 'fulfilled') {
90
- this.data = dashboardResult.value;
78
+ this.data = await API.get('/api/dashboard/status');
79
+ // Load multi-agent status (Sprint 3 F2)
80
+ try {
81
+ this.multiAgentData = await API.get('/api/multi-agent/status');
91
82
  }
92
- else {
93
- throw dashboardResult.reason;
94
- }
95
- // Process multi-agent status
96
- if (multiAgentResult.status === 'fulfilled') {
97
- this.multiAgentData = multiAgentResult.value;
98
- }
99
- else {
100
- logger.warn('[Dashboard] Multi-agent status unavailable:', multiAgentResult.reason);
83
+ catch (e) {
84
+ logger.warn('[Dashboard] Multi-agent status unavailable:', e);
101
85
  this.multiAgentData = { enabled: false, agents: [] };
102
86
  }
103
- // Process delegations
104
- if (delegationsResult.status === 'fulfilled') {
105
- this.delegationsData = delegationsResult.value;
87
+ // Load delegations (F4 endpoint)
88
+ try {
89
+ this.delegationsData = await API.get('/api/multi-agent/delegations?limit=10');
106
90
  }
107
- else {
108
- logger.warn('[Dashboard] Delegations unavailable:', delegationsResult.reason);
91
+ catch (e) {
92
+ logger.warn('[Dashboard] Delegations unavailable:', e);
109
93
  this.delegationsData = { delegations: [], count: 0 };
110
94
  }
111
- // Process cron jobs
112
- if (cronResult.status === 'fulfilled') {
113
- this.cronData = cronResult.value;
95
+ // Load cron jobs
96
+ try {
97
+ this.cronData = await API.getCronJobs();
114
98
  }
115
- else {
116
- logger.warn('[Dashboard] Cron data unavailable:', cronResult.reason);
99
+ catch (e) {
100
+ logger.warn('[Dashboard] Cron data unavailable:', e);
117
101
  this.cronData = null;
118
102
  }
119
- // Process token data
120
- if (tokenResult.status === 'fulfilled') {
121
- const [summary, byAgent] = tokenResult.value;
103
+ // Load token summary
104
+ try {
105
+ const [summary, byAgent] = await Promise.all([
106
+ API.getTokenSummary(),
107
+ API.getTokensByAgent(),
108
+ ]);
122
109
  this.tokenData = { summary, byAgent };
123
110
  }
124
- else {
125
- logger.warn('[Dashboard] Token data unavailable:', tokenResult.reason);
111
+ catch (e) {
112
+ logger.warn('[Dashboard] Token data unavailable:', e);
126
113
  this.tokenData = null;
127
114
  }
128
- // Process MCP servers
129
- if (mcpResult.status === 'fulfilled' && mcpResult.value && 'servers' in mcpResult.value) {
130
- this.mcpServers = mcpResult.value.servers || [];
131
- this.renderMCPServers();
132
- }
133
- else {
134
- logger.warn('[Dashboard] MCP servers unavailable');
135
- this.mcpServers = [];
136
- }
115
+ // Load MCP servers
116
+ await this.loadMCPServers();
137
117
  this.render();
138
118
  this.setStatus(`Last updated: ${new Date().toLocaleTimeString()}`);
139
119
  }
@@ -142,6 +122,21 @@ export class DashboardModule {
142
122
  this.setStatus(`Error: ${getErrorMessage(error)}`, 'error');
143
123
  }
144
124
  }
125
+ /**
126
+ * Load MCP servers from API
127
+ */
128
+ async loadMCPServers() {
129
+ try {
130
+ const data = await API.get('/api/mcp-servers');
131
+ if (data && 'servers' in data) {
132
+ this.mcpServers = data.servers || [];
133
+ this.renderMCPServers();
134
+ }
135
+ }
136
+ catch (error) {
137
+ logger.error('Failed to load MCP servers:', error);
138
+ }
139
+ }
145
140
  /**
146
141
  * Render all dashboard sections
147
142
  */
@@ -97,15 +97,11 @@ export function showToast(message, duration = 3000) {
97
97
  * @param {HTMLElement} container - Container to scroll
98
98
  */
99
99
  export function scrollToBottom(container) {
100
- // Use setTimeout to ensure DOM has updated before scrolling
101
- const doScroll = () => {
102
- container.scrollTop = container.scrollHeight;
103
- if (container.scrollTo) {
104
- container.scrollTo({ top: container.scrollHeight, behavior: 'auto' });
105
- }
106
- };
107
- setTimeout(doScroll, 50);
108
- requestAnimationFrame(doScroll);
100
+ // Use requestAnimationFrame to batch layout read/write and avoid forced reflow
101
+ requestAnimationFrame(() => {
102
+ const scrollHeight = container.scrollHeight; // Single read
103
+ container.scrollTop = scrollHeight; // Single write
104
+ });
109
105
  }
110
106
  /**
111
107
  * Auto-resize textarea to fit content
@@ -113,10 +109,14 @@ export function scrollToBottom(container) {
113
109
  * @param {number} maxRows - Maximum number of rows (default: 5)
114
110
  */
115
111
  export function autoResizeTextarea(textarea, maxRows = 5) {
116
- textarea.style.height = 'auto';
117
- const computedLineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight);
118
- const lineHeight = Number.isFinite(computedLineHeight) && computedLineHeight > 0 ? computedLineHeight : 20;
119
- const maxHeight = lineHeight * maxRows;
120
- const newHeight = Math.min(textarea.scrollHeight, maxHeight);
121
- textarea.style.height = newHeight + 'px';
112
+ // Use requestAnimationFrame to defer resize to the next frame, avoiding layout thrash from rapid input events
113
+ requestAnimationFrame(() => {
114
+ textarea.style.height = 'auto'; // Reset height before measuring
115
+ const computedLineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight); // Forces reflow (unavoidable)
116
+ const scrollHeight = textarea.scrollHeight;
117
+ const lineHeight = Number.isFinite(computedLineHeight) && computedLineHeight > 0 ? computedLineHeight : 20;
118
+ const maxHeight = lineHeight * maxRows;
119
+ const newHeight = Math.min(scrollHeight, maxHeight);
120
+ textarea.style.height = newHeight + 'px';
121
+ });
122
122
  }
@@ -98,6 +98,7 @@ export class ChatModule {
98
98
  history: ChatHistoryMessage[] = [];
99
99
  historyPrefix = 'mama_chat_history_';
100
100
  maxHistoryMessages = 50;
101
+ maxDomMessages = 100; // Limit DOM elements for performance
101
102
  historyExpiryMs = 24 * 60 * 60 * 1000;
102
103
  checkpointCooldown = false;
103
104
  COOLDOWN_MS = 60 * 1000;
@@ -147,7 +148,10 @@ export class ChatModule {
147
148
  }
148
149
 
149
150
  async autoCheckpoint(): Promise<void> {
150
- // Auto-checkpoint disabled - use /checkpoint command for manual saves
151
+ // DISABLED: Auto-checkpoint was saving raw conversation history to MAMA memory.
152
+ // Checkpoints should only be saved manually via /checkpoint command with proper summaries.
153
+ // The viewer chat uses localStorage for session persistence instead.
154
+ logger.info('Auto-checkpoint disabled (use /checkpoint for manual saves)');
151
155
  return;
152
156
  }
153
157
 
@@ -1657,7 +1661,7 @@ export class ChatModule {
1657
1661
  }
1658
1662
 
1659
1663
  /**
1660
- * Restore chat history
1664
+ * Restore chat history (optimized with DocumentFragment)
1661
1665
  */
1662
1666
  restoreHistory(sessionId: string): boolean {
1663
1667
  const history = this.loadHistory(sessionId);
@@ -1674,7 +1678,12 @@ export class ChatModule {
1674
1678
 
1675
1679
  this.removePlaceholder();
1676
1680
 
1677
- history.forEach((msg) => {
1681
+ // Use DocumentFragment for batch DOM insertion
1682
+ const fragment = document.createDocumentFragment();
1683
+ // Limit to last N messages for DOM performance
1684
+ const messagesToRender = history.slice(-this.maxDomMessages);
1685
+
1686
+ messagesToRender.forEach((msg) => {
1678
1687
  const msgEl = document.createElement('div');
1679
1688
  msgEl.className = `chat-message ${msg.role}`;
1680
1689
 
@@ -1706,9 +1715,10 @@ export class ChatModule {
1706
1715
  `;
1707
1716
  }
1708
1717
 
1709
- container.appendChild(msgEl);
1718
+ fragment.appendChild(msgEl);
1710
1719
  });
1711
1720
 
1721
+ container.appendChild(fragment);
1712
1722
  scrollToBottom(container);
1713
1723
  showToast('Previous conversation restored');
1714
1724
 
@@ -1716,7 +1726,7 @@ export class ChatModule {
1716
1726
  }
1717
1727
 
1718
1728
  /**
1719
- * Display history received from server
1729
+ * Display history received from server (optimized with DocumentFragment)
1720
1730
  */
1721
1731
  displayHistory(messages: ChatHistoryMessage[]): void {
1722
1732
  const container = getElementByIdOrNull<HTMLDivElement>('chat-messages');
@@ -1731,9 +1741,14 @@ export class ChatModule {
1731
1741
  }
1732
1742
 
1733
1743
  container.innerHTML = '';
1734
- this.history = [];
1744
+ this.history = messages;
1745
+
1746
+ // Use DocumentFragment for batch DOM insertion
1747
+ const fragment = document.createDocumentFragment();
1748
+ // Limit to last N messages for DOM performance
1749
+ const messagesToRender = messages.slice(-this.maxDomMessages);
1735
1750
 
1736
- messages.forEach((msg) => {
1751
+ messagesToRender.forEach((msg) => {
1737
1752
  const msgEl = document.createElement('div');
1738
1753
  msgEl.className = `chat-message ${msg.role}`;
1739
1754
 
@@ -1755,11 +1770,12 @@ export class ChatModule {
1755
1770
  `;
1756
1771
  }
1757
1772
 
1758
- container.appendChild(msgEl);
1773
+ fragment.appendChild(msgEl);
1759
1774
  });
1760
1775
 
1776
+ container.appendChild(fragment);
1761
1777
  scrollToBottom(container);
1762
- logger.info('Displayed', messages.length, 'history messages');
1778
+ logger.info('Displayed', messagesToRender.length, 'history messages');
1763
1779
  }
1764
1780
 
1765
1781
  /**
@@ -2079,11 +2095,16 @@ export class ChatModule {
2079
2095
  return;
2080
2096
  }
2081
2097
 
2082
- const shouldOpen = forceState !== undefined ? forceState : panel.classList.contains('hidden');
2098
+ const isClosed = panel.classList.contains('chat-panel-closed');
2099
+ const shouldOpen = forceState !== undefined ? forceState : isClosed;
2083
2100
 
2084
2101
  if (shouldOpen) {
2085
- panel.classList.remove('hidden');
2086
- panel.classList.add('animate-slide-up');
2102
+ // Lazy session init on first open
2103
+ if (!this.ws) {
2104
+ this.initSession();
2105
+ }
2106
+ panel.classList.remove('chat-panel-closed');
2107
+ panel.classList.add('chat-panel-open', 'animate-slide-up');
2087
2108
  this.restorePanelState(panel);
2088
2109
  if (bubble) {
2089
2110
  bubble.classList.add('scale-0');
@@ -2100,8 +2121,8 @@ export class ChatModule {
2100
2121
  messages.scrollTop = messages.scrollHeight;
2101
2122
  }
2102
2123
  } else {
2103
- panel.classList.add('hidden');
2104
- panel.classList.remove('animate-slide-up');
2124
+ panel.classList.add('chat-panel-closed');
2125
+ panel.classList.remove('chat-panel-open', 'animate-slide-up');
2105
2126
  if (bubble) {
2106
2127
  bubble.classList.remove('scale-0');
2107
2128
  }
@@ -2157,7 +2178,7 @@ export class ChatModule {
2157
2178
  */
2158
2179
  isFloatingOpen(): boolean {
2159
2180
  const panel = getElementByIdOrNull<HTMLDivElement>('chat-panel');
2160
- return Boolean(panel && !panel.classList.contains('hidden'));
2181
+ return Boolean(panel && panel.classList.contains('chat-panel-open'));
2161
2182
  }
2162
2183
 
2163
2184
  /**
@@ -177,75 +177,51 @@ export class DashboardModule {
177
177
 
178
178
  /**
179
179
  * Load dashboard status from API
180
- * Uses Promise.allSettled for parallel loading to improve performance
181
180
  */
182
181
  async loadStatus(): Promise<void> {
183
182
  try {
184
- // Load all data in parallel for better performance
185
- const [
186
- dashboardResult,
187
- multiAgentResult,
188
- delegationsResult,
189
- cronResult,
190
- tokenResult,
191
- mcpResult,
192
- ] = await Promise.allSettled([
193
- API.get<DashboardData>('/api/dashboard/status'),
194
- API.get<MultiAgentDashboardStatus>('/api/multi-agent/status'),
195
- API.get<DashboardDelegationsData>('/api/multi-agent/delegations?limit=10'),
196
- API.getCronJobs(),
197
- Promise.all([API.getTokenSummary(), API.getTokensByAgent()]),
198
- API.get<McpServersResponse>('/api/mcp-servers'),
199
- ]);
200
-
201
- // Process dashboard status (required)
202
- if (dashboardResult.status === 'fulfilled') {
203
- this.data = dashboardResult.value;
204
- } else {
205
- throw dashboardResult.reason;
206
- }
183
+ this.data = await API.get<DashboardData>('/api/dashboard/status');
207
184
 
208
- // Process multi-agent status
209
- if (multiAgentResult.status === 'fulfilled') {
210
- this.multiAgentData = multiAgentResult.value;
211
- } else {
212
- logger.warn('[Dashboard] Multi-agent status unavailable:', multiAgentResult.reason);
185
+ // Load multi-agent status (Sprint 3 F2)
186
+ try {
187
+ this.multiAgentData = await API.get<MultiAgentDashboardStatus>('/api/multi-agent/status');
188
+ } catch (e) {
189
+ logger.warn('[Dashboard] Multi-agent status unavailable:', e);
213
190
  this.multiAgentData = { enabled: false, agents: [] };
214
191
  }
215
192
 
216
- // Process delegations
217
- if (delegationsResult.status === 'fulfilled') {
218
- this.delegationsData = delegationsResult.value;
219
- } else {
220
- logger.warn('[Dashboard] Delegations unavailable:', delegationsResult.reason);
193
+ // Load delegations (F4 endpoint)
194
+ try {
195
+ this.delegationsData = await API.get<DashboardDelegationsData>(
196
+ '/api/multi-agent/delegations?limit=10'
197
+ );
198
+ } catch (e) {
199
+ logger.warn('[Dashboard] Delegations unavailable:', e);
221
200
  this.delegationsData = { delegations: [], count: 0 };
222
201
  }
223
202
 
224
- // Process cron jobs
225
- if (cronResult.status === 'fulfilled') {
226
- this.cronData = cronResult.value;
227
- } else {
228
- logger.warn('[Dashboard] Cron data unavailable:', cronResult.reason);
203
+ // Load cron jobs
204
+ try {
205
+ this.cronData = await API.getCronJobs();
206
+ } catch (e) {
207
+ logger.warn('[Dashboard] Cron data unavailable:', e);
229
208
  this.cronData = null;
230
209
  }
231
210
 
232
- // Process token data
233
- if (tokenResult.status === 'fulfilled') {
234
- const [summary, byAgent] = tokenResult.value;
211
+ // Load token summary
212
+ try {
213
+ const [summary, byAgent] = await Promise.all([
214
+ API.getTokenSummary(),
215
+ API.getTokensByAgent(),
216
+ ]);
235
217
  this.tokenData = { summary, byAgent };
236
- } else {
237
- logger.warn('[Dashboard] Token data unavailable:', tokenResult.reason);
218
+ } catch (e) {
219
+ logger.warn('[Dashboard] Token data unavailable:', e);
238
220
  this.tokenData = null;
239
221
  }
240
222
 
241
- // Process MCP servers
242
- if (mcpResult.status === 'fulfilled' && mcpResult.value && 'servers' in mcpResult.value) {
243
- this.mcpServers = mcpResult.value.servers || [];
244
- this.renderMCPServers();
245
- } else {
246
- logger.warn('[Dashboard] MCP servers unavailable');
247
- this.mcpServers = [];
248
- }
223
+ // Load MCP servers
224
+ await this.loadMCPServers();
249
225
 
250
226
  this.render();
251
227
  this.setStatus(`Last updated: ${new Date().toLocaleTimeString()}`);
@@ -255,6 +231,21 @@ export class DashboardModule {
255
231
  }
256
232
  }
257
233
 
234
+ /**
235
+ * Load MCP servers from API
236
+ */
237
+ async loadMCPServers(): Promise<void> {
238
+ try {
239
+ const data = await API.get<McpServersResponse>('/api/mcp-servers');
240
+ if (data && 'servers' in data) {
241
+ this.mcpServers = data.servers || [];
242
+ this.renderMCPServers();
243
+ }
244
+ } catch (error) {
245
+ logger.error('Failed to load MCP servers:', error);
246
+ }
247
+ }
248
+
258
249
  /**
259
250
  * Render all dashboard sections
260
251
  */
@@ -111,15 +111,11 @@ export function showToast(message: string, duration = 3000): void {
111
111
  * @param {HTMLElement} container - Container to scroll
112
112
  */
113
113
  export function scrollToBottom(container: HTMLElement): void {
114
- // Use setTimeout to ensure DOM has updated before scrolling
115
- const doScroll = () => {
116
- container.scrollTop = container.scrollHeight;
117
- if (container.scrollTo) {
118
- container.scrollTo({ top: container.scrollHeight, behavior: 'auto' });
119
- }
120
- };
121
- setTimeout(doScroll, 50);
122
- requestAnimationFrame(doScroll);
114
+ // Use requestAnimationFrame to batch layout read/write and avoid forced reflow
115
+ requestAnimationFrame(() => {
116
+ const scrollHeight = container.scrollHeight; // Single read
117
+ container.scrollTop = scrollHeight; // Single write
118
+ });
123
119
  }
124
120
 
125
121
  /**
@@ -128,11 +124,15 @@ export function scrollToBottom(container: HTMLElement): void {
128
124
  * @param {number} maxRows - Maximum number of rows (default: 5)
129
125
  */
130
126
  export function autoResizeTextarea(textarea: HTMLTextAreaElement, maxRows = 5): void {
131
- textarea.style.height = 'auto';
132
- const computedLineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight);
133
- const lineHeight =
134
- Number.isFinite(computedLineHeight) && computedLineHeight > 0 ? computedLineHeight : 20;
135
- const maxHeight = lineHeight * maxRows;
136
- const newHeight = Math.min(textarea.scrollHeight, maxHeight);
137
- textarea.style.height = newHeight + 'px';
127
+ // Use requestAnimationFrame to defer resize to the next frame, avoiding layout thrash from rapid input events
128
+ requestAnimationFrame(() => {
129
+ textarea.style.height = 'auto'; // Reset height before measuring
130
+ const computedLineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight); // Forces reflow (unavoidable)
131
+ const scrollHeight = textarea.scrollHeight;
132
+ const lineHeight =
133
+ Number.isFinite(computedLineHeight) && computedLineHeight > 0 ? computedLineHeight : 20;
134
+ const maxHeight = lineHeight * maxRows;
135
+ const newHeight = Math.min(scrollHeight, maxHeight);
136
+ textarea.style.height = newHeight + 'px';
137
+ });
138
138
  }
@@ -4,6 +4,25 @@
4
4
  Typography: Fredoka (display) + Nunito (body)
5
5
  ============================================================================ */
6
6
 
7
+ /* ============================================================================
8
+ CRITICAL CSS - Prevent CLS before Tailwind loads
9
+ ============================================================================ */
10
+
11
+ header {
12
+ min-height: 56px;
13
+ display: flex;
14
+ align-items: center;
15
+ }
16
+
17
+ main {
18
+ min-height: calc(100vh - 56px);
19
+ }
20
+
21
+ [data-tab] {
22
+ min-width: 80px;
23
+ min-height: 36px;
24
+ }
25
+
7
26
  /* ============================================================================
8
27
  ANIMATIONS
9
28
  ============================================================================ */
@@ -68,6 +87,19 @@
68
87
  min-height: 320px;
69
88
  max-width: 96vw;
70
89
  max-height: 85vh;
90
+ /* GPU compositing for smooth toggle */
91
+ transform: translateZ(0);
92
+ will-change: opacity;
93
+ }
94
+
95
+ #chat-panel.chat-panel-closed {
96
+ opacity: 0;
97
+ pointer-events: none;
98
+ }
99
+
100
+ #chat-panel.chat-panel-open {
101
+ opacity: 1;
102
+ pointer-events: auto;
71
103
  }
72
104
  .chat-panel-draggable {
73
105
  position: fixed !important;
@@ -461,6 +493,7 @@
461
493
  /* Tab visibility */
462
494
  .tab-content {
463
495
  display: none;
496
+ content-visibility: hidden;
464
497
  }
465
498
  .tab-content.active {
466
499
  display: flex;
@@ -468,6 +501,12 @@
468
501
  flex: 1;
469
502
  height: 100%;
470
503
  min-height: 0;
504
+ content-visibility: visible;
505
+ }
506
+ /* Memory tab needs visible content-visibility for checkpoint details elements */
507
+ #tab-memory,
508
+ #tab-memory.active {
509
+ content-visibility: visible;
471
510
  }
472
511
 
473
512
  /* Modal visibility */
@@ -634,8 +673,12 @@
634
673
  background: #FFCE00;
635
674
  }
636
675
 
637
- /* Firefox scrollbar */
638
- * {
676
+ /* Firefox scrollbar - scoped to scrollable containers */
677
+ body,
678
+ .tab-content,
679
+ #chat-messages,
680
+ .overflow-y-auto,
681
+ .overflow-auto {
639
682
  scrollbar-width: thin;
640
683
  scrollbar-color: #D4C4E0 transparent;
641
684
  }